25

I was asked to create such an object called foo that can chain the function log and wait.

For example:

foo.log('breakfast').wait(3000).log('lunch').wait(3000).log('dinner');

In this scenario it prints breakfast first, waits 3 seconds, prints lunch, and then after 3 seconds it prints dinner.

I tried something like this, but it doesn't work. What did I miss?

var foo = {
  log: function(text){
    console.log(text);
    return foo;
  },

  wait: function(time) {
    setTimeout(function() {
      return foo;
    }, time);
  }
}

foo.log('breakfast').wait(3000).log('lunch').wait(3000).log('dinner');
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
hiroshi
  • 461
  • 3
  • 8
  • 1
    The problem is in wait function I guess - setTimeout returns immediately and it returns nothing, so calling log on nothing gives an error. – kiner_shah Dec 31 '21 at 10:10
  • @kiner_shah what's the correct way to achieve it? I have no idear how can I make it wait and then return the object again. – hiroshi Dec 31 '21 at 10:13
  • 2
    Maybe make wait a async function and then define sleep function as in this post: https://stackoverflow.com/a/39914235 – kiner_shah Dec 31 '21 at 10:15
  • it looks a bit like this question: https://stackoverflow.com/q/32081949/1447675 – Nina Scholz Jan 04 '22 at 17:46

6 Answers6

36

It's always better to use promises. An implementation of this functionality could be;

class Foo {
  constructor(){
    this.promise = Promise.resolve();
  }
  log(txt){
    this.promise = this.promise.then(_ => console.log(txt))
    return this;
  }
  wait(ms){
    this.promise = this.promise.then(_ => new Promise(v => setTimeout(v,ms)));
    return this;
  }
}
  
  var foo = new Foo();
  foo.log("happy").wait(1000).log("new").wait(1000).log("year");
Redu
  • 25,060
  • 6
  • 56
  • 76
  • 10
    Woah what socery is this? Thanks so much and I have to check the Promise and take it in for a moment! Salute and happy new year to you sir! – hiroshi Dec 31 '21 at 10:28
  • 2
    You might want to make the object thenable, so that one can `await foo`. Also consider making it immutable, so that you can reuse an object without conflicts between two strands of execution. – Bergi Dec 31 '21 at 18:27
  • @Bergi Thank you. Actually i tried to simplify a continuity pattern (having a resolved promise at hand in advance) within the context of the question. Perhaps it would be an overkill to present any further here. However i would still prefer to have the `promise` property as a private instance field like `this.#promise` rather freezing the object. As per thenability, the approach i have here is in fact forms the building blocks of my Asynchronous Queue ([AQ](https://github.com/kedicesur/AQ)) library which i believe does such things. – Redu Dec 31 '21 at 18:53
13

For the record, Redu's excellent answer without the class sugar.

See also

const foo = {
  promise: Promise.resolve(),
  log(txt) {
    this.promise.then(_ => console.log(txt));
    return this;
  },
  wait(ms) {
    this.promise = this.promise.then(_ => new Promise(v => setTimeout(v, ms)));
    return this;
  }
};

// OR
const Foo = (defaultMs = 1000) => {
  let promised = Promise.resolve();
  return {
    log(txt) {
      promised.then(_ => console.log(txt));
      return this;
    },
    wait: function(ms) {
      promised = promised.then( _=> 
        new Promise( rs => setTimeout(rs, ms || defaultMs) ) );
      return this;
    }
  };
};

foo.log("Happy").wait(1000).log("new").wait(1000).log("year");
Foo().wait(3000)
  .log(`** From Foo ;)`).log(`Happy`).wait().log("new").wait().log("year");
KooiInc
  • 119,216
  • 31
  • 141
  • 177
1

Place the call to wait inside the previous one, and as the last item, like a recursive function.

meals=['breakfast','elevenses','lunch','afternoon tea','dinner','supper'];
c=0;
wait=t=>{setTimeout(function() {
      if (c<meals.length) document.write(meals[c++],'<br>');wait(500);
    }, t);}

wait(500);
JMP
  • 4,417
  • 17
  • 30
  • 41
1

I was inspired by @James 's solution, which is partially wrong because the log messages are in the reverse order, but he does not use Promises. I still think that @Redu 's solution should be the accepted one (after all if you can use Promises, that is perfect), but this one is interesting too in my opinion:

const foo1 = {
    incrementalTimeout: 0,
    nextActions: [],
    log(text) {
        const textLog = () => { console.log(text); };
        if (this.incrementalTimeout == 0)
            textLog();
        else
            this.nextActions.push(textLog);
        return this;
    },
    wait(time) {
        let newObj = {...this, incrementalTimeout: this.incrementalTimeout + time, nextActions: []};
        setTimeout(() => { newObj.nextActions.forEach((action) => action()); } , newObj.incrementalTimeout);
        return newObj;
    }
}
foo1.log('breakfast').wait(1000).log('lunch').wait(3000).log('dinner');

The idea is that I do not immediately log text but I push a lambda with the console.log in an array that is going to be called after the correct timeout expires.

I run all the log and wait operations one after the other, but I keep track of the seconds to wait before executing the actions. Every time a new wait is called, the incrementalTimeout is increased by time. To keep the nextActions belonging to different time frames separated, I return a newObj every time, more or less like @James does.

Marco Luzzara
  • 5,540
  • 3
  • 16
  • 42
0

Shorter, within Promise (not recommended).

Promise.prototype.log = function(txt) {
    return this.then(() => console.log(txt))
}

Promise.prototype.wait = function(ms) {
    return this.then(() => new Promise(res => setTimeout(res, ms)))
}

var foo = Promise.resolve()
foo.log('breakfast').wait(3000).log('lunch').wait(3000).log('dinner')
lledr
  • 537
  • 1
  • 3
  • 12
0

You can do it without promises:

const foo = {
    log(text) {
        return {...foo, start: () => {
            this.start();
            console.log(text);
        }};
    },
    wait(time) {
        return {...foo, start: () => {
            setTimeout(this.start, time);
        }};
    },
    start() {}
};
foo.log('breakfast').wait(3000).log('lunch').wait(3000).log('dinner').start();
The functions foo.log() and foo.wait() return immediately, returning a modified clone of foo. A clone is made using {...foo}, but with the start() function modified so that it calls the caller's this.start() followed by the new operation. When the chain is complete you call start() to start the actions.
James
  • 5,635
  • 2
  • 33
  • 44
  • 2
    This implicit linked list you're building is interesting - unfortunately `wait` runs it in the wrong order – Bergi Jan 30 '22 at 12:51
  • @Bergi I agree, that is an interesting idea, I have recycled it in my answer if you want to take a look. – Marco Luzzara Mar 14 '22 at 19:02