diff --git a/README.md b/README.md index 1b45d98..2c3007d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# creed :: async +# creed :: async [![Join the chat at https://gitter.im/briancavalier/creed](https://badges.gitter.im/briancavalier/creed.svg)](https://gitter.im/briancavalier/creed?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Sophisticated and functionally-minded async with advanced features: coroutines, promises, ES2015 iterables, [fantasy-land](https://github.com/fantasyland/fantasy-land). +Sophisticated and functionally-minded async with advanced features: promises, cancellation, coroutines, ES2015 iterables, [fantasy-land](https://github.com/fantasyland/fantasy-land). -Creed simplifies async by letting you write coroutines using ES2015 generators and promises, and encourages functional programming via fantasy-land. It also makes uncaught errors obvious by default, and supports other ES2015 features such as iterables. +Creed simplifies async by letting you write coroutines using ES2015 generators and promises, and encourages functional programming via fantasy-land. It also makes uncaught errors obvious by default, and supports other ES2015 features such as iterables. It empowers you to direct the execution flow in detail through cancellation of callbacks. You can also use [babel](https://babeljs.io) and the [babel-creed-async](https://github.com/briancavalier/babel-creed-async) plugin to write ES7 `async` functions backed by creed coroutines. @@ -18,8 +18,6 @@ You can also use [babel](https://babeljs.io) and the [babel-creed-async](https:/ Using creed coroutines, ES2015, and FP to solve the [async-problem](https://github.com/plaid/async-problem): ```javascript -'use strict'; - import { runNode, all, coroutine } from 'creed'; import { readFile } from 'fs'; import { join } from 'path'; @@ -103,7 +101,7 @@ Promise { fulfilled: winner } # Errors & debugging -By design, uncaught creed promise errors are fatal. They will crash your program, forcing you to fix or [`.catch`](#catch--promise-e-a--e--bpromise-e-b--promise-e-b) them. You can override this behavior by [registering your own error event listener](#debug-events). +By design, uncaught creed promise errors are fatal. They will crash your program, forcing you to fix or [`.catch`](#catch--promise-e-a--e--bthenable-e-b--canceltoken-e--promise-e-b) them. You can override this behavior by [registering your own error event listener](#debug-events). Consider this small program, which contains a `ReferenceError`. @@ -214,9 +212,9 @@ function reportHandled(promise) { ## Run async tasks -### coroutine :: Generator a → (...* → Promise e a) +### coroutine :: GeneratorFunction a → (...* → Promise e a) -Create an async coroutine from a promise-yielding generator. +Create an async coroutine from a promise-yielding generator function. ```js import { coroutine } from 'creed'; @@ -226,7 +224,7 @@ function fetchTextFromUrl(url) { return promise; } -// Make an async coroutine from a generator +// Make an async coroutine from a generator function let getUserProfile = coroutine(function* (userId) { try { let profileUrl = yield getUserProfileUrlFromDB(userId); @@ -242,6 +240,8 @@ getUserProfile(123) .then(profile => console.log(profile)); ``` +For cancellation of coroutines see the [cancellation docs](cancellation.md#coroutines). + ### fromNode :: NodeApi e a → (...* → Promise e a) type NodeApi e a = ...* → Nodeback e a → ()
type Nodeback e a = e → a → () @@ -283,7 +283,7 @@ Run a function to produce a promised result. ```js import { runPromise } from 'creed'; -/* Run a function, threading in a url parameter */ +/* Run a function, passing in a url parameter */ let p = runPromise((url, resolve, reject) => { var xhr = new XMLHttpRequest; xhr.addEventListener("error", reject); @@ -295,7 +295,7 @@ let p = runPromise((url, resolve, reject) => { p.then(result => console.log(result)); ``` -Parameter threading also makes it easy to create reusable tasks +Parameter passing also makes it easy to create reusable tasks that don't rely on closures and scope chain capturing. ```js @@ -313,28 +313,21 @@ runPromise(xhrGet, 'http://...') .then(result => console.log(result)); ``` -### merge :: (...* → b) → ...Promise e a → Promise e b - -Merge promises by passing their fulfillment values to a merge -function. Returns a promise for the result of the merge function. -Effectively liftN for promises. - -```js -import { merge, resolve } from 'creed'; +### new Promise :: Producer e a [→ CancelToken e] → Promise e a -merge((x, y) => x + y, resolve(123), resolve(1)) - .then(z => console.log(z)); //=> 124 -``` +ES6-compliant promise constructor. Run an executor function to produce a promised result. +If the optional cancellation token is passed, it will be associated to the promise. ## Make promises -### future :: () → { resolve: Resolve e a, promise: Promise e a } +### future :: ()|CancelToken e → { resolve :: Resolve e a, promise :: Promise e a } type Resolve e a = a|Thenable e a → ()
Create a `{ resolve, promise }` pair, where `resolve` is a function that seals the fate of `promise`. +If the optional cancellation token is passed, it will be associated to the `promise`. ```js -import { future, reject } from 'creed'; +import { future, reject, CancelToken } from 'creed'; // Fulfill let { resolve, promise } = future(); @@ -350,11 +343,18 @@ resolve(anotherPromise); //=> make promise's fate the same as anotherPromise's let { resolve, promise } = future(); resolve(reject(new Error('oops'))); promise.catch(e => console.log(e)); //=> [Error: oops] + +// Cancel +let { cancel, token } = CancelToken.source(); +let { resolve, promise } = future(token); +cancel(new Error('already done')); +promise.trifurcate(null, null, e => console.log(e)); //=> [Error: already done] ``` -### resolve :: a|Thenable e a → Promise e a +### resolve :: a|Thenable e a [→ CancelToken e] → Promise e a Coerce a value or Thenable to a promise. +If the optional cancellation token is passed, it will be associated to the promise. ```js import { resolve } from 'creed'; @@ -387,6 +387,10 @@ resolve(fulfill(123)) .then(x => console.log(x)); //=> 123 ``` +### Promise.of :: a → Promise e a + +Alias for `fulfill`, completing the [Fantasy-land Applicative](//github.com/fantasyland/fantasy-land#applicative). + ### reject :: Error e => e → Promise e a Make a rejected promise for an error. @@ -398,7 +402,7 @@ reject(new TypeError('oops!')) .catch(e => console.log(e.message)); //=> oops! ``` -### never :: Promise e a +### never :: () => Promise e a Make a promise that remains pending forever. @@ -410,16 +414,23 @@ never() ``` Note: `never` consumes virtually no resources. It does not hold references -to any functions passed to `then`, `map`, `chain`, etc. +to any functions passed to `then`, `map`, `chain`, etc. + +### Promise.empty :: () => Promise e a + +Alias for `never`, completing the [Fantasy-land Monoid](//github.com/fantasyland/fantasy-land#monoid). ## Transform promises -### .then :: Promise e a → (a → b|Promise e b) → Promise e b +### .then :: Promise e a → (a → b|Thenable e b) → (e → b|Thenable e b) [→ CancelToken e] → Promise e b [Promises/A+ then](http://promisesaplus.com/). -Transform a promise's value by applying a function to the -promise's fulfillment value. Returns a new promise for the -transformed result. +Transform a promise's value by applying the first function to the +promise's fulfillment value or the second function to the rejection reason. +Returns a new promise for the transformed result. +If the respective argument is no function, the resolution is passed through. +If the optional cancellation token is passed, it will be associated to the result promise. +The callbacks will never run after cancellation has been requested. ```js import { resolve } from 'creed'; @@ -433,9 +444,11 @@ resolve(1) .then(y => console.log(y)); //=> 2 ``` -### .catch :: Promise e a → (e → b|Promise e b) → Promise e b +### .catch :: Promise e a → (e → b|Thenable e b) [→ CancelToken e] → Promise e b -Catch and handle a promise error. +Catch and handle a promise error. Equivalent to `.then(undefined, onRejected)`. +If the optional cancellation token is passed, it will be associated to the result promise. +The callback will never run after cancellation has been requested. ```js import { reject, resolve } from 'creed'; @@ -449,12 +462,14 @@ reject(new Error('oops!')) .then(x => console.log(x)); //=> 123 ``` -### .map :: Promise e a → (a → b) → Promise e b +### .map :: Promise e a → (a → b) [→ CancelToken e] → Promise e b [Fantasy-land Functor](https://github.com/fantasyland/fantasy-land#functor). Transform a promise's value by applying a function. The return value of the function will be used verbatim, even if it is a promise. Returns a new promise for the transformed value. +If the optional cancellation token is passed, it will be associated to the result promise. +The callback will never run after cancellation has been requested. ```js import { resolve } from 'creed'; @@ -464,11 +479,13 @@ resolve(1) .then(y => console.log(y)); //=> 2 ``` -### .ap :: Promise e (a → b) → Promise e a → Promise e b +### .ap :: Promise e (a → b) → Promise e a [→ CancelToken e] → Promise e b [Fantasy-land Apply](https://github.com/fantasyland/fantasy-land#apply). Apply a promised function to a promised value. Returns a new promise for the result. +If the optional cancellation token is passed, it will be associated to the result promise. +The callback will never run after cancellation has been requested. ```js import { resolve } from 'creed'; @@ -483,11 +500,13 @@ resolve(x => y => x+y) .then(y => console.log(y)); //=> 124 ``` -### .chain :: Promise e a → (a → Promise e b) → Promise e b +### .chain :: Promise e a → (a → Promise e b) [→ CancelToken e] → Promise e b [Fantasy-land Chain](https://github.com/fantasyland/fantasy-land#chain). Sequence async actions. When a promise fulfills, run another async action and return a promise for its result. +If the optional cancellation token is passed, it will be associated to the result promise. +The callback will never run after cancellation has been requested. ```js let profileText = getUserProfileUrlFromDB(userId) @@ -511,16 +530,72 @@ fulfill(123).concat(fulfill(456)) .then(x => console.log(x)); //=> 123 ``` +### .untilCancel :: Promise e a → CancelToken e → Promise e a + +Returns a promise equivalent to the receiver, but with the token associated to it. Equivalent to `.then(null, null, token)`. +Essentially the cancellation is raced against the resolution. +Preference is given to the former, it always returns a cancelled promise if the token is already revoked. + +### .trifurcate :: Promise e a → (a → b|Thenable e b) → (e → b|Thenable e b) → (e → b|Thenable e b) → Promise e b + +Transform a promise's value by applying the first function to the +promise's fulfillment value, the second function to the rejection reason +or the third function to the cancellation reason if the promise was rejected through its associated token. +Returns a new promise for the transformed result, with no cancellation token associated. +If the respective argument is no function, the resolution is passed through. + +It is guaranteed that at most one of the callbacks is called. +It can happen that the `onFulfilled` or `onRejected` callbacks run despite the cancellation having been requested. + +``` +import { delay, CancelToken } from 'creed'; + +const { cancel, token } = CancelToken.source(); +setTimeout(() => { + cancel(new Error('timeout')); +}, 2000); + +fetch(…).untilCancel(token) // better: fetch(…, token) + .trifurcate(x => console.log('result', x), e => console.error(e), e => console.log('cancel', e)); +``` + +### .finally :: Promise e a → (Promise e a → b|Thenable e b) → Promise e a + +Runs the function when the promise settles or its associated token is revoked. +The resolution is not transformed, the callback result is awaited but ignored, unless it rejects. +The returned promise has no cancellation token associated to it. + +In case of cancellation, the callback is executed synchronously like a token subscription, its return value is yielded to the `cancel()` caller. + +### merge :: (...* → b) → ...Promise e a → Promise e b + +Merge promises by passing their fulfillment values to a merge +function. Returns a promise for the result of the merge function. +Effectively liftN for promises. + +```js +import { merge, resolve } from 'creed'; + +merge((x, y) => x + y, resolve(123), resolve(1)) + .then(z => console.log(z)); //=> 124 +``` + +## Cancellation + +For the `CancelToken` documentation see the separate [cancellation API description](cancellation.md#API). + ## Control time -### delay :: Int → a|Promise e a → Promise e a +### delay :: Int → a|Promise e a [→ CancelToken e] → Promise e a Create a delayed promise for a value, or further delay the fulfillment of an existing promise. Delay only delays fulfillment: it has no effect on rejected promises. +If the optional cancellation token is passed, it will be associated to the result promise. +When the cancellation is requested, the timeout is cleared. ```js -import { delay, reject } from 'creed'; +import { delay, reject, CancelToken } from 'creed'; delay(5000, 'hi') .then(x => console.log(x)); //=> 'hi' after 5 seconds @@ -530,6 +605,11 @@ delay(5000, delay(1000, 'hi')) delay(5000, reject(new Error('oops'))) .catch(e => console.log(e.message)); //=> 'oops' immediately + +const { cancel, token } = CancelToken.source(); +delay(2000, 'over').then(cancel); +delay(5000, 'result', token) + .catch(e => console.log(e)); //=> 'over' after 2 seconds ``` ### timeout :: Int → Promise e a → Promise e a @@ -552,7 +632,7 @@ Creed's iterable functions accept any ES2015 Iterable. Most of the examples in this section show Arrays, but Sets, generators, etc. will work as well. -### all :: Iterable (Promise e a) → Promise e [a] +### all :: Iterable (Promise e a) → Promise e (Array a) Await all promises from an Iterable. Returns a promise that fulfills with an array containing all input promise fulfillment values, @@ -627,7 +707,7 @@ any([]) .catch(e => console.log(e)); //=> [RangeError: No fulfilled promises in input] ``` -### settle :: Iterable (Promise e a) → Promise e [Promise e a] +### settle :: Iterable (Promise e a) → Promise e (Array (Promise e a)) Returns a promise that fulfills with an array of settled promises. @@ -647,12 +727,14 @@ settle([resolve(123), reject(new Error('oops')), resolve(456)]) Returns true if the promise is fulfilled. ```js -import { isFulfilled, resolve, reject, delay, never } from 'creed'; +import { isFulfilled, resolve, reject, delay, never, CancelToken } from 'creed'; +const token = new CancelToken(cancel => cancel()); isFulfilled(resolve(123)); //=> true isFulfilled(reject(new Error())); //=> false isFulfilled(delay(0, 123)); //=> true isFulfilled(delay(1, 123)); //=> false +isFulfilled(token.getCancelled());//=> false isFulfilled(never()); //=> false ``` @@ -667,6 +749,7 @@ isRejected(resolve(123)); //=> false isRejected(reject(new Error())); //=> true isRejected(delay(0, 123)); //=> false isRejected(delay(1, 123)); //=> false +isRejected(token.getCancelled());//=> true isRejected(never()); //=> false ``` @@ -681,6 +764,7 @@ isSettled(resolve(123)); //=> true isSettled(reject(new Error())); //=> true isSettled(delay(0, 123)); //=> true isSettled(delay(1, 123)); //=> false +isSettled(token.getCancelled());//=> true isSettled(never()); //=> false ``` @@ -698,6 +782,26 @@ isPending(delay(1, 123)); //=> true isPending(never()); //=> true ``` +### isCancelled :: Promise e a → boolean + +Returns true if the promise is rejected because cancellation was requested through its associated token. + +``` +import { isFulfilled, resolve, reject, delay, never, CancelToken } from 'creed'; +const cancelledToken = new CancelToken(cancel => cancel()); +const { cancel, token } = CancelToken.source() +const p = future(token).promise + +isCancelled(resolve(123)); //=> false +isCancelled(reject(new Error())); //=> false +isCancelled(delay(1, 123)); //=> false +isCancelled(future(cancelledToken).promise); //=> true +isCancelled(delay(0, 123, cancelledToken)); //=> true +isCancelled(p); //=> false +cancel(); +isCancelled(p); //=> true +``` + ### isNever :: Promise e a → boolean Returns true if it is known that the promise will remain pending @@ -711,6 +815,7 @@ isNever(resolve(123)); //=> false isNever(reject(new Error())); //=> false isNever(delay(0, 123)); //=> false isNever(delay(1, 123)); //=> false +isNever(token.getCancelled()); //=> false isNever(never()); //=> true isNever(resolve(never())); //=> true isNever(delay(1000, never())); //=> true @@ -747,8 +852,8 @@ getReason(never()); //=> throws TypeError ### shim :: () → PromiseConstructor|undefined -Polyfill the global `Promise` constructor with an ES6-compliant -creed `Promise`. If there was a pre-existing global `Promise`, +Polyfill the global `Promise` constructor with the [creed `Promise` constructor](#new-promise--producer-e-a--canceltoken-e--promise-e-a). +If there was a pre-existing global `Promise`, it is returned. ```js diff --git a/cancellation.md b/cancellation.md new file mode 100644 index 0000000..2d9df6e --- /dev/null +++ b/cancellation.md @@ -0,0 +1,353 @@ +Creed features cancellation with a cancellation token based approach. + +# Terminology + +1. A **cancellation token** (**`CancelToken`**) is an object with methods for determining whether and when an operation should be cancelled. +2. A **revoked token** is a `CancelToken` that is in the cancelled state, denoting that the result of an operation is no longer of interest. +3. The cancellation can be **requested** by the issuer of the `CancelToken`, thereby revoking it. +4. A **cancellation reason** is a value used to request a cancellation and reject the respective promises. +5. One `CancelToken` might be **associated** with a promise. +6. A **cancelled promise** is a promise that got rejected because its associated token has been revoked. +7. A **cancelled callback** is an `onFulfilled` or `onRejected` handler whose corresponding cancellation token has been revoked. It might be considered an **unregistered** or **ignored** callback. + +# Cancelling… + +Cancellation allows you to stop asynchronous operations built with promises. Use cases might both be in programmatical cancellation, where your program stops doing things after e.g. a timeout has expired or another operation has finished earlier, and in interactive cancellation, where a user triggers the stop through input methods. + +Operations that are supposed to be stoppable must support this explicitly. It is not desired that anyone who holds a promise can cancel the operation that computes the result, therefore the invoker of the operation has to pass in a cancellation token that only he can revoke to request the cancellation. +Passing around this capability explicitly can be a bit verbose at times, but everything else is done by Creed for you. + +## …Promises + +A promise can be cancelled through a cancellation token at any time before it is fulfilled or rejected. For this, the token is associated with the promise. The `new Promise` constructor, the `future` factory and the `resolve` function support this via an optional parameter: +```javascript +import { future, Promise, CancelToken } from 'creed'; + +const token = new CancelToken(…); +var cancellablePromise = new Promise(…, token); +var cancellableFuture = future(token); +var cancellableResolution = resolve(…, token); +``` +Many of the builtin methods also return promises that have a cancellation token associated with them. + +The token that is associated with a promise cannot be changed or removed afterwards (see [`CancelToken.reference`](#) for an alternative). +When the cancellation is requested, all promises that are associated to the token become immediately rejected unless they are already settled. +The rejection reason will be the one that is given as the reason to the cancellation request. +Notice that even promises that already have been resolved to another promise but are still not settled will be cancelled: +```javascript +import { delay, CancelToken } from 'creed'; + +const { token, cancel } = CancelToken.source(); +delay(3000, 'over').then(cancel); + +const { promise, resolve } = future(token); +resolve(delay(5000, 'result')); +promise.then(x => console.log(x), e => console.log(e)); //=> 'over' after 3 seconds +``` + +If you want to associate a token to an already existing promise, you can use the `.untilCancel(token)` method, although this is rarely necessary. + +## …Callbacks + +The most important feature to avoid unnecessary work and to ignore the results of any promise is to prevent callbacks from running. +The main [transformation methods](README.md#transform-promises) (`then`, `catch`, `map`, `ap`, `chain`) have an optional token parameter for this in Creed. +The cancellation token is registered together with the callback that are to be executed when the promise fulfills or rejects. +As soon as the cancellation is requested, the respective callbacks are guaranteed not to be invoked any more (even when the promise is already fulfilled or rejected). The callbacks are "unregistered" or "cancelled" through this. + +The passed token is associated with the returned promise. +```javascript +import { delay, CancelToken } from 'creed'; + +const { token, cancel } = CancelToken.source(); +delay(3000, 'over').then(cancel); + +const p = delay(1000).chain(x => delay(4000, 'result'), token); +// the token is associated with p, so despite p being resolved with the delay we get +p.then(x => console.log(x), e => console.log(e)); //=> 'over' after 3 seconds + +const q = delay(4000).chain(x => { + console.log('never executed'); + return delay(1000, 'result'); +}, token); +// the token being revoked prevents the inner delay from ever being created, and we get +q.then(x => console.log(x), e => console.log(e)); //=> 'over' after 3 seconds +``` + +### Usage + +As a rule of thumb, take + +> You will normally want to pass the token +> +> * to every asynchronous function you call +> * to every transformation method you invoke +> +> or in short, to everything that returns a promise + +A typical function might therefore look like +```javascript +function load(url, token) { + return fetch(url, token) + .then(response => response.readText(token), token) + .map(JSON.parse, token) + .then(d => getDetails(d.result, token), token) + .catch(e => reject(new WrapError('fetching problem', e)), token); +} +``` +When the cancellation is reqested, every promise in the chain (that is not already settled) will be rejected, +and at the same time none of the callbacks (that did not already run) will ever be executed. +If the caller of `load` does not intend to cancel it, he would just pass no `token` (or `undefined` or `null`) and the chain would not be cancellable. + +If you want a strand of actions to run without being cancelled after they have begun, just omit the `token` for them. +Beware of the usage of `.catch` without a token however, it would catch the cancellation reason then, so if you need to deal with exceptions in there better nest: +```javascript +function notCancellable(…) { + return …; // no token within here +} +function partiallyCancellable(…, token) { + return … // use token here + .chain(notCancellable, token) + …; // and there +} +``` + +If an API you are calling does not support cancellation, you of course don't have to pass it a token either. +Just `resolve` it to a Creed promise and attach your callbacks with a token, which means the operation will continue but be ignored when cancellation is requested. + +### finally + +The `finally` method is a helper for ensuring a callback always gets called. It does work a bit like +```javascript +Promise.prototype.finally = function(f) { + const g = () => resolve(f(this)).then(() => this) + return this.then(g, g) +}; +``` +but in contrast to a regular `onRejected` handler without a token it does get called synchronously from a cancellation request on the associated token of `this`, +yielding the result of the `f` call to the canceller so that he might handle possible exceptions which otherwise are usually ignored. + +You can use it for something like +```javascript +startSpinner(); +const token = new CancelToken(showStopbutton); +const p = load('http://…', token) +p.finally(() => { + stopSpinner(); + hideStopbutton(); +}).then(showResult, showErrormessage, token); +``` + +### trifurcate + +Sometimes you want to distinguish wether a promise was fulfilled, rejected, or cancelled through its associated token. +You could do it with synchronous inspection in a `finally` handler, but there is an easier way. +The `trifurcate` method is essentially equivalent to +```javascript +Promise.prototype.trifurcate = function(onFulfilled, onRejected, onCancelled) { + return this.then(onFulfilled, r => (isCancelled(this) ? onCancelled : onRejected)(r)); +}; +``` +You can use it for something like +```javascript +const token = new CancelToken(cancel => { + setTimeout(cancel, 3000) +}); +load('http://…', token).trifurcate(showResult, showErrormessage, showTimeoutmessage); +``` + +## …Coroutines + +Coroutines work with cancellation as well. They simplify dealing with cancellation tokens just like they avoid callbacks. +The above example would read +```javascript +const load = coroutine(function* (url, token) { + coroutince.cancel = token; + try { + const response = yield fetch(url, token); + const d = JSON.parse(yield response.readText(token)); + return yield getDetails(d.result, token); + } catch (e) { + throw new WrapError('fetching problem', e)); + } +}); +``` +You still would have to pass the token to all promise-returning asynchronous function calls, but there are no callbacks any more that you have to register the token with. +Instead, the magic `coroutine.cancel` setter allows you to choose the cancellation token that is used while waiting for each `yield`ed promise. +If the cancellation is requested during the time a promise is awaited, the coroutine will abort and immediately return a completion from the `yield` expression that does only trigger `finally` blocks in the generator function. The promise returned by the coroutine will be rejected like if the token was associated to it. + +This does allow for quite classical patterns: +```javascript +coroutine.cancel = token; +const conn = db.open(); +try { + … yield conn.query(…, token); + return … +} finally { + conn.close(); +} +``` +where the connection is always closed, even when the `token` is revoked during the query. + +It does also make it possible to react specifically to cancellation during a strand of execution in a coroutine: +```javascript +coroutine.cancel = token; +try { + … +} finally { + if (token.requested) { + … // cancelled during a yield in the try block + } +} +``` + +It is also possible to change the `coroutine.cancel` token during the execution of a coroutine: +```javascript +coroutine.cancel = token; +… // uses token here when yielding +coroutine.cancel = null; +… // not cancellable during this section +if (token.requested) …; // manually checking for cancellation +… +coroutine.cancel = token; +yield; // immediately abort if already cancelled +… // uses token here again +``` +The end of the uncancellable section can also be combined into a single `yield coroutine.cancel = token;` statement. + +On accessing, the magic `coroutine.cancel` getter returns the `CancelToken` that is associated with the promise returned by the coroutine. + +# API + +## Create tokens + +### new CancelToken :: ((r → ()) → ()) → CancelToken r + +Calls an executor callback with a function that allows to cancel the created `CancelToken`. + +### CancelToken.source :: () → { cancel :: r → (), token :: CancelToken r } + +Creates a `{ token, cancel }` pair where `token` is a new `CancelToken` and `cancel` is a function to request cancellation. + +### CancelToken.for :: Thenable _ r → CancelToken r + +Creates a cancellation token that is requested when the input promise fulfills. + +### CancelToken.empty :: () → CancelToken _ + +Creates a cancellation token that is never requested, completing the [Fantasy-land Monoid](//github.com/fantasyland/fantasy-land#monoid). + +## Subscribe + +### .requested :: CancelToken r → boolean + +Synchronously determines whether the token is revoked. +```javascript +const { token, cancel } = CancelToken.source(); +console.log(token.requested); //=> false +cancel(); +console.log(token.requested); //=> true +``` + +### .getCancelled :: CancelToken r → Promise r _ + +Returns a promise with this token associated, i.e. one that rejects when the cancellation is requested. Allows for asynchronous subscription: +```javascript +const { token, cancel } = CancelToken.source(); +token.getCancelled().then(null, e => console.log(e)); +token.getCancelled().catch(e => console.log(e)); +token.getCancelled().trifurcate(null, null, e => console.log(e)); +cancel('reason'); +//=> reason, reason, reason +``` + +### .subscribe :: CancelToken r → (r → a|Thenable e a) → Promise e a + +Transforms the token's cancellation reason by applying the function to it. +Returns a promise for the transformed result. +The callback is invoked synchronously from a cancellation request, returning the promise also to the canceller. +If the token is already revoked, the callback is invoked asynchronously. +```javascript +const { token, cancel } = CancelToken.source(); +const p = token.subscribe(r => r + ' accepted'); +const q = token.subscribe(r => { throw new Error(…); }); +console.log(cancel('reason')); //=> [ Fulfilled { value: "reason accepted" }, Rejected { value: Error {…} } ] +p.then(x => console.log(x)); //=> reason accepted +``` + +### .subscribeOrCall :: CancelToken r → (r → a|Thenable e a) [→ (...* → b)] → (...* → b|()) + +Subscribes the callback to be cancelled synchronously from a cancellation request or asynchronously when the token is already revoked. +Returns a function that unsubscribes the callback. + +Unless the callback has already been executed, if the optional second parameter is a function it will be invoked at most once with the unsubscription arguments. +```javascript +const { token, cancel } = CancelToken.source(); +const a = token.subscribeOrCall(r => r + ' accepted', () => console.log('never executed')); +const b = token.subscribeOrCall(r => console.log('never executed'), x => console.log('executed ' + x)); +const c = token.subscribeOrCall(r => { throw new Error(…); }); +b('once'); //=> executed once +b('twice'); // nothing happens +console.log(cancel('reason')); //=> [ Fulfilled { value: "reason accepted" }, Rejected { value: Error {…} } ] +a(); // nothing happens +b(); // still nothing +``` + +This is an especially helpful tool in the promisification of cancellable APIs: +```javascript +import { Promise, reject, CancelToken } from 'creed'; +function fetch(opts, token) { + if (typeof opts == 'string') { + opts = { method: 'GET', url: opts }; + } + token = CancelToken.from(token) || CancelToken.never(); + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + const nocancelAndResolve = token.subscribeOrCall(r => { + xhr.abort(r); + }, resolve); + xhr.onload = () => nocancelAndResolve(fulfill(xhr.response)); + xhr.onerror = e => nocancelAndResolve(reject(e)); + xhr.open(opts.method, opts.url, true); + }, token); +} +``` + +## Combine tokens + +### .concat :: CancelToken r → CancelToken r → CancelToken r + +[Fantasy-land Semigroup](https://github.com/fantasyland/fantasy-land#semigroup). +Returns a new cancellation token that is requested when the earlier of the two is requested. + +### CancelToken.race :: Iterable (CancelToken r) → Race r + +type Race r = { add :: CancelToken r → ... → (), get :: () → CancelToken r } + +The function returns a `Race` object populated with the tokens from the iterable. + +* The `add` method appends one or more tokens to the collection +* The `get` method returns a `CancelToken` that is revoked with the reason of the first requested cancellation in the collection + +Once the resulting token is cancelled, further `add` calls don't have any effect. + +### CancelToken.pool :: Iterable (CancelToken r) → Pool r + +type Pool r = { add :: CancelToken r → ... → (), get :: () → CancelToken r } + +The function returns a `Pool` object populated with the tokens from the iterable. + +* The `add` method appends one or more tokens to the collection +* The `get` method returns a `CancelToken` that is revoked with an array of the reasons once all (but at least one) tokens in the collection have requested cancellation + +Once the resulting token is cancelled, further `add` calls don't have any effect. + +### CancelToken.reference :: ()|CancelToken r → Reference r + +type Reference r = { set :: ()|CancelToken r → (), get :: () → CancelToken r } + +The function returns a `Reference` object storing the token (or nothing) from the argument + +* The `set` method puts a token or nothing (`null`, `undefined`) in the reference +* The `get` method returns a `CancelToken` that is revoked with the reason of the current reference once cancellation is requested + +Once the resulting token is cancelled, further `set` calls are forbidden.