03:30
<littledan>

I talked through what AsyncContext was with Jordan and he said he understands it better now than previously. I focused on how it was used; he said he found the pseudo-implementation confusing (sorry for the bad recommendation there!)

When presenting, I recommend you spend a bunch of the time reviewing what the proposal is, somehow in terms a little different from last time, and what the use cases are, before going ahead. The open questions to answer during Stage 2 should probably go faster without dwelling too much.

03:31
<littledan>
Maybe we could have a break in the middle to go through the queue and answer questions about what the proposal is and why we want it, before going on
03:34
<littledan>
I think this lack of understanding all of the basics is a common pattern in proposals and doesn’t really reflect on the quality of your first presentation, or on Jordan. I heard similar lack of understanding of the motivation for a couple other proposals whose motivation had been extensively discussed.
05:21
<Justin Ridgewell>
Chengzhong Wu: I added you as an editor of the slide deck, if you want to add some slides on OTEL
05:23
<Justin Ridgewell>
You can use https://carbon.now.sh/aXSZjJ58QreRZMzMLEfP to generate code images (use 17 lines of code to get optimal size for slides)
05:24
<Justin Ridgewell>
I can explain a fair bit of usage and usecases between slides 2 and 3
05:31
<Justin Ridgewell>
I think having a before and after might be helpful. Eg, normal use code that might be written, and how the code has to change (or rather doesn’t have to change) if we add OTEL
05:32
<Justin Ridgewell>
A bit like the logs example I used in the last deck, user code doesn’t need to be written with telemetry in mind, it just gets added seamlessly
06:00
<Chengzhong Wu>
Thanks, I'll add the slides on OTEL
07:09
<littledan>
Note, code images aren’t accessible by themselves but you can fix this by putting the code in the speakers notes
15:31
<littledan>
I wonder if the presentation should mention this layering question (about whether it should be a web API) and our plan to investigate it (by trying out implementing it in Chrome)
15:32
<littledan>
Chrome has put in a lot of time to articulate that concern, so we should probably speak to it somehow
15:32
<littledan>
(I am happy to explain this part if people want)
15:56
<Andreu Botella>
whether AsyncContext should be a web API ≠ whether Chrome (or browsers in general) should implement it as a web API
15:56
<Andreu Botella>
but yeah, it might be good to discuss that as well
16:00
<littledan>
whether AsyncContext should be a web API ≠ whether Chrome (or browsers in general) should implement it as a web API
Yes, I think this is a good point to make in the presentation as well
16:00
<littledan>
But I believe Yoav if he says it is easier for him to implement and expose it as a web API
16:00
<Justin Ridgewell>
Slides updated, and I added the code as speaker notes
16:01
<Justin Ridgewell>
I wonder if the presentation should mention this layering question (about whether it should be a web API) and our plan to investigate it (by trying out implementing it in Chrome)
I'm going to mention this in my foreword
16:43
<Mathieu Hofman>
I for one would prefer seeing this in the language itself
16:46
<James M Snell>
While I think I'd prefer language level also, I'm sympathetic to the web API approach and it's likely easier for us to implement that way anyway. Specifically, even if it were in the language we'd want v8 to treat it largely as an embedder API anyway to make it easier for us to integrate with other embedder use cases
16:49
<Andreu Botella>
Could you be more specific about those other embedder use cases?
17:00
<littledan>
I for one would prefer seeing this in the language itself
Maybe this is something you could articulate (with justification) in a queue item after the presentation?
17:01
<littledan>
While I think I'd prefer language level also, I'm sympathetic to the web API approach and it's likely easier for us to implement that way anyway. Specifically, even if it were in the language we'd want v8 to treat it largely as an embedder API anyway to make it easier for us to integrate with other embedder use cases
Could you elaborate on this point? It might feed into the design of the V8 API, to ensure it meets your use cases (if we go that way at all).
17:04
<Mathieu Hofman>
I need to articulate it better, but I think what I'll need is to extend the current API to support capturing both the registering context AND the resolving context, mostly to support join type use case like Promise.all.
17:10
<littledan>
I need to articulate it better, but I think what I'll need is to extend the current API to support capturing both the registering context AND the resolving context, mostly to support join type use case like Promise.all.
huh, this is a big request; I'm looking forward to you articulating it more.
17:11
<Mathieu Hofman>
I don't believe it's that big, we already need to capture the rejection context for unhandled rejections
17:14
<Ben Newman (Apollo, @benjamn on GH)>
when a context is captured, how are we imagining it can be accessed later?
17:14
<Ben Newman (Apollo, @benjamn on GH)>
maybe it's an AsyncContext.wrap-bound function that runs other functions in that bound context?
17:15
<Mathieu Hofman>
I have a sketch I'll try to clean up
17:16
<Ben Newman (Apollo, @benjamn on GH)>
are you hoping these registering/resolving contexts will be preserved separately, or merged into one context?
17:18
<Mathieu Hofman>
I just need to be able to get the list of my values associated to the different executions if there are multiple. Aka asyncContext.getAll(): Array<T>
17:19
<Ben Newman (Apollo, @benjamn on GH)>
that's nice because it lets consuming/application code decide how to merge the multiple values
17:20
<littledan>
I don't understand in what situations we'd provide multiple contexts, and how we'd expose them. It seems to complicate what model we should use for embedders. So I'm looking forward to more concreteness and use cases here.
17:20
<Justin Ridgewell>
Instead of introducing a new API, why can't the context be used with an array of values?
17:20
<littledan>
Let's focus right now on what we need to get to Stage 2, and then open this question up more aftewards
17:20
<Justin Ridgewell>
AsyncContext<T> -> AsyncContext<T[]>
17:21
<Mathieu Hofman>
const p = new Promise(resolve => { asyncContext.run(123, resolve); });
asyncContext.run(456, () => p.then(() => console.log(...asyncContext.getAll())));
// Prints 456, 123
17:21
<Ben Newman (Apollo, @benjamn on GH)>
say a function is bound with AsyncContext.wrap in one context and then called in another context, where some of the AsyncContext values in the two contexts are not the same
17:22
<Mathieu Hofman>
Yes, I'm not trying to ask to change anything now, but it's something I'd like us to consider at stage 2
17:22
<Andreu Botella>
I think we've been referring to those as "snapshots" or "maps", rather than "contexts"
17:23
<littledan>
OK, good, I'm looking forward to discussing this during Stage 2 once you're able to articulate the motivation
17:23
<Ben Newman (Apollo, @benjamn on GH)>
I'm all for avoiding the naked word "context" since it has so many overloads 😅
17:23
<Ben Newman (Apollo, @benjamn on GH)>
I like "snapshots" here
17:23
<littledan>
maybe we should make, say, monthly AsyncContext calls which would give an easy slot for this discussion
17:23
<Mathieu Hofman>
The motivation is join like execution flows, of which the prominent example is Promise.all
17:25
<littledan>
It will be good to hear what problem you're trying to solve concretely, just to give an example. But I'm not asking for an answer right now.
17:25
<littledan>
when you do Promise.all(...).then, the execution context is clear (it's inherited from where Promise.all is called, and has nothing to do with the promises passed into Promise.all)
17:25
<Chengzhong Wu>
maybe we should make, say, monthly AsyncContext calls which would give an easy slot for this discussion
👍 We can establish the regular call after the plenary
17:27
<Justin Ridgewell>
Also, the context that's active when the all'd promise's resolve() is invoked is only the last promise item to resolve.
17:27
<littledan>
Sure but this is also not used in the .then
17:27
<Ben Newman (Apollo, @benjamn on GH)>
ongoing/open question: what is the best data structure to represent the snapshots?
17:28
<Mathieu Hofman>
Right, I said it's the most obvious case, but basically it boils down to I need to learn the data associated with the context that resolved the promise
17:28
<littledan>
Yeah, I just am missing how this applies at all. So I'm looking forward to an example application, maybe just concretely what motivates you to start thinking about this (even if that's not a relevant motivation for everyone)
17:31
<Mathieu Hofman>
I'm actually surprised no one wants this. It's invaluable for diagnostics to understand the 2 causality paths on how you got where you are. Sure the registration path is often the most important, but the resolving path is also relevant.
17:32
<littledan>
OK, good, we're getting concrete: diagnostics.
17:32
<Mathieu Hofman>
That is one of my use cases yes
17:32
<littledan>
OK, looking forward to the writeup giving the set of your use cases; sorry for the distraction from Shu's presentation.
17:33
<Ben Newman (Apollo, @benjamn on GH)>
I think I share/want this use case, though I need more details for alignment
17:34
<Ben Newman (Apollo, @benjamn on GH)>
looking forward to digging into use cases during stage 2
17:34
<bakkot>
This feels like something which shouldn't be shared to me? I don't expect that resolving a promise exposes anything to consumers of the promise except the value of the promise
17:34
<bakkot>
the async context which I was in when I did the resolving isn't theirs to see, it's mine
17:34
<littledan>
(My intuition matches bakkot's. I'm really baffled by this whole thread. That's why I'm asking for the motivation.)
17:35
<Mathieu Hofman>
to clarify, you can only learn information if you associated a value through your own asyncContext.run
17:36
<Ben Newman (Apollo, @benjamn on GH)>
my general motivation is that it should be feasible/cheap to keep around (potentially many) snapshots of past context, so you can inspect them for diagnostic purposes later (in development, especially)
17:36
<littledan>
wrap is the capability to snapshot contexts
17:36
<Ben Newman (Apollo, @benjamn on GH)>
I'm less opinionated about exactly which snapshots should be preserved/exposed/reported for promises
17:37
<Ben Newman (Apollo, @benjamn on GH)>
you might not want to allocate a function for every snapshot you capture?
17:37
<littledan>
It's frequently been posited that wrap may be less efficient than something like AsyncResource. I think this is something we should investigate in the context of an actual implementation during Stage 2.
17:37
<littledan>
It's clear that whatever mechanism we adopt here has to be "efficient enough"
17:38
<littledan>
this still doesn't seem to relate to the idea of multiple inheritance of AsyncContext stuff
17:39
<littledan>
It's frequently been posited that wrap may be less efficient than something like AsyncResource. I think this is something we should investigate in the context of an actual implementation during Stage 2.
to be clear, I mean: if wrap is too inefficient, then we should definitely switch to a different API, maybe one more shaped like AsyncResource.
17:40
<bakkot>
Mathieu Hofman [moving off thread for readability]: also if you want to pass data from the promise-resolver to the consumer-of-the-resolved-promise, you can just... stick it in the promise? that seems like a very very different thing than the async contexts proposal
17:40
<Ben Newman (Apollo, @benjamn on GH)>
I think there are some other valid critiques of wrap, but it's possible to build many of the patterns you might want in userland, using only the current proposal
17:40
<Justin Ridgewell>
I agree, I think the promise should be responsible for passing data
17:41
<Mathieu Hofman>
I do not control intermediary code that creates and resolves these promises
17:41
<littledan>
you might not want to allocate a function for every snapshot you capture?
yeah I was specifically responding to this: you can't rebuild performance in JS
17:41
<littledan>
but wrap captures the expressiveness of snapshotting
17:41
<Mathieu Hofman>
Hence I cannot pass data throughout
17:41
<Justin Ridgewell>
And in most cases, because you own the AsyncContext that you invoked .run() on, it should be possible for you to chain onto the promise and pass the value
17:41
<Mathieu Hofman>
That's what async context is for in the first place
17:41
<ljharb>
wouldn't async context Just Work for what mathieu's talking about tho? shouldn't every promise spun off of "one that has the context" also have it?
17:42
<Mathieu Hofman>
As I mention in the thread, I do not control intermediary promises
17:42
<ljharb>
i'd assume that "adopting the state of another promise" also adopts/merges its context map or whatever the term is
17:42
<Justin Ridgewell>
No, but you control the promise as it's returned from your .run()
17:42
<Mathieu Hofman>
You're assuming the promise returned by run is related?
17:43
<Ben Newman (Apollo, @benjamn on GH)>
another issue is that wrap always ignores the current calling context of the function, when you might potentially want to merge the originally bound snapshot with the calling snapshot
17:43
<Justin Ridgewell>
Yes? If it doesn't escape the run, then the context should be whatever is currently in the context.
17:43
<Mathieu Hofman>
run creates an execution flow. the program can store / share promises of their own creation between flows
17:43
<Justin Ridgewell>
If it does escape the run, then you control it.
17:44
<littledan>
yeah, this is what I'd like to understand better: the use cases for merging. E.g., do you need to merge over all variables, or just one or two that you're thinking about.
17:44
<Mathieu Hofman>
I don't control promises resolved from that promise
17:45
<littledan>
This discussion is just too abstract for me to understand it. Let's pause and continue once we have this written example, as Mathieu Hofman has already promised.
17:45
<Mathieu Hofman>
I'm baffled by the request to explicitly attach information to promises given the whole premise of this proposal is based on the fact it's pretty much impossible to do that when you don't control the program
17:48
<Mathieu Hofman>
This "merging" is exactly what I have in mind
17:48
<Ben Newman (Apollo, @benjamn on GH)>
I'm hoping it's the latter, so merging can be cheap
17:49
<littledan>
(I just still don't understand what's being discussed)
17:49
<Ben Newman (Apollo, @benjamn on GH)>
if the merge is lazy, so the system does not have to eagerly merge all the variables up-front, you could potentially wait to see which variables are accessed, and resolve merge conflicts then
17:52
<littledan>
(yeah still confused; looking forward to concrete applications)
17:58
<bakkot>

