|
| 1 | +# AbortController / AbortSignal |
| 2 | + |
| 3 | +The runtime installs the DOM Standard's abort primitives as globals in every |
| 4 | +isolate (main and workers): `AbortController`, and `AbortSignal` with the |
| 5 | +`abort`, `timeout` and `any` statics. The implementation is |
| 6 | +`test-app/runtime/src/main/cpp/js/abort-signal.js`, evaluated during `Events::Init` |
| 7 | +right after the `Event`/`EventTarget` builtin it is layered on, so the |
| 8 | +interfaces exist before any user code runs. `AbortSignal` extends the |
| 9 | +runtime's `EventTarget`; `new AbortSignal()` throws `TypeError: Illegal |
| 10 | +constructor` — instances come from a controller or one of the statics. |
| 11 | + |
| 12 | +## Surface |
| 13 | + |
| 14 | +- `new AbortController()` — `controller.signal` (stable identity) and |
| 15 | + `controller.abort(reason?)`. |
| 16 | +- `signal.aborted`, `signal.reason`, `signal.throwIfAborted()`, and the |
| 17 | + `abort` event (`addEventListener("abort", …)` or the `onabort` handler |
| 18 | + attribute with HTML event-handler semantics). |
| 19 | +- `AbortSignal.abort(reason?)` — an already-aborted signal; no event fires. |
| 20 | +- `AbortSignal.timeout(delay)` — aborts with a `TimeoutError`-named reason |
| 21 | + after `delay` ms. `delay` must be an integer in `[0, 2^32 − 1]` |
| 22 | + (`TypeError` for non-numbers, `RangeError` otherwise), matching Node's |
| 23 | + validation. |
| 24 | +- `AbortSignal.any(signals)` — a composite signal that aborts with the first |
| 25 | + source's reason. Accepts any iterable whose members are all `AbortSignal`s |
| 26 | + (`TypeError` otherwise). Composites are flattened: `any([any([a]), b])` |
| 27 | + follows `a` and `b` directly. Per spec, every affected signal's |
| 28 | + `aborted`/`reason` flips before the first `abort` event fires. |
| 29 | + |
| 30 | +## GC contract |
| 31 | + |
| 32 | +The implementation is GC-transparent the way Node's is: internal references |
| 33 | +never keep an unobservable signal alive, and never let an observable abort |
| 34 | +be dropped. |
| 35 | + |
| 36 | +- A `timeout()` timer closes over a `WeakRef`, so a signal nobody can |
| 37 | + observe is collectable before it fires; a `FinalizationRegistry` cancels |
| 38 | + the pending native timer when that happens. |
| 39 | +- `any()` links are `WeakRef`s in both directions (source → dependent and |
| 40 | + dependent → source), with prune registries clearing dead entries — so |
| 41 | + per-request composites never accumulate on a long-lived source, and a |
| 42 | + collected source leaves its composites' source lists (a composite whose |
| 43 | + sources are all gone can never abort and stops being retained). |
| 44 | +- Weakness alone would silently drop the abort of a signal that is |
| 45 | + listened-to but otherwise unreachable, so a strong `gcPersistentSignals` |
| 46 | + set holds exactly the signals whose abort someone can still observe: live |
| 47 | + timeout signals and live non-empty composites while they have `abort` |
| 48 | + listeners (`onabort` counts — it registers a real listener), plus timeout |
| 49 | + sources a composite follows, until their timer fires. The listener |
| 50 | + accounting comes from an internal symbol-keyed hook the events builtin |
| 51 | + calls from every listener-list mutation path (add, remove, and `once` |
| 52 | + removal during dispatch); the key travels only through the builtin-only |
| 53 | + `internals` object (see `test-app/runtime/src/main/cpp/js/README.md`) and |
| 54 | + never reaches app code, so the accounting cannot be bypassed via a |
| 55 | + captured `EventTarget.prototype.addEventListener`. |
| 56 | + |
| 57 | +Entries leave the persistent set on abort, on the last abort-listener |
| 58 | +removal, or when a composite loses its last source. |
| 59 | + |
| 60 | +## Deviations from Node / the web |
| 61 | + |
| 62 | +- **No `DOMException`.** As with [structuredClone](structured-clone.md) and |
| 63 | + the [Performance API](performance.md), default reasons are `Error` |
| 64 | + instances with `name` patched: `"AbortError"` (default abort) and |
| 65 | + `"TimeoutError"` (timeout). `instanceof DOMException` checks cannot work; |
| 66 | + match on `reason.name`. |
| 67 | +- Abort events carry no `isTrusted` flag (the runtime's `Event` doesn't |
| 68 | + model it). |
| 69 | + |
| 70 | +Listener errors during the abort dispatch go through the runtime's standard |
| 71 | +listener-error pipeline (see [error handling](error-handling.md)); a throwing |
| 72 | +listener never prevents the remaining listeners from running. |
0 commit comments