Skip to content
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

Host hook-based cancellation #31

Open
rbuckton opened this issue Apr 7, 2022 · 7 comments
Open

Host hook-based cancellation #31

rbuckton opened this issue Apr 7, 2022 · 7 comments

Comments

@rbuckton
Copy link
Collaborator

rbuckton commented Apr 7, 2022

The last time we looked at this, we were considering a symbol-based protocol in #22. @domenic suggested that we instead consider a host-hook based approach.

Part of this will be determining what the goals of this approach are, and where the boundaries between ECMA-262 and the Host should be:

Goals

  • This will not introduce a new cancellation primitive for end users.
  • ECMA-262 will have the ability to internally leverage cancellation in a consistent manner, regardless of the Host, similar to how Promise can be used internally.
  • Hosts will be able to leverage the provided APIs and host hooks to interoperate with ECMA-262.

Boundaries

  • For ECMA-262 to be able to internally leverage cancellation, certain parts of existing Host algorithms relating to cancellation may need to be moved into ECMA-262.
  • ECMA-262 should be able to observe the cancellation state synchronously.
  • ECMA-262 should be able to react to a cancellation signal synchronously.
  • ECMA-262 should be able to signal cancellation.
  • The Host should provide User-accessible cancellation primitives.
  • User-code reactions to cancellation signals should be added via the cancellation primitive provided by the Host.
  • User-code reactions to cancellation signals should be executed by the Host.

While this is still a fairly early draft, the following is a rough outline of what I'm considering for a host-hook based implementation. I've also tentatively outlined PreventAbort, a capability I would like to have in a cancellation signal implementation to perform cleanup when an algorithm no longer has need of a cancellation signal, and thus it can be garbage collected. There's more I'd like to add to this relating to linking cancellation sources so that entire cancellation graphs clean up references and allow GC, but for now consider PreventAbort to be mostly a stub definition to be expanded later.


Records

AbortCapability Record

  • [[AbortState]] — ~pending~, ~aborted~, or ~closed~
    • ~pending~ — The capability has not yet been aborted or closed
    • ~aborted~ — The capability has been aborted
    • ~closed~ — The capability can no longer be aborted
  • [[AbortReason]] — an ECMAScript language value.
  • [[AbortReactions]] — a List of AbortReaction Records or undefined
  • [[HostDefined]] — anything (default value is ~empty~)
    • Field reserved for use by hosts.

AbortReaction Record

  • [[AbortCapability]] — an AbortCapability Record
  • [[Handler]] — a JobCallback Record or ~empty~

Algorithms

CreateAbortCapability()

  1. Let reactions be a new empty List.
  2. Let capability be the AbortCapability Record { [[AbortState]]: ~pending~, [[AbortReason]]: undefined, [[AbortReactions]]: reactions, [[HostDefined]]: ~empty~ }.
  3. Perform ? HostInitializeAbortCapability(capability).
  4. Return capability.

HostInitializeAbortCapability(capability: an AbortCapability Record)

  • Conformance:
    • It must return ~unused~.
  • Default steps:
    1. Return ~unused~.

AddAbortReaction(capability: an AbortCapability Record, handler: a Function object)

  1. If capability.[[AbortState]] is ~closed~, return undefined.
  2. Let job be HostMakeJobCallback(handler).
  3. If capability.[[AbortState]] is ~aborted~, then
    1. Perform ? HostCallJobCallback(handler, undefined, « »).
    2. Return undefined.
  4. Assert: capability.[[AbortReactions]] is not undefined.
  5. For each element existing of capability.[[AbortReactions]], do
    1. If SameValue(existing.[[Handler]].[[Callback]], handler) is true, then
      1. Return undefined.
  6. Let reaction be the AbortReaction Record { [[AbortCapability]]: capability, [[Handler]]: job }.
  7. Add reaction as the last element of the List that is capability.[[AbortReactions]].
  8. Return reaction.

RemoveAbortReaction(capability: an AbortCapability Record, reaction: an AbortReaction Record)

  1. If capability.[[AbortState]] is not ~pending~, return ~unused~.
  2. Assert: capability.[[AbortReactions]] is not undefined.
  3. Let newReactions be a new List.
  4. For each element existing of capability.[[AbortReaction]], do
    1. If SameValue(existing.[[Handler]].[[Callback]], reaction.[[Handler]].[[Callback]]) is false, then
      1. Add existing as the last element of the List that is newReactions.
  5. Set capability.[[AbortReactions]] to newReactions.
  6. Return ~unused~.

