/** * An object representing a "promise" for a future value * * @param {?function(T, ?)=} onSuccess a function to handle successful * resolution of this promise * @param {?function(!Error, ?)=} onFail a function to handle failed * resolution of this promise * @constructor * @template T */ function Promise(onSuccess, onFail) { this.promise = this this._isPromise = true this._successFn = onSuccess this._failFn = onFail this._scope = this this._boundArgs = null this._hasContext = false this._nextContext = undefined this._currentContext = undefined } /** * Specify that the current promise should have a specified context * @param {*} context context * @private */ Promise.prototype._useContext = function (context) { this._nextContext = this._currentContext = context this._hasContext = true return this } Promise.prototype.clearContext = function () { this._hasContext = false this._nextContext = undefined return this } /** * Set the context for all promise handlers to follow * * NOTE(dpup): This should be considered deprecated. It does not do what most * people would expect. The context will be passed as a second argument to all * subsequent callbacks. * * @param {*} context An arbitrary context */ Promise.prototype.setContext = function (context) { this._nextContext = context this._hasContext = true return this } /** * Get the context for a promise * @return {*} the context set by setContext */ Promise.prototype.getContext = function () { return this._nextContext } /** * Resolve this promise with a specified value * * @param {*=} data */ Promise.prototype.resolve = function (data) { if (this._error || this._hasData) throw new Error("Unable to resolve or reject the same promise twice") var i if (data && isPromise(data)) { this._child = data if (this._promises) { for (i = 0; i < this._promises.length; i += 1) { data._chainPromise(this._promises[i]) } delete this._promises } if (this._onComplete) { for (i = 0; i < this._onComplete.length; i+= 1) { data.fin(this._onComplete[i]) } delete this._onComplete } } else if (data && isPromiseLike(data)) { data.then( function(data) { this.resolve(data) }.bind(this), function(err) { this.reject(err) }.bind(this) ) } else { this._hasData = true this._data = data if (this._onComplete) { for (i = 0; i < this._onComplete.length; i++) { this._onComplete[i]() } } if (this._promises) { for (i = 0; i < this._promises.length; i += 1) { this._promises[i]._withInput(data) } delete this._promises } } } /** * Reject this promise with an error * * @param {!Error} e */ Promise.prototype.reject = function (e) { if (this._error || this._hasData) throw new Error("Unable to resolve or reject the same promise twice") var i this._error = e if (this._ended) { process.nextTick(function onPromiseThrow() { throw e }) } if (this._onComplete) { for (i = 0; i < this._onComplete.length; i++) { this._onComplete[i]() } } if (this._promises) { for (i = 0; i < this._promises.length; i += 1) { this._promises[i]._withError(e) } delete this._promises } } /** * Provide a callback to be called whenever this promise successfully * resolves. Allows for an optional second callback to handle the failure * case. * * @param {?function(this:void, T, ?): RESULT|undefined} onSuccess * @param {?function(this:void, !Error, ?): RESULT=} onFail * @return {!Promise.} returns a new promise with the output of the onSuccess or * onFail handler * @template RESULT */ Promise.prototype.then = function (onSuccess, onFail) { var promise = new Promise(onSuccess, onFail) if (this._nextContext) promise._useContext(this._nextContext) if (this._child) this._child._chainPromise(promise) else this._chainPromise(promise) return promise } /** * Provide a callback to be called whenever this promise successfully * resolves. The callback will be executed in the context of the provided scope. * * @param {function(this:SCOPE, T, ?): RESULT} onSuccess * @param {SCOPE} scope Object whose context callback will be executed in. * @param {...*} var_args Additional arguments to be passed to the promise callback. * @return {!Promise.} returns a new promise with the output of the onSuccess * @template SCOPE, RESULT */ Promise.prototype.thenBound = function (onSuccess, scope, var_args) { var promise = new Promise(onSuccess) if (this._nextContext) promise._useContext(this._nextContext) promise._scope = scope if (arguments.length > 2) { promise._boundArgs = Array.prototype.slice.call(arguments, 2) } // Chaining must happen after setting args and scope since it may fire callback. if (this._child) this._child._chainPromise(promise) else this._chainPromise(promise) return promise } /** * Provide a callback to be called whenever this promise is rejected * * @param {function(this:void, !Error, ?)} onFail * @return {!Promise.} returns a new promise with the output of the onFail handler */ Promise.prototype.fail = function (onFail) { return this.then(null, onFail) } /** * Provide a callback to be called whenever this promise is rejected. * The callback will be executed in the context of the provided scope. * * @param {function(this:SCOPE, Error, ?)} onFail * @param {SCOPE} scope Object whose context callback will be executed in. * @param {...?} var_args * @return {!Promise.} returns a new promise with the output of the onSuccess * @template SCOPE */ Promise.prototype.failBound = function (onFail, scope, var_args) { var promise = new Promise(null, onFail) if (this._nextContext) promise._useContext(this._nextContext) promise._scope = scope if (arguments.length > 2) { promise._boundArgs = Array.prototype.slice.call(arguments, 2) } // Chaining must happen after setting args and scope since it may fire callback. if (this._child) this._child._chainPromise(promise) else this._chainPromise(promise) return promise } /** * Provide a callback to be called whenever this promise is either resolved * or rejected. * * @param {function()} onComplete * @return {!Promise.} returns the current promise */ Promise.prototype.fin = function (onComplete) { if (this._hasData || this._error) { onComplete() return this } if (this._child) { this._child.fin(onComplete) } else { if (!this._onComplete) this._onComplete = [onComplete] else this._onComplete.push(onComplete) } return this } /** * Mark this promise as "ended". If the promise is rejected, this will throw an * error in whatever scope it happens to be in * * @return {!Promise.} returns the current promise * @deprecated Prefer done(), because it's consistent with Q. */ Promise.prototype.end = function () { this._end() return this } /** * Mark this promise as "ended". * @private */ Promise.prototype._end = function () { if (this._error) { throw this._error } this._ended = true return this } /** * Close the promise. Any errors after this completes will be thrown to the global handler. * * @param {?function(this:void, T, ?)=} onSuccess a function to handle successful * resolution of this promise * @param {?function(this:void, !Error, ?)=} onFailure a function to handle failed * resolution of this promise * @return {void} */ Promise.prototype.done = function (onSuccess, onFailure) { var self = this if (onSuccess || onFailure) { self = self.then(onSuccess, onFailure) } self._end() } /** * Return a new promise that behaves the same as the current promise except * that it will be rejected if the current promise does not get fulfilled * after a certain amount of time. * * @param {number} timeoutMs The timeout threshold in msec * @param {string=} timeoutMsg error message * @return {!Promise.} a new promise with timeout */ Promise.prototype.timeout = function (timeoutMs, timeoutMsg) { var deferred = new Promise() var isTimeout = false var timeout = setTimeout(function() { deferred.reject(new Error(timeoutMsg || 'Promise timeout after ' + timeoutMs + ' ms.')) isTimeout = true }, timeoutMs) this.then(function (data) { if (!isTimeout) { clearTimeout(timeout) deferred.resolve(data) } }, function (err) { if (!isTimeout) { clearTimeout(timeout) deferred.reject(err) } }) return deferred.promise } /** * Attempt to resolve this promise with the specified input * * @param {*} data the input */ Promise.prototype._withInput = function (data) { if (this._successFn) { try { this.resolve(this._call(this._successFn, [data, this._currentContext])) } catch (e) { this.reject(e) } } else this.resolve(data) // context is no longer needed delete this._currentContext } /** * Attempt to reject this promise with the specified error * * @param {!Error} e * @private */ Promise.prototype._withError = function (e) { if (this._failFn) { try { this.resolve(this._call(this._failFn, [e, this._currentContext])) } catch (thrown) { this.reject(thrown) } } else this.reject(e) // context is no longer needed delete this._currentContext } /** * Calls a function in the correct scope, and includes bound arguments. * @param {Function} fn * @param {Array} args * @return {*} * @private */ Promise.prototype._call = function (fn, args) { if (this._boundArgs) { args = this._boundArgs.concat(args) } return fn.apply(this._scope, args) } /** * Chain a promise to the current promise * * @param {!Promise} promise the promise to chain * @private */ Promise.prototype._chainPromise = function (promise) { var i if (this._hasContext) promise._useContext(this._nextContext) if (this._child) { this._child._chainPromise(promise) } else if (this._hasData) { promise._withInput(this._data) } else if (this._error) { promise._withError(this._error) } else if (!this._promises) { this._promises = [promise] } else { this._promises.push(promise) } } /** * Utility function used for creating a node-style resolver * for deferreds * * @param {!Promise} deferred a promise that looks like a deferred * @param {Error=} err an optional error * @param {*=} data optional data */ function resolver(deferred, err, data) { if (err) deferred.reject(err) else deferred.resolve(data) } /** * Creates a node-style resolver for a deferred by wrapping * resolver() * * @return {function(?Error, *)} node-style callback */ Promise.prototype.makeNodeResolver = function () { return resolver.bind(null, this) } /** * Return true iff the given object is a promise of this library. * * Because kew's API is slightly different than other promise libraries, * it's important that we have a test for its promise type. If you want * to test for a more general A+ promise, you should do a cap test for * the features you want. * * @param {*} obj The object to test * @return {boolean} Whether the object is a promise */ function isPromise(obj) { return !!obj._isPromise } /** * Return true iff the given object is a promise-like object, e.g. appears to * implement Promises/A+ specification * * @param {*} obj The object to test * @return {boolean} Whether the object is a promise-like object */ function isPromiseLike(obj) { return typeof obj === 'object' && typeof obj.then === 'function' } /** * Static function which creates and resolves a promise immediately * * @param {T} data data to resolve the promise with * @return {!Promise.} * @template T */ function resolve(data) { var promise = new Promise() promise.resolve(data) return promise } /** * Static function which creates and rejects a promise immediately * * @param {!Error} e error to reject the promise with * @return {!Promise} */ function reject(e) { var promise = new Promise() promise.reject(e) return promise } /** * Replace an element in an array with a new value. Used by .all() to * call from .then() * * @param {!Array} arr * @param {number} idx * @param {*} val * @return {*} the val that's being injected into the array */ function replaceEl(arr, idx, val) { arr[idx] = val return val } /** * Replace an element in an array as it is resolved with its value. * Used by .allSettled(). * * @param {!Array} arr * @param {number} idx * @param {*} value The value from a resolved promise. * @return {*} the data that's being passed in */ function replaceElFulfilled(arr, idx, value) { arr[idx] = { state: 'fulfilled', value: value } return value } /** * Replace an element in an array as it is rejected with the reason. * Used by .allSettled(). * * @param {!Array} arr * @param {number} idx * @param {*} reason The reason why the original promise is rejected * @return {*} the data that's being passed in */ function replaceElRejected(arr, idx, reason) { arr[idx] = { state: 'rejected', reason: reason } return reason } /** * Takes in an array of promises or literals and returns a promise which returns * an array of values when all have resolved. If any fail, the promise fails. * * @param {!Array.} promises * @return {!Promise.} */ function all(promises) { if (arguments.length != 1 || !Array.isArray(promises)) { promises = Array.prototype.slice.call(arguments, 0) } if (!promises.length) return resolve([]) var outputs = [] var finished = false var promise = new Promise() var counter = promises.length for (var i = 0; i < promises.length; i += 1) { if (!promises[i] || !isPromiseLike(promises[i])) { outputs[i] = promises[i] counter -= 1 } else { promises[i].then(replaceEl.bind(null, outputs, i)) .then(function decrementAllCounter() { counter-- if (!finished && counter === 0) { finished = true promise.resolve(outputs) } }, function onAllError(e) { if (!finished) { finished = true promise.reject(e) } }) } } if (counter === 0 && !finished) { finished = true promise.resolve(outputs) } return promise } /** * Takes in an array of promises or values and returns a promise that is * fulfilled with an array of state objects when all have resolved or * rejected. If a promise is resolved, its corresponding state object is * {state: 'fulfilled', value: Object}; whereas if a promise is rejected, its * corresponding state object is {state: 'rejected', reason: Object}. * * @param {!Array} promises or values * @return {!Promise.} Promise fulfilled with state objects for each input */ function allSettled(promises) { if (!Array.isArray(promises)) { throw Error('The input to "allSettled()" should be an array of Promise or values') } if (!promises.length) return resolve([]) var outputs = [] var promise = new Promise() var counter = promises.length for (var i = 0; i < promises.length; i += 1) { if (!promises[i] || !isPromiseLike(promises[i])) { replaceElFulfilled(outputs, i, promises[i]) if ((--counter) === 0) promise.resolve(outputs) } else { promises[i] .then(replaceElFulfilled.bind(null, outputs, i), replaceElRejected.bind(null, outputs, i)) .then(function () { if ((--counter) === 0) promise.resolve(outputs) }) } } return promise } /** * Create a new Promise which looks like a deferred * * @return {!Promise} */ function defer() { return new Promise() } /** * Return a promise which will wait a specified number of ms to resolve * * @param {*} delayMsOrVal A delay (in ms) if this takes one argument, or ther * return value if it takes two. * @param {number=} opt_delayMs * @return {!Promise} */ function delay(delayMsOrVal, opt_delayMs) { var returnVal = undefined var delayMs = delayMsOrVal if (typeof opt_delayMs != 'undefined') { delayMs = opt_delayMs returnVal = delayMsOrVal } if (typeof delayMs != 'number') { throw new Error('Bad delay value ' + delayMs) } var defer = new Promise() setTimeout(function onDelay() { defer.resolve(returnVal) }, delayMs) return defer } /** * Returns a promise that has the same result as `this`, but fulfilled * after at least ms milliseconds * @param {number} ms */ Promise.prototype.delay = function (ms) { return this.then(function (val) { return delay(val, ms) }) } /** * Return a promise which will evaluate the function fn in a future turn with * the provided args * * @param {function(...)} fn * @param {...*} var_args a variable number of arguments * @return {!Promise} */ function fcall(fn, var_args) { var rootArgs = Array.prototype.slice.call(arguments, 1) var defer = new Promise() process.nextTick(function onNextTick() { try { defer.resolve(fn.apply(undefined, rootArgs)) } catch (e) { defer.reject(e) } }) return defer } /** * Returns a promise that will be invoked with the result of a node style * callback. All args to fn should be given except for the final callback arg * * @param {function(...)} fn * @param {...*} var_args a variable number of arguments * @return {!Promise} */ function nfcall(fn, var_args) { // Insert an undefined argument for scope and let bindPromise() do the work. var args = Array.prototype.slice.call(arguments, 0) args.splice(1, 0, undefined) return bindPromise.apply(undefined, args)() } /** * Binds a function to a scope with an optional number of curried arguments. Attaches * a node style callback as the last argument and returns a promise * * @param {function(...)} fn * @param {Object} scope * @param {...*} var_args a variable number of arguments * @return {function(...)}: !Promise} */ function bindPromise(fn, scope, var_args) { var rootArgs = Array.prototype.slice.call(arguments, 2) return function onBoundPromise(var_args) { var defer = new Promise() try { fn.apply(scope, rootArgs.concat(Array.prototype.slice.call(arguments, 0), defer.makeNodeResolver())) } catch (e) { defer.reject(e) } return defer } } module.exports = { all: all , bindPromise: bindPromise , defer: defer , delay: delay , fcall: fcall , isPromise: isPromise , isPromiseLike: isPromiseLike , nfcall: nfcall , resolve: resolve , reject: reject , allSettled: allSettled , Promise: Promise }