mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(javascript): keep timer cancellation within the Node handle family (#7712)
* 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.
This commit is contained in:
1 parent
36dcd985e1
commit
923e6f77e3
2 files changed
+61
-1
No files matched your search
@@ -87,3 +87,37 @@ it("keeps cancellation uncertain across a mutable closure capture", () => {
|
||||
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",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -68,6 +68,24 @@ const TIMER_CANCEL_METHODS = [
|
||||
"clearImmediate",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Node keeps two timer handle families. `clearTimeout` and `clearInterval` are
|
||||
* aliases that act only on a `Timeout` from `setTimeout` or `setInterval`, and
|
||||
* `clearImmediate` acts only on an `Immediate` from `setImmediate`. Clearing
|
||||
* within a family always cancels. Clearing across the families is
|
||||
* runtime-dependent: `setImmediate` then `clearTimeout`/`clearInterval` never
|
||||
* cancels, but `setTimeout` then `clearImmediate` reports as cancelled on some
|
||||
* supported Node versions and fires on others. The analysis therefore treats a
|
||||
* cross-family clear as unresolved rather than as proof that the timer kept
|
||||
* running.
|
||||
*/
|
||||
const timerHandleFamily = (
|
||||
method: JavaScriptSemanticTimerOperation["method"],
|
||||
): "timeout" | "immediate" =>
|
||||
method === "setImmediate" || method === "clearImmediate"
|
||||
? "immediate"
|
||||
: "timeout";
|
||||
|
||||
interface AsyncEffectAnalysis {
|
||||
readonly eventOperations: readonly JavaScriptSemanticEventOperation[];
|
||||
readonly timerOperations: readonly JavaScriptSemanticTimerOperation[];
|
||||
@@ -435,7 +453,15 @@ const resolveTimers = (
|
||||
const linked =
|
||||
candidate.handleRead?.origins.flatMap((origin) => {
|
||||
const schedule = schedules.get(origin);
|
||||
return schedule === undefined ? [] : [schedule];
|
||||
// A handle always clears its own family. Clearing across families is
|
||||
// runtime-dependent, so it establishes no link in either direction.
|
||||
if (
|
||||
schedule === undefined ||
|
||||
timerHandleFamily(schedule.method) !==
|
||||
timerHandleFamily(candidate.method)
|
||||
)
|
||||
return [];
|
||||
return [schedule];
|
||||
}) ?? [];
|
||||
const incompleteOrigins =
|
||||
candidate.handleRead !== null &&
|
||||
|
||||
Reference in new issue
Block a user