10:19
<littledan>
If we seriously have extra time on the agenda, there are a couple wild open discussion topics which might be fun. Emoji-react something if you're interested in them. (Apologies for not raising this before the agenda deadline.)
10:19
<littledan>
Cancellable promises
10:20
<littledan>
Private name declarations outside of classes
10:34
<rbuckton>
Cancellable promises
Would it make sense to discuss this on https://github.com/tc39/proposal-cancellation?
10:37
<littledan>
Would it make sense to discuss this on https://github.com/tc39/proposal-cancellation?
oh thanks that gives a lot of helpful references to review. I have been thinking about this area again in the context of AsyncContext, which gives us more options for propagating the cancel token.
10:37
<littledan>
also AbortSignal.any is a very helpful, core capability in this area, which I don't think was there last time we discussed this
10:40
<littledan>
(I guess it basically enables what your previous proposal's new CancellationTokenSource(linkedTokens?) does, roughly)
10:46
<rbuckton>
Unfortunately, since AbortSignal is outside the purview of TC39 the best forum for that may be WHATWG.
10:47
<rbuckton>
Though I've long held that cancellation graphs like that are very valuable
10:47
<littledan>
We'll definitely have to collaborate between standards groups to make this happen, but I think you were right to bring this to TC39, and believe we can make some progress together.
10:49
<littledan>
also if we want integration with async/await (e.g., all awaits are implicitly racing with the cancel token), something would need to happen in TC39
10:50
<rbuckton>
I also agree that AsyncContext would have been the way to traffic a cancellation token in the way Yehuda wanted, so long as there was a way to suppress async context flow when needed
10:50
<littledan>
I also agree that AsyncContext would have been the way to traffic a cancellation token in the way Yehuda wanted, so long as there was a way to suppress async context flow when needed
could you say more about this suppression use case?
10:50
<littledan>
I mean, requirements for it
10:50
<rbuckton>
The only avenue given to us at this point is a host hook.
10:51
<littledan>
IIRC wanting the cancel token to be implicitly propagated was a goal of Domenic's as well
10:51
<rbuckton>
could you say more about this suppression use case?
Sometimes you don't want to propagate the token.
10:51
<rbuckton>
I have to step away, I'll discuss more shortly.
10:52
<littledan>
The only avenue given to us at this point is a host hook.
I hope we can first think about the problem space and what's needed for developers (as you were trying to do) and then we can go from there to "how do we lay this out across the various specs"
11:02
<rbuckton>
With the token as a parameter, it is up to the caller to determine whether to pass the token to a function. If you had a function that invoked a REST API, you might want to allow it to be cancellable in some cases, but not others. If there is no suppression mechanism, then there is no way for the caller to make this determination. The structure of the Cancellation API (so far as that proposal was concerned) ensured an appropriate separation of concerns so that the correct level of control was available with respect to the caller and the call site.
11:10
<rbuckton>
(I guess it basically enables what your previous proposal's new CancellationTokenSource(linkedTokens?) does, roughly)
One of the major reasons I wanted a linked cancellation graph was to address memory overhead. If cancellation sources could be intrinsically linked, and could be disposed when cancellation was no longer needed, then all of the token subscriptions could be GC'd (incl. the callbacks and closed-over variables that they held).
11:11
<littledan>
With the token as a parameter, it is up to the caller to determine whether to pass the token to a function. If you had a function that invoked a REST API, you might want to allow it to be cancellable in some cases, but not others. If there is no suppression mechanism, then there is no way for the caller to make this determination. The structure of the Cancellation API (so far as that proposal was concerned) ensured an appropriate separation of concerns so that the correct level of control was available with respect to the caller and the call site.
yes, so if we had an AsyncContext variable for the current cancel token, and then a function you could call to set that variable to a fresh token while running a callback, that would achieve suppression, right?
11:11
<littledan>
One of the major reasons I wanted a linked cancellation graph was to address memory overhead. If cancellation sources could be intrinsically linked, and could be disposed when cancellation was no longer needed, then all of the token subscriptions could be GC'd (incl. the callbacks and closed-over variables that they held).
yes, I agree this is important. Do you see AbortSignal.any as solving that issue too?
11:11
<rbuckton>
A fresh token, or no token.
11:11
<rbuckton>
I would have to think about that. IIRC, the issue with any is how ownership is controlled for a subgraph.
11:12
<littledan>
I would have to think about that. IIRC, the issue with any is how ownership is controlled for a subgraph.
I think any has to be used in a sort of opinionated way to make things work
11:12
<littledan>
and your proposed signature sort of already encapsulates that pattern
11:13
<rbuckton>
Let's say I receive a token and want to call another function with both that and my own token. With any, I can close my source, but since I have no control over the incoming token, the graph can't be GC'd
11:14
<littledan>
which graph do you mean?
11:16
<rbuckton>
function outer(signal) {
  const myController = new AbortController();
  const combinedSignal = AbortSignal.any([signal, myController.signal]);
  const promise = inner(combinedSignal);
  ...
  // we've progressed to a point where cancellation shouldn't occur, 
  // but we can't signal that to `inner`
  myController.dispose(); 
}
11:17
<rbuckton>
Here, even if we think inner should keep going, if signal is canceled then combinedSignal is cancelled.
11:18
<rbuckton>
If any returns a signal, then I can't model this relationship using it.
11:19
<rbuckton>

But if any returns a controller, then I can exert this level of control:

function outer(signal) {
  const combinedController = AbortController.any([signal]);
  const promise = inner(combinedController.signal);
  ...
  // we've progressed to a point where cancellation shouldn't occur
  combinedController.dispose(); 
}
11:21
<rbuckton>
The new CancellationTokenSource(linkedTokens?) API allowed you to express this relationship. If you need control over the subgraph, you hold a reference to the source. If you don't need control over the subgraph, you don't hold a reference to the source and just pass along it's token.
11:21
<littledan>
Btw did you point this out in any issue on the AbortSignal.any repo?
11:22
<rbuckton>
I wasn't aware of an AbortSignal.any repo, but I've definitely discussed it many years ago in TC39 as part of the cancellation proposal.
11:23
<littledan>
This was the repo, but it's already shipping across browsers https://github.com/shaseley/abort-signal-any
11:24
<littledan>
I had trouble following all the aspects of your previous presentation, and was watching this proposal later, and thought it was good and solved the problems you were raising.
11:26
<rbuckton>
Ah, that's unfortunate.
11:26
<littledan>
I have trouble tracing the leak in the above code. It's that it's less apparent that the controller is dead?
11:26
<rbuckton>
No, the problem is that the controller isn't dead in the first example.
11:27
<littledan>
sure, that it isn't dead, so how big of a leak is that?
11:27
<littledan>
it's only referred to by that local variable, and that can be collected once you leave the scope. or is there anything else?
11:28
<littledan>
(I thought solving this particular GC issue was like 80% of the point of AbortSignal.any in the first place)
11:28
<rbuckton>
It's not a leak, it's bigger than that. If I wanted to be able to control whether inner could even be cancelled anymore after a certain point, I would not be able to do so in that approach, so it actually affects capabilities, not just memory.
11:29
<rbuckton>
Also, if there is no dispose()/close() then you're not addressing the GC concern at all.
11:29
<littledan>
how can you do that with the API you're proposing?
11:29
<rbuckton>
It's quite hard to explain in text without drawing a graph :/
11:57
<rbuckton>

Lets assume for a moment that an AbortController has a dispose() method. In example one, you have an outer abort controller A (and signal a) and create an inner abort controller B (and signal b). Calling AbortSignal.any([a, b]) produces a signal ab with the following semantics:

  • Aborting A aborts a and ab
  • Aborting B aborts b and ab
  • Disposing A disposes a but not ab, because ab could still be aborted by B.
  • Disposing B disposes b but not ab, because ab could still be aborted by A.

My algorithm reaches a point of no return where I no longer want inner to be cancelable, at which point any subscriptions added by inner can be collected. Unfortunately, AbortSignal.any does not give me this capability since neither A nor B dominates the token ab.

12:03
<rbuckton>

In the new CancellationTokenSource(linkedTokens?) approach, you produce a new source/controller that dominates the cancellation interaction with inner. You have the same outer controller A (and signal a), and you wrap it with an inner controller B(a) (with signal b(a)). Signal b(a) has the following semantics:

  • Aborting A aborts a and b(a) so long as a link still remains between a and B(a).
  • Aborting B(a) aborts b(a).
  • Disposing A disposes a but not b(a), because b(a) could still be aborted by B(a). However, the link between B(a) and a can be removed and the subscription can be GC'd.
  • Disposing B(a) disposes b(a), but not a. Any subscriptions to b(a) can be GC'd.
12:08
<rbuckton>
So the difference between the two designs is not just one of memory efficiency, but capability. b(a) has a capability that ab does not.
12:27
<rbuckton>
I also discuss a lot of this in https://github.com/tc39/proposal-cancellation/blob/master/stage0/README.md, which was pulled out of the explainer when it advanced to stage 1.
14:52
<littledan>
I don't see anything in these docs about disposal. Is disposal really a necessary feature?
14:57
<littledan>
There's some related discussion about being on AbortController vs AbortSignal in https://github.com/shaseley/abort-signal-any/#exposure-through-abortsignal-vs-abortcontroller
17:03
<rbuckton>
I don't see anything in these docs about disposal. Is disposal really a necessary feature?
See source.close() in that stage 0 explainer. I believe it is important, and the lack of it today is wasteful.
17:09
<rbuckton (traveling)>
https://github.com/tc39/proposal-cancellation/blob/master/stage0%2FREADME.md#sourceclose
17:17
<rbuckton (traveling)>
If you can close/dispose a source, functions that receive the closed token can use more efficenct code paths, and registrations that would have introduced closures can be skipped. A source that is left open instead of canceling/closing results in closures holding references to closed over values far longer than necessary. IIRC, AbortSignal doesn't even clean up user code registrations when aborted since user code registrations are event based
17:28
<rbuckton (traveling)>
FYI, the most recent iteration of this API is here: https://esfx.js.org/esfx/api/canceltoken.html?tabs=ts
17:31
<rbuckton (traveling)>
Where CancelToken.race(cancelables) is the same as AbortSignal.any, but CancelToken.source(cancelables) is preferred.
22:00
<littledan>
See source.close() in that stage 0 explainer. I believe it is important, and the lack of it today is wasteful.
Oh, I see. This is new for me; I need to think more to understand the implications.