Skip to content

cancjs/canc

Repository files navigation

canc ⮿ a crafty foundation for cancelable promises

Travis CI License PRs Welcome

Cancelable promise ecosystem based on native Promise: coroutines, async iterators, decorators, utilities, third-party library helpers.


Features

  • cancelable promise implementation built on top of ES Promise
  • generator-based cancelable replacements for async..await and async iterators
  • lazily evaluated cancelable promises
  • cancelable Fetch API
  • utility toolbox (delay, timeout, etc)
  • library helpers (Axios, Bluebird, RxJS, etc)
  • decorators for TypeScript and Babel
  • base packages to be used with custom promise implementation
  • UMD and ESM builds for modern and legacy browsers and Node.js
  • TypeScript-ready

Packages

Cancelable Promises

Cancellation-aware promise utilities:

Name Version Description
@cancjs/promise Version Cancelable promise implementation based on ES Promise
@cancjs/coroutine Version Cancelable generator-based drop-in replacements for async..await and async iterators
@cancjs/fetch Version Cross-platform Fetch API that uses cancelable promises
@cancjs/lazy-promise Version Cancelable lazily evaluated promise-like class
@cancjs/toolbox Version A collection of cancellation-aware promise helper functions and ponyfills

Native Promises

General-purpose promise utilities that use built-in Promise as promise implementation where applicable:

Package Version Description
@cancjs/coroutine-native Version Generator-based drop-in replacements for async..await and async iterators
@cancjs/lazy-promise-native Version Lazily evaluated promise-like class
@cancjs/toolbox Version A collection of promise helper functions

How It Works

Cancellation is a special form of promise rejection with cancel error that triggers registered handlers for the entire cancellation-aware promise chain.

canc promises implement two-way cancellation mechanism that treats promise chains as subscriptions:

  • cancellation propagates down the promise chain when parent promise is canceled

  • cancellation bubbles up the chain when all child promises are canceled and parent promise value is no longer consumed

Cancellation bubbling can be explicitly disabled on parent promise in case a promise causes side effects that shouldn't be implicitly discarded.

Two-way cancellation mechanism is supported for all common ways to establish a promise chain, including all, race, etc composition methods and coroutine yield.

A chain is cancelable only if it consists of canc promises. This requires to use cancellation-aware wrappers for Fetch API and third-party librararies, async and async* need to be replaced with cancelable generator-based coroutines.

Motivation

Promise cancellation is highly beneficial in real life scenarios yet it's not a part of existing ECMAScript specification. JavaScript API like Fetch AbortController use their own mechanisms that aren't unified with native promises.

A situation that is common in modern JavaScript applications is that a process like network request that stands behind long-running asynchronous task is abortable, consumers need to unsubscribe from results and abort initial process when it's no longer needed. This eventually becomes harder with uncancelable promises when a task is composed of smaller independent tasks.

See examples for more use cases.

Background

  • No official solution. Native cancelable promises were incompatible with ES6 promise semantics, provided one-way cancellation, used unwieldy cancel tokens and have been abandoned.

  • Bluebird stepped aside. Bluebird has bulky stable API and has been largely superseded by ES promises where applicable, particularly due to async..await. Two-way cancellation is disabled by default and incompatible with native promise semantics.

  • No universal third-party options. JavaScript community provides no comprehensive alternatives based on native promises. Renowned p-* package collection only supports one-way cancellation and targets Node.js.

  • Observables aren't a magic bullet. Observables can provide a superset of promise features, as well as cancellation. However, observables don't offer expressive sugar similar to async..await, cancellation may be lost in promise interop. Observables are push-based and cannot displace pull-based async* async iterators. RxJS is commonly used implementation with complex API, no native observable implementation exists yet.

Compatibility

Packages rely on following ECMAScript 2015+ features: Symbol (ES2018 for async iterators), Reflect, Promise (ES2018 for finally, ES2020 for allSettled), Object.assign, Object.setPrototypeOf.

Native Support

Supported in modern browsers and Node.js:

  • Chrome 49
  • Opera 36
  • Edge 12
  • Firefox 42
  • Safari macOS/iOS 10
  • Android 7 (WebView)
  • Node.js 6

Polyfilled Support

Supported in legacy browsers and Node.js with core-js or polyfill.io:

  • Chrome 5
  • Opera 12
  • Edge 12
  • IE 11
  • Firefox 4
  • Safari macOS/iOS 5
  • Android 4.4 (WebView, browser)
  • Node.js 0.10

The incompatibility between native and polyfilled Promise and Reflect in engines with incomplete ES6 support (Node.js 0.12 to 5, etc) requires to match globals before polyfilling:

Modular environment
var _global = typeof globalThis !== 'undefined' && globalThis
  || typeof self !== 'undefined' && self
  || typeof global !== 'undefined' && global;

if (!('Reflect' in _global) && 'Promise' in _global)
  delete _global.Promise;

require('core-js/stable');
Browser webpage
<script>
if (!('Reflect' in window) && 'Promise' in window)
  delete window.Promise;
</script>
<!-- no es2020 allSettled yet -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=es2015,es2018&flags=always,gated"></script>

Examples

Can be found in examples section.

Contributing

You are welcome to participate through issues and pull requests!

License

MIT