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