Skip to content
Advertisement

how to chain async methods

The API I’ve written has several async methods that do not return a value, but still should be executed in the order they are called. I’d like to abstract the awaiting resolution from the end user so that they can chain method calls and expect that each promise is executed after the previous is resolved like so:

api = new Api();
api.doAsync().doAnotherAsync().doAThirdAsync();

It is not important that we get a value from these methods, just that they are executed in order. I have tried using a linked structure, but it hasn’t been reliable.

class Api {
    resolvingMethodChain = false;
    constructor() {
        this._methodChain = {
            next: null,
            promise: Promise.resolve(),
        }
    }

    _chain(p) {
        this._methodChain.next = {
            promise: p,
            next: null,
        };

        // if we are not finished resolving the method chain, just append to the current chain
        if (!this.resolvingMethodChain) this._resolveMethodChain(this._methodChain);

        this._methodChain = this._methodChain.next;
        return this
    }

    async _resolveMethodChain(chain) {
        if (!this.resolvingPromiseChain) {
            this.resolvingPromiseChain = true;
        }

        // base case
        if (chain === null) {
            this.resolvingPromiseChain = false;
            return;
        }

        // resolve the promise in the current chain
        await chain.promise;

        // resolve the next promise in the chain
        this._resolvePromiseChain(c.next);   
    }
}

The doAsync methods would all defer to _chain like so

doAsync() {
    const p = new Promise(// do some async stuff);
    return _chain(p); // returns this and adds the promise to the methodChain
}

I know I could write it like this

async doAsync() {
    // do async thing
    return this;
}

And use it like this

doAsync.then(api => api).then(...)

But explicitly returning the this object from each then call is what I want to avoid if I can, it just doesn’t seem as clean as the synchronous way of api.doAsync().doAnotherAsync()...

Advertisement

Answer

You can start with a simple wrapper around a Promise

const effect = f => x =>
  (f (x), x)
  
const Api = (p = Promise.resolve ()) =>
  ({ foo: () => 
       Api (p.then (effect (x => console.log ('foo', x))))
     
   , bar: (arg) =>
       Api (p.then (effect (x => console.log ('bar', arg))))
     
  })
  
Api().foo().foo().bar(5)
// foo undefined
// foo undefined
// bar 5

We can add other functions that do more useful things. Note because we’re using Promises, we can sequence synchronous or asynchronous functions with ease

const effect = f => x =>
  (f (x), x)
  
const square = x =>
  x * x
  
const Api = (p = Promise.resolve ()) =>
  ({ log: () =>
       Api (p.then (effect (console.log)))
       
   , foo: () => 
       Api (p.then (effect (x => console.log ('foo', x))))
     
   , bar: (arg) =>
       Api (p.then (effect (x => console.log ('bar', arg))))
  
   , then: f =>
       Api (p.then (f))
  })

  
Api().log().then(() => 5).log().then(square).log()
// undefined
// 5
// 25

Add whatever functions you want now. This example shows functions that actually do something more realistic

const effect = f => x =>
  (f (x), x)
  
const DB =
  { 10: { id: 10, name: 'Alice' }
  , 20: { id: 20, name: 'Bob' }
  }
  
const Database =
  { getUser: id =>
      new Promise (r =>
        setTimeout (r, 250, DB[id]))
  }
  
const Api = (p = Promise.resolve ()) =>
  ({ log: () =>
       Api (p.then (effect (console.log)))
       
   , getUser: (id) =>
       Api (p.then (() => Database.getUser (id)))
       
   , displayName: () =>
       Api (p.then (effect (user => console.log (user.name))))
  
  })

  
Api().getUser(10).log().displayName().log()
// { id: 10, name: 'Alice' }
// Alice
// { id: 10, name: 'Alice' }

Api().getUser(10).log().getUser(20).log().displayName()
// { id: 10, name: 'Alice' }
// { id: 20, name: 'Bob' }
// Bob
User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement