Source code

Revision control

Copy as Markdown

Other Tools

// |jit-test| skip-if: getBuildConfiguration("release_or_beta")
//
// JS::SafeResolve: promise-like and non-native-promise resolution values must
// be handled via a deferred microtask, so that Get(resolution, "then") never
// runs on the caller's stack.
// ---------------------------------------------------------------------------
// Plain thenable with a callable data "then" property on the object itself.
{
const {promise} = Promise.withResolvers();
const observations = [];
const thenable = {
then(onFulfilled, onRejected) {
observations.push("then-called");
onFulfilled(42);
},
};
safeResolvePromise(promise, thenable);
// Neither the getter nor the returned function should be called.
assertEq(observations.length, 0);
let settled;
promise.then(v => { settled = v; });
drainJobQueue();
assertEq(observations[0], "then-called");
assertEq(settled, 42);
}
// ---------------------------------------------------------------------------
// Thenable where "then" is an accessor (getter). Pure lookup can't read it;
// SafeResolve must defer and the getter runs in the job, not synchronously.
{
const {promise} = Promise.withResolvers();
const log = [];
const thenable = {
get then() {
log.push("get-then");
return function(onFulfilled) {
log.push("call-then");
onFulfilled("from-getter");
};
},
};
safeResolvePromise(promise, thenable);
// The getter must not have run yet.
assertEq(log.length, 0);
let settled;
promise.then(v => { settled = v; });
drainJobQueue();
assertEq(log.join(","), "get-then,call-then");
assertEq(settled, "from-getter");
}
// ---------------------------------------------------------------------------
// Thenable where "then" lives on the prototype (inherited). Must be treated
// as callable and deferred.
{
const {promise} = Promise.withResolvers();
const log = [];
class Thenable {
then(onFulfilled) {
log.push("proto-then");
onFulfilled("proto-value");
}
}
const thenable = new Thenable();
safeResolvePromise(promise, thenable);
assertEq(log.length, 0);
let settled;
promise.then(v => { settled = v; });
drainJobQueue();
assertEq(log[0], "proto-then");
assertEq(settled, "proto-value");
}
// ---------------------------------------------------------------------------
// Proxy with a "then" trap. Proxies MUST force deferral regardless of trap
// behavior, because any MOP access could run user code.
{
const {promise} = Promise.withResolvers();
const log = [];
const target = {};
const handler = {
get(t, name) {
log.push("proxy-get:" + String(name));
if (name === "then") {
return function(onFulfilled) {
log.push("proxy-then");
onFulfilled("proxy-value");
};
}
return t[name];
},
};
const thenable = new Proxy(target, handler);
safeResolvePromise(promise, thenable);
// No MOP access should have happened yet.
assertEq(log.length, 0);
let settled;
promise.then(v => { settled = v; });
drainJobQueue();
// In the microtask, Get(resolution, "then") happens exactly once, then the
// assimilation job calls the returned function (a separate MOP access —
// "get" trap is not invoked for calling a function already retrieved).
assertEq(log[0], "proxy-get:then");
assertEq(log[1], "proxy-then");
assertEq(settled, "proxy-value");
}
// ---------------------------------------------------------------------------
// Revoked proxy: Get("then") throws. The rejection must happen in the job,
// not synchronously.
{
const {promise} = Promise.withResolvers();
const {proxy, revoke} = Proxy.revocable({}, {});
revoke();
safeResolvePromise(promise, proxy);
let result = null;
promise.then(v => { result = {fulfilled: v}; },
e => { result = {rejected: e}; });
drainJobQueue();
assertEq(result !== null, true);
assertEq("rejected" in result, true);
}
// ---------------------------------------------------------------------------
// Proxy whose "then" is NOT callable. Must still defer, then fulfill with the
// object itself (because IsCallable(then) is false in PerformPromiseResolution).
{
const {promise} = Promise.withResolvers();
const target = {};
const proxy = new Proxy(target, {
get(t, name) { return name === "then" ? 42 : t[name]; },
});
safeResolvePromise(promise, proxy);
let settled;
promise.then(v => { settled = v; });
drainJobQueue();
assertEq(settled, proxy);
}
// ---------------------------------------------------------------------------
// A real Promise as the resolution value, with an observable own "then" so we
// can verify the call happens in the deferred job, not on the caller's stack.
{
const {promise} = Promise.withResolvers();
const inner = Promise.resolve("inner-value");
const observations = [];
const originalThen = Promise.prototype.then;
Object.defineProperty(inner, "then", {
value: function(...args) {
observations.push("then-called");
return originalThen.apply(this, args);
},
writable: true, configurable: true,
});
safeResolvePromise(promise, inner);
assertEq(observations.length, 0);
let settled;
promise.then(v => { settled = v; });
drainJobQueue();
assertEq(observations[0], "then-called");
assertEq(settled, "inner-value");
}
// ---------------------------------------------------------------------------
// Cross-compartment promise as the resolution value.
{
const g = newGlobal({newCompartment: true});
const {promise} = Promise.withResolvers();
const inner = g.eval("Promise.resolve('cross-realm')");
safeResolvePromise(promise, inner);
let settled;
promise.then(v => { settled = v; });
drainJobQueue();
assertEq(settled, "cross-realm");
}