mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
* fix(javascript): keep timer cancellation within Node's handle family Node keeps timeouts and immediates in disjoint handle families. clearTimeout and clearInterval are aliases that act only on a Timeout from setTimeout or setInterval, while clearImmediate acts only on an Immediate from setImmediate. Crossing the families returns without doing anything, so the scheduled callback still runs. REA linked a cancellation whenever the cleared binding traced back to any timer schedule, so clearImmediate(setTimeout(work, 10)) reported a complete cancellation and the graph emitted a resolved cancels-timer relation. It also flagged no uncertainty for it. Require the cancel and the schedule to share a handle family before linking them. A cross-family clear now keeps linkedTimerId null and resolution unresolved, and the graph reports the cancellation as a candidate with an ambiguous-target unknown instead of a resolved fact. Verified against Node v24.22.1 over all nine schedule/cancel pairs: setTimeout and setInterval with clearTimeout or clearInterval cancel, and setImmediate with clearImmediate cancels; the other five combinations leave the callback running. Node's own lib/timers.js shows why: clearTimeout requires timer?._onTimeout, clearImmediate requires immediate?._onImmediate and returns early otherwise, and clearInterval delegates to clearTimeout. * chore: retrigger title and readiness checks after the title was corrected * fix(javascript): say cross-family timer clearing is runtime-dependent Review feedback from @morluto: the previous wording claimed clearing across the timer handle families "returns without doing anything, so the timer keeps running". That does not hold on every supported Node version. Measured on Node 24.22.1, all three cross-family pairs fire: | scheduled with | cancelled with | Node 24.22.1 | | --- | --- | --- | | `setTimeout` | `clearImmediate` | fires | | `setImmediate` | `clearTimeout` | fires | | `setImmediate` | `clearInterval` | fires | `clearImmediate` cancelling a `Timeout` on Node 22 is the case the old claim missed. Within a family the clear always cancels, which is why the analysis links only same-family pairs; across families the outcome depends on the runtime, which is exactly why the analysis reports `unresolved` rather than `complete` in either direction. The behavior and the tests are unchanged. The `timerHandleFamily` doc comment, the linked-origin comment, and the test comment now state the runtime dependence and why it justifies an unresolved result.
124 lines
3.6 KiB
TypeScript
124 lines
3.6 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
|
|
|
const timerOperations = (source: string) =>
|
|
analyzeJavaScriptSemantics(source).timerOperations;
|
|
|
|
it("does not link cancellation to a timer after its handle is cleared", () => {
|
|
const operations = timerOperations(`
|
|
let handle = setTimeout(work, 10);
|
|
handle = null;
|
|
clearTimeout(handle);
|
|
`);
|
|
|
|
expect(operations.at(-1)).toMatchObject({
|
|
kind: "cancel",
|
|
linkedTimerId: null,
|
|
resolution: "unresolved",
|
|
});
|
|
});
|
|
|
|
it("links cancellation to the latest preceding timer assignment", () => {
|
|
const operations = timerOperations(`
|
|
let handle = setTimeout(first, 10);
|
|
handle = setTimeout(second, 20);
|
|
clearTimeout(handle);
|
|
`);
|
|
|
|
expect(operations[2]).toMatchObject({
|
|
kind: "cancel",
|
|
linkedTimerId: operations[1]?.timerId,
|
|
resolution: "complete",
|
|
});
|
|
});
|
|
|
|
it("does not link cancellation to a timer scheduled later", () => {
|
|
const operations = timerOperations(`
|
|
let handle;
|
|
clearTimeout(handle);
|
|
handle = setTimeout(work, 10);
|
|
`);
|
|
|
|
expect(operations[0]).toMatchObject({
|
|
kind: "cancel",
|
|
linkedTimerId: null,
|
|
resolution: "unresolved",
|
|
});
|
|
});
|
|
|
|
it("preserves an immutable timer handle captured by a closure", () => {
|
|
const operations = timerOperations(`
|
|
const handle = setTimeout(work, 10);
|
|
const cancel = () => clearTimeout(handle);
|
|
`);
|
|
|
|
expect(operations[1]).toMatchObject({
|
|
kind: "cancel",
|
|
linkedTimerId: operations[0]?.timerId,
|
|
resolution: "complete",
|
|
});
|
|
});
|
|
|
|
it("keeps cancellation uncertain after a conditional handle replacement", () => {
|
|
const operations = timerOperations(`
|
|
let handle = setTimeout(work, 10);
|
|
if (condition) handle = null;
|
|
clearTimeout(handle);
|
|
`);
|
|
|
|
expect(operations.at(-1)).toMatchObject({
|
|
kind: "cancel",
|
|
linkedTimerId: operations[0]?.timerId,
|
|
resolution: "partial",
|
|
});
|
|
});
|
|
|
|
it("keeps cancellation uncertain across a mutable closure capture", () => {
|
|
const operations = timerOperations(`
|
|
let handle = setTimeout(work, 10);
|
|
const cancel = () => clearTimeout(handle);
|
|
handle = null;
|
|
`);
|
|
|
|
expect(operations[1]).toMatchObject({
|
|
kind: "cancel",
|
|
linkedTimerId: operations[0]?.timerId,
|
|
resolution: "partial",
|
|
});
|
|
});
|
|
|
|
it("keeps cancellation within the handle family that Node clears", () => {
|
|
// Node keeps timeouts and immediates in separate handle families:
|
|
// clearTimeout/clearInterval act only on a Timeout, clearImmediate only on an
|
|
// Immediate. Clearing within a family always cancels; clearing across them is
|
|
// runtime-dependent (setImmediate then clearTimeout never cancels, but
|
|
// setTimeout then clearImmediate does on some supported Node versions), so a
|
|
// cross-family clear is retained as unresolved rather than as proven either way.
|
|
const pairs = [
|
|
["setTimeout", "clearTimeout", true],
|
|
["setTimeout", "clearInterval", true],
|
|
["setTimeout", "clearImmediate", false],
|
|
["setInterval", "clearTimeout", true],
|
|
["setInterval", "clearInterval", true],
|
|
["setInterval", "clearImmediate", false],
|
|
["setImmediate", "clearTimeout", false],
|
|
["setImmediate", "clearInterval", false],
|
|
["setImmediate", "clearImmediate", true],
|
|
] as const;
|
|
|
|
for (const [schedule, cancel, linked] of pairs) {
|
|
const operations = timerOperations(`
|
|
const handle = ${schedule}(work, 10);
|
|
${cancel}(handle);
|
|
`);
|
|
|
|
expect(operations[1], `${schedule} then ${cancel}`).toMatchObject({
|
|
kind: "cancel",
|
|
method: cancel,
|
|
linkedTimerId: linked ? operations[0]?.timerId : null,
|
|
resolution: linked ? "complete" : "unresolved",
|
|
});
|
|
}
|
|
});
|