Skip to content Skip to sidebar Skip to footer

Keep Object Chainable Using Async Methods

Let's say I have a class Test with around 10-20 methods, all of which are chainable. In another method, I have some asynchronous work to do. let test = new Test(); console.log(test

Solution 1:

I doupt that it is a really good idea to do something like that. But using a Proxy would allow to create such a beahviour if the original Object meets certain conditions. And I would highly recommend to not do it that way.

Be aware that this code is a proof of concept to show that it is somehow possible, but doesn't care about edge cases and most likely will break certains functionalities.

One Proxy is used to wrap the original class Test so that is is possible to patch each of is instance to make them chainable.

The second one will patch each function call and creates a queue, for these functions calls so that they are called in order.

classTest {
      /**
       * Executes some async code
       * @returns {Test} The current {@link Test}
       */asynch() {
        console.log('asynch')
        returnnewPromise((resolve, reject) =>setTimeout(resolve, 1000))
      }

      /**
       * Executes some code
       * @returns {Test} The current {@link Test}
       */something() {
        console.log('something')

        returnthis
      }
    }


    varTestChainable = newProxy(Test, {
      construct(target, args) {
        returnnewProxy(newtarget(...args), {

          // a promise used for chainingpendingPromise: Promise.resolve(),

          get(target, key, receiver) {
            //  intercept each get on the objectif (key === 'then' || key === 'catch') {
              // if then/catch is requested, return the chaining promisereturn(...args2) => {
                returnthis.pendingPromise[key](...args2)
              }
            } elseif (target[key] instanceofFunction) {
              // otherwise chain with the "chainingPromise" // and call the original function as soon// as the previous call finished return(...args2) => {
                this.pendingPromise = this.pendingPromise.then(() => {
                  target[key](...args2)
                })

                console.log('calling ', key)

                // return the proxy so that chaining can continuereturn receiver
              }
            } else {
              // if it is not a function then just return itreturn target[key]
            }
          }
        })
      }
    });

    var t = newTestChainable
    t.asynch()
      .something()
      .asynch()
      .asynch()
      .then(() => {
        console.log('all calles are finished')
      })

Solution 2:

I don't think that is it possible to use such syntax for now. It would require to access the promise in the in the function to return it.

Different ways to chain functions:

Promise with then

bob.bar()
    .then(() => bob.baz())
    .then(() => bob.anotherBaz())
    .then(() => bob.somethingElse());

And you could also use compositions, to obtain another style of functional, reusable syntax to chain async and sync functions

constapplyAsync = (acc,val) => acc.then(val);
constcomposeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

Or using async / await

for (const f of [func1, func2]) {
  await f();
}

Solution 3:

As discussed in comments to OP, this can be accomplished by using Proxy.

I recognize that t.niese provided a similar answer a few hours ago. My approach differs somewhat, but it's still substantively trapping method calls, returning the receiver and internally stacking up thennables.

classProxyBase {

    constructor () {

        // Initialize a base thennable.this.promiseChain = Promise.resolve();

    }

    /**
     * Creates a new instance and returns an object proxying it.
     * 
     * @return {Proxy<ProxyBase>}
     */static create () {

        returnnewProxy(newthis(), {

            // Trap all property access.get: (target, propertyName, receiver) => {

                const value = target[propertyName];

                // If the requested property is a method and not a reserved method...if (typeof value === 'function' && !['then'].includes(propertyName)) {

                    // Return a new function wrapping the method call.returnfunction (...args) {

                        target.promiseChain = target.promiseChain.then(() => value.apply(target, args));

                        // Return the proxy for chaining.return receiver;

                    }

                } elseif (propertyName === 'then') {
                    return(...args) => target.promiseChain.then(...args);
                }

                // If the requested property is not a method, simply attempt to return its value.return value;

            }

        });

    }

}

// Sample implementation class. Nonsense lies ahead.classTestextendsProxyBase {

    constructor () {
        super();
        this.chainValue = 0;
    }

    foo () {
        returnnewPromise(resolve => {
            setTimeout(() => {
                this.chainValue += 3;
                resolve();
            }, 500);
        });
    }

    bar () {
        this.chainValue += 5;
        returntrue;
    }

    baz () {
        returnnewPromise(resolve => {
            setTimeout(() => {
                this.chainValue += 7;
                resolve();
            }, 100);
        });
    }

}

const test = Test.create();

test.foo().bar().baz().then(() =>console.log(test.chainValue)); // 15

Post a Comment for "Keep Object Chainable Using Async Methods"