@crewhaus/abort-controller 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +11 -0
- package/dist/index.js +64 -0
- package/package.json +8 -5
- package/src/index.test.ts +0 -271
- package/src/index.ts +0 -81
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type AbortTree = {
|
|
2
|
+
readonly signal: AbortSignal;
|
|
3
|
+
abort(reason?: unknown): void;
|
|
4
|
+
child(): AbortTree;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Build an `AbortTree` rooted at the given parent (or no parent for a fresh
|
|
8
|
+
* root). The returned tree's `child()` produces children whose `signal`
|
|
9
|
+
* fires when this tree's `signal` fires.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createAbortTree(parent?: AbortSignal): AbortTree;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog R1 `abort-controller` — parent/child cancellation tree.
|
|
3
|
+
*
|
|
4
|
+
* Semantics:
|
|
5
|
+
* - Parent abort cascades to all children (recursively).
|
|
6
|
+
* - Sibling abort does NOT propagate.
|
|
7
|
+
* - Child abort does NOT propagate up to the parent.
|
|
8
|
+
* - If `parent` is already aborted at construction time, the child is born aborted.
|
|
9
|
+
*
|
|
10
|
+
* Memory: parent → child propagation goes through WeakRefs so an abandoned
|
|
11
|
+
* child doesn't pin the parent, and a finalized child's listener is removed
|
|
12
|
+
* from the parent on its own abort. Mirrors the proven pattern in
|
|
13
|
+
* `claude-code/utils/abortController.ts`.
|
|
14
|
+
*
|
|
15
|
+
* Listener limit: Node's default of 10 trips a warning when a busy turn fans
|
|
16
|
+
* out into many tool calls; we raise it to 50 per signal.
|
|
17
|
+
*/
|
|
18
|
+
import { setMaxListeners } from "node:events";
|
|
19
|
+
const MAX_LISTENERS_PER_SIGNAL = 50;
|
|
20
|
+
/**
|
|
21
|
+
* Build an `AbortTree` rooted at the given parent (or no parent for a fresh
|
|
22
|
+
* root). The returned tree's `child()` produces children whose `signal`
|
|
23
|
+
* fires when this tree's `signal` fires.
|
|
24
|
+
*/
|
|
25
|
+
export function createAbortTree(parent) {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
setMaxListeners(MAX_LISTENERS_PER_SIGNAL, controller.signal);
|
|
28
|
+
if (parent !== undefined) {
|
|
29
|
+
if (parent.aborted) {
|
|
30
|
+
controller.abort(parent.reason);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
attachParent(parent, controller);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
abort(reason) {
|
|
39
|
+
controller.abort(reason);
|
|
40
|
+
},
|
|
41
|
+
child() {
|
|
42
|
+
return createAbortTree(controller.signal);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Wire `parent` so its abort propagates to `childCtl`. Uses WeakRefs both ways
|
|
48
|
+
* so neither the parent nor the listener pins the other.
|
|
49
|
+
*/
|
|
50
|
+
function attachParent(parent, childCtl) {
|
|
51
|
+
const weakChild = new WeakRef(childCtl);
|
|
52
|
+
function onParentAbort() {
|
|
53
|
+
const ctl = weakChild.deref();
|
|
54
|
+
if (ctl !== undefined && !ctl.signal.aborted) {
|
|
55
|
+
ctl.abort(this.reason);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
parent.addEventListener("abort", onParentAbort, { once: true });
|
|
59
|
+
// When the child aborts on its own, drop the parent listener so the parent
|
|
60
|
+
// doesn't accumulate dead listeners across many children.
|
|
61
|
+
childCtl.signal.addEventListener("abort", () => {
|
|
62
|
+
parent.removeEventListener("abort", onParentAbort);
|
|
63
|
+
}, { once: true });
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/abort-controller",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Parent/child abort tree with WeakRef cascade for cooperative cancellation",
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
10
13
|
},
|
|
11
14
|
"scripts": {
|
|
12
15
|
"test": "bun test src"
|
|
@@ -29,5 +32,5 @@
|
|
|
29
32
|
"publishConfig": {
|
|
30
33
|
"access": "public"
|
|
31
34
|
},
|
|
32
|
-
"files": ["
|
|
35
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
33
36
|
}
|
package/src/index.test.ts
DELETED
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { getEventListeners } from "node:events";
|
|
3
|
-
import { createAbortTree } from "./index";
|
|
4
|
-
|
|
5
|
-
describe("createAbortTree — basics", () => {
|
|
6
|
-
test("root has an unsignalled signal", () => {
|
|
7
|
-
const root = createAbortTree();
|
|
8
|
-
expect(root.signal.aborted).toBe(false);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("aborting root signals its signal", () => {
|
|
12
|
-
const root = createAbortTree();
|
|
13
|
-
root.abort();
|
|
14
|
-
expect(root.signal.aborted).toBe(true);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("abort reason propagates", () => {
|
|
18
|
-
const root = createAbortTree();
|
|
19
|
-
root.abort(new Error("test reason"));
|
|
20
|
-
expect((root.signal.reason as Error).message).toBe("test reason");
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("parent → child cascade", () => {
|
|
25
|
-
test("parent abort cascades to child", () => {
|
|
26
|
-
const root = createAbortTree();
|
|
27
|
-
const child = root.child();
|
|
28
|
-
expect(child.signal.aborted).toBe(false);
|
|
29
|
-
root.abort();
|
|
30
|
-
expect(child.signal.aborted).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("parent abort cascades to grandchild", () => {
|
|
34
|
-
const root = createAbortTree();
|
|
35
|
-
const child = root.child();
|
|
36
|
-
const grandchild = child.child();
|
|
37
|
-
root.abort();
|
|
38
|
-
expect(child.signal.aborted).toBe(true);
|
|
39
|
-
expect(grandchild.signal.aborted).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("siblings are independent", () => {
|
|
43
|
-
const root = createAbortTree();
|
|
44
|
-
const a = root.child();
|
|
45
|
-
const b = root.child();
|
|
46
|
-
a.abort();
|
|
47
|
-
expect(a.signal.aborted).toBe(true);
|
|
48
|
-
expect(b.signal.aborted).toBe(false);
|
|
49
|
-
expect(root.signal.aborted).toBe(false);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("child abort does NOT propagate to parent", () => {
|
|
53
|
-
const root = createAbortTree();
|
|
54
|
-
const child = root.child();
|
|
55
|
-
child.abort();
|
|
56
|
-
expect(child.signal.aborted).toBe(true);
|
|
57
|
-
expect(root.signal.aborted).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("child constructed from already-aborted parent is born aborted", () => {
|
|
61
|
-
const root = createAbortTree();
|
|
62
|
-
root.abort();
|
|
63
|
-
const child = root.child();
|
|
64
|
-
expect(child.signal.aborted).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("createAbortTree(parent) — external parent signal", () => {
|
|
68
|
-
const ctl = new AbortController();
|
|
69
|
-
const tree = createAbortTree(ctl.signal);
|
|
70
|
-
expect(tree.signal.aborted).toBe(false);
|
|
71
|
-
ctl.abort();
|
|
72
|
-
expect(tree.signal.aborted).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("reason propagation", () => {
|
|
77
|
-
test("parent abort reason cascades to child and grandchild", () => {
|
|
78
|
-
const root = createAbortTree();
|
|
79
|
-
const child = root.child();
|
|
80
|
-
const grandchild = child.child();
|
|
81
|
-
const reason = new Error("cascade reason");
|
|
82
|
-
root.abort(reason);
|
|
83
|
-
expect(child.signal.reason).toBe(reason);
|
|
84
|
-
expect(grandchild.signal.reason).toBe(reason);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("already-aborted parent passes its reason to a freshly born child", () => {
|
|
88
|
-
const root = createAbortTree();
|
|
89
|
-
const reason = new Error("born-aborted reason");
|
|
90
|
-
root.abort(reason);
|
|
91
|
-
const child = root.child();
|
|
92
|
-
expect(child.signal.aborted).toBe(true);
|
|
93
|
-
expect(child.signal.reason).toBe(reason);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("child's own abort reason does not leak to the parent", () => {
|
|
97
|
-
const root = createAbortTree();
|
|
98
|
-
const child = root.child();
|
|
99
|
-
child.abort(new Error("child-only reason"));
|
|
100
|
-
expect(root.signal.aborted).toBe(false);
|
|
101
|
-
expect(root.signal.reason).toBeUndefined();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("abort with no reason yields the default DOMException reason on cascade", () => {
|
|
105
|
-
const root = createAbortTree();
|
|
106
|
-
const child = root.child();
|
|
107
|
-
root.abort();
|
|
108
|
-
expect(child.signal.aborted).toBe(true);
|
|
109
|
-
// AbortController with no explicit reason produces an AbortError DOMException.
|
|
110
|
-
expect(child.signal.reason).toBeInstanceOf(DOMException);
|
|
111
|
-
expect((child.signal.reason as DOMException).name).toBe("AbortError");
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe("idempotency & ordering", () => {
|
|
116
|
-
test("aborting the root twice is a no-op the second time (reason is sticky)", () => {
|
|
117
|
-
const root = createAbortTree();
|
|
118
|
-
const first = new Error("first");
|
|
119
|
-
root.abort(first);
|
|
120
|
-
root.abort(new Error("second"));
|
|
121
|
-
expect(root.signal.reason).toBe(first);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("aborting a parent after the child already self-aborted leaves the child's reason intact", () => {
|
|
125
|
-
const root = createAbortTree();
|
|
126
|
-
const child = root.child();
|
|
127
|
-
const childReason = new Error("child self");
|
|
128
|
-
child.abort(childReason);
|
|
129
|
-
// Parent aborts afterwards; child must keep its own reason, not adopt the parent's.
|
|
130
|
-
root.abort(new Error("parent later"));
|
|
131
|
-
expect(child.signal.reason).toBe(childReason);
|
|
132
|
-
expect(root.signal.aborted).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("deep chain (5 levels) all cascade from a single root abort", () => {
|
|
136
|
-
const root = createAbortTree();
|
|
137
|
-
const levels = [root];
|
|
138
|
-
for (let i = 0; i < 5; i++) {
|
|
139
|
-
const next = levels[levels.length - 1];
|
|
140
|
-
if (next === undefined) throw new Error("unreachable");
|
|
141
|
-
levels.push(next.child());
|
|
142
|
-
}
|
|
143
|
-
for (const node of levels) expect(node.signal.aborted).toBe(false);
|
|
144
|
-
root.abort();
|
|
145
|
-
for (const node of levels) expect(node.signal.aborted).toBe(true);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe("listener hygiene on the parent signal (finalization-removal path)", () => {
|
|
150
|
-
test("each live child registers exactly one abort listener on the parent", () => {
|
|
151
|
-
const root = createAbortTree();
|
|
152
|
-
expect(getEventListeners(root.signal, "abort").length).toBe(0);
|
|
153
|
-
const a = root.child();
|
|
154
|
-
expect(getEventListeners(root.signal, "abort").length).toBe(1);
|
|
155
|
-
const b = root.child();
|
|
156
|
-
expect(getEventListeners(root.signal, "abort").length).toBe(2);
|
|
157
|
-
// Keep references alive so neither can be GC'd before we assert.
|
|
158
|
-
expect(a.signal.aborted).toBe(false);
|
|
159
|
-
expect(b.signal.aborted).toBe(false);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("a child's own abort removes its listener from the parent", () => {
|
|
163
|
-
const root = createAbortTree();
|
|
164
|
-
const a = root.child();
|
|
165
|
-
const b = root.child();
|
|
166
|
-
expect(getEventListeners(root.signal, "abort").length).toBe(2);
|
|
167
|
-
|
|
168
|
-
a.abort();
|
|
169
|
-
expect(getEventListeners(root.signal, "abort").length).toBe(1);
|
|
170
|
-
|
|
171
|
-
b.abort();
|
|
172
|
-
expect(getEventListeners(root.signal, "abort").length).toBe(0);
|
|
173
|
-
// Parent itself stays unaborted throughout.
|
|
174
|
-
expect(root.signal.aborted).toBe(false);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("parent abort consumes the once-listener (parent left with none afterwards)", () => {
|
|
178
|
-
const root = createAbortTree();
|
|
179
|
-
const child = root.child();
|
|
180
|
-
expect(getEventListeners(root.signal, "abort").length).toBe(1);
|
|
181
|
-
root.abort();
|
|
182
|
-
expect(child.signal.aborted).toBe(true);
|
|
183
|
-
// The { once: true } parent listener is consumed on fire.
|
|
184
|
-
expect(getEventListeners(root.signal, "abort").length).toBe(0);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
describe("external parent signal", () => {
|
|
189
|
-
test("createAbortTree wraps an external AbortController signal and cascades", () => {
|
|
190
|
-
const ext = new AbortController();
|
|
191
|
-
const tree = createAbortTree(ext.signal);
|
|
192
|
-
expect(tree.signal.aborted).toBe(false);
|
|
193
|
-
const grandchild = tree.child();
|
|
194
|
-
const reason = new Error("external abort");
|
|
195
|
-
ext.abort(reason);
|
|
196
|
-
expect(tree.signal.aborted).toBe(true);
|
|
197
|
-
expect(tree.signal.reason).toBe(reason);
|
|
198
|
-
expect(grandchild.signal.aborted).toBe(true);
|
|
199
|
-
expect(grandchild.signal.reason).toBe(reason);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("already-aborted external signal yields a born-aborted tree", () => {
|
|
203
|
-
const ext = new AbortController();
|
|
204
|
-
const reason = new Error("pre-aborted external");
|
|
205
|
-
ext.abort(reason);
|
|
206
|
-
const tree = createAbortTree(ext.signal);
|
|
207
|
-
expect(tree.signal.aborted).toBe(true);
|
|
208
|
-
expect(tree.signal.reason).toBe(reason);
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
describe("memory-safety: collected child does not break parent abort", () => {
|
|
213
|
-
// Exercises the WeakRef deref()===undefined branch in attachParent's
|
|
214
|
-
// onParentAbort: an abandoned child must not pin the parent, and the parent
|
|
215
|
-
// aborting after the child is collected must be a safe no-op for that child.
|
|
216
|
-
test("aborting a parent whose child was GC'd does not throw", () => {
|
|
217
|
-
const root = createAbortTree();
|
|
218
|
-
|
|
219
|
-
// Create a child but retain only a WeakRef to its signal so the underlying
|
|
220
|
-
// AbortController is eligible for collection once this helper returns.
|
|
221
|
-
const probe = ((): WeakRef<AbortSignal> => {
|
|
222
|
-
const child = root.child();
|
|
223
|
-
return new WeakRef(child.signal);
|
|
224
|
-
})();
|
|
225
|
-
|
|
226
|
-
// Bun.gc(true) performs a synchronous, deterministic full GC.
|
|
227
|
-
let collected = false;
|
|
228
|
-
for (let i = 0; i < 20 && !collected; i++) {
|
|
229
|
-
Bun.gc(true);
|
|
230
|
-
collected = probe.deref() === undefined;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// The parent still holds one listener for the (now possibly collected) child.
|
|
234
|
-
// Aborting must never throw, regardless of whether collection happened.
|
|
235
|
-
expect(() => root.abort(new Error("after-collect"))).not.toThrow();
|
|
236
|
-
expect(root.signal.aborted).toBe(true);
|
|
237
|
-
|
|
238
|
-
// Bun reliably collects the unreferenced controller; assert we actually hit
|
|
239
|
-
// the deref()===undefined path. If a future runtime cannot collect here,
|
|
240
|
-
// this assertion documents the regression rather than silently passing.
|
|
241
|
-
expect(collected).toBe(true);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// T3: Integration — abort the parent and observe a real Bun.spawn'd child
|
|
246
|
-
// process exit (SIGTERM cascade via { signal }).
|
|
247
|
-
describe("T3 — child-process SIGTERM on parent abort", () => {
|
|
248
|
-
test("Bun.spawn(['sleep', '30'], { signal: child.signal }) exits when parent aborts", async () => {
|
|
249
|
-
const root = createAbortTree();
|
|
250
|
-
const child = root.child();
|
|
251
|
-
|
|
252
|
-
const proc = Bun.spawn(["sleep", "30"], {
|
|
253
|
-
signal: child.signal,
|
|
254
|
-
stdout: "pipe",
|
|
255
|
-
stderr: "pipe",
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
const start = Date.now();
|
|
259
|
-
// Abort 100 ms in to make sure the spawn has time to start.
|
|
260
|
-
setTimeout(() => root.abort(), 100);
|
|
261
|
-
|
|
262
|
-
const exitCode = await proc.exited;
|
|
263
|
-
const elapsedMs = Date.now() - start;
|
|
264
|
-
|
|
265
|
-
expect(elapsedMs).toBeLessThan(2_000); // sleep 30 — would have run for 30 s without abort
|
|
266
|
-
expect(child.signal.aborted).toBe(true);
|
|
267
|
-
// Bun's signal-driven exit yields a non-zero exit code (typically null/143).
|
|
268
|
-
// We just assert the process ended early.
|
|
269
|
-
expect(exitCode).not.toBe(0);
|
|
270
|
-
});
|
|
271
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Catalog R1 `abort-controller` — parent/child cancellation tree.
|
|
3
|
-
*
|
|
4
|
-
* Semantics:
|
|
5
|
-
* - Parent abort cascades to all children (recursively).
|
|
6
|
-
* - Sibling abort does NOT propagate.
|
|
7
|
-
* - Child abort does NOT propagate up to the parent.
|
|
8
|
-
* - If `parent` is already aborted at construction time, the child is born aborted.
|
|
9
|
-
*
|
|
10
|
-
* Memory: parent → child propagation goes through WeakRefs so an abandoned
|
|
11
|
-
* child doesn't pin the parent, and a finalized child's listener is removed
|
|
12
|
-
* from the parent on its own abort. Mirrors the proven pattern in
|
|
13
|
-
* `claude-code/utils/abortController.ts`.
|
|
14
|
-
*
|
|
15
|
-
* Listener limit: Node's default of 10 trips a warning when a busy turn fans
|
|
16
|
-
* out into many tool calls; we raise it to 50 per signal.
|
|
17
|
-
*/
|
|
18
|
-
import { setMaxListeners } from "node:events";
|
|
19
|
-
|
|
20
|
-
const MAX_LISTENERS_PER_SIGNAL = 50;
|
|
21
|
-
|
|
22
|
-
export type AbortTree = {
|
|
23
|
-
readonly signal: AbortSignal;
|
|
24
|
-
abort(reason?: unknown): void;
|
|
25
|
-
child(): AbortTree;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Build an `AbortTree` rooted at the given parent (or no parent for a fresh
|
|
30
|
-
* root). The returned tree's `child()` produces children whose `signal`
|
|
31
|
-
* fires when this tree's `signal` fires.
|
|
32
|
-
*/
|
|
33
|
-
export function createAbortTree(parent?: AbortSignal): AbortTree {
|
|
34
|
-
const controller = new AbortController();
|
|
35
|
-
setMaxListeners(MAX_LISTENERS_PER_SIGNAL, controller.signal);
|
|
36
|
-
|
|
37
|
-
if (parent !== undefined) {
|
|
38
|
-
if (parent.aborted) {
|
|
39
|
-
controller.abort(parent.reason);
|
|
40
|
-
} else {
|
|
41
|
-
attachParent(parent, controller);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
signal: controller.signal,
|
|
47
|
-
abort(reason?: unknown) {
|
|
48
|
-
controller.abort(reason);
|
|
49
|
-
},
|
|
50
|
-
child() {
|
|
51
|
-
return createAbortTree(controller.signal);
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Wire `parent` so its abort propagates to `childCtl`. Uses WeakRefs both ways
|
|
58
|
-
* so neither the parent nor the listener pins the other.
|
|
59
|
-
*/
|
|
60
|
-
function attachParent(parent: AbortSignal, childCtl: AbortController): void {
|
|
61
|
-
const weakChild = new WeakRef(childCtl);
|
|
62
|
-
|
|
63
|
-
function onParentAbort(this: AbortSignal): void {
|
|
64
|
-
const ctl = weakChild.deref();
|
|
65
|
-
if (ctl !== undefined && !ctl.signal.aborted) {
|
|
66
|
-
ctl.abort(this.reason);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
parent.addEventListener("abort", onParentAbort, { once: true });
|
|
71
|
-
|
|
72
|
-
// When the child aborts on its own, drop the parent listener so the parent
|
|
73
|
-
// doesn't accumulate dead listeners across many children.
|
|
74
|
-
childCtl.signal.addEventListener(
|
|
75
|
-
"abort",
|
|
76
|
-
() => {
|
|
77
|
-
parent.removeEventListener("abort", onParentAbort);
|
|
78
|
-
},
|
|
79
|
-
{ once: true },
|
|
80
|
-
);
|
|
81
|
-
}
|