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
reasonargument toCancellationSource#cancelthat can be used to provide a customErrororCancelSignal. - P2 - Add
CancellationSource#close()can be used to lock down a source to prevent future cancellation. - P2 - Add
CancellationToken#canBeCanceledwhich can be used to help developers optimize code paths for tokens that will never be cancelled because their source was closed. - P2 - Add a
reasonargument to the callback supplied toCancellationToken#registerthat can be used to observe the cancellation signal to better interoperate with therejectcallback 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
CancelSignalthat can be used to customize the signal. - P3 - Add
CancelSignal.isCancelSignalfor 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;
}