3 * An object representing a "promise" for a future value
5 * @param {?function(T, ?)=} onSuccess a function to handle successful
6 * resolution of this promise
7 * @param {?function(!Error, ?)=} onFail a function to handle failed
8 * resolution of this promise
12 function Promise(onSuccess, onFail) {
14 this._isPromise = true
15 this._successFn = onSuccess
18 this._boundArgs = null
19 this._hasContext = false
20 this._nextContext = undefined
21 this._currentContext = undefined
25 * @param {function()} callback
27 function nextTick (callback) {
31 if (typeof process !== 'undefined' && typeof process.nextTick === 'function') {
32 nextTick = process.nextTick
36 * All callback execution should go through this function. While the
37 * implementation below is simple, it can be replaced with more sophisticated
38 * implementations that enforce QoS on the event loop.
40 * @param {Promise} defer
41 * @param {Function} callback
42 * @param {Object|undefined} scope
45 function nextTickCallback (defer, callback, scope, args) {
47 defer.resolve(callback.apply(scope, args))
54 * Used for accessing the nextTick function from outside the kew module.
58 function getNextTickFunction () {
63 * Used for overriding the nextTick function from outside the kew module so that
64 * the user can plug and play lower level schedulers
65 * @param {!Function} fn
67 function setNextTickFunction (fn) {
72 * Keep track of the number of promises that are rejected along side
73 * the number of rejected promises we call _failFn on so we can look
74 * for leaked rejections.
77 function PromiseStats() {
79 this.errorsEmitted = 0
82 this.errorsHandled = 0
85 var stats = new PromiseStats()
87 Promise.prototype._handleError = function () {
88 if (!this._errorHandled) {
90 this._errorHandled = true
95 * Specify that the current promise should have a specified context
96 * @param {*} context context
99 Promise.prototype._useContext = function (context) {
100 this._nextContext = this._currentContext = context
101 this._hasContext = true
105 Promise.prototype.clearContext = function () {
106 this._hasContext = false
107 this._nextContext = undefined
112 * Set the context for all promise handlers to follow
114 * NOTE(dpup): This should be considered deprecated. It does not do what most
115 * people would expect. The context will be passed as a second argument to all
116 * subsequent callbacks.
118 * @param {*} context An arbitrary context
120 Promise.prototype.setContext = function (context) {
121 this._nextContext = context
122 this._hasContext = true
127 * Get the context for a promise
128 * @return {*} the context set by setContext
130 Promise.prototype.getContext = function () {
131 return this._nextContext
135 * Resolve this promise with a specified value
139 Promise.prototype.resolve = function (data) {
140 if (this._error || this._hasData) throw new Error("Unable to resolve or reject the same promise twice")
143 if (data && isPromise(data)) {
145 if (this._promises) {
146 for (i = 0; i < this._promises.length; i += 1) {
147 data._chainPromise(this._promises[i])
149 delete this._promises
152 if (this._onComplete) {
153 for (i = 0; i < this._onComplete.length; i+= 1) {
154 data.fin(this._onComplete[i])
156 delete this._onComplete
158 } else if (data && isPromiseLike(data)) {
160 function(data) { this.resolve(data) }.bind(this),
161 function(err) { this.reject(err) }.bind(this)
167 if (this._onComplete) {
168 for (i = 0; i < this._onComplete.length; i++) {
169 this._onComplete[i]()
173 if (this._promises) {
174 for (i = 0; i < this._promises.length; i += 1) {
175 this._promises[i]._useContext(this._nextContext)
176 this._promises[i]._withInput(data)
178 delete this._promises
184 * Reject this promise with an error
188 Promise.prototype.reject = function (e) {
189 if (this._error || this._hasData) throw new Error("Unable to resolve or reject the same promise twice")
193 stats.errorsEmitted++
197 process.nextTick(function onPromiseThrow() {
202 if (this._onComplete) {
203 for (i = 0; i < this._onComplete.length; i++) {
204 this._onComplete[i]()
208 if (this._promises) {
210 for (i = 0; i < this._promises.length; i += 1) {
211 this._promises[i]._useContext(this._nextContext)
212 this._promises[i]._withError(e)
214 delete this._promises
219 * Provide a callback to be called whenever this promise successfully
220 * resolves. Allows for an optional second callback to handle the failure
223 * @param {?function(this:void, T, ?): RESULT|undefined} onSuccess
224 * @param {?function(this:void, !Error, ?): RESULT=} onFail
225 * @return {!Promise.<RESULT>} returns a new promise with the output of the onSuccess or
229 Promise.prototype.then = function (onSuccess, onFail) {
230 var promise = new Promise(onSuccess, onFail)
231 if (this._nextContext) promise._useContext(this._nextContext)
233 if (this._child) this._child._chainPromise(promise)
234 else this._chainPromise(promise)
240 * Provide a callback to be called whenever this promise successfully
241 * resolves. The callback will be executed in the context of the provided scope.
243 * @param {function(this:SCOPE, ...): RESULT} onSuccess
244 * @param {SCOPE} scope Object whose context callback will be executed in.
245 * @param {...*} var_args Additional arguments to be passed to the promise callback.
246 * @return {!Promise.<RESULT>} returns a new promise with the output of the onSuccess
247 * @template SCOPE, RESULT
249 Promise.prototype.thenBound = function (onSuccess, scope, var_args) {
250 var promise = new Promise(onSuccess)
251 if (this._nextContext) promise._useContext(this._nextContext)
253 promise._scope = scope
254 if (arguments.length > 2) {
255 promise._boundArgs = Array.prototype.slice.call(arguments, 2)
258 // Chaining must happen after setting args and scope since it may fire callback.
259 if (this._child) this._child._chainPromise(promise)
260 else this._chainPromise(promise)
266 * Provide a callback to be called whenever this promise is rejected
268 * @param {function(this:void, !Error, ?)} onFail
269 * @return {!Promise.<T>} returns a new promise with the output of the onFail handler
271 Promise.prototype.fail = function (onFail) {
272 return this.then(null, onFail)
276 * Provide a callback to be called whenever this promise is rejected.
277 * The callback will be executed in the context of the provided scope.
279 * @param {function(this:SCOPE, ...)} onFail
280 * @param {SCOPE} scope Object whose context callback will be executed in.
281 * @param {...?} var_args
282 * @return {!Promise.<T>} returns a new promise with the output of the onSuccess
285 Promise.prototype.failBound = function (onFail, scope, var_args) {
286 var promise = new Promise(null, onFail)
287 if (this._nextContext) promise._useContext(this._nextContext)
289 promise._scope = scope
290 if (arguments.length > 2) {
291 promise._boundArgs = Array.prototype.slice.call(arguments, 2)
294 // Chaining must happen after setting args and scope since it may fire callback.
295 if (this._child) this._child._chainPromise(promise)
296 else this._chainPromise(promise)
302 * Spread a promises outputs to the functions arguments.
303 * @param {?function(this:void, ...): RESULT|undefined} onSuccess
304 * @return {!Promise.<RESULT>} returns a new promise with the output of the onSuccess or
308 Promise.prototype.spread = function (onSuccess) {
309 return this.then(allInternal)
310 .then(function (array) {
311 return onSuccess.apply(null, array)
316 * Spread a promises outputs to the functions arguments.
317 * @param {function(this:SCOPE, ...): RESULT} onSuccess
318 * @param {SCOPE} scope Object whose context callback will be executed in.
319 * @param {...*} var_args Additional arguments to be passed to the promise callback.
320 * @return {!Promise.<RESULT>} returns a new promise with the output of the onSuccess
321 * @template SCOPE, RESULT
323 Promise.prototype.spreadBound = function (onSuccess, scope, var_args) {
324 var args = Array.prototype.slice.call(arguments, 2)
325 return this.then(allInternal)
326 .then(function (array) {
327 return onSuccess.apply(scope, args.concat(array))
332 * Provide a callback to be called whenever this promise is either resolved
335 * @param {function()} onComplete
336 * @return {!Promise.<T>} returns the current promise
338 Promise.prototype.fin = function (onComplete) {
339 if (this._hasData || this._error) {
345 this._child.fin(onComplete)
347 if (!this._onComplete) this._onComplete = [onComplete]
348 else this._onComplete.push(onComplete)
355 * Mark this promise as "ended". If the promise is rejected, this will throw an
356 * error in whatever scope it happens to be in
358 * @return {!Promise.<T>} returns the current promise
359 * @deprecated Prefer done(), because it's consistent with Q.
361 Promise.prototype.end = function () {
368 * Mark this promise as "ended".
371 Promise.prototype._end = function () {
381 * Close the promise. Any errors after this completes will be thrown to the global handler.
383 * @param {?function(this:void, T, ?)=} onSuccess a function to handle successful
384 * resolution of this promise
385 * @param {?function(this:void, !Error, ?)=} onFailure a function to handle failed
386 * resolution of this promise
389 Promise.prototype.done = function (onSuccess, onFailure) {
391 if (onSuccess || onFailure) {
392 self = self.then(onSuccess, onFailure)
398 * Return a new promise that behaves the same as the current promise except
399 * that it will be rejected if the current promise does not get fulfilled
400 * after a certain amount of time.
402 * @param {number} timeoutMs The timeout threshold in msec
403 * @param {string=} timeoutMsg error message
404 * @return {!Promise.<T>} a new promise with timeout
406 Promise.prototype.timeout = function (timeoutMs, timeoutMsg) {
407 var deferred = new Promise()
408 var isTimeout = false
410 var timeout = setTimeout(function() {
411 deferred.reject(new Error(timeoutMsg || 'Promise timeout after ' + timeoutMs + ' ms.'))
415 this.then(function (data) {
417 clearTimeout(timeout)
418 deferred.resolve(data)
423 clearTimeout(timeout)
428 return deferred.promise
432 * Attempt to resolve this promise with the specified input
434 * @param {*} data the input
436 Promise.prototype._withInput = function (data) {
437 if (this._successFn) {
438 this._nextTick(this._successFn, [data, this._currentContext])
443 // context is no longer needed
444 delete this._currentContext
448 * Attempt to reject this promise with the specified error
453 Promise.prototype._withError = function (e) {
455 this._nextTick(this._failFn, [e, this._currentContext])
460 // context is no longer needed
461 delete this._currentContext
465 * Calls a function in the correct scope, and includes bound arguments.
466 * @param {Function} fn
467 * @param {Array} args
470 Promise.prototype._nextTick = function (fn, args) {
471 if (this._boundArgs) {
472 args = this._boundArgs.concat(args)
474 nextTick(nextTickCallback.bind(null, this, fn, this._scope, args))
478 * Chain a promise to the current promise
480 * @param {!Promise} promise the promise to chain
483 Promise.prototype._chainPromise = function (promise) {
485 if (this._hasContext) promise._useContext(this._nextContext)
488 this._child._chainPromise(promise)
489 } else if (this._hasData) {
490 promise._withInput(this._data)
491 } else if (this._error) {
492 // We can't rely on _withError() because it's called on the chained promises
493 // and we need to use the source's _errorHandled state
495 promise._withError(this._error)
496 } else if (!this._promises) {
497 this._promises = [promise]
499 this._promises.push(promise)
504 * Utility function used for creating a node-style resolver
507 * @param {!Promise} deferred a promise that looks like a deferred
508 * @param {Error=} err an optional error
509 * @param {*=} data optional data
511 function resolver(deferred, err, data) {
512 if (err) deferred.reject(err)
513 else deferred.resolve(data)
517 * Creates a node-style resolver for a deferred by wrapping
520 * @return {function(?Error, *)} node-style callback
522 Promise.prototype.makeNodeResolver = function () {
523 return resolver.bind(null, this)
527 * Return true iff the given object is a promise of this library.
529 * Because kew's API is slightly different than other promise libraries,
530 * it's important that we have a test for its promise type. If you want
531 * to test for a more general A+ promise, you should do a cap test for
532 * the features you want.
534 * @param {*} obj The object to test
535 * @return {boolean} Whether the object is a promise
537 function isPromise(obj) {
538 return !!obj._isPromise
542 * Return true iff the given object is a promise-like object, e.g. appears to
543 * implement Promises/A+ specification
545 * @param {*} obj The object to test
546 * @return {boolean} Whether the object is a promise-like object
548 function isPromiseLike(obj) {
549 return (typeof obj === 'object' || typeof obj === 'function') &&
550 typeof obj.then === 'function'
554 * Static function which creates and resolves a promise immediately
556 * @param {T} data data to resolve the promise with
557 * @return {!Promise.<T>}
560 function resolve(data) {
561 var promise = new Promise()
562 promise.resolve(data)
567 * Static function which creates and rejects a promise immediately
569 * @param {!Error} e error to reject the promise with
573 var promise = new Promise()
579 * Replace an element in an array with a new value. Used by .all() to
582 * @param {!Array} arr
583 * @param {number} idx
585 * @return {*} the val that's being injected into the array
587 function replaceEl(arr, idx, val) {
593 * Replace an element in an array as it is resolved with its value.
594 * Used by .allSettled().
596 * @param {!Array} arr
597 * @param {number} idx
598 * @param {*} value The value from a resolved promise.
599 * @return {*} the data that's being passed in
601 function replaceElFulfilled(arr, idx, value) {
610 * Replace an element in an array as it is rejected with the reason.
611 * Used by .allSettled().
613 * @param {!Array} arr
614 * @param {number} idx
615 * @param {*} reason The reason why the original promise is rejected
616 * @return {*} the data that's being passed in
618 function replaceElRejected(arr, idx, reason) {
627 * Takes in an array of promises or literals and returns a promise which returns
628 * an array of values when all have resolved. If any fail, the promise fails.
630 * @param {!Array.<!Promise>} promises
631 * @return {!Promise.<!Array>}
633 function all(promises) {
634 if (arguments.length != 1 || !Array.isArray(promises)) {
635 promises = Array.prototype.slice.call(arguments, 0)
637 return allInternal(promises)
641 * A version of all() that does not accept var_args
643 * @param {!Array.<!Promise>} promises
644 * @return {!Promise.<!Array>}
646 function allInternal(promises) {
647 if (!promises.length) return resolve([])
651 var promise = new Promise()
652 var counter = promises.length
654 for (var i = 0; i < promises.length; i += 1) {
655 if (!promises[i] || !isPromiseLike(promises[i])) {
656 outputs[i] = promises[i]
659 promises[i].then(replaceEl.bind(null, outputs, i))
660 .then(function decrementAllCounter() {
662 if (!finished && counter === 0) {
664 promise.resolve(outputs)
666 }, function onAllError(e) {
675 if (counter === 0 && !finished) {
677 promise.resolve(outputs)
684 * Takes in an array of promises or values and returns a promise that is
685 * fulfilled with an array of state objects when all have resolved or
686 * rejected. If a promise is resolved, its corresponding state object is
687 * {state: 'fulfilled', value: Object}; whereas if a promise is rejected, its
688 * corresponding state object is {state: 'rejected', reason: Object}.
690 * @param {!Array} promises or values
691 * @return {!Promise.<!Array>} Promise fulfilled with state objects for each input
693 function allSettled(promises) {
694 if (!Array.isArray(promises)) {
695 throw Error('The input to "allSettled()" should be an array of Promise or values')
697 if (!promises.length) return resolve([])
700 var promise = new Promise()
701 var counter = promises.length
703 for (var i = 0; i < promises.length; i += 1) {
704 if (!promises[i] || !isPromiseLike(promises[i])) {
705 replaceElFulfilled(outputs, i, promises[i])
706 if ((--counter) === 0) promise.resolve(outputs)
709 .then(replaceElFulfilled.bind(null, outputs, i), replaceElRejected.bind(null, outputs, i))
711 if ((--counter) === 0) promise.resolve(outputs)
720 * Takes an array of results and spreads them to the arguments of a function.
721 * @param {!Array} array
722 * @param {!Function} fn
724 function spread(array, fn) {
725 resolve(array).spread(fn)
729 * Create a new Promise which looks like a deferred
738 * Return a promise which will wait a specified number of ms to resolve
740 * @param {*} delayMsOrVal A delay (in ms) if this takes one argument, or ther
741 * return value if it takes two.
742 * @param {number=} opt_delayMs
745 function delay(delayMsOrVal, opt_delayMs) {
746 var returnVal = undefined
747 var delayMs = delayMsOrVal
748 if (typeof opt_delayMs != 'undefined') {
749 delayMs = opt_delayMs
750 returnVal = delayMsOrVal
753 if (typeof delayMs != 'number') {
754 throw new Error('Bad delay value ' + delayMs)
757 var defer = new Promise()
758 setTimeout(function onDelay() {
759 defer.resolve(returnVal)
765 * Returns a promise that has the same result as `this`, but fulfilled
766 * after at least ms milliseconds
769 Promise.prototype.delay = function (ms) {
770 return this.then(function (val) {
771 return delay(val, ms)
776 * Return a promise which will evaluate the function fn in a future turn with
779 * @param {function(...)} fn
780 * @param {...*} var_args a variable number of arguments
783 function fcall(fn, var_args) {
784 var rootArgs = Array.prototype.slice.call(arguments, 1)
785 var defer = new Promise()
786 nextTick(nextTickCallback.bind(null, defer, fn, undefined, rootArgs))
792 * Returns a promise that will be invoked with the result of a node style
793 * callback. All args to fn should be given except for the final callback arg
795 * @param {function(...)} fn
796 * @param {...*} var_args a variable number of arguments
799 function nfcall(fn, var_args) {
800 // Insert an undefined argument for scope and let bindPromise() do the work.
801 var args = Array.prototype.slice.call(arguments, 0)
802 args.splice(1, 0, undefined)
803 return ncall.apply(undefined, args)
808 * Like `nfcall`, but permits passing a `this` context for the call.
810 * @param {function(...)} fn
811 * @param {Object} scope
812 * @param {...*} var_args
815 function ncall(fn, scope, var_args) {
816 return bindPromise.apply(null, arguments)()
821 * Binds a function to a scope with an optional number of curried arguments. Attaches
822 * a node style callback as the last argument and returns a promise
824 * @param {function(...)} fn
825 * @param {Object} scope
826 * @param {...*} var_args a variable number of arguments
827 * @return {function(...)}: !Promise}
829 function bindPromise(fn, scope, var_args) {
830 var rootArgs = Array.prototype.slice.call(arguments, 2)
831 return function onBoundPromise(var_args) {
832 var defer = new Promise()
834 fn.apply(scope, rootArgs.concat(Array.prototype.slice.call(arguments, 0), defer.makeNodeResolver()))
844 bindPromise: bindPromise,
848 isPromise: isPromise,
849 isPromiseLike: isPromiseLike,
856 allSettled: allSettled,
858 getNextTickFunction: getNextTickFunction,
859 setNextTickFunction: setNextTickFunction,