diff --git a/modules/es6.promise.js b/modules/es6.promise.js index fdaefe8cf528..a55f9c185f77 100644 --- a/modules/es6.promise.js +++ b/modules/es6.promise.js @@ -1,26 +1,15 @@ 'use strict'; -var LIBRARY = require('./_library') - , global = require('./_global') - , ctx = require('./_ctx') +var global = require('./_global') , classof = require('./_classof') , $export = require('./_export') - , isObject = require('./_is-object') - , anObject = require('./_an-object') - , aFunction = require('./_a-function') - , anInstance = require('./_an-instance') - , forOf = require('./_for-of') - , from = require('./_array-from-iterable') + , assign = require('./_object-assign') + , redefine = require('./_redefine') + , wks = require('./_wks') , setProto = require('./_set-proto').set - , speciesConstructor = require('./_species-constructor') - , task = require('./_task').set - , microtask = require('./_microtask') , PROMISE = 'Promise' - , TypeError = global.TypeError - , process = global.process , $Promise = global[PROMISE] - , isNode = classof(process) == 'process' , empty = function(){ /* empty */ } - , Internal, GenericPromiseCapability, Wrapper; + , Yaku = require('./yaku'); var USE_NATIVE = !!function(){ try { @@ -28,262 +17,42 @@ var USE_NATIVE = !!function(){ var promise = $Promise.resolve(1) , FakePromise1 = promise.constructor = function(exec){ exec(empty, empty); } , FakePromise2 = function(exec){ exec(empty, empty); }; - require('./_object-dp')(FakePromise1, require('./_wks')('species'), {value: FakePromise2}); + require('./_object-dp')(FakePromise1, wks('species'), {value: FakePromise2}); // unhandled rejections tracking support, NodeJS Promise without it fails @@species test return (isNode || typeof PromiseRejectionEvent == 'function') && promise.then(empty) instanceof FakePromise2; } catch(e){ /* empty */ } }(); -// helpers -var sameConstructor = function(a, b){ - // with library wrapper special case - return a === b || a === $Promise && b === Wrapper; -}; -var isThenable = function(it){ - var then; - return isObject(it) && typeof (then = it.then) == 'function' ? then : false; -}; -var newPromiseCapability = function(C){ - return sameConstructor($Promise, C) - ? new PromiseCapability(C) - : new GenericPromiseCapability(C); -}; -var PromiseCapability = GenericPromiseCapability = function(C){ - var resolve, reject; - this.promise = new C(function($$resolve, $$reject){ - if(resolve !== undefined || reject !== undefined)throw TypeError('Bad Promise constructor'); - resolve = $$resolve; - reject = $$reject; - }); - this.resolve = aFunction(resolve); - this.reject = aFunction(reject); -}; -var perform = function(exec){ - try { - exec(); - } catch(e){ - return {error: e}; - } -}; -var notify = function(promise, isReject){ - if(promise._n)return; - promise._n = true; - var chain = promise._c; - microtask(function(){ - var value = promise._v - , ok = promise._s == 1 - , i = 0; - var run = function(reaction){ - var handler = ok ? reaction.ok : reaction.fail - , resolve = reaction.resolve - , reject = reaction.reject - , result, then; - try { - if(handler){ - if(!ok){ - if(promise._h == 2)onHandleUnhandled(promise); - promise._h = 1; - } - result = handler === true ? value : handler(value); - if(result === reaction.promise){ - reject(TypeError('Promise-chain cycle')); - } else if(then = isThenable(result)){ - then.call(result, resolve, reject); - } else resolve(result); - } else reject(value); - } catch(e){ - reject(e); - } - }; - while(chain.length > i)run(chain[i++]); // variable length - can't use forEach - promise._c = []; - promise._n = false; - if(isReject && !promise._h)onUnhandled(promise); - }); -}; -var onUnhandled = function(promise){ - task.call(global, function(){ - if(isUnhandled(promise)){ - var value = promise._v - , handler, console; - if(isNode){ - process.emit('unhandledRejection', value, promise); - } else if(handler = global.onunhandledrejection){ - handler({promise: promise, reason: value}); - } else if((console = global.console) && console.error){ - console.error('Unhandled promise rejection', value); - } promise._h = 2; - } promise._a = undefined; - }); -}; -var isUnhandled = function(promise){ - var chain = promise._a || promise._c - , i = 0 - , reaction; - if(promise._h == 1)return false; - while(chain.length > i){ - reaction = chain[i++]; - if(reaction.fail || !isUnhandled(reaction.promise))return false; - } return true; -}; -var onHandleUnhandled = function(promise){ - task.call(global, function(){ - var handler; - if(isNode){ - process.emit('rejectionHandled', promise); - } else if(handler = global.onrejectionhandled){ - handler({promise: promise, reason: promise._v}); - } - }); -}; -var $reject = function(value){ - var promise = this; - if(promise._d)return; - promise._d = true; - promise = promise._w || promise; // unwrap - promise._v = value; - promise._s = 2; - if(!promise._a)promise._a = promise._c.slice(); - notify(promise, true); -}; -var $resolve = function(value){ - var promise = this - , then; - if(promise._d)return; - promise._d = true; - promise = promise._w || promise; // unwrap - try { - if(promise === value)throw TypeError("Promise can't be resolved itself"); - if(then = isThenable(value)){ - microtask(function(){ - var wrapper = {_w: promise, _d: false}; // wrap - try { - then.call(value, ctx($resolve, wrapper, 1), ctx($reject, wrapper, 1)); - } catch(e){ - $reject.call(wrapper, e); - } - }); - } else { - promise._v = value; - promise._s = 1; - notify(promise, false); - } - } catch(e){ - $reject.call({_w: promise, _d: false}, e); // wrap - } -}; - // constructor polyfill if(!USE_NATIVE){ - // 25.4.3.1 Promise(executor) - $Promise = function Promise(executor){ - anInstance(this, $Promise, PROMISE, '_h'); - aFunction(executor); - Internal.call(this); - try { - executor(ctx($resolve, this, 1), ctx($reject, this, 1)); - } catch(err){ - $reject.call(this, err); - } - }; - Internal = function Promise(executor){ - this._c = []; // <- awaiting reactions - this._a = undefined; // <- checked in isUnhandled reactions - this._s = 0; // <- state - this._d = false; // <- done - this._v = undefined; // <- value - this._h = 0; // <- rejection state, 0 - default, 1 - handled, 2 - unhandled - this._n = false; // <- notify + + // Promise need two well-known symbols. + Yaku.Symbol = { + iterator: wks('iterator'), + species: wks('species'), }; - Internal.prototype = require('./_redefine-all')($Promise.prototype, { - // 25.4.5.3 Promise.prototype.then(onFulfilled, onRejected) - then: function then(onFulfilled, onRejected){ - var reaction = newPromiseCapability(speciesConstructor(this, $Promise)); - reaction.ok = typeof onFulfilled == 'function' ? onFulfilled : true; - reaction.fail = typeof onRejected == 'function' && onRejected; - this._c.push(reaction); - if(this._a)this._a.push(reaction); - if(this._s)notify(this, false); - return reaction.promise; - }, - // 25.4.5.1 Promise.prototype.catch(onRejected) - 'catch': function(onRejected){ - return this.then(undefined, onRejected); + + // Use the special optimized microtask. + Yaku.nextTick = require('./_microtask'); + + // For subclass construction. + Yaku.speciesConstructor = require('./_species-constructor'); + + $Promise = Yaku; + + var redefineAll = function (target) { + var k; + for (k in target) { + var v = target[k]; + delete target[k]; + redefine(target, k, v); } - }); - PromiseCapability = function(){ - var promise = new Internal; - this.promise = promise; - this.resolve = ctx($resolve, promise, 1); - this.reject = ctx($reject, promise, 1); - }; + } + + redefineAll($Promise); + redefineAll($Promise.prototype); } $export($export.G + $export.W + $export.F * !USE_NATIVE, {Promise: $Promise}); require('./_set-to-string-tag')($Promise, PROMISE); require('./_set-species')(PROMISE); -Wrapper = require('./_core')[PROMISE]; - -// statics -$export($export.S + $export.F * !USE_NATIVE, PROMISE, { - // 25.4.4.5 Promise.reject(r) - reject: function reject(r){ - var capability = newPromiseCapability(this) - , $$reject = capability.reject; - $$reject(r); - return capability.promise; - } -}); -$export($export.S + $export.F * (LIBRARY || !USE_NATIVE), PROMISE, { - // 25.4.4.6 Promise.resolve(x) - resolve: function resolve(x){ - // instanceof instead of internal slot check because we should fix it without replacement native Promise core - if(x instanceof $Promise && sameConstructor(x.constructor, this))return x; - var capability = newPromiseCapability(this) - , $$resolve = capability.resolve; - $$resolve(x); - return capability.promise; - } -}); -$export($export.S + $export.F * !(USE_NATIVE && require('./_iter-detect')(function(iter){ - $Promise.all(iter)['catch'](empty); -})), PROMISE, { - // 25.4.4.1 Promise.all(iterable) - all: function all(iterable){ - var C = this - , capability = newPromiseCapability(C) - , resolve = capability.resolve - , reject = capability.reject; - var abrupt = perform(function(){ - var values = from(iterable) - , remaining = values.length - , results = Array(remaining); - var f = function(promise, index){ - var alreadyCalled = false; - C.resolve(promise).then(function(value){ - if(alreadyCalled)return; - alreadyCalled = true; - results[index] = value; - --remaining || resolve(results); - }, reject); - }; - if(remaining)for(var i = 0, l = values.length; l > i; i++)f(values[i], i); - else resolve(results); - }); - if(abrupt)reject(abrupt.error); - return capability.promise; - }, - // 25.4.4.4 Promise.race(iterable) - race: function race(iterable){ - var C = this - , capability = newPromiseCapability(C) - , reject = capability.reject; - var abrupt = perform(function(){ - forOf(iterable, false, function(promise){ - C.resolve(promise).then(capability.resolve, reject); - }); - }); - if(abrupt)reject(abrupt.error); - return capability.promise; - } -}); \ No newline at end of file diff --git a/modules/yaku.js b/modules/yaku.js new file mode 100644 index 000000000000..6e66bd635972 --- /dev/null +++ b/modules/yaku.js @@ -0,0 +1,827 @@ +/*eslint-disable*/ +(function () { + "use strict"; + + var $undefined + , $null = null + , root = typeof global === "object" ? global : window + , isLongStackTrace = false + , process = root.process + + , $rejected = 0 + , $resolved = 1 + , $pending = 2 + + , $Symbol = "Symbol" + , $iterator = "iterator" + + , $unhandled = "_uh" + , $promiseTrace = "_pt" + , $settlerTrace = "_st" + + , $invalidThis = "Invalid this" + , $invalidArgument = "Invalid argument" + , $fromPrevious = "From previous event:" + , $promiseCircularChain = "Chaining cycle detected for promise" + , $unhandledRejectionMsg = "Uncaught (in promise)" + , $rejectionHandled = "rejectionHandled" + , $unhandledRejection = "unhandledRejection" + + , $tryCatchFn + , $tryCatchThis + , $tryErr = { e: $null } + , $noop = function () {} + ; + + /** + * This class follows the [Promises/A+](https://promisesaplus.com) and + * [ES6](http://people.mozilla.org/~jorendorff/es6-draft.html#sec-promise-objects) spec + * with some extra helpers. + * @param {Function} executor Function object with two arguments resolve, reject. + * The first argument fulfills the promise, the second argument rejects it. + * We can call these functions, once our operation is completed. + */ + var Yaku = module.exports = function Promise (executor) { + var self = this, + err; + + if (!isYaku(self) || self._state !== $undefined) + throw genTypeError($invalidThis); + + self._state = $pending; + + if (isLongStackTrace) self[$promiseTrace] = genTraceInfo(); + + if (executor !== $noop) { + if (!isFunction(executor)) + throw genTypeError($invalidArgument); + + err = genTryCatcher(executor)( + genSettler(self, $resolved), + genSettler(self, $rejected) + ); + + if (err === $tryErr) + settlePromise(self, $rejected, err.e); + } + }; + + Yaku["default"] = Yaku; + + extendPrototype(Yaku, { + /** + * Appends fulfillment and rejection handlers to the promise, + * and returns a new promise resolving to the return value of the called handler. + * @param {Function} onFulfilled Optional. Called when the Promise is resolved. + * @param {Function} onRejected Optional. Called when the Promise is rejected. + * @return {Yaku} It will return a new Yaku which will resolve or reject after + * @example + * the current Promise. + * ```js + * var Promise = require('yaku'); + * var p = Promise.resolve(10); + * + * p.then((v) => { + * console.log(v); + * }); + * ``` + */ + then: function then (onFulfilled, onRejected) { + return addHandler( + this, + newEmptyPromise(Yaku.speciesConstructor(this, Yaku)), + onFulfilled, + onRejected + ); + }, + + /** + * The `catch()` method returns a Promise and deals with rejected cases only. + * It behaves the same as calling `Promise.prototype.then(undefined, onRejected)`. + * @param {Function} onRejected A Function called when the Promise is rejected. + * This function has one argument, the rejection reason. + * @return {Yaku} A Promise that deals with rejected cases only. + * @example + * ```js + * var Promise = require('yaku'); + * var p = Promise.reject(new Error("ERR")); + * + * p['catch']((v) => { + * console.log(v); + * }); + * ``` + */ + "catch": function (onRejected) { + return this.then($undefined, onRejected); + }, + + // The number of current promises that attach to this Yaku instance. + _pCount: 0, + + // The parent Yaku. + _pre: $null, + + // A unique type flag, it helps different versions of Yaku know each other. + _Yaku: 1 + }); + + /** + * The `Promise.resolve(value)` method returns a Promise object that is resolved with the given value. + * If the value is a thenable (i.e. has a then method), the returned promise will "follow" that thenable, + * adopting its eventual state; otherwise the returned promise will be fulfilled with the value. + * @param {Any} value Argument to be resolved by this Promise. + * Can also be a Promise or a thenable to resolve. + * @return {Yaku} + * @example + * ```js + * var Promise = require('yaku'); + * var p = Promise.resolve(10); + * ``` + */ + Yaku.resolve = function resolve (val) { + return isYaku(val) ? val : settleWithX(newEmptyPromise(this), val); + }; + + /** + * The `Promise.reject(reason)` method returns a Promise object that is rejected with the given reason. + * @param {Any} reason Reason why this Promise rejected. + * @return {Yaku} + * @example + * ```js + * var Promise = require('yaku'); + * var p = Promise.reject(new Error("ERR")); + * ``` + */ + Yaku.reject = function reject (reason) { + return settlePromise(newEmptyPromise(this), $rejected, reason); + }; + + /** + * The `Promise.race(iterable)` method returns a promise that resolves or rejects + * as soon as one of the promises in the iterable resolves or rejects, + * with the value or reason from that promise. + * @param {iterable} iterable An iterable object, such as an Array. + * @return {Yaku} The race function returns a Promise that is settled + * the same way as the first passed promise to settle. + * It resolves or rejects, whichever happens first. + * @example + * ```js + * var Promise = require('yaku'); + * Promise.race([ + * 123, + * Promise.resolve(0) + * ]) + * .then((value) => { + * console.log(value); // => 123 + * }); + * ``` + */ + Yaku.race = function race (iterable) { + var iter, len, i = 0; + + var p = newEmptyPromise(this), item; + + if (isPlainArray(iterable)) { + len = iterable.length; + while (i < len) { + settleWithX(p, iterable[i++]); + if (p._state !== $pending) break; + } + } else { + iter = genIterator(iterable); + + if (isError(iter)) return Yaku.reject(iter); + + while (!(item = iter.next()).done) { + settleWithX(p, item.value); + if (p._state !== $pending) break; + } + } + + return p; + }; + + /** + * The `Promise.all(iterable)` method returns a promise that resolves when + * all of the promises in the iterable argument have resolved. + * + * The result is passed as an array of values from all the promises. + * If something passed in the iterable array is not a promise, + * it's converted to one by Promise.resolve. If any of the passed in promises rejects, + * the all Promise immediately rejects with the value of the promise that rejected, + * discarding all the other promises whether or not they have resolved. + * @param {iterable} iterable An iterable object, such as an Array. + * @return {Yaku} + * @example + * ```js + * var Promise = require('yaku'); + * Promise.all([ + * 123, + * Promise.resolve(0) + * ]) + * .then((values) => { + * console.log(values); // => [123, 0] + * }); + * ``` + * @example + * Use with iterable. + * ```js + * var Promise = require('yaku'); + * Promise.all((function * () { + * yield 10; + * yield new Promise(function (r) { setTimeout(r, 1000, "OK") }); + * })()) + * .then((values) => { + * console.log(values); // => [123, 0] + * }); + * ``` + */ + Yaku.all = function all (iterable) { + var p1 = newEmptyPromise(this) + , res = [] + , item + , countDown = 0 + , iter + , len; + + function onRejected (reason) { + settlePromise(p1, $rejected, reason); + } + + if (isPlainArray(iterable)) { + len = iterable.length; + while (countDown < len) { + runAll(countDown, iterable[countDown++], p1, res, onRejected); + } + } else { + iter = genIterator(iterable); + + if (isError(iter)) return Yaku.reject(iter); + + while (!(item = iter.next()).done) { + runAll(countDown++, item.value, p1, res, onRejected); + } + } + + onRejected._c = countDown; + + if (!countDown) settlePromise(p1, $resolved, []); + + return p1; + }; + + function runAll (i, el, p1, res, onRejected) { + Yaku.resolve(el).then(function (value) { + res[i] = value; + if (!--onRejected._c) settlePromise(p1, $resolved, res); + }, onRejected); + } + + /** + * The ES6 Symbol object that Yaku should use, by default it will use the + * global one. + * @type {Object} + * @example + * ```js + * var core = require("core-js/library"); + * var Promise = require("yaku"); + * Promise.Symbol = core.Symbol; + * ``` + */ + Yaku.Symbol = root[$Symbol] || {}; + + /** + * Use this api to custom the species behavior. + * https://tc39.github.io/ecma262/#sec-speciesconstructor + * @param {Any} O The current this object. + * @param {Function} defaultConstructor + */ + Yaku.speciesConstructor = function (O, D) { return O.constructor || D; }; + + /** + * Catch all possibly unhandled rejections. If you want to use specific + * format to display the error stack, overwrite it. + * If it is set, auto `console.error` unhandled rejection will be disabled. + * @param {Any} reason The rejection reason. + * @param {Yaku} p The promise that was rejected. + * @example + * ```js + * var Promise = require('yaku'); + * Promise.onUnhandledRejection = (reason) => { + * console.error(reason); + * }; + * + * // The console will log an unhandled rejection error message. + * Promise.reject('my reason'); + * + * // The below won't log the unhandled rejection error message. + * Promise.reject('v').catch(() => {}); + * ``` + */ + Yaku.unhandledRejection = function (reason, p) { + var con = root.console; + if (con) { + var info = genStackInfo(reason, p); + con.error($unhandledRejectionMsg, info[0], info[1] || ""); + } + }; + + /** + * Emitted whenever a Promise was rejected and an error handler was + * attached to it (for example with .catch()) later than after an event loop turn. + * @param {Any} reason The rejection reason. + * @param {Yaku} p The promise that was rejected. + */ + Yaku.rejectionHandled = $noop; + + /** + * It is used to enable the long stack trace. + * Once it is enabled, it can't be reverted. + * While it is very helpful in development and testing environments, + * it is not recommended to use it in production. It will slow down your + * application and waste your memory. + * @example + * ```js + * var Promise = require('yaku'); + * Promise.enableLongStackTrace(); + * ``` + */ + Yaku.enableLongStackTrace = function () { + isLongStackTrace = true; + }; + + /** + * Only Node has `process.nextTick` function. For browser there are + * so many ways to polyfill it. Yaku won't do it for you, instead you + * can choose what you prefer. For example, this project + * [setImmediate](https://github.com/YuzuJS/setImmediate). + * By default, Yaku will use `process.nextTick` on Node, `setTimeout` on browser. + * @type {Function} + * @example + * ```js + * var Promise = require('yaku'); + * Promise.nextTick = fn => window.setImmediate(fn); + * ``` + * @example + * You can even use sync resolution if you really know what you are doing. + * ```js + * var Promise = require('yaku'); + * Promise.nextTick = fn => fn(); + * ``` + */ + Yaku.nextTick = process ? + process.nextTick : + function (fn) { setTimeout(fn); }; + + // ********************** Private ********************** + + Yaku._Yaku = 1; + + /** + * All static variable name will begin with `$`. Such as `$rejected`. + * @private + */ + + // ******************************* Utils ******************************** + + function extendPrototype (src, target) { + for (var k in target) { + src.prototype[k] = target[k]; + } + return src; + } + + function isObject (obj) { + return typeof obj === "object"; + } + + function isFunction (obj) { + return typeof obj === "function"; + } + + function isPlainArray (obj) { + return obj + && typeof obj.length === "number" + && !isFunction(obj[Yaku[$Symbol][$iterator]]); + } + + function isError (obj) { + return obj instanceof Error; + } + + /** + * Wrap a function into a try-catch. + * @private + * @return {Any | $tryErr} + */ + function tryCatcher () { + try { + return $tryCatchFn.apply($tryCatchThis, arguments); + } catch (e) { + $tryErr.e = e; + return $tryErr; + } + } + + /** + * Generate a try-catch wrapped function. + * @private + * @param {Function} fn + * @return {Function} + */ + function genTryCatcher (fn, self) { + $tryCatchFn = fn; + $tryCatchThis = self; + return tryCatcher; + } + + /** + * Generate a scheduler. + * @private + * @param {Integer} initQueueSize + * @param {Function} fn `(Yaku, Value) ->` The schedule handler. + * @return {Function} `(Yaku, Value) ->` The scheduler. + */ + function genScheduler (initQueueSize, fn) { + /** + * All async promise will be scheduled in + * here, so that they can be execute on the next tick. + * @private + */ + var fnQueue = Array(initQueueSize) + , fnQueueLen = 0; + + /** + * Run all queued functions. + * @private + */ + function flush () { + var i = 0; + while (i < fnQueueLen) { + fn(fnQueue[i], fnQueue[i + 1]); + fnQueue[i++] = $undefined; + fnQueue[i++] = $undefined; + } + + fnQueueLen = 0; + if (fnQueue.length > initQueueSize) fnQueue.length = initQueueSize; + } + + return function (v, arg) { + fnQueue[fnQueueLen++] = v; + fnQueue[fnQueueLen++] = arg; + + if (fnQueueLen === 2) Yaku.nextTick(flush); + }; + } + + /** + * Generate a iterator + * @param {Any} obj + * @private + * @return {Object || TypeError} + */ + function genIterator (obj) { + if (obj) { + var gen = obj[Yaku[$Symbol][$iterator]]; + if (isFunction(gen)) { + return gen.call(obj); + } + + if (isFunction(obj.next)) { + return obj; + } + } + + return genTypeError($invalidArgument); + } + + /** + * Generate type error object. + * @private + * @param {String} msg + * @return {TypeError} + */ + function genTypeError (msg) { + return new TypeError(msg); + } + + function genTraceInfo (noTitle) { + return ((new Error()).stack || "").replace( + "Error", + noTitle ? "" : $fromPrevious + ); + } + + + // *************************** Promise Helpers **************************** + + /** + * Resolve the value returned by onFulfilled or onRejected. + * @private + * @param {Yaku} p1 + * @param {Yaku} p2 + */ + var scheduleHandler = genScheduler(999, function (p1, p2) { + var x, handler; + + // 2.2.2 + // 2.2.3 + handler = p1._state ? p2._onFulfilled : p2._onRejected; + + // 2.2.7.3 + // 2.2.7.4 + if (handler === $undefined) { + settlePromise(p2, p1._state, p1._value); + return; + } + + // 2.2.7.1 + x = genTryCatcher(callHanler)(handler, p1._value); + if (x === $tryErr) { + // 2.2.7.2 + settlePromise(p2, $rejected, x.e); + return; + } + + settleWithX(p2, x); + }); + + var scheduleUnhandledRejection = genScheduler(9, function (p) { + if (!hashOnRejected(p)) { + p[$unhandled] = 1; + emitEvent($unhandledRejection, p); + } + }); + + function emitEvent (name, p) { + var browserEventName = "on" + name.toLowerCase() + , browserHandler = root[browserEventName]; + + if (process && process.listeners(name).length) + name === $unhandledRejection ? + process.emit(name, p._value, p) : process.emit(name, p); + else if (browserHandler) + browserHandler({ reason: p._value, promise: p }); + else + Yaku[name](p._value, p); + } + + function isYaku (val) { return val && val._Yaku; } + + function newEmptyPromise (Self) { + if (!isYaku(Self)) throw genTypeError($invalidThis); + + return new Self($noop); + } + + /** + * It will produce a settlePromise function to user. + * Such as the resolve and reject in this `new Yaku (resolve, reject) ->`. + * @private + * @param {Yaku} self + * @param {Integer} state The value is one of `$pending`, `$resolved` or `$rejected`. + * @return {Function} `(value) -> undefined` A resolve or reject function. + */ + function genSettler (self, state) { + return function (value) { + if (isLongStackTrace) + self[$settlerTrace] = genTraceInfo(true); + + if (state === $resolved) + settleWithX(self, value); + else + settlePromise(self, state, value); + }; + } + + /** + * Link the promise1 to the promise2. + * @private + * @param {Yaku} p1 + * @param {Yaku} p2 + * @param {Function} onFulfilled + * @param {Function} onRejected + */ + function addHandler (p1, p2, onFulfilled, onRejected) { + // 2.2.1 + if (isFunction(onFulfilled)) + p2._onFulfilled = onFulfilled; + if (isFunction(onRejected)) { + if (p1[$unhandled]) emitEvent($rejectionHandled, p1); + + p2._onRejected = onRejected; + } + + if (isLongStackTrace) p2._pre = p1; + p1[p1._pCount++] = p2; + + // 2.2.6 + if (p1._state !== $pending) + scheduleHandler(p1, p2); + + // 2.2.7 + return p2; + } + + // iterate tree + function hashOnRejected (node) { + // A node shouldn't be checked twice. + if (node._umark) + return true; + else + node._umark = true; + + var i = 0 + , len = node._pCount + , child; + + while (i < len) { + child = node[i++]; + if (child._onRejected || hashOnRejected(child)) return true; + } + } + + function genStackInfo (reason, p) { + var stackInfo = [] + , stackStr + , i; + + function trim (str) { return str.replace(/^\s+|\s+$/g, ""); } + + function push (trace) { + return stackInfo.push(trim(trace)); + } + + if (isLongStackTrace && p[$promiseTrace]) { + if (p[$settlerTrace]) + push(p[$settlerTrace]); + + // Hope you guys could understand how the back trace works. + // We only have to iterate through the tree from the bottom to root. + (function iter (node) { + if (node) { + iter(node._next); + push(node[$promiseTrace]); + iter(node._pre); + } + })(p); + } + + stackStr = "\n" + stackInfo.join("\n"); + + function clean (stack, cleanPrev) { + if (cleanPrev && (i = stack.indexOf("\n" + $fromPrevious)) > 0) + stack = stack.slice(0, i); + + return stack.replace(/^.+\/node_modules\/yaku\/.+\n?/mg, ""); + } + + return [( + reason ? + reason.stack ? + clean(trim(reason.stack), true) + : + reason + : + reason + ), clean(stackStr)]; + } + + function callHanler (handler, value) { + // 2.2.5 + return handler(value); + } + + /** + * Resolve or reject a promise. + * @private + * @param {Yaku} p + * @param {Integer} state + * @param {Any} value + */ + function settlePromise (p, state, value) { + var i = 0 + , len = p._pCount + , p2 + , stack; + + // 2.1.2 + // 2.1.3 + if (p._state === $pending) { + // 2.1.1.1 + p._state = state; + p._value = value; + + if (state === $rejected) { + if (isLongStackTrace && value && value.stack) { + stack = genStackInfo(value, p); + value.stack = stack[0] + stack[1]; + } + + scheduleUnhandledRejection(p); + } + + // 2.2.4 + while (i < len) { + p2 = p[i++]; + + if (p2._state !== $pending) continue; + + scheduleHandler(p, p2); + } + } + + return p; + } + + /** + * Resolve or reject promise with value x. The x can also be a thenable. + * @private + * @param {Yaku} p + * @param {Any | Thenable} x A normal value or a thenable. + */ + function settleWithX (p, x) { + // 2.3.1 + if (x === p && x) { + settlePromise(p, $rejected, genTypeError($promiseCircularChain)); + return p; + } + + // 2.3.2 + // 2.3.3 + if (x !== $null && (isFunction(x) || isObject(x))) { + // 2.3.2.1 + var xthen = genTryCatcher(getThen)(x); + + if (xthen === $tryErr) { + // 2.3.3.2 + settlePromise(p, $rejected, xthen.e); + return p; + } + + if (isFunction(xthen)) { + if (isLongStackTrace && isYaku(x)) + p._next = x; + + // Fix https://bugs.chromium.org/p/v8/issues/detail?id=4162 + if (isYaku(x)) + settleXthen(p, x, xthen); + else + Yaku.nextTick(function () { + settleXthen(p, x, xthen); + }); + } else + // 2.3.3.4 + settlePromise(p, $resolved, x); + } else + // 2.3.4 + settlePromise(p, $resolved, x); + + return p; + } + + /** + * Try to get a promise's then method. + * @private + * @param {Thenable} x + * @return {Function} + */ + function getThen (x) { return x.then; } + + /** + * Resolve then with its promise. + * @private + * @param {Yaku} p + * @param {Thenable} x + * @param {Function} xthen + */ + function settleXthen (p, x, xthen) { + // 2.3.3.3 + var err = genTryCatcher(xthen, x)(function (y) { + // 2.3.3.3.3 + if (x) { + x = $null; + + // 2.3.3.3.1 + settleWithX(p, y); + } + }, function (r) { + // 2.3.3.3.3 + if (x) { + x = $null; + + // 2.3.3.3.2 + settlePromise(p, $rejected, r); + } + }); + + // 2.3.3.3.4.1 + if (err === $tryErr && x) { + // 2.3.3.3.4.2 + settlePromise(p, $rejected, err.e); + x = $null; + } + } + +})();