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:
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:
AsyncContext
, and have it automatically flow through spec scheduling mechanism like AsyncContext
flows through JS schedulersWhile 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.
HTML-adjacent specifications use multiple scheduling mechanisms:
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:
The async context snapshot cannot be introspected when running in parallel, but only propagated.
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.
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 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()
.
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?
HTMLCanvasElement.prototype.toBlob()
Although toBlob()
would probably use the WebIDL approach, it's a good first example of implicit propagation:
toBlob()
is called, there is a current async context snapshot s.toBlob()
ReadableStream's unserlying source's pull() when called by ReadableStreamDefaultReader.prototype.read()
NOTE: This is all synchronous, so the result would be the same if web specs were completely unaware of AsyncContext
.read()
is called, there is a current async context snapshot s.XMLHttpRequest's readystatechange event handlers
This is a more complete example where the implicit propagation is actually useful.
.send()
is called, there is a current async context snapshot s..send()
runs fetch, implicitly passing the async context snapshot s, and explictly passing the processResponse steps..send()
.readystatechange
, implicitly passing the async context snapshot s to them.event.dispatchSnapshot
:
event.dispatchSnapshot
to CreateAsyncContextSnapshot(_s_)
.