Skip to content

Add opt-in Event, EventTarget, CustomEvent, AbortController and AbortSignal - #3096

Merged
lahma merged 2 commits into
sebastienros:mainfrom
lahma:webapi/events
Aug 20, 2026
Merged

lahma merged 2 commits into
sebastienros:mainfrom
lahma:webapi/events

Conversation

@lahma

@lahma lahma commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Part of the opt-in web API series (#3079): Event, CustomEvent, EventTarget, AbortController and AbortSignal behind WebApiFeatures.Events (in Default). These are prerequisites for fetch's cancellation model. Usual rules: BCL only, net8+, opt-in, default engine unchanged.

Dispatch is flat, and deliberately runs both passes. The DOM standard's dispatch walks an event path built from a node tree; author-created EventTargets have no tree, so the path is the single item «target» — and everything that survives that reduction is the real algorithm, not an approximation: both passes still run with eventPhase at AT_TARGET, so capture decides which pass a listener runs in (a capturing listener on a flat target runs before a non-capturing one whatever order they were registered — the standard's own behavior, pinned); stopPropagation ends the dispatch after the current pass while stopImmediatePropagation also breaks out of it; each pass walks a clone so a listener added mid-dispatch does not run and one removed does not either; once listeners are removed before invocation so a throwing callback cannot run twice.

A listener that throws erupts from dispatchEvent — the spec's "report the exception" presumes a reportError channel that does not exist yet, and swallowing would lose the error entirely on a console-less engine; so it propagates, matching the timer-callback contract, with dispatch state unwound in a finally. When reportError lands this becomes report-and-continue behind the sink.

AbortSignal is fetch's cancellation seam. Abort algorithms run before the abort event — the engine's own algorithm (cancelling the signal's lazily-created CancellationTokenSource) runs first of all, so a CLR-side operation stops before any script observes the abort. AbortSignal.timeout(ms) schedules on the timer queue (fires only while pumped, counts against MaxActiveTimers, [EnforceRange] semantics); AbortSignal.any implements dependent signals with source flattening and link-dropping on abort so an aborted signal retains nothing; addEventListener's signal option unregisters on abort, and removeEventListener also takes the algorithm off the signal — avoiding a retention the spec itself leaves behind. The events feature creates the per-engine web-API state even without the timers flag, because Event.timeStamp reads the shared time origin and AbortSignal.timeout needs the queue.

162 new tests. Documented WebIDL simplifications follow the series' precedent (non-enumerable prototype operations; strong rather than weak dependent-signal lists, with the trade-off explained on the type). Verified: all-TFM build, both suites both frameworks, both host-contract-verification configurations.

🤖 Generated with Claude Code

lahma and others added 2 commits August 20, 2026 20:09
Evaluate itself pumps the event loop, so asserting a real 1ms timeout
signal has not yet aborted races the machine: a loaded runner passes
more than a millisecond before the read and the signal has legitimately
fired, which is exactly what three CI legs demonstrated. A held manual
clock makes the not-yet half a fact and an advanced one makes the fired
half deterministic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma merged commit ead7b0f into sebastienros:main Aug 20, 2026
9 of 10 checks passed
@lahma
lahma deleted the webapi/events branch August 20, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant