filer/lib/when.js

739 lines
23 KiB
JavaScript

/** @license MIT License (c) copyright B Cavalier & J Hann */
/**
* when
* 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 1.0.4
*/
(function(define) {
define(function() {
var freeze, reduceArray, undef;
/**
* No-Op function used in method replacement
* @private
*/
function noop() {}
/**
* Allocate a new Array of size n
* @private
* @param n {number} size of new Array
* @returns {Array}
*/
function allocateArray(n) {
return new Array(n);
}
/**
* Use freeze if it exists
* @function
* @private
*/
freeze = Object.freeze || function(o) { return o; };
// 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 */) {
// 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;
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;
};
/**
* Trusted Promise constructor. A Promise created from this constructor is
* a trusted when.js promise. Any other duck-typed promise is considered
* untrusted.
*/
function Promise() {}
/**
* Create an already-resolved promise for the supplied value
* @private
*
* @param value anything
* @return {Promise}
*/
function resolved(value) {
var p = new Promise();
p.then = function(callback) {
checkCallbacks(arguments);
var nextValue;
try {
if(callback) nextValue = callback(value);
return promise(nextValue === undef ? value : nextValue);
} catch(e) {
return rejected(e);
}
};
return freeze(p);
}
/**
* Create an already-rejected {@link Promise} with the supplied
* rejection reason.
* @private
*
* @param reason rejection reason
* @return {Promise}
*/
function rejected(reason) {
var p = new Promise();
p.then = function(callback, errback) {
checkCallbacks(arguments);
var nextValue;
try {
if(errback) {
nextValue = errback(reason);
return promise(nextValue === undef ? reason : nextValue)
}
return rejected(reason);
} catch(e) {
return rejected(e);
}
};
return freeze(p);
}
/**
* Helper that checks arrayOfCallbacks to ensure that each element is either
* a function, or null or undefined.
*
* @param arrayOfCallbacks {Array} array to check
* @throws {Error} if any element of arrayOfCallbacks is something other than
* a Functions, null, or undefined.
*/
function checkCallbacks(arrayOfCallbacks) {
var arg, i = arrayOfCallbacks.length;
while(i) {
arg = arrayOfCallbacks[--i];
if (arg != null && typeof arg != 'function') throw new Error('callback is not a function');
}
}
/**
* Creates a new, CommonJS compliant, Deferred with fully isolated
* resolver and promise parts, either or both of which may be given out
* safely to consumers.
* The Deferred itself has the full API: resolve, reject, progress, and
* then. The resolver has resolve, reject, and progress. The promise
* only has then.
*
* @memberOf when
* @function
*
* @returns {Deferred}
*/
function defer() {
var deferred, promise, listeners, progressHandlers, _then, _progress, complete;
listeners = [];
progressHandlers = [];
/**
* Pre-resolution then() that adds the supplied callback, errback, and progback
* functions to the registered listeners
*
* @private
*
* @param [callback] {Function} resolution handler
* @param [errback] {Function} rejection handler
* @param [progback] {Function} progress handler
*
* @throws {Error} if any argument is not null, undefined, or a Function
*/
_then = function unresolvedThen(callback, errback, progback) {
// Check parameters and fail immediately if any supplied parameter
// is not null/undefined and is also not a function.
// That is, any non-null/undefined parameter must be a function.
checkCallbacks(arguments);
var deferred = defer();
listeners.push(function(promise) {
promise.then(callback, errback)
.then(deferred.resolve, deferred.reject, deferred.progress);
});
progback && progressHandlers.push(progback);
return deferred.promise;
};
/**
* Registers a handler for this {@link Deferred}'s {@link Promise}. Even though all arguments
* are optional, each argument that *is* supplied must be null, undefined, or a Function.
* Any other value will cause an Error to be thrown.
*
* @memberOf Promise
*
* @param [callback] {Function} resolution handler
* @param [errback] {Function} rejection handler
* @param [progback] {Function} progress handler
*
* @throws {Error} if any argument is not null, undefined, or a Function
*/
function then(callback, errback, progback) {
return _then(callback, errback, progback);
}
/**
* Resolves this {@link Deferred}'s {@link Promise} with val as the
* resolution value.
*
* @memberOf Resolver
*
* @param val anything
*/
function resolve(val) {
complete(resolved(val));
}
/**
* Rejects this {@link Deferred}'s {@link Promise} with err as the
* reason.
*
* @memberOf Resolver
*
* @param err anything
*/
function reject(err) {
complete(rejected(err));
}
/**
* @private
* @param update
*/
_progress = function(update) {
var progress, i = 0;
while (progress = progressHandlers[i++]) progress(update);
};
/**
* Emits a progress update to all progress observers registered with
* this {@link Deferred}'s {@link Promise}
*
* @memberOf Resolver
*
* @param update anything
*/
function progress(update) {
_progress(update);
}
/**
* Transition from pre-resolution state to post-resolution state, notifying
* all listeners of the resolution or rejection
*
* @private
*
* @param completed {Promise} the completed value of this deferred
*/
complete = function(completed) {
var listener, i = 0;
// Replace _then with one that directly notifies with the result.
_then = completed.then;
// Replace complete so that this Deferred can only be completed
// once. Also Replace _progress, so that subsequent attempts to issue
// progress throw.
complete = _progress = function alreadyCompleted() {
// TODO: Consider silently returning here so that parties who
// have a reference to the resolver cannot tell that the promise
// has been resolved using try/catch
throw new Error("already completed");
};
// Free progressHandlers array since we'll never issue progress events
// for this promise again now that it's completed
progressHandlers = undef;
// Notify listeners
// Traverse all listeners registered directly with this Deferred
while (listener = listeners[i++]) {
listener(completed);
}
listeners = [];
};
/**
* The full Deferred object, with both {@link Promise} and {@link Resolver}
* parts
* @class Deferred
* @name Deferred
* @augments Resolver
* @augments Promise
*/
deferred = {};
// Promise and Resolver parts
// Freeze Promise and Resolver APIs
/**
* The Promise API
* @namespace Promise
* @name Promise
*/
promise = new Promise();
promise.then = deferred.then = then;
/**
* The {@link Promise} for this {@link Deferred}
* @memberOf Deferred
* @name promise
* @type {Promise}
*/
deferred.promise = freeze(promise);
/**
* The {@link Resolver} for this {@link Deferred}
* @namespace Resolver
* @name Resolver
* @memberOf Deferred
* @name resolver
* @type {Resolver}
*/
deferred.resolver = freeze({
resolve: (deferred.resolve = resolve),
reject: (deferred.reject = reject),
progress: (deferred.progress = progress)
});
return deferred;
}
/**
* Determines if promiseOrValue is a promise or not. Uses the feature
* test from http://wiki.commonjs.org/wiki/Promises/A to determine if
* promiseOrValue is a promise.
*
* @param promiseOrValue anything
*
* @returns {Boolean} true if promiseOrValue is a {@link Promise}
*/
function isPromise(promiseOrValue) {
return promiseOrValue && typeof promiseOrValue.then === 'function';
}
/**
* Register an observer for a promise or immediate value.
*
* @function
* @name when
* @namespace
*
* @param promiseOrValue anything
* @param {Function} [callback] callback to be called when promiseOrValue is
* successfully resolved. If promiseOrValue is an immediate value, callback
* will be invoked immediately.
* @param {Function} [errback] callback to be called when promiseOrValue is
* rejected.
* @param {Function} [progressHandler] callback to be called when progress updates
* are issued for promiseOrValue.
*
* @returns {Promise} a new {@link Promise} that will complete with the return
* value of callback or errback or the completion value of promiseOrValue if
* callback and/or errback is not supplied.
*/
function when(promiseOrValue, callback, errback, progressHandler) {
// Get a promise for the input promiseOrValue
// See promise()
var trustedPromise = promise(promiseOrValue);
// Register promise handlers
return trustedPromise.then(callback, errback, progressHandler);
}
/**
* Returns promiseOrValue if promiseOrValue is a {@link Promise}, a new Promise if
* promiseOrValue is a foreign promise, or a new, already-resolved {@link Promise}
* whose resolution value is promiseOrValue if promiseOrValue is an immediate value.
*
* Note that this function is not safe to export since it will return its
* input when promiseOrValue is a {@link Promise}
*
* @private
*
* @param promiseOrValue anything
*
* @returns Guaranteed to return a trusted Promise. If promiseOrValue is a when.js {@link Promise}
* returns promiseOrValue, otherwise, returns a new, already-resolved, when.js {@link Promise}
* whose resolution value is:
* * the resolution value of promiseOrValue if it's a foreign promise, or
* * promiseOrValue if it's a value
*/
function promise(promiseOrValue) {
var promise, deferred;
if(promiseOrValue instanceof Promise) {
// It's a when.js promise, so we trust it
promise = promiseOrValue;
} else {
// It's not a when.js promise. Check to see if it's a foreign promise
// or a value.
deferred = defer();
if(isPromise(promiseOrValue)) {
// It's a compliant promise, but we don't know where it came from,
// so we don't trust its implementation entirely. Introduce a trusted
// middleman when.js promise
// IMPORTANT: This is the only place when.js should ever call .then() on
// an untrusted promise.
promiseOrValue.then(deferred.resolve, deferred.reject, deferred.progress);
promise = deferred.promise;
} else {
// It's a value, not a promise. Create an already-resolved promise
// for it.
deferred.resolve(promiseOrValue);
promise = deferred.promise;
}
}
return promise;
}
/**
* Return a promise that will resolve when howMany of the supplied promisesOrValues
* have resolved. The resolution value of the returned promise will be an array of
* length howMany containing the resolutions values of the triggering promisesOrValues.
*
* @memberOf when
*
* @param promisesOrValues {Array} array of anything, may contain a mix
* of {@link Promise}s and values
* @param howMany
* @param [callback]
* @param [errback]
* @param [progressHandler]
*
* @returns {Promise}
*/
function some(promisesOrValues, howMany, callback, errback, progressHandler) {
var toResolve, results, ret, deferred, resolver, rejecter, handleProgress, len, i;
len = promisesOrValues.length >>> 0;
toResolve = Math.max(0, Math.min(howMany, len));
results = [];
deferred = defer();
ret = when(deferred, callback, errback, progressHandler);
// Wrapper so that resolver can be replaced
function resolve(val) {
resolver(val);
}
// Wrapper so that rejecter can be replaced
function reject(err) {
rejecter(err);
}
// Wrapper so that progress can be replaced
function progress(update) {
handleProgress(update);
}
function complete() {
resolver = rejecter = handleProgress = noop;
}
// No items in the input, resolve immediately
if (!toResolve) {
deferred.resolve(results);
} else {
// Resolver for promises. Captures the value and resolves
// the returned promise when toResolve reaches zero.
// Overwrites resolver var with a noop once promise has
// be resolved to cover case where n < promises.length
resolver = function(val) {
// This orders the values based on promise resolution order
// Another strategy would be to use the original position of
// the corresponding promise.
results.push(val);
if (!--toResolve) {
complete();
deferred.resolve(results);
}
};
// Rejecter for promises. Rejects returned promise
// immediately, and overwrites rejecter var with a noop
// once promise to cover case where n < promises.length.
// TODO: Consider rejecting only when N (or promises.length - N?)
// promises have been rejected instead of only one?
rejecter = function(err) {
complete();
deferred.reject(err);
};
handleProgress = deferred.progress;
// TODO: Replace while with forEach
for(i = 0; i < len; ++i) {
if(i in promisesOrValues) {
when(promisesOrValues[i], resolve, reject, progress);
}
}
}
return ret;
}
/**
* 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 promisesOrValues {Array} array of anything, may contain a mix
* of {@link Promise}s and values
* @param [callback] {Function}
* @param [errback] {Function}
* @param [progressHandler] {Function}
*
* @returns {Promise}
*/
function all(promisesOrValues, callback, errback, progressHandler) {
var results, promise;
results = allocateArray(promisesOrValues.length);
promise = reduce(promisesOrValues, reduceIntoArray, results);
return when(promise, callback, errback, progressHandler);
}
function reduceIntoArray(current, val, i) {
current[i] = val;
return current;
}
/**
* Return a promise that will resolve when any one of the supplied promisesOrValues
* has resolved. The resolution value of the returned promise will be the resolution
* value of the triggering promiseOrValue.
*
* @memberOf when
*
* @param promisesOrValues {Array} array of anything, may contain a mix
* of {@link Promise}s and values
* @param [callback] {Function}
* @param [errback] {Function}
* @param [progressHandler] {Function}
*
* @returns {Promise}
*/
function any(promisesOrValues, callback, errback, progressHandler) {
function unwrapSingleResult(val) {
return callback(val[0]);
}
return some(promisesOrValues, 1, unwrapSingleResult, errback, progressHandler);
}
/**
* 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}
*
* @memberOf when
*
* @param promisesOrValues {Array} array of anything, may contain a mix
* of {@link Promise}s and values
* @param mapFunc {Function} 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(promisesOrValues, mapFunc) {
var results, i;
// Since we know the resulting length, we can preallocate the results
// array to avoid array expansions.
i = promisesOrValues.length;
results = allocateArray(i);
// Since mapFunc may be async, get all invocations of it into flight
// asap, and then use reduce() to collect all the results
for(;i >= 0; --i) {
if(i in promisesOrValues)
results[i] = when(promisesOrValues[i], mapFunc);
}
// Could use all() here, but that would result in another array
// being allocated, i.e. map() would end up allocating 2 arrays
// of size len instead of just 1. Since all() uses reduce()
// anyway, avoid the additional allocation by calling reduce
// directly.
return reduce(results, reduceIntoArray, results);
}
/**
* Traditional reduce function, similar to `Array.prototype.reduce()`, but
* input may contain {@link Promise}s and/or values, but reduceFunc
* may return either a value or a {@link Promise}, *and* initialValue may
* be a {@link Promise} for the starting value.
*
* @memberOf when
*
* @param promisesOrValues {Array} array of anything, may contain a mix
* of {@link Promise}s and values
* @param reduceFunc {Function} 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.
* @param initialValue starting value, or a {@link Promise} for the starting value
*
* @returns {Promise} that will resolve to the final reduced value
*/
function reduce(promisesOrValues, reduceFunc, initialValue) {
var total, args;
total = promisesOrValues.length;
// Skip promisesOrValues, since it will be used as 'this' in the call
// to the actual reduce engine below.
// Wrap the supplied reduceFunc with one that handles promises and then
// delegates to the supplied.
args = [
function (current, val, i) {
return when(current, function (c) {
return when(val, function (value) {
return reduceFunc(c, value, i, total);
});
});
}
];
if (arguments.length >= 3) args.push(initialValue);
return promise(reduceArray.apply(promisesOrValues, args));
}
/**
* Ensure that resolution of promiseOrValue will complete resolver with the completion
* value of promiseOrValue, or instead with resolveValue if it is provided.
*
* @memberOf when
*
* @param promiseOrValue
* @param resolver {Resolver}
* @param [resolveValue] anything
*
* @returns {Promise}
*/
function chain(promiseOrValue, resolver, resolveValue) {
var useResolveValue = arguments.length > 2;
return when(promiseOrValue,
function(val) {
if(useResolveValue) val = resolveValue;
resolver.resolve(val);
return val;
},
function(e) {
resolver.reject(e);
return rejected(e);
},
resolver.progress
);
}
//
// Public API
//
when.defer = defer;
when.isPromise = isPromise;
when.some = some;
when.all = all;
when.any = any;
when.reduce = reduce;
when.map = map;
when.chain = chain;
return when;
});
})(typeof define == 'function'
? define
: function (factory) { typeof module != 'undefined'
? (module.exports = factory())
: (this.when = factory());
}
// Boilerplate for AMD, Node, and browser global
);