Files
jsrt/lib/stdlib/promise.js
T
2026-10-26 12:39:58 +00:00

202 lines
6.1 KiB
JavaScript

// Promises, as the spec says, except:
// - A throw in a handler or an executor does not reject, because there is no
// `try`. `reject()` and a returned rejected promise do
// - An unhandled rejection does nothing. Node prints it and stops
// - `Promise.all` takes an array, not any iterable
//
// Written for type inference, which names an object by the instr that makes
// it and the call place of the call it is made in (see compiler/src/infer):
// - A promise is an object literal, made in the fn that user code calls, not
// in a helper. So each place in user code gets its own promise objects, and
// their fields are always there
// - The closures that run a reaction are made in `then`, for the same reason
// - Fulfilment and rejection have their own fns and fields, so that a result
// and a reason never share a type
// - A fn that makes a closure keeps out of the common path: a captured arg is
// in a box, which every call from one place shares
import { __queue_job } from "jsrt:loop";
// The states of a promise
const __PENDING = 0;
const __FULFILLED = 1;
const __REJECTED = 2;
export function Promise(executor) {
const p = { __proto__: this.__proto__, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
if (executor !== undefined) {
executor((value) => __resolve(p, value), (reason) => __reject(p, reason));
}
return p;
}
Promise.resolve = function (value) {
if (__is_promise(value)) return value;
const p = { __proto__: this.prototype, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
__resolve(p, value);
return p;
};
Promise.reject = function (reason) {
const p = { __proto__: this.prototype, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
__reject(p, reason);
return p;
};
Promise.all = function (promises) {
const p = { __proto__: this.prototype, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
const values = [];
let left = promises.length;
if (left === 0) __resolve(p, values);
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(
(value) => {
values[i] = value;
left--;
if (left === 0) __resolve(p, values);
},
(reason) => __reject(p, reason),
);
}
return p;
};
Promise.prototype = {
then(onFulfilled, onRejected) {
const derived = { __proto__: this.__proto__, state: __PENDING, value: undefined, reason: undefined, resolved: false, reactions: [] };
let value;
let reason;
const fulfilled = () => {
if (typeof onFulfilled === "function") __resolve(derived, onFulfilled(value));
else __resolve(derived, value);
};
const rejected = () => {
if (typeof onRejected === "function") __resolve(derived, onRejected(reason));
else __reject(derived, reason);
};
// What `__fulfil()` and `__settle_rejected()` call: each runs its
// handler in a job of its own
const reaction = {
fulfil: (v) => {
value = v;
__queue_job(fulfilled);
},
reject: (r) => {
reason = r;
__queue_job(rejected);
},
};
if (this.state === __PENDING) {
this.reactions.push(reaction);
} else if (this.state === __FULFILLED) {
reaction.fulfil(this.value);
} else {
reaction.reject(this.reason);
}
return derived;
},
catch(onRejected) {
return this.then(undefined, onRejected);
},
finally(f) {
return this.then(
(value) => {
f();
return value;
},
(reason) => {
f();
return Promise.reject(reason);
},
);
},
};
export function __is_promise(x) {
return typeof x === "object" && x !== null && x.reactions !== undefined;
}
// Resolve `p` with `value`, unless it was resolved or rejected before
export function __resolve(p, value) {
if (p.resolved) return;
p.resolved = true;
__fulfil_or_adopt(p, value);
}
// Reject `p` with `reason`, unless it was resolved or rejected before
export function __reject(p, reason) {
if (p.resolved) return;
p.resolved = true;
__settle_rejected(p, reason);
}
// Fulfil `p` with `value`, or if `value` is a thenable, adopt its state. That
// is in a job of its own, as the spec says, so it takes a tick more
function __fulfil_or_adopt(p, value) {
if ((typeof value === "object" && value !== null) || typeof value === "function") {
const then = value.then;
if (typeof then === "function") {
__queue_adopt(p, value);
return;
}
}
__fulfil(p, value);
}
// A closure boxes the args that it captures, and the box is one for every
// call from one place. So only this fn captures `p` and `value`, and only
// thenables share its boxes
function __queue_adopt(p, thenable) {
__queue_job(() => __adopt(p, thenable));
}
// The reactions are cleared in place: a new array here would be one array
// for every promise
function __fulfil(p, value) {
p.state = __FULFILLED;
p.value = value;
const reactions = p.reactions;
for (let i = 0; i < reactions.length; i++) reactions[i].fulfil(value);
reactions.length = 0;
}
function __settle_rejected(p, reason) {
p.state = __REJECTED;
p.reason = reason;
const reactions = p.reactions;
for (let i = 0; i < reactions.length; i++) reactions[i].reject(reason);
reactions.length = 0;
}
// Make `p` follow the thenable `thenable`
function __adopt(p, thenable) {
thenable.then(
(value) => {
if (p.state === __PENDING) __fulfil_or_adopt(p, value);
},
(reason) => {
if (p.state === __PENDING) __settle_rejected(p, reason);
},
);
}
export function queueMicrotask(f) {
__queue_job(f);
}