/* * Minimal ES2015+ Promise polyfill * * Limitations (also see XXX in source): * * - Caller must manually call Promise.runQueue() to process pending jobs. * - No Promise subclassing or non-subclass foreign Promises yet. * - Promise.all() and Promise.race() assume a plain array, not iterator. * - Doesn't handle errors from core operations, e.g. out-of-memory or * internal error when queueing/running jobs. These are implementation * defined for the most part. * * This polyfill was originally used to gain a better understanding of the * ES2015 specification algorithms, before implementing Promises natively. * * The polyfill uses a Symbol to mark Promise instances, but falls back to * an ordinary (non-enumerable) property if no Symbol support is available. * Presence of the polyfill itself can be checked using "Promise.isPolyfill". * * Unhandled Promise rejections use a custom API signature. For now, a * single Promise.unhandledRejection() hook receives both 'rawReject' and * 'rawHandle' events directly from HostPromiseRejectionTracker, and higher * level (Node.js / WHATWG like) 'reject' and 'handle' events which filter * out cases where a rejected Promise is initially unhandled but gets handled * within the same "tick". * * See also: https://github.com/stefanpenner/es6-promise#readme */ (function () { if (typeof Promise !== 'undefined') { return; } // As far as the specification goes, almost all Promise settling is via // concrete resolve/reject functions with mutual protection from being // called multiple times. Sometimes the actual resolve/reject functions // are not exposed to calling code, and can safely be omitted which is // useful because resolve/reject functions are memory heavy. These // optimizations are enabled by default; set to false to disable. var allowOptimization = true; // Job queue to simulate ES2015 job queues, linked list, 'next' reference. // While ES2015 doesn't guarantee the relative order of jobs in different // job queues, within a certain queue strict FIFO is required. See ES5.1 // https://www.ecma-international.org/ecma-262/6.0/#sec-jobs-and-job-queues: // "The PendingJob records from a single Job Queue are always initiated in // FIFO order. This specification does not define the order in which // multiple Job Queues are serviced." var queueHead = null, queueTail = null; function enqueueJob(job) { // Avoid inheriting conflicting properties if caller already didn't // ensure it. Object.setPrototypeOf(job, null); compact(job); if (queueHead) { queueTail.next = job; compact(queueTail); queueTail = job; } else { queueHead = job; queueTail = job; } } function dequeueJob() { var ret = queueHead; if (ret) { queueHead = ret.next; if (!queueHead) { queueTail = null; } } return ret; } function queueEmpty() { return !queueHead; } // Helper to define/modify properties more compactly. function def(obj, key, val, attrs) { if (attrs === void 0) { attrs = 'wc'; } Object.defineProperty(obj, key, { value: val, writable: attrs.indexOf('w') >= 0, enumerable: attrs.indexOf('e') >= 0, configurable: attrs.indexOf('c') >= 0 }); } // Helper for Duktape specific object compaction. var compact = (typeof Duktape === 'object' && Duktape.compact) || function (v) { return v; }; // Shared no-op function. var nop = function nop() {}; // Promise detection (plain or subclassed Promise), in spec has // [[PromiseState]] internal slot which isn't affected by Proxy // behaviors etc. var haveSymbols = (typeof Symbol === 'function'); var promiseMarker = haveSymbols ? Symbol('promise') : '__PromiseInstance__'; function isPromise(p) { return p !== null && typeof p === 'object' && promiseMarker in p; } function requirePromise(p) { if (!isPromise(p)) { throw new TypeError('Promise required'); } } // Raw HostPromiseRejectionTracker call. This operation should "never" // fail but that's in practice unachievable due to possible out-of-memory // on any operation (including invocation of the callback). Higher level // hook events are emitted from Promise.runQueue(). function safeCallUnhandledRejection(event) { try { cons.unhandledRejection(event); } catch (e) { //console.log('Promise.unhandledRejection failed:', e); } } function rejectionTracker(p, operation) { try { if (operation === 'reject') { // Unhandled at resolution. safeCallUnhandledRejection({ promise: p, event: 'rawReject', reason: p.value }); def(p, 'unhandled', 1); cons.potentiallyUnhandled.push(p); } else if (operation === 'handle') { safeCallUnhandledRejection({ promise: p, event: 'rawHandle', reason: p.value }); if (p.unhandled === 2) { // Unhandled, already notified, need handled notification. def(p, 'unhandled', 3); cons.potentiallyUnhandled.push(p); } else { // Handled but not yet notified -> no action needed. // XXX: If this.unhandled was 1, we'd like to remove // the Promise from cons.potentiallyUnhandled list. // We skip that here because it would mean an expensive // list remove. If cons.potentiallyUnhandled was a // Set, it would be natural to remove from Set here. delete p.unhandled; } } } catch (e) { //console.log('HostPromiseRejectionTracker failed:', e); } } // Raw fulfill/reject operations, assume resolution processing done. // The specification algorithms RejectPromise() and FulfillPromise() // assert that the Promise is pending so the initial check in these // implementations (p.state !== void 0) is not needed: the resolve/reject // function pairs always ensure a Promise is not ultimately settled twice. // With some of the "as if" optimizations we rely on these raw operations // to protect against multiple attempts to settle the Promise so the checks // are actually needed. function doFulfill(p, val) { if (p.state !== void 0) { return; } // additional check needed with optimizations p.state = true; p.value = val; var reactions = p.fulfillReactions; delete p.fulfillReactions; delete p.rejectReactions; compact(p); reactions.forEach(function (ent) { // Conceptually: create a job from the registered reaction. // In practice: reuse the reaction object because it is unique, // never leaks to calling code, and is never reused. ent.value = val; enqueueJob(ent); }); } function doReject(p, val) { if (p.state !== void 0) { return; } // additional check needed with optimizations p.state = false; p.value = val; var reactions = p.rejectReactions; delete p.fulfillReactions; delete p.rejectReactions; compact(p); reactions.forEach(function (ent) { // As for doFulfill(), reuse the registered reaction object. ent.value = val; if (!ent.handler) { // Without a .handler, we're dealing with an optimized // entry where only .target exists and the resolve/reject // behavior is simulated when the entry runs. However, // we need to know whether to simulate resolve or reject // at that time, so flag rejection explicitly (resolve // requires no flag). ent.rejected = true; } enqueueJob(ent); }); if (!p.isHandled) { rejectionTracker(p, 'reject'); } } // Create a new resolve/reject pair for a Promise. Multiple pairs are // needed in thenable handling, with all but the most recent pair being // neutralized ('alreadyResolved'). Because Promises are resolved only // via this resolution process, it shouldn't be possible for the Promise // to be settled but check it anyway: it may be useful for e.g. the C API // to forcibly resolve/fulfill/reject a Promise regardless of extant // resolve/reject functions. function createResolutionFunctions(p) { // In ES2015 the resolve/reject functions have a shared 'state' object // with a [[AlreadyResolved]] slot. Here we use an in-scope variable. var alreadyResolved = false; var reject = function (err) { if (new.target) { throw new TypeError('reject is not constructable'); } if (alreadyResolved) { return; } alreadyResolved = true; // neutralize resolve/reject if (p.state !== void 0) { return; } doReject(p, err); }; reject.prototype = null; // drop .prototype object var resolve = function (val) { if (new.target) { throw new TypeError('resolve is not constructable'); } if (alreadyResolved) { return; } alreadyResolved = true; // neutralize resolve/reject if (p.state !== void 0) { return; } if (val === p) { return doReject(p, new TypeError('self resolution')); } try { var then = (val !== null && typeof val === 'object' && val.then); if (typeof then === 'function') { var t = createResolutionFunctions(p); var optimized = allowOptimization; if (optimized) { // XXX: this optimization may not be useful because the // job entry runs usually very quickly, and as part of // running the job, the resolve/reject function must be // created for the then() call. return enqueueJob({ thenable: val, then: then, target: p }); } else { return enqueueJob({ thenable: val, then: then, resolve: t.resolve, reject: t.reject }); } // old resolve/reject is neutralized, only new pair is live } return doFulfill(p, val); } catch (e) { return doReject(p, e); } }; resolve.prototype = null; // drop .prototype object return { resolve: resolve, reject: reject }; } // Job queue simulation. function runQueueEntry() { // XXX: In optimized cases, creating both resolution functions is // not always necessary. There's also no need for alreadySettled // protections for the optimized cases either. var job = dequeueJob(); var tmp; if (!job) { return false; } if (job.then) { // PromiseResolveThenableJob if (job.target) { tmp = createResolutionFunctions(job.target); } try { if (tmp) { void job.then.call(job.thenable, tmp.resolve, tmp.reject); } else { void job.then.call(job.thenable, job.resolve, job.reject); } } catch (e) { if (tmp) { tmp.reject.call(void 0, e); } else { job.reject.call(void 0, e); } } } else { // PromiseReactionJob try { if (job.handler === void 0) { // Optimized case where two Promises are tied together // without the need for an actual 'handler'. tmp = createResolutionFunctions(job.target); // must exist in this case tmp = job.rejected ? tmp.reject : tmp.resolve; tmp.call(void 0, job.value); return true; } else if (job.handler === 'Identity') { res = job.value; } else if (job.handler === 'Thrower') { throw job.value; } else { res = job.handler.call(void 0, job.value); } if (job.target) { createResolutionFunctions(job.target).resolve.call(void 0, res); } else { job.resolve.call(void 0, res); } } catch (e) { if (job.target) { createResolutionFunctions(job.target).reject.call(void 0, e); } else { job.reject.call(void 0, e); } } } return true; } // %Promise% constructor. var cons = function Promise(executor) { if (!new.target) { throw new TypeError('Promise must be called as a constructor'); } if (typeof executor !== 'function') { throw new TypeError('executor must be callable'); } var _this = this; def(this, promiseMarker, true, ''); def(this, 'state', void 0); // undefined (pending), true/false def(this, 'value', void 0); def(this, 'fulfillReactions', []); def(this, 'rejectReactions', []); def(this, 'isHandled', false); // XXX: roll into 'state' to minimize fields compact(this); var t = createResolutionFunctions(this); try { void executor(t.resolve, t.reject); } catch (e) { t.reject(e); } }; var proto = cons.prototype; def(cons, 'prototype', proto, ''); def(cons, 'potentiallyUnhandled', [], ''); // %Promise%.resolve(). // XXX: direct handling function resolve(val) { if (isPromise(val) && val.constructor === this) { return val; } return new Promise(function (resolve, reject) { resolve(val); }); } // %Promise%.reject() // XXX: direct handling function reject(val) { return new Promise(function (resolve, reject) { reject(val); }); } // %Promise%.all(). function all(list) { if (!Array.isArray(list)) { throw new TypeError('non-array all() argument not supported'); } var resolveFn, rejectFn; var p = new Promise(function (resolve, reject) { resolveFn = resolve; rejectFn = reject; }); var values = []; var index = 0; var remaining = 1; // remaining intentionally 1, not 0 list.forEach(function (x) { // XXX: no iterator support var t = Promise.resolve(x); var f = function promiseAllElement(val) { var F = promiseAllElement; if (F.alreadyCalled) { return; } F.alreadyCalled = true; values[F.index] = val; if (--remaining === 0) { resolveFn.call(void 0, values); } }; // In ES2015 the functions would reference a shared state object // explicitly. Here the conceptual state is in scope. f.index = index++; remaining++; t.then(f, rejectFn); }); if (--remaining === 0) { resolveFn.call(void 0, values); } return p; } // %Promise%.race(). function race(list) { if (!Array.isArray(list)) { throw new TypeError('non-array race() argument not supported'); } var resolveFn, rejectFn; var p = new Promise(function (resolve, reject) { resolveFn = resolve; rejectFn = reject; }); list.forEach(function (x) { // XXX: no iterator support var t = Promise.resolve(x); var func = t.then; var optimized = (func === then) && allowOptimization; if (optimized) { // If the .then() of the Promise.resolve() is the original // built-in implementation, we don't need to queue the actual // resolve and reject functions explicitly because (1) the // functions don't leak and can't be called by anyone else, // and (2) the onFulfilled/onRejected functions would just // directly forward the result from 't' to 'p'. optimizedThen(t, p); } else { // Generic case, the result Promise of .then() is ignored. void func.call(t, resolveFn, rejectFn); } }); return p; } // %PromisePrototype%.then(), also used for .catch(). function then(onFulfilled, onRejected) { // No subclassing support here now, no NewPromiseCapability() handling. requirePromise(this); var resolveFn, rejectFn; var p = new Promise(function (resolve, reject) { resolveFn = resolve; rejectFn = reject; }); var optimized = allowOptimization; if (typeof onFulfilled !== 'function') { onFulfilled = 'Identity'; } if (typeof onRejected !== 'function') { onRejected = 'Thrower'; } if (this.state === void 0) { // pending if (optimized) { this.fulfillReactions.push({ handler: onFulfilled, target: p }); this.rejectReactions.push({ handler: onRejected, target: p }); } else { this.fulfillReactions.push({ handler: onFulfilled, resolve: resolveFn, reject: rejectFn }); this.rejectReactions.push({ handler: onRejected, resolve: resolveFn, reject: rejectFn }); } } else if (this.state) { // fulfilled if (optimized) { enqueueJob({ handler: onFulfilled, target: p, value: this.value }); } else { enqueueJob({ handler: onFulfilled, resolve: resolveFn, reject: rejectFn, value: this.value }); } } else { // rejected if (!this.isHandled) { rejectionTracker(this, 'handle'); } if (optimized) { enqueueJob({ handler: onRejected, target: p, value: this.value }); } else { enqueueJob({ handler: onRejected, resolve: resolveFn, reject: rejectFn, value: this.value }); } } this.isHandled = true; return p; } // Optimized .then() where a specific source Promise just forwards its // result to a target Promise unless its already settled. function optimizedThen(source, target) { if (source.state === void 0) { // pending source.fulfillReactions.push({ target: target }); source.rejectReactions.push({ target: target }); } else if (source.state) { // fulfilled enqueueJob({ target: target, value: source.value }); } else { // rejected if (!source.isHandled) { rejectionTracker(source, 'handle'); } enqueueJob({ target: target, value: source.value, rejected: true }); } source.isHandled = true; } // %PromisePrototype%.catch. var _catch = function (onRejected) { return this.then.call(this, void 0, onRejected); }; def(_catch, 'name', 'catch', 'c'); // %Promise%.try(), https://github.com/tc39/proposal-promise-try, // simple polyfill-style implementation. var _try = function (func) { // XXX: check 'this' for callability, or Promise / subclass. return new this(function (resolve, reject) { resolve(func()); }); }; def(_try, 'name', 'try', 'c'); // Emit higher level Node.js/WHATWG like 'reject' and 'handle' events, // filtering out some cases where a rejected Promise is initially unhandled // but is handled within the same "tick" (for a relatively murky definition // of a "tick"). // https://html.spec.whatwg.org/multipage/webappapis.html#unhandled-promise-rejections // https://www.ecma-international.org/ecma-262/8.0/#sec-host-promise-rejection-tracker function checkUnhandledRejections() { var idx; // The unhandledRejection() callbacks may have queued more Promises, // settled existing Promises on the list, etc. Keep going until // the list is empty. Null out entries to allow early GC when the // Promises are no longer reachable. Callbacks may also queue more // ordinary Promise jobs; they are also handled to completion within // the tick. // XXX: It might be more natural to handle the notification callbacks // via the job queue. This might be a bit simpler, but would change // the Promise job vs. unhandledRejection callback ordering a bit. // For example, Node.js emits 'handle' events before the related // catch callbacks are called, while the polyfill in its current // state does not. for (idx = 0; idx < cons.potentiallyUnhandled.length; idx++) { var p = cons.potentiallyUnhandled[idx]; cons.potentiallyUnhandled[idx] = null; // For consistency with hook calls from HostPromiseRejectionTracker // errors from user callback are silently eaten. If a process exit // is desirable, user callback may call a custom native binding to // do that ("process.exit(1)" or similar). // // Use a custom object argument convention for flexibility. if (p.unhandled === 1) { safeCallUnhandledRejection({ promise: p, event: 'reject', reason: p.value }); def(p, 'unhandled', 2); } else if (p.unhandled === 3) { safeCallUnhandledRejection({ promise: p, event: 'handle', reason: p.value }); delete p.unhandled; } } cons.potentiallyUnhandled.length = 0; return idx > 0; // true if we processed entries } // Define visible objects and properties. (function () { def(this, 'Promise', cons); def(cons, 'resolve', resolve); def(cons, 'reject', reject); def(cons, 'all', all); def(cons, 'race', race); def(cons, 'try', _try); def(cons, 'isPolyfill', true); // needed by e.g. testcases def(proto, 'then', then); def(proto, 'catch', _catch); if (haveSymbols) { def(proto, Symbol.toStringTag, 'Promise', 'c'); } // Custom API to drive the "job queue". We only want to exit when // there are no more Promise jobs or unhandledRejection() callbacks, // i.e. no more work to do. Note that an unhandledRejection() // callback may queue more Promise job entries and vice versa. def(cons, 'runQueue', function _runQueueUntilEmpty() { do { while (runQueueEntry()) {} checkUnhandledRejections(); } while(!(queueEmpty() && cons.potentiallyUnhandled.length === 0)); }); def(cons, 'unhandledRejection', nop); compact(this); compact(cons); compact(proto); }()); }());