Abort(capability: an AbortCapability Record, reason: an ECMAScript language value)

  1. If capability.[[AbortState]] is not ~pending~, return ~unused~.
  2. Assert: capability.[[AbortReactions]] is not undefined.
  3. Let reactions be capability.[[AbortReactions]].
  4. Set capability.[[AbortState]] to ~aborted~.
  5. Set capability.[[AbortReactions]] to undefined.
  6. Perform ? HostSetAbortReason(capability, reason).
  7. Perform ? TriggerAbortReactions(reactions).
  8. Perform ? HostAbort(capability).
  9. Return ~unused~.

HostSetAbortReason(capability: an AbortCapability Record, reason: an ECMAScript language value)

  • Conformance:
    • It must return ~unused~.
  • Default steps:
    1. If reason is undefined, set reason to be a new Error.
    2. Set capability.[[AbortReason]] to reason.
    3. Return ~unused~.

TriggerAbortReactions(reactions: A List of AbortReaction Records)

  1. For each element reaction of reactions, do
    1. Perform ? HostCallJobCallback(reaction.[[Handler]], undefined, « »).
  2. Return ~unused~.

PreventAbort(capability: an AbortCapability Record)

  1. Assert: capability.[[AbortState]] is ~pending~.
  2. Assert: capability.[[AbortReactions]] is not undefined.
  3. Set capability.[[AbortState]] to ~closed~.
  4. Set capability.[[AbortReactions]] to undefined.
  5. Perform ? HostPreventAbort(capability).
  6. Return ~unused~.

HostPreventAbort(capability: an AbortCapability Record)

  • Conformance:
    • It must return ~unused~.
  • Default steps:
    1. Return ~unused~.

GetAbortCapability(object: an Object)

  1. Let capability be ? HostGetAbortCapability(object).
  2. If capability is undefined, throw a new TypeError exception.
  3. Assert: capability is an AbortCapability Record.
  4. Return capability.

HostGetAbortCapability(object: an Object)

  • Conformance:
    • It must return either an AbortCapability Record or undefined.
  • Default steps:
    1. Return undefined.
@bergus
Copy link

bergus commented Apr 8, 2022

The Host should provide User-accessible cancellation primitives.

I don't see how that's a good idea. Will this mean that all JS engines are going to implement the AbortController/AbortSignal web standard for de-facto interoperability?

ECMA-262 will have the ability to internally leverage cancellation in a consistent manner

This is a big vague. I mean, I understand the goal of simple integration of ECMA-262 with host code, but how/where is ECMAScript actually going to use cancellation? How could I write ECMAScript code that should be cancellable in a standard way, if all the concrete cancellation signal objects are host-dependent?

@benjamingr
Copy link

I don't see how that's a good idea. Will this mean that all JS engines are going to implement the AbortController/AbortSignal web standard for de-facto interoperability?

All (well most) JS environments implement AbortSignal/AbortController (browsers/node/workers/deno/etc) - that's one of the incentives for a hook based approach. No one really likes AbortSignal/AbortController very much but iterating on it and improving it is very doable.

This is a big vague. I mean, I understand the goal of simple integration of ECMA-262 with host code, but how/where is ECMAScript actually going to use cancellation?

That's probably for future proposals but there are plenty of places (like import(...)) that could use the capability to cancel things but currently are unable to because of the lack of language-level cancellation facilities.

How could I write ECMAScript code that should be cancellable in a standard way, if all the concrete cancellation signal objects are host-dependent?

You could not unless composing existing cancellable things from the language or platform. Note this is still an improvement since currently you both cannot cancel your own ongoing things (in the language) or the language's - this approach makes the latter possible.

@bergus
Copy link

bergus commented Apr 9, 2022

I think I don't get the point of specifying these hooks and abstract operations, if ECMA-262 is not actually going to call them. It feels backwards - normally there is a design of how a new ECMAScript code (syntax/builtin function) should work, then abstract operations are defined to describe that behaviour in the necessary spec abstraction. We don't start with records and abstract operations only to see what we can then build from them - without a concrete use case I can't judge whether they're adequate.
Is this just for layering?

Most JS environments implement AbortSignal/AbortController - that's one of the incentives for a hook based approach.

I don't understand how that's an incentive. Why wouldn't we then just specify AbortSignal & AbortController in ECMA-262 - is it because everyone agrees they're ugly, and specifying them now would make it harder to remove them later? Why specify only these hooks, but not how they work together with AbortSignal/AbortController? Is this just an accommodation for those engines that didn't implement them, to keep them optional?

