Version 1
[yaffs-website] / node_modules / kew / kew.js
1
2 /**
3  * An object representing a "promise" for a future value
4  *
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
9  * @constructor
10  * @template T
11  */
12 function Promise(onSuccess, onFail) {
13   this.promise = this
14   this._isPromise = true
15   this._successFn = onSuccess
16   this._failFn = onFail
17   this._scope = this
18   this._boundArgs = null
19   this._hasContext = false
20   this._nextContext = undefined
21   this._currentContext = undefined
22 }
23
24 /**
25  * @param {function()} callback
26  */
27 function nextTick (callback) {
28   callback()
29 }
30
31 if (typeof process !== 'undefined' && typeof process.nextTick === 'function') {
32   nextTick = process.nextTick
33 }
34
35 /**
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.
39  *
40  * @param {Promise} defer
41  * @param {Function} callback
42  * @param {Object|undefined} scope
43  * @param {Array} args
44  */
45 function nextTickCallback (defer, callback, scope, args) {
46   try {
47     defer.resolve(callback.apply(scope, args))
48   } catch (thrown) {
49     defer.reject(thrown)
50   }
51 }
52
53 /**
54  * Used for accessing the nextTick function from outside the kew module.
55  *
56  * @return {Function}
57  */
58 function getNextTickFunction () {
59   return nextTick
60 }
61
62 /**
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
66  */
67 function setNextTickFunction (fn) {
68   nextTick = fn
69 }
70
71 /**
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.
75  * @constructor
76  */
77 function PromiseStats() {
78   /** @type {number} */
79   this.errorsEmitted = 0
80
81   /** @type {number} */
82   this.errorsHandled = 0
83 }
84
85 var stats = new PromiseStats()
86
87 Promise.prototype._handleError = function () {
88   if (!this._errorHandled) {
89     stats.errorsHandled++
90     this._errorHandled = true
91   }
92 }
93
94 /**
95  * Specify that the current promise should have a specified context
96  * @param  {*} context context
97  * @private
98  */
99 Promise.prototype._useContext = function (context) {
100   this._nextContext = this._currentContext = context
101   this._hasContext = true
102   return this
103 }
104
105 Promise.prototype.clearContext = function () {
106   this._hasContext = false
107   this._nextContext = undefined
108   return this
109 }
110
111 /**
112  * Set the context for all promise handlers to follow
113  *
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.
117  *
118  * @param {*} context An arbitrary context
119  */
120 Promise.prototype.setContext = function (context) {
121   this._nextContext = context
122   this._hasContext = true
123   return this
124 }
125
126 /**
127  * Get the context for a promise
128  * @return {*} the context set by setContext
129  */
130 Promise.prototype.getContext = function () {
131   return this._nextContext
132 }
133
134 /**
135  * Resolve this promise with a specified value
136  *
137  * @param {*=} data
138  */
139 Promise.prototype.resolve = function (data) {
140   if (this._error || this._hasData) throw new Error("Unable to resolve or reject the same promise twice")
141
142   var i
143   if (data && isPromise(data)) {
144     this._child = data
145     if (this._promises) {
146       for (i = 0; i < this._promises.length; i += 1) {
147         data._chainPromise(this._promises[i])
148       }
149       delete this._promises
150     }
151
152     if (this._onComplete) {
153       for (i = 0; i < this._onComplete.length; i+= 1) {
154         data.fin(this._onComplete[i])
155       }
156       delete this._onComplete
157     }
158   } else if (data && isPromiseLike(data)) {
159     data.then(
160       function(data) { this.resolve(data) }.bind(this),
161       function(err) { this.reject(err) }.bind(this)
162     )
163   } else {
164     this._hasData = true
165     this._data = data
166
167     if (this._onComplete) {
168       for (i = 0; i < this._onComplete.length; i++) {
169         this._onComplete[i]()
170       }
171     }
172
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)
177       }
178       delete this._promises
179     }
180   }
181 }
182
183 /**
184  * Reject this promise with an error
185  *
186  * @param {!Error} e
187  */
188 Promise.prototype.reject = function (e) {
189   if (this._error || this._hasData) throw new Error("Unable to resolve or reject the same promise twice")
190
191   var i
192   this._error = e
193   stats.errorsEmitted++
194
195   if (this._ended) {
196     this._handleError()
197     process.nextTick(function onPromiseThrow() {
198       throw e
199     })
200   }
201
202   if (this._onComplete) {
203     for (i = 0; i < this._onComplete.length; i++) {
204       this._onComplete[i]()
205     }
206   }
207
208   if (this._promises) {
209     this._handleError()
210     for (i = 0; i < this._promises.length; i += 1) {
211       this._promises[i]._useContext(this._nextContext)
212       this._promises[i]._withError(e)
213     }
214     delete this._promises
215   }
216 }
217
218 /**
219  * Provide a callback to be called whenever this promise successfully
220  * resolves. Allows for an optional second callback to handle the failure
221  * case.
222  *
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
226  *     onFail handler
227  * @template RESULT
228  */
229 Promise.prototype.then = function (onSuccess, onFail) {
230   var promise = new Promise(onSuccess, onFail)
231   if (this._nextContext) promise._useContext(this._nextContext)
232
233   if (this._child) this._child._chainPromise(promise)
234   else this._chainPromise(promise)
235
236   return promise
237 }
238
239 /**
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.
242  *
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
248  */
249 Promise.prototype.thenBound = function (onSuccess, scope, var_args) {
250   var promise = new Promise(onSuccess)
251   if (this._nextContext) promise._useContext(this._nextContext)
252
253   promise._scope = scope
254   if (arguments.length > 2) {
255     promise._boundArgs = Array.prototype.slice.call(arguments, 2)
256   }
257
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)
261
262   return promise
263 }
264
265 /**
266  * Provide a callback to be called whenever this promise is rejected
267  *
268  * @param {function(this:void, !Error, ?)} onFail
269  * @return {!Promise.<T>} returns a new promise with the output of the onFail handler
270  */
271 Promise.prototype.fail = function (onFail) {
272   return this.then(null, onFail)
273 }
274
275 /**
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.
278  *
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
283  * @template SCOPE
284  */
285 Promise.prototype.failBound = function (onFail, scope, var_args) {
286   var promise = new Promise(null, onFail)
287   if (this._nextContext) promise._useContext(this._nextContext)
288
289   promise._scope = scope
290   if (arguments.length > 2) {
291     promise._boundArgs = Array.prototype.slice.call(arguments, 2)
292   }
293
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)
297
298   return promise
299 }
300
301 /**
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
305  *     onFail handler
306  * @template RESULT
307  */
308 Promise.prototype.spread = function (onSuccess) {
309   return this.then(allInternal)
310   .then(function (array) {
311     return onSuccess.apply(null, array)
312   })
313 }
314
315 /**
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
322  */
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))
328   })
329 }
330
331 /**
332  * Provide a callback to be called whenever this promise is either resolved
333  * or rejected.
334  *
335  * @param {function()} onComplete
336  * @return {!Promise.<T>} returns the current promise
337  */
338 Promise.prototype.fin = function (onComplete) {
339   if (this._hasData || this._error) {
340     onComplete()
341     return this
342   }
343
344   if (this._child) {
345     this._child.fin(onComplete)
346   } else {
347     if (!this._onComplete) this._onComplete = [onComplete]
348     else this._onComplete.push(onComplete)
349   }
350
351   return this
352 }
353
354 /**
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
357  *
358  * @return {!Promise.<T>} returns the current promise
359  * @deprecated Prefer done(), because it's consistent with Q.
360  */
361 Promise.prototype.end = function () {
362   this._end()
363   return this
364 }
365
366
367 /**
368  * Mark this promise as "ended".
369  * @private
370  */
371 Promise.prototype._end = function () {
372   if (this._error) {
373     this._handleError()
374     throw this._error
375   }
376   this._ended = true
377   return this
378 }
379
380 /**
381  * Close the promise. Any errors after this completes will be thrown to the global handler.
382  *
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
387  * @return {void}
388  */
389 Promise.prototype.done = function (onSuccess, onFailure) {
390   var self = this
391   if (onSuccess || onFailure) {
392     self = self.then(onSuccess, onFailure)
393   }
394   self._end()
395 }
396
397 /**
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.
401  *
402  * @param {number} timeoutMs The timeout threshold in msec
403  * @param {string=} timeoutMsg error message
404  * @return {!Promise.<T>} a new promise with timeout
405  */
406  Promise.prototype.timeout = function (timeoutMs, timeoutMsg) {
407   var deferred = new Promise()
408   var isTimeout = false
409
410   var timeout = setTimeout(function() {
411     deferred.reject(new Error(timeoutMsg || 'Promise timeout after ' + timeoutMs + ' ms.'))
412     isTimeout = true
413   }, timeoutMs)
414
415   this.then(function (data) {
416     if (!isTimeout) {
417       clearTimeout(timeout)
418       deferred.resolve(data)
419     }
420   },
421   function (err) {
422     if (!isTimeout) {
423       clearTimeout(timeout)
424       deferred.reject(err)
425     }
426   })
427
428   return deferred.promise
429 }
430
431 /**
432  * Attempt to resolve this promise with the specified input
433  *
434  * @param {*} data the input
435  */
436 Promise.prototype._withInput = function (data) {
437   if (this._successFn) {
438     this._nextTick(this._successFn, [data, this._currentContext])
439   } else {
440     this.resolve(data)
441   }
442
443   // context is no longer needed
444   delete this._currentContext
445 }
446
447 /**
448  * Attempt to reject this promise with the specified error
449  *
450  * @param {!Error} e
451  * @private
452  */
453 Promise.prototype._withError = function (e) {
454   if (this._failFn) {
455     this._nextTick(this._failFn, [e, this._currentContext])
456   } else {
457     this.reject(e)
458   }
459
460   // context is no longer needed
461   delete this._currentContext
462 }
463
464 /**
465  * Calls a function in the correct scope, and includes bound arguments.
466  * @param {Function} fn
467  * @param {Array} args
468  * @private
469  */
470 Promise.prototype._nextTick = function (fn, args) {
471   if (this._boundArgs) {
472     args = this._boundArgs.concat(args)
473   }
474   nextTick(nextTickCallback.bind(null, this, fn, this._scope, args))
475 }
476
477 /**
478  * Chain a promise to the current promise
479  *
480  * @param {!Promise} promise the promise to chain
481  * @private
482  */
483 Promise.prototype._chainPromise = function (promise) {
484   var i
485   if (this._hasContext) promise._useContext(this._nextContext)
486
487   if (this._child) {
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
494     this._handleError()
495     promise._withError(this._error)
496   } else if (!this._promises) {
497     this._promises = [promise]
498   } else {
499     this._promises.push(promise)
500   }
501 }
502
503 /**
504  * Utility function used for creating a node-style resolver
505  * for deferreds
506  *
507  * @param {!Promise} deferred a promise that looks like a deferred
508  * @param {Error=} err an optional error
509  * @param {*=} data optional data
510  */
511 function resolver(deferred, err, data) {
512   if (err) deferred.reject(err)
513   else deferred.resolve(data)
514 }
515
516 /**
517  * Creates a node-style resolver for a deferred by wrapping
518  * resolver()
519  *
520  * @return {function(?Error, *)} node-style callback
521  */
522 Promise.prototype.makeNodeResolver = function () {
523   return resolver.bind(null, this)
524 }
525
526 /**
527  * Return true iff the given object is a promise of this library.
528  *
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.
533  *
534  * @param {*} obj The object to test
535  * @return {boolean} Whether the object is a promise
536  */
537 function isPromise(obj) {
538   return !!obj._isPromise
539 }
540
541 /**
542  * Return true iff the given object is a promise-like object, e.g. appears to
543  * implement Promises/A+ specification
544  *
545  * @param {*} obj The object to test
546  * @return {boolean} Whether the object is a promise-like object
547  */
548 function isPromiseLike(obj) {
549   return (typeof obj === 'object' || typeof obj === 'function') &&
550     typeof obj.then === 'function'
551 }
552
553 /**
554  * Static function which creates and resolves a promise immediately
555  *
556  * @param {T} data data to resolve the promise with
557  * @return {!Promise.<T>}
558  * @template T
559  */
560 function resolve(data) {
561   var promise = new Promise()
562   promise.resolve(data)
563   return promise
564 }
565
566 /**
567  * Static function which creates and rejects a promise immediately
568  *
569  * @param {!Error} e error to reject the promise with
570  * @return {!Promise}
571  */
572 function reject(e) {
573   var promise = new Promise()
574   promise.reject(e)
575   return promise
576 }
577
578 /**
579  * Replace an element in an array with a new value. Used by .all() to
580  * call from .then()
581  *
582  * @param {!Array} arr
583  * @param {number} idx
584  * @param {*} val
585  * @return {*} the val that's being injected into the array
586  */
587 function replaceEl(arr, idx, val) {
588   arr[idx] = val
589   return val
590 }
591
592 /**
593  * Replace an element in an array as it is resolved with its value.
594  * Used by .allSettled().
595  *
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
600  */
601 function replaceElFulfilled(arr, idx, value) {
602   arr[idx] = {
603     state: 'fulfilled',
604     value: value
605   }
606   return value
607 }
608
609 /**
610  * Replace an element in an array as it is rejected with the reason.
611  * Used by .allSettled().
612  *
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
617  */
618 function replaceElRejected(arr, idx, reason) {
619   arr[idx] = {
620     state: 'rejected',
621     reason: reason
622   }
623   return reason
624 }
625
626 /**
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.
629  *
630  * @param {!Array.<!Promise>} promises
631  * @return {!Promise.<!Array>}
632  */
633 function all(promises) {
634   if (arguments.length != 1 || !Array.isArray(promises)) {
635     promises = Array.prototype.slice.call(arguments, 0)
636   }
637   return allInternal(promises)
638 }
639
640 /**
641  * A version of all() that does not accept var_args
642  *
643  * @param {!Array.<!Promise>} promises
644  * @return {!Promise.<!Array>}
645  */
646 function allInternal(promises) {
647   if (!promises.length) return resolve([])
648
649   var outputs = []
650   var finished = false
651   var promise = new Promise()
652   var counter = promises.length
653
654   for (var i = 0; i < promises.length; i += 1) {
655     if (!promises[i] || !isPromiseLike(promises[i])) {
656       outputs[i] = promises[i]
657       counter -= 1
658     } else {
659       promises[i].then(replaceEl.bind(null, outputs, i))
660       .then(function decrementAllCounter() {
661         counter--
662         if (!finished && counter === 0) {
663           finished = true
664           promise.resolve(outputs)
665         }
666       }, function onAllError(e) {
667         if (!finished) {
668           finished = true
669           promise.reject(e)
670         }
671       })
672     }
673   }
674
675   if (counter === 0 && !finished) {
676     finished = true
677     promise.resolve(outputs)
678   }
679
680   return promise
681 }
682
683 /**
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}.
689  *
690  * @param {!Array} promises or values
691  * @return {!Promise.<!Array>} Promise fulfilled with state objects for each input
692  */
693 function allSettled(promises) {
694   if (!Array.isArray(promises)) {
695     throw Error('The input to "allSettled()" should be an array of Promise or values')
696   }
697   if (!promises.length) return resolve([])
698
699   var outputs = []
700   var promise = new Promise()
701   var counter = promises.length
702
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)
707     } else {
708       promises[i]
709         .then(replaceElFulfilled.bind(null, outputs, i), replaceElRejected.bind(null, outputs, i))
710         .then(function () {
711           if ((--counter) === 0) promise.resolve(outputs)
712         })
713     }
714   }
715
716   return promise
717 }
718
719 /**
720  * Takes an array of results and spreads them to the arguments of a function.
721  * @param {!Array} array
722  * @param {!Function} fn
723  */
724 function spread(array, fn) {
725   resolve(array).spread(fn)
726 }
727
728 /**
729  * Create a new Promise which looks like a deferred
730  *
731  * @return {!Promise}
732  */
733 function defer() {
734   return new Promise()
735 }
736
737 /**
738  * Return a promise which will wait a specified number of ms to resolve
739  *
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
743  * @return {!Promise}
744  */
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
751   }
752
753   if (typeof delayMs != 'number') {
754     throw new Error('Bad delay value ' + delayMs)
755   }
756
757   var defer = new Promise()
758   setTimeout(function onDelay() {
759     defer.resolve(returnVal)
760   }, delayMs)
761   return defer
762 }
763
764 /**
765  * Returns a promise that has the same result as `this`, but fulfilled
766  * after at least ms milliseconds
767  * @param {number} ms
768  */
769 Promise.prototype.delay = function (ms) {
770   return this.then(function (val) {
771     return delay(val, ms)
772   })
773 }
774
775 /**
776  * Return a promise which will evaluate the function fn in a future turn with
777  * the provided args
778  *
779  * @param {function(...)} fn
780  * @param {...*} var_args a variable number of arguments
781  * @return {!Promise}
782  */
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))
787   return defer
788 }
789
790
791 /**
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
794  *
795  * @param {function(...)} fn
796  * @param {...*} var_args a variable number of arguments
797  * @return {!Promise}
798  */
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)
804 }
805
806
807 /**
808  * Like `nfcall`, but permits passing a `this` context for the call.
809  *
810  * @param {function(...)} fn
811  * @param {Object} scope
812  * @param {...*} var_args
813  * @return {!Promise}
814  */
815 function ncall(fn, scope, var_args) {
816   return bindPromise.apply(null, arguments)()
817 }
818
819
820 /**
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
823  *
824  * @param {function(...)} fn
825  * @param {Object} scope
826  * @param {...*} var_args a variable number of arguments
827  * @return {function(...)}: !Promise}
828  */
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()
833     try {
834       fn.apply(scope, rootArgs.concat(Array.prototype.slice.call(arguments, 0), defer.makeNodeResolver()))
835     } catch (e) {
836       defer.reject(e)
837     }
838     return defer
839   }
840 }
841
842 module.exports = {
843   all: all,
844   bindPromise: bindPromise,
845   defer: defer,
846   delay: delay,
847   fcall: fcall,
848   isPromise: isPromise,
849   isPromiseLike: isPromiseLike,
850   ncall: ncall,
851   nfcall: nfcall,
852   resolve: resolve,
853   reject: reject,
854   spread: spread,
855   stats: stats,
856   allSettled: allSettled,
857   Promise: Promise,
858   getNextTickFunction: getNextTickFunction,
859   setNextTickFunction: setNextTickFunction,
860 }