5

I'm trying to add a "default callback" to a prototype that will assign a callback function (in the form of a promise) to an async method if none is provided.

The goal is to have a class's chain of async methods run synchronously

Item.async1().async2()....asyncN()

Mind you, the async functions themselves expect a callback but they're not passed as arguments in the function call (which tells me that the class needs a default behavior when the callback lookup fails)

Spec states that I cannot directly modify the behavior or side effects of the prototype methods. I can add prototype methods. We have no visibility into how these prototype methods are implemented.

TLDR: Without modifying the prototype methods, how can you chain an N number of async methods and ensure they run sequentially?

BTW: Promisifying the prototype methods in question would be helpful if I wanted to implement the promisified versions, but it looks like we're constrained to the original function calls

Anthony Chung
  • 1,467
  • 2
  • 22
  • 44
  • 1
    FYI, the usual promisify operations create a new function/method that returns a promise - they don't modify the existing ones. That seems allowed. But, promises don't chain the way you say you want to chain so while promisification might be useful internal to the implementation, it wouldn't directly give you `Item.async1().async2()....asyncN()` anyway. – jfriend00 Aug 19 '16 at 00:54

3 Answers3

9

Well, I wasn't going to answer - but I was challenged.

It's quite easy to utilize the built in abilities of promises in order to get this sort of queuing for free. Here is how this conversion will work:

  • We convert the callback API to promises on a new object with a promise subclass.
  • We add all the promised methods to the subclass itself - so it chains.
    • We tell the subclass to execute all the methods in a then, so they'll queue as then is the queueing mechanism promises have.

Note: The promisify and promisifyAll methods I write here - you should grab off NPM - lots of good and fast usages that take a promise constructor.

First, we need a method that converts a callback API to promises:

// F is a promise subclass
function promisify(fn) { // take a function
    return function(...args) {  // return a new one with promises
      return new F((resolve, reject) => { // that returns a promise
         // that calls the original function and resolves the promise
         fn.call(this, ...args, (err, data) => err ? reject(err) : resolve(data));
      });
    };
  } 

Now, let's promisify the whole object:

  function promisifyAll(obj) {
    const o = {};
    for(const prop in obj) {
      if(!(obj[prop].call)) continue; // only functions
      o[prop] = promisify(obj[prop]).bind(obj);
    }
    return o;
  }

So far, nothing new, lots of NPM libraries do this - now for the magic of promises - let's create a method that executes functions on an original object in a then:

function whenReadyAll(obj) {
    const obj2 = {}; // create a new object
    for(const prop in obj) { // for each original object
       obj2[prop] = function(...args) { 
         // return a function that does the same thing in a `then`
         return this.then(() => obj[prop](...args));
       };
    }
    return obj2;
  }

Now, let's wrap things up

function liquidate(obj) {
  const promised = promisifyAll(obj); // convert the object to a promise API
  class F extends Promise {} // create a promise subclass
  Object.assign(F.prototype, whenReadyAll(promised)); // add the API to it
  return promised; // return it
  // previous code here
}

And that's it, if we want the example to be self contained (again, promise and promisifyAll are provided by a library usually):

function liquidate(obj) {
  const promised = promisifyAll(obj);
  class F extends Promise {}
  Object.assign(F.prototype, whenReadyAll(promised)); // add the API  
  return promised;
  function whenReadyAll(obj) {
    const obj2 = {};
    for(const prop in obj) {
       obj2[prop] = function(...args) { 
         return this.then(() => obj[prop](...args));
       };
    }
    return obj2;
  }
  function promisifyAll(obj) {
    const o = {};
    for(const prop in obj) {
      if(!(obj[prop].call)) continue; // only functions
      o[prop] = promisify(obj[prop]).bind(obj);
    }
    return o;
  }
  function promisify(fn) {
    return function(...args) { 
      return new F((resolve, reject) => {
         fn.call(this, ...args, (err, data) => err ? reject(err) : resolve(data));
      });
    };
  } 
}

Or with a library that does promisify:

function liquidate(obj) { // 14 LoC
  class F extends Promise {} 
  const promised = promisifyAll(obj, F); // F is the promise impl
  Object.assign(F.prototype, whenReadyAll(promised)); // add the API  
  return promised;
  function whenReadyAll(obj) {
    const obj2 = {};
    for(const prop in obj) {
       obj2[prop] = function(...args) { 
         return this.then(() => obj[prop](...args));
       };
    }
    return obj2;
  }
}

And what's an answer without a demo:

var o = {  // object with a delay callback method
  delay(cb) { 
    console.log("delay"); 
    setTimeout(() => cb(null), 1000); 
  }
};
var o2 = liquidate(o); // let's liquidate it
// and we even get `then` for free, so we can verify this works
var p = o2.delay().then(x => console.log("First Delay!")).
                   delay().
                   then(x => console.log("Second Delay!"));

// logs delay, then First Delay! after a second, 
// then delay and then Second Delay! after a second

