00:01 | <Andreu Botella> | I was kinda thinking out loud, and the fact that zone.js was probably the main reason why we were going with registration-time by default is something I noticed just now |
00:01 | <Andreu Botella> | but I will |
00:04 | <littledan> | I think Jia Li is the person, but I didn’t manage to reach him through Twitter DMs https://x.com/Jialipassion |
00:05 | <littledan> | We can work with other Angular devs to get his attention if we have trouble |
00:06 | <littledan> | Or meet him in Tokyo for TC39 this October! He showed up at last year’s community event |
00:10 | <Andreu Botella> | hm, I can't going to Finland, but I'd love to have a business reason to go to Tokyo 😅 |
00:25 | <Steve Hicks> | I think that maybe the only cases where events should be registration time are the ones where there isn't any possible JS origin for the event |
01:02 | <littledan> | The implementation (and specification) of this version will certainly be more complex, including for browsers. So it is important that we consolidate documentation for exactly why this design is useful and provides better context. |
01:03 | <littledan> | Sometimes the most relevant context isn’t there exactly synchronously and needs to be saved and restored (unhandled rejection is an example of this) |
09:22 | <Stephen Belanger> | What is the example code? What is the indicated issue URL? We’d previosuly discussed why we don’t have an This is exactly what I'm talking about. You seem to be explicitly aiming for binding around awaits, cutting off the branched execution merging back. This is explicitly what most users have told me they don't want. The code coming out of that await is a continuation, so it's essentially the same as if the remaining code was passed into that call as a callback, which does propagate context through. The fact that promises and async/await will not flow context through to continuations is extremely confusing to literally every user I've talked to about this. The whole point of context management to everyone I have talked to is to be able to set a value at some point in a branch of async execution and to be able to retrieve that same value anywhere in logically continuing execution, which any merge point like awaits, promise continuations, or callbacks all are. The fact we are specifying a behaviour which seemingly no one outside of this group is asking for is very strange to me. |
09:23 | <Stephen Belanger> | What issue? There were like 400 messages in this channel over the last week. |
09:28 | <Stephen Belanger> | So it’s the case where No, this is not user error. This is a common execution pattern which auto-instrumentation needs to be able to resolve, and it can't be done unless we flow context through merging branches. This is user code which we do not control but are expected to be able to trace through correctly. There is no user error here because the user expects to not have to modify their code for it to be traceable, and there's no error in how APM vendors are handling these situations because we simply lack the tools for it to be possible to track directly. This was the entire point of creating AsyncLocalStorage in the first place, but it sadly only got part of the way to correctness due to resource constraints and lacking runtime capabilities. |
09:35 | <Stephen Belanger> |
For the first part:
As for the second part:
|
09:39 | <Stephen Belanger> | I was kinda thinking out loud, and the fact that zone.js was probably the main reason why we were going with registration-time by default is something I noticed just now |
14:27 | <Andreu Botella> | How about an option on dispatchEvent to fire the event with the registration context, rather than the current one? |
15:04 | <Andreu Botella> | Node.js defaults to running everything in the scope where the emit happened, but EventEmitterAsyncResource exists to chose register time for an entire event emitter as-needed. |
15:05 | <Andreu Botella> | do you ever use that to monkeypatch built-ins? is it better than just monkeypatching the EventEmitter methods on the object itself? |
16:10 | <Steve Hicks> | How about an option on |
16:28 | <Steve Hicks> | Honestly I find the flow-through behavior to be much more confusing. I suspect this has to do with my starting point. I (and I think others here) come from a perspective where this is an implicit version of what golang does explicitly with their Context object. When you pass a Context into a subtask, that task is free to make its own child Contexts with different mappings, but because Context is itself immutable, there's no way for those changes to be visible to the caller. So from the point of view of seeing this as just a bag for passing implicit parameters, there's no expectation that a callee could change one for a caller (unless the passed object is itself mutable). If you are instead coming from a perspective where the purpose of this is to provide a mechanism to track execution flow, then I suspect the surprise goes in the opposite direction. So the question is (1) which of these is AsyncContext intended to be, and (2) is there any reasonable way for it to do both? Is "context" even the right concept for tracking execution flow, or is there something else that could do it more naturally? |
16:51 | <Steve Hicks> | Cancellation is an example of a use case where bind-around is what we want. Suppose we have a "globally well-known" abort: AsyncContext.Variable<AbortSignal> for the current task. Well-behaved jobs can inspect it and throwIfAborted , pass it along to things like fetch (or even have fetch use it implicitly), etc. Suppose you want to do something that can be cancelled independently of the upstream abort - you can const c = new AbortController() and then abort.run(AbortSignal.any(abort.get(), c.signal), ...) , and the child task can be cancelled by either the parent or the new child controller. Now imagine you've just programmatically added 5 cards to a view, which each need to fetch data and render themselves. You might kick off 5 fetches, each with its own child signal. If the user dismisses one of those cards while it was still loading, we can abort that one child controller individually. But it would clearly be a mistake to end up with one of those child signals as the current signal after await Promise.all(childCards) . |
17:02 | <Justin Ridgewell> |
enterWith . enterWith is a foot-gun that exposes an unfixable memory leak, and I specifically opose that. I will not accept a design that has an unparied enter/exit. |
17:05 | <Andreu Botella> | I think that maybe the only cases where events should be registration time are the ones where there isn't any possible JS origin for the event |
17:06 | <Justin Ridgewell> | What APIs would that include? I think that means all click listeners are call-time, but what context is that for a user’s click vs a programatic click? |
17:07 | <Andreu Botella> | For a user's click, it would be registration time, and for a programatic click, it would be call time on the call to .click() |
17:08 | <Justin Ridgewell> | I’d prefer registration-time for both cases for consistentcy, but I’d see both options being acceptable. |
17:09 | <Justin Ridgewell> | Was the unwrap() API discussed? |
17:10 | <Andreu Botella> | no |
17:11 | <Andreu Botella> | I'm not too much of a fan of unwrap() , but it is an option we could use |
17:13 | <Andreu Botella> | btw, I think at some point Jatin said he had a use case for detecting the null context (the difference between user's click and programmatic click in this case) |
17:13 | <Andreu Botella> | Steve Hicks: do you know anything about that? |
17:18 | <Stephen Belanger> | do you ever use that to monkeypatch built-ins? is it better than just monkeypatching the |
17:21 | <Chengzhong Wu> | do you ever use that to monkeypatch built-ins? is it better than just monkeypatching the |
17:22 | <Chengzhong Wu> | However, this support is not defined in web platform yet |
17:27 | <Stephen Belanger> |
Yep, I get that you have different use cases, and I assume with this much energy pushing in that direction there must be some valid reasons for aiming for these semantics. However, in Node.js most users are coming from the space of having a bunch of historically callback-oriented code taking advantage of context flowing to logically continuing execution so they can flow data between their components without needing to get teams owning intermediary components to explicitly pass along their bits of data. In more recent history they are starting to adapt that callback code to promise code and then being confused/angry/disappointed that their data no longer flows into that later code. I suggested yesterday that it might make sense to have a separate equivalent to AsyncContext called something like ContinuationContext which more semantically matches the downstream availability semantics these users are expecting. I also commented that I think the naming of AsyncContext could maybe be more illustrative of exactly what the flow is to not confuse users into thinking this thing has the flow they are expecting. |
17:33 | <Stephen Belanger> | Cancellation is an example of a use case where bind-around is what we want. Suppose we have a "globally well-known" |
17:36 | <Stephen Belanger> | I think you’re misinterpreting my statement. We can have flow-through semantics without |
17:42 | <Stephen Belanger> | What all other languages which have automatically propagated context do is exactly this:
With the exception that the |
17:42 | <Stephen Belanger> | So .NET defines a scope around each async function call. Sync calls too actually, so it's even more consistent than AsyncContext is which just flows over linear time in a sync block of code. |
17:43 | <Stephen Belanger> | Which produces exactly the semantics you are describing. |
17:43 | <Stephen Belanger> | While also allowing mutability of the slot within that function safely. |
17:45 | <Stephen Belanger> | The only difference with how AsyncContext is defined presently is just that the defineScope(...) and setForCurrentScope(...) concepts are forcefully made to be only possible to do at the same point, but in terms of actual safety and control they are identical. |
17:45 | <Stephen Belanger> | And nothing stops us from also providing users the ability to describe additional scopes. |
17:46 | <Stephen Belanger> | We're also essentially already doing this with capture scopes around things like awaits. They are just another manifestation of setting a value for some synchronous window. The fact that it is propagated from elsewhere is somewhat immaterial. |
17:49 | <Stephen Belanger> | The set/get style is much better in terms of closure reduction, more flexible and better suited to async/await flows, and equally as safe as the store.run(...) form. |
18:02 | <Justin Ridgewell> | Cancellation is an example of a use case where bind-around is what we want. Suppose we have a "globally well-known" |
18:11 | <Justin Ridgewell> | It's not an unpaired enter/exit though. The enter/exit is just left up to the runtime. so a microtask boundary, or the execution scope of the top-level script or module. There's still a clearly defined scope you can use, and this is what literally every other language does, it's just under full control of the runtime to define them rather than leaving it up to the user to define their own custom boundaries, which are arguably more complex to reason about because the runtime can't do things like having the optimizer rearrange execution around these boundaries without breaking the context guarantees. |
18:13 | <Justin Ridgewell> | The set/get style is much better in terms of closure reduction, more flexible and better suited to async/await flows, and equally as safe as the .run() . It is defintely better DX without the nested closures, but we don’t need to sacrifice safety to get that DX. |
18:14 | <Justin Ridgewell> | And again, enterWith is unnecessary to achieve flows-through. I don’t think we should spend time debating it. |