I guess basically my thought process is:

  • if it's a promise you make/resolve yourself, you can thread state through
  • if it's not, the person doing the resolving isn't expecting to give the consumer any information other than the promise value itself; promises are supposed to be dumb wrappers for values

maybe I'm missing something because we don't have a concrete example in front of us though.

18:03
<ljharb>
right but isn't the point of asynccontext that the person doing the resolving doesn't have to know to pass stuff down, it's just ambiently available as long as you have the key?
18:04
<ljharb>
in react, context's entire purpose is to replace prop drilling in a way that still avoids globals by requiring the context key, and this seems similar to me
18:10
<bakkot>
the thing which gets passed down is created when you create the callback, not when you resolve the promise
18:10
<bakkot>
capturing state when you resolve the promise is a very different thing than capturing state when you create the callback
18:11
<bakkot>
though again hard to talk about without something concrete
18:28
<ljharb>
+1 for a description (like symbols have)
18:29
<Lenz Weber-Tronic (phryneas)>
default value could be a getter method - so there could be a choice to either return a value, or throw a customized Error
18:43
<Chengzhong Wu>
default value could be a getter method - so there could be a choice to either return a value, or throw a customized Error
+1, this can avoid unexpected modification on the default values somehow
18:45
<Ben Newman (Apollo, @benjamn on GH)>
while defaultValue is super convenient, you could implement a subclass of AsyncContext that takes a defaultValue in the constructor and overrides the get method to return that default
18:47
<Lenz Weber-Tronic (phryneas)>
true.
18:47
<James M Snell>
eek... I'm not sure I'm a fan of this at all, especially with the values being just an array like this. It also greatly complicates the implementation which currently allows each context frame to be independent of all others, including the frames they inherited from
19:12
<rbuckton>
ljharb: agreed it needs to be easy, but I also believe we need ways of opting out (which snapshot() and wrap() help with).
19:14
<Ben Newman (Apollo, @benjamn on GH)>
congrats on stage 2!! 🥳
19:15
<littledan>
Yay congrats!!!
19:16
<littledan>
The next steps for next meeting are clear: Focus completely on use cases
19:16
<Lenz Weber-Tronic (phryneas)>
🎉
19:16
<ljharb>
ljharb: agreed it needs to be easy, but I also believe we need ways of opting out (which snapshot() and wrap() help with).
totally agree; i'd prefer the default be "include everything" and the methods to be for opting out
19:16
<littledan>
In our new regular meetings, we can dig down towards proposed answers to the many questions Justin explained. I think we demonstrated to committee that we are looking into details, but I think we should be proposing answers at the same time as we explain the issues.
19:17
<littledan>
so some time later than next meeting (where we focus on use cases) we can present details on those.
19:27
<James M Snell>

