mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 23:51:23 +00:00
test: add abort .bind() behavioral tests (#7174)
This commit is contained in:
committed by
Peter Steinberger
parent
d9c582627c
commit
5ac8d1d2bb
72
src/infra/abort-pattern.test.ts
Normal file
72
src/infra/abort-pattern.test.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression test for #7174: Memory leak from closure-wrapped controller.abort().
|
||||||
|
*
|
||||||
|
* Using `() => controller.abort()` creates a closure that captures the
|
||||||
|
* surrounding lexical scope (controller, timer, locals). In long-running
|
||||||
|
* processes these closures accumulate and prevent GC.
|
||||||
|
*
|
||||||
|
* The fix is `controller.abort.bind(controller)` which creates a minimal
|
||||||
|
* bound function with no scope capture.
|
||||||
|
*
|
||||||
|
* This test verifies the behavioral equivalence of .bind() for both the
|
||||||
|
* setTimeout and addEventListener use-cases.
|
||||||
|
*/
|
||||||
|
describe("abort pattern: .bind() vs arrow closure (#7174)", () => {
|
||||||
|
it("controller.abort.bind(controller) aborts the signal", () => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const boundAbort = controller.abort.bind(controller);
|
||||||
|
expect(controller.signal.aborted).toBe(false);
|
||||||
|
boundAbort();
|
||||||
|
expect(controller.signal.aborted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bound abort works with setTimeout", async () => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(controller.abort.bind(controller), 10);
|
||||||
|
expect(controller.signal.aborted).toBe(false);
|
||||||
|
await new Promise((r) => setTimeout(r, 50));
|
||||||
|
expect(controller.signal.aborted).toBe(true);
|
||||||
|
clearTimeout(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bound abort works as addEventListener callback and can be removed", () => {
|
||||||
|
const parent = new AbortController();
|
||||||
|
const child = new AbortController();
|
||||||
|
const onAbort = child.abort.bind(child);
|
||||||
|
|
||||||
|
parent.signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
expect(child.signal.aborted).toBe(false);
|
||||||
|
|
||||||
|
parent.abort();
|
||||||
|
expect(child.signal.aborted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removeEventListener works with saved .bind() reference", () => {
|
||||||
|
const parent = new AbortController();
|
||||||
|
const child = new AbortController();
|
||||||
|
const onAbort = child.abort.bind(child);
|
||||||
|
|
||||||
|
parent.signal.addEventListener("abort", onAbort);
|
||||||
|
// Remove before parent aborts — child should NOT be aborted
|
||||||
|
parent.signal.removeEventListener("abort", onAbort);
|
||||||
|
parent.abort();
|
||||||
|
expect(child.signal.aborted).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bound abort forwards abort through combined signals", () => {
|
||||||
|
// Simulates the combineAbortSignals pattern from pi-tools.abort.ts
|
||||||
|
const signalA = new AbortController();
|
||||||
|
const signalB = new AbortController();
|
||||||
|
const combined = new AbortController();
|
||||||
|
|
||||||
|
const onAbort = combined.abort.bind(combined);
|
||||||
|
signalA.signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
signalB.signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
|
||||||
|
expect(combined.signal.aborted).toBe(false);
|
||||||
|
signalA.abort();
|
||||||
|
expect(combined.signal.aborted).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user