Skip to content

Commit a790407

Browse files
Update web integrations doc for events with current approach and rationale
1 parent acee8f4 commit a790407

File tree

1 file changed

+120
-21
lines changed

1 file changed

+120
-21
lines changed

Diff for: WEB-INTEGRATION.md

+120-21
Original file line numberDiff line numberDiff line change
@@ -426,27 +426,126 @@ Event dispatches can be one of the following:
426426
some web API, but the dispatch happens at a later point. In these cases, the
427427
context should be tracked along the data flow of the operation, even across
428428
code running in parallel (but not through tasks enqueued on other agents'
429-
event loops). [See below on implicit context
430-
propagation](#implicit-context-propagation) for how this data flow tracking
431-
should happen.
432-
433-
This classification of event dispatches is the way it should be in theory, as
434-
well as a long-term goal. However, as we describe later in the section on
435-
implicit context propagation, for the initial rollout we propose treating the
436-
vast majority of asynchronous dispatches as if they were browser-originated.
437-
The exceptions would be:
438-
439-
- The `popstate` event
440-
- The `message` and `messageerror` events
441-
- All events dispatched on `XMLHttpRequest` or `XMLHttpRequestUpload` objects
442-
- The `unhandledrejection` and `rejectionhandled` events on the global object
443-
(see below)
444-
445-
> TODO: The exact principle for which event listeners are included in this list is still under discussion.
446-
447-
The list above is not meant to be hard-coded in the events machinery as a "is this event part of that list?" check. Instead, the spec text and browser code that fires
448-
each of these individual events would be modified so that it keeps track of the
449-
context in which these events were scheduled (e.g. the context of `window.postMessage` or `xhr.send()`), and so that that context is restored before firing the event.
429+
event loops).
430+
431+
For events triggered by JavaScript code (either synchronously or asynchronously),
432+
the goal is for them to behave equivalently as if they were implemented by a
433+
JavaScript developer that is not explicitly thinking about AsyncContext propagation:
434+
listeners for events dispatched either **synchronously** or **asynchronously** from
435+
JS or from a web API would use the context that API is called with.
436+
437+
<details>
438+
<summary>Expand this section for examples of the equivalece with JS-authored code</summary>
439+
440+
Let's consider a simple approximation of the `EventTarget` interface, authored in JavaScript:
441+
```javascript
442+
class EventTarget {
443+
#listeners = [];
444+
445+
addEventListener(type, listener) {
446+
this.#listeners.push({ type, listener });
447+
}
448+
449+
dispatchEvent(event) {
450+
for (const { type, listener } of this.#listeners) {
451+
if (type === event.type) {
452+
listener.call(this, event);
453+
}
454+
}
455+
}
456+
}
457+
```
458+
459+
An example _synchronous_ event is `AbortSignal`'s `abort` event. A naive approximation
460+
in JavaScript would look like the following:
461+
462+
```javascript
463+
class AbortController {
464+
constructor() {
465+
this.signal = new AbortSignal();
466+
}
467+
468+
abort() {
469+
this.signal.aborted = true;
470+
this.signal.dispatchEvent(new Event("abort"));
471+
}
472+
}
473+
```
474+
475+
When calling `abortController.abort()`, there is a current async context active in the agent. All operations that lead to the `abort` event being dispatched are synchronous and do not manually change the current async context: the active async context will remain the same through the whole `.abort()` process,
476+
including in the event listener callbacks:
477+
478+
```javascript
479+
const abortController = new AbortController();
480+
const asyncVar = new AsyncContext.Variable();
481+
abortController.signal.addEventListener("abort", () => {
482+
console.log(asyncVar.get()); // "foo"
483+
});
484+
asyncVar.run("foo", () => {
485+
abortController.abort();
486+
});
487+
```
488+
489+
Let's consider now a more complex case: the asynchronous `"load"` event of `XMLHttpRequest`. Let's try
490+
to implement `XMLHttpRequest` in JavaScript, on top of fetch:
491+
492+
```javascript
493+
class XMLHttpRequest extends EventTarget {
494+
#method;
495+
#url;
496+
open(method, url) {
497+
this.#method = method;
498+
this.#url = url;
499+
}
500+
send() {
501+
(async () => {
502+
try {
503+
const response = await fetch(this.#url, { method: this.#method });
504+
const reader = response.body.getReader();
505+
let done;
506+
while (!done) {
507+
const { done: d, value } = await reader.read();
508+
done = d;
509+
this.dispatchEvent(new ProgressEvent("progress", { /* ... */ }));
510+
}
511+
this.dispatchEvent(new Event("load"));
512+
} catch (e) {
513+
this.dispatchEvent(new Event("error"));
514+
}
515+
})();
516+
}
517+
}
518+
```
519+
520+
And lets trace how the context propagates from `.send()` in the following case:
521+
```javascript
522+
const asyncVar = new AsyncContext.Variable();
523+
const xhr = new XMLHttpRequest();
524+
xhr.open("GET", "https://example.com");
525+
xhr.addEventListener("load", () => {
526+
console.log(asyncVar.get()); // "foo"
527+
});
528+
asyncVar.run("foo", () => {
529+
xhr.send();
530+
});
531+
```
532+
- when `.send()` is called, the value of `asyncVar` is `"foo"`.
533+
- it is synchronously propagated up to the `fetch()` call in `.send()`
534+
- the `await` snapshots the context before pausing, and restores it (to `asyncVar: "foo"`) when the `fetch` completes
535+
- the `await`s in the reader loop propagate the context as well
536+
- when `this.dispatchEvent(new Event("load"))`, is called, the current active async context is thus
537+
the same one as when `.send()` was called
538+
- the `"load"` callback thus runs with `asyncVar` set to `"foo"`.
539+
540+
Note that this example uses `await`, but due to the proposed semantics for `.then` and `setTimeout`
541+
(and similar APIs), the same would hapepn when using other asynchronicity primitives.
542+
543+
</details>
544+
545+
Event listeners for events dispatched **from the browser** rather than as a consequence of some JS action (e.g. a user clicking on a button) will by default run in the root (empty) context. This is the same
546+
context that the browser uses, for example, for the top-level execution of scripts.
547+
548+
> NOTE: To keep agents isolated, events dispatched from different agents (e.g. from a worker, or from a cross-origin iframe) will behave as events dispatched by user interaction.
450549
451550
### Fallback context ([#107](https://github.com/tc39/proposal-async-context/issues/107))
452551

0 commit comments

Comments
 (0)