-
-
Save mhofman/e4031aa4cd2375d0f151f62691724475 to your computer and use it in GitHub Desktop.
Test cases for promise and resolver collection
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @ts-check | |
if (typeof console === "undefined") { | |
globalThis.console = { | |
log() { | |
print(...arguments); | |
}, | |
}; | |
} | |
let queueGCJob; | |
if (typeof setTimeout === "function") { | |
queueGCJob = function () { | |
return new Promise(function (resolve) { | |
setTimeout(resolve, 0); | |
}); | |
}; | |
} else if (typeof drainJobQueue === "function") { | |
queueGCJob = function () { | |
return new Promise(function (resolve) { | |
drainJobQueue(); | |
resolve(); | |
}); | |
}; | |
} else { | |
queueGCJob = function () { | |
return Promise.resolve(); | |
}; | |
} | |
const setupGC = async () => { | |
let gc = globalThis.gc || (typeof $262 !== "undefined" ? $262.gc : null); | |
if (!gc) { | |
try { | |
const [{ ["default"]: v8 }, { ["default"]: vm }] = await Promise.all([ | |
import("v8"), | |
import("vm"), | |
]); | |
v8.setFlagsFromString("--expose_gc"); | |
gc = vm.runInNewContext("gc"); | |
v8.setFlagsFromString("--no-expose_gc"); | |
} catch (err) { | |
gc = () => void Array.from({ length: 2 ** 24 }, () => Math.random()); | |
} | |
} | |
globalThis.gc = gc; | |
}; | |
/** @type {FinalizationRegistry<() => void>} */ | |
const fr = new FinalizationRegistry((held) => { | |
held(); | |
}); | |
const forever = new Promise(() => {}); | |
/** | |
* @template {any} [T=any] | |
* @typedef {object} PromiseKit | |
* @property {Promise<T>} promise | |
* @property {(value?: T | PromiseLike<T>) => void} resolve | |
* @property {(err?: any) => void} reject | |
*/ | |
/** | |
* @template [T=any] | |
* @typedef {Pick<PromiseKit<T>, 'reject' | 'resolve'>} Deferred | |
*/ | |
/** | |
* @template T | |
* @returns {PromiseKit<T>} | |
*/ | |
const makePromiseKit = () => { | |
let resolve, reject; | |
const promise = new Promise((res, rej) => { | |
resolve = res; | |
reject = rej; | |
}); | |
// @ts-ignore | |
return { promise, resolve, reject }; | |
}; | |
/** @param {<T>(register: (val: T, name?: string, shouldCollect?: boolean) => T, done: Promise) => PromiseLike | void} testCase */ | |
function test(testCase) { | |
let targets = []; | |
const register = ( | |
val, | |
name = `value${targets.length + 1}`, | |
shouldCollect = true | |
) => { | |
const collected = makePromiseKit(); | |
const targetInfo = { | |
name, | |
shouldCollect, | |
wasCollected: false, | |
onCollected: collected.promise, | |
}; | |
targets.push(targetInfo); | |
collected.promise.then(() => { | |
targetInfo.wasCollected = true; | |
}); | |
fr.register(val, collected.resolve); | |
return val; | |
}; | |
const done = makePromiseKit(); | |
return Promise.resolve(testCase(register, done.promise)) | |
.then(() => { | |
const sentinelCollected = new Promise((resolve) => { | |
fr.register({}, resolve); | |
}); | |
queueGCJob().then(gc); | |
return sentinelCollected; | |
}) | |
.then(() => {}) | |
.then(() => { | |
const result = Object.fromEntries( | |
targets.map(({ name, wasCollected, shouldCollect }) => [ | |
name, | |
{ wasCollected, shouldCollect }, | |
]) | |
); | |
done.resolve(); | |
return result; | |
}); | |
} | |
const resolveWith = (value) => Promise.resolve().then(() => value); | |
const tests = { | |
// The promise and the resolver are dropped, and collected as expected | |
DropPromiseDropResolve: (register) => { | |
register( | |
new Promise((resolve) => { | |
register(resolve, "resolve"); | |
}), | |
"promise" | |
); | |
// Test keeps nothing around | |
}, | |
// // promise is collected by all engines | |
DropPromiseDropResolveWithReaction: (register) => { | |
register( | |
new Promise((resolve) => { | |
register(resolve, "resolve"); | |
}), | |
"promise" | |
).then(() => { | |
console.log("never"); | |
}); | |
}, | |
// The promise is kept but resolver dropped, resolver is collected (except SM) | |
KeepPromiseDropResolve: (register, done) => { | |
const promise = new Promise((resolve) => { | |
register(resolve, "resolve"); | |
}); | |
// Test keeps promise through the test | |
done.then(() => console.log("KeepPromiseDropResolve done:", promise)); | |
}, | |
// The resolver is kept but promise dropped. The promise could be collected. | |
// The promise has no reactions and is not observed by the program | |
KeepResolveDropPromiseNoReactions: (register, done) => { | |
let resolve; | |
register( | |
new Promise((r) => { | |
resolve = r; | |
}), | |
"promise" | |
); | |
// Test keeps the resolver around | |
done.then(() => resolve("foo")); | |
}, | |
// The resolver is kept but promise dropped. The promise should not be collected. | |
// The promise has reactions, and while it will not observed by the program, | |
// Dropping it before settlement may be unexpected (and against our use case) | |
KeepResolveDropPromiseWithReactions: (register, done) => { | |
let resolve; | |
const printResult = (value) => | |
console.log("KeepResolveDropPromiseWithReactions done:", value); | |
register( | |
(() => { | |
const p = new Promise((r) => { | |
resolve = r; | |
}); | |
// add reaction without closing over "p" | |
p.then(printResult); | |
return p; | |
})(), | |
"promise", | |
// Should not collect | |
false | |
); | |
// Test keeps the resolver around | |
done.then(() => resolve("foo")); | |
}, | |
// The resolver is kept but settled promise is dropped. | |
// The promise should be collected (only SM). | |
// The promise no longer has reactions (resolver is inert), | |
// and is not observed by the program. | |
KeepResolveDropSettledPromise: (register, done) => | |
register( | |
new Promise((resolve) => { | |
done.then(resolve); | |
resolve(null); | |
}), | |
"promise" | |
), | |
// `candidate` promise is settled, it should be collected | |
// `raced` promise is settled, it should be collected | |
// `result` promise is settled, and is correctly collected | |
Race: (register) => | |
(() => { | |
const candidate = Promise.resolve("done"); | |
const raced = Promise.race([forever, candidate]); | |
const result = resolveWith(candidate); | |
register(raced, "candidate"); | |
register(raced, "raced"); | |
register(result, "result"); | |
return Promise.allSettled([raced, result]); | |
})().then(([val1, val2]) => { | |
console.log("Race done:", JSON.stringify(val1), JSON.stringify(val2)); | |
}), | |
}; | |
setupGC().then(async () => { | |
Object.entries(tests).reduce( | |
(prev, [name, testCase]) => | |
prev | |
.then(() => test(testCase)) | |
.then((collected) => | |
console.log( | |
name, | |
"collected:", | |
Object.entries(collected) | |
.map( | |
([name, { wasCollected, shouldCollect }]) => | |
`${name}=${wasCollected}(${ | |
shouldCollect !== wasCollected ? "un" : "" | |
}expected)` | |
) | |
.join(", ") | |
) | |
), | |
Promise.resolve() | |
); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#### JavaScriptCore | |
DropPromiseDropResolve collected: resolve=true(expected), promise=true(expected) | |
DropPromiseDropResolveWithReaction collected: resolve=true(expected), promise=true(expected) | |
KeepPromiseDropResolve done: [object Promise] | |
KeepPromiseDropResolve collected: resolve=true(expected) | |
KeepResolveDropPromiseNoReactions collected: promise=false(unexpected) | |
KeepResolveDropPromiseWithReactions collected: promise=false(expected) | |
KeepResolveDropSettledPromise collected: promise=false(unexpected) | |
Race done: {"status":"fulfilled","value":"done"} {"status":"fulfilled","value":"done"} | |
Race collected: candidate=false(unexpected), raced=false(unexpected), result=true(expected) | |
foo: undefined | |
#### Moddable XS, V8 | |
DropPromiseDropResolve collected: resolve=true(expected), promise=true(expected) | |
DropPromiseDropResolveWithReaction collected: resolve=true(expected), promise=true(expected) | |
KeepPromiseDropResolve done: [object Promise] | |
KeepPromiseDropResolve collected: resolve=true(expected) | |
KeepResolveDropPromiseNoReactions collected: promise=false(unexpected) | |
KeepResolveDropPromiseWithReactions done: foo | |
KeepResolveDropPromiseWithReactions collected: promise=false(expected) | |
KeepResolveDropSettledPromise collected: promise=false(unexpected) | |
Race done: {"status":"fulfilled","value":"done"} {"status":"fulfilled","value":"done"} | |
Race collected: candidate=false(unexpected), raced=false(unexpected), result=true(expected) | |
#### SpiderMonkey | |
DropPromiseDropResolve collected: resolve=true(expected), promise=true(expected) | |
DropPromiseDropResolveWithReaction collected: resolve=true(expected), promise=true(expected) | |
KeepPromiseDropResolve done: [object Promise] | |
KeepPromiseDropResolve collected: resolve=false(unexpected) | |
KeepResolveDropPromiseNoReactions collected: promise=false(unexpected) | |
KeepResolveDropPromiseWithReactions done: foo | |
KeepResolveDropPromiseWithReactions collected: promise=false(expected) | |
KeepResolveDropSettledPromise collected: promise=true(expected) | |
Race done: {"status":"fulfilled","value":"done"} {"status":"fulfilled","value":"done"} | |
Race collected: candidate=false(unexpected), raced=false(unexpected), result=true(expected) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment