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
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:
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
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With