Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
RFC/roadmap for async fn / CancelToken integration #98
Async Function / CancelTokens
The two main ideas of the proposal are that:
EC has taken a slightly different approach to cancelation since its inception, and the purpose of this mini-RFC is to propose a roadmap and future APIs for bridging the gap between EC APIs, async functions, and cancel tokens.
EC: Cancellation as Third State
Similar to the cancelable promise proposal, EC has embraced the idea that cancelation is a third state; since version 0.7.0, invoking a child task in a try/catch block no longer invokes the catch block if that child task is canceled. This is a major ergonomic win since the alternative (and API prior to 0.7.0) is having to check if the "error" thrown was a Cancellation. The only remnant of "cancellation as error state" is when treating a TaskInstance (the object returned from
EC: Initiating Cancellation
TC39's proposal takes a non-preemptive/cooperative approach to cancellation, such that async operations are, by default, uncancellable, but can opt into cancelability by 1. adjusting their APIs to accept cancel tokens and 2. checking for canceled tokens at specific points in the async operation.
EC takes a preemptive approach to cancellation, such that tasks (async operations) are, by default, cancellable at any point where a
Both of these approaches have their pros and cons, but in my experience maintaining EC, no one has yet complained about EC's preemptive approach, arguably because EC docs discourage explicit/external cancelation in favor of using the declarative Task Modifier API, which encourages the user to write tasks in a clean, well-structured manner that all but eliminates the need for messy explicit/external cancellation.
If EC API had adopted a more cooperative/non-preemptive approach, a task that presently looks like
might instead be written like this
That said, EC doesn't really have an answer for "how can I prevent this task from being abruptly cancelled if it's in the middle of some sensitive operation?" Fortunately, I think there is a middle-ground approach that EC can take.
Problems EC needs to solve
Note: from now on this document will refer to Generator Function-Driven Ember Concurrency Tasks as GFECTs, and the async function variant as AFECTs
EC Tasks: async functions
Ideally, it should be possible to convert a GFECT to an AFECT:
How might the above be written using async functions?
First off, let's clarify the similarities/differences between EC tasks written with generator functions and vanilla async functions.
So, with these constraints in mind, an async function version of the above might look like:
This implies that:
The present API for invoking a task is
CancelToken as Last Arg
warning: wishy-washy hand-wave-y stuff
Perhaps the simplest API to impose would be for EC to always supply a cancelToken as the last argument to AFECTs, and to inspect the last argument to
A MaybeCancelToken describes the circumstances under which the performed task can be cancelled, and can either be/represent:
Overlapping Cancel States
Imagine you have a
Perhaps the default should be to prevent overlap (possibly with a warning in dev mode?) and to consider the second task instance to be in a "queued" state (similar to pending task instances when using
Note that there is actually a case with present-day EC API where this overlap can happen:
EC presently considers this undefined behavior (though I believe it overlaps).
await.cancelToken and GFECT converse
The CancelToken spec acknowledges the verbosity of constantly running
Conversely, EC could provide a GFECT-specific API to opt into non-preemptive mode to prevent untimely cancelation:
What about .isDestroyed / structured concurrency?
An unfortunate consequence of a non-preemptive EC API is that Ember's API is very preemptive; a component can be unrendered at any point and Ember (nor any other framework I know of) is going to wait for unfinished tasks to complete before doing so. So you probably don't want to have AFECTs on Components unless you're extremely careful not to
Are AFECTs worth it?
Not sure; on the one hand, it seems bad to fragment the world with two pretty semantically different approaches to implementing tasks, but on the other hand, the dichotomy between preemptive / non-preemptive tasks is unavoidable, and it may or may not be nice, when deciding how to implement a task, to think of GFECT=preemptive, AFECT=non-preemptive.
Either way, it seems like GFECTs would be benefit from an API for opting into non-preemption using a CancelToken-based API so that they might seamlessly integrate with other CancelToken-based APIs and async functions.