If someone really wanted multiple values like this they could easily do something like this with the current API...

const ac = new AsyncContext();
ac.run([], () => {
  const p = new Promise(resolve => {
    ac.get().unshift(123);
    resolve();
  });
  ac.get().unshift(456);
  await p;
  console.log(ac.get()); // 456, 123
});
19:34
<Mathieu Hofman>
I do not control the sites where resolve is called, my example was an simplification
19:38
<Mathieu Hofman>
aka the run call is where I invoke the user program, which can do all sorts of things with promises. The important part is that the program will be invoked with different run contexts, and is thus able to "join" these. When they call back into my supervisor, I need to be able to look up all the associated run values, not just one (which usually would be the last in a join)
19:39
<rbuckton>
totally agree; i'd prefer the default be "include everything" and the methods to be for opting out
I don't think "capture context at declaration" makes sense. At some point you will execute the function, and you want to use the current context to do it. If you want to capture the context at the time of declaration, that's what wrap() is for.
19:41
<rbuckton>
"capture context at declaration" would also make snapshot completely useless. One of the benefits of snapshot() is that you can capture a "clean/empty" environment from which to start fresh, much like how some code today captures primordials early.
19:50
<rbuckton>
Unless I am misunderstanding your point about capturing context
20:00
<ljharb>
i mean like, arr.map(() => { /* context should be available here no matter when or where this function is called */ }) or replace arr.map( with someAPI(
20:01
<littledan>
"capture context at declaration" would also make snapshot completely useless. One of the benefits of snapshot() is that you can capture a "clean/empty" environment from which to start fresh, much like how some code today captures primordials early.
Indeed. Something that people might be missing is, the innermost time setting the context "wins". So if all closures captured the context, there'd be no overriding it.
20:03
<rbuckton>
i mean like, arr.map(() => { /* context should be available here no matter when or where this function is called */ }) or replace arr.map( with someAPI(
I disagree. That is specifically what .wrap() is intended to address. This whole feature is related to async flow through call stacks. A function declaration doesn't have a call stack.
20:05
<ljharb>
JS has deep dep trees, and code i don't control will be wrapping my functions, and/or calling them, all the time - it seems really unfortunate if i have to .wrap() manually in order to ensure my context works
20:06
<littledan>
JS has deep dep trees, and code i don't control will be wrapping my functions, and/or calling them, all the time - it seems really unfortunate if i have to .wrap() manually in order to ensure my context works
This should really not be the case; .wrap is pretty obscure and things should inherit correctly for the most part. The exception is if you're somehow making your own system for an event loop/queueing/batching, for callbacks (promises are already handled comprehensively)
20:07
<littledan>
AsyncContext inherits automatically over deep dependency trees
20:07
<Ben Newman (Apollo, @benjamn on GH)>
should host environments use AsyncContext.wrap to wrap setTimeout and setInterval callbacks? (since they're the ones implementing those scheduling tools)
20:07
<littledan>
should host environments use AsyncContext.wrap to wrap setTimeout and setInterval callbacks? (since they're the ones implementing those scheduling tools)
They should use the same underlying algorithm at some point, yes
20:15
<Justin Ridgewell>
This should really not be the case; .wrap is pretty obscure and things should inherit correctly for the most part. The exception is if you're somehow making your own system for an event loop/queueing/batching, for callbacks (promises are already handled comprehensively)
I think this is exactly the right take. For 99% usecase, it happens correctly, automatically. If you're not explicitly making your own queueing/batching system, you won't even need to think about this.
20:16
<ljharb>
if that's actually the case then that's great!
20:17
<ljharb>
i look forward to being convinced by more examples in the future :-) perhaps a temporal-like "cookbook" would help?
20:21
<littledan>
i look forward to being convinced by more examples in the future :-) perhaps a temporal-like "cookbook" would help?
good idea! Maybe it could be derived from these excellent slides by Chengzhong Wu https://docs.google.com/document/d/1wO7mbGr6f3tDnEWAaMSVI0bMeEHIdcLbAKeOQe-Zh4U/edit#heading=h.71r4ayj7zowk
20:24
<bakkot>
is AsyncContext.wrap expected to be free if there's no active contexts?
20:24
<bakkot>
or as free as like a .bind or whatever, at least
20:25
<bakkot>
I would hope so but just want to confirm
20:25
<bakkot>
(otherwise we probably want a "is there an active context" so you can conditionally wrap)
20:25
<Andreu Botella>
I expect implementations could make it free
20:26
<Andreu Botella>
say, make the snapshot an optional/nullable weak map
20:26
<Justin Ridgewell>
It should be as fast as bind, but it could be optimized for memory to return a blank wrapper function if there's not context
20:27
<Justin Ridgewell>
But that could be done as an impl optimization, not something we need to explicitly design in the spec.
20:28
<Justin Ridgewell>
Note that the pre/post steps of the wrapper need to run, setting the global state to the empty context, so that can't go away
21:32
<Ben Newman (Apollo, @benjamn on GH)>
are we considering dependency injection as a potential application of AsyncContext?
21:33
<littledan>
are we considering dependency injection as a potential application of AsyncContext?
If you have thoughts about how/whether it would be useful, it'd be really helpful to have a writeup on that. I would expect it to be controversial, however.
21:33
<ljharb>
that doesn't seem like a good strategic choice to market it that way
21:34
<Lenz Weber-Tronic (phryneas)>
People will definitely use it that way, after having done that for half a decade with a React Feature of the same name :)
Although, technically, "DI" is the wrong term for it ^^
21:35
<Ben Newman (Apollo, @benjamn on GH)>
another technical term with almost as many flavors as "context"
21:35
<littledan>
hehe look you've already been called wrong two different ways
21:35
<Lenz Weber-Tronic (phryneas)>
I had a twitter argument (I was calling it DI) with someone making very good arguments for "if you have to call out for it, it's not injection" - I still keep using the term DI though :)
21:37
<Ben Newman (Apollo, @benjamn on GH)>
cool, I appreciate that kind of feedback
21:37
<ljharb>
DI in JS is just function arguments, and i like it that way :-)
21:38
<littledan>
sometimes!
21:39
<Ben Newman (Apollo, @benjamn on GH)>
I like how static(ally analyzable) the parameter decorators for DI could be, compared to the more dynamic version you'd get with AsyncContext
23:29
<rbuckton>
is AsyncContext.wrap expected to be free if there's no active contexts?
Not if it can't ensure calling it later won't adopt a different context. This is necessary to escape out of a context later, which is necessary to create isolation
23:31
<rbuckton>
DI in JS is just function arguments, and i like it that way :-)
I mean, parameter decorators just tell the DI container what arguments to put where.
23:33
<rbuckton>
I think DI patterns are far better for improving testability than hacking the module loader like Jest does to perform mocking.