-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integration with web APIs #82
Comments
After some discussion in today's meeting, it was pointed out that when you need the originating context for an event, you usually want to choose which context to use when registering the event. One way of doing this would be to add an option to the If such an option is passed, what should happen when there is no originating context? Note that many events can be fired with or without an originating context, depending on the specific cause of the event dispatch. Would it make sense to use the empty context? If not, would it make sense to use the registration-time context even though the option was meant to opt out of it? |
There is always a valid originating context, it's just not necessarily directly synchronously calling the continuation. For example, in all those XMLHttpRequest cases the originating context would be when the send() method was called. Some async stuff happens in the background, but logically a line can be drawn through all that internal behaviour back to having been triggered by the send() call. This is yet another reason why I strongly believe context should flow through the path through internals and only bind around some points at the end of an edge and not the beginning, so everywhere, including internals should always have a valid context. Async context functions by modelling segments of sync execution on cpu rearranged by task scheduling to resume the state as it was at the point the task was scheduled. This means modelling the behaviour on cpu exactly and not just what it looks like from the dynamic language perspective. I have a bunch of writing I'm working on making shareable soon which goes into details like this of how to do complete context management. |
That is not the case. In the browser, a user-initiated click event does not have an originating context (other than the empty one), simply because there's no JS code causing the event, synchronously or asynchronously. The same goes for events coming from outside the thread or process, such as worker |
I think we should avoid ever using the empty context. It feels like a malformed leak to do so; it would mean that we can't use AsyncContext for things where we need to be able to inherit from somewhere and get a reliable value. I'd prefer falling back to the registration time context if the origination context is selected (e.g., in an addEventListener option) but then isn't available at runtime. |
Which context is used at the beginning of the main script? The/An empty context or something else? |
When starting an agent (a V8 isolate, a Node.js process...) from scratch, the context there would be the empty context indeed. But this question is actually related to another topic of discussion related to the web integration that I also wanted to bring up (but forgot to mention in the OP): should cross-document navigations have an associated context? This would affect the originating context of various events fired during page load, as well as the running context of scripts found during parsing, and the registration-time context for event handler attributes (e.g. Cross-origin navigations of any kind can't possibly keep any context from before the navigation, since the AsyncContext state is agent-specific. Same-origin navigations caused by the user (e.g. by clicking a link or navigating via the URL bar) also wouldn't have an originating context, and so would need to have the empty context. But what about same-origin navigations caused by setting |
The originating context of a click would be the setting up of a system to track clicks, which would be the page loading in the first place and therefore should be the top-level context. Events coming from elsewhere originate from whatever initiated the connection to those elsewhere things. Nothing ever happens without code at some level having been defined to initiate that a thing can happen. That system might be very far away from where the final result is, but every bit of logic can eventually be traced back to the process starting. This is what context is modelling. Within a browser you would surely want to sandbox process-level context down to page-level context, but that's still a subset that flows out of other systems with connections to external systems set up at page load, so that boundary becomes the "start" of the context management window. The point I'm trying to make is that context is an expression of application behaviour all the way back to the root, whatever that root may be expressed as, and all things derive from points in that tree. If something would be initiated from "outside" that execution tree then it still has to have some source of when that system became connected which is likely just the root of that context. Having no contextual parent is not valid as nothing can execute without something having started that execution. |
Fair enough. But in the web specs, all of this happens implicitly, and this is not something web developers have any insight into. Furthermore, as @littledan points out above, in cases where the only originating context would be the one in which basic tracking systems are set up at page load, that context is of no use to developers (other than maybe in distinguishing it from other contexts that they have set up).
This raises an interesting question. Windows, workers and And this gets really interesting with |
I have some writing which I will be sharing soon which covers this issue and others. Need some time to make it publicly consumable as it's part of internal Datadog docs at the moment. It describes a useful way of thinking about it, which I will summarize here. Asynchrony is rearranging sync segments of execution based on readiness. There is generally a scheduler managing segments at the barrier between what is actually running in cpu vs what is running in the "runtime" environment. However there's also often further layers doing their own scheduling of sorts like connection pools or task queues. Sometimes these things are useful to express but not always. Mechanically though, execution flows through these arrangement systems and so context must also flow through them for them to ensure context is always present. In many cases there are universally acceptable reductions of that graph, like carving the path around a connection pooling mechanism to represent user intent rather than runtime machinery. This binding of points does not mean that path through internals has no context however. It just means that when it reaches the task it wants bound to a different context that the context it flowed through by call path automatically will no longer be used in that task. All systems have an internal path to follow and it should be followed because that will be the valid propagation case in every case except the exceptions which bind around patterns like connection pools. So internal path is the default and user intent path is a reduction applied in particular cases where it makes universal sense. There's also a way to restore internal path as-needed, if you do flow through the internal path and just bind around it, but I'll leave that for when I share the other docs. Basically though we should be considering execution flow at the cpu level as the default expression of how context flows, only carving out specific exceptions where register path is more relevant to the user. |
This is not correct; cross-origin postMessage() between windows works fine. |
I've got one more example of how this could mesh with a Web API that was passed over: Having async context available would mean backing store and tracking of dependent observables could flow via async context and signals could track dependencies easily in async functions. Transparent to the API consumer that creates a derived signal. See also the discussion for async signals: |
In this issue I meant "web API" meaning "an API defined by the web platform specs", not something defined in TC39. You're right that the integration with signals has to be considered (and @shaylew and @littledan have already started taking a look at it), but since signals is at a lower stage from AsyncContext, that consideration could be considered at some later point when signals is farther along, whereas the integration with well-established web APIs needs to be figured out before the AsyncContext proposal advances to stage 2.7. |
Since I opened this issue, I noticed a couple cases that I missed in my analysis:
|
I'm considering whether it would not be better to use the originating context by default for events, rather than the registration context. If we do this, there wouldn't be any need for an API change when registering event listeners, since you can use One of the main reasons why I went with registration-time by default was because this is zone.js's default behavior. However, zone.js also allows running code whenever a zone is switched on or off, or whenever a task is enqueued using that zone – which are all out of scope for the AsyncContext proposal – and it seems like this might have been the main reason why zone.js defaults to registration-time for events. With this reason off the table, it's not clear (to me, at least) that AsyncContext should use registration-time. Going with the originating context also brings into a sharper focus the discussion of what to do when there is no originating context1. As a reminder, the originating context for And, especially if we use the registration-time context if there is no originating context, this would be a problem for polyfilling events, since there's no way to manually dispatch an event with no originating context. (If we use the empty context, then this might also be hard to polyfill, since you might not have an empty context available, but you could get close enough.) A way to fix this would be to add an option to Footnotes
|
I've been looking at how AsyncContext would integrate with various APIs and events in the web platform. These are my conclusions:
Web APIs
These can be grouped in various categories. I've listed APIs shipping in at least two browser engines, with a few single-engine additions that I thought relevant.
Schedulers
For these APIs, their sole purpose is to take a callback and schedule it in the event loop in some way. The callback will run at most once (except for
setInterval
) after the function returns, when there is no other JS code running on the stack.Since the default context for these callbacks would be the empty context, they should be called with the context in which the API was called:
setTimeout
setInterval
queueMicrotask
requestAnimationFrame
HTMLVideoElement
'srequestVideoFrameCallback
methodrequestIdleCallback
scheduler.postTask
Completion callbacks
These APIs take callbacks that are called once, to indicate the completion of an asynchronous operation that they start. These act just like the callbacks passed to
.then
/.catch
in promises, and therefore they should behave the same, being called with the context in which the web API was called:<canvas>
'stoBlob
methodDataTransferItem
'sgetAsString
method (drag & drop)Notification.requestPermission
BaseAudioContext
'sdecodeAudioData
method (web audio API)RTCPeerConnection
'screateOffer
,setLocalDescription
,createAnswer
,setRemoteDescription
andaddIceCandidate
methods (WebRTC)Geolocation
'sgetCurrentPosition
methodFileSystemEntry
'sgetParent
methodFileSystemDirectoryEntry
'sgetFile
andgetDirectory
methodsFileSystemFileEntry
'sfile
methodFileSystemDirectoryReader
'sreadEntries
methodCallbacks run as part of an async algorithm
The following APIs call the callback to run user code at most once as part of an asynchronous operation that they start. Therefore, they should probably use the context in which the web API was called:
Document
'sstartViewTransition
method (CSS view transitions)LockManager
'srequest
method (web locks)Note that these APIs are conceptually similar to this JS code, where the callback would be called with the same context as the call to
api
:(See also streams in "others" below.)
Observers
Observers are web API classes that take a callback in its constructor, and provide an
observe()
method to register things to observe. Whenever something relevant happens to the observed things, the callback will be called asynchronously to indicate that those observations have taken place.You could think of both the construction of an observer, and the time its
observe()
method is called, as being contexts that could be relevant to a call resulting from that observation. However, for all web API observers, the updates are batched so that the callback is called with an array of observation records, which can go back to multiple calls toobserve()
.Unlike with the case with multiple async originating contexts in events, here it doesn't make sense to go with the latest originating call to
observe()
, since that context only matters for one of the observations. Therefore, the only context that seems to make sense is the one active when the observer class is constructed.These are the web APIs which follow this pattern:
MutationObserver
ResizeObserver
IntersectionObserver
PerformanceObserver
ReportingObserver
There is also an observer-like class defined in TC39, namely
FinalizationRegistry
. It differs from web APIs in that the callback is called once for each registered object that has been GC'd, rather than with a list of observations. This would make it possible to call the callback with the context at whichregister()
(the equivalent ofobserve()
) was called, but we chose to have it behave the same as web API observers instead. See #69 for the relevant discussion.Action registrations
These APIs register a callback or constructor to be invoked whenever some action runs:
MediaSession
'ssetActionHandler
methodGeolocation
'swatchPosition
methodRemotePlayback
'swatchAvailability
methodAudioWorkletGlobalScope
'sregisterProcessor
,PaintWorkletGlobalScope
'sregisterPaint
)In all of the above cases, the action will be invoked browser-internally, so there is no JS on the stack or any other context that could be used other than that active when the API is called. (In the terms we define below for events, they have no originating context.)
Additionally, custom element classes can be registered with
customElements.define
, and their constructor and reaction methods are also invoked whenever some action runs. However, these can be called with JS code on the stack (when the element is changed via web APIs), or without it (when it's changed through a user interaction, such as editing). (In event terms, they could optionally have a sync call-time context.)Others
The DOM spec's
NodeIterator
andTreeWalker
. These APIs are created from a document method (document.createNodeIterator()
anddocument.createTreeWalker()
), which takes either a function or an object with anacceptNode()
method to act as a filter. When the various methods of these classes are called, the filter is invoked to check whether a node should be returned or skipped. The possibilities are to use the creation-time context, or the call-time context of the methods of those classes. This is essentially the same asit.filter(cb)
in the iterator helpers proposal, and the behavior should be consistent with whatever we decide there (see Interaction with iterator helpers #75).Streams (
{Readable,Writable,Transform}Stream
). Constructors for these classes take an object on which methods will be called, and they also take an options bag (two forTransformStream
) with an optionalsize
callback field. While thestart
method of the first argument will only ever be called synchronously in the class's construction (so it's not a concern wrt AsyncContext), the other methods and callbacks are trickier.These methods are run as part of an algorithm, but they differ from the APIs listed above in that they're registered by a different API than they're used by (they're registered by the constructor and used by e.g.
reader.read()
). Furthermore, they can be run with JS code on the stack (e.g.reader.read()
), or without it (e.g. when passing aReadableStream
as a request body tofetch()
). Although piping could make things more complicated, and more research is needed, the possibilities for these methods seem to be registration-time context and originating context, as with events.Events
The web platform has many events. Just to give you an idea, according to BCD (which provides browser compatibility data for MDN and caniuse.com), there are 263 different event names which are supported in at least two browser engines. Furthermore, the same event name can have different meanings in different APIs (e.g. the
error
event onwindow
indicates uncaught script errors, but theerror
event onXMLHttpRequest
indicates a fetch failure), so that's certainly an underestimate of the amount of work needed to figure out all of them. I have not yet analyzed them all, but these are my findings from analyzing a subset.Background
Every time an event callback is invoked, there is at least one relevant context: the time at which the event listener or handler was registered (i.e. when
addEventListener
was called, or e.g.onclick
was set). We will be calling this the registration-time context.Although most of the time events are fired asynchronously, there are some times when calling an API will caused an event to be fired synchronously. Some examples are calling the
abort()
method onXMLHttpRequest
, which will fire anabort
error; or changing the value oflocation.hash
to a non-empty value, which will cause a synchronous navigation which in turn fires thepopstate
event onwindow
. The context when this synchronous API is called is the sync call-time context, and it is the default that would be used if we don't change the web specs.1In all other cases, the event will fire when there's no other JS code on the stack. Sometimes this is because the event was triggered by the browser (i.e. after a user interaction), in which the only possible context that matters is the registration-time one. But other times there are APIs that asynchronously cause the event to fire, such as
XMLHttpRequest
'ssend()
method causing theload
event. These are async originating contexts.It seems like whenever there is a sync call-time context in the web platform, there are no async originating contexts that matter. After all, the immediate cause of the event is the synchronous web API call.2 However, one same type of event could sometimes be fired with a call-time context, sometimes with an async originating context, and sometimes with no originating contexts. An example is the
click
event, which is usually browser-triggered, butel.click()
will fire it synchronously.Sometimes there are multiple async contexts that could originate an event dispatch. For example, if you have a
<video>
element, and in quick succession the user hits play, and then some JS code runsvideo.load()
andvideo.play()
, which would be the async originating context (if any) for theload
event? But you could define some criterion to sort them: for example, always use the most important of such contexts (which would need to be judged independently for each API), and if there are multiple, the latest.If we define some such criterion, then for every event there would be at most one originating context, which would be the sync call-time context if there is one, and otherwise the "winning" async originating context. Only if the event was browser-triggered there would be no originating context. And with that, the decision for every event would be a binary one between the registration-time context and the originating context.
The choice of context
This decision is not yet a settled question for every event, but there are two events in particular which we know have specific needs:
unhandledrejection
event is an asynchronous event, and its async originating context is that active when theHostPromiseRejectTracker
host hook is called. Bloomberg's use case for this proposal needs this originating context to be accessible from theunhandledrejection
event listener, although it does not necessarily need to be the active context when the listener is called. For their needs it would be sufficient to expose that context as anAsyncContext.Snapshot
property in the event object, for example. (This was previously discussed in Specifyingunhandledrejection
behavior #16.)message
event onwindow
andMessagePort
is an asynchronous event, whose async originating context is that active whenwindow.postMessage()
ormessagePort.postMessage()
was called to enqueue the relevant message. Although this event is meant for communication, it is often used in the wild as a way to schedule tasks, since it has scheduling properties that other scheduling APIs didn't have beforescheduler.postTask()
. As such, the event listener should be called with the originating context by default (at least when the message is being sent to the same window), so it behaves like other scheduling APIs.For other events, there are various possibilities. However, it seems clear that if there is an originating context, advanced users of AsyncContext need to be able to access it, and using the registration-time context would make this impossible.3 At the same time, it seems like the context that non-advanced users would expect is the registration-time.
The choice between registration-time and originating context actually reflects different use cases for AsyncContext (something that Yoav Weiss discusses in some detail in this blog post, applied to the needs of task attribution). So the best course of action is probably to let listeners opt into the originating context, if one exists. This could be done by having an
AsyncContext.Snapshot
as a property of the event object, or by usingAsyncContext.callingContext()
(#77).cc @annevk @domenic @smaug---- @yoavweiss @shaseley
Footnotes
All event types can be called with a sync call-time context, through the
dispatchEvent()
method. We will be ignoring this, though, since we're focusing on events fired by web platform APIs. ↩One example of an API where this wouldn't be the case would be something that enqueues tasks or events, and runs them synchronously at some later point when some API is called. As far as I know, there's nothing like this built into the web platform. ↩
The inverse is not true: if events use the originating context, an event listener could make sure the callback is called with the registration-time context by wrapping it with
AsyncContext.Snapshot.wrap
. ↩The text was updated successfully, but these errors were encountered: