Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.something()); // Test
console.log(test.asynch()); // undefined since the async code isn't done yet
console.log(test.asynch().something()); // ERROR > My goal is to make this 

Since every other method is chainable, I feel like it would be weird for the user if this sole method isn't.

Is there a way for me to maintain the chainable theme of my Class?


I have already thought of passing the next method in a callback function inside this method's parameter, but it's not really chaining.

test.asynch(() => something())

Same thing with Promises, it's not really chaining.

test.asynch().then(() => something())

The result I want is

test.asynch().something()

Here is a snippet that demonstrates my issue :

class Test {
  /**
   * Executes some async code
   * @returns {Test} The current {@link Test}
   */
  asynch() {
    if (true) { //Condition isn't important
      setTimeout(() => { //Some async stuff
        return this;
      }, 500);
    } else {
      // ...
      return this;
    }
  }

  /**
   * Executes some code
   * @returns {Test} The current {@link Test}
   */
  something() {
    // ...
    return this
  }
}

let test = new Test();
console.log(test.something()); // Test
console.log(test.asynch()); // undefined
console.log(test.asynch().something()); // ERROR > My goal is to make this work.
like image 573
Zenoo Avatar asked Aug 01 '18 14:08

Zenoo


People also ask

What is chaining and chainable in C++?

Chaining and Chainable is a design methodology used to design object behaviors so that calls to object functions return references to self, or another object, providing access to additional function calls allowing the calling statement to chain together many calls without the need to reference the variable holding the object/s.

What are the parameters of an async method?

It has the same parameters as the method that you want to execute asynchronously, plus two additional optional parameters. The first parameter is an AsyncCallback delegate that references a method to be called when the asynchronous call completes. The second parameter is a user-defined object that passes information into the callback method.

What are the best practices in asynchronous programming?

Async/Await - Best Practices in Asynchronous Programming Name Description Exceptions Avoid async void Prefer async Task methods over async voi ... Event handlers Async all the way Don’t mix blocking and async code Console main method Configure context Use ConfigureAwait (false) when you can Methods that require con­text

Why should I avoid chaining objects in my API?

It only takes one time for your chainable object to not return the correct reference (easy to forget to add return this) and the person using your API will lose trust and avoid chaining. Chainable objects should be all or nothing (not a chainable object even if parts are). An object should not be called chainable if only some of its functions are.


3 Answers

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.

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

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

        return this
      }
    }


    var TestChainable = new Proxy(Test, {
      construct(target, args) {
        return new Proxy(new target(...args), {

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

          get(target, key, receiver) {
            //  intercept each get on the object
            if (key === 'then' || key === 'catch') {
              // if then/catch is requested, return the chaining promise
              return (...args2) => {
                return this.pendingPromise[key](...args2)
              }
            } else if (target[key] instanceof Function) {
              // 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 continue
                return receiver
              }
            } else {
              // if it is not a function then just return it
              return target[key]
            }
          }
        })
      }
    });

    var t = new TestChainable
    t.asynch()
      .something()
      .asynch()
      .asynch()
      .then(() => {
        console.log('all calles are finished')
      })
like image 182
t.niese Avatar answered Oct 14 '22 04:10

t.niese


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

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...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();
}
like image 41
NanoPish Avatar answered Oct 14 '22 06:10

NanoPish


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.

class ProxyBase {

    constructor () {

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

    }

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

        return new Proxy(new this(), {

            // 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.
                    return function (...args) {

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

                        // Return the proxy for chaining.
                        return receiver;

                    }

                } else if (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.
class Test extends ProxyBase {

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

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

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

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

}

const test = Test.create();

test.foo().bar().baz().then(() => console.log(test.chainValue)); // 15
like image 2
Fissure King Avatar answered Oct 14 '22 06:10

Fissure King