Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Last active April 8, 2017 11:09
Show Gist options
  • Save rbuckton/9473f7303f2fa0b80f3fa7d1a7118ac3 to your computer and use it in GitHub Desktop.
Save rbuckton/9473f7303f2fa0b80f3fa7d1a7118ac3 to your computer and use it in GitHub Desktop.
ECMAScript Cancellation Strawman

Overview

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.
  • 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.

Motivations

  • 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.)

Observing cancellation requests

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.

Finalizing a cancellation request

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.

Complex cancellation graphs

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();
  }
}

API

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;
}

Stretch goals

The following augments the above strawman with additional P2/P3 stretch goals for the proposal:

  • P3 - Add an optional reason argument to CancellationSource#cancel that can be used to provide a custom Error or CancelSignal.
  • 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 to CancellationToken#register that can be used to observe the cancellation signal to better interoperate with the reject callback for a Promise.
    • P3 - or custom signal supplied to CancellationSource#cancel().
  • P3 - CancellationToken#throwIfCancellationRequested() would thow the custom cancellation reason if one was supplied to CancellationSource#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 to Array.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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment