04:31
<littledan>
I think that's tricky due to races - for example, the exact ordering of a promise resolving vs. the promise.then call would make one or the other context more recent. But I think we probably want to run in the same context (resolution vs registration) regardless of the order.
Huh, what do you mean? I wasn’t imagining this kind of case, I was imagining that a particular cause would lead to a particular snapshot context being restored. Could you give an example of the race you are concerned about?
16:35
<Steve Hicks>

it's a little pedantic, but if you have something like

const p = v.run(1, () => Promise.resolve());
await p;
// p is now fully resolved
v.run(2, p.then(() => console.log(v.get())));

Then "the most specific (i.e. causally recent) relevant context" would presumably be the 2, since the callback was registered after the promise resolved. Without the await, it would be 1.

16:42
<Chengzhong Wu>
this smells like zalgo -- it depends on how the handlers are called
17:49
<Justin Ridgewell>
@Stephen Belanger pointed out that if we were to have an flows-through ExecutionVariable and implement scoped using mutations, we’d have a problem with propogating the execution state through the resolve
async function foo() {
  using _ = v.scope(1);
  return 2;
}
17:50
<Justin Ridgewell>
Because the scoped mutation is disposed in the FunctionStatementList evaluation of the function, and the outer promise is resolved in the AsyncFunctionBody execution after StatementList is evaluated
17:51
<Justin Ridgewell>
Then there’s weird interactions with how you’re supposed to resolve the promise, and what if the disposals caused an exeception
17:52
<Steve Hicks>
this smells like zalgo -- it depends on how the handlers are called
Right, that's why I'm saying that "using the most specific (i.e. causally recent) relevant context" is probably a bad specification.
17:55
<Steve Hicks>
Because the scoped mutation is disposed in the FunctionStatementList evaluation of the function, and the outer promise is resolved in the AsyncFunctionBody execution after StatementList is evaluated
I asked Tuesday whether scoping mutations would somehow prevent escaping the value and he suggested that there's still ways to extract the data. But I never got a good sense of what those ways were.
17:57
<Justin Ridgewell>
We’d have to redefine the using methods to perform DisposeResources in each of the *FunctionBody node types
17:58
<Justin Ridgewell>
Instead of using the FunctionStatementList node
17:58
<Justin Ridgewell>
That way we’d dispose the variables after resolving
18:30
<Stephen Belanger>
But using needs to work with block scopes too, right? So it'd need to be conditional on if the block is the function body or if it is nested.
18:31
<Stephen Belanger>
It'd add a bit of complexity.
18:31
<Stephen Belanger>
This is why other languages like .NET went for just doing set/get and persisting until the context would change.
18:32
<littledan>

it's a little pedantic, but if you have something like

const p = v.run(1, () => Promise.resolve());
await p;
// p is now fully resolved
v.run(2, p.then(() => console.log(v.get())));

Then "the most specific (i.e. causally recent) relevant context" would presumably be the 2, since the callback was registered after the promise resolved. Without the await, it would be 1.

yeah I have been assuming that .then would always use the registration context, and that we were only talking about web APIs.
18:32
<littledan>
(so there is no await vs .then mismatch)
18:34
<Stephen Belanger>

it's a little pedantic, but if you have something like

const p = v.run(1, () => Promise.resolve());
await p;
// p is now fully resolved
v.run(2, p.then(() => console.log(v.get())));

Then "the most specific (i.e. causally recent) relevant context" would presumably be the 2, since the callback was registered after the promise resolved. Without the await, it would be 1.

In flow-through model that should always be 1, as that is what was set when the resolve happened. We don't much care about where a continuation is attached.
18:34
<Steve Hicks>
This is why other languages like .NET went for just doing set/get and persisting until the context would change.
Is there some nuance here between persisting until the context would change vs. persisting until the end of a scope? Does the former somehow allow escaping a changed value out of a scope where the latter doesn't? I'm still trying to wrap my mind around how you can actually observe a changed value in a child scope if the change gets reverted before you resume.
18:35
<littledan>
Is there some nuance here between persisting until the context would change vs. persisting until the end of a scope? Does the former somehow allow escaping a changed value out of a scope where the latter doesn't? I'm still trying to wrap my mind around how you can actually observe a changed value in a child scope if the change gets reverted before you resume.
I think this comes up with block scopes within a function, where you'd restore the previous value in the latter case and not the former
18:35
<littledan>
so this is the difference between using and .set
18:35
<Steve Hicks>
In flow-through model that should always be 1, as that is what was set when the resolve happened. We don't much care about where a continuation is attached.
Right, but I'm imagining a world where await is flow-around, but then restores the resolution context, since that would still allow accessing all the flow-through causes as needed.
18:36
<Stephen Belanger>
Is there some nuance here between persisting until the context would change vs. persisting until the end of a scope? Does the former somehow allow escaping a changed value out of a scope where the latter doesn't? I'm still trying to wrap my mind around how you can actually observe a changed value in a child scope if the change gets reverted before you resume.
In flow-through model you capture both at call and at return. So when an async function or a promise resolves you capture and when the continuation of that begins you restore.
18:37
<Stephen Belanger>
Right, but I'm imagining a world where await is flow-around, but then restores the resolution context, since that would still allow accessing all the flow-through causes as needed.
Yes, restoring the orphaned branch is what I was thinking callingContext would be helpful for there.
18:37
<Stephen Belanger>
It's a bit awkward though.
18:37
<Stephen Belanger>
And probably would have terrible performance for APMs which would need to be doing that basically always.
18:39
<Steve Hicks>
And probably would have terrible performance for APMs which would need to be doing that basically always.
Yes, I expected the performance to be bad, but I was wondering if we could spec out the semantics such that it's all self-consistent, and then potentially provide a builtin alternative that exposes that common operation (which can now be emulated inefficiently) in a more efficient way.
18:40
<Steve Hicks>
Not sure if that made any sense... basically, you could explain the semantics of callingContext as if were a userland implementation, but it could be implemented internally in a more efficient way.
18:42
<Stephen Belanger>
If we have to actually do that explicitly on every await though it doesn't matter how much we optimize it, it will always have significant overhead. We need a type that has the appropriate flow by default or we're basically doubling all the context state modifications.
18:43
<Stephen Belanger>
This is why I was suggesting either having a separate type for the different flow, or having a per-store configuration to pick the different flow.
18:45
<Stephen Belanger>
Requiring that users manually manage snapshots all over the place is going to get expensive fast. This is why I had previously suggested flow-through by default and then a per-store config to additionally snapshot around awaits as the functionality for that binding layer is quite discrete and could be handled fairly performantly by just keeping a separate list of variables which want to snapshot there.
18:47
<Stephen Belanger>
But it seems some are of the opinion that flow-around should be the default, even though it's the more costly option to escape from.
18:53
<Steve Hicks>
I think it's a sticky situation. The default is less relevant - even if flow-around is the user-facing default, I don't see why it couldn't be implemented internally as the option that adds additional processing on top of the flow-through codepath. What's not clear to me is whether flow-through is viable at all, even as an option. My understanding is that SES would block it outright, so even if we want it we can't really have it.
18:53
<littledan>
yeah I have been assuming that .then would always use the registration context, and that we were only talking about web APIs.
Steve Hicks: Thoughts on this?
18:58
<Steve Hicks>
Steve Hicks: Thoughts on this?
Assuming we have flow-around for await, there will always be fundamental disagreement between the context propagation of callbacks and async-await. Promises has to choose one or the other. Registration aligns it with async-await, resolution aligns it with callbacks. It's not clear to me that one is fundamentally better than the other, but the latter opens up a possibility to access semantics that are impossible with the former.
19:01
<Stephen Belanger>
I think it's a sticky situation. The default is less relevant - even if flow-around is the user-facing default, I don't see why it couldn't be implemented internally as the option that adds additional processing on top of the flow-through codepath. What's not clear to me is whether flow-through is viable at all, even as an option. My understanding is that SES would block it outright, so even if we want it we can't really have it.
I don't see why it'd get blocked. It has the same trait that accessing the value is impossible without having access to the variable store to begin with, so any argument about it influencing other code is no more valid with flow-through than it is with event emitters.
19:01
<Steve Hicks>
(of course, maybe that already makes it problematic for SES anyway)
19:02
<Steve Hicks>
I wasn't part of those earlier conversations, so I don't really understand the reasoning here
19:02
<Steve Hicks>
They've got some super-theoretical framework where they transformed SyncContext to AsyncContext and transposed the ownership and reasoned that nothing dangerous happened at any step...
19:04
<Steve Hicks>
But IIUC the concern was less about passing a value directly and more about causing an observable side effect in a variable (by controlling the global snapshotting) without actually having access to that variable.
19:51
<littledan>
Assuming we have flow-around for await, there will always be fundamental disagreement between the context propagation of callbacks and async-await. Promises has to choose one or the other. Registration aligns it with async-await, resolution aligns it with callbacks. It's not clear to me that one is fundamentally better than the other, but the latter opens up a possibility to access semantics that are impossible with the former.
yes, well, within the registration world, what do you think about the web platform giving some event handlers a "more specific" context? I don't think that's racy, it would just depend on the cause of the event being triggered.
19:53
<littledan>
But IIUC the concern was less about passing a value directly and more about causing an observable side effect in a variable (by controlling the global snapshotting) without actually having access to that variable.
that was one concern, but I think we're good about variables being a necessary thing to have access to either way, even if we go with flow-through. I think they were also concerned about, once you already have a variable, for dataflow to be "well-behaved" (whatever that means--it's a vibe)
19:53
<littledan>
aesthetically, I do like keeping things well-behaved, all else being equal!
20:45
<Stephen Belanger>
Being "well-behaved" is deeply dependant on what behaviour is expected. And as I expressed with my examples about confusion in regard to differences between callback and promise flows, flow-through semantic seem to be more well-behaved in this regard that if flow-through is what you expect then there is rarely, if ever, a need to patch things with snapshotting.
20:46
<Stephen Belanger>
Once you start trying to carve out sub-graphs it becomes a matter of opinion which sub-graphs are correct.
20:49
<littledan>
things "nesting" and "not accidentally escaping" feels well-behaved to someone without background in the domain. We'll need to be able to explain the argument for the other semantics in a clear way to get through to them.
20:49
<Stephen Belanger>
I expressed this in the doc and slides I shared previously, but to repeat that: there are many layers of execution rearrangement occurring in JS. The base level of the microtask scheduler is fairly obvious to bind around, but as you raise further and further out into layers closer to user code it becomes less and less obvious what the "correct" path is when flowing around.
20:49
<littledan>
BTW I mentioned in the previous TG3 (SES) meeting that this is under discussion
20:50
<Stephen Belanger>
Not accidentally escaping. Intentionally escaping.
20:50
<littledan>
I expressed this in the doc and slides I shared previously, but to repeat that: there are many layers of execution rearrangement occurring in JS. The base level of the microtask scheduler is fairly obvious to bind around, but as you raise further and further out into layers closer to user code it becomes less and less obvious what the "correct" path is when flowing around.
lol I think we're all getting on the same page about how it's less and less obvious what the correct path is!
20:50
<littledan>
Not accidentally escaping. Intentionally escaping.
sure, well, that explanation will have to be clear and explained in a self-contained way. They will be concerned about users who will not be expecting the escaping, even if you're doing it on purpose.
20:51
<Stephen Belanger>
Yep, that's why I aimed for through path first in my own RFC and provide tools for reducing the graph as-needed. My experience has been that it's a lot easier to reduce the graph than to expand it.
20:51
<littledan>
but the through path doesn't provide more information, it just provides different information; it still loses stuff that the around path had
20:52
<Stephen Belanger>
Admittedly though, ALS never had a way to expand the graph, so that experience is possibly incomplete.
20:52
<Stephen Belanger>
The through path can represent the around path if you don't change the value in the branches.
20:52
<Stephen Belanger>
The reverse is not true though.
20:53
<Stephen Belanger>
So you can get the same behaviour as flow-around if you use the variables carefully.
20:54
<Stephen Belanger>
And the bind tools let you reduce graphs to flow-around easily to prevent internal branch modifications from flowing out.
20:55
<Stephen Belanger>
Something like await bindPromise(promise) which would just capture context at the point bindPromise(...) is called and restore it when the given promise calls its continuation let you trivially restore around flow.
20:55
<littledan>
So you can get the same behaviour as flow-around if you use the variables carefully.
could you explain further what you mean? I wonder if we can encourage/enforce "careful" usage, but I don't know what property you want to maintain.
20:55
<littledan>
Something like await bindPromise(promise) which would just capture context at the point bindPromise(...) is called and restore it when the given promise calls its continuation let you trivially restore around flow.
yep we have come to the conclusion that they can each be defined in terms of the other with explicit opt-in at the propagation sites
20:56
<Stephen Belanger>
So if you do an await doSomething() and then don't change the value anywhere in the through flow of doSomething() then what comes out the other side will be exactly the same as what was present when the await first started.
20:58
<Stephen Belanger>
The only exception being if you do await existingPromise, which is what bind was created for in the first place for the callback equivalent, and the bindPromise(...) can equally solve by binding to the correct place directly at the await.
20:59
<Stephen Belanger>
It just happens to be a bit annoying to need to do bindPromise(...) everywhere, so it makes sense to have some separate optimization to be able to inform the system that you want every await to apply a particular bind, which is what the around flow is essentially actually doing...it just doesn't give you a choice in the matter.
21:03
<Stephen Belanger>
Flow-around semantics are always a subset of flow-through semantics. They just look different because flow-around often orphans branches which would have otherwise flowed through so it appears like flow-through has return-based semantics when really it's just that flow-around decided to cut off the natural through flow to restore a snapshot captured before a given await. The flow is otherwise completely identical, apart from the single additional step of also capturing at resolve and restoring that unless a different link point has been established.
21:04
<Stephen Belanger>
With flow-around, the context still flows through the entire branch up to the point where a snapshot clobbers the value that had otherwise flowed through all the way to the resolve edge.
21:10
<Stephen Belanger>
It's probably more useful to think of it as flowing forward. Context always flows forward in time to any caused branches. The only question is if that context reaching the end of that branch where execution merges back should be raised out to continue flowing forward or if we want to intentionally cut that branch off for some reason. For the purpose of branch cancellation we would want a barrier where it does not raise out to unrelated execution, which is somewhat what the flow-around is solving. But for the purpose of flowing into future execution needed by tracing or most users wanting to store request-scoped data it needs to flow through backward merges all the way to the end of any execution caused by the initiation of that request.
21:13
<Stephen Belanger>
Flow-around may sometimes work for request-scoped data, but it needs to be set at a level which can never be merged back to or subsequent execution might not have a context available. This becomes problematic if you, for example, want to create a database connection in a middleware and flow that into subsequent middlewares or routers.
21:18
<Stephen Belanger>
// Measure request time
app.use(async function (ctx, next) {
  const start = Date.now()
  await next()

  // While this happens _after_ the following middleware runs,
  // the store value will not be set.
  const id = store.get()
  console.log(`Request #${id} took ${Date.now() - start}ms`)
})

// Store a request id value
let id = 0
app.use(async function (ctx, next) {
  await store.run(++id, next)
})
21:20
<Stephen Belanger>
The further you get from the store logic, the easier it is to shoot yourself in the foot with behaviours like this.
21:20
<Stephen Belanger>
And the whole point of async context storage is that you can retrieve values distant from where the store is set.