Copy paste this to your friendly neighborhood console and see for yourself :)


To prove this preserves state on the original object (it's easy to modify it not to if that's a requirement) let's add an i variable and increment it in delay and see that things work:

var o = {  // object with a delay callback method
  delay(cb) { 
    console.log("delay", this.i++); 
    setTimeout(() => cb(null), 1000); 
  },
  i: 0
};
var o2 = liquidate(o); // let's liquidate it
// and we even get `then` for free, so we can verify this works
var p = o2.delay().then(x => console.log("First Delay!")).
                   delay().
                   then(x => console.log("Second Delay!", o.i));
//logs:
// delay 0
// First Delay!
// delay 1
// Second Delay! 2
Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Why makes two copies of the object in `liquidate()`? And, aren't you making some assumptions that the object can even be successfully copied this way? And, since it's the copy that might get mutated by any of the methods called, aren't you assuming that nobody else has a meaningful reference to the original object and that it is never used any more? The thinking here is cool, but I don't think you've properly stated all your assumptions or limitations of the implementation. – jfriend00 Aug 19 '16 at 02:30
  • For example if someone else has a reference to the object passed into `liquidate(obj)` and expects all future method calls involved in this scenario to operate on that original object, does this do that? It doesn't seem so to me. You've made a copy and are now operating only on the copy. – jfriend00 Aug 19 '16 at 02:31
  • The implementation was meant to be readable - not performant - and an external promisify is probably a better idea - that's why the two object clones - both reference the original object. I've made a copy and then I bound all its methods to the original object via a closure - I could have done that better - but it would have complicated the implementation. – Benjamin Gruenbaum Aug 19 '16 at 02:31
  • I'll add an example with state – Benjamin Gruenbaum Aug 19 '16 at 02:35
  • FYI, can Bluebird promises be subclassed in this way? In the back of my head, I'm thinking there were some limitations with that because of performance considerations. Or am I not remembering correctly? – jfriend00 Aug 19 '16 at 02:45
  • @jfriend00 with Bluebird, you'd accomplish this with `var F = Promise.getNewLibraryCopy()` instead of `class F extends Promise {}` and then doing `F.promisifyAll` - it would be less code. I didn't want to add a bluebird answer because OP didn't mention it. – Benjamin Gruenbaum Aug 19 '16 at 02:57
  • I just remembered that you couldn't always subclass a Promise in the standard way which is why I asked. – jfriend00 Aug 19 '16 at 02:59
  • Yeah, and you were right, it would look something like this: https://gist.github.com/benjamingr/a82abf6f5486c5ce88cd70cf892cfcd6 - honestly I didn't really think this answer through too much - you just suggested the challenge and I thought it was an interesting idea - I do utilize the technique of wrapping returns in `then` and making a fluent an interface quite a bit in C# though. – Benjamin Gruenbaum Aug 19 '16 at 03:08
  • How do you "know" whether the `Promise` library in use can or can't be subclassed in the normal way? Do you just have to know which library is which and how it has to work for that specific library? Is there a way to write generic code that successfully subclasses a Promise without knowing anything about the Promise implementation being used? – jfriend00 Aug 19 '16 at 03:25
  • That's a little off topic for this question - but generally no. In this case I'd just check if I was using bluebird or not - I don't really see why I'd use something that is not bluebird or native promises today. – Benjamin Gruenbaum Aug 19 '16 at 03:30
  • Seems germaine to me if you want your code to work regardless of which promise library is in play which is often the case when you're supplying a module for others to use in whatever circumstances they have. Yes, you could force in a known promise library just for your own module, but sometimes that isn't considered appropriate and I thought both the OP and others reading this should understand the constraints or requirements. Anyway, thanks for the info. – jfriend00 Aug 19 '16 at 03:34
  • And again, you're right - and by all means feel free to take the code here and build a rock solid library out of it. I've done the bare minimum in order to write code that generates a fluid interface on top of a callback API with the main concept being the `whenReadyAll` function and adding the functions to the (scoped) Promise prototype. I would not use the code I wrote here in production without testing it extensively first. – Benjamin Gruenbaum Aug 19 '16 at 03:37
  • That means that ... `.then(...)` chains use the `.constructor` property to build up the Promise and not `new Promise` ? thats interesting ... – Jonas Wilms Nov 12 '18 at 21:37
  • Look up Symbol.species, and yes :) – Benjamin Gruenbaum Nov 13 '18 at 11:23
2

If .async1() and .async2() are already provided and they require a callback and you aren't allowed to modify them, then you can't achieve Item.async1().async2()....asyncN(). The methods you're calling just aren't built to work that way and if you're not allowed to change them, there's not much you can do other than replace those methods with methods that do work the way you want.

If you can create new methods with their own names that internally use the original methods, then that can be done. One model for how to do that is jQuery animations. They allow you do things like this:

$("#progress").slideDown(300).delay(1000).slideUp(300);

