Skip to content

Instantly share code, notes, and snippets.

@pchampin
Last active February 8, 2017 09:53
Show Gist options
  • Save pchampin/6751c974561b3a7ae9ac to your computer and use it in GitHub Desktop.
Save pchampin/6751c974561b3a7ae9ac to your computer and use it in GitHub Desktop.
An extension of JS Promises with a forEach method.
/* global Promise */
/**
* An IterablePromise is used to combine the ease of use of loops,
* with the power of Promises.
*
* Assume you want to apply an asynchronous function process(),
* returning a Promise, to each item of an array ``a``,
* but wait for each element to be processed before processing the next one.
* You would do it like this:
*
* new IterablePromise(a).forEach(function (value) {
* process(value); // may throw an exception
* }.then(function(allResults) {
* // do something when all the results that have been processed
* }).catch(function(err) {
* // something bad happend
* });
*
* ItarablePromise also accept a preprocess function as its second argument.
* This function may be asynchronous (i.e. return a promise).
*
* IterablePromise guarantees that
* - the values of ``a`` will all be preprocessed ASAP;
* - then they will be processed ASAP but in the correct order,
* i.e. waiting each process() to resolve before processing the next value,
* but not waiting for the preprocessing of subsequent values to start;
* - an exception thrown by preprocess() or process() will stop the processing,
* (just like in a standard synchronous loop),
* preventing all further elements of ``a`` to be processed
* (but not preprocessed);
* - as a bonus, if the string 'break' is thrown,
* this will stop the iteration (as any other exception),
* but will *not* be considered as a rejection of the promise.
*
* In other words, in the callback of the forEach() method:
* - ``return`` acts as a ``continue`` in a standard loop,
* - ``throw 'break'`` acts as a break in a standard loop,
* - any other exception acts as an exception in a standard loop.
*
* Note that ``new IterablePromise(a, preprocess)`` can also be used
* as a standard promise (i.e. without calling forEach),
* in which case it is equivalent to:
*
* Promise.all(a.map(preprocess))
*
* See testIterablePromise below for examples of use.
*/
function IterablePromise(arrayLike, preprocess) {
if (preprocess !== undefined) {
arrayLike = Array.prototype.map.bind(arrayLike)(preprocess);
}
var that = this;
var lst = [];
var i = 0;
that.forEach = function(callback) {
if (i >= arrayLike.length) {
return Promise.resolve(lst);
} else {
return Promise.resolve(arrayLike[i++])
.then(callback)
.then(function(x) { lst.push(x); })
.then(that.forEach.bind(that, callback))
.catch(function(err) {if(err !== 'break') throw err; else return lst; })
;
}
};
that.then = function(onFullfilled, onRejected) {
return that.forEach(function(x){return x}).then(onFullfilled, onRejected);
};
that.catch = function(onRejected) {
return that.forEach(function(x){return x}).catch(onRejected);
};
}
/**
* Minimal test suite
*/
function testIterablePromise() {
// simple handling of elements
function test1() {
return new Promise(function(resolve, reject) {
console.log('--- test1');
var a = [1,2,3];
new IterablePromise(a)
.forEach(function(value) {
return process(value);
}).then(function(allResults) {
console.log(allResults);
resolve();
}).catch(function(err) {
reject("test1: " + err);
});
});
}
// simple handling of elements with preprocessing
function test2() {
return new Promise(function(resolve, reject) {
console.log('--- test2');
var a = [1,2,3];
new IterablePromise(a, preprocess)
.forEach(function(value) {
return process(value);
}).then(function(allResults) {
console.log(allResults);
resolve();
}).catch(function(err) {
reject("test2: " + err);
});
});
}
// use IterablePromise as a simple Promise
function test3() {
return new Promise(function(resolve, reject) {
console.log('--- test3');
var a = [1,2,3];
new IterablePromise(a, preprocess)
.then(function(allResults) {
console.log(allResults);
resolve();
}).catch(function(err) {
reject("test3: " + err);
});
});
}
// exception thrown by preprocess (on 0)
function test4() {
return new Promise(function(resolve, reject) {
console.log('--- test4');
var a = [1,2,3,0,4];
new IterablePromise(a, preprocess)
.forEach(function(value) {
return process(value);
}).then(function(allResults) {
reject("test4: should have caught an error");
}).catch(function(err) {
resolve();
});
});
}
// exception thrown by process
function test5() {
return new Promise(function(resolve, reject) {
console.log('--- test4');
var a = [1,2,3,4,5];
new IterablePromise(a, preprocess)
.forEach(function(value) {
if (value === 4) throw "error";
return process(value);
}).then(function(allResults) {
reject("test4: should have caught an error");
}).catch(function(err) {
resolve();
});
});
}
// throw "break"
function test6() {
return new Promise(function(resolve, reject) {
console.log('--- test5');
var a = [1,2,3,4,5];
new IterablePromise(a, preprocess)
.forEach(function(value) {
if (value === 4) throw 'break';
return process(value);
}).then(function(allResults) {
console.log(allResults);
resolve();
}).catch(function(err) {
reject("test5: throw 'break' should not be rejected");
});
});
}
// utility functions
function makeTimeoutPromise(f, delay) {
if (delay === undefined) delay = 200;
return new Promise(function (resolve, reject) {
setTimeout(function() {
try { resolve(f()); }
catch (err) { reject(err); }
}, delay);
});
}
function preprocess(x) {
var delay = 200;
if (x == 2) delay = 600;
return makeTimeoutPromise(function() {
console.log("preprocess " + x);
if (x === 0) throw "preprocess failed";
return x;
}, delay);
}
function process(x) {
return makeTimeoutPromise(function() {
console.log("process " + x);
return x*10;
}, 500 - 300*(x%2));
}
Promise.resolve()
.then(test1)
.then(test2)
.then(test3)
.then(test4)
.then(test5)
.then(test6)
.catch(function(err) {
console.error(err);
});
}
testIterablePromise();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment