UNPKG

20.2 kBJavaScriptView Raw
1/**
2 * This is the minimal implementation of Yaku.
3 * No extra helper methods.
4 */
5
6(function () {
7 "use strict";
8
9 var $undefined
10 , $null = null
11 , isBrowser = typeof window === "object"
12 , root = isBrowser ? window : global
13 , process = root.process
14 , Arr = Array
15
16 , $rejected = 0
17 , $resolved = 1
18 , $pending = 2
19
20 , $Symbol = "Symbol"
21 , $iterator = "iterator"
22 , $species = "species"
23 , $speciesKey = $Symbol + "(" + $species + ")"
24 , $return = "return"
25
26 , $unhandled = "_uh"
27
28 , $invalidThis = "Invalid this"
29 , $invalidArgument = "Invalid argument"
30 , $promiseCircularChain = "Chaining cycle detected for promise"
31 , $rejectionHandled = "rejectionHandled"
32 , $unhandledRejection = "unhandledRejection"
33
34 , $tryCatchFn
35 , $tryCatchThis
36 , $tryErr = { e: $null }
37 , $noop = function () {}
38
39 , Symbol = root[$Symbol] || {}
40 , nextTick = isBrowser ?
41 function (fn) { setTimeout(fn); } :
42 process.nextTick
43 , speciesConstructor = function (O, D) {
44 var C = O.constructor;
45
46 return C ? (C[getSpecies()] || D) : D;
47 }
48 ;
49
50 /**
51 * This class follows the [Promises/A+](https://promisesaplus.com) and
52 * [ES6](http://people.mozilla.org/~jorendorff/es6-draft.html#sec-promise-objects) spec
53 * with some extra helpers.
54 * @param {Function} executor Function object with two arguments resolve, reject.
55 * The first argument fulfills the promise, the second argument rejects it.
56 * We can call these functions, once our operation is completed.
57 */
58 var Yaku = function Promise (executor) {
59 var self = this,
60 err;
61
62 // "this._s" is the internal state of: pending, resolved or rejected
63 // "this._v" is the internal value
64
65 if (!isObject(self) || self._s !== $undefined)
66 throw genTypeError($invalidThis);
67
68 self._s = $pending;
69
70 if (executor !== $noop) {
71 if (!isFunction(executor))
72 throw genTypeError($invalidArgument);
73
74 err = genTryCatcher(executor)(
75 genSettler(self, $resolved),
76 genSettler(self, $rejected)
77 );
78
79 if (err === $tryErr)
80 settlePromise(self, $rejected, err.e);
81 }
82 };
83
84 Yaku["default"] = Yaku;
85
86 extendPrototype(Yaku, {
87 /**
88 * Appends fulfillment and rejection handlers to the promise,
89 * and returns a new promise resolving to the return value of the called handler.
90 * @param {Function} onFulfilled Optional. Called when the Promise is resolved.
91 * @param {Function} onRejected Optional. Called when the Promise is rejected.
92 * @return {Yaku} It will return a new Yaku which will resolve or reject after
93 * @example
94 * the current Promise.
95 * ```js
96 * var Promise = require('yaku');
97 * var p = Promise.resolve(10);
98 *
99 * p.then((v) => {
100 * console.log(v);
101 * });
102 * ```
103 */
104 then: function then (onFulfilled, onRejected) {
105 if (this._s === undefined) throw genTypeError();
106
107 return addHandler(
108 this,
109 newCapablePromise(speciesConstructor(this, Yaku)),
110 onFulfilled,
111 onRejected
112 );
113 },
114
115 /**
116 * The `catch()` method returns a Promise and deals with rejected cases only.
117 * It behaves the same as calling `Promise.prototype.then(undefined, onRejected)`.
118 * @param {Function} onRejected A Function called when the Promise is rejected.
119 * This function has one argument, the rejection reason.
120 * @return {Yaku} A Promise that deals with rejected cases only.
121 * @example
122 * ```js
123 * var Promise = require('yaku');
124 * var p = Promise.reject(new Error("ERR"));
125 *
126 * p['catch']((v) => {
127 * console.log(v);
128 * });
129 * ```
130 */
131 "catch": function (onRejected) {
132 return this.then($undefined, onRejected);
133 },
134
135 /**
136 * Register a callback to be invoked when a promise is settled (either fulfilled or rejected).
137 * Similar with the try-catch-finally, it's often used for cleanup.
138 * @param {Function} onFinally A Function called when the Promise is settled.
139 * It will not receive any argument.
140 * @return {Yaku} A Promise that will reject if onFinally throws an error or returns a rejected promise.
141 * Else it will resolve previous promise's final state (either fulfilled or rejected).
142 * @example
143 * ```js
144 * var Promise = require('yaku');
145 * var p = Promise.reject(new Error("ERR"));
146 *
147 * p['catch']((v) => {
148 * console.log(v);
149 * });
150 * ```
151 */
152 "finally": function (onFinally) {
153 function eventually (value) {
154 return Yaku.resolve(onFinally()).then(function () {
155 return value;
156 });
157 }
158
159 return this.then(eventually, eventually);
160 },
161
162 // The number of current promises that attach to this Yaku instance.
163 _pCount: 0,
164
165 // The parent Yaku.
166 _pre: $null,
167
168 // A unique type flag, it helps different versions of Yaku know each other.
169 _Yaku: 1
170 });
171
172 /**
173 * The `Promise.resolve(value)` method returns a Promise object that is resolved with the given value.
174 * If the value is a thenable (i.e. has a then method), the returned promise will "follow" that thenable,
175 * adopting its eventual state; otherwise the returned promise will be fulfilled with the value.
176 * @param {Any} value Argument to be resolved by this Promise.
177 * Can also be a Promise or a thenable to resolve.
178 * @return {Yaku}
179 * @example
180 * ```js
181 * var Promise = require('yaku');
182 * var p = Promise.resolve(10);
183 * ```
184 */
185 Yaku.resolve = function resolve (val) {
186 return isYaku(val) ? val : settleWithX(newCapablePromise(this), val);
187 };
188
189 /**
190 * The `Promise.reject(reason)` method returns a Promise object that is rejected with the given reason.
191 * @param {Any} reason Reason why this Promise rejected.
192 * @return {Yaku}
193 * @example
194 * ```js
195 * var Promise = require('yaku');
196 * var p = Promise.reject(new Error("ERR"));
197 * ```
198 */
199 Yaku.reject = function reject (reason) {
200 return settlePromise(newCapablePromise(this), $rejected, reason);
201 };
202
203 /**
204 * The `Promise.race(iterable)` method returns a promise that resolves or rejects
205 * as soon as one of the promises in the iterable resolves or rejects,
206 * with the value or reason from that promise.
207 * @param {iterable} iterable An iterable object, such as an Array.
208 * @return {Yaku} The race function returns a Promise that is settled
209 * the same way as the first passed promise to settle.
210 * It resolves or rejects, whichever happens first.
211 * @example
212 * ```js
213 * var Promise = require('yaku');
214 * Promise.race([
215 * 123,
216 * Promise.resolve(0)
217 * ])
218 * .then((value) => {
219 * console.log(value); // => 123
220 * });
221 * ```
222 */
223 Yaku.race = function race (iterable) {
224 var self = this
225 , p = newCapablePromise(self)
226
227 , resolve = function (val) {
228 settlePromise(p, $resolved, val);
229 }
230
231 , reject = function (val) {
232 settlePromise(p, $rejected, val);
233 }
234
235 , ret = genTryCatcher(each)(iterable, function (v) {
236 self.resolve(v).then(resolve, reject);
237 });
238
239 if (ret === $tryErr) return self.reject(ret.e);
240
241 return p;
242 };
243
244 /**
245 * The `Promise.all(iterable)` method returns a promise that resolves when
246 * all of the promises in the iterable argument have resolved.
247 *
248 * The result is passed as an array of values from all the promises.
249 * If something passed in the iterable array is not a promise,
250 * it's converted to one by Promise.resolve. If any of the passed in promises rejects,
251 * the all Promise immediately rejects with the value of the promise that rejected,
252 * discarding all the other promises whether or not they have resolved.
253 * @param {iterable} iterable An iterable object, such as an Array.
254 * @return {Yaku}
255 * @example
256 * ```js
257 * var Promise = require('yaku');
258 * Promise.all([
259 * 123,
260 * Promise.resolve(0)
261 * ])
262 * .then((values) => {
263 * console.log(values); // => [123, 0]
264 * });
265 * ```
266 * @example
267 * Use with iterable.
268 * ```js
269 * var Promise = require('yaku');
270 * Promise.all((function * () {
271 * yield 10;
272 * yield new Promise(function (r) { setTimeout(r, 1000, "OK") });
273 * })())
274 * .then((values) => {
275 * console.log(values); // => [123, 0]
276 * });
277 * ```
278 */
279 Yaku.all = function all (iterable) {
280 var self = this
281 , p1 = newCapablePromise(self)
282 , res = []
283 , ret
284 ;
285
286 function reject (reason) {
287 settlePromise(p1, $rejected, reason);
288 }
289
290 ret = genTryCatcher(each)(iterable, function (item, i) {
291 self.resolve(item).then(function (value) {
292 res[i] = value;
293 if (!--ret) settlePromise(p1, $resolved, res);
294 }, reject);
295 });
296
297 if (ret === $tryErr) return self.reject(ret.e);
298
299 if (!ret) settlePromise(p1, $resolved, []);
300
301 return p1;
302 };
303
304 // To support browsers that don't support `Object.defineProperty`.
305 genTryCatcher(function () {
306 Object.defineProperty(Yaku, getSpecies(), {
307 get: function () { return this; }
308 });
309 })();
310
311 // ********************** Private **********************
312
313 Yaku._Yaku = 1;
314
315 /**
316 * All static variable name will begin with `$`. Such as `$rejected`.
317 * @private
318 */
319
320 // ******************************* Utils ********************************
321
322 function getSpecies () {
323 return Symbol[$species] || $speciesKey;
324 }
325
326 function extendPrototype (src, target) {
327 for (var k in target) {
328 src.prototype[k] = target[k];
329 }
330 return src;
331 }
332
333 function isObject (obj) {
334 return obj && typeof obj === "object";
335 }
336
337 function isFunction (obj) {
338 return typeof obj === "function";
339 }
340
341 function isInstanceOf (a, b) {
342 return a instanceof b;
343 }
344
345 function ensureType (obj, fn, msg) {
346 if (!fn(obj)) throw genTypeError(msg);
347 }
348
349 /**
350 * Wrap a function into a try-catch.
351 * @private
352 * @return {Any | $tryErr}
353 */
354 function tryCatcher () {
355 try {
356 return $tryCatchFn.apply($tryCatchThis, arguments);
357 } catch (e) {
358 $tryErr.e = e;
359 return $tryErr;
360 }
361 }
362
363 /**
364 * Generate a try-catch wrapped function.
365 * @private
366 * @param {Function} fn
367 * @return {Function}
368 */
369 function genTryCatcher (fn, self) {
370 $tryCatchFn = fn;
371 $tryCatchThis = self;
372 return tryCatcher;
373 }
374
375 /**
376 * Generate a scheduler.
377 * @private
378 * @param {Integer} initQueueSize
379 * @param {Function} fn `(Yaku, Value) ->` The schedule handler.
380 * @return {Function} `(Yaku, Value) ->` The scheduler.
381 */
382 function genScheduler (initQueueSize, fn) {
383 /**
384 * All async promise will be scheduled in
385 * here, so that they can be execute on the next tick.
386 * @private
387 */
388 var fnQueue = Arr(initQueueSize)
389 , fnQueueLen = 0;
390
391 /**
392 * Run all queued functions.
393 * @private
394 */
395 function flush () {
396 var i = 0;
397 while (i < fnQueueLen) {
398 fn(fnQueue[i], fnQueue[i + 1]);
399 fnQueue[i++] = $undefined;
400 fnQueue[i++] = $undefined;
401 }
402
403 fnQueueLen = 0;
404 if (fnQueue.length > initQueueSize) fnQueue.length = initQueueSize;
405 }
406
407 return function (v, arg) {
408 fnQueue[fnQueueLen++] = v;
409 fnQueue[fnQueueLen++] = arg;
410
411 if (fnQueueLen === 2) nextTick(flush);
412 };
413 }
414
415 /**
416 * Generate a iterator
417 * @param {Any} obj
418 * @private
419 * @return {Object || TypeError}
420 */
421 function each (iterable, fn) {
422 var len
423 , i = 0
424 , iter
425 , item
426 , ret
427 ;
428
429 if (!iterable) throw genTypeError($invalidArgument);
430
431 var gen = iterable[Symbol[$iterator]];
432 if (isFunction(gen))
433 iter = gen.call(iterable);
434 else if (isFunction(iterable.next)) {
435 iter = iterable;
436 }
437 else if (isInstanceOf(iterable, Arr)) {
438 len = iterable.length;
439 while (i < len) {
440 fn(iterable[i], i++);
441 }
442 return i;
443 } else
444 throw genTypeError($invalidArgument);
445
446 while (!(item = iter.next()).done) {
447 ret = genTryCatcher(fn)(item.value, i++);
448 if (ret === $tryErr) {
449 isFunction(iter[$return]) && iter[$return]();
450 throw ret.e;
451 }
452 }
453
454 return i;
455 }
456
457 /**
458 * Generate type error object.
459 * @private
460 * @param {String} msg
461 * @return {TypeError}
462 */
463 function genTypeError (msg) {
464 return new TypeError(msg);
465 }
466
467 // *************************** Promise Helpers ****************************
468
469 /**
470 * Resolve the value returned by onFulfilled or onRejected.
471 * @private
472 * @param {Yaku} p1
473 * @param {Yaku} p2
474 */
475 var scheduleHandler = genScheduler(999, function (p1, p2) {
476 var x, handler;
477
478 // 2.2.2
479 // 2.2.3
480 handler = p1._s ? p2._onFulfilled : p2._onRejected;
481
482 // 2.2.7.3
483 // 2.2.7.4
484 if (handler === $undefined) {
485 settlePromise(p2, p1._s, p1._v);
486 return;
487 }
488
489 // 2.2.7.1
490 x = genTryCatcher(callHanler)(handler, p1._v);
491 if (x === $tryErr) {
492 // 2.2.7.2
493 settlePromise(p2, $rejected, x.e);
494 return;
495 }
496
497 settleWithX(p2, x);
498 });
499
500 var scheduleUnhandledRejection = genScheduler(9, function (p) {
501 if (!hashOnRejected(p)) {
502 p[$unhandled] = 1;
503 emitEvent($unhandledRejection, p);
504 }
505 });
506
507 function emitEvent (name, p) {
508 var browserEventName = "on" + name.toLowerCase()
509 , browserHandler = root[browserEventName];
510
511 if (process && process.listeners(name).length)
512 name === $unhandledRejection ?
513 process.emit(name, p._v, p) : process.emit(name, p);
514 else if (browserHandler)
515 browserHandler({ reason: p._v, promise: p });
516 }
517
518 function isYaku (val) { return val && val._Yaku; }
519
520 function newCapablePromise (Constructor) {
521 if (isYaku(Constructor)) return new Constructor($noop);
522
523 var p, r, j;
524 p = new Constructor(function (resolve, reject) {
525 if (p) throw genTypeError();
526
527 r = resolve;
528 j = reject;
529 });
530
531 ensureType(r, isFunction);
532 ensureType(j, isFunction);
533
534 return p;
535 }
536
537 /**
538 * It will produce a settlePromise function to user.
539 * Such as the resolve and reject in this `new Yaku (resolve, reject) ->`.
540 * @private
541 * @param {Yaku} self
542 * @param {Integer} state The value is one of `$pending`, `$resolved` or `$rejected`.
543 * @return {Function} `(value) -> undefined` A resolve or reject function.
544 */
545 function genSettler (self, state) {
546 return function (value) {
547 if (state === $resolved)
548 settleWithX(self, value);
549 else
550 settlePromise(self, state, value);
551 };
552 }
553
554 /**
555 * Link the promise1 to the promise2.
556 * @private
557 * @param {Yaku} p1
558 * @param {Yaku} p2
559 * @param {Function} onFulfilled
560 * @param {Function} onRejected
561 */
562 function addHandler (p1, p2, onFulfilled, onRejected) {
563 // 2.2.1
564 if (isFunction(onFulfilled))
565 p2._onFulfilled = onFulfilled;
566 if (isFunction(onRejected)) {
567 if (p1[$unhandled]) emitEvent($rejectionHandled, p1);
568
569 p2._onRejected = onRejected;
570 }
571
572 p1[p1._pCount++] = p2;
573
574 // 2.2.6
575 if (p1._s !== $pending)
576 scheduleHandler(p1, p2);
577
578 // 2.2.7
579 return p2;
580 }
581
582 // iterate tree
583 function hashOnRejected (node) {
584 // A node shouldn't be checked twice.
585 if (node._umark)
586 return true;
587 else
588 node._umark = true;
589
590 var i = 0
591 , len = node._pCount
592 , child;
593
594 while (i < len) {
595 child = node[i++];
596 if (child._onRejected || hashOnRejected(child)) return true;
597 }
598 }
599
600 function callHanler (handler, value) {
601 // 2.2.5
602 return handler(value);
603 }
604
605 /**
606 * Resolve or reject a promise.
607 * @private
608 * @param {Yaku} p
609 * @param {Integer} state
610 * @param {Any} value
611 */
612 function settlePromise (p, state, value) {
613 var i = 0
614 , len = p._pCount;
615
616 // 2.1.2
617 // 2.1.3
618 if (p._s === $pending) {
619 // 2.1.1.1
620 p._s = state;
621 p._v = value;
622
623 if (state === $rejected) {
624 scheduleUnhandledRejection(p);
625 }
626
627 // 2.2.4
628 while (i < len) {
629 scheduleHandler(p, p[i++]);
630 }
631 }
632
633 return p;
634 }
635
636 /**
637 * Resolve or reject promise with value x. The x can also be a thenable.
638 * @private
639 * @param {Yaku} p
640 * @param {Any | Thenable} x A normal value or a thenable.
641 */
642 function settleWithX (p, x) {
643 // 2.3.1
644 if (x === p && x) {
645 settlePromise(p, $rejected, genTypeError($promiseCircularChain));
646 return p;
647 }
648
649 // 2.3.2
650 // 2.3.3
651 if (x !== $null && (isFunction(x) || isObject(x))) {
652 // 2.3.2.1
653 var xthen = genTryCatcher(getThen)(x);
654
655 if (xthen === $tryErr) {
656 // 2.3.3.2
657 settlePromise(p, $rejected, xthen.e);
658 return p;
659 }
660
661 if (isFunction(xthen)) {
662 // Fix https://bugs.chromium.org/p/v8/issues/detail?id=4162
663 if (isYaku(x))
664 settleXthen(p, x, xthen);
665 else
666 nextTick(function () {
667 settleXthen(p, x, xthen);
668 });
669 } else
670 // 2.3.3.4
671 settlePromise(p, $resolved, x);
672 } else
673 // 2.3.4
674 settlePromise(p, $resolved, x);
675
676 return p;
677 }
678
679 /**
680 * Try to get a promise's then method.
681 * @private
682 * @param {Thenable} x
683 * @return {Function}
684 */
685 function getThen (x) { return x.then; }
686
687 /**
688 * Resolve then with its promise.
689 * @private
690 * @param {Yaku} p
691 * @param {Thenable} x
692 * @param {Function} xthen
693 */
694 function settleXthen (p, x, xthen) {
695 // 2.3.3.3
696 var err = genTryCatcher(xthen, x)(function (y) {
697 // 2.3.3.3.3
698 // 2.3.3.3.1
699 x && (x = $null, settleWithX(p, y));
700 }, function (r) {
701 // 2.3.3.3.3
702 // 2.3.3.3.2
703 x && (x = $null, settlePromise(p, $rejected, r));
704 });
705
706 // 2.3.3.3.4.1
707 if (err === $tryErr && x) {
708 // 2.3.3.3.4.2
709 settlePromise(p, $rejected, err.e);
710 x = $null;
711 }
712 }
713
714 try {
715 module.exports = Yaku;
716 } catch (e) {
717 root.Yaku = Yaku;
718 }
719})();