5

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.
Zenoo
  • 12,670
  • 4
  • 45
  • 69
  • 2
    What about using promises, like there ? : https://stackoverflow.com/questions/39028882/chaining-async-method-calls-javascript – NanoPish Aug 01 '18 at 14:10
  • 1
    @NanoPish It still breaks the chaining, `test.asynch().something()` would become `test.asynch().then(() => something())` – Zenoo Aug 01 '18 at 14:12
  • 1
    reading the answer to the question I linked, he links 3 functions using promises, it seems to resolve your issue ? – NanoPish Aug 01 '18 at 14:13
  • @NanoPish It's using multiple `.then()`, of course. I would prefer if it was a straight up method chain. – Zenoo Aug 01 '18 at 14:15
  • I see. What about Composition ? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#Composition It will be effectively the same thing, but with a nicer syntax as you want ? – NanoPish Aug 01 '18 at 14:18
  • @NanoPish I don't see how composing Promises would let me achieve the chaining syntax. Can you elaborate please? – Zenoo Aug 01 '18 at 14:25
  • well it wont achieve the syntax test.asynch().something(), but it will achieve a cleaner one, without multiple then. The example in the doc are pretty close to a synchronous syntax, with a functional style – NanoPish Aug 01 '18 at 14:26
  • 2
    @Zenoo, unfortunately, what you're describing isn't possible. At least, as far as I know. At best it would produce confusing code to anybody else reading it, however, as that is pretty far from idiomatic. You're familiar with using `async`/`await`, I assume? That's about as close as we can get to treating asynchronous code as synchronous. – Fissure King Aug 01 '18 at 14:27
  • @FissureKing Yes, but I was hoping to be able to keep this simple chaining syntax. I'll keep this question open, maybe someone will come close to a solution. – Zenoo Aug 01 '18 at 14:29
  • 2
    @Zenoo hmm... well now I think I've caught a case of your curiosity. I wondered if `await` would extend to chained executions, but it seems it does not: https://jsfiddle.net/7v5y9jck/. You might be able to create a class that offloads every method invocation into a queue and return the instance itself. Actually, you could probably use `Proxy` for that, and then include a `then` method that ultimately resolves as your final result. – Fissure King Aug 01 '18 at 14:40
  • @FissureKing Interesting approach ! I will try it on my own, but if you manage to make it work, don't be shy and post it as an answer, this will be a bounty-worth answer ! – Zenoo Aug 01 '18 at 14:43
  • Are all functions that the object has chain-able? Or are at least all of them async? Does `something` have to wait until `async` is finished or are they allowed to interleave. – t.niese Aug 01 '18 at 14:48
  • @t.niese Every function from the class is indeed chainable. None of them are async, they just don't use asynchronous code. And yes, ideally, `something` would need to wait until `asynch` is done. Otherwise I would just add a `return this` at the end of every method. – Zenoo Aug 01 '18 at 14:51
  • 1
    You can always return something that *contains* a promise (and maybe even is thenable itself) and provides methods that proxy themselves to be executed when the promise is fulfilled. But it doesn't seem so wrong to break your style if you are doing something different (and asynchronous things *are* different enough), you still can use `(await test.asynch()).something()` if you insist on chaining. – Bergi Aug 01 '18 at 15:11
  • @Zenoo, turned out I did have a little bit of spare time today. Stand by for a working prototype. – Fissure King Aug 01 '18 at 18:58

3 Answers3

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

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();
}
NanoPish
  • 1,379
  • 1
  • 19
  • 35
2

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')
      })
t.niese
  • 39,256
  • 9
  • 74
  • 101
  • Why do you advise not to do it? Do you have some examples for what could break using this code? Finally, do you think it would have a bad impact performance wise? – Zenoo Aug 01 '18 at 22:48
  • @Zenoo It was/is mainly a vague premonition due to many projects and ideas, where problems occuered at a really late stage. One problem that I see right now is that each newly started chaining will use the last Promise of the previous chain. So if the previous chain failed and the error was not catched then the newly started chain will fail immediatly. To overcome this you need to create an own Proxy for each started chain, but then you need to need to make sure that those chains don't interleave. – t.niese Aug 02 '18 at 05:42
  • @Zenoo and I'm sure there will be more problems that will become visible later. You will at least need to speed a long time to test with many edge cases. – t.niese Aug 02 '18 at 05:44
2

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
Fissure King
  • 1,250
  • 7
  • 17
  • Thank you, I don't know if I'll end up using it, but at least you proved it was possible. I will accept @t.neise answer though, since it was the first one posted and it handles the `catch` too. – Zenoo Aug 02 '18 at 12:46
  • That would have been my suggestion as well. Cheers. – Fissure King Aug 02 '18 at 14:06