mirror of https://github.com/svaarala/duktape.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
276 lines
11 KiB
276 lines
11 KiB
/*
|
|
* Minimal ES2015+ Promise polyfill
|
|
*
|
|
* Limitations:
|
|
*
|
|
* - 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.
|
|
* - See XXX in source for more.
|
|
*
|
|
* This polyfill was originally used to gain a better understanding of the
|
|
* ES2015 specification algorithms, before implementing Promises natively.
|
|
* As such this polyfill may not be production quality.
|
|
*
|
|
* See also: https://github.com/stefanpenner/es6-promise#readme
|
|
*/
|
|
|
|
(function () {
|
|
if ('Promise' in this) { return; }
|
|
|
|
// 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;
|
|
var queueTail = null;
|
|
function enqueueJob(job) {
|
|
if (queueHead) {
|
|
queueTail.next = job;
|
|
queueTail = job;
|
|
} else {
|
|
queueHead = queueTail = job;
|
|
}
|
|
}
|
|
function dequeueJob() {
|
|
var ret = queueHead;
|
|
if (ret) {
|
|
queueHead = ret.next;
|
|
if (!queueHead) {
|
|
queueTail = null;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// Helper to define non-enumerable properties.
|
|
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
|
|
});
|
|
}
|
|
|
|
// Promise detection (plain or subclassed Promise), in spec has [[PromiseState]]
|
|
// internal slot which isn't affected by Proxy behaviors etc.
|
|
var symMarker = Symbol('promise');
|
|
function isPromise(p) { return p !== null && typeof p === 'object' && symMarker in p; }
|
|
function requirePromise(p) { if (!isPromise(p)) { throw new TypeError('Promise required'); } }
|
|
|
|
// Raw fulfill/reject operations, assume resolution processing done.
|
|
function doFulfill(p, val) {
|
|
if (p.state !== void 0) { return; } // should not happen
|
|
p.state = true; p.value = val;
|
|
var reactions = p.fulfillReactions;
|
|
delete p.fulfillReactions; delete p.rejectReactions;
|
|
reactions.forEach(function (r) {
|
|
enqueueJob({ handler: r.handler, resolve: r.resolve, reject: r.reject, value: val }); // only value is new
|
|
});
|
|
}
|
|
function doReject(p, val) {
|
|
if (p.state !== void 0) { return; } // should not happen
|
|
p.state = false; p.value = val;
|
|
var reactions = p.rejectReactions;
|
|
delete p.fulfillReactions; delete p.rejectReactions;
|
|
reactions.forEach(function (r) {
|
|
enqueueJob({ handler: r.handler, resolve: r.resolve, reject: r.reject, value: val }); // only value is new
|
|
});
|
|
}
|
|
|
|
// 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 getResolutionFunctions(p) {
|
|
var reject = function (err) {
|
|
if (reject.state.alreadyResolved) { return; }
|
|
reject.state.alreadyResolved = true; // neutralize this resolve/reject pair
|
|
if (p.state !== void 0) { return; }
|
|
doReject(p, err);
|
|
};
|
|
var resolve = function (val) {
|
|
if (resolve.state.alreadyResolved) { return; }
|
|
resolve.state.alreadyResolved = true; // neutralize this resolve/reject pair
|
|
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 = getResolutionFunctions(p);
|
|
return enqueueJob({ thenable: val, then: then, resolve: t.resolve, reject: t.reject });
|
|
// old resolve/reject is neutralized, only the new pair is live
|
|
}
|
|
return doFulfill(p, val);
|
|
} catch (e) {
|
|
return doReject(p, e);
|
|
}
|
|
};
|
|
reject.state = resolve.state = {}; // shared state for [[AlreadyResolved]]
|
|
return { resolve: resolve, reject: reject };
|
|
}
|
|
|
|
// Job queue simulation.
|
|
function runQueueEntry() {
|
|
var job = dequeueJob();
|
|
if (!job) { return false; }
|
|
if (job.then && job.resolve && job.reject) {
|
|
try {
|
|
void job.then.call(job.thenable, job.resolve, job.reject);
|
|
} catch (e) {
|
|
job.reject(e);
|
|
}
|
|
} else if (job.handler && job.resolve && job.reject) {
|
|
try {
|
|
if (job.handler === 'Identity') {
|
|
res = job.value;
|
|
} else if (job.handler === 'Thrower') {
|
|
throw job.value;
|
|
} else {
|
|
res = job.handler.call(void 0, job.value);
|
|
}
|
|
job.resolve(res);
|
|
} catch (e) {
|
|
job.reject(e);
|
|
}
|
|
} else {
|
|
throw new Error('internal error'); // unknown job
|
|
}
|
|
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;
|
|
this[symMarker] = true;
|
|
def(this, 'state', void 0); // undefined (pending), true (fulfilled), false (rejected)
|
|
def(this, 'value', void 0);
|
|
def(this, 'fulfillReactions', []);
|
|
def(this, 'rejectReactions', []);
|
|
var t = getResolutionFunctions(this);
|
|
try {
|
|
void executor(t.resolve, t.reject);
|
|
} catch (e) {
|
|
t.reject(e);
|
|
}
|
|
};
|
|
var proto = cons.prototype;
|
|
Object.defineProperty(cons, 'prototype', { writable: false, enumerable: false, configurable: false });
|
|
|
|
// %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) {
|
|
var resolveFn, rejectFn;
|
|
var p = new Promise(function (resolve, reject) { resolveFn = resolve; rejectFn = reject; });
|
|
var state = { values: [], remaining: 1, resolve: resolveFn, reject: rejectFn }; // remaining intentionally 1, not 0
|
|
var index = 0;
|
|
list.forEach(function (x) { // XXX: no iterator support
|
|
var t = Promise.resolve(x);
|
|
var f = function promiseAllElement(val) {
|
|
var F = promiseAllElement;
|
|
var S = F.state;
|
|
if (F.alreadyCalled) { return; }
|
|
F.alreadyCalled = true;
|
|
S.values[F.index] = val;
|
|
if (--S.remaining === 0) {
|
|
S.resolve.call(void 0, S.values);
|
|
}
|
|
};
|
|
f.state = state;
|
|
f.index = index++;
|
|
state.remaining++;
|
|
t.then(f, rejectFn);
|
|
});
|
|
if (--state.remaining === 0) {
|
|
resolveFn.call(void 0, state.values);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
// %Promise%.race().
|
|
function race(list) {
|
|
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);
|
|
t.then(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; });
|
|
onFulfilled = (typeof onFulfilled === 'function' ? onFulfilled : 'Identity');
|
|
onRejected = (typeof onRejected === 'function' ? onRejected : 'Thrower');
|
|
if (this.state === void 0) { // pending
|
|
this.fulfillReactions.push({ handler: onFulfilled, resolve: resolveFn, reject: rejectFn });
|
|
this.rejectReactions.push({ handler: onRejected, resolve: resolveFn, reject: rejectFn });
|
|
} else if (this.state) { // fulfilled
|
|
enqueueJob({ handler: onFulfilled, resolve: resolveFn, reject: rejectFn, value: this.value });
|
|
} else { // rejected
|
|
enqueueJob({ handler: onRejected, resolve: resolveFn, reject: rejectFn, value: this.value });
|
|
}
|
|
return p;
|
|
}
|
|
|
|
// %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');
|
|
|
|
// 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(proto, 'then', then);
|
|
def(proto, 'catch', _catch);
|
|
def(proto, Symbol.toStringTag, 'Promise', 'c');
|
|
|
|
// Not part of the actual Promise API, but used to drive the "job queue".
|
|
def(cons, 'runQueue', function _runQueueUntilEmpty() {
|
|
while (runQueueEntry()) {}
|
|
});
|
|
}());
|
|
}());
|
|
|