Skip to content

Instantly share code, notes, and snippets.

@Twisol
Last active December 13, 2015 16:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Twisol/869d40fa63244a5a6442 to your computer and use it in GitHub Desktop.
Save Twisol/869d40fa63244a5a6442 to your computer and use it in GitHub Desktop.
Implementation of reduced-nextTick promises using a trampoline-based approach.
/** @license MIT License (c) copyright B Cavalier & J Hann */
/**
* A lightweight CommonJS Promises/A and when() implementation
* when is part of the cujo.js family of libraries (http://cujojs.com/)
*
* Licensed under the MIT License at:
* http://www.opensource.org/licenses/mit-license.php
*
* @version 2.x.x
*/
(function(define) { 'use strict';
define(function() {
var undef, nextTick, slice, reduceArray;
/*global setImmediate:true */
nextTick = typeof process === 'object' ? process.nextTick
: typeof setImmediate === 'function' ? setImmediate
: function(task) { setTimeout(task, 0); };
function identity(x) { return x; }
function noop() {}
function Promise(then) {
this.then = then;
}
Promise.prototype = {
/**
* Register a callback that will be called when a promise is
* fulfilled or rejected. Optionally also register a progress handler.
* Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress)
* @param {function?} [onFulfilledOrRejected]
* @param {function?} [onProgress]
* @return {Promise}
*/
always: function(onFulfilledOrRejected, onProgress) {
return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress);
},
/**
* Register a rejection handler. Shortcut for .then(undefined, onRejected)
* @param {function?} onRejected
* @return {Promise}
*/
otherwise: function(onRejected) {
return this.then(undef, onRejected);
},
/**
* Shortcut for .then(function() { return value; })
* @param {*} value
* @return {Promise} a promise that:
* - is fulfilled if value is not a promise, or
* - if value is a promise, will fulfill with its value, or reject
* with its reason.
*/
yield: function(value) {
return this.then(function() {
return value;
});
},
/**
* Assumes that this promise will fulfill with an array, and arranges
* for the onFulfilled to be called with the array as its argument list
* i.e. onFulfilled.spread(undefined, array).
* @param {function} onFulfilled function to receive spread arguments
* @return {Promise}
*/
spread: function(onFulfilled) {
return this.then(function(array) {
// array may contain promises, so resolve its contents.
return all(array, function(array) {
return onFulfilled.apply(undef, array);
});
});
}
};
// Duck-typing for promises
function isPromise(p) {
return (p && typeof p.then === 'function');
}
// A promise that immediately calls an onFulfilled handler with a value.
function fulfilled(value) {
return new Promise(function(onFulfilled) {
if (typeof onFulfilled !== 'function') {
onFulfilled = fulfilled;
}
var result;
try {
result = onFulfilled(value);
if (!isPromise(result)) {
result = fulfilled(result);
} else if (!(result instanceof Promise)) {
result = canonize(result);
}
} catch (err) {
result = rejected(err);
}
return result;
});
}
// A promise that immediately calls an onRejected handler with a reason.
function rejected(reason) {
return new Promise(function(onFulfilled, onRejected) {
if (typeof onRejected !== 'function') {
onRejected = rejected;
}
var result;
try {
result = onRejected(reason);
if (!isPromise(result)) {
result = fulfilled(result);
} else if (!(result instanceof Promise)) {
result = canonize(result);
}
} catch (err) {
result = rejected(err);
}
return result;
});
}
function progressing(update) {
return new Promise(function(onFulfilled, onRejected, onProgress) {
if (typeof onProgress !== 'function') {
onProgress = progressing;
}
var result;
try {
result = onProgress(update);
if (!isPromise(result)) {
result = progressing(result);
} else if (!(result instanceof Promise)) {
result = canonize(result);
}
} catch (err) {
result = progressing(err);
}
return result;
});
}
// Pumps through a chain of handlers for every external stimulus.
var gTrampoline = (function() {
var stack = [];
var running = false;
function pump() {
running = true;
try {
while (stack.length > 0) {
stack.pop()();
}
} finally {
running = false;
}
}
return {
invoke: function(f) {
stack.push(f);
if (!running) {
pump();
}
},
push: function(f) {
stack.push(f);
},
pump: pump
};
})();
// Defers the execution of a chain of handlers until a later tick
// in the event loop.
function Trampoline() {
var _invoke;
var stack = [];
function callLater(f) {
stack.push(f);
_invoke = callUnshift;
setTimeout(function() {
_invoke = callImmediate;
while (stack.length > 0) {
gTrampoline.push(stack.pop());
}
gTrampoline.pump();
}, 0);
}
function callUnshift(f) {
stack.push(f);
}
function callImmediate(f) {
gTrampoline.invoke(f);
}
_invoke = callLater;
return {
invoke: function(f) {
return _invoke(f);
}
};
}
// Resolves an associate promise, invoking it handlers via
// the provided trampoline.
function deferred(trampoline) {
var _handlers = [];
var _promise;
var _then, _resolve, _reject, _progress;
function liveThen(onFulfilled, onRejected, onProgress) {
var trigger = deferred(trampoline);
_handlers.push(function(promise) {
promise
.then(onFulfilled, onRejected, onProgress)
.then(
function(x) { trigger.resolve(x); },
function(x) { trigger.reject(x); },
function(x) { trigger.progress(x); });
});
return trigger.promise;
}
function deadThen(onFulfilled, onRejected, onProgress) {
trampoline = new Trampoline();
_handlers = [];
_then = liveThen;
fire(_promise);
return liveThen(onFulfilled, onRejected, onProgress);
}
function fire(promise) {
var _trampoline = trampoline;
_trampoline.invoke(function() {
_then = deadThen;
for (var i = _handlers.length - 1; i >= 0; --i) {
_trampoline.invoke(_handlers[i].bind(undef, promise));
}
});
}
function freeze(promise) {
var oldPromise = _promise;
_promise = promise;
_resolve = function(value) {
return defer().resolve(value);
};
_reject = function(reason) {
return defer().reject(reason);
};
_progress = identity;
return oldPromise;
}
_resolve = function(value) {
if (!isPromise(value)) {
value = fulfilled(value);
} else if (!(value instanceof Promise)) {
value = canonize(value);
}
fire(value);
return freeze(value);
};
_reject = function(reason) {
reason = rejected(reason);
fire(reason);
return freeze(reason);
};
_progress = function(update) {
update = progressing(update);
fire(update);
trampoline = new Trampoline();
};
_then = liveThen;
_promise = new Promise(function(onSuccess, onFailure, onProgress) {
return _then(onSuccess, onFailure, onProgress);
});
var resolver = {
resolve: function(value) {
return _resolve(value);
},
reject: function(reason) {
return _reject(reason);
},
progress: function(update) {
_progress(update);
return update;
}
};
return {
promise: _promise,
resolver: resolver,
then: _promise.then,
resolve: resolver.resolve,
reject: resolver.reject,
progress: resolver.progress
};
}
function defer() {
return deferred(new Trampoline());
}
// Wrap an external promise with a trusted promise.
function canonize(promise) {
var trigger = defer();
promise.then(
function(x) { trigger.resolve(x); },
function(x) { trigger.reject(x); });
return trigger.promise;
}
function when(promise, onFulfilled, onRejected, onProgress) {
if (!isPromise(promise)) {
promise = defer().resolve(promise);
} else if (!(promise instanceof Promise)) {
promise = canonize(promise);
}
return promise.then(onFulfilled, onRejected, onProgress);
}
/*
* Non-core primitives
*/
slice = [].slice;
// ES5 reduce implementation if native not available
// See: http://es5.github.com/#x15.4.4.21 as there are many
// specifics and edge cases.
reduceArray = [].reduce ||
function(reduceFunc /*, initialValue */) {
/*jshint maxcomplexity: 7*/
// ES5 dictates that reduce.length === 1
// This implementation deviates from ES5 spec in the following ways:
// 1. It does not check if reduceFunc is a Callable
var arr, args, reduced, len, i;
i = 0;
// This generates a jshint warning, despite being valid
// "Missing 'new' prefix when invoking a constructor."
// See https://github.com/jshint/jshint/issues/392
arr = Object(this);
len = arr.length >>> 0;
args = arguments;
// If no initialValue, use first item of array (we know length !== 0 here)
// and adjust i to start at second item
if(args.length <= 1) {
// Skip to the first real element in the array
for(;;) {
if(i in arr) {
reduced = arr[i++];
break;
}
// If we reached the end of the array without finding any real
// elements, it's a TypeError
if(++i >= len) {
throw new TypeError();
}
}
} else {
// If initialValue provided, use it
reduced = args[1];
}
// Do the actual reduce
for(;i < len; ++i) {
// Skip holes
if(i in arr) {
reduced = reduceFunc(reduced, arr[i], i, arr);
}
}
return reduced;
};
/**
* Helper that checks arrayOfCallbacks to ensure that each element is either
* a function, or null or undefined.
* @private
* @param {number} start index at which to start checking items in arrayOfCallbacks
* @param {Array} arrayOfCallbacks array to check
* @throws {Error} if any element of arrayOfCallbacks is something other than
* a functions, null, or undefined.
*/
function checkCallbacks(start, arrayOfCallbacks) {
// TODO: Promises/A+ update type checking and docs
var arg, i = arrayOfCallbacks.length;
while(i > start) {
arg = arrayOfCallbacks[--i];
if (arg != null && typeof arg != 'function') {
throw new Error('arg '+i+' must be a function');
}
}
}
/**
* Initiates a competitive race, returning a promise that will resolve when
* howMany of the supplied promisesOrValues have resolved, or will reject when
* it becomes impossible for howMany to resolve, for example, when
* (promisesOrValues.length - howMany) + 1 input promises reject.
*
* @param {Array} promisesOrValues array of anything, may contain a mix
* of promises and values
* @param howMany {number} number of promisesOrValues to resolve
* @param {function?} [onFulfilled] resolution handler
* @param {function?} [onRejected] rejection handler
* @param {function?} [onProgress] progress handler
* @returns {Promise} promise that will resolve to an array of howMany values that
* resolved first, or will reject with an array of (promisesOrValues.length - howMany) + 1
* rejection reasons.
*/
function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) {
checkCallbacks(2, arguments);
return when(promisesOrValues, function(promisesOrValues) {
var toResolve, toReject, values, reasons, deferred, fulfillOne, rejectOne, progress, len, i;
len = promisesOrValues.length >>> 0;
toResolve = Math.max(0, Math.min(howMany, len));
values = [];
toReject = (len - toResolve) + 1;
reasons = [];
deferred = defer();
// No items in the input, resolve immediately
if (!toResolve) {
deferred.resolve(values);
} else {
progress = deferred.progress;
rejectOne = function(reason) {
reasons.push(reason);
if(!--toReject) {
fulfillOne = rejectOne = noop;
deferred.reject(reasons.slice());
}
};
fulfillOne = function(val) {
// This orders the values based on promise resolution order
// Another strategy would be to use the original position of
// the corresponding promise.
values.push(val);
if (!--toResolve) {
fulfillOne = rejectOne = noop;
deferred.resolve(values.slice());
}
};
for(i = 0; i < len; ++i) {
if(i in promisesOrValues) {
when(promisesOrValues[i], fulfiller, rejecter, progress);
}
}
}
return deferred.then(onFulfilled, onRejected, onProgress);
function rejecter(reason) {
rejectOne(reason);
}
function fulfiller(val) {
fulfillOne(val);
}
});
}
/**
* Initiates a competitive race, returning a promise that will resolve when
* any one of the supplied promisesOrValues has resolved or will reject when
* *all* promisesOrValues have rejected.
*
* @param {Array|Promise} promisesOrValues array of anything, may contain a mix
* of {@link Promise}s and values
* @param {function?} [onFulfilled] resolution handler
* @param {function?} [onRejected] rejection handler
* @param {function?} [onProgress] progress handler
* @returns {Promise} promise that will resolve to the value that resolved first, or
* will reject with an array of all rejected inputs.
*/
function any(promisesOrValues, onFulfilled, onRejected, onProgress) {
function unwrapSingleResult(val) {
return onFulfilled ? onFulfilled(val[0]) : val[0];
}
return some(promisesOrValues, 1, unwrapSingleResult, onRejected, onProgress);
}
/**
* Return a promise that will resolve only once all the supplied promisesOrValues
* have resolved. The resolution value of the returned promise will be an array
* containing the resolution values of each of the promisesOrValues.
* @memberOf when
*
* @param {Array|Promise} promisesOrValues array of anything, may contain a mix
* of {@link Promise}s and values
* @param {function?} [onFulfilled] resolution handler
* @param {function?} [onRejected] rejection handler
* @param {function?} [onProgress] progress handler
* @returns {Promise}
*/
function all(promisesOrValues, onFulfilled, onRejected, onProgress) {
checkCallbacks(1, arguments);
return map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress);
}
/**
* Joins multiple promises into a single returned promise.
* @return {Promise} a promise that will fulfill when *all* the input promises
* have fulfilled, or will reject when *any one* of the input promises rejects.
*/
function join(/* ...promises */) {
return map(arguments, identity);
}
/**
* Traditional map function, similar to `Array.prototype.map()`, but allows
* input to contain {@link Promise}s and/or values, and mapFunc may return
* either a value or a {@link Promise}
*
* @param {Array|Promise} promise array of anything, may contain a mix
* of {@link Promise}s and values
* @param {function} mapFunc mapping function mapFunc(value) which may return
* either a {@link Promise} or value
* @returns {Promise} a {@link Promise} that will resolve to an array containing
* the mapped output values.
*/
function map(promise, mapFunc) {
return when(promise, function(array) {
var results, len, toResolve, resolve, i, d;
// Since we know the resulting length, we can preallocate the results
// array to avoid array expansions.
toResolve = len = array.length >>> 0;
results = [];
d = defer();
if(!toResolve) {
d.resolve(results);
} else {
resolve = function resolveOne(item, i) {
when(item, mapFunc).then(function(mapped) {
results[i] = mapped;
if(!--toResolve) {
d.resolve(results);
}
}, d.reject);
};
// Since mapFunc may be async, get all invocations of it into flight
for(i = 0; i < len; i++) {
if(i in array) {
resolve(array[i], i);
} else {
--toResolve;
}
}
}
return d.promise;
});
}
/**
* Traditional reduce function, similar to `Array.prototype.reduce()`, but
* input may contain promises and/or values, and reduceFunc
* may return either a value or a promise, *and* initialValue may
* be a promise for the starting value.
*
* @param {Array|Promise} promise array or promise for an array of anything,
* may contain a mix of promises and values.
* @param {function} reduceFunc reduce function reduce(currentValue, nextValue, index, total),
* where total is the total number of items being reduced, and will be the same
* in each call to reduceFunc.
* @returns {Promise} that will resolve to the final reduced value
*/
function reduce(promise, reduceFunc /*, initialValue */) {
var args = slice.call(arguments, 1);
return when(promise, function(array) {
var total;
total = array.length;
// Wrap the supplied reduceFunc with one that handles promises and then
// delegates to the supplied.
args[0] = function (current, val, i) {
return when(current, function (c) {
return when(val, function (value) {
return reduceFunc(c, value, i, total);
});
});
};
return reduceArray.apply(array, args);
});
}
/**
* Ensure that resolution of promiseOrValue will trigger resolver with the
* value or reason of promiseOrValue, or instead with resolveValue if it is provided.
*
* @param promiseOrValue
* @param {Object} resolver
* @param {function} resolver.resolve
* @param {function} resolver.reject
* @param {*} [resolveValue]
* @returns {Promise}
*/
function chain(promiseOrValue, resolver, resolveValue) {
var useResolveValue = arguments.length > 2;
return when(promiseOrValue,
function(val) {
val = useResolveValue ? resolveValue : val;
resolver.resolve(val);
return val;
},
function(reason) {
resolver.reject(reason);
return rejected(reason);
},
resolver.progress
);
}
when.defer = defer;
when.isPromise = isPromise; // Determine if a thing is a promise
when.resolve = function(value) {
return defer().resolve(value);
};
when.reject = function(reason) {
if (!isPromise(reason)) {
return defer().reject(reason);
} else {
if (!(reason instanceof Promise)) {
reason = canonize(reason);
}
return reason.then(rejected, rejected);
}
//return reason.then(onFulfilled, onRejected, onProgress);
return defer().reject(reason);
};
when.join = join; // Join 2 or more promises
when.all = all; // Resolve a list of promises
when.map = map; // Array.map() for promises
when.reduce = reduce; // Array.reduce() for promises
when.any = any; // One-winner race
when.some = some; // Multi-winner race
when.chain = chain; // Make a promise trigger another resolver
return when;
});
})(typeof define == 'function' && define.amd
? define
: function (factory) { typeof exports === 'object'
? (module.exports = factory())
: (this.when = factory());
}
// Boilerplate for AMD, Node, and browser global
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment