diff --git a/js/lib/when-2.0.0.js b/js/lib/when-2.4.0.js similarity index 61% rename from js/lib/when-2.0.0.js rename to js/lib/when-2.4.0.js index 78249532..aa386275 100644 --- a/js/lib/when-2.0.0.js +++ b/js/lib/when-2.4.0.js @@ -9,27 +9,30 @@ * * @author Brian Cavalier * @author John Hann - * @version 2.0.0 + * @version 2.4.0 */ -(function(define) { 'use strict'; -define(function () { +(function(define, global) { 'use strict'; +define(function (require) { // Public API - when.defer = defer; // Create a deferred + when.promise = promise; // Create a pending promise when.resolve = resolve; // Create a resolved promise when.reject = reject; // Create a rejected promise + when.defer = defer; // Create a {promise, resolver} pair 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.settle = settle; // Settle a list of promises when.any = any; // One-winner race when.some = some; // Multi-winner race - when.isPromise = isPromise; // Determine if a thing is a promise + when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike + when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable /** * Register an observer for a promise or immediate value. @@ -57,13 +60,35 @@ define(function () { * a trusted when.js promise. Any other duck-typed promise is considered * untrusted. * @constructor + * @param {function} sendMessage function to deliver messages to the promise's handler + * @param {function?} inspect function that reports the promise's state * @name Promise */ - function Promise(then) { - this.then = then; + function Promise(sendMessage, inspect) { + this._message = sendMessage; + this.inspect = inspect; } Promise.prototype = { + /** + * Register handlers for this promise. + * @param [onFulfilled] {Function} fulfillment handler + * @param [onRejected] {Function} rejection handler + * @param [onProgress] {Function} progress handler + * @return {Promise} new Promise + */ + then: function(onFulfilled, onRejected, onProgress) { + /*jshint unused:false*/ + var args, sendMessage; + + args = arguments; + sendMessage = this._message; + + return _promise(function(resolve, reject, notify) { + sendMessage('when', args, resolve, notify); + }, this._status && this._status.observed()); + }, + /** * Register a rejection handler. Shortcut for .then(undefined, onRejected) * @param {function?} onRejected @@ -84,9 +109,7 @@ define(function () { * @returns {Promise} */ ensure: function(onFulfilledOrRejected) { - var self = this; - - return this.then(injectHandler, injectHandler).yield(self); + return this.then(injectHandler, injectHandler)['yield'](this); function injectHandler() { return resolve(onFulfilledOrRejected()); @@ -107,6 +130,16 @@ define(function () { }); }, + /** + * Runs a side effect when this promise fulfills, without changing the + * fulfillment value. + * @param {function} onFulfilledSideEffect + * @returns {Promise} + */ + tap: function(onFulfilledSideEffect) { + return this.then(onFulfilledSideEffect)['yield'](this); + }, + /** * Assumes that this promise will fulfill with an array, and arranges * for the onFulfilled to be called with the array as its argument list @@ -162,13 +195,16 @@ define(function () { } /** - * Creates a new Deferred with fully isolated resolver and promise parts, - * either or both of which may be given out safely to consumers. + * Creates a {promise, resolver} pair, either or both of which + * may be given out safely to consumers. * The resolver has resolve, reject, and progress. The promise - * only has then. + * has then plus extended promise API. * * @return {{ * promise: Promise, + * resolve: function:Promise, + * reject: function:Promise, + * notify: function:Promise * resolver: { * resolve: function:Promise, * reject: function:Promise, @@ -216,12 +252,26 @@ define(function () { /** * Creates a new promise whose fate is determined by resolver. - * @private (for now) * @param {function} resolver function(resolve, reject, notify) * @returns {Promise} promise whose fate is determine by resolver */ function promise(resolver) { - var value, handlers = []; + return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus()); + } + + /** + * Creates a new promise, linked to parent, whose fate is determined + * by resolver. + * @param {function} resolver function(resolve, reject, notify) + * @param {Promise?} status promise from which the new promise is begotten + * @returns {Promise} promise whose fate is determine by resolver + * @private + */ + function _promise(resolver, status) { + var self, value, consumers = []; + + self = new Promise(_message, inspect); + self._status = status; // Call the provider resolver to seal the promise's fate try { @@ -231,29 +281,34 @@ define(function () { } // Return the promise - return new Promise(then); + return self; /** - * Register handlers for this promise. - * @param [onFulfilled] {Function} fulfillment handler - * @param [onRejected] {Function} rejection handler - * @param [onProgress] {Function} progress handler - * @return {Promise} new Promise + * Private message delivery. Queues and delivers messages to + * the promise's ultimate fulfillment value or rejection reason. + * @private + * @param {String} type + * @param {Array} args + * @param {Function} resolve + * @param {Function} notify */ - function then(onFulfilled, onRejected, onProgress) { - return promise(function(resolve, reject, notify) { - handlers - // Call handlers later, after resolution - ? handlers.push(function(value) { - value.then(onFulfilled, onRejected, onProgress) - .then(resolve, reject, notify); - }) - // Call handlers soon, but not in the current stack - : enqueue(function() { - value.then(onFulfilled, onRejected, onProgress) - .then(resolve, reject, notify); - }); - }); + function _message(type, args, resolve, notify) { + consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); }); + + function deliver(p) { + p._message(type, args, resolve, notify); + } + } + + /** + * Returns a snapshot of the promise's state at the instant inspect() + * is called. The returned object is not live and will not update as + * the promise's state changes. + * @returns {{ state:String, value?:*, reason?:* }} status snapshot + * of the promise. + */ + function inspect() { + return value ? value.inspect() : toPendingState(); } /** @@ -262,14 +317,17 @@ define(function () { * @param {*|Promise} val resolution value */ function promiseResolve(val) { - if(!handlers) { + if(!consumers) { return; } value = coerce(val); - scheduleHandlers(handlers, value); + scheduleConsumers(consumers, value); + consumers = undef; - handlers = undef; + if(status) { + updateStatus(value, status); + } } /** @@ -285,27 +343,90 @@ define(function () { * @param {*} update progress event payload to pass to all listeners */ function promiseNotify(update) { - if(handlers) { - scheduleHandlers(handlers, progressing(update)); + if(consumers) { + scheduleConsumers(consumers, progressed(update)); } } } + /** + * Creates a fulfilled, local promise as a proxy for a value + * NOTE: must never be exposed + * @param {*} value fulfillment value + * @returns {Promise} + */ + function fulfilled(value) { + return near( + new NearFulfilledProxy(value), + function() { return toFulfilledState(value); } + ); + } + + /** + * Creates a rejected, local promise with the supplied reason + * NOTE: must never be exposed + * @param {*} reason rejection reason + * @returns {Promise} + */ + function rejected(reason) { + return near( + new NearRejectedProxy(reason), + function() { return toRejectedState(reason); } + ); + } + + /** + * Creates a near promise using the provided proxy + * NOTE: must never be exposed + * @param {object} proxy proxy for the promise's ultimate value or reason + * @param {function} inspect function that returns a snapshot of the + * returned near promise's state + * @returns {Promise} + */ + function near(proxy, inspect) { + return new Promise(function (type, args, resolve) { + try { + resolve(proxy[type].apply(proxy, args)); + } catch(e) { + resolve(rejected(e)); + } + }, inspect); + } + + /** + * Create a progress promise with the supplied update. + * @private + * @param {*} update + * @return {Promise} progress promise + */ + function progressed(update) { + return new Promise(function (type, args, _, notify) { + var onProgress = args[2]; + try { + notify(typeof onProgress === 'function' ? onProgress(update) : update); + } catch(e) { + notify(e); + } + }); + } + /** * Coerces x to a trusted Promise * * @private * @param {*} x thing to coerce - * @returns {Promise} Guaranteed to return a trusted Promise. If x + * @returns {*} Guaranteed to return a trusted Promise. If x * is trusted, returns x, otherwise, returns a new, trusted, already-resolved * Promise whose resolution value is: * * the resolution value of x if it's a foreign promise, or * * x if it's a value */ function coerce(x) { - if(x instanceof Promise) { + if (x instanceof Promise) { return x; - } else if (x !== Object(x)) { + } + + if (!(x === Object(x) && 'then' in x)) { return fulfilled(x); } @@ -332,61 +453,34 @@ define(function () { } /** - * Create an already-fulfilled promise for the supplied value - * @private + * Proxy for a near, fulfilled value * @param {*} value - * @return {Promise} fulfilled promise + * @constructor */ - function fulfilled(value) { - var self = new Promise(function (onFulfilled) { - try { - return typeof onFulfilled == 'function' - ? coerce(onFulfilled(value)) : self; - } catch (e) { - return rejected(e); - } - }); - - return self; + function NearFulfilledProxy(value) { + this.value = value; } + NearFulfilledProxy.prototype.when = function(onResult) { + return typeof onResult === 'function' ? onResult(this.value) : this.value; + }; + /** - * Create an already-rejected promise with the supplied rejection reason. - * @private + * Proxy for a near rejection * @param {*} reason - * @return {Promise} rejected promise + * @constructor */ - function rejected(reason) { - var self = new Promise(function (_, onRejected) { - try { - return typeof onRejected == 'function' - ? coerce(onRejected(reason)) : self; - } catch (e) { - return rejected(e); - } - }); - - return self; + function NearRejectedProxy(reason) { + this.reason = reason; } - /** - * Create a progress promise with the supplied update. - * @private - * @param {*} update - * @return {Promise} progress promise - */ - function progressing(update) { - var self = new Promise(function (_, __, onProgress) { - try { - return typeof onProgress == 'function' - ? progressing(onProgress(update)) : self; - } catch (e) { - return progressing(e); - } - }); - - return self; - } + NearRejectedProxy.prototype.when = function(_, onError) { + if(typeof onError === 'function') { + return onError(this.reason); + } else { + throw this.reason; + } + }; /** * Schedule a task that will process a list of handlers @@ -395,7 +489,7 @@ define(function () { * @param {Array} handlers queue of handlers to execute * @param {*} value passed as the only arg to each handler */ - function scheduleHandlers(handlers, value) { + function scheduleConsumers(handlers, value) { enqueue(function() { var handler, i = 0; while (handler = handlers[i++]) { @@ -404,14 +498,23 @@ define(function () { }); } + function updateStatus(value, status) { + value.then(statusFulfilled, statusRejected); + + function statusFulfilled() { status.fulfilled(); } + function statusRejected(r) { status.rejected(r); } + } + /** - * Determines if promiseOrValue is a promise or not - * - * @param {*} promiseOrValue anything - * @returns {boolean} true if promiseOrValue is a {@link Promise} + * Determines if x is promise-like, i.e. a thenable object + * NOTE: Will return true for *any thenable object*, and isn't truly + * safe, since it may attempt to access the `then` property of x (i.e. + * clever/malicious getters may do weird things) + * @param {*} x anything + * @returns {boolean} true if x is promise-like */ - function isPromise(promiseOrValue) { - return promiseOrValue && typeof promiseOrValue.then === 'function'; + function isPromiseLike(x) { + return x && typeof x.then === 'function'; } /** @@ -423,17 +526,15 @@ define(function () { * @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 + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @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) { return promise(resolveSome).then(onFulfilled, onRejected, onProgress); @@ -457,7 +558,7 @@ define(function () { rejectOne = function(reason) { reasons.push(reason); if(!--toReject) { - fulfillOne = rejectOne = noop; + fulfillOne = rejectOne = identity; reject(reasons); } }; @@ -466,7 +567,7 @@ define(function () { // This orders the values based on promise resolution order values.push(val); if (!--toResolve) { - fulfillOne = rejectOne = noop; + fulfillOne = rejectOne = identity; resolve(values); } }; @@ -496,9 +597,9 @@ define(function () { * * @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 + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to the value that resolved first, or * will reject with an array of all rejected inputs. */ @@ -519,14 +620,13 @@ define(function () { * * @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 + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} */ function all(promisesOrValues, onFulfilled, onRejected, onProgress) { - checkCallbacks(1, arguments); - return map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); + return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); } /** @@ -535,28 +635,49 @@ define(function () { * have fulfilled, or will reject when *any one* of the input promises rejects. */ function join(/* ...promises */) { - return map(arguments, identity); + 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} array 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. + * Settles all input promises such that they are guaranteed not to + * be pending once the returned promise fulfills. The returned promise + * will always fulfill, except in the case where `array` is a promise + * that rejects. + * @param {Array|Promise} array or promise for array of promises to settle + * @returns {Promise} promise that always fulfills with an array of + * outcome snapshots for each input promise. + */ + function settle(array) { + return _map(array, toFulfilledState, toRejectedState); + } + + /** + * Promise-aware array map function, similar to `Array.prototype.map()`, + * but input array may contain promises or values. + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. */ function map(array, mapFunc) { + return _map(array, mapFunc); + } + + /** + * Internal map that allows a fallback to handle rejections + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @param {function?} fallback function to handle rejected promises + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. + */ + function _map(array, mapFunc, fallback) { return when(array, function(array) { - return promise(resolveMap); + return _promise(resolveMap); function resolveMap(resolve, reject, notify) { - var results, len, toResolve, resolveOne, i; + var results, len, toResolve, i; // Since we know the resulting length, we can preallocate the results // array to avoid array expansions. @@ -565,27 +686,28 @@ define(function () { if(!toResolve) { resolve(results); - } else { + return; + } - resolveOne = function(item, i) { - when(item, mapFunc).then(function(mapped) { - results[i] = mapped; - - if(!--toResolve) { - resolve(results); - } - }, reject, notify); - }; - - // Since mapFunc may be async, get all invocations of it into flight - for(i = 0; i < len; i++) { - if(i in array) { - resolveOne(array[i], i); - } else { - --toResolve; - } + // Since mapFunc may be async, get all invocations of it into flight + for(i = 0; i < len; i++) { + if(i in array) { + resolveOne(array[i], i); + } else { + --toResolve; } } + + function resolveOne(item, i) { + when(item, mapFunc, fallback).then(function(mapped) { + results[i] = mapped; + notify(mapped); + + if(!--toResolve) { + resolve(results); + } + }, reject); + } } }); } @@ -625,12 +747,46 @@ define(function () { }); } + // Snapshot states + + /** + * Creates a fulfilled state snapshot + * @private + * @param {*} x any value + * @returns {{state:'fulfilled',value:*}} + */ + function toFulfilledState(x) { + return { state: 'fulfilled', value: x }; + } + + /** + * Creates a rejected state snapshot + * @private + * @param {*} x any reason + * @returns {{state:'rejected',reason:*}} + */ + function toRejectedState(x) { + return { state: 'rejected', reason: x }; + } + + /** + * Creates a pending state snapshot + * @private + * @returns {{state:'pending'}} + */ + function toPendingState() { + return { state: 'pending' }; + } + // - // Utilities, etc. + // Internals, utilities, etc. // var reduceArray, slice, fcall, nextTick, handlerQueue, - timeout, funcProto, call, arrayProto, undef; + setTimeout, funcProto, call, arrayProto, monitorApi, + cjsRequire, undef; + + cjsRequire = require; // // Shared handler queue processing @@ -648,20 +804,13 @@ define(function () { */ function enqueue(task) { if(handlerQueue.push(task) === 1) { - scheduleDrainQueue(); + nextTick(drainQueue); } } /** - * Schedule the queue to be drained in the next tick. - */ - function scheduleDrainQueue() { - nextTick(drainQueue); - } - - /** - * Drain the handler queue entirely or partially, being careful to allow - * the queue to be extended while it is being processed, and to continue + * Drain the handler queue entirely, being careful to allow the + * queue to be extended while it is being processed, and to continue * processing until it is truly empty. */ function drainQueue() { @@ -674,20 +823,36 @@ define(function () { handlerQueue = []; } - // - // Capture function and array utils - // - /*global setImmediate:true*/ + // capture setTimeout to avoid being caught by fake timers + // used in time based tests + setTimeout = global.setTimeout; - // capture setTimeout to avoid being caught by fake timers used in time based tests - timeout = setTimeout; - nextTick = typeof setImmediate === 'function' - ? typeof window === 'undefined' - ? setImmediate - : setImmediate.bind(window) - : typeof process === 'object' - ? process.nextTick - : function(task) { timeout(task, 0); }; + // Allow attaching the monitor to when() if env has no console + monitorApi = typeof console != 'undefined' ? console : when; + + // Prefer setImmediate or MessageChannel, cascade to node, + // vertx and finally setTimeout + /*global setImmediate,MessageChannel,process*/ + if (typeof setImmediate === 'function') { + nextTick = setImmediate.bind(global); + } else if(typeof MessageChannel !== 'undefined') { + var channel = new MessageChannel(); + channel.port1.onmessage = drainQueue; + nextTick = function() { channel.port2.postMessage(0); }; + } else if (typeof process === 'object' && process.nextTick) { + nextTick = process.nextTick; + } else { + try { + // vert.x 1.x || 2.x + nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext; + } catch(ignore) { + nextTick = function(t) { setTimeout(t, 0); }; + } + } + + // + // Capture/polyfill function and array utils + // // Safe function calls funcProto = Function.prototype; @@ -748,40 +913,10 @@ define(function () { return reduced; }; - // - // Utility functions - // - - /** - * 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'); - } - } - } - - function noop() {} - function identity(x) { return x; } return when; }); -})( - typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(); } -); +})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this); diff --git a/js/package.json b/js/package.json index 8a4e425d..9134bbc0 100644 --- a/js/package.json +++ b/js/package.json @@ -16,7 +16,7 @@ "dependencies": { "bane": "~1.0.0", "faye-websocket": "~0.4.4", - "when": "~2.0.0" + "when": "~2.4.0" }, "devDependencies": { "buster": "~0.6.12",