WHATWG DOM Abort

Elide implements the WHATWG [DOM Living Standard][spec] abort interfaces as globals: AbortController and AbortSignal. They integrate with EventTargetAbortSignal.prototype inherits from EventTarget.prototype, and addEventListener(..., { signal }) removes the listener automatically when the signal aborts.

[spec]: https://dom.spec.whatwg.org/#aborting-ongoing-activities

AbortController

js
new AbortController()
Calling AbortController(...) without new throws TypeError — per WebIDL, interface objects with constructors must be invoked with new.
MemberTypeNotes
signalAbortSignalReadonly, [SameObject] — repeated reads return the same signal instance.
abort(reason?)undefinedMarks signal as aborted with reason and dispatches a trusted abort event. Idempotent — later calls are no-ops. When reason is omitted, a fresh new DOMException("The operation was aborted", "AbortError") is stored (WebIDL signature is new DOMException(message, name)).

AbortSignal

new AbortSignal() always throws TypeError — signals are only created via AbortController, AbortSignal.abort(...), or AbortSignal.any(...).
MemberTypeNotes
abortedbooleanReadonly; true once abort(...) has been called.
reasonanyReadonly; the value passed to abort(...), or a default DOMException("AbortError").
throwIfAborted()undefinedThrows reason if aborted is true; otherwise a no-op.
onabortEventHandlerGet/set the single abort event handler. Setting null or a non-callable clears it; setting a new function replaces the previous handler.
addEventListener / removeEventListener / dispatchEventinheritedInherited from EventTarget.prototype — the signal is itself an EventTarget.

Static factories

MethodTypeNotes
AbortSignal.abort(reason?)AbortSignalReturns a fresh, already-aborted signal.
AbortSignal.any(signals)AbortSignalReturns a signal that aborts as soon as any input signal aborts. Inputs must be AbortSignals; if any is already aborted when called, the returned signal is aborted before return.
AbortSignal.timeout(ms)AbortSignalReturns a fresh signal that aborts after ms milliseconds with a TimeoutError DOMException (distinct from the AbortError used by AbortController.abort()). The argument follows WebIDL [EnforceRange] unsigned long long semantics with a practical cap at Number.MAX_SAFE_INTEGER: NaN, ±Infinity, negative values, and values above the cap throw TypeError. Fractional inputs are truncated toward zero (1.5 → 1).

Integration with addEventListener

The third argument to EventTarget.addEventListener(type, callback, options) accepts a { signal } property. When present:

  • If the signal is already aborted, the listener is not registered.
  • Otherwise the listener is added normally, and the signal arranges for the listener to be removed automatically when the signal aborts.
js
const target = new EventTarget();
const ctrl = new AbortController();
target.addEventListener("message", onMessage, { signal: ctrl.signal });

// Later — removes the listener, no matter who registered it:
ctrl.abort();

Non-AbortSignal values passed as signal are ignored (they behave as if the property were absent), matching Node.js and browsers.

Example

js
// AbortSignal.timeout produces a fresh signal that aborts after the given delay.
async function fetchWithTimeout(url, ms) {
  return fetch(url, { signal: AbortSignal.timeout(ms) });
}

// Compose with another cancellation source — typing Ctrl-C, parent shutdown, etc.
const any = AbortSignal.any([userCancel.signal, AbortSignal.timeout(5000)]);

Notes on the implementation

  • Plain-call AbortController(...) is routed to TypeError via a manual construct?-ternary patch in the generated JSAbortControllerClassBase.java (same pattern as DOMException, the encoding classes and the event classes). The upstream elide-dev/bindgen generator emits this branch automatically on the next regenerate. AbortSignal's "not directly constructible" TypeError is already present in the generated base.
  • The two classes skip the generated putBuiltinClass and use a custom installAsGlobal in the agent. The generated path consumes a slot in the realm's shared JSObjectFactory.IntrinsicBuilder shape array — that array is sized at JSContext creation from graal-js's own factory count with no public extension point, and non-ordinary subclass layouts (AbortSignalObject / AbortControllerObject extend JSNonProxyObject) collide with graal-js's own JSOrdinaryObject shapes at high slots. installAsGlobal sidesteps that collision by building the factory via JSObjectFactory.createBound, which keeps the shape on the factory itself rather than in the realm-shared array.
  • The signal option on addEventListener is parsed by EventsOps.parseListenerOptions and consumed by JSEventTarget.addListener through the public AbortOps.registerRemoveOnAbort hook, which calls JSEventTarget.removeListenerByReference from the signal's abort-cleanup list. No public API change to the EventTarget contract; behaviour is additive.