Search code examples
javascriptpromisesynchronizationes6-promise

How to implement a fluent interface with Promises?


I want to make this code work

$('start') // push initial value to an array
.addItem(2) // add another value
.printAll() // print everything in array
.delay(2000) // wait x seconds
.addItem(5)
.printAll()
.start(); // execute all the commands before this line

I created a class with data array to hold the items. and steps array to hold the operations. Then i used chainable promises to execute them. is that the best way to do what i'm trying to achieve? Do I really need to store the operations in an array?

 class Sync {

        constructor() {}

        init(startValue) {
            this.data = [startValue];
            this.steps = [];
            return this;
        }

        addItem(value) {
            const append = (v)=>this.data.push(v);
            this._addStep(append, value);
            return this;
        }

        printAll() {
            this._addStep(v=>console.log(v.join()), this.data);
            return this;
        }

        delay(value) {
            this._addStep(window.setTimeout, value);
            return this;
        }

        _addStep(fun, ...args) {
            this.steps.push({
                fun,
                args
            });
        }

        start() {
            let start = Promise.resolve();
            this.steps.forEach(({fun, args})=>{
                start = start.then(()=>{
                    return new Promise(resolve=>{
                        if (fun === window.setTimeout) {
                            setTimeout(resolve, ...args);
                        } else {
                            fun.apply(this, args);
                            resolve();
                        }

                    }
                    );
                }
                );
            }
            );

            return this;
        }
    }
    const lib = new Sync();
    const $ = lib.init.bind(lib);
    $('start')
    .addItem(2)
    .printAll()
    .delay(2000)
    .addItem(5)
    .printAll()
    .start();

Solution

  • Although your question belongs to https://codereview.stackexchange.com/ according to me, I tried to think of another implementation without Promises. It works with closures and callbacks only:

    class Sync {
    
        constructor() {}
    
        init(startValue) {
            this.data = [startValue];
            this.steps = [];
            return this;
        }
    
        addItem(value) {
            const append = v => this.data.push(v);
            this._addStep(append, value);
            return this;
        }
    
        printAll() {
            this._addStep(v => console.log(v.join()), this.data);
            return this;
        }
    
        delay(value) {
            this._addStep(window.setTimeout, value);
            return this;
        }
    
        _addStep(fun, ...args) {
            this.steps.push({
                fun,
                args
            });
        }
    
        start() {
            let finalFunction;
            this.steps.reverse().forEach(({ fun, args }) => {
                if (fun === window.setTimeout) {
                    finalFunction = finalFunction ? encloseFunctionWithArgs(null, null, finalFunction, next => setTimeout(next, ...args)) : null;
                } else {
                    finalFunction = encloseFunctionWithArgs(fun, args, finalFunction);
                }
    
            });
            finalFunction();
            return this;
        }
    }
    
    function encloseFunctionWithArgs(fun, args, next, trigger) {
        return function () {
            if (fun)
                fun(args);
            if (next)
                trigger ? trigger(next) : next();
        }
    }
    
    const lib = new Sync();
    const $ = lib.init.bind(lib);
    $('start')
    .addItem(2)
    .printAll()
    .delay(2000)
    .addItem(5)
    .printAll()
    .start();

    EDIT

    I finally managed to achieve what you want:

    const base = {
      init(startValue) {
        this.promise = new Promise(resolve => this.start = resolve).then(() => [startValue]);
        return this;
      },
    
      addItem(value) {
        this.promise = this.promise.then(v => [...v, value]);
        return this;
      },
    
      printAll() {
        this.promise.then(v => v.join()).then(console.log);
        return this;
      },
    
      delay(value) {
        this.promise = this.promise.then(v => new Promise(resolve => setTimeout(() => resolve(v), value)));
        return this;
      }
    }
    
    const factory = base => arg => Object.create(base).init(arg);
    
    const $ = factory(base);
    
    $('start')
    .addItem(2)
    .printAll()
    .delay(2000)
    .addItem(5)
    .printAll()
    .start();