# Web integration for AsyncContext: in-spec context tracking One of the objectives of the AsyncContext web integration is to figure out how and when to expose to event handlers the AsyncContext of what caused the event to be dispatched. This can be done in two possible ways: either by running the event handler in that context, or by exposing its snapshot as a property on the event itself. For example: ```javascript const myVar = new AsyncContext.Variable(); const req = new XMLHttpRequest(); req.open("GET", "https://example.com/"); myVar.run("Hello!", () => { // Send the request. This will eventually // cause the `load` event to fire req.send(); }); req.addEventListener("load", (e) => { // somehow, get the value "Hello!" out of myVar here }) ``` Regardless of how this is exposed to the users, specs have to wire this snapshot through to make sure that it's available when running the event handler. While this is trivial for events that are dispatched synchronously (such as when calling `Element.prototype.click()`) because it's available as the ambient context, asynchronous events require the context to be snapshotted and propagated. Editorially, there are two possible ways of propagating it: - explicitly, by storing the snapshot in a variable and passing it around - implicitly, by defining a spec-internal concept similar to `AsyncContext`, and have it automatically flow through spec scheduling mechanism like `AsyncContext` flows through JS schedulers While the first approach has the advantage of (obviously! :P) being explicit and thus making it clear what is going on, it has the effect of adding a significant amount of noise to every algorihtm that might result in an event being fired. Most casual the readers of the specification will probably not be interested in how AsyncContext is flowing through its events, and instead they just want to learn the semantics of a specific algorithm. The second approach can solve that problem, by introducing some complexity of the spec that readers will only be affected by when they are specifically interested in `AsyncContext` propagation through a given API. It has the drawback that implementers might miss it when implementing a new API: we would have to make sure to add `AsyncContext` WPT tests for every possible way of triggering each event that propagates it. ## How to implicitly track context HTML-adjacent specifications use multiple scheduling mechanisms: - you can schedule tasks on a [task queue](https://html.spec.whatwg.org/#task-queue), which is run by the event loop - you can run algorithms [in parallel](https://html.spec.whatwg.org/#in-parallel) - you can use a few other queues run by the [window event loop](https://html.spec.whatwg.org/#event-loop-processing-model), such as the _fullscreen steps_ or the _scroll steps_ All algorithms need to accept an implicit **async context snapshot** argument, that is implicitly passed whenever running another algorithm/steps. This implicit **async context snapshot** is read/written in multiple places: - whenever JavaScript code calls into WebIDL, WebIDL sets the value of the **async context snapshot** to [AsyncContextSnapshot()](https://tc39.es/proposal-async-context/#sec-asynccontextsnapshot). - whenever WebIDL calls into JavaScript code, WebIDL performs [AsyncContextSwap(**async context snapshot**)](https://tc39.es/proposal-async-context/#sec-asynccontextswap). - whenever [queueing a task](https://html.spec.whatwg.org/#queue-a-task), HTML needs to store the current **async context snapshot** in the queued [task](https://html.spec.whatwg.org/#concept-task). Then, when [running the task](https://html.spec.whatwg.org/#event-loop-processing-model) it must run its steps with the **async context snapshot** stored in the task itself. - whenever running some steps [in parallel](https://html.spec.whatwg.org/#in-parallel) from another algorithm, the **async context snapshot** is captured and implicitly propagated to the parallel steps - other custom scheduling implementations, such as the [list of pending fullscreen events](https://fullscreen.spec.whatwg.org/#list-of-pending-fullscreen-events) whose events are fired by [window event loops](https://html.spec.whatwg.org/#event-loop-processing-model) need to have their own similar tracking of the **async context snapshot** The **async context snapshot** cannot be introspected when running in parallel, but only propagated. ### TODO - Check what to do with [parallel queues](https://html.spec.whatwg.org/#parallel-queue) - The JavaScript-visible async context must not leak across agents (which is observable through round-trips), so WebIDL needs to have some guard against it. ## Effectiveness of the implicit tracking When it comes to event handlers, which are the largest surface area for JavaScript code triggered asynchronously as a consequence of other JavaScript code, this implicit event tracking works in many cases but not all of them. We started an analysis at https://docs.google.com/spreadsheets/d/1r-IjEyTEuCzQtJgSyY-a5htrQFME9RPylkv3hT_puqg, see the _"Context easily available at async dispatch"_ column. For events where the dispatch context is not available (or for the rare cases in which there is a context available but not the expected one), it would still need to be passed manually. ## Correctness of the implicit tracking There are a limited amount of places in web specs where JavaScript code is run asynchronously, all listed at https://github.com/tc39/proposal-async-context/pull/100/files. Setting aside events for a moment, there are only ~25 APIs that take a callback and run it asynchronously. The correctness of implicit tracking in all of those cases must be verified and tested as part of the first iteration, with adjustments done to those specs as needed. For all cases in which the callback is registered in the same operation as the one starting the work (for example, `setTimeout()` both receives the callback and schedules it), we can have a WebIDL flag that marks APIs as "these automatically call `AsyncContext.wrap` on the callback", so that they don't need to rely on propagation through spec algorithms. Events are more complex, because there are hundreds of them and getting all of them right upfront without bugs is a monumental task (both validating that the spec propagates the context as expected, and implementing it). We should instead explore an incremental approach, trying to get the easy part done soon and iterating on the more complex cases over time. To do so, we can add a boolean flag "use dispatch context" to the [fire an event](https://dom.spec.whatwg.org/#concept-event-fire) which defaults to false. When this flag is set to false, the event listener is run in the fallback root context instead of in whatever context is being automatically propagated to it. We can enable this flag gradually, as we validate the callers of this algorithm in the various specs. A first sets of events that can already have thus "use dispatch context" set are all the events that are triggered only either by non-JS causes (for which the dispatch context and the root context correspond) or by sync JS causes (for which the dispatch context is trivially propagated): these includes all the user-interaction-like events that can be triggered by HTML methods such as `.click()`. :::info **TODO**: Explore how safe this incremental approach would be. Is there any realistic code that would break if it receives a non-empty context rather than the empty context? ::: <details open> <summary>Examples in which the implicit propagation would work</summary> [**HTMLCanvasElement.prototype.toBlob()**](https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-toblob) Although `toBlob()` would probably use the WebIDL approach, it's a good first example of implicit propagation: - When `toBlob()` is called, there is a current **async context snapshot** _s_. - It schedules some steps to run in parallel, that implicitly inherit the **async context snapshot** _s_. from `toBlob()` - They [queue an element task](https://html.spec.whatwg.org/multipage/webappapis.html#queue-an-element-task), explicitly passing some steps and implicitly passing the _s_ to it. - [Queue an element task](https://html.spec.whatwg.org/multipage/webappapis.html#queue-an-element-task) then runs [queue a global task](https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-global-task), implicitly passing _s_ along. The lattern then runs [queue a task](https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-task), passing _s_ along again. - [Queue a task](https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-task) creates a [task](https://html.spec.whatwg.org/multipage/webappapis.html#concept-task) whose **async context snapshot** is set to _s_. - When the [event loop](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model) runs the task, it passes the task's **async context snapshot** _s_ to the associated steps. - The steps (which were defined in [toBlob()](https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-toblob)) receive _s_ as the implicit **async context snapshot**, and [invoke](https://webidl.spec.whatwg.org/#invoke-a-callback-function) the callback implicitly passing it along. - [Invoke](https://webidl.spec.whatwg.org/#invoke-a-callback-function) would perform [AsyncContextSwap(_s_)](https://tc39.es/proposal-async-context/#sec-asynccontextswap) before calling the JavaScript function. [**ReadableStream's unserlying source's pull()**](https://streams.spec.whatwg.org/#dom-underlyingsource-pull) when called by [**ReadableStreamDefaultReader.prototype.read()**](https://streams.spec.whatwg.org/#default-reader-read) NOTE: This is all synchronous, so the result would be the same if web specs were completely unaware of AsyncContext - When `.read()` is called, there is a current **async context snapshot** _s_. - It runs [ReadableStreamDefaultReaderRead](https://streams.spec.whatwg.org/#readable-stream-default-reader-read), implicitly passing the **async context snapshot** _s_ along. - [ReadableStreamDefaultReaderRead](https://streams.spec.whatwg.org/#readable-stream-default-reader-read) runs the [[[pullAlgorithm]]](https://streams.spec.whatwg.org/#readablestreamdefaultcontroller-pullalgorithm) steps, implicitly passing _s_. - NOTE: The [[[pullAlgorithm]]](https://streams.spec.whatwg.org/#readablestreamdefaultcontroller-pullalgorithm) step is defined in step 6 of [SetUpReadableStreamDefaultControllerFromUnderlyingSource](https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller-from-underlying-source) - Those steps [invoke](https://webidl.spec.whatwg.org/#invoke-a-callback-function) the [pull()](https://streams.spec.whatwg.org/#dom-underlyingsource-pull) method, impclitly passing _c_ along. - [Invoke](https://webidl.spec.whatwg.org/#invoke-a-callback-function) would perform [AsyncContextSwap(_s_)](https://tc39.es/proposal-async-context/#sec-asynccontextswap) (which in this case would be a no-op) before calling the JavaScript function. [**XMLHttpRequest**](https://xhr.spec.whatwg.org/)'s [**readystatechange**](https://xhr.spec.whatwg.org/#event-xhr-readystatechange) event handlers This is a more complete example where the implicit propagation is actually useful. - When [`.send()`](https://xhr.spec.whatwg.org/#the-send()-method) is called, there is a current **async context snapshot** _s_. - Step 11.10 of [`.send()`](https://xhr.spec.whatwg.org/#the-send()-method) runs [fetch](https://fetch.spec.whatwg.org/#concept-fetch), implicitly passing the **async context snapshot** _s_, and explictly passing the _processResponse_ steps. - [Fetch](https://fetch.spec.whatwg.org/#concept-fetch) runs the [main fetch](https://fetch.spec.whatwg.org/#concept-main-fetch) steps, which then calls the [fetch response handover](https://fetch.spec.whatwg.org/#fetch-finale) steps, always implicitly passing _s_ along. - [Fetch response handover](https://fetch.spec.whatwg.org/#fetch-finale) [queues a fetch task](https://fetch.spec.whatwg.org/#queue-a-fetch-task) to run a step. - This step will, when run, the _processResponse_ steps provided by `.send()`. - [Queue a fetch task](https://fetch.spec.whatwg.org/#queue-a-fetch-task) implicitly propagates the **async context snapshot** _s_ to [queue a task](https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-task), which as seen above will make the event loop pass it as the **async context snapshot** of the queued step. - The [event loop](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model) runs the queued step, it provides thus provides the **async context snapshot** _s_ to it. - The step runs the _processResponse_ steps defined by [send()](https://xhr.spec.whatwg.org/#the-send()-method) (step 11.9), implicitly passing _s_ to them. - _processResponse_ runs the step to [fire the event](https://dom.spec.whatwg.org/#concept-event-fire) `readystatechange`, implicitly passing the **async context snapshot** _s_ to them. - [Fire an event](https://dom.spec.whatwg.org/#concept-event-fire) runs [dispatch](https://dom.spec.whatwg.org/#concept-event-dispatch), implicitly passing _s_ to it. [Dispatch](https://dom.spec.whatwg.org/#concept-event-dispatch) then [invokes](https://dom.spec.whatwg.org/#concept-event-listener-invoke), which then runs [inner invoke](https://dom.spec.whatwg.org/#concept-event-listener-invoke), always passing _s_ along. - _If the goal is to expose the dispatch async context snapshot as `event.dispatchSnapshot`_: - [Fire an event](https://dom.spec.whatwg.org/#concept-event-fire) would set `event.dispatchSnapshot` to [`CreateAsyncContextSnapshot(_s_)`](https://tc39.es/proposal-async-context/#sec-createasynccontextsnapshot). - _Else, if the goal is to run the event listener in dispatch async context_: - For each event listeners, the [inner invoke](https://dom.spec.whatwg.org/#concept-event-listener-invoke) runs the steps to [call a user object's operation](https://webidl.spec.whatwg.org/#call-a-user-objects-operation), implicitly passing the **async context snapshot** _s_ to them. - [Call a user object's operation](https://webidl.spec.whatwg.org/#call-a-user-objects-operation) then runs the JavaScript callback, and would need to run [AsyncContextSwap(_s_)](https://tc39.es/proposal-async-context/#sec-asynccontextswap) before doing so. </details>