And each of those async operations will be chained together. jQuery accomplishes that by doing the following:

  1. Each method returns the original object so chaining of any method on the object will work.
  2. If an async operation is already running, then each new async method that gets called goes into a queue (on the object) along with the arguments for that method.
  3. The underlying implementation of each method can use an async operation with a traditional callback (or promise). When the operation is done, it then checks the queue to see if there are more operations to be run and if so, starts the next operation form the queue and removes it from the queue.
  4. If new async operations are called while another is running, they again just get added to the end of the queue. Each object (or whatever group of items that you want to be serialized) has its own queue.

So, if the original async methods that expect callbacks were .async1() and .async2(), you could create .async1Chain() and .async2Chain() such that you could make it work like this:

Item.async1Chain().async2Chain()....asyncNChain()

Where internally, .async1Chain() would call .async1() with a local callback and that callback would be configured to check the queue to run the next queued operation if there was one.

This is just one method of solving the problem. There are likely others.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • "Each method returns the original object so chaining will work." that's not necessarily true, and honestly it's not a great idea. – Benjamin Gruenbaum Aug 19 '16 at 00:45
  • Got it. It definitely seems like an unnecessarily difficult constraint to disallow modification of the original prototype methods. – Anthony Chung Aug 19 '16 at 00:46
  • Also, it's entirely unnecessary - you don't need to implement your own queues you already have promises - you just need to subclass promises with a promisified version of the object prototype and then make the object return those promises and be done with it. No need to write in 100 lines what 15 can do. – Benjamin Gruenbaum Aug 19 '16 at 01:03
  • @BenjaminGruenbaum - In the specific jQuery example I'm describing it DOES return the original object and it works great for what it's doing so I'm not sure what your objection is there. If you want to propose an answer using only promises, then by all means show us all how to do that in your own answer. In the world I've been working in, subclassing promises where other code creates promises and you don't necessarily control that code has been a mess. If you know of a world where that isn't a problem, please show us how. – jfriend00 Aug 19 '16 at 01:06
  • In jQuery animations the whole point is that it refers to the same DOM node - this is quite obviously not the case with what OP is doing. – Benjamin Gruenbaum Aug 19 '16 at 01:08
  • @BenjaminGruenbaum - It's just an example of chaining by returning the original object. Geez. I was explaining one way to do that type of chaining. Please stop shooting down a method that works perfectly well. If you have a better way of doing things, feel free to put your energy into your own answer. We'd all like to see it and the OP can decide which they like best. – jfriend00 Aug 19 '16 at 01:09
  • You honestly and truthfully believe that implementing your own queueing is a good methodology when already having promises and their own built in queueing that has been tested to work in a variety of scenarios? I don't object to your jQuery example - the part that bothers me is "so chaining will work" - mutation is not only not required for it to work - it is typically not even done - jQuery animations is one of the rare examples where it is done (because you have no choice, the DOM is mutable) where promises and observables are two counter-examples. – Benjamin Gruenbaum Aug 19 '16 at 01:10
  • @BenjaminGruenbaum - I personally don't know how to solve some of the problems involved in what you're suggesting. So, please show us how. – jfriend00 Aug 19 '16 at 01:11
  • Fair enough, I'll try to write an answer. – Benjamin Gruenbaum Aug 19 '16 at 01:11
  • @BenjaminGruenbaum - And, keep in mind that the OP said the requirement was `Item.async1().async2()....asyncN()`. Also, downvotes are generally for incorrect or bad code answers, not for answers that work just fine, but you happen to think there's a better way. So, if you're the downvoter, I think the better response here is for you to just post your own answer that proposes your better way and let the community see both choices. – jfriend00 Aug 19 '16 at 01:13
  • Ok, I [admit that was fun](http://stackoverflow.com/a/39029819/1348195) the downvote is for suggesting mutation here which I think it frankly beneath you as a code - if you point out that's not a requirement I will _gladly_ revert it. I'm pretty sure I owned up for that downvote by explaining specifically what I didn't like about the answer and you're welcome to clarify. – Benjamin Gruenbaum Aug 19 '16 at 01:33
0

I whould suggest you to use a library for that, i made one myself that allow not only secuential chaining but let you use loops and ifElse structures.

https://github.com/Raising/PromiseChain

(note that we are not using a parent object in this example, that cleans the code a lot) The internal scope save the result of each continue with the name provided

var internalScope = {}; //i'll use scope

new PromiseChain(internalScope )  
    .continue(function(internalScope){ return async1();},"firstResult")  
    .continue(function(internalScope){ return async2();},"secondResult")  
    .continue(function(internalScope){ return async3();},"thridResult")  
.end();

Alternatively if all functions belong to the same object and only need the scope as parameter you can do this

new PromiseChain(internalScope,yourObject)  // this is important if you use the 'this' keyword inside the functions, it works as a .bind(yourObject) for every function
    .continue(yourObject.async1,"firstResult")  
    .continue(yourObject.async2,"secondResult")  
    .continue(yourObject.async3,"thridResult")  
.end();