Cancellation follows a source -> sink model and consists of three components: Source, Sink, and Signal.
- Source - Created by the caller of an asynchronous operation, a Source is a Signal producer.
- Represented in this proposal as
CancellationSource
.
- Represented in this proposal as
- Sink - Provided by the caller to an asynchronous operation, a Sink is a Signal consumer.
- A Source and its Sink are entangled.
- A Sink can only be used to consume or observe a cancellation Signal.
- Represented in this proposal as a
CancellationToken
.
- Signal - Produced by a Source and consumed by a Sink.
- May be thrown by an asynchronous operation to indicate that the operation was cancelled.
- Represented in this proposal as a
CancelSignal
.
- A clear and consistent approach to cancelling asynchronous operations:
- Fetching remote resources (HTTP, I/O, etc.)
- Interacting with background tasks (Web Workers, forked processes, etc.)
- Long-running operations (animations, etc.)
- A general-purpose coordination primitive with many use cases:
- Synchronous observation (e.g. in a game loop)
- Asynchronous observation (e.g. aborting an XMLHttpRequest, stopping an animation)
- Easy to use in async functions.
- Scale from single source->sink relationships to complex cancellation graphs.
- A single shared API that is reusable in multiple host environments (Browser, NodeJS, IoT, etc.)
A request for cancellation may be observed either synchronously or asynchronously. To observe a cancellation request
synchronously you can either check the token.cancellationRequested
property, or invoke the
token.throwIfCancellationRequested()
method. To observe a cancellation request asynchronously, you can register a
callback using the token.register()
method which returns an object that can be used to unregister the callback once
you no longer need to observe the signal.
When you invoke source.cancel()
, it schedules each registered callback to execute in a later turn and returns a Promise.
Once all registered callbacks have run to completion, the Promise is resolved. If any registered callback results in an
exception, the Promise is rejected.
You can model complex cancellation graphs by further entangling a CancellationSource
with one or more CancellationToken
objects.
For example, you can have a multiple CancellationSource
objects for various asynchronous operations (such as fetching data, running
animations, etc.) that are linked back to a root CancellationSource
that can be used to cancel all operations (such as when the user
navigates to another page):
const root = new CancellationSource();
const animationSources = new WeakMap();
let completionsSource;
function onNavigate() {
root.cancel();
}
function onKeyPress(e) {
// cancel any existing completion
if (completionsSource) completionsSource.cancel();
// create and track a cancellation source linked to the root
completionsSource = new CancellationSource([root.token]);
// fetch auto-complete entries
fetchCompletions(e.target.value, completionsSource.token);
}
function fadeIn(element) {
// cancel any existing animation
const existingSource = animationSources.get(element);
if (existingSource) existingSource.cancel();
// create and track a cancellation source linked to the root
const fadeInSource = new CancellationSource([root.token]);
animationSources.set(element, fadeInSource);
// hand off element and token to animation
beginFadeIn(element, fadeInSource.token);
}
Another usage is to create a CancellationSource
linked to other asynchronous operations:
async function startMonitoring(timeoutSource, disconnectSource) {
const monitorSource = new CancellationSource([timeoutSource, disconnectSource]);
while (!monitorSource.cancellationRequested) {
await pingUser();
}
}
class CancellationSource {
constructor(linkedTokens?: Iterable<CancellationToken>);
readonly token: CancellationToken;
cancel(): Promise<void>;
}
class CancellationToken {
static readonly none: CancellationToken;
static readonly canceled: CancellationToken;
readonly cancellationRequested: boolean;
throwIfCancellationRequested(): void;
register(callback: () => void): { unregister(): void; };
}
class CancelSignal {
constructor(token: CancellationToken);
readonly token: CancellationToken;
}
The following augments the above strawman with additional P2/P3 stretch goals for the proposal:
- P3 - Add an optional
reason
argument toCancellationSource#cancel
that can be used to provide a customError
orCancelSignal
. - P2 - Add
CancellationSource#close()
can be used to lock down a source to prevent future cancellation. - P2 - Add
CancellationToken#canBeCanceled
which can be used to help developers optimize code paths for tokens that will never be cancelled because their source was closed. - P2 - Add a
reason
argument to the callback supplied toCancellationToken#register
that can be used to observe the cancellation signal to better interoperate with thereject
callback for aPromise
.- P3 - or custom signal supplied to
CancellationSource#cancel()
.
- P3 - or custom signal supplied to
- P3 -
CancellationToken#throwIfCancellationRequested()
would thow the custom cancellation reason if one was supplied toCancellationSource#cancel()
. - P3 - Add more information to
CancelSignal
that can be used to customize the signal. - P3 - Add
CancelSignal.isCancelSignal
for unforgeable cross-realm tests for cancellation signals (similar toArray.isArray
).
class CancellationSource {
constructor(linkedTokens?: Iterable<CancellationToken>);
readonly token: CancellationToken;
cancel(reason?: any): Promise<void>;
close(): void;
}
class CancellationToken {
static readonly none: CancellationToken;
static readonly canceled: CancellationToken;
readonly cancellationRequested: boolean;
readonly canBeCanceled: boolean;
throwIfCancellationRequested(): void;
register(callback: (reason: any) => void): { unregister(): void; };
}
class CancelSignal {
constructor(token?: CancellationToken, message?: string);
constructor(message: string);
token: CancellationToken;
message: string;
static isCancelSignal(value: any): value is CancelSignal;
}