But still, if that's the reason, a protocol-based approach seems far superior - AbortSignal could simply implement [Symbol.cancelSignal] from #22. No need for hooks?

What is the direction in which you want to iterate?

That's probably for future proposals but there are plenty of places (like import(...)) that could use the capability to cancel things but currently are unable to because of the lack of language-level cancellation facilities.

I know plenty of these places :-) However, taking the dynamic import() example: A host is already allowed to reject an import(…) promise if it thinks it should do so (say, when a hypothetical window.navigator.preventFurtherModuleLoading() method is called) - we don't need to change the specification for that, it's a valid host extension today.
And the proposed abstract operations don't seem to allow something like import(specifier, {cancel: …}) - an AbortCapability is a specification value, not a tangible language value, it can't be passed around by ECMAScript code. Neither seem the proposed operations prepare for allowing the use of an AbortSignal in that place.

Is an AbortCapability only meant for communication with the host? E.g. specifying an abort: AbortCapability parameter in Module.evaluate, so that a host can cancel the evaluation of module code with top-level await and the language observes this? And conversely, as an argument to HostImportModuleDynamically, so that the language can decide when a module is no longer needed (say, due to an exception elsewhere) and tell the host to cancel loading?

But again, without seeing examples of where something like CreateAbortCapability() would be used in ECMA-262 (and ideally, how to call it from ECMAScript code), discussing these hooks and records seems rather pointless.

@Jack-Works
Copy link
Member

I'm wondering, if we're not choosing a symbol protocol, how can membrane emulate a signal in another realm.

@benjamingr
Copy link

I think I don't get the point of specifying these hooks and abstract operations, if ECMA-262 is not actually going to call them.

As explained above - there are multiple places where they can be called - note this is a stage 1 proposal and the discussion about the idea. What Ron wrote above isn't "the final spec text" it's an idea.

I don't understand how that's an incentive. Why wouldn't we then just specify AbortSignal & AbortController in ECMA-262 - is it because everyone agrees they're ugly, and specifying them now would make it harder to remove them later?

Because it would require specifying EventTarget and a bunch of other stuff which is pretty big.

But still, if that's the reason, a protocol-based approach seems far superior - AbortSignal could simply implement [Symbol.cancelSignal] from #22. No need for hooks?

Simply put because Chrome don't want two cancellation primitives and are blocking this - that's what's currently blocking the protocol based approach. I can understand that sentiment (users don't care where things are specified and they don't want them to have to cancellation primitives).

What is the direction in which you want to iterate?

Making AbortSignal better by supporting things like AbortSignal.prototype.follow and general ergonomics improvements.

@bergus
Copy link

bergus commented Apr 10, 2022

Ah, thanks @benjamingr, I think that makes the picture a bit clearer. So the goal really is to pass an AbortSignal to ECMAScript operations (say, import(), promises, whatever), without actually specifying AbortSignal and the things surrounding it in ECMA-262. The iteration on the AbortSignal methods (like follow and other combinators) would then happen in the web standards though?

Neither seem the proposed operations prepare for allowing the use of an AbortSignal in that place.

Oh wait I was wrong there. GetAbortCapability would do exactly this. I can pass a new AbortController().signal object to import(), which would then use GetAbortCapability on the object, and can use the AbortCapability to observe the cancellation and do stuff like stopping module evaluation.

Chrome don't want two cancellation primitives and are blocking this - that's what's currently blocking the protocol based approach.

Are Chrome fundamentally opposed to a symbol-based protocol, or are they just trying to avoid having both AbortSignal (web) and CancelSignal (ECMA) in the global namespace? I wonder if this could still be achieved using hooks to specify a "CancelSignal interface" (properties, methods etc) in the language, that a host could then implement on AbortSignal (and also extend inheritance to EventTarget etc).

@tbondwilkinson
Copy link

A few thoughts

  1. AddAbortReaction: To better match the semantics of AbortSignal, would it be better to do nothing in the case that the capability.[[AbortState]] is aborted? If we're trying to match the equivalent of onabort, the callback will never get called in the case that the abort event has already been sent. Essentially maybe AddAbortReaction would be best to just defer if the capability hasn't settled yet.
  2. In the case that AddAbortReaction matches an existing callback, should it return that callback entry? I'm not sure why undefined is better in that case, it seems like either way the caller wants the AbortReaction record.
  3. I like PreventAbort, I think it's an important primitive as well. But I think it should be observable, so maybe there should be an AddCloseReaction.

In general I think this hook approach seems promising, and would slot into existing implementations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants