diff --git a/.gitignore b/.gitignore
index d022f19..7eaac36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ experiments/
node_modules/
build/
coverage/
+perf/logs/
diff --git a/.jsinspectrc b/.jsinspectrc
index 8bfd709..ce70d0a 100644
--- a/.jsinspectrc
+++ b/.jsinspectrc
@@ -1,3 +1,4 @@
{
+ "identifiers": true,
"threshold": 35
}
diff --git a/README.md b/README.md
index 1b45d98..18e08f5 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](cancellation.md), 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 behaviour 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.
+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,7 +387,11 @@ resolve(fulfill(123))
.then(x => console.log(x)); //=> 123
```
-### reject :: Error e => e → Promise e a
+### 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 cancelled.
+
+### .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 cancelled.
+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..334a7c3
--- /dev/null
+++ b/cancellation.md
@@ -0,0 +1,359 @@
+Creed features cancellation with a cancellation token based approach.
+
+It is modelled after [this promise cancellation proposal](https://github.com/bergus/promise-cancellation),
+altough minor discrepancies might be possible (if you find anything, please report a bug).
+
+# Terminology
+
+1. A **cancellation token** (**`CancelToken`**) is an object with methods for determining whether and when an operation should be cancelled.
+2. The cancellation can be **requested** by the issuer of the `CancelToken`, denoting that the result of an operation is no longer of interest
+ and that the operation should be terminated if applicable.
+3. A **cancelled token** is a `CancelToken` that represents a requested cancellation
+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 cancellation was requested through its associated token
+7. The **corresponding** cancellation token of a handler is the associated token of the promise that the handler is meant to resolve
+8. A **cancelled callback** is an `onFulfilled` or `onRejected` handler whose corresponding cancellation token has been cancelled.
+ 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 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`](#canceltokenreference--canceltoken-r--reference-r) 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 cancelled 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 requested, 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 whether 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 cancelled 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 cancellation has been requested.
+```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 cancelled, 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 cancelled.
+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 cancelled 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 cancelled 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 cancelled with the reason of the current reference once cancellation is requested
+
+Once the resulting token is cancelled, further `set` calls are forbidden.
diff --git a/package.json b/package.json
index 9b83e00..83acb08 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"jsnext:main": "dist/creed.es.js",
"files": [
"dist/creed.js",
- "dist/creed.es.js"
+ "dist/creed.es.js",
+ "dist/creed.node.js"
],
"repository": {
"type": "git",
@@ -28,13 +29,13 @@
"scripts": {
"compile": "npm run compile-src",
"compile-src": "mkdirp build/src && buble -m -i src -o build/src --no modules",
- "build-dist": "npm run compile && mkdirp dist && rollup -c",
- "build": "npm run build-dist && uglifyjs -c 'warnings=false' -m -o dist/creed.min.js -- dist/creed.js",
+ "build-dist": "npm run compile && mkdirp dist && rollup -c && rollup -f cjs -o dist/creed.node.js src/main.js",
+ "build": "npm run build-dist && uglifyjs -c \"warnings=false\" -m -o dist/creed.min.js -- dist/creed.js",
"preversion": "npm run build",
"check-coverage": "istanbul check-coverage --statements 100 --branches 100 --lines 100 --functions 100 coverage/coverage*.json",
"lint": "jsinspect src && eslint src",
"pretest": "npm run lint",
- "test": "istanbul cover _mocha",
+ "test": "istanbul cover node_modules/mocha/bin/_mocha",
"posttest": "npm run test-aplus",
"test-aplus": "promises-aplus-tests test/aplus.js --reporter dot"
},
diff --git a/perf/doxbee-sequential-errors/promises-creed-algebraic.js b/perf/doxbee-sequential-errors/promises-creed-algebraic.js
index 8df2c47..736ad7f 100644
--- a/perf/doxbee-sequential-errors/promises-creed-algebraic.js
+++ b/perf/doxbee-sequential-errors/promises-creed-algebraic.js
@@ -2,7 +2,7 @@ global.useCreed = true;
global.useQ = false;
global.useBluebird = false;
-var creed = require('../../dist/creed');
+var creed = require('../..');
require('../lib/fakesP');
diff --git a/perf/doxbee-sequential-errors/promises-creed-generator.js b/perf/doxbee-sequential-errors/promises-creed-generator.js
index 6eac22f..8ee5607 100644
--- a/perf/doxbee-sequential-errors/promises-creed-generator.js
+++ b/perf/doxbee-sequential-errors/promises-creed-generator.js
@@ -2,7 +2,7 @@ global.useCreed = true;
global.useQ = false;
global.useBluebird = false;
-var creed = require('../../dist/creed');
+var creed = require('../..');
require('../lib/fakesP');
diff --git a/perf/doxbee-sequential-errors/promises-creed.js b/perf/doxbee-sequential-errors/promises-creed.js
index dce1b70..733e745 100644
--- a/perf/doxbee-sequential-errors/promises-creed.js
+++ b/perf/doxbee-sequential-errors/promises-creed.js
@@ -2,7 +2,7 @@ global.useCreed = true;
global.useQ = false;
global.useBluebird = false;
-var creed = require('../../dist/creed');
+var creed = require('../..');
require('../lib/fakesP');
diff --git a/perf/doxbee-sequential/promises-creed-algebraic.js b/perf/doxbee-sequential/promises-creed-algebraic.js
index 9ab6b5d..74b5a95 100644
--- a/perf/doxbee-sequential/promises-creed-algebraic.js
+++ b/perf/doxbee-sequential/promises-creed-algebraic.js
@@ -2,7 +2,7 @@ global.useCreed = true;
global.useQ = false;
global.useBluebird = false;
-var creed = require('../../dist/creed');
+var creed = require('../..');
require('../lib/fakesP');
@@ -52,7 +52,7 @@ module.exports = function upload(stream, idOrPath, tag, done) {
}).chain(function() {
return File.whereUpdate({id: fileId}, {version: version.id})
.execWithin(tx);
- }).map(function() {
+ }).then(function() {
tx.commit();
return done();
}, function(err) {
diff --git a/perf/doxbee-sequential/promises-creed-generator.js b/perf/doxbee-sequential/promises-creed-generator.js
index d594b87..5ed6323 100644
--- a/perf/doxbee-sequential/promises-creed-generator.js
+++ b/perf/doxbee-sequential/promises-creed-generator.js
@@ -1,7 +1,7 @@
global.useBluebird = false;
global.useQ = false;
global.useCreed = true;
-var creed = require('../../dist/creed');
+var creed = require('../..');
require('../lib/fakesP');
module.exports = creed.coroutine(function* upload(stream, idOrPath, tag, done) {
diff --git a/perf/doxbee-sequential/promises-creed.js b/perf/doxbee-sequential/promises-creed.js
index d582818..c0bf159 100644
--- a/perf/doxbee-sequential/promises-creed.js
+++ b/perf/doxbee-sequential/promises-creed.js
@@ -2,7 +2,7 @@ global.useCreed = true;
global.useQ = false;
global.useBluebird = false;
-var creed = require('../../dist/creed');
+var creed = require('../..');
require('../lib/fakesP');
diff --git a/perf/lib/fakesP.js b/perf/lib/fakesP.js
index 9fd13d6..7e8d3ff 100644
--- a/perf/lib/fakesP.js
+++ b/perf/lib/fakesP.js
@@ -83,7 +83,7 @@ else if (global.useNative) {
};
}
else if (global.useCreed) {
- var lifter = require('../../dist/creed').fromNode;
+ var lifter = require('../..').fromNode;
}
else {
var lifter = require('when/node').lift;
diff --git a/perf/madeup-parallel/promises-creed-generator.js b/perf/madeup-parallel/promises-creed-generator.js
index 7dd5ba2..4ce86f7 100644
--- a/perf/madeup-parallel/promises-creed-generator.js
+++ b/perf/madeup-parallel/promises-creed-generator.js
@@ -3,7 +3,7 @@ global.useQ = false;
global.useWhen = false;
global.useCreed = true;
-var creed = require('../../dist/creed');
+var creed = require('../..');
require('../lib/fakesP');
module.exports = creed.coroutine(function* upload(stream, idOrPath, tag, done) {
diff --git a/perf/madeup-parallel/promises-creed.js b/perf/madeup-parallel/promises-creed.js
index 301ca5d..c8487e0 100644
--- a/perf/madeup-parallel/promises-creed.js
+++ b/perf/madeup-parallel/promises-creed.js
@@ -4,7 +4,7 @@ global.useWhen = false;
global.useCreed = true;
-var creed = require('../../dist/creed');
+var creed = require('../..');
require('../lib/fakesP');
diff --git a/perf/performance.js b/perf/performance.js
index 8d42ce8..0bca2b6 100644
--- a/perf/performance.js
+++ b/perf/performance.js
@@ -1,6 +1,5 @@
var args = require('optimist').argv;
-
var path = require('path');
global.LIKELIHOOD_OF_REJECTION = args.e || 0.1;
@@ -138,7 +137,7 @@ function measure(files, requests, time, parg, callback) {
async.mapSeries(files, function(f, done) {
console.log("benchmarking", f);
var logFile = path.basename(f) + ".log";
- var profileFlags = ["--prof", "--logfile=C:/etc/v8/" + logFile];
+ var profileFlags = ["--prof", "--logfile=logs/" + logFile];
var argsFork = [__filename,
'--n', requests,
diff --git a/src/Action.js b/src/Action.js
new file mode 100644
index 0000000..7d16424
--- /dev/null
+++ b/src/Action.js
@@ -0,0 +1,106 @@
+import { noop } from './util'
+import { reject } from './Promise'
+
+export default class Action {
+ constructor (promise) {
+ // the Future which this Action tries to resolve
+ // when null, the action is cancelled and won't be executed
+ this.promise = promise
+ }
+
+ destroy () {
+ this.promise = null
+ }
+
+ cancel (p) {
+ /* istanbul ignore else */
+ if (this.promise._isResolved()) { // promise checks for cancellation itself
+ this.destroy()
+ }
+ }
+
+ // default onFulfilled action
+ fulfilled (p) {
+ this.put(p)
+ }
+
+ // default onRejected action
+ rejected (p) {
+ this.put(p)
+ return false
+ }
+
+ // default onCancelled action
+ cancelled (p) {
+ reject(p.near().value)._runAction(this)
+ }
+
+ // when this.promise is to be settled (possible having awaited the result)
+ put (p) {
+ // assert: isSettled(p) || p.token === this.promise.token
+ // asssert: this.promise != null
+ this.end().__become(p)
+ }
+
+ end () {
+ const promise = this.promise
+ const token = promise.token
+ this.promise = null
+ if (token != null) token._unsubscribe(this)
+ return promise
+ }
+}
+
+const sentinel = noop // Symbol('currently executing')
+
+export class CancellableAction extends Action {
+ constructor (f, promise) {
+ super(promise)
+ // the function that produces the resolution result for the promise
+ // when null, the function has been executed but the promise might still get cancelled
+ this.f = f
+ }
+
+ destroy () {
+ this.promise = null
+ this.f = null
+ }
+
+ cancel (results) {
+ if (this.promise._isResolved()) { // promise checks for cancellation itself
+ if (this.f !== sentinel) { // not currently running
+ this.destroy()
+ }
+ // otherwise keep the cancelled .promise so that it stays usable in handle()
+ // and ignores whatever is done with the f() result
+ return true
+ }
+ return false
+ }
+
+ fulfilled (p) {
+ if (this.f) {
+ this.tryCall(this.f, p.value)
+ } else {
+ this.put(p)
+ }
+ }
+
+ tryCall (f, x) {
+ this.f = sentinel
+ let result
+ try {
+ result = f(x)
+ } catch (e) {
+ this.f = null
+ this.end()._reject(e)
+ return
+ }
+ this.f = null
+ return this.handle(result)
+ }
+
+ handle (p) {
+ this.promise._resolve(p, this)
+ }
+}
diff --git a/src/Any.js b/src/Any.js
index be84b2e..278349a 100644
--- a/src/Any.js
+++ b/src/Any.js
@@ -1,4 +1,5 @@
-import { silenceError } from './inspect.js'
+import { silenceError } from './Promise' // deferred
+import CancelReason from './CancelReason'
export default class Any {
constructor () {
@@ -10,7 +11,9 @@ export default class Any {
}
fulfillAt (p, i, promise) {
+ const token = promise.token
promise._become(p)
+ token._cancel(new CancelReason('result is already fulfilled'))
}
rejectAt (p, i, promise) {
diff --git a/src/CancelReason.js b/src/CancelReason.js
new file mode 100644
index 0000000..f017167
--- /dev/null
+++ b/src/CancelReason.js
@@ -0,0 +1,2 @@
+export default class CancelReason extends Error {
+}
diff --git a/src/CancelToken.js b/src/CancelToken.js
new file mode 100644
index 0000000..6c372fd
--- /dev/null
+++ b/src/CancelToken.js
@@ -0,0 +1,308 @@
+import { noop } from './util'
+import { Future, resolve, cancel, taskQueue } from './Promise' // deferred
+import { subscribe, subscribeOrCall } from './subscribe'
+
+export default class CancelToken {
+ // https://domenic.github.io/cancelable-promise/#sec-canceltoken-constructor
+ constructor (executor) {
+ if (typeof executor !== 'function') {
+ throw new TypeError('must provide an executor function')
+ }
+ this._cancelled = false
+ this.promise = void 0
+ this.length = 0
+ this.scanLow = 0
+ this.scanHigh = 0
+ if (executor !== noop) {
+ executor(reason => this._cancel(reason))
+ }
+ }
+ _cancel (reason) {
+ if (this._cancelled) return
+ return this.__cancel(cancel(reason))
+ }
+ __cancel (p) {
+ this._cancelled = true
+ if (this.length) {
+ taskQueue.add(this) // needs to be called before __become
+ }
+ if (this.promise !== void 0) {
+ this.promise.__become(p)
+ } else {
+ // p.token = this no more necessary
+ this.promise = p
+ }
+ return this._runSync([])
+ }
+ _runSync (results) {
+ // let j = 0;
+ for (let i = 0; i < this.length; ++i) {
+ if (this[i] && this[i].promise) { // not already destroyed
+ this[i].cancel(results, this.promise)
+ // if (this[i].promise) {
+ // this[j++] = this[i]
+ // }
+ }
+ // if (j < i)
+ // this[i] = void 0
+ // }
+ }
+ // this.length = j;
+ return results
+ }
+ run () {
+ const l = this.length
+ for (let i = 0; i < l; ++i) {
+ if (this[i] && this[i].promise) { // not already destroyed
+ this[i].cancelled(this.promise)
+ }
+ this[i] = void 0
+ }
+ if (this.length === l) {
+ this.length = 0
+ } else {
+ taskQueue.add(this)
+ }
+ }
+ _subscribe (action) {
+ if (this.requested) {
+ action.cancel(null, this)
+ if (this.length === 0) {
+ taskQueue.add(this)
+ }
+ }
+ this[this.length++] = action
+ }
+ _unsubscribe (action) {
+ /* eslint complexity:[2,6] */
+ let i = this._cancelled ? 0 : Math.min(5, this.length)
+ while (i--) {
+ // an inplace-filtering algorithm to remove empty actions
+ // executed at up to 5 steps per unsubscribe
+ if (this.scanHigh < this.length) {
+ if (this[this.scanHigh] === action) {
+ this[this.scanHigh] = action = null
+ } else if (this[this.scanHigh].promise == null) {
+ this[this.scanHigh] = null
+ } else {
+ this[this.scanLow++] = this[this.scanHigh]
+ }
+ this.scanHigh++
+ } else {
+ this.length = this.scanLow
+ this.scanLow = this.scanHigh = 0
+ }
+ }
+ if (action) { // when not found
+ action.destroy() // at least mark explicitly as empty
+ }
+ }
+ subscribe (fn, token) {
+ return subscribe(fn, this, new Future(token))
+ }
+ subscribeOrCall (fn, c) {
+ return subscribeOrCall(fn, c, this, new Future())
+ }
+ getCancelled () {
+ if (this.promise === void 0) {
+ this.promise = new Future(this) // while not settled, provides a reference to token
+ }
+ return this.promise
+ }
+ // https://domenic.github.io/cancelable-promise/#sec-canceltoken.prototype.requested
+ get requested () {
+ return this._cancelled
+ }
+ // https://domenic.github.io/cancelable-promise/#sec-canceltoken.source
+ static source () {
+ if (this === CancelToken) {
+ const token = new this(noop)
+ return {
+ token,
+ cancel (r) { return token._cancel(r) }
+ }
+ } else {
+ let cancel
+ const token = new this(c => { cancel = c })
+ return {token, cancel}
+ }
+ }
+ static for (thenable) {
+ return new this(cancel => resolve(thenable).then(cancel)) // finally?
+ }
+ static from (cancelTokenlike) {
+ if (cancelTokenlike == null) return null
+ /* istanbul ignore else */
+ if (cancelTokenlike instanceof CancelToken) return cancelTokenlike
+ else throw new TypeError('not a CancelToken') // TODO
+ }
+ static empty () {
+ return new this(noop) // NeverCancelToken
+ }
+ concat (token) {
+ return new CancelTokenRace([this, token]).get()
+ }
+ static race (tokens) {
+ return new CancelTokenRace(tokens)
+ }
+ static pool (tokens) {
+ return new CancelTokenPool(tokens)
+ }
+ static reference (cur) {
+ return new CancelTokenReference(cur)
+ }
+}
+
+class LiveCancelToken extends CancelToken {
+ constructor (check) {
+ super(noop)
+ this.check = check
+ }
+ __cancel (p) {
+ this.check = null
+ return super.__cancel(p)
+ }
+ get requested () {
+ return this._cancelled || this.check._testRequested()
+ }
+}
+
+class CancelTokenCombinator { // implements cancel parts of Action
+ constructor () {
+ // should be named "token" but is necessary for Action-like usage
+ this.promise = new LiveCancelToken(this)
+ }
+ /* istanbul ignore next */
+ destroy () {
+ // possibly called when unsubscribed from a token
+ }
+ cancelled (p) {}
+ // abstract cancel (res, p) {}
+ // abstract _testRequested () {}
+ get () {
+ return this.promise
+ }
+}
+
+class CancelTokenRace extends CancelTokenCombinator {
+ constructor (tokens) {
+ super()
+ this.tokens = []
+ if (tokens) this.add(...tokens)
+ }
+ cancel (results, p) {
+ /* istanbul ignore if */
+ if (this.tokens == null) return // when called after been unsubscribed but not destroyed
+ // assert: !this.promise._cancelled
+ // for (let t of this.tokens) { // https://phabricator.babeljs.io/T2164
+ for (let i = 0, t; i < this.tokens.length && (t = this.tokens[i]); i++) {
+ t._unsubscribe(this)
+ }
+ this.tokens = null
+ const res = this.promise.__cancel(p)
+ if (results) results.push(...res)
+ }
+ _testRequested () {
+ return this.tokens.some(t => t.requested)
+ }
+ add (...tokens) {
+ if (this.tokens == null) return
+ // for (let t of tokens) { // https://phabricator.babeljs.io/T2164
+ for (let i = 0, t; i < tokens.length && (t = tokens[i]); i++) {
+ t = CancelToken.from(t)
+ if (t === this.promise || t == null) {
+ continue
+ }
+ if (t.requested) {
+ this.cancel(null, t.getCancelled())
+ break
+ } else {
+ this.tokens.push(t)
+ t._subscribe(this)
+ }
+ }
+ }
+}
+
+class CancelTokenPool extends CancelTokenCombinator {
+ constructor (tokens) {
+ super()
+ this.tokens = []
+ this.count = 0
+ if (tokens) this.add(...tokens)
+ }
+ cancel (results, p) {
+ // assert: !this.promise._cancelled
+ this.count--
+ const res = this._check()
+ if (results && res) results.push(...res)
+ }
+ _testRequested () {
+ return this.tokens.length > 0 && this.tokens.every(t => t.requested)
+ }
+ _check () {
+ if (this.count === 0) {
+ const reasons = this.tokens.map(t => t.getCancelled().near().value)
+ this.tokens = null
+ return this.promise.__cancel(cancel(reasons))
+ }
+ }
+ add (...tokens) {
+ if (this.tokens == null) return
+ this.count += tokens.length
+ // for (let t of tokens) { // https://phabricator.babeljs.io/T2164
+ for (let i = 0, t; i < tokens.length && (t = tokens[i]); i++) {
+ t = CancelToken.from(t)
+ if (t === this.promise || t == null) {
+ this.count--
+ continue
+ }
+ this.tokens.push(t)
+ if (t.requested) {
+ this.count--
+ } else {
+ t._subscribe(this)
+ }
+ }
+ if (this.tokens.length > 0) {
+ this._check()
+ }
+ }
+}
+
+export class CancelTokenReference extends CancelTokenCombinator {
+ constructor (cur) {
+ super()
+ this.curToken = cur
+ }
+ cancel (results, p) {
+ /* istanbul ignore if */
+ if (this.curToken == null || this.curToken.getCancelled() !== p) return // when called from an oldToken
+ // assert: !this.promise._cancelled
+ const res = this.promise.__cancel(p)
+ if (results) results.push(...res)
+ }
+ _testRequested () {
+ return this.curToken != null && this.curToken.requested
+ }
+ set (newToken) {
+ /* eslint complexity:[2,7] */
+ const oldToken = this.curToken
+ if (oldToken && oldToken.requested) {
+ throw new ReferenceError('token must not be changed after being cancelled')
+ }
+ if (oldToken !== newToken && this.promise !== newToken) {
+ if (oldToken) {
+ oldToken._unsubscribe(this)
+ }
+ this.curToken = newToken
+ if (newToken) {
+ if (newToken.requested) {
+ this.promise.__cancel(newToken.getCancelled())
+ } else {
+ newToken._subscribe(this)
+ }
+ }
+ }
+ }
+}
diff --git a/src/ErrorHandler.js b/src/ErrorHandler.js
index b4e0607..a53247f 100644
--- a/src/ErrorHandler.js
+++ b/src/ErrorHandler.js
@@ -1,20 +1,22 @@
-import { silenceError, isHandled } from './inspect'
+import { silenceError } from './Promise' // deferred
+import { isHandled } from './inspect'
-const UNHANDLED_REJECTION = 'unhandledRejection'
-const HANDLED_REJECTION = 'rejectionHandled'
+export const UNHANDLED_REJECTION = 'unhandledRejection'
+export const HANDLED_REJECTION = 'rejectionHandled'
export default class ErrorHandler {
constructor (emitEvent, reportError) {
this.errors = []
this.emit = emitEvent
this.reportError = reportError
+ this.report = () => this._reportErrors()
}
track (e) {
if (!this.emit(UNHANDLED_REJECTION, e, e.value)) {
/* istanbul ignore else */
if (this.errors.length === 0) {
- setTimeout(reportErrors, 1, this.reportError, this.errors)
+ setTimeout(this.report, 1)
}
this.errors.push(e)
}
@@ -24,22 +26,22 @@ export default class ErrorHandler {
silenceError(e)
this.emit(HANDLED_REJECTION, e)
}
-}
-function reportErrors (report, errors) {
- try {
- reportAll(errors, report)
- } finally {
- errors.length = 0
+ _reportErrors () {
+ try {
+ this._reportAll(this.errors)
+ } finally {
+ this.errors.length = 0
+ }
}
-}
-function reportAll (errors, report) {
- for (let i = 0; i < errors.length; ++i) {
- const e = errors[i]
- /* istanbul ignore else */
- if (!isHandled(e)) {
- report(e)
+ _reportAll (errors) {
+ for (let i = 0; i < errors.length; ++i) {
+ const e = errors[i]
+ /* istanbul ignore else */
+ if (!isHandled(e)) {
+ this.reportError(e)
+ }
}
}
}
diff --git a/src/Merge.js b/src/Merge.js
index 4d988e2..6f24377 100644
--- a/src/Merge.js
+++ b/src/Merge.js
@@ -1,3 +1,5 @@
+import CancelReason from './CancelReason'
+
export default class Merge {
constructor (mergeHandler, results) {
this.pending = 0
@@ -15,7 +17,9 @@ export default class Merge {
}
rejectAt (p, i, promise) {
+ const token = promise.token
promise._become(p)
+ token._cancel(new CancelReason('result is already rejected', p.value))
}
complete (total, promise) {
diff --git a/src/Promise.js b/src/Promise.js
index f6299a4..f6b5134 100644
--- a/src/Promise.js
+++ b/src/Promise.js
@@ -1,23 +1,26 @@
-import TaskQueue from './TaskQueue'
+import { isObject, noop } from './util'
+import { PENDING, FULFILLED, REJECTED, CANCELLED, NEVER, HANDLED } from './state'
+import { isRejected, isNever, isSettled } from './inspect'
+
+import { TaskQueue, Continuation } from './TaskQueue'
import ErrorHandler from './ErrorHandler'
-import makeEmitError from './emitError'
-import maybeThenable from './maybeThenable'
-import { PENDING, FULFILLED, REJECTED, NEVER } from './state'
-import { isNever, isSettled } from './inspect'
+import emitError from './emitError'
+import Action from './Action'
import then from './then'
import map from './map'
import chain from './chain'
+import fin from './finally'
+import trifurcate from './trifurcate'
+
+import CancelToken from './CancelToken'
-import Race from './Race'
-import Merge from './Merge'
-import { resolveIterable, resultsArray } from './iterable'
+import { race } from './combinators'
-const taskQueue = new TaskQueue()
-export { taskQueue }
+export const taskQueue = new TaskQueue()
/* istanbul ignore next */
-const errorHandler = new ErrorHandler(makeEmitError(), e => {
+const errorHandler = new ErrorHandler(emitError, e => {
throw e.value
})
@@ -36,6 +39,15 @@ class Core {
static of (x) {
return fulfill(x)
}
+
+ // toString :: Promise e a -> String
+ toString () {
+ return '[object ' + this.inspect() + ']'
+ }
+
+ _whenToken (action) {
+ return action
+ }
}
// data Promise e a where
@@ -47,44 +59,45 @@ class Core {
// Future :: Promise e a
// A promise whose value cannot be known until some future time
export class Future extends Core {
- constructor () {
+ constructor (token) {
super()
this.ref = void 0
this.action = void 0
+ this.token = CancelToken.from(token)
this.length = 0
}
// then :: Promise e a -> (a -> b) -> Promise e b
// then :: Promise e a -> () -> (e -> b) -> Promise e b
// then :: Promise e a -> (a -> b) -> (e -> b) -> Promise e b
- then (f, r) {
+ then (f, r, token) {
const n = this.near()
- return n === this ? then(f, r, n, new Future()) : n.then(f, r)
+ return n === this ? then(f, r, n, new Future(token)) : n.then(f, r, token)
}
// catch :: Promise e a -> (e -> b) -> Promise e b
- catch (r) {
+ catch (r, token) {
const n = this.near()
- return n === this ? then(void 0, r, n, new Future()) : n.catch(r)
+ return n === this ? then(void 0, r, n, new Future(token)) : n.catch(r, token)
}
// map :: Promise e a -> (a -> b) -> Promise e b
- map (f) {
+ map (f, token) {
const n = this.near()
- return n === this ? map(f, n, new Future()) : n.map(f)
+ return n === this ? map(f, n, new Future(token)) : n.map(f, token)
}
// ap :: Promise e (a -> b) -> Promise e a -> Promise e b
- ap (p) {
+ ap (p, token) {
const n = this.near()
const pp = p.near()
- return n === this ? this.chain(f => pp.map(f)) : n.ap(pp)
+ return n === this ? this.chain(f => pp.map(f, token), token) : n.ap(pp, token)
}
// chain :: Promise e a -> (a -> Promise e b) -> Promise e b
- chain (f) {
+ chain (f, token) {
const n = this.near()
- return n === this ? chain(f, n, new Future()) : n.chain(f)
+ return n === this ? chain(f, n, new Future(token)) : n.chain(f, token)
}
// concat :: Promise e a -> Promise e a -> Promise e a
@@ -98,9 +111,35 @@ export class Future extends Core {
: race([n, bp])
}
- // toString :: Promise e a -> String
- toString () {
- return '[object ' + this.inspect() + ']'
+ // untilCancel :: Promise e a -> CancelToken e -> Promise e a
+ untilCancel (token) {
+ /* eslint complexity:[2,5] */
+ const n = this.near()
+ if (n !== this) {
+ return n.untilCancel(token)
+ } else if (token == null || token === this.token) {
+ return this
+ }
+ const p = new Future(token)
+ if (p.token.requested) {
+ return p.token.getCancelled()
+ }
+ const put = new Action(p)
+ token._subscribe(put)
+ this._runAction(put)
+ return p
+ }
+
+ // finally :: Promise e a -> (Promise e a -> ()) -> Promise e a
+ finally (f) {
+ const n = this.near()
+ return n === this ? fin(f, this, new Future(this.token)) : n.finally(f)
+ }
+
+ // trifurcate :: Promise e a -> (a -> b) -> (e -> b) -> (e -> b) -> Promise e b
+ trifurcate (f, r, c) {
+ const n = this.near()
+ return n === this ? trifurcate(f, r, c, this, new Future()) : n.trifurcate(f, r, c)
}
// inspect :: Promise e a -> String
@@ -111,20 +150,23 @@ export class Future extends Core {
// near :: Promise e a -> Promise e a
near () {
- if (!this._isResolved()) {
+ if (!this._isResolved() || this.ref === this) {
return this
+ } else {
+ this.ref = this.ref.near()
+ return this.ref
}
-
- this.ref = this.ref.near()
- return this.ref
}
// state :: Promise e a -> Int
state () {
- return this._isResolved() ? this.ref.near().state() : PENDING
+ return this._isResolved() && this.ref !== this ? this.ref.near().state() : PENDING
}
_isResolved () {
+ if (this.token != null && this.token.requested) {
+ this.__become(this.token.getCancelled())
+ }
return this.ref !== void 0
}
@@ -140,8 +182,50 @@ export class Future extends Core {
}
}
- _resolve (x) {
- this._become(resolve(x))
+ _whenToken (action) {
+ if (this.token != null) {
+ // assert: !this.token.requested
+ this.token._subscribe(action)
+ }
+ return action
+ }
+
+ _resolve (x, cancelAction) {
+ if (this._isResolved()) {
+ return // TODO: still resolve thenables when cancelled?
+ }
+ if (isPromise(x)) {
+ this._resolvePromise(x.near(), cancelAction)
+ } else {
+ // TODO: can a thenable end up with a Never?
+ if (cancelAction) {
+ cancelAction.end()
+ }
+ this.__become(isObject(x) ? refForMaybeThenable(x, this.token) : new Fulfilled(x))
+ }
+ }
+
+ _resolvePromise (p, cancelAction) {
+ /* eslint complexity:[2,6] */
+ if (p === this) {
+ p = cycle()
+ } else {
+ const state = p.state()
+ if ((state & NEVER) > 0) {
+ p = p.untilCancel(this.token)
+ } else if ((state & CANCELLED) > 0) {
+ p = reject(p.value)
+ } else if ((state & PENDING) > 0 && this.token !== p.token) {
+ this.ref = this
+ // reuse cancelAction - do not .end() it here
+ p._runAction(cancelAction || this._whenToken(new Action(this)))
+ return
+ }
+ }
+ if (cancelAction) {
+ cancelAction.end()
+ }
+ this.__become(p)
}
_fulfill (x) {
@@ -153,7 +237,7 @@ export class Future extends Core {
return
}
- this.__become(new Rejected(e))
+ this.__become(reject(e))
}
_become (p) {
@@ -165,7 +249,10 @@ export class Future extends Core {
}
__become (p) {
- this.ref = p === this ? cycle() : p
+ // assert: isSettled(p) || isNever(p) || p.token === this.token
+ // assert: this.ref == null || this.ref === this
+ this.ref = p
+ this.token = null
if (this.action === void 0) {
return
@@ -175,14 +262,18 @@ export class Future extends Core {
}
run () {
+ /* eslint complexity:[2,6] */
const p = this.ref.near()
- p._runAction(this.action)
+ if (this.action.promise) p._runAction(this.action)
+ else if (isRejected(p)) silenceError(p)
this.action = void 0
for (let i = 0; i < this.length; ++i) {
- p._runAction(this[i])
+ if (this[i].promise) p._runAction(this[i])
+ else if (isRejected(p)) silenceError(p)
this[i] = void 0
}
+ this.length = 0
}
}
@@ -194,32 +285,40 @@ class Fulfilled extends Core {
this.value = x
}
- then (f) {
- return typeof f === 'function' ? then(f, void 0, this, new Future()) : this
+ then (f, _, token) {
+ return typeof f === 'function' ? then(f, void 0, this, new Future(token)) : cancelledIfRequested(token, this)
}
- catch () {
- return this
+ catch (_, token) {
+ return cancelledIfRequested(token, this)
}
- map (f) {
- return map(f, this, new Future())
+ map (f, token) {
+ return map(f, this, new Future(token))
}
- ap (p) {
- return p.map(this.value)
+ ap (p, token) {
+ return p.map(this.value, token)
}
- chain (f) {
- return chain(f, this, new Future())
+ chain (f, token) {
+ return chain(f, this, new Future(token))
}
- concat () {
+ concat (_) {
return this
}
- toString () {
- return '[object ' + this.inspect() + ']'
+ untilCancel (token) {
+ return cancelledIfRequested(token, this)
+ }
+
+ finally (f) {
+ return fin(f, this, new Future())
+ }
+
+ trifurcate (f, r, c) {
+ return typeof f === 'function' ? then(f, undefined, this, new Future()) : this
}
inspect () {
@@ -239,6 +338,7 @@ class Fulfilled extends Core {
}
_runAction (action) {
+ // assert: action.promise != null
action.fulfilled(this)
}
}
@@ -249,36 +349,43 @@ class Rejected extends Core {
constructor (e) {
super()
this.value = e
- this._state = REJECTED
- errorHandler.track(this)
+ this._state = REJECTED // mutated by the silencer
}
- then (_, r) {
- return typeof r === 'function' ? this.catch(r) : this
+ then (_, r, token) {
+ return typeof r === 'function' ? this.catch(r, token) : this._cancelledIfRequested(token)
}
- catch (r) {
- return then(void 0, r, this, new Future())
+ catch (r, token) {
+ return then(void 0, r, this, new Future(token))
}
- map () {
- return this
+ map (_, token) {
+ return this._cancelledIfRequested(token)
}
- ap () {
- return this
+ ap (_, token) {
+ return this._cancelledIfRequested(token)
}
- chain () {
- return this
+ chain (_, token) {
+ return this._cancelledIfRequested(token)
}
- concat () {
- return this
+ concat (_) {
+ return this._cancelledIfRequested(null)
}
- toString () {
- return '[object ' + this.inspect() + ']'
+ untilCancel (token) {
+ return this._cancelledIfRequested(token)
+ }
+
+ finally (f) {
+ return fin(f, this, new Future())
+ }
+
+ trifurcate (f, r, c) {
+ return typeof r === 'function' ? then(undefined, r, this, new Future()) : this
}
inspect () {
@@ -293,44 +400,108 @@ class Rejected extends Core {
return this
}
+ _cancelledIfRequested (token) {
+ return cancelledIfRequested(token, this)
+ }
+
_when (action) {
taskQueue.add(new Continuation(action, this))
}
_runAction (action) {
+ // assert: action.promise != null
if (action.rejected(this)) {
errorHandler.untrack(this)
}
}
}
+// Cancelled :: Error e => e -> Promise e a
+// A promise whose value was invalidated and cannot be known
+class Cancelled extends Rejected {
+ finally (f) {
+ return fin(f, this, this)
+ }
+
+ trifurcate (f, r, c) {
+ return trifurcate(undefined, undefined, c, this, new Future())
+ }
+
+ inspect () {
+ return 'Promise { cancelled: ' + this.value + ' }'
+ }
+
+ state () {
+ return REJECTED | CANCELLED | HANDLED
+ }
+
+ _cancelledIfRequested (token) {
+ // like cancelledIfRequested(token, this), but not quite
+ token = CancelToken.from(token)
+ return token != null && token.requested ? token.getCancelled() : reject(this.value)
+ }
+
+ _isResolved () {
+ // called by Final::cancel
+ return true
+ }
+
+ _runAction (action) {
+ // assert: action.promise != null
+ action.rejected(this)
+ }
+
+ _whenToken (action) {
+ // behaves as if there was a .token
+ action.cancel(null, this)
+ taskQueue.add(new Continuation(action, {
+ _runAction: action => {
+ action.cancelled(this)
+ }
+ }))
+ return action
+ }
+}
+
// Never :: Promise e a
// A promise that waits forever for its value to be known
class Never extends Core {
- then () {
- return this
+ then (_, __, token) {
+ return cancelledWhen(token, this)
}
- catch () {
- return this
+ catch (_, token) {
+ return cancelledWhen(token, this)
}
- map () {
- return this
+ map (_, token) {
+ return cancelledWhen(token, this)
}
- ap () {
- return this
+ ap (_, token) {
+ return cancelledWhen(token, this)
}
- chain () {
- return this
+ chain (_, token) {
+ return cancelledWhen(token, this)
}
concat (b) {
return b
}
+ untilCancel (token) {
+ return cancelledWhen(token, this)
+ }
+
+ finally (_) {
+ return this
+ }
+
+ trifurcate (f, r, c) {
+ return this
+ }
+
toString () {
return '[object ' + this.inspect() + ']'
}
@@ -354,21 +525,51 @@ class Never extends Core {
}
}
+const silencer = new Action(never())
+silencer.fulfilled = noop
+silencer.cancelled = noop
+silencer.rejected = function setHandled (p) {
+ p._state |= HANDLED
+}
+
+export function silenceError (p) {
+ p._runAction(silencer)
+}
+
// -------------------------------------------------------------
// ## Creating promises
// -------------------------------------------------------------
+// resolve :: Thenable e a -> CancelToken e -> Promise e a
// resolve :: Thenable e a -> Promise e a
// resolve :: a -> Promise e a
-export function resolve (x) {
- return isPromise(x) ? x.near()
- : maybeThenable(x) ? refForMaybeThenable(fulfill, x)
- : new Fulfilled(x)
+export function resolve (x, token) {
+ /* eslint complexity:[2,7] */
+ if (isPromise(x)) {
+ return x.untilCancel(token)
+ } else if (token != null && token.requested) {
+ return token.getCancelled()
+ } else if (isObject(x)) {
+ return refForMaybeThenable(x, token)
+ } else {
+ return new Fulfilled(x)
+ }
+}
+
+export function resolveObject (o) {
+ return isPromise(o) ? o.near() : refForMaybeThenable(o, null)
}
// reject :: e -> Promise e a
export function reject (e) {
- return new Rejected(e)
+ const r = new Rejected(e)
+ errorHandler.track(r)
+ return r
+}
+
+// cancel :: e -> Promise e a
+export function cancel (e) {
+ return new Cancelled(e)
}
// never :: Promise e a
@@ -383,43 +584,54 @@ export function fulfill (x) {
// future :: () -> { resolve: Resolve e a, promise: Promise e a }
// type Resolve e a = a|Thenable e a -> ()
-export function future () {
- const promise = new Future()
- return {resolve: x => promise._resolve(x), promise}
-}
-
-// -------------------------------------------------------------
-// ## Iterables
-// -------------------------------------------------------------
-
-// all :: Iterable (Promise e a) -> Promise e [a]
-export function all (promises) {
- const handler = new Merge(allHandler, resultsArray(promises))
- return iterablePromise(handler, promises)
-}
-
-const allHandler = {
- merge (promise, args) {
- promise._fulfill(args)
+export function future (token) {
+ const promise = new Future(token)
+ if (promise.token == null) {
+ return {
+ promise,
+ resolve (x) { promise._resolve(x) }
+ }
+ }
+ let put = new Action(promise)
+ promise.token._subscribe(put)
+ return {
+ promise,
+ resolve (x) {
+ if (put == null) return
+ promise._resolve(x, put)
+ put = null
+ }
}
}
-// race :: Iterable (Promise e a) -> Promise e a
-export function race (promises) {
- return iterablePromise(new Race(never), promises)
-}
-
-function isIterable (x) {
- return typeof x === 'object' && x !== null
-}
-
-export function iterablePromise (handler, iterable) {
- if (!isIterable(iterable)) {
- return reject(new TypeError('expected an iterable'))
+// makeResolvers :: Promise e a -> { resolve: Resolve e a, reject: e -> () }
+export function makeResolvers (promise) {
+ if (promise.token != null) {
+ let put = new Action(promise)
+ promise.token._subscribe(put)
+ return {
+ resolve (x) {
+ if (put == null || put.promise == null) return
+ promise._resolve(x, put)
+ put = promise = null
+ },
+ reject (e) {
+ if (put == null || put.promise == null) return
+ promise._reject(e)
+ put.end()
+ put = promise = null
+ }
+ }
+ } else {
+ return {
+ resolve (x) {
+ promise._resolve(x)
+ },
+ reject (e) {
+ promise._reject(e)
+ }
+ }
}
-
- const p = new Future()
- return resolveIterable(resolveMaybeThenable, handler, iterable, p)
}
// -------------------------------------------------------------
@@ -431,27 +643,32 @@ function isPromise (x) {
return x instanceof Core
}
-function resolveMaybeThenable (x) {
- return isPromise(x) ? x.near() : refForMaybeThenable(fulfill, x)
+function cancelledIfRequested (token, settled) {
+ token = CancelToken.from(token)
+ return token != null && token.requested ? token.getCancelled() : settled
+}
+
+function cancelledWhen (token, never) {
+ if (token == null) return never
+ return CancelToken.from(token).getCancelled()
}
-function refForMaybeThenable (otherwise, x) {
+function refForMaybeThenable (x, token) {
try {
const then = x.then
return typeof then === 'function'
- ? extractThenable(then, x)
- : otherwise(x)
+ ? extractThenable(then, x, new Future(token))
+ : fulfill(x)
} catch (e) {
- return new Rejected(e)
+ return reject(e)
}
}
// WARNING: Naming the first arg "then" triggers babel compilation bug
-function extractThenable (thn, thenable) {
- const p = new Future()
-
+function extractThenable (thn, thenable, p) {
+ const { resolve, reject } = makeResolvers(p)
try {
- thn.call(thenable, x => p._resolve(x), e => p._reject(e))
+ thn.call(thenable, resolve, reject, p.token)
} catch (e) {
p._reject(e)
}
@@ -460,16 +677,5 @@ function extractThenable (thn, thenable) {
}
function cycle () {
- return new Rejected(new TypeError('resolution cycle'))
-}
-
-class Continuation {
- constructor (action, promise) {
- this.action = action
- this.promise = promise
- }
-
- run () {
- this.promise._runAction(this.action)
- }
+ return reject(new TypeError('resolution cycle'))
}
diff --git a/src/Race.js b/src/Race.js
index 9718298..846921f 100644
--- a/src/Race.js
+++ b/src/Race.js
@@ -1,23 +1,26 @@
-export default class Race {
- constructor (never) {
- this.never = never
- }
+import { never } from './Promise' // deferred
+import CancelReason from './CancelReason'
+export default class Race {
valueAt (x, i, promise) {
promise._fulfill(x)
}
fulfillAt (p, i, promise) {
+ const token = promise.token
promise._become(p)
+ token._cancel(new CancelReason('result is already fulfilled'))
}
rejectAt (p, i, promise) {
+ const token = promise.token
promise._become(p)
+ token._cancel(new CancelReason('result is already rejected', p.value))
}
complete (total, promise) {
if (total === 0) {
- promise._become(this.never())
+ promise._become(never())
}
}
}
diff --git a/src/Settle.js b/src/Settle.js
index 789c3d6..e2aa930 100644
--- a/src/Settle.js
+++ b/src/Settle.js
@@ -1,14 +1,13 @@
-import { silenceError } from './inspect'
+import { fulfill, silenceError } from './Promise' // deferred
export default class Settle {
- constructor (resolve, results) {
+ constructor (results) {
this.pending = 0
this.results = results
- this.resolve = resolve
}
valueAt (x, i, promise) {
- this.settleAt(this.resolve(x), i, promise)
+ this.settleAt(fulfill(x), i, promise)
}
fulfillAt (p, i, promise) {
diff --git a/src/TaskQueue.js b/src/TaskQueue.js
index 14dd40e..6f84e1a 100644
--- a/src/TaskQueue.js
+++ b/src/TaskQueue.js
@@ -1,6 +1,6 @@
import makeAsync from './async'
-export default class TaskQueue {
+export class TaskQueue {
constructor () {
this.tasks = new Array(2 << 15)
this.length = 0
@@ -24,3 +24,15 @@ export default class TaskQueue {
this.length = 0
}
}
+
+// make an Action runnable on a Promise
+export class Continuation {
+ constructor (action, promise) {
+ this.action = action
+ this.promise = promise
+ }
+
+ run () {
+ if (this.action.promise) this.promise._runAction(this.action)
+ }
+}
diff --git a/src/async.js b/src/async.js
index edc4c2b..4aa4b51 100644
--- a/src/async.js
+++ b/src/async.js
@@ -2,11 +2,11 @@ import { isNode, MutationObs } from './env'
/* global process,document */
-export default function (f) {
- return isNode ? createNodeScheduler(f) /* istanbul ignore next */
- : MutationObs ? createBrowserScheduler(f)
- : createFallbackScheduler(f)
-}
+const createScheduler = isNode ? createNodeScheduler /* istanbul ignore next */
+ : MutationObs ? createBrowserScheduler
+ : createFallbackScheduler
+
+export { createScheduler as default }
/* istanbul ignore next */
function createFallbackScheduler (f) {
diff --git a/src/chain.js b/src/chain.js
index 9f48b5a..07f6dff 100644
--- a/src/chain.js
+++ b/src/chain.js
@@ -1,34 +1,20 @@
-import maybeThenable from './maybeThenable'
+import { isObject } from './util'
+import { CancellableAction } from './Action'
-export default function (f, p, promise) {
- p._when(new Chain(f, promise))
+export default function chain (f, p, promise) {
+ if (promise.token != null && promise.token.requested) {
+ return promise.token.getCancelled()
+ }
+ p._when(promise._whenToken(new Chain(f, promise)))
return promise
}
-class Chain {
- constructor (f, promise) {
- this.f = f
- this.promise = promise
- }
-
- fulfilled (p) {
- try {
- runChain(this.f, p.value, this.promise)
- } catch (e) {
- this.promise._reject(e)
+class Chain extends CancellableAction {
+ handle (y) {
+ if (!(isObject(y) && typeof y.then === 'function')) {
+ this.end()._reject(new TypeError('f must return a promise'))
+ } else {
+ this.promise._resolve(y, this)
}
}
-
- rejected (p) {
- this.promise._become(p)
- }
-}
-
-function runChain (f, x, p) {
- const y = f(x)
- if (!(maybeThenable(y) && typeof y.then === 'function')) {
- throw new TypeError('f must return a promise')
- }
-
- p._resolve(y)
}
diff --git a/src/combinators.js b/src/combinators.js
new file mode 100644
index 0000000..556a639
--- /dev/null
+++ b/src/combinators.js
@@ -0,0 +1,76 @@
+import { taskQueue } from './Promise' // deferred
+import { iterablePromise, resultsArray } from './iterable'
+import Race from './Race'
+import Any from './Any'
+import Merge from './Merge'
+import Settle from './Settle'
+
+// -------------------------------------------------------------
+// ## Iterables
+// -------------------------------------------------------------
+
+// all :: Iterable (Promise e a) -> Promise e [a]
+export function all (promises) {
+ const handler = new Merge(allHandler, resultsArray(promises))
+ return iterablePromise(handler, promises)
+}
+
+const allHandler = {
+ merge (promise, args) {
+ promise._fulfill(args)
+ }
+}
+
+// race :: Iterable (Promise e a) -> Promise e a
+export function race (promises) {
+ return iterablePromise(new Race(), promises)
+}
+
+// any :: Iterable (Promise e a) -> Promise e a
+export function any (promises) {
+ return iterablePromise(new Any(), promises)
+}
+
+// settle :: Iterable (Promise e a) -> Promise e [Promise e a]
+export function settle (promises) {
+ const handler = new Settle(resultsArray(promises))
+ return iterablePromise(handler, promises)
+}
+
+// -------------------------------------------------------------
+// ## Lifting
+// -------------------------------------------------------------
+
+// merge :: (...* -> b) -> ...Promise e a -> Promise e b
+export function merge (f, ...args) {
+ return runMerge(f, this, args)
+}
+
+function runMerge (f, thisArg, args) {
+ const handler = new Merge(new MergeHandler(f, thisArg), resultsArray(args))
+ return iterablePromise(handler, args)
+}
+
+class MergeHandler {
+ constructor (f, c) {
+ this.f = f
+ this.c = c
+ this.promise = void 0
+ this.args = void 0
+ }
+
+ merge (promise, args) {
+ this.promise = promise
+ this.args = args
+ taskQueue.add(this)
+ }
+
+ run () {
+ try {
+ // assert: this.promise.token == null
+ this.promise._resolve(this.f.apply(this.c, this.args))
+ } catch (e) {
+ this.promise._reject(e)
+ }
+ }
+}
diff --git a/src/coroutine.js b/src/coroutine.js
index cee3dc4..55b5130 100644
--- a/src/coroutine.js
+++ b/src/coroutine.js
@@ -1,41 +1,111 @@
-export default function (resolve, iterator, promise) {
- new Coroutine(resolve, iterator, promise).run()
+import { Future, resolve, reject } from './Promise'
+import CancelToken from './CancelToken'
+import Action from './Action'
+
+// -------------------------------------------------------------
+// ## Coroutine
+// -------------------------------------------------------------
+
+// coroutine :: Generator e a -> (...* -> Promise e a)
+// Make a coroutine from a promise-yielding generator
+export default function coroutine (generatorFunction) {
+ return function coroutinified () {
+ return runGenerator(generatorFunction.apply(this, arguments))
+ }
+}
+
+const stack = []
+Object.defineProperty(coroutine, 'cancel', {
+ get () {
+ if (!stack.length) throw new SyntaxError('coroutine.cancel is only available inside a coroutine')
+ return stack[stack.length - 1].getToken()
+ },
+ set (token) {
+ if (!stack.length) throw new SyntaxError('coroutine.cancel is only available inside a coroutine')
+ token = CancelToken.from(token)
+ stack[stack.length - 1].setToken(token)
+ },
+ configurable: true
+})
+
+function runGenerator (generator) {
+ const swappable = CancelToken.reference(null)
+ const promise = new Future(swappable.get())
+ promise._whenToken(new Coroutine(generator, promise, swappable)).run()
+ // taskQueue.add(new Coroutine(generator, promise, swappable))
return promise
}
-class Coroutine {
- constructor (resolve, iterator, promise) {
- this.resolve = resolve
- this.iterator = iterator
- this.promise = promise
+class Coroutine extends Action {
+ constructor (generator, promise, ref) {
+ super(promise)
+ // the generator that is driven. Empty after cancellation
+ this.generator = generator
+ // a CancelTokenReference
+ this.tokenref = ref
}
run () {
- this.step(this.iterator.next, void 0)
+ this.step(this.generator.next, void 0)
}
- step (continuation, x) {
- try {
- this.handle(continuation.call(this.iterator, x))
- } catch (e) {
- this.promise._reject(e)
- }
+ fulfilled (ref) {
+ if (this.generator == null) return
+ this.step(this.generator.next, ref.value)
+ }
+
+ rejected (ref) {
+ if (this.generator == null) return false
+ this.step(this.generator.throw, ref.value)
+ return true
}
- handle (result) {
- if (result.done) {
- return this.promise._resolve(result.value)
+ cancel (results) {
+ /* istanbul ignore else */
+ if (this.promise._isResolved()) { // promise checks for cancellation itself
+ const res = new Future()
+ this.promise = new Coroutine(this.generator, res, null) // not cancellable
+ this.generator = null
+ this.tokenref = null
+ if (results) results.push(res)
}
+ }
- this.resolve(result.value)._runAction(this)
+ cancelled (p) {
+ const cancelRoutine = this.promise
+ this.promise = null
+ const reason = p.near().value
+ cancelRoutine.step(cancelRoutine.generator.return, reason)
}
- fulfilled (ref) {
- this.step(this.iterator.next, ref.value)
+ step (f, x) {
+ /* eslint complexity:[2,5] */
+ let result
+ stack.push(this)
+ try {
+ result = f.call(this.generator, x)
+ } catch (e) {
+ result = {value: reject(e), done: true}
+ } finally {
+ stack.pop() // assert: === this
+ }
+ if (this.generator) { // not cancelled during execution
+ const res = resolve(result.value, this.promise.token) // TODO optimise token?
+ if (result.done) {
+ this.put(res)
+ } else {
+ res._runAction(this)
+ }
+ }
}
- rejected (ref) {
- this.step(this.iterator.throw, ref.value)
- return true
+ setToken (t) {
+ if (this.tokenref == null) throw new ReferenceError('coroutine.cancel is only available until cancellation')
+ this.tokenref.set(t)
+ }
+
+ getToken () {
+ if (this.tokenref == null) throw new ReferenceError('coroutine.cancel is only available until cancellation')
+ return this.tokenref.get()
}
}
diff --git a/src/delay.js b/src/delay.js
index fdb5827..f41be19 100644
--- a/src/delay.js
+++ b/src/delay.js
@@ -1,24 +1,33 @@
-export default function (ms, p, promise) {
- p._runAction(new Delay(ms, promise))
+import Action from './Action'
+
+export default function delay (ms, p, promise) {
+ p._runAction(promise._whenToken(new Delay(ms, promise)))
return promise
}
-class Delay {
+class Delay extends Action {
constructor (time, promise) {
+ super(promise)
this.time = time
- this.promise = promise
+ this.id = null
}
- fulfilled (p) {
- /*global setTimeout*/
- setTimeout(become, this.time, p, this.promise)
+ destroy () {
+ super.destroy()
+ this.time = 0
+ if (this.id) {
+ /* global clearTimeout */
+ clearTimeout(this.id)
+ this.id = null
+ }
}
- rejected (p) {
- this.promise._become(p)
+ fulfilled (p) {
+ /* global setTimeout */
+ this.id = setTimeout(put, this.time, p, this)
}
}
-function become (p, promise) {
- promise._become(p)
+function put (p, action) {
+ action.put(p)
}
diff --git a/src/emitError.js b/src/emitError.js
index 9542313..a23c758 100644
--- a/src/emitError.js
+++ b/src/emitError.js
@@ -1,47 +1,44 @@
import { isNode } from './env'
+import { noop } from './util'
+import { UNHANDLED_REJECTION } from './ErrorHandler'
-const UNHANDLED_REJECTION = 'unhandledRejection'
-
-export default function () {
- /*global process, self, CustomEvent*/
- // istanbul ignore else */
- if (isNode && typeof process.emit === 'function') {
- // Returning falsy here means to call the default reportRejection API.
- // This is safe even in browserify since process.emit always returns
- // falsy in browserify:
- // https://github.com/defunctzombie/node-process/blob/master/browser.js#L40-L46
- return function (type, error) {
- return type === UNHANDLED_REJECTION
- ? process.emit(type, error.value, error)
- : process.emit(type, error)
+let emitError
+/*global process, self, CustomEvent*/
+// istanbul ignore else */
+if (isNode && typeof process.emit === 'function') {
+ // Returning falsy here means to call the default reportRejection API.
+ // This is safe even in browserify since process.emit always returns
+ // falsy in browserify:
+ // https://github.com/defunctzombie/node-process/blob/master/browser.js#L40-L46
+ emitError = function emit (type, error) {
+ return type === UNHANDLED_REJECTION
+ ? process.emit(type, error.value, error)
+ : process.emit(type, error)
+ }
+} else if (typeof self !== 'undefined' && typeof CustomEvent === 'function') {
+ emitError = (function (self, CustomEvent) {
+ try {
+ let usable = new CustomEvent(UNHANDLED_REJECTION) instanceof CustomEvent
+ if (!usable) return noop
+ } catch (e) {
+ return noop
}
- } else if (typeof self !== 'undefined' && typeof CustomEvent === 'function') {
- return (function (noop, self, CustomEvent) {
- var hasCustomEvent
- try {
- hasCustomEvent = new CustomEvent(UNHANDLED_REJECTION) instanceof CustomEvent
- } catch (e) {
- hasCustomEvent = false
- }
- return !hasCustomEvent ? noop : function (type, error) {
- const ev = new CustomEvent(type, {
- detail: {
- reason: error.value,
- promise: error
- },
- bubbles: false,
- cancelable: true
- })
+ return function emit (type, error) {
+ const ev = new CustomEvent(type, {
+ detail: {
+ reason: error.value,
+ promise: error
+ },
+ bubbles: false,
+ cancelable: true
+ })
- return !self.dispatchEvent(ev)
- }
- }(noop, self, CustomEvent))
- }
-
- // istanbul ignore next */
- return noop
+ return !self.dispatchEvent(ev)
+ }
+ }(self, CustomEvent))
+} else {
+ emitError = noop
}
-// istanbul ignore next */
-function noop () {}
+export default emitError
diff --git a/src/finally.js b/src/finally.js
new file mode 100644
index 0000000..d81c71c
--- /dev/null
+++ b/src/finally.js
@@ -0,0 +1,68 @@
+import { resolve, Future } from './Promise'
+// import { isFulfilled, isRejected } from './inspect'
+import { CancellableAction } from './Action'
+
+export default function _finally (f, p, promise) {
+ // assert: promise.token == p.token
+ if (typeof f !== 'function') throw new TypeError('finally does require a callback function')
+ p._when(p._whenToken(new Final(f, promise)))
+ return promise
+}
+
+class Final extends CancellableAction {
+ destroy () {
+ this.promise = null
+ // don't destroy f
+ }
+
+ cancel (results) {
+ super.cancel(null) // cancel the final promise
+ if (typeof this.f === 'function') { // yet to be run or currently running
+ // assert: this.f === sentinel || this.promise = null
+ this.promise = new Future() // create new promise for the cancel result
+ if (results) results.push(this.promise)
+ } else { // f already ran, .f holds the original now, .promise was the final promise
+ // do anything to the f() result?
+ }
+ }
+
+ fulfilled (p) {
+ this.settled(p, this.f)
+ }
+
+ rejected (p) {
+ return this.settled(p, null)
+ }
+
+ cancelled (p) {
+ this.runFin(p.near(), null)
+ }
+
+ settled (p, orig) {
+ if (typeof this.f === 'function') { // f is the callback
+ this.runFin(p, p)
+ return true
+ } else { // f held the original result
+ this.put(orig == null ? p : orig)
+ this.f = null
+ return false
+ }
+ }
+
+ runFin (p, orig) {
+ const res = this.tryCall(this.f, p)
+ if (res !== undefined) { // f returned a promise to wait for
+ // if (isFulfilled(res)) return this.put(p)
+ // if (isRejected(res)) return this.put(res)
+ this.f = orig // reuse property to store eventual result
+ res._runAction(this)
+ } else if (this.promise) { // f returned nothing and didn't throw
+ this.put(p)
+ }
+ }
+
+ handle (result) {
+ if (result == null) return
+ return resolve(result)
+ }
+}
diff --git a/src/inspect.js b/src/inspect.js
index 951de89..f6c38b1 100644
--- a/src/inspect.js
+++ b/src/inspect.js
@@ -1,4 +1,5 @@
-import { PENDING, FULFILLED, REJECTED, SETTLED, NEVER, HANDLED } from './state'
+import { PENDING, FULFILLED, REJECTED, CANCELLED, SETTLED, NEVER, HANDLED } from './state'
+import { silenceError } from './Promise' // deferred
export function isPending (p) {
return (p.state() & PENDING) > 0
@@ -12,6 +13,10 @@ export function isRejected (p) {
return (p.state() & REJECTED) > 0
}
+export function isCancelled (p) {
+ return (p.state() & CANCELLED) > 0
+}
+
export function isSettled (p) {
return (p.state() & SETTLED) > 0
}
@@ -42,16 +47,3 @@ export function getReason (p) {
silenceError(n)
return n.value
}
-
-export function silenceError (p) {
- p._runAction(silencer)
-}
-
-const silencer = {
- fulfilled () {},
- rejected: setHandled
-}
-
-function setHandled (rejected) {
- rejected._state |= HANDLED
-}
diff --git a/src/iterable.js b/src/iterable.js
index d3eeec2..da038ab 100644
--- a/src/iterable.js
+++ b/src/iterable.js
@@ -1,31 +1,47 @@
-import { isFulfilled, isRejected, silenceError } from './inspect'
-import maybeThenable from './maybeThenable'
+import { isObject, noop } from './util'
+import { Future, reject, resolveObject, silenceError } from './Promise' // deferred
+import { isFulfilled, isRejected } from './inspect'
+import CancelToken from './CancelToken'
+import Action from './Action'
+
+function isIterable (x) {
+ return typeof x === 'object' && x !== null
+}
+
+export function iterablePromise (handler, iterable) {
+ if (!isIterable(iterable)) {
+ return reject(new TypeError('expected an iterable'))
+ }
+
+ const p = new Future(new CancelToken(noop))
+ return resolveIterable(handler, iterable, p)
+}
export function resultsArray (iterable) {
return Array.isArray(iterable) ? new Array(iterable.length) : []
}
-export function resolveIterable (resolve, handler, promises, promise) {
+export function resolveIterable (handler, promises, promise) {
const run = Array.isArray(promises) ? runArray : runIterable
try {
- run(resolve, handler, promises, promise)
+ run(handler, promises, promise)
} catch (e) {
promise._reject(e)
}
return promise.near()
}
-function runArray (resolve, handler, promises, promise) {
+function runArray (handler, promises, promise) {
let i = 0
for (; i < promises.length; ++i) {
- handleItem(resolve, handler, promises[i], i, promise)
+ handleItem(handler, promises[i], i, promise)
}
handler.complete(i, promise)
}
-function runIterable (resolve, handler, promises, promise) {
+function runIterable (handler, promises, promise) {
let i = 0
const iter = promises[Symbol.iterator]()
@@ -34,20 +50,20 @@ function runIterable (resolve, handler, promises, promise) {
if (step.done) {
break
}
- handleItem(resolve, handler, step.value, i++, promise)
+ handleItem(handler, step.value, i++, promise)
}
handler.complete(i, promise)
}
-function handleItem (resolve, handler, x, i, promise) {
+function handleItem (handler, x, i, promise) {
/*eslint complexity:[1,6]*/
- if (!maybeThenable(x)) {
+ if (!isObject(x)) {
handler.valueAt(x, i, promise)
return
}
- const p = resolve(x)
+ const p = resolveObject(x)
if (promise._isResolved()) {
if (!isFulfilled(p)) {
@@ -58,18 +74,27 @@ function handleItem (resolve, handler, x, i, promise) {
} else if (isRejected(p)) {
handler.rejectAt(p, i, promise)
} else {
- settleAt(p, handler, i, promise)
+ p._runAction(promise._whenToken(new Indexed(handler, i, promise)))
}
}
-function settleAt (p, handler, i, promise) {
- p._runAction({handler, i, promise, fulfilled, rejected})
-}
+class Indexed extends Action {
+ constructor (handler, i, promise) {
+ super(promise)
+ this.i = i
+ this.handler = handler
+ }
-function fulfilled (p) {
- this.handler.fulfillAt(p, this.i, this.promise)
-}
+ destroy () {
+ super.destroy()
+ this.handler = null
+ }
-function rejected (p) {
- return this.handler.rejectAt(p, this.i, this.promise)
+ fulfilled (p) {
+ this.handler.fulfillAt(p, this.i, this.promise)
+ }
+
+ rejected (p) {
+ return this.handler.rejectAt(p, this.i, this.promise)
+ }
}
diff --git a/src/main.js b/src/main.js
index afee964..1e901cf 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,44 +1,24 @@
-import { isFulfilled, isRejected, isSettled, isPending, isNever, getValue, getReason } from './inspect'
-import { Future, resolve, reject, future, never, fulfill, all, race, iterablePromise, taskQueue } from './Promise'
-
-import _delay from './delay'
-import _timeout from './timeout'
-
-import Any from './Any'
-import Merge from './Merge'
-import Settle from './Settle'
-import { resultsArray } from './iterable'
-
-import _runPromise from './runPromise'
-import _runNode from './node'
-import _runCoroutine from './coroutine.js'
-
// -------------------------------------------------------------
// ## Core promise methods
// -------------------------------------------------------------
-export {
- resolve, reject, future, never, fulfill, all, race,
- isFulfilled, isRejected, isSettled, isPending, isNever,
- getValue, getReason
-}
+/* eslint-disable no-duplicate-imports */
+export { resolve, reject, future, never, fulfill } from './Promise'
+import { Future, resolve, reject, makeResolvers } from './Promise'
+export { isFulfilled, isRejected, isCancelled, isSettled, isPending, isNever, getValue, getReason } from './inspect'
+import { isRejected, isSettled, isNever } from './inspect'
+export { all, race, any, settle, merge } from './combinators'
+import { all, race } from './combinators'
-// -------------------------------------------------------------
-// ## Coroutine
-// -------------------------------------------------------------
+export { default as CancelToken } from './CancelToken'
-// coroutine :: Generator e a -> (...* -> Promise e a)
-// Make a coroutine from a promise-yielding generator
-export function coroutine (generator) {
- return function (...args) {
- return runGenerator(generator, this, args)
- }
-}
+export { default as coroutine } from './coroutine.js'
-function runGenerator (generator, thisArg, args) {
- const iterator = generator.apply(thisArg, args)
- return _runCoroutine(resolve, iterator, new Future())
-}
+import _delay from './delay'
+import _timeout from './timeout'
+
+import _runPromise from './runPromise'
+import _runNode from './node'
// -------------------------------------------------------------
// ## Node-style async
@@ -50,17 +30,30 @@ function runGenerator (generator, thisArg, args) {
// fromNode :: NodeApi e a -> (...args -> Promise e a)
// Turn a Node API into a promise API
export function fromNode (f) {
- return function (...args) {
- return runResolver(_runNode, f, this, args, new Future())
+ checkFunction(f)
+ return function promisified (...args) {
+ return runNodeFunction(f, this, args)
}
}
// runNode :: NodeApi e a -> ...* -> Promise e a
// Run a Node API, returning a promise for the outcome
export function runNode (f, ...args) {
- return runResolver(_runNode, f, this, args, new Future())
+ checkFunction(f)
+ return runNodeFunction(f, this, args)
}
+function runNodeFunction (f, thisArg, args) {
+ const p = new Future()
+
+ try {
+ _runNode(f, thisArg, args, p)
+ } catch (e) {
+ p._reject(e)
+ }
+
+ return p
+}
// -------------------------------------------------------------
// ## Make a promise
// -------------------------------------------------------------
@@ -70,31 +63,40 @@ export function runNode (f, ...args) {
// type Producer e a = (...* -> Resolve e a -> Reject e -> ())
// runPromise :: Producer e a -> ...* -> Promise e a
export function runPromise (f, ...args) {
- return runResolver(_runPromise, f, this, args, new Future())
+ checkFunction(f)
+ return runResolver(f, this, args, new Future())
}
-function runResolver (run, f, thisArg, args, p) {
- checkFunction(f)
+function runResolver (f, thisArg, args, p) {
+ const resolvers = makeResolvers(p)
try {
- run(f, thisArg, args, p)
+ _runPromise(f, thisArg, args, resolvers)
} catch (e) {
- p._reject(e)
+ resolvers.reject(e)
}
return p
}
+function checkFunction (f) {
+ if (typeof f !== 'function') {
+ throw new TypeError('must provide a resolver function')
+ }
+}
+
// -------------------------------------------------------------
// ## Time
// -------------------------------------------------------------
// delay :: number -> Promise e a -> Promise e a
-export function delay (ms, x) {
- /* eslint complexity:[2,4] */
+export function delay (ms, x, token) {
+ /* eslint complexity:[2,5] */
+ if (token != null && token.requested) return token.getCancelled()
const p = resolve(x)
- return ms <= 0 || isRejected(p) || isNever(p) ? p
- : _delay(ms, p, new Future())
+ if (ms <= 0) return p
+ if (token == null && (isRejected(p) || isNever(p))) return p
+ return _delay(ms, p, new Future(token))
}
// timeout :: number -> Promise e a -> Promise (e|TimeoutError) a
@@ -103,64 +105,6 @@ export function timeout (ms, x) {
return isSettled(p) ? p : _timeout(ms, p, new Future())
}
-// -------------------------------------------------------------
-// ## Iterables
-// -------------------------------------------------------------
-
-// any :: Iterable (Promise e a) -> Promise e a
-export function any (promises) {
- return iterablePromise(new Any(), promises)
-}
-
-// settle :: Iterable (Promise e a) -> Promise e [Promise e a]
-export function settle (promises) {
- const handler = new Settle(resolve, resultsArray(promises))
- return iterablePromise(handler, promises)
-}
-
-// -------------------------------------------------------------
-// ## Lifting
-// -------------------------------------------------------------
-
-// merge :: (...* -> b) -> ...Promise e a -> Promise e b
-export function merge (f, ...args) {
- return runMerge(f, this, args)
-}
-
-function runMerge (f, thisArg, args) {
- const handler = new Merge(new MergeHandler(f, thisArg), resultsArray(args))
- return iterablePromise(handler, args)
-}
-
-class MergeHandler {
- constructor (f, c) {
- this.f = f
- this.c = c
- this.promise = void 0
- this.args = void 0
- }
-
- merge (promise, args) {
- this.promise = promise
- this.args = args
- taskQueue.add(this)
- }
-
- run () {
- try {
- this.promise._resolve(this.f.apply(this.c, this.args))
- } catch (e) {
- this.promise._reject(e)
- }
- }
-}
-
-function checkFunction (f) {
- if (typeof f !== 'function') {
- throw new TypeError('must provide a resolver function')
- }
-}
-
// -------------------------------------------------------------
// ## ES6 Promise polyfill
// -------------------------------------------------------------
@@ -169,11 +113,13 @@ const NOARGS = []
// type Resolve a = a -> ()
// type Reject e = e -> ()
-// Promise :: (Resolve a -> Reject e) -> Promise e a
+// Promise :: (Resolve a -> Reject e -> ()) -> Promise e a
class CreedPromise extends Future {
- constructor (f) {
- super()
- runResolver(_runPromise, f, void 0, NOARGS, this)
+ constructor (f, token) {
+ super(token)
+ if (!this._isResolved()) { // test for cancellation
+ runResolver(f, void 0, NOARGS, this)
+ }
}
}
diff --git a/src/map.js b/src/map.js
index e0469b5..0fd8e17 100644
--- a/src/map.js
+++ b/src/map.js
@@ -1,24 +1,15 @@
-export default function (f, p, promise) {
- p._when(new Map(f, promise))
- return promise
-}
-
-class Map {
- constructor (f, promise) {
- this.f = f
- this.promise = promise
- }
+import { CancellableAction } from './Action'
- fulfilled (p) {
- try {
- const f = this.f
- this.promise._fulfill(f(p.value))
- } catch (e) {
- this.promise._reject(e)
- }
+export default function map (f, p, promise) {
+ if (promise.token != null && promise.token.requested) {
+ return promise.token.getCancelled()
}
+ p._when(promise._whenToken(new Map(f, promise)))
+ return promise
+}
- rejected (p) {
- this.promise._become(p)
+class Map extends CancellableAction {
+ handle (result) {
+ this.promise._fulfill(result)
}
}
diff --git a/src/maybeThenable.js b/src/maybeThenable.js
deleted file mode 100644
index 396d8e4..0000000
--- a/src/maybeThenable.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// maybeThenable :: * -> boolean
-export default function maybeThenable (x) {
- return (typeof x === 'object' || typeof x === 'function') && x !== null
-}
diff --git a/src/runPromise.js b/src/runPromise.js
index b6b966c..7fae73a 100644
--- a/src/runPromise.js
+++ b/src/runPromise.js
@@ -1,12 +1,6 @@
-export default function runPromise (f, thisArg, args, promise) {
+export default function runPromise (f, thisArg, args, resolvers) {
/* eslint complexity:[2,5] */
- function resolve (x) {
- promise._resolve(x)
- }
-
- function reject (e) {
- promise._reject(e)
- }
+ const { resolve, reject } = resolvers
switch (args.length) {
case 0:
@@ -25,6 +19,4 @@ export default function runPromise (f, thisArg, args, promise) {
args.push(resolve, reject)
f.apply(thisArg, args)
}
-
- return promise
}
diff --git a/src/state.js b/src/state.js
index b932693..c84bfef 100644
--- a/src/state.js
+++ b/src/state.js
@@ -4,5 +4,6 @@ export const FULFILLED = 1 << 1
export const REJECTED = 1 << 2
export const SETTLED = FULFILLED | REJECTED
export const NEVER = 1 << 3
+export const CANCELLED = 1 << 4
-export const HANDLED = 1 << 4
+export const HANDLED = 1 << 5
diff --git a/src/subscribe.js b/src/subscribe.js
new file mode 100644
index 0000000..d70ee70
--- /dev/null
+++ b/src/subscribe.js
@@ -0,0 +1,44 @@
+import { noop } from './util'
+import { CancellableAction } from './Action'
+
+export function subscribe (f, t, promise) {
+ if (promise.token != null && promise.token.requested) {
+ return promise.token.getCancelled()
+ }
+ t._subscribe(new Subscription(f, promise))
+ return promise
+}
+
+export function subscribeOrCall (f, g, t, promise) {
+ let sub = new Subscription(f, promise)
+ t._subscribe(sub)
+ return function call () {
+ // TODO: should `g` run despite `t.requested`,
+ // or should none run immediately despite `call` having been called?
+ if (sub != null && sub.f != null && sub.f !== noop) { // noop is the "currently running" sentinel
+ t._unsubscribe(sub)
+ t = sub = null
+ if (typeof g === 'function') {
+ return g.apply(this, arguments)
+ }
+ }
+ }
+}
+
+class Subscription extends CancellableAction {
+ cancel (results) {
+ /* eslint complexity:[2,4] */
+ const promise = this.promise
+ if (promise.token != null) { // possibly called from promise.token
+ if (super.cancel()) { // if promise is cancelled
+ return
+ }
+ }
+ // otherwise called from standalone token
+ if (results) results.push(this.promise)
+ }
+
+ cancelled (p) {
+ this.tryCall(this.f, p.near().value)
+ }
+}
diff --git a/src/then.js b/src/then.js
index 435fd54..d65bc3c 100644
--- a/src/then.js
+++ b/src/then.js
@@ -1,38 +1,40 @@
+import { CancellableAction } from './Action'
+
export default function then (f, r, p, promise) {
- p._when(new Then(f, r, promise))
+ if (promise.token != null && promise.token.requested) {
+ return promise.token.getCancelled()
+ }
+ p._when(promise._whenToken(new Then(f, r, promise)))
return promise
}
-class Then {
+class Then extends CancellableAction {
constructor (f, r, promise) {
- this.f = f
+ super(f, promise)
this.r = r
- this.promise = promise
}
- fulfilled (p) {
- runThen(this.f, p, this.promise)
+ destroy () {
+ super.destroy()
+ this.r = null
}
- rejected (p) {
- return runThen(this.r, p, this.promise)
+ fulfilled (p) {
+ this.runThen(this.f, p)
}
-}
-function runThen (f, p, promise) {
- if (typeof f !== 'function') {
- promise._become(p)
- return false
+ rejected (p) {
+ return this.runThen(this.r, p)
}
- tryMapNext(f, p.value, promise)
- return true
-}
-
-function tryMapNext (f, x, promise) {
- try {
- promise._resolve(f(x))
- } catch (e) {
- promise._reject(e)
+ runThen (f, p) {
+ const hasHandler = (this.f != null || this.r != null) && typeof f === 'function'
+ if (hasHandler) {
+ this.r = null
+ this.tryCall(f, p.value)
+ } else {
+ this.put(p)
+ }
+ return hasHandler
}
}
diff --git a/src/timeout.js b/src/timeout.js
index adce7de..b8a05bc 100644
--- a/src/timeout.js
+++ b/src/timeout.js
@@ -1,15 +1,16 @@
+import Action from './Action'
import TimeoutError from './TimeoutError'
-export default function (ms, p, promise) {
+export default function timeout (ms, p, promise) {
const timer = setTimeout(rejectOnTimeout, ms, promise)
- p._runAction(new Timeout(timer, promise))
+ p._runAction(promise._whenToken(new Timeout(timer, promise)))
return promise
}
-class Timeout {
+class Timeout extends Action {
constructor (timer, promise) {
+ super(promise)
this.timer = timer
- this.promise = promise
}
fulfilled (p) {
@@ -19,8 +20,7 @@ class Timeout {
rejected (p) {
clearTimeout(this.timer)
- this.promise._become(p)
- return false
+ return super.rejected(p)
}
}
diff --git a/src/trifurcate.js b/src/trifurcate.js
new file mode 100644
index 0000000..764462c
--- /dev/null
+++ b/src/trifurcate.js
@@ -0,0 +1,55 @@
+import { CancellableAction } from './Action'
+
+export default function trifurcate (f, r, c, p, promise) {
+ // assert: promise.token == null
+ p._when(p._whenToken(new Trifurcation(f, r, c, promise)))
+ return promise
+}
+
+class Trifurcation extends CancellableAction {
+ constructor (f, r, c, promise) {
+ super(f, promise)
+ this.r = r
+ this.c = c
+ }
+
+ cancel (res) {
+ // assert: cancelled() is called later, before rejected() is called
+ }
+
+ fulfilled (p) {
+ this.runTee(this.f, p)
+ }
+
+ rejected (p) {
+ return this.runTee(this.r, p)
+ }
+
+ cancelled (p) {
+ if (typeof this.c !== 'function') {
+ this.end()._reject(p.near().value)
+ } else {
+ this.runTee(this.c, p.near())
+ }
+ // assert: this.promise == null, so that rejected won't run
+ }
+
+ runTee (f, p) {
+ /* eslint complexity:[2,4] */
+ const hasHandler = (this.f != null || this.r != null || this.c != null) && typeof f === 'function'
+ if (hasHandler) {
+ this.r = null
+ this.c = null
+ this.tryCall(f, p.value)
+ } else {
+ this.put(p)
+ }
+ return hasHandler
+ }
+
+ end () {
+ const promise = this.promise
+ this.promise = null
+ return promise
+ }
+}
diff --git a/src/util.js b/src/util.js
new file mode 100644
index 0000000..38d15eb
--- /dev/null
+++ b/src/util.js
@@ -0,0 +1,7 @@
+// isObject :: * -> boolean
+export function isObject (x) {
+ return (typeof x === 'object' || typeof x === 'function') && x !== null
+}
+
+/* istanbul ignore next */
+export function noop (_) {}
diff --git a/test/CancelToken-test.js b/test/CancelToken-test.js
new file mode 100644
index 0000000..6675a11
--- /dev/null
+++ b/test/CancelToken-test.js
@@ -0,0 +1,678 @@
+import { describe, it } from 'mocha'
+import { CancelToken, isCancelled, isPending, getReason, future, all } from '../src/main'
+import { assertSame, FakeCancelAction } from './lib/test-util'
+import assert from 'assert'
+
+describe('CancelToken', function () {
+ describe('constructor', () => {
+ it('should synchronously call the executor with a function', () => {
+ let cancel
+ new CancelToken(c => { cancel = c })
+ assert.strictEqual(typeof cancel, 'function')
+ })
+
+ it('should throw synchronously when function not provided', () => {
+ assert.throws(() => new CancelToken(), TypeError)
+ })
+
+ it('should not catch exceptions', () => {
+ const err = new Error()
+ assert.throws(() => {
+ new CancelToken(() => { throw err })
+ }, e => e === err)
+ })
+ })
+
+ describe('static source()', () => {
+ it('should return a token and a cancel function', () => {
+ const {token, cancel} = CancelToken.source()
+ assert(token instanceof CancelToken)
+ assert.strictEqual(typeof cancel, 'function')
+ })
+ })
+
+ it('should have a boolean .requested property', () => {
+ const {token, cancel} = CancelToken.source()
+ assert.strictEqual(token.requested, false)
+ cancel()
+ assert.strictEqual(token.requested, true)
+ })
+
+ describe('getCancelled()', () => {
+ it('should return a cancelled promise after the token was cancelled', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel()
+ assert(isCancelled(token.getCancelled()))
+ })
+
+ it('should return a pending promise until the token is cancelled', () => {
+ const {token, cancel} = CancelToken.source()
+ const p = token.getCancelled()
+ assert(isPending(p))
+ cancel()
+ assert(isCancelled(p))
+ })
+
+ it('should cancel with the argument of the first cancel call', () => {
+ const {token, cancel} = CancelToken.source()
+ const r = {}
+ cancel(r)
+ cancel({})
+ const p = token.getCancelled()
+ cancel({})
+ return p.trifurcate(assert.ifError, assert.ifError, e => {
+ assert.strictEqual(e, r)
+ })
+ })
+
+ it('should return a pending promise until the token is asynchronously cancelled', () => {
+ const {token, cancel} = CancelToken.source()
+ const p = token.getCancelled()
+ const r = {}
+ setTimeout(() => {
+ assert(isPending(p))
+ assert(!token.requested)
+ cancel(r)
+ }, 5)
+ return p.trifurcate(assert.ifError, assert.ifError, e => {
+ assert(token.requested)
+ assert.strictEqual(e, r)
+ })
+ })
+ })
+
+ it('should be subclassible', () => {
+ let constructorCalled = false
+ let myCancel
+ class MyCancelToken extends CancelToken {
+ constructor (exec) {
+ constructorCalled = true
+ super(c => {
+ assert.strictEqual(typeof c, 'function')
+ myCancel = () => { c() }
+ exec(myCancel)
+ })
+ }
+ }
+ const {token, cancel} = MyCancelToken.source()
+ assert(constructorCalled)
+ assert(token instanceof MyCancelToken)
+ assert.strictEqual(cancel, myCancel)
+ cancel()
+ assert(token.requested)
+ })
+
+ describe('_subscribe()', () => {
+ it('should synchronously run subscriptions', () => {
+ const {token, cancel} = CancelToken.source()
+ const r = {}
+ const action = new FakeCancelAction({})
+ token._subscribe(action)
+ assert(!action.isCancelled)
+ cancel(r)
+ assert(action.isCancelled)
+ })
+
+ it('should not run subscriptions multiple times', () => {
+ const {token, cancel} = CancelToken.source()
+ const action = new FakeCancelAction({})
+ token._subscribe(action)
+ assert(!action.isCancelled)
+ cancel()
+ cancel()
+ assert.strictEqual(action.isCancelled, 1)
+ })
+
+ it('should not run destroyed subscriptions', () => {
+ const {token, cancel} = CancelToken.source()
+ const action = new FakeCancelAction({})
+ token._subscribe(action)
+ action.destroy()
+ assert(!action.promise)
+ cancel()
+ assert(!action.isCancelled)
+ })
+
+ it('should not run unsubscribed actions', () => {
+ const {token, cancel} = CancelToken.source()
+ const action = new FakeCancelAction({})
+ token._subscribe(action)
+ token._unsubscribe(action)
+ cancel()
+ assert(!action.isCancelled)
+ })
+
+ it('should do the same with lots of subscriptions', () => {
+ const {token, cancel} = CancelToken.source()
+ const active = new Set()
+ const inactive = new Set()
+ for (let i = 0; i < 150; i++) {
+ if (active.size && Math.random() < 0.3) {
+ const action = Array.from(active)[Math.floor(Math.pow(Math.random(), 1.5) * active.size)]
+ if (Math.random() < 0.4) {
+ action.destroy()
+ } else {
+ token._unsubscribe(action)
+ }
+ active.delete(action)
+ inactive.add(action)
+ } else {
+ const action = new FakeCancelAction({})
+ token._subscribe(action)
+ active.add(action)
+ }
+ }
+ // console.log(active.size, inactive.size)
+ for (const action of active) assert(!action.isDestroyed && !action.isCancelled)
+ cancel()
+ for (const action of active) assert(action.isCancelled)
+ for (const action of inactive) assert(!action.isCancelled)
+ })
+
+ it('should run subscriptions when already requested', () => {
+ const {token, cancel} = CancelToken.source()
+ const {resolve, promise} = future()
+ const r = {}
+ const action = new FakeCancelAction({})
+ cancel(r)
+ token._subscribe(action)
+ assert(action.isCancelled)
+ })
+ })
+
+ describe('subscribe()', () => {
+ it('should asynchronously call subscriptions', () => {
+ const {token, cancel} = CancelToken.source()
+ const {resolve, promise} = future()
+ token.subscribe(resolve)
+ cancel()
+ assert(isPending(promise))
+ return promise
+ })
+
+ it('should return a promise for the result', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = {}
+ const p = token.subscribe(() => expected)
+ assert.strictEqual(cancel()[0], p)
+ return p.then(x => {
+ assert.strictEqual(x, expected)
+ })
+ })
+
+ it('should call subscriptions with the reason', () => {
+ const {token, cancel} = CancelToken.source()
+ const r = {}
+ const p = token.subscribe(e => {
+ assert.strictEqual(e, r)
+ })
+ cancel(r)
+ return p
+ })
+
+ it('should not call subscriptions multiple times', () => {
+ const {token, cancel} = CancelToken.source()
+ let calls = 0
+ const p = token.subscribe(() => {
+ calls++
+ assert.strictEqual(calls, 1)
+ })
+ assert.strictEqual(calls, 0)
+ cancel()
+ cancel()
+ return p
+ })
+
+ it('should ignore exceptions thrown by subscriptions', () => {
+ const {token, cancel} = CancelToken.source()
+ let isCalled = false
+ token.subscribe(() => { throw new Error() })
+ const p = token.subscribe(e => {
+ isCalled = true
+ })
+ cancel()
+ return p.then(() => assert(isCalled))
+ })
+
+ it('should call subscriptions when already requested', () => {
+ const {token, cancel} = CancelToken.source()
+ const {resolve, promise} = future()
+ cancel()
+ token.subscribe(resolve)
+ return promise
+ })
+
+ it('should call subscriptions in order', () => {
+ const {token, cancel} = CancelToken.source()
+ let s = 0
+ const a = token.subscribe(() => {
+ assert.strictEqual(s, 0)
+ s = 1
+ })
+ const b = token.subscribe(() => {
+ assert.strictEqual(s, 1)
+ s = 2
+ })
+ cancel()
+ assert.strictEqual(s, 0)
+ return all([a, b]).then(() => assert.strictEqual(s, 2))
+ })
+
+ it('should behave nearly like getCancelled().catch()', () => {
+ const {token, cancel} = CancelToken.source()
+ const p = token.subscribe(x => x)
+ const q = token.getCancelled().catch(x => x)
+ const res = cancel({})
+ assert.strictEqual(res.length, 1)
+ assert.strictEqual(res[0], p)
+ return assertSame(p, q)
+ })
+
+ it('should behave like rejection for throw', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = {}
+ const p = token.subscribe(() => { throw expected })
+ assert.strictEqual(cancel()[0], p)
+ return p.then(assert.ifError, x => assert.strictEqual(x, expected))
+ })
+
+ it('should call subscriptions before the token is cancelled', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = {}
+ const p = token.subscribe(() => expected, CancelToken.empty())
+ cancel()
+ return p.then(x => assert.strictEqual(x, expected))
+ })
+
+ it('should not call subscriptions when the token is cancelled', () => {
+ const a = CancelToken.source()
+ const b = CancelToken.source()
+ const p = a.token.subscribe(() => {
+ throw new Error("should not be called")
+ }, b.token)
+ assert.strictEqual(p.token, b.token)
+ b.cancel()
+ a.cancel()
+ return assertSame(p, b.token.getCancelled())
+ })
+
+ it('should return cancellation with already cancelled token', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel()
+ const p = CancelToken.empty().subscribe(() => {}, token)
+ assert.strictEqual(p, token.getCancelled())
+ })
+
+ it('should behave like cancellation when token is cancelled from the subscription', () => {
+ const a = CancelToken.source()
+ const b = CancelToken.source()
+ const p = a.token.subscribe(() => {
+ b.cancel()
+ }, b.token)
+ assert.strictEqual(p.token, b.token)
+ a.cancel()
+ return assertSame(p, b.token.getCancelled())
+ })
+
+ it('should call subscriptions that are subscribed from the callback', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = {}
+ const p = token.subscribe(() => token.subscribe(() => expected))
+ assert.strictEqual(cancel().length, 1)
+ return p.then(x => assert.strictEqual(x, expected))
+ })
+ })
+
+ describe('subscribeOrCall()', () => {
+ it('should invoke f if the token is cancelled before the call', () => {
+ const {token, cancel} = CancelToken.source()
+ const {resolve, promise} = future()
+ let called = 0
+ const call = token.subscribeOrCall(() => { called |= 1; resolve() }, () => { called |= 2 })
+ assert.strictEqual(called, 0)
+ cancel()
+ return promise.then(() => {
+ assert.strictEqual(called, 1)
+ call()
+ assert.strictEqual(called, 1)
+ })
+ })
+
+ it('should invoke g immediately if the call happens before the cancellation', () => {
+ const {token, cancel} = CancelToken.source()
+ let called = 0
+ const call = token.subscribeOrCall(() => { called |= 1 }, () => { called |= 2 })
+ assert.strictEqual(called, 0)
+ call()
+ assert.strictEqual(called, 2)
+ cancel()
+ return token.subscribe(() => assert.strictEqual(called, 2))
+ })
+
+ it('should only invoke f if the call happens during the cancellation', () => {
+ const {token, cancel} = CancelToken.source()
+ const {resolve, promise} = future()
+ let called = 0
+ const call = token.subscribeOrCall(() => { called |= 1; call(); resolve() }, () => { called |= 2 })
+ assert.strictEqual(called, 0)
+ cancel()
+ return promise.then(() => assert.strictEqual(called, 1))
+ })
+
+ it('should only invoke g if the token is cancelled during the call', () => {
+ const {token, cancel} = CancelToken.source()
+ let called = 0
+ const call = token.subscribeOrCall(() => { called |= 1 }, () => { called |= 2; cancel() })
+ assert.strictEqual(called, 0)
+ call()
+ assert.strictEqual(called, 2)
+ return token.subscribe(() => assert.strictEqual(called, 2))
+ })
+
+ it('should cope with undefined g', () => {
+ const {token, cancel} = CancelToken.source()
+ let called = 0
+ const call = token.subscribeOrCall(() => { called |= 1 })
+ assert.strictEqual(called, 0)
+ call()
+ assert.strictEqual(called, 0)
+ cancel()
+ return token.subscribe(() => assert.strictEqual(called, 0))
+ })
+
+ it('should throw exceptions from g', () => {
+ const expected = {}
+ const call = CancelToken.empty().subscribeOrCall(() => {}, () => { throw expected })
+ assert.throws(call, e => e === expected)
+ })
+
+ it('should invoke g with the arguments and context of the call', () => {
+ const o = {a: {}, b: []}
+ const call = CancelToken.empty().subscribeOrCall(() => {}, function (a, b) {
+ assert.strictEqual(this, o)
+ assert.strictEqual(a, o.a)
+ assert.strictEqual(b, o.b)
+ })
+ call.call(o, o.a, o.b)
+ })
+
+ it('should not invoke g multiple times', () => {
+ let called = 0
+ const call = CancelToken.empty().subscribeOrCall(() => {}, () => { called++ })
+ assert.strictEqual(called, 0)
+ call()
+ assert.strictEqual(called, 1)
+ call()
+ assert.strictEqual(called, 1)
+ })
+ })
+
+ describe('concat()', () => {
+ it('should cancel the result token when a is cancelled first', () => {
+ const a = CancelToken.source()
+ const b = CancelToken.source()
+ const token = a.token.concat(b.token)
+ const r = {}
+ assert(!token.requested)
+ a.cancel(r)
+ assert(token.requested)
+ b.cancel({})
+ return token.getCancelled().then(assert.ifError, e => assert.strictEqual(e, r))
+ })
+
+ it('should cancel the result token when b is cancelled first', () => {
+ const a = CancelToken.source()
+ const b = CancelToken.source()
+ const token = a.token.concat(b.token)
+ const r = {}
+ assert(!token.requested)
+ b.cancel(r)
+ assert(token.requested)
+ a.cancel({})
+ return token.getCancelled().then(assert.ifError, e => assert.strictEqual(e, r))
+ })
+
+ it('should cancel the result token when a is already cancelled', () => {
+ const a = CancelToken.source()
+ const b = CancelToken.source()
+ const r = {}
+ a.cancel(r)
+ const token = a.token.concat(b.token)
+ assert(token.requested)
+ b.cancel({})
+ return token.getCancelled().then(assert.ifError, e => assert.strictEqual(e, r))
+ })
+
+ it('should cancel the result token when b is already cancelled', () => {
+ const a = CancelToken.source()
+ const b = CancelToken.source()
+ const r = {}
+ b.cancel(r)
+ const token = a.token.concat(b.token)
+ assert(token.requested)
+ a.cancel({})
+ return token.getCancelled().then(assert.ifError, e => assert.strictEqual(e, r))
+ })
+ })
+
+ describe('static empty()', () => {
+ it('should produce a token that is never cancelled', () => {
+ const token = CancelToken.empty()
+ assert(token instanceof CancelToken)
+ token.subscribe(() => {
+ setTimeout(assert.ok, 0, false, 'must not be called')
+ })
+ })
+ })
+
+ describe('for()', () => {
+ it('should cancel the token when the promise fulfills', () => {
+ const {resolve, promise} = future()
+ const token = CancelToken.for(promise)
+ assert(!token.requested)
+ const r = {}
+ resolve(r)
+ return token.getCancelled().then(assert.ifError, e => assert.strictEqual(e, r))
+ })
+ })
+
+ describe('static race()', () => {
+ // see also concat test cases
+ it('should ignore tokens added after cancellation', () => {
+ const race = CancelToken.race()
+ const a = CancelToken.source()
+ race.add(a.token)
+ const expected = {}
+ a.cancel(expected)
+ assert(race.get().requested)
+ const b = CancelToken.source()
+ b.cancel()
+ race.add(b.token)
+ assert(race.get().requested)
+ return race.get().getCancelled().then(assert.ifError, e => {
+ assert.strictEqual(e, expected)
+ const c = CancelToken.source()
+ race.add(c.token)
+ assert(race.get().requested)
+ })
+ })
+
+ it('should be requested faster than the subscription', () => {
+ const {token, cancel} = CancelToken.source()
+ token.subscribe(() => {
+ assert(token.requested)
+ assert(race.get().requested)
+ })
+ const race = CancelToken.race([token])
+ return cancel()[0]
+ })
+
+ it('should ignore itself', () => {
+ const {token, cancel} = CancelToken.source()
+ const race = CancelToken.race()
+ race.add(race.get())
+ assert(!race.get().requested)
+ race.add(token)
+ cancel()
+ assert(race.get().requested)
+ })
+ })
+
+ describe('static pool()', () => {
+ it('should cancel when all tokens are cancelled', () => {
+ const sources = []
+ const reasons = []
+ for (let i = 0; i < 5; i++) {
+ sources.push(CancelToken.source())
+ reasons.push({t: i})
+ }
+ const pool = CancelToken.pool(sources.map(s => s.token))
+ for (let i = 0; i < sources.length; i++) {
+ assert(!pool.get().requested)
+ sources[i].cancel(reasons[i])
+ }
+ return pool.get().getCancelled().then(assert.ifError, r => {
+ assert.deepEqual(r, reasons)
+ })
+ })
+
+ it('should allow tokens to be added before cancellation', () => {
+ const pool = CancelToken.pool()
+ const sources = []
+ for (let i = 0; i < 5; i++) {
+ sources[i] = CancelToken.source()
+ sources[i].added = true
+ pool.add(sources[i].token)
+ }
+ for (let i = 0; i < 15; i++) {
+ sources.push(CancelToken.source())
+ }
+ const token = pool.get()
+ while (sources.some(s => s.added && !s.token.requested)) {
+ assert(!token.requested)
+ let s = sources[Math.floor(Math.random() * sources.length)]
+ if (s.added && s.token.requested) {
+ s = sources[sources.length - 1]
+ if (s.added && s.token.requested) {
+ sources.pop()
+ continue
+ }
+ }
+ if (!s.added && Math.random() < 0.9) {
+ if (Math.random() < 0.3) {
+ const x = CancelToken.source()
+ sources.push(x)
+ pool.add(s.token, x.token)
+ x.added = true
+ } else {
+ pool.add(s.token)
+ }
+ s.added = true
+ } else if (!s.token.requested) {
+ s.cancel()
+ }
+ }
+ return pool.get().getCancelled().then(assert.ifError, r => {
+ assert(token.requested)
+ })
+ })
+
+ it('should not allow tokens to be added after cancellation', () => {
+ const pool = CancelToken.pool()
+ const a = CancelToken.source()
+ pool.add(a.token)
+ const b = CancelToken.source()
+ b.cancel()
+ pool.add(b.token)
+ const c = CancelToken.source()
+ pool.add(c.token)
+ c.cancel()
+ a.cancel()
+ return pool.get().getCancelled().then(assert.ifError, () => {
+ const d = CancelToken.source()
+ pool.add(d.token)
+ assert(pool.get().requested)
+ })
+ })
+
+ it('should be requested faster than the subscription', () => {
+ const {token, cancel} = CancelToken.source()
+ token.subscribe(() => {
+ assert(token.requested)
+ assert(pool.get().requested)
+ })
+ const pool = CancelToken.pool([token])
+ return cancel()[0]
+ })
+
+ it('should ignore itself', () => {
+ const {token, cancel} = CancelToken.source()
+ const pool = CancelToken.pool()
+ pool.add(pool.get())
+ assert.strictEqual(pool.tokens.length, 0)
+ assert(!pool.get().requested)
+ pool.add(token)
+ assert(!pool.get().requested)
+ cancel()
+ assert(pool.get().requested)
+ })
+
+ it('should reject if already-cancelled tokens are added', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = {}
+ cancel(expected)
+ assert(CancelToken.pool([token]).get().requested)
+ const pool = CancelToken.pool()
+ pool.add(token)
+ return pool.get().getCancelled().then(assert.ifError, r => {
+ assert.strictEqual(r.length, 1)
+ assert.strictEqual(r[0], expected)
+ })
+ })
+ })
+
+ describe('static reference()', () => {
+ it('should behave like the last assigned token', () => {
+ const {token, cancel} = CancelToken.source()
+ const ref = CancelToken.reference()
+ assert(!ref.get().requested)
+ ref.set(CancelToken.empty())
+ assert(!ref.get().requested)
+ ref.set(null)
+ assert(!ref.get().requested)
+ ref.set(token)
+ assert(!ref.get().requested)
+ cancel({})
+ assert(ref.get().requested)
+ return assertSame(token.getCancelled(), ref.get().getCancelled())
+ })
+
+ it('should throw when assigned to after cancellation', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel()
+ const ref = CancelToken.reference(token)
+ assert(ref.get().requested)
+ assert.throws(() => { ref.set(null) }, ReferenceError)
+ assert.throws(() => { ref.set(CancelToken.empty()) }, ReferenceError)
+ })
+
+ it('should be requested faster than the subscription', () => {
+ const {token, cancel} = CancelToken.source()
+ token.subscribe(() => {
+ assert(token.requested)
+ assert(ref.get().requested)
+ })
+ const ref = CancelToken.reference(token)
+ return cancel()[0]
+ })
+
+ it('should ignore itself', () => {
+ const {token, cancel} = CancelToken.source()
+ const ref = CancelToken.reference(token)
+ ref.set(token)
+ ref.set(ref.get())
+ cancel()
+ assert(ref.get().requested)
+ })
+ })
+})
diff --git a/test/ErrorHandler-test.js b/test/ErrorHandler-test.js
index 1e224ab..b535013 100644
--- a/test/ErrorHandler-test.js
+++ b/test/ErrorHandler-test.js
@@ -1,14 +1,16 @@
import { describe, it } from 'mocha'
+import '../src/Promise'
+import { isHandled } from '../src/inspect'
import ErrorHandler from '../src/ErrorHandler'
import assert from 'assert'
-import { HANDLED } from '../src/state'
function fakeError (value) {
return {
- value: value,
+ value,
_state: 0,
+ near () { return this },
state () { return this._state },
- _runAction () { this._state |= HANDLED }
+ _runAction (a) { a.rejected(this) }
}
}
@@ -73,7 +75,7 @@ describe('ErrorHandler', () => {
const eh = new ErrorHandler(() => true, fail)
eh.untrack(expected)
- assert.equal(expected.state(), HANDLED)
+ assert(isHandled(expected))
})
})
})
diff --git a/test/Promise-test.js b/test/Promise-test.js
index 6b4dc5e..1b8c009 100644
--- a/test/Promise-test.js
+++ b/test/Promise-test.js
@@ -1,5 +1,6 @@
import { describe, it } from 'mocha'
-import { Promise, fulfill, reject } from '../src/main'
+import { Promise, fulfill, reject, isCancelled, CancelToken } from '../src/main'
+import { assertSame } from './lib/test-util'
import assert from 'assert'
describe('Promise', () => {
@@ -13,33 +14,165 @@ describe('Promise', () => {
return p
})
+ it('should not call executor and immediately cancel when token is requested', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ let called = false
+ const p = new Promise((resolve, reject) => {
+ called = true
+ }, token)
+ assert(isCancelled(p))
+ assert(!called)
+ return assertSame(token.getCancelled(), p)
+ })
+
it('should reject if resolver throws synchronously', () => {
- let expected = new Error()
+ const expected = new Error()
return new Promise(() => { throw expected })
.then(assert.ifError, x => assert.strictEqual(expected, x))
})
- it('should fulfill with value', () => {
- let expected = {}
- return new Promise(resolve => resolve(expected))
- .then(x => assert.strictEqual(expected, x))
- })
+ describe('resolvers', () => {
+ it('should fulfill with value', () => {
+ const expected = {}
+ return new Promise(resolve => resolve(expected))
+ .then(x => assert.strictEqual(expected, x))
+ })
- it('should resolve to fulfilled promise', () => {
- let expected = {}
- return new Promise(resolve => resolve(fulfill(expected)))
- .then(x => assert.strictEqual(expected, x))
- })
+ it('should resolve to fulfilled promise', () => {
+ const expected = {}
+ return new Promise(resolve => resolve(fulfill(expected)))
+ .then(x => assert.strictEqual(expected, x))
+ })
- it('should resolve to rejected promise', () => {
- let expected = {}
- return new Promise(resolve => resolve(reject(expected)))
- .then(assert.ifError, x => assert.strictEqual(expected, x))
+ it('should resolve to rejected promise', () => {
+ const expected = new Error()
+ return new Promise(resolve => resolve(reject(expected)))
+ .then(assert.ifError, x => assert.strictEqual(expected, x))
+ })
+
+ it('should reject with value', () => {
+ const expected = new Error()
+ return new Promise((resolve, reject) => reject(expected))
+ .then(assert.ifError, x => assert.strictEqual(expected, x))
+ })
+
+ it('should asynchronously fulfill with value', () => {
+ const expected = {}
+ return new Promise(resolve => setTimeout(resolve, 1, expected))
+ .then(x => assert.strictEqual(expected, x))
+ })
+
+ it('should asynchronously resolve to fulfilled promise', () => {
+ const expected = {}
+ return new Promise(resolve => setTimeout(resolve, 1, fulfill(expected)))
+ .then(x => assert.strictEqual(expected, x))
+ })
+
+ it('should asynchronously resolve to rejected promise', () => {
+ const expected = new Error()
+ return new Promise(resolve => setTimeout(resolve, 1, reject(expected)))
+ .then(assert.ifError, x => assert.strictEqual(expected, x))
+ })
+
+ it('should asynchronously reject with value', () => {
+ const expected = new Error()
+ return new Promise((resolve, reject) => setTimeout(reject, 1, reject(expected)))
+ .then(assert.ifError, x => assert.strictEqual(expected, x))
+ })
+
+ it('should not change state when called multiple times', () => {
+ let res, rej
+ const promise = new Promise((resolve, reject) => {
+ res = resolve
+ rej = reject
+ })
+ res(1)
+ const expected = promise.state()
+ res(2)
+ assert.strictEqual(promise.state(), expected)
+ rej(3)
+ assert.strictEqual(promise.state(), expected)
+ })
+
+ it('should not change state with token when called multiple times', () => {
+ let res, rej
+ const promise = new Promise((resolve, reject) => {
+ res = resolve
+ rej = reject
+ }, CancelToken.empty())
+ res(1)
+ const expected = promise.state()
+ res(2)
+ assert.strictEqual(promise.state(), expected)
+ rej(3)
+ assert.strictEqual(promise.state(), expected)
+ })
})
- it('should reject with value', () => {
- let expected = {}
- return new Promise((resolve, reject) => reject(expected))
- .then(assert.ifError, x => assert.strictEqual(expected, x))
+ describe('token', () => {
+ it('should immediately cancel the promise when cancelled', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = new Error()
+ const p = new Promise(resolve => {}, token)
+ cancel(expected)
+ assert(isCancelled(p))
+ return p.then(assert.ifError, x => assert.strictEqual(expected, x))
+ })
+
+ it('should prevent otherwise fulfilling the promise after cancellation', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = new Error()
+ return new Promise(resolve => {
+ setTimeout(() => {
+ cancel(expected)
+ resolve(1)
+ }, 1)
+ }, token).then(assert.ifError, x => assert.strictEqual(expected, x))
+ })
+
+ it('should prevent otherwise rejecting the promise after cancellation', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = new Error()
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ cancel(expected)
+ reject(1)
+ }, 1)
+ }, token).then(assert.ifError, x => assert.strictEqual(expected, x))
+ })
+
+ it('should have no effect after fulfilling the promise', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = {}
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve(expected)
+ cancel(new Error())
+ }, 1)
+ }, token).then(x => assert.strictEqual(expected, x))
+ })
+
+ it('should have no effect after rejecting the promise', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = new Error()
+ return new Promise((_, reject) => {
+ setTimeout(() => {
+ reject(expected)
+ cancel(1)
+ }, 1)
+ }, token).then(assert.ifError, x => assert.strictEqual(expected, x))
+ })
+
+ it('should still reject the promise after resolving the promise without settling it', () => {
+ const {token, cancel} = CancelToken.source()
+ const expected = {}
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve(new Promise(resolve => setTimeout(resolve, 1)))
+ cancel(expected)
+ }, 1)
+ }, token).then(assert.ifError, x => assert.strictEqual(expected, x))
+ })
})
})
diff --git a/test/TaskQueue-test.js b/test/TaskQueue-test.js
index 097fba1..c39f404 100644
--- a/test/TaskQueue-test.js
+++ b/test/TaskQueue-test.js
@@ -1,5 +1,5 @@
import { describe, it } from 'mocha'
-import TaskQueue from '../src/TaskQueue'
+import { TaskQueue } from '../src/TaskQueue'
import assert from 'assert'
describe('TaskQueue', () => {
diff --git a/test/all-test.js b/test/all-test.js
index ffd5c21..f377281 100644
--- a/test/all-test.js
+++ b/test/all-test.js
@@ -1,5 +1,6 @@
import { describe, it } from 'mocha'
-import { Future, all, resolve } from '../src/Promise'
+import { all, resolve } from '../src/main'
+import { Future } from '../src/Promise'
import { throwingIterable, arrayIterable } from './lib/test-util'
import assert from 'assert'
diff --git a/test/cancellation-test.js b/test/cancellation-test.js
new file mode 100644
index 0000000..e54a747
--- /dev/null
+++ b/test/cancellation-test.js
@@ -0,0 +1,1792 @@
+import { describe, it } from 'mocha'
+import { future, reject, fulfill, never, delay, isCancelled, CancelToken } from '../src/main'
+import { silenceError } from '../src/Promise'
+import { assertSame } from './lib/test-util'
+import assert from 'assert'
+
+const silenced = p => (silenceError(p), p)
+const f = x => x + 1
+const fp = x => fulfill(x + 1)
+const rp = x => silenced(reject(x))
+
+// cancellation tests for method calls on already settled or never-resolved promises
+// with already cancelled or never cancelled tokens
+// can be found in fulfill-test.js, reject-test-js and never-test.js
+
+describe('fulfill', () => {
+ describe('when being cancelled immediately after', () => {
+ describe('then', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = fulfill(1).then(assert.ifError, null, token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ /* describe('catch', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = fulfill(1).catch(assert.ifError, token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ }) */
+
+ describe('map', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = fulfill(1).map(assert.ifError, token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = fulfill(assert.ifError).ap(fulfill(1), token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = fulfill(1).chain(assert.ifError, token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+ })
+
+ describe('when being cancelled after resolution just before the handler', () => {
+ describe('then', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ fulfill(1).then(() => cancel({}))
+ const res = fulfill(1).then(assert.ifError, null, token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ /* describe('catch', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ fulfill(1).then(() => cancel({}))
+ const res = fulfill(1).catch(assert.ifError, token)
+ return assertSame(token.getCancelled(), res)
+ })
+ }) */
+
+ describe('map', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ fulfill(1).then(() => cancel({}))
+ const res = fulfill(1).map(assert.ifError, token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ fulfill(1).then(() => cancel({}))
+ const res = fulfill(assert.ifError).ap(fulfill(1), token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ fulfill(1).then(() => cancel({}))
+ const res = fulfill(1).chain(assert.ifError, token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+ })
+
+ describe('when being cancelled from the handler', () => {
+ describe('then', () => {
+ it('should behave like cancellation and ignore exceptions for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = fulfill(1).then(() => {
+ cancel({})
+ throw new Error()
+ }, null, token)
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = fulfill(1).then(() => cancel({}), null, token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = fulfill(1).map(() => cancel({}), token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = fulfill(() => cancel({})).ap(fulfill(1), token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = fulfill(1).chain(() => cancel({}), token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+ })
+
+ describe('when being cancelled after the handler', () => {
+ describe('then', () => {
+ it('should behave like mapped for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = fulfill(1)
+ const res = promise.then(f, null, token)
+ promise.then(() => cancel({}))
+ return assertSame(promise.map(f), res)
+ })
+
+ it('should behave like chained for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = fulfill(1)
+ const res = promise.then(fp, null, token)
+ promise.then(() => cancel({}))
+ return assertSame(promise.chain(fp), res)
+ })
+
+ it('should behave like rejection chained for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = fulfill(1)
+ const res = promise.then(rp, null, token)
+ promise.then(() => cancel({}))
+ return assertSame(promise.chain(rp), res)
+ })
+ })
+
+ /* describe('catch', () => {
+ it('should behave like fulfillment for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = fulfill(1)
+ const res = promise.catch(f, token)
+ promise.then(() => cancel({}))
+ return assertSame(promise, res)
+ })
+ }) */
+
+ describe('map', () => {
+ it('should behave like mapped for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = fulfill(1)
+ const res = promise.map(f, token)
+ promise.then(() => cancel({}))
+ return assertSame(promise.map(f), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like apply for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = fulfill(f)
+ const q = fulfill(1)
+ const res = promise.ap(q, token)
+ promise.ap(q).then(() => cancel({}))
+ return assertSame(promise.ap(q), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like chained for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = fulfill(1)
+ const res = promise.chain(fp, token)
+ promise.then(() => cancel({}))
+ return assertSame(promise.chain(fp), res)
+ })
+
+ it('should behave like rejection chained for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = fulfill(1)
+ const res = promise.chain(rp, token)
+ promise.then(() => cancel({}))
+ return assertSame(promise.chain(rp), res)
+ })
+ })
+ })
+})
+
+describe('reject', () => {
+ describe('when being cancelled immediately after', () => {
+ describe('catch', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = silenced(reject(1)).catch(assert.ifError, token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ /* describe('then', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = silenced(reject(1)).then(assert.ifError, null, token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = silenced(reject(1)).map(assert.ifError, token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = silenced(reject(1)).ap(fulfill(1), token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = silenced(reject(1)).chain(assert.ifError, token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ }) */
+ })
+
+ describe('when being cancelled after resolution just before the handler', () => {
+ describe('catch', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ reject(1).catch(() => cancel({}))
+ const res = silenced(reject(1)).catch(assert.ifError, token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ /* describe('then', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ reject(1).catch(() => cancel({}))
+ const res = silenced(reject(1)).then(assert.ifError, null, token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ reject(1).catch(() => cancel({}))
+ const res = silenced(reject(1)).map(assert.ifError, token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ reject(1).catch(() => cancel({}))
+ const res = silenced(reject(1)).ap(fulfill(1), token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ reject(1).catch(() => cancel({}))
+ const res = silenced(reject(1)).chain(assert.ifError, token)
+ return assertSame(token.getCancelled(), res)
+ })
+ }) */
+ })
+
+ describe('when being cancelled from the handler', () => {
+ describe('catch', () => {
+ it('should behave like cancellation and ignore exceptions for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = reject(1).catch(() => {
+ cancel({})
+ throw new Error()
+ }, token)
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const res = silenced(reject(1)).catch(() => cancel({}), token)
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+ })
+
+ describe('when being cancelled after the handler', () => {
+ describe('catch', () => {
+ it('should behave like mapped for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = reject(1)
+ const res = promise.catch(f, token)
+ promise.catch(() => cancel({}))
+ return assertSame(promise.catch(f), res)
+ })
+
+ it('should behave like chained for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = reject(1)
+ const res = promise.catch(fp, token)
+ promise.catch(() => cancel({}))
+ return assertSame(promise.catch(fp), res)
+ })
+
+ it('should behave like rejection chained for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = reject(1)
+ const res = promise.catch(rp, token)
+ promise.catch(() => cancel({}))
+ return assertSame(promise.catch(rp), res)
+ })
+ })
+
+ /* describe('then', () => {
+ it('should behave like rejection for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = silenced(reject(1))
+ const res = promise.then(f, null, token)
+ promise.catch(() => cancel({}))
+ return assertSame(promise, res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like rejection for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = silenced(reject(1))
+ const res = promise.map(f, token)
+ promise.catch(() => cancel({}))
+ return assertSame(promise, res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like rejection for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = silenced(reject(f))
+ const res = promise.ap(fulfill(1), token)
+ promise.catch(() => cancel({}))
+ return assertSame(promise, res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like rejection for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const promise = silenced(reject(1))
+ const res = promise.chain(fp, token)
+ promise.catch(() => cancel({}))
+ return assertSame(promise, res)
+ })
+ }) */
+ })
+})
+
+describe('future', () => {
+ describe('with token', () => {
+ it('should reject the future when cancelled', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ const expected = {}
+ const res = promise.catch(e => assert.strictEqual(e, expected))
+ cancel(expected)
+ return res
+ })
+
+ it('should behave like cancellation before cancelled for no-token resolution', () => {
+ const { token, cancel } = CancelToken.source()
+ const a = future(token)
+ const b = future()
+ a.resolve(b.promise)
+ const res = assertSame(a.promise, token.getCancelled())
+ cancel({})
+ return res
+ })
+
+ it('should behave like cancellation after cancelled for no-token resolution', () => {
+ const { token, cancel } = CancelToken.source()
+ const a = future(token)
+ const b = future()
+ a.resolve(b.promise)
+ cancel({})
+ return assertSame(a.promise, token.getCancelled())
+ })
+
+ it('should behave like cancellation before cancelled for same-token resolution', () => {
+ const { token, cancel } = CancelToken.source()
+ const a = future(token)
+ const b = future(token)
+ a.resolve(b.promise)
+ const res = assertSame(a.promise, token.getCancelled())
+ cancel({})
+ return res
+ })
+
+ it('should behave like cancellation after cancelled for same-token resolution', () => {
+ const { token, cancel } = CancelToken.source()
+ const a = future(token)
+ const b = future(token)
+ a.resolve(b.promise)
+ cancel({})
+ return assertSame(a.promise, token.getCancelled())
+ })
+
+ it('should behave like cancellation before cancelled for different-token resolution', () => {
+ const { token, cancel } = CancelToken.source()
+ const a = future(token)
+ const b = future(CancelToken.empty())
+ a.resolve(b.promise)
+ const res = assertSame(a.promise, token.getCancelled())
+ cancel({})
+ return res
+ })
+
+ it('should behave like cancellation after cancelled for different-token resolution', () => {
+ const { token, cancel } = CancelToken.source()
+ const a = future(token)
+ const b = future(CancelToken.empty())
+ a.resolve(b.promise)
+ cancel({})
+ return assertSame(a.promise, token.getCancelled())
+ })
+
+ it('should behave like rejection before resolution cancelled for no token', () => {
+ const { token, cancel } = CancelToken.source()
+ const a = future()
+ const b = future(token)
+ a.resolve(b.promise)
+ const expected = {}
+ const res = assertSame(a.promise, reject(expected))
+ cancel(expected)
+ return res
+ })
+
+ it('should behave like rejection after resolution cancelled for no token', () => {
+ const { token, cancel } = CancelToken.source()
+ const a = future()
+ const b = future(token)
+ a.resolve(b.promise)
+ const expected = {}
+ cancel(expected)
+ return assertSame(a.promise, reject(expected))
+ })
+
+ it('should behave like rejection before resolution cancelled for different token', () => {
+ const { token, cancel } = CancelToken.source()
+ const a = future(CancelToken.empty())
+ const b = future(token)
+ a.resolve(b.promise)
+ const expected = {}
+ const res = assertSame(a.promise, reject(expected))
+ cancel(expected)
+ return res
+ })
+
+ it('should behave like rejection after resolution cancelled for different token', () => {
+ const { token, cancel } = CancelToken.source()
+ const a = future(CancelToken.empty())
+ const b = future(token)
+ a.resolve(b.promise)
+ const expected = {}
+ cancel(expected)
+ return assertSame(a.promise, reject(expected))
+ })
+
+ it('should behave like fulfillment before no-token resolution fulfilled', () => {
+ const a = future(CancelToken.empty())
+ const b = future()
+ a.resolve(b.promise)
+ const expected = fulfill({})
+ const res = assertSame(a.promise, expected)
+ b.resolve(expected)
+ return res
+ })
+
+ it('should behave like fulfillment after no-token resolution fulfilled', () => {
+ const a = future(CancelToken.empty())
+ const b = future()
+ a.resolve(b.promise)
+ const expected = fulfill({})
+ b.resolve(expected)
+ return assertSame(a.promise, expected)
+ })
+
+ it('should behave like fulfillment before same-token resolution fulfilled', () => {
+ const token = CancelToken.empty()
+ const a = future(token)
+ const b = future(token)
+ a.resolve(b.promise)
+ const expected = fulfill({})
+ const res = assertSame(a.promise, expected)
+ b.resolve(expected)
+ return res
+ })
+
+ it('should behave like fulfillment after same-token resolution fulfilled', () => {
+ const token = CancelToken.empty()
+ const a = future(token)
+ const b = future(token)
+ a.resolve(b.promise)
+ const expected = fulfill({})
+ b.resolve(expected)
+ return assertSame(a.promise, expected)
+ })
+
+ it('should behave like fulfillment before different-token resolution fulfilled', () => {
+ const a = future(CancelToken.empty())
+ const b = future(CancelToken.empty())
+ a.resolve(b.promise)
+ const expected = fulfill({})
+ const res = assertSame(a.promise, expected)
+ b.resolve(expected)
+ return res
+ })
+
+ it('should behave like fulfillment after different-token resolution fulfilled', () => {
+ const a = future(CancelToken.empty())
+ const b = future(CancelToken.empty())
+ a.resolve(b.promise)
+ const expected = fulfill({})
+ b.resolve(expected)
+ return assertSame(a.promise, expected)
+ })
+
+ it('should behave like fulfillment before some-token resolution fulfilled', () => {
+ const a = future()
+ const b = future(CancelToken.empty())
+ a.resolve(b.promise)
+ const expected = fulfill({})
+ const res = assertSame(a.promise, expected)
+ b.resolve(expected)
+ return res
+ })
+
+ it('should behave like fulfillment after some-token resolution fulfilled', () => {
+ const a = future()
+ const b = future(CancelToken.empty())
+ a.resolve(b.promise)
+ const expected = fulfill({})
+ b.resolve(expected)
+ return assertSame(a.promise, expected)
+ })
+
+ it('should behave like rejection before no-token resolution rejected', () => {
+ const a = future(CancelToken.empty())
+ const b = future()
+ a.resolve(b.promise)
+ const expected = reject({})
+ const res = assertSame(a.promise, expected)
+ b.resolve(expected)
+ return res
+ })
+
+ it('should behave like rejection after no-token resolution rejected', () => {
+ const a = future(CancelToken.empty())
+ const b = future()
+ a.resolve(b.promise)
+ const expected = reject({})
+ b.resolve(expected)
+ return assertSame(a.promise, expected)
+ })
+
+ it('should behave like rejection before same-token resolution rejected', () => {
+ const token = CancelToken.empty()
+ const a = future(token)
+ const b = future(token)
+ a.resolve(b.promise)
+ const expected = reject({})
+ const res = assertSame(a.promise, expected)
+ b.resolve(expected)
+ return res
+ })
+
+ it('should behave like rejection after same-token resolution rejected', () => {
+ const token = CancelToken.empty()
+ const a = future(token)
+ const b = future(token)
+ a.resolve(b.promise)
+ const expected = reject({})
+ b.resolve(expected)
+ return assertSame(a.promise, expected)
+ })
+
+ it('should behave like rejection before different-token resolution rejected', () => {
+ const a = future(CancelToken.empty())
+ const b = future(CancelToken.empty())
+ a.resolve(b.promise)
+ const expected = reject({})
+ const res = assertSame(a.promise, expected)
+ b.resolve(expected)
+ return res
+ })
+
+ it('should behave like rejection after different-token resolution rejected', () => {
+ const a = future(CancelToken.empty())
+ const b = future(CancelToken.empty())
+ a.resolve(b.promise)
+ const expected = reject({})
+ b.resolve(expected)
+ return assertSame(a.promise, expected)
+ })
+
+ it('should behave like rejection before some-token resolution rejected', () => {
+ const a = future()
+ const b = future(CancelToken.empty())
+ a.resolve(b.promise)
+ const expected = reject({})
+ const res = assertSame(a.promise, expected)
+ b.resolve(expected)
+ return res
+ })
+
+ it('should behave like rejection after some-token resolution rejected', () => {
+ const a = future()
+ const b = future(CancelToken.empty())
+ a.resolve(b.promise)
+ const expected = reject({})
+ b.resolve(expected)
+ return assertSame(a.promise, expected)
+ })
+ })
+
+ describe('then without callbacks', () => {
+ it('should behave like cancellation when cancelled', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future()
+ const res = promise.then(null, null, token)
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation when cancelled for never', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.then(null, null, token)
+ resolve(never())
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like fulfillment when never cancelled for fulfill', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.then(null, null, token)
+ resolve(p)
+ return assertSame(p, res)
+ })
+
+ it('should behave like rejection when never cancelled for reject', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = reject(1)
+ const res = promise.then(null, null, token)
+ resolve(p)
+ return assertSame(p, res)
+ })
+ })
+
+ describe('when not being cancelled', () => {
+ describe('then', () => {
+ it('should behave like mapped for fulfill', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.then(f, null, token)
+ resolve(p)
+ return assertSame(p.map(f), res)
+ })
+
+ it('should behave like chained for fulfill', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.then(fp, null, token)
+ resolve(p)
+ return assertSame(p.chain(fp), res)
+ })
+
+ it('should behave like rejection chained for fulfill', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.then(rp, null, token)
+ resolve(p)
+ return assertSame(p.chain(rp), res)
+ })
+
+ it('should behave like rejection for reject', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
+ const res = promise.then(f, null, token)
+ resolve(p)
+ return assertSame(p, res)
+ })
+ })
+
+ describe('catch', () => {
+ it('should behave like fulfillment for fulfill', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.catch(f, token)
+ resolve(p)
+ return assertSame(p, res)
+ })
+
+ it('should behave like mapped for reject', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = reject(1)
+ const res = promise.catch(f, token)
+ resolve(p)
+ return assertSame(p.catch(f), res)
+ })
+
+ it('should behave like chained for reject', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = reject(1)
+ const res = promise.catch(fp, token)
+ resolve(p)
+ return assertSame(p.catch(fp), res)
+ })
+
+ it('should behave like rejection chained for reject', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = reject(1)
+ const res = promise.catch(rp, token)
+ resolve(p)
+ return assertSame(p.catch(rp), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like mapped for fulfill', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.map(f, token)
+ resolve(p)
+ return assertSame(p.map(f), res)
+ })
+
+ it('should behave like rejection for reject', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
+ const res = promise.map(f, token)
+ resolve(p)
+ return assertSame(p, res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like apply for fulfill', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(f)
+ const q = fulfill(1)
+ const res = promise.ap(q, token)
+ resolve(p)
+ return assertSame(p.ap(q), res)
+ })
+
+ it('should behave like rejection for reject', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = silenced(reject(f))
+ const res = promise.ap(fulfill(1), token)
+ resolve(p)
+ return assertSame(p, res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like chained for fulfill', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.chain(fp, token)
+ resolve(p)
+ return assertSame(p.chain(fp), res)
+ })
+
+ it('should behave like rejection chained for fulfill', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.chain(rp, token)
+ resolve(p)
+ return assertSame(p.chain(rp), res)
+ })
+
+ it('should behave like rejection for reject', () => {
+ const { token } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
+ const res = promise.chain(fp, token)
+ resolve(p)
+ return assertSame(p, res)
+ })
+ })
+ })
+
+ describe('when called with already cancelled token', () => {
+ describe('then', () => {
+ it('should return cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ cancel({})
+ const res = promise.then(assert.ifError, null, token)
+ resolve(fulfill(1))
+ assert.strictEqual(token.getCancelled(), res)
+ })
+
+ it('should return cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ cancel({})
+ const res = promise.then(assert.ifError, null, token)
+ resolve(silenced(reject(1)))
+ assert.strictEqual(token.getCancelled(), res)
+ })
+ })
+
+ describe('catch', () => {
+ it('should return cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ cancel({})
+ const res = promise.catch(assert.ifError, token)
+ resolve(fulfill(1))
+ assert.strictEqual(token.getCancelled(), res)
+ })
+
+ it('should return cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ cancel({})
+ const res = promise.catch(assert.ifError, token)
+ resolve(silenced(reject(1)))
+ assert.strictEqual(token.getCancelled(), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should return cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ cancel({})
+ const res = promise.map(assert.ifError, token)
+ resolve(fulfill(1))
+ assert.strictEqual(token.getCancelled(), res)
+ })
+
+ it('should return cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ cancel({})
+ const res = promise.map(assert.ifError, token)
+ resolve(silenced(reject(1)))
+ assert.strictEqual(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should return cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ cancel({})
+ const res = promise.ap(fulfill(1), token)
+ resolve(fulfill(assert.ifError))
+ assert.strictEqual(token.getCancelled(), res)
+ })
+
+ it('should return cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ cancel({})
+ const res = promise.ap(fulfill(1), token)
+ resolve(silenced(reject(1)))
+ assert.strictEqual(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should return cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ cancel({})
+ const res = promise.chain(assert.ifError, token)
+ resolve(fulfill(1))
+ assert.strictEqual(token.getCancelled(), res)
+ })
+
+ it('should return cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ cancel({})
+ const res = promise.chain(assert.ifError, token)
+ resolve(silenced(reject(1)))
+ assert.strictEqual(token.getCancelled(), res)
+ })
+ })
+ })
+
+ describe('when being cancelled and never resolved', () => {
+ describe('then', () => {
+ it('should behave like cancellation', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future()
+ const res = promise.then(assert.ifError, null, token)
+ cancel({})
+ assert(isCancelled(res))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for never', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.then(assert.ifError, null, token)
+ resolve(never())
+ cancel({})
+ assert(isCancelled(res))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('catch', () => {
+ it('should behave like cancellation', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future()
+ const res = promise.catch(assert.ifError, token)
+ cancel({})
+ assert(isCancelled(res))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for never', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.catch(assert.ifError, token)
+ resolve(never())
+ cancel({})
+ assert(isCancelled(res))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like cancellation', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future()
+ const res = promise.map(assert.ifError, token)
+ cancel({})
+ assert(isCancelled(res))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for never', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.map(assert.ifError, token)
+ resolve(never())
+ cancel({})
+ assert(isCancelled(res))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like cancellation', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future()
+ const res = promise.ap(fulfill(1), token)
+ cancel({})
+ assert(isCancelled(res))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for never', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.ap(fulfill(1), token)
+ resolve(never())
+ cancel({})
+ assert(isCancelled(res))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like cancellation', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future()
+ const res = promise.chain(assert.ifError, token)
+ cancel({})
+ assert(isCancelled(res))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for never', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.chain(assert.ifError, token)
+ resolve(never())
+ cancel({})
+ assert(isCancelled(res))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+ })
+
+ describe('when being cancelled before resolution', () => {
+ describe('then', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.then(assert.ifError, null, token)
+ cancel({})
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.then(assert.ifError, null, token)
+ cancel({})
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('catch', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.catch(assert.ifError, token)
+ cancel({})
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.catch(assert.ifError, token)
+ cancel({})
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.map(assert.ifError, token)
+ cancel({})
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.map(assert.ifError, token)
+ cancel({})
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.ap(fulfill(1), token)
+ cancel({})
+ resolve(fulfill(assert.ifError))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.ap(fulfill(1), token)
+ cancel({})
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.chain(assert.ifError, token)
+ cancel({})
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.chain(assert.ifError, token)
+ cancel({})
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+ })
+
+ describe('when being cancelled after resolution', () => {
+ describe('then', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.then(assert.ifError, null, token)
+ resolve(fulfill(1))
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.then(assert.ifError, null, token)
+ resolve(silenced(reject(1)))
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('catch', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.catch(assert.ifError, token)
+ resolve(fulfill(1))
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.catch(assert.ifError, token)
+ resolve(silenced(reject(1)))
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.map(assert.ifError, token)
+ resolve(fulfill(1))
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.map(assert.ifError, token)
+ resolve(silenced(reject(1)))
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.ap(fulfill(1), token)
+ resolve(fulfill(assert.ifError))
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.ap(fulfill(1), token)
+ resolve(silenced(reject(1)))
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.chain(assert.ifError, token)
+ resolve(fulfill(1))
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.chain(assert.ifError, token)
+ resolve(silenced(reject(1)))
+ cancel({})
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+ })
+
+ describe('when being cancelled after resolution just before the handler', () => {
+ describe('then', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ promise.then(() => cancel({}))
+ const res = promise.then(assert.ifError, null, token)
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ promise.catch(() => cancel({}))
+ const res = promise.then(assert.ifError, null, token)
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('catch', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ promise.then(() => cancel({}))
+ const res = promise.catch(assert.ifError, token)
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ promise.catch(() => cancel({}))
+ const res = promise.catch(assert.ifError, token)
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ promise.then(() => cancel({}))
+ const res = promise.map(assert.ifError, token)
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ promise.catch(() => cancel({}))
+ const res = promise.map(assert.ifError, token)
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ promise.then(() => cancel({}))
+ const res = promise.ap(fulfill(1), token)
+ resolve(fulfill(assert.ifError))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ promise.catch(() => cancel({}))
+ const res = promise.ap(fulfill(1), token)
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ promise.then(() => cancel({}))
+ const res = promise.chain(assert.ifError, token)
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ promise.catch(() => cancel({}))
+ const res = promise.chain(assert.ifError, token)
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+ })
+
+ describe('when being cancelled from the handler', () => {
+ // see also how trifurcate handles this differently
+ describe('then', () => {
+ it('should behave like cancellation and ignore exceptions for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.then(() => {
+ cancel({})
+ throw new Error()
+ }, null, token)
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.then(() => cancel({}), null, token)
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('catch', () => {
+ it('should behave like cancellation for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.catch(() => cancel({}), token)
+ resolve(silenced(reject(1)))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.map(() => cancel({}), token)
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.ap(fulfill(1), token)
+ resolve(fulfill(() => cancel({})))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like cancellation for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const res = promise.chain(() => cancel({}), token)
+ resolve(fulfill(1))
+ return assertSame(token.getCancelled(), res)
+ })
+ })
+ })
+
+ describe('when being cancelled after the handler', () => {
+ describe('then', () => {
+ it('should behave like mapped for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.then(f, null, token)
+ promise.then(() => cancel({}))
+ resolve(p)
+ return assertSame(p.map(f), res)
+ })
+
+ it('should behave like chained for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.then(fp, null, token)
+ promise.then(() => cancel({}))
+ resolve(p)
+ return assertSame(p.chain(fp), res)
+ })
+
+ it('should behave like rejection chained for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.then(rp, null, token)
+ promise.then(() => cancel({}))
+ resolve(p)
+ return assertSame(p.chain(rp), res)
+ })
+
+ it('should behave like rejection for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
+ const res = promise.then(f, null, token)
+ promise.catch(() => cancel({}))
+ resolve(p)
+ return assertSame(p, res)
+ })
+ })
+
+ describe('catch', () => {
+ it('should behave like fulfillment for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.catch(f, token)
+ promise.then(() => cancel({}))
+ resolve(p)
+ return assertSame(p, res)
+ })
+
+ it('should behave like mapped for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = reject(1)
+ const res = promise.catch(f, token)
+ promise.catch(() => cancel({}))
+ resolve(p)
+ return assertSame(p.catch(f), res)
+ })
+
+ it('should behave like chained for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = reject(1)
+ const res = promise.catch(fp, token)
+ promise.catch(() => cancel({}))
+ resolve(p)
+ return assertSame(p.catch(fp), res)
+ })
+
+ it('should behave like rejection chained for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = reject(1)
+ const res = promise.catch(rp, token)
+ promise.catch(() => cancel({}))
+ resolve(p)
+ return assertSame(p.catch(rp), res)
+ })
+ })
+
+ describe('map', () => {
+ it('should behave like mapped for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.map(f, token)
+ promise.then(() => cancel({}))
+ resolve(p)
+ return assertSame(p.map(f), res)
+ })
+
+ it('should behave like rejection for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
+ const res = promise.map(f, token)
+ promise.catch(() => cancel({}))
+ resolve(p)
+ return assertSame(p, res)
+ })
+ })
+
+ describe('ap', () => {
+ it('should behave like apply for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(f)
+ const q = fulfill(1)
+ const res = promise.ap(q, token)
+ promise.ap(q).then(() => cancel({}))
+ resolve(p)
+ return assertSame(p.ap(q), res)
+ })
+
+ it('should behave like rejection for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = silenced(reject(f))
+ const res = promise.ap(fulfill(1), token)
+ promise.catch(() => cancel({}))
+ resolve(p)
+ return assertSame(p, res)
+ })
+ })
+
+ describe('chain', () => {
+ it('should behave like chained for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.chain(fp, token)
+ promise.then(() => cancel({}))
+ resolve(p)
+ return assertSame(p.chain(fp), res)
+ })
+
+ it('should behave like rejection chained for fulfill', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.chain(rp, token)
+ promise.then(() => cancel({}))
+ resolve(p)
+ return assertSame(p.chain(rp), res)
+ })
+
+ it('should behave like rejection for reject', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
+ const res = promise.chain(fp, token)
+ promise.catch(() => cancel({}))
+ resolve(p)
+ return assertSame(p, res)
+ })
+ })
+ })
+})
+
+describe('then with nested callbacks', () => {
+ it('should near to inner when both have the same token', () => {
+ const { token } = CancelToken.source()
+ const expected = fulfill({})
+ const p = delay(3, expected, token)
+ const q = delay(1)
+ const r = q.then(() => p, undefined, token)
+ return q.then(() => {
+ assert.strictEqual(r.near(), p)
+ return assertSame(expected, r)
+ })
+ })
+
+ it('should behave like cancellation when the outer promise is cancelled for no inner token', () => {
+ const { token, cancel } = CancelToken.source()
+ const p = delay(1, {})
+ const res = p.then(() => delay(1), undefined, token)
+ p.then(cancel)
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation when the outer promise is cancelled for same inner token', () => {
+ const { token, cancel } = CancelToken.source()
+ const p = delay(1, {})
+ const res = p.then(() => delay(1, null, token), undefined, token)
+ p.then(cancel)
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation when the outer promise is cancelled for different inner token', () => {
+ const { token, cancel } = CancelToken.source()
+ const p = delay(1, {})
+ const res = p.then(() => delay(1, null, CancelToken.empty()), undefined, token)
+ p.then(cancel)
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like fulfillment for no inner token', () => {
+ const expected = fulfill({})
+ const res = delay(1).then(() => delay(1, expected), undefined, CancelToken.empty())
+ return assertSame(expected, res)
+ })
+
+ it('should behave like fulfillment for same inner token', () => {
+ const token = CancelToken.empty()
+ const expected = fulfill({})
+ const res = delay(1).then(() => delay(1, expected, token), undefined, token)
+ return assertSame(expected, res)
+ })
+
+ it('should behave like fulfillment for different inner token', () => {
+ const expected = fulfill({})
+ const res = delay(1).then(() => delay(1, expected, CancelToken.empty()), undefined, CancelToken.empty())
+ return assertSame(expected, res)
+ })
+
+ it('should behave like rejection for no inner token', () => {
+ const expected = reject({})
+ const res = delay(1).then(() => delay(1, expected), undefined, CancelToken.empty())
+ return assertSame(expected, res)
+ })
+
+ it('should behave like rejection for same inner token', () => {
+ const token = CancelToken.empty()
+ const expected = reject({})
+ const res = delay(1).then(() => delay(1, expected, token), undefined, token)
+ return assertSame(expected, res)
+ })
+
+ it('should behave like rejection for different inner token', () => {
+ const expected = reject({})
+ const res = delay(1).then(() => delay(1, expected, CancelToken.empty()), undefined, CancelToken.empty())
+ return assertSame(expected, res)
+ })
+
+ it('should behave like rejection when the inner promise is cancelled for no outer token', () => {
+ const { token, cancel } = CancelToken.source()
+ const p = delay(1, {})
+ const res = p.then(() => delay(1, null, token))
+ p.then(cancel)
+ return assertSame(p.then(reject), res)
+ })
+
+ it('should behave like rejection when the inner promise is cancelled for different outer token', () => {
+ const { token, cancel } = CancelToken.source()
+ const p = delay(1, {})
+ const res = p.then(() => delay(1, null, token), undefined, CancelToken.empty())
+ p.then(cancel)
+ return assertSame(p.then(reject), res)
+ })
+})
+
+describe('chain with nested callbacks', () => {
+ it('should near to inner when both have the same token', () => {
+ const { token } = CancelToken.source()
+ const expected = fulfill({})
+ const p = delay(3, expected, token)
+ const q = delay(1)
+ const r = q.chain(() => p, token)
+ return q.then(() => {
+ assert.strictEqual(r.near(), p)
+ return assertSame(expected, r)
+ })
+ })
+
+ it('should behave like cancellation when the outer promise is cancelled for no inner token', () => {
+ const { token, cancel } = CancelToken.source()
+ const p = delay(1, {})
+ const res = p.chain(() => delay(1), token)
+ p.then(cancel)
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation when the outer promise is cancelled for same inner token', () => {
+ const { token, cancel } = CancelToken.source()
+ const p = delay(1, {})
+ const res = p.chain(() => delay(1, null, token), token)
+ p.then(cancel)
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like cancellation when the outer promise is cancelled for different inner token', () => {
+ const { token, cancel } = CancelToken.source()
+ const p = delay(1, {})
+ const res = p.chain(() => delay(1, null, CancelToken.empty()), token)
+ p.then(cancel)
+ return assertSame(token.getCancelled(), res)
+ })
+
+ it('should behave like fulfillment for no inner token', () => {
+ const expected = fulfill({})
+ const res = delay(1).chain(() => delay(1, expected), CancelToken.empty())
+ return assertSame(expected, res)
+ })
+
+ it('should behave like fulfillment for same inner token', () => {
+ const token = CancelToken.empty()
+ const expected = fulfill({})
+ const res = delay(1).chain(() => delay(1, expected, token), token)
+ return assertSame(expected, res)
+ })
+
+ it('should behave like fulfillment for different inner token', () => {
+ const expected = fulfill({})
+ const res = delay(1).chain(() => delay(1, expected, CancelToken.empty()), CancelToken.empty())
+ return assertSame(expected, res)
+ })
+
+ it('should behave like rejection for no inner token', () => {
+ const expected = reject({})
+ const res = delay(1).chain(() => delay(1, expected), CancelToken.empty())
+ return assertSame(expected, res)
+ })
+
+ it('should behave like rejection for same inner token', () => {
+ const token = CancelToken.empty()
+ const expected = reject({})
+ const res = delay(1).chain(() => delay(1, expected, token), token)
+ return assertSame(expected, res)
+ })
+
+ it('should behave like rejection for different inner token', () => {
+ const expected = reject({})
+ const res = delay(1).chain(() => delay(1, expected, CancelToken.empty()), CancelToken.empty())
+ return assertSame(expected, res)
+ })
+
+ it('should behave like rejection when the inner promise is cancelled for no outer token', () => {
+ const { token, cancel } = CancelToken.source()
+ const p = delay(1, {})
+ const res = p.chain(() => delay(1, null, token))
+ p.then(cancel)
+ return assertSame(p.then(reject), res)
+ })
+
+ it('should behave like rejection when the inner promise is cancelled for different outer token', () => {
+ const { token, cancel } = CancelToken.source()
+ const p = delay(1, {})
+ const res = p.chain(() => delay(1, null, token), CancelToken.empty())
+ p.then(cancel)
+ return assertSame(p.then(reject), res)
+ })
+})
diff --git a/test/chain-test.js b/test/chain-test.js
index 3f204cd..76c5cef 100644
--- a/test/chain-test.js
+++ b/test/chain-test.js
@@ -29,4 +29,9 @@ describe('chain', function () {
return delay(1, expected).then(reject).chain(() => null)
.then(assert.ifError, x => assert.strictEqual(x, expected))
})
+
+ it('should have cycle detection', () => {
+ const p = delay(1).chain(() => p)
+ return p.then(assert.ifError, e => assert(e instanceof TypeError))
+ })
})
diff --git a/test/concat-test.js b/test/concat-test.js
index afa61dd..14853a7 100644
--- a/test/concat-test.js
+++ b/test/concat-test.js
@@ -1,6 +1,6 @@
import { describe, it } from 'mocha'
import { fulfill, delay, reject, never } from '../src/main'
-import { silenceError } from '../src/inspect'
+import { silenceError } from '../src/Promise'
import { assertSame } from './lib/test-util'
import assert from 'assert'
@@ -22,26 +22,32 @@ describe('concat', function () {
assert.strictEqual(p2, p1.concat(p2))
})
- it('should return earlier future', () => {
+ it('should behave like earlier future', () => {
+ const expected = {}
+ const p = delay(1, expected).concat(delay(10))
+ return assertSame(p, fulfill(expected))
+ })
+
+ it('should behave like other earlier future', () => {
const expected = {}
const p = delay(10).concat(delay(1, expected))
return assertSame(p, fulfill(expected))
})
- it('should behave like fulfilled', () => {
+ it('should return other with fulfilled', () => {
const expected = {}
const p = fulfill(expected)
return assert.strictEqual(delay(10).concat(p), p)
})
- it('should behave like rejected', () => {
+ it('should return other with rejected', () => {
const expected = {}
const p = reject(expected)
silenceError(p)
return assert.strictEqual(delay(10).concat(p), p)
})
- it('should behave like never', () => {
+ it('should be identity with never', () => {
const p2 = never()
const p1 = delay(10)
return assert.strictEqual(p1.concat(p2), p1)
diff --git a/test/coroutine-test.js b/test/coroutine-test.js
index cd88b5e..d260868 100644
--- a/test/coroutine-test.js
+++ b/test/coroutine-test.js
@@ -1,5 +1,6 @@
import { describe, it } from 'mocha'
-import { fulfill, reject, delay, coroutine } from '../src/main'
+import { coroutine, fulfill, reject, delay, all, isCancelled, CancelToken } from '../src/main'
+import { assertSame } from './lib/test-util'
import assert from 'assert'
describe('coroutine', function () {
@@ -43,4 +44,210 @@ describe('coroutine', function () {
return f(expected)
.then(assert.ifError, e => assert.strictEqual(e, expected))
})
+
+ describe('cancellation', () => {
+ it('should receive a token cancelled outside', () => {
+ let executed = false
+ const f = coroutine(function* (token) {
+ coroutine.cancel = token
+ yield delay(5)
+ executed = true
+ return 1
+ })
+ const {token, cancel} = CancelToken.source()
+ const expected = {}
+ delay(3, expected).then(cancel)
+ return f(token).then(assert.ifError, x => {
+ assert.strictEqual(x, expected)
+ return delay(5).then(() => assert(!executed))
+ })
+ })
+
+ it('should execute finally but not catch statements', () => {
+ let executedT = false
+ let executedC = false
+ let executedF = false
+ const f = coroutine(function* (token) {
+ coroutine.cancel = token
+ try {
+ yield delay(5)
+ executedT = true
+ } catch (e) {
+ executedC = true
+ } finally {
+ executedF = true
+ }
+ return 1
+ })
+ const {token, cancel} = CancelToken.source()
+ f(token)
+ return delay(3, {})
+ .then(cancel)
+ .then(all)
+ .then(() => {
+ assert(!executedT, 'after yield')
+ assert(!executedC, 'catch block')
+ assert(executedF, 'finally block')
+ })
+ })
+
+ it('should wait on yields in finally statements', () => {
+ let executed = false
+ const f = coroutine(function* (token) {
+ coroutine.cancel = token
+ try {
+ yield delay(5)
+ } finally {
+ yield delay(2)
+ executed = true
+ }
+ return 1
+ })
+ const {token, cancel} = CancelToken.source()
+ const p = f(token)
+ const d = delay(3, {}).then(() => {
+ cancel()
+ assert(isCancelled(p))
+ assert(!executed, 'at yield')
+ })
+ return delay(5, d).then(() => assert(executed, 'after yield in finally block'))
+ })
+
+ it('should receive a token cancelled inside', () => {
+ const expected = {}
+ let rejected = false
+ let executedT = false
+ let executedF = false
+ const f = coroutine(function* () {
+ const {token, cancel} = CancelToken.source()
+ coroutine.cancel = token
+ yield delay(1)
+ try {
+ cancel(expected)
+ rejected = isCancelled(p)
+ yield
+ executedT = true
+ } finally {
+ executedF = true
+ }
+ })
+ const p = f()
+ return p.then(assert.ifError, x => {
+ assert(rejected, 'immediately rejected')
+ assert.strictEqual(x, expected)
+ assert(!executedT, 'after yield')
+ assert(executedF, 'finally block')
+ })
+ })
+
+ it('should cancel when receiving a cancelled token', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ const f = coroutine(function* () {
+ coroutine.cancel = token
+ })
+ return assertSame(f(), token.getCancelled())
+ })
+
+ it('should not cancel when the last received token is not cancelled', () => {
+ return coroutine(function* () {
+ const {token, cancel} = CancelToken.source()
+ coroutine.cancel = token
+ yield
+ coroutine.cancel = null
+ cancel({})
+ return 1
+ })().then(x => assert.strictEqual(x, 1))
+ })
+
+ it('should work for recursive coroutines', () => {
+ let counter = 0
+ const f = coroutine(function* (token) {
+ coroutine.cancel = token
+ yield delay(1)
+ try {
+ counter++
+ yield f(token)
+ } finally {
+ counter--
+ }
+ })
+ const {token, cancel} = CancelToken.source()
+ const p = f(token)
+ return delay(15).then(() => {
+ const res = cancel({})
+ assert(isCancelled(p))
+ return all(res)
+ }).then(() => {
+ assert.strictEqual(counter, 0)
+ })
+ })
+ })
+
+ describe('coroutine.cancel', () => {
+ it('should always return the same token', () => {
+ return coroutine(function* () {
+ const token = coroutine.cancel
+ assert.strictEqual(coroutine.cancel, token)
+ coroutine.cancel = CancelToken.empty()
+ assert.strictEqual(coroutine.cancel, token)
+ coroutine.cancel = null
+ assert.strictEqual(coroutine.cancel, token)
+ coroutine.cancel = coroutine.cancel
+ assert.strictEqual(coroutine.cancel, token)
+ })()
+ })
+
+ it('should return the token of the result promise', () => {
+ const p = coroutine(function* () {
+ yield delay(1)
+ assert.strictEqual(coroutine.cancel, p.token)
+ })()
+ return p
+ })
+
+ it('should not be available outside a coroutine', () => {
+ assert.throws(() => coroutine.cancel, SyntaxError)
+ assert.throws(() => { coroutine.cancel = null }, SyntaxError)
+ })
+
+ it('should not be available in finally blocks after cancellation', () => {
+ let err
+ const p = coroutine(function* () {
+ const {token, cancel} = CancelToken.source()
+ coroutine.cancel = token
+ try {
+ yield delay(1)
+ yield cancel()
+ } finally {
+ try {
+ assert(isCancelled(p))
+ assert.throws(() => coroutine.cancel, ReferenceError)
+ assert.throws(() => { coroutine.cancel = null }, ReferenceError)
+ } catch (e) {
+ err = e
+ }
+ }
+ })()
+ return p.then(assert.ifError, () => assert.ifError(err))
+ })
+
+ it('should throw when assigned to after cancellation', () => {
+ let err
+ const p = coroutine(function* () {
+ const {token, cancel} = CancelToken.source()
+ coroutine.cancel = token
+ yield delay(1)
+ cancel()
+ try {
+ assert(isCancelled(p))
+ assert.throws(() => { coroutine.cancel = null }, ReferenceError)
+ assert.throws(() => { coroutine.cancel = CancelToken.empty() }, ReferenceError)
+ } catch (e) {
+ err = e
+ }
+ })()
+ return p.then(assert.ifError, () => assert.ifError(err))
+ })
+ })
})
diff --git a/test/delay-test.js b/test/delay-test.js
index 52770be..2772536 100644
--- a/test/delay-test.js
+++ b/test/delay-test.js
@@ -1,7 +1,6 @@
import { describe, it } from 'mocha'
-import { delay } from '../src/main'
-import { Future, never, reject, fulfill } from '../src/Promise'
-import { silenceError, isNever, isPending } from '../src/inspect'
+import { delay, never, reject, fulfill, isCancelled, isNever, isPending, CancelToken } from '../src/main'
+import { Future, silenceError } from '../src/Promise'
import { assertSame } from './lib/test-util'
import assert from 'assert'
@@ -54,4 +53,64 @@ describe('delay', function () {
return assertSame(fulfill(x), p)
.then(() => assert(lte(t, Date.now() - now)))
})
+
+ it('should delay fulfilled when never cancelled', () => {
+ const x = {}
+ const t = 10
+ const p = delay(t, fulfill(x), CancelToken.empty())
+
+ const now = Date.now()
+ return assertSame(fulfill(x), p)
+ .then(() => assert(lte(t, Date.now() - now)))
+ })
+
+ it('should return cancellation with cancelled token for fulfill', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ const p = delay(10, fulfill(1), token)
+ assert.strictEqual(token.getCancelled(), p)
+ })
+
+ it('should return cancellation with cancelled token for reject', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ const p = delay(10, reject(1), token)
+ assert.strictEqual(token.getCancelled(), p)
+ })
+
+ it('should behave like cancellation when cancelled for never', () => {
+ const {token, cancel} = CancelToken.source()
+ const p = delay(10, never(), token)
+ cancel({})
+ assert(isCancelled(p))
+ return assertSame(token.getCancelled(), p)
+ })
+
+ it('should behave like cancellation when cancelled', () => {
+ const {token, cancel} = CancelToken.source()
+ const p = delay(10, fulfill(1), token)
+ cancel({})
+ assert(isCancelled(p))
+ return assertSame(token.getCancelled(), p)
+ })
+
+ it('should behave like cancellation when cancelled during delay', () => {
+ const {token, cancel} = CancelToken.source()
+ const p = delay(10, fulfill(1), token)
+ return delay(5).then(() => {
+ cancel({})
+ assert(isCancelled(p))
+ return assertSame(token.getCancelled(), p)
+ })
+ })
+
+ it('should behave like cancellation when cancelled before fulfill', () => {
+ const {token, cancel} = CancelToken.source()
+ const p = delay(5, delay(10), token)
+ return delay(5).then(() => {
+ cancel({})
+ assert(isCancelled(p))
+ return assertSame(token.getCancelled(), p)
+ })
+ })
})
diff --git a/test/empty-test.js b/test/empty-test.js
index 107b2c0..0508829 100644
--- a/test/empty-test.js
+++ b/test/empty-test.js
@@ -1,10 +1,9 @@
import { describe, it } from 'mocha'
-import { Future } from '../src/Promise'
-import { isNever } from '../src/inspect'
+import { Promise, isNever } from '../src/main'
import assert from 'assert'
describe('empty', function () {
it('should return never', () => {
- assert(isNever(Future.empty()))
+ assert(isNever(Promise.empty()))
})
})
diff --git a/test/finally-test.js b/test/finally-test.js
new file mode 100644
index 0000000..4d82bdf
--- /dev/null
+++ b/test/finally-test.js
@@ -0,0 +1,183 @@
+import { describe, it } from 'mocha'
+import { future, fulfill, reject, delay, CancelToken } from '../src/main'
+import { assertSame } from './lib/test-util'
+import assert from 'assert'
+
+describe('finally', () => {
+ it('should throw when f is not a function', () => {
+ const p = fulfill()
+ assert.throws(() => p.finally(), TypeError)
+ assert.throws(() => p.finally(''), TypeError)
+ assert.throws(() => p.finally(1), TypeError)
+ assert.throws(() => p.finally(false), TypeError)
+ })
+
+ it('should call f for fulfill', () => {
+ let called = false
+ return fulfill().finally(() => {
+ called = true
+ }).then(() => {
+ assert(called)
+ })
+ })
+
+ it('should call f for reject', () => {
+ let called = false
+ return reject().finally(() => {
+ called = true
+ }).then(assert.ifError, () => {
+ assert(called)
+ })
+ })
+
+ it('should call f for future fulfill', () => {
+ let called = false
+ const {promise, resolve} = future()
+ const p = promise.finally(() => {
+ called = true
+ }).then(assert.ifError, () => {
+ assert(called)
+ })
+ resolve(fulfill())
+ return p
+ })
+
+ it('should call f for future reject', () => {
+ let called = false
+ const {promise, resolve} = future()
+ const p = promise.finally(() => {
+ called = true
+ }).then(assert.ifError, () => {
+ assert(called)
+ })
+ resolve(reject())
+ return p
+ })
+
+ it('should call f with uncancelled token for future fulfill', () => {
+ let called = false
+ const {promise, resolve} = future(CancelToken.empty())
+ const p = promise.finally(() => {
+ called = true
+ }).then(() => {
+ assert(called)
+ })
+ resolve(fulfill())
+ return p
+ })
+
+ it('should call f with uncancelled token for already fulfilled future', () => {
+ let called = false
+ const {promise, resolve} = future(CancelToken.empty())
+ resolve(fulfill())
+ return promise.finally(() => {
+ called = true
+ }).then(() => {
+ assert(called)
+ })
+ })
+
+ it('should call f for already cancelled future', () => {
+ let called = false
+ const {token, cancel} = CancelToken.source()
+ cancel()
+ const {promise} = future(token)
+ return promise.finally(() => {
+ called = true
+ }).trifurcate(assert.ifError, assert.ifError, () => {
+ assert(called)
+ })
+ })
+
+ describe('cancel', () => {
+ it('should call f asynchronously', () => {
+ let called = false
+ const {token, cancel} = CancelToken.source()
+ const p = delay(1, null, token).finally(() => {
+ called = true
+ })
+ cancel()
+ assert(!called)
+ return p.trifurcate(assert.ifError, assert.ifError, () => {
+ assert(called)
+ })
+ })
+
+ it('should return fulfilled callback result', () => {
+ const expected = fulfill({})
+ const reason = new Error('cancelled')
+ const {token, cancel} = CancelToken.source()
+ const p = delay(1, null, token).finally(() => {
+ return expected
+ }).trifurcate(assert.ifError, assert.ifError, e => {
+ assert.strictEqual(e, reason)
+ return assertSame(c[0], expected)
+ })
+ const c = cancel(reason)
+ return p
+ })
+
+ it('should return callback exception', () => {
+ const expected = {}
+ const {token, cancel} = CancelToken.source()
+ const p = delay(1, null, token).finally(() => {
+ throw expected
+ }).trifurcate(assert.ifError, assert.ifError, () => {
+ return assertSame(c[0], reject(expected))
+ })
+ const c = cancel()
+ return p
+ })
+
+ it('should cancel result during f call for fulfilled future', () => {
+ const reason = {}
+ const {token, cancel} = CancelToken.source()
+ let c
+ return delay(1, null, token).finally(() => {
+ c = cancel(reason)
+ }).trifurcate(assert.ifError, assert.ifError, e => {
+ assert.strictEqual(e, reason)
+ assert.strictEqual(c.length, 1)
+ })
+ })
+ })
+
+ describe('return value', () => {
+ it('should behave like input for fulfill', () => {
+ const p = fulfill({})
+ return assertSame(p.finally(() => {}), p)
+ })
+
+ it('should behave like input for reject', () => {
+ const p = reject({})
+ return assertSame(p.finally(() => {}), p)
+ })
+
+ it('should not resolve before the callback result', () => {
+ let called = false
+ const expected = {}
+ return fulfill(expected).finally(() => {
+ return delay(3).then(() => { called = true })
+ }).then(x => {
+ assert.strictEqual(x, expected)
+ assert(called)
+ })
+ })
+
+ it('should behave like rejection for throwing callback', () => {
+ const expected = {}
+ return fulfill().finally(() => {
+ throw expected
+ }).then(assert.ifError, e => {
+ assert.strictEqual(e, expected)
+ })
+ })
+
+ it('should behave like rejection for rejecting callback', () => {
+ const p = reject({})
+ return assertSame(p, fulfill().finally(() => {
+ return p
+ }))
+ })
+ })
+})
diff --git a/test/fulfill-test.js b/test/fulfill-test.js
index ad5d11d..3d34565 100644
--- a/test/fulfill-test.js
+++ b/test/fulfill-test.js
@@ -1,6 +1,7 @@
import { describe, it } from 'mocha'
-import { fulfill, reject } from '../src/main'
-import { silenceError, getValue } from '../src/inspect'
+import { fulfill, reject, getValue, CancelToken } from '../src/main'
+import { silenceError } from '../src/Promise'
+import { assertSame } from './lib/test-util'
import assert from 'assert'
describe('fulfill', () => {
@@ -25,13 +26,63 @@ describe('fulfill', () => {
return fulfill(x).then(y => assert(x === y))
})
+ it('then should be identity without f callback', () => {
+ const p = fulfill(true)
+ assert.strictEqual(p, p.then())
+ })
+
+ it('then with uncancelled token should be identity without f callback', () => {
+ const p = fulfill(true)
+ assert.strictEqual(p, p.then(null, null, CancelToken.empty()))
+ })
+
+ it('then with cancelled token should behave like cancellation', () => {
+ const p = fulfill(true)
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return assertSame(token.getCancelled(), p.then(assert.ifError, assert.ifError, token))
+ })
+
it('catch should be identity', () => {
const p = fulfill(true)
assert.strictEqual(p, p.catch(assert.ifError))
})
- it('then should be identity when typeof f !== function', () => {
+ it('catch with uncancelled token should be identity', () => {
const p = fulfill(true)
- assert.strictEqual(p, p.then())
+ assert.strictEqual(p, p.catch(assert.ifError, CancelToken.empty()))
+ })
+
+ it('catch with cancelled token should behave like cancellation', () => {
+ const p = fulfill(true)
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return assertSame(token.getCancelled(), p.catch(assert.ifError, token))
+ })
+
+ it('map with cancelled token should behave like cancellation', () => {
+ const p = fulfill(true)
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return assertSame(token.getCancelled(), p.map(assert.ifError, token))
+ })
+
+ it('ap with cancelled token should behave like cancellation', () => {
+ const p = fulfill(assert.ifError)
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return assertSame(token.getCancelled(), p.ap(fulfill(true), token))
+ })
+
+ it('chain with cancelled token should behave like cancellation', () => {
+ const p = fulfill(true)
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return assertSame(token.getCancelled(), p.chain(assert.ifError, token))
+ })
+
+ it('trifurcate should be identity without f callback', () => {
+ const p = fulfill(true)
+ assert.strictEqual(p, p.trifurcate(undefined, assert.ifError, assert.ifError))
})
})
diff --git a/test/future-test.js b/test/future-test.js
index 7691849..3b272b5 100644
--- a/test/future-test.js
+++ b/test/future-test.js
@@ -1,11 +1,13 @@
import { describe, it } from 'mocha'
-import { future, reject, fulfill, never, Future } from '../src/Promise'
-import { silenceError } from '../src/inspect'
+import { future, reject, fulfill, isSettled, isPending, never, CancelToken } from '../src/main'
+import { Future, cancel, silenceError } from '../src/Promise'
import { assertSame } from './lib/test-util'
import assert from 'assert'
+const silenced = p => (silenceError(p), p)
const f = x => x + 1
const fp = x => fulfill(x + 1)
+const rp = x => silenced(reject(x))
describe('future', () => {
it('should return { resolve, promise }', () => {
@@ -23,6 +25,64 @@ describe('future', () => {
})
})
+ describe('state', () => {
+ it('should not change after resolution', () => {
+ const { resolve, promise } = future()
+ resolve(fulfill())
+ const expected = promise.state()
+ resolve(fulfill())
+ assert.strictEqual(promise.state(), expected)
+ resolve(reject())
+ assert.strictEqual(promise.state(), expected)
+ })
+
+ it('should not change after resolution with future', () => {
+ const { resolve, promise } = future()
+ const f = future()
+ resolve(f.promise)
+ const expected = promise.state()
+ resolve(fulfill())
+ assert.strictEqual(promise.state(), expected)
+ resolve(reject())
+ assert.strictEqual(promise.state(), expected)
+
+ f.resolve(reject())
+ const rejected = promise.state()
+ resolve(fulfill())
+ assert.strictEqual(promise.state(), rejected)
+ resolve(reject())
+ assert.strictEqual(promise.state(), rejected)
+ })
+
+ it('should not change with token after resolution', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ resolve(reject())
+ const expected = promise.state()
+ resolve(fulfill())
+ assert.strictEqual(promise.state(), expected)
+ resolve(reject())
+ assert.strictEqual(promise.state(), expected)
+ })
+
+ it('should not change with token after resolution with future', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const f = future()
+ resolve(f.promise)
+ const expected = promise.state()
+ resolve(fulfill())
+ assert.strictEqual(promise.state(), expected)
+ resolve(reject())
+ assert.strictEqual(promise.state(), expected)
+
+ f.resolve(fulfill())
+ const fulfilled = promise.state()
+ resolve(fulfill())
+ assert.strictEqual(promise.state(), fulfilled)
+ resolve(reject())
+ assert.strictEqual(promise.state(), fulfilled)
+ })
+ })
+
describe('resolve', () => {
it('should fulfill promise with value', () => {
const { resolve, promise } = future()
@@ -44,83 +104,130 @@ describe('future', () => {
resolve(reject(expected))
return promise.then(assert.ifError, x => assert.strictEqual(expected, x))
})
+
+ it('should reject for cancelled promise', () => {
+ const { resolve, promise } = future()
+ const expected = {}
+ resolve(cancel(expected))
+ return promise.trifurcate(assert.ifError, e => assert.strictEqual(e, expected), assert.ifError)
+ })
})
describe('when resolved to another promise', () => {
describe('state', () => {
it('should have fulfilled state', () => {
const { resolve, promise } = future()
-
const p = fulfill(1)
resolve(p)
- assert.equal(p.state(), promise.state())
+ assert.strictEqual(p.state(), promise.state())
})
it('should have rejected state', () => {
const { resolve, promise } = future()
-
- const p = reject(1)
- silenceError(p)
+ const p = silenced(reject(1))
resolve(p)
- assert.equal(p.state(), promise.state())
+ assert.strictEqual(p.state(), promise.state())
})
it('should have never state', () => {
const { resolve, promise } = future()
-
const p = never()
resolve(p)
- assert.equal(p.state(), promise.state())
+ assert.strictEqual(p.state(), promise.state())
})
})
describe('inspect', () => {
it('should have fulfilled state', () => {
const { resolve, promise } = future()
-
const p = fulfill(1)
resolve(p)
- assert.equal(p.inspect(), promise.inspect())
+ assert.strictEqual(p.inspect(), promise.inspect())
})
it('should have rejected state', () => {
const { resolve, promise } = future()
-
- const p = reject(1)
- silenceError(p)
+ const p = silenced(reject(1))
resolve(p)
- assert.equal(p.inspect(), promise.inspect())
+ assert.strictEqual(p.inspect(), promise.inspect())
})
it('should have never state', () => {
const { resolve, promise } = future()
+ const p = never()
+ resolve(p)
+ assert.strictEqual(p.inspect(), promise.inspect())
+ })
+ })
+
+ describe('then', () => {
+ it('should behave like mapped for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ resolve(p)
+ return assertSame(p.map(f), promise.then(f))
+ })
+
+ it('should behave like chained for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ resolve(p)
+ return assertSame(p.chain(fp), promise.then(fp))
+ })
+
+ it('should behave like rejection chained for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ resolve(p)
+ return assertSame(p.chain(rp), promise.then(rp))
+ })
+ it('should be identity for reject', () => {
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
+ resolve(p)
+ assert.strictEqual(p, promise.then(f))
+ })
+
+ it('should be identity for never', () => {
+ const { resolve, promise } = future()
const p = never()
resolve(p)
- assert.equal(p.inspect(), promise.inspect())
+ assert.strictEqual(p, promise.then(f))
})
})
describe('catch', () => {
- it('should behave like fulfilled', () => {
+ it('should be identity for fulfill', () => {
const { resolve, promise } = future()
-
const p = fulfill(1)
resolve(p)
assert.strictEqual(p, promise.catch(f))
})
- it('should have rejected state', () => {
+ it('should behave like mapped for reject', () => {
const { resolve, promise } = future()
-
const p = reject(1)
resolve(p)
return assertSame(p.catch(f), promise.catch(f))
})
- it('should have never state', () => {
+ it('should behave like chained for reject', () => {
const { resolve, promise } = future()
+ const p = reject(1)
+ resolve(p)
+ return assertSame(p.catch(fp), promise.catch(fp))
+ })
+ it('should behave like rejection chained for reject', () => {
+ const { resolve, promise } = future()
+ const p = reject(1)
+ resolve(p)
+ return assertSame(p.catch(rp), promise.catch(rp))
+ })
+
+ it('should be identity for never', () => {
+ const { resolve, promise } = future()
const p = never()
resolve(p)
assert.strictEqual(p, promise.catch(f))
@@ -128,26 +235,22 @@ describe('future', () => {
})
describe('map', () => {
- it('should behave like fulfilled', () => {
+ it('should behave like mapped for fulfill', () => {
const { resolve, promise } = future()
-
const p = fulfill(1)
resolve(p)
return assertSame(p.map(f), promise.map(f))
})
- it('should have rejected state', () => {
+ it('should be identity for reject', () => {
const { resolve, promise } = future()
-
- const p = reject(1)
- silenceError(p)
+ const p = silenced(reject(1))
resolve(p)
assert.strictEqual(p, promise.map(f))
})
- it('should have never state', () => {
+ it('should be identity for never', () => {
const { resolve, promise } = future()
-
const p = never()
resolve(p)
assert.strictEqual(p, promise.map(f))
@@ -155,26 +258,29 @@ describe('future', () => {
})
describe('chain', () => {
- it('should behave like fulfilled', () => {
+ it('should behave like chained for fulfill', () => {
const { resolve, promise } = future()
-
const p = fulfill(1)
resolve(p)
return assertSame(p.chain(fp), promise.chain(fp))
})
- it('should have rejected state', () => {
+ it('should behave like rejection chained for fulfill', () => {
const { resolve, promise } = future()
+ const p = fulfill(1)
+ resolve(p)
+ return assertSame(p.chain(rp), promise.chain(rp))
+ })
- const p = reject(1)
- silenceError(p)
+ it('should be identity for reject', () => {
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
resolve(p)
assert.strictEqual(p, promise.chain(fp))
})
- it('should have never state', () => {
+ it('should be identity for never', () => {
const { resolve, promise } = future()
-
const p = never()
resolve(p)
assert.strictEqual(p, promise.chain(fp))
@@ -182,27 +288,23 @@ describe('future', () => {
})
describe('ap', () => {
- it('should behave like fulfilled', () => {
+ it('should behave like apply for fulfill', () => {
const { resolve, promise } = future()
-
const p = fulfill(f)
const q = fulfill(1)
resolve(p)
return assertSame(p.ap(q), promise.ap(q))
})
- it('should behave like rejected', () => {
+ it('should be identity for reject', () => {
const { resolve, promise } = future()
-
- const p = reject(f)
- silenceError(p)
+ const p = silenced(reject(f))
resolve(p)
assert.strictEqual(p, promise.ap(fulfill(1)))
})
- it('should behave like never', () => {
+ it('should be identity for never', () => {
const { resolve, promise } = future()
-
const p = never()
resolve(p)
return assert.strictEqual(p, promise.ap(fulfill(1)))
@@ -210,36 +312,267 @@ describe('future', () => {
})
describe('concat', () => {
- it('should behave like fulfilled', () => {
+ it('should be identity for fulfill', () => {
const { resolve, promise } = future()
-
const p1 = fulfill(1)
const p2 = fulfill(2)
-
resolve(p1)
- return assertSame(p1.concat(p2), promise.concat(p2))
+ assert.strictEqual(p1, promise.concat(p2))
})
- it('should behave like rejected', () => {
+ it('should be identity for reject', () => {
const { resolve, promise } = future()
+ const p1 = silenced(reject(new Error()))
+ const p2 = silenced(reject(new Error()))
+ resolve(p1)
+ assert.strictEqual(p1, promise.concat(p2))
+ })
- const p1 = reject(new Error())
- const p2 = reject(new Error())
- silenceError(p1)
- silenceError(p2)
-
+ it('should return other for never', () => {
+ const { resolve, promise } = future()
+ const p1 = never()
+ const p2 = fulfill(2)
resolve(p1)
assert.strictEqual(p1.concat(p2), promise.concat(p2))
})
+ })
+ })
+
+ describe('before being resolved to another promise', () => {
+ describe('state', () => {
+ it('should be pending', () => {
+ const { promise } = future()
+ assert(isPending(promise))
+ })
+
+ it('should not be settled', () => {
+ const { promise } = future()
+ assert(!isSettled(promise))
+ })
+ })
+
+ describe('inspect', () => {
+ it('should not be fulfilled', () => {
+ const { promise } = future()
+ assert.notStrictEqual(fulfill().inspect(), promise.inspect())
+ })
- it('should behave like never', () => {
+ it('should not be rejected', () => {
+ const { promise } = future()
+ assert.notStrictEqual(silenced(reject()).inspect(), promise.inspect())
+ })
+ })
+
+ describe('then', () => {
+ it('should behave like mapped for fulfill', () => {
const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.then(f)
+ resolve(p)
+ return assertSame(p.map(f), res)
+ })
- const p1 = never()
- const p2 = fulfill(2)
+ it('should behave like chained for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.then(fp)
+ resolve(p)
+ return assertSame(p.chain(fp), res)
+ })
- resolve(p1)
- return assertSame(p1.concat(p2), promise.concat(p2))
+ it('should behave like rejection chained for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.then(rp)
+ resolve(p)
+ return assertSame(p.chain(rp), res)
+ })
+
+ it('should behave like rejected for reject', () => {
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
+ const res = promise.then(f)
+ resolve(p)
+ return assertSame(p, res)
+ })
+
+ /* it('should have never state for never', () => {
+ const { resolve, promise } = future()
+ const p = never()
+ const res = promise.then(f)
+ resolve(p)
+ assert(isNever(res))
+ }) */
+ })
+
+ describe('catch', () => {
+ it('should behave like fulfilled for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.catch(f)
+ resolve(p)
+ return assertSame(p, res)
+ })
+
+ it('should behave like mapped for reject', () => {
+ const { resolve, promise } = future()
+ const p = reject(1)
+ const res = promise.catch(f)
+ resolve(p)
+ return assertSame(p.catch(f), res)
+ })
+
+ it('should behave like chained for reject', () => {
+ const { resolve, promise } = future()
+ const p = reject(1)
+ const res = promise.catch(fp)
+ resolve(p)
+ return assertSame(p.catch(fp), res)
+ })
+
+ it('should behave like rejection chained for reject', () => {
+ const { resolve, promise } = future()
+ const p = reject(1)
+ const res = promise.catch(rp)
+ resolve(p)
+ return assertSame(p.catch(rp), res)
+ })
+
+ /* it('should have never state for never', () => {
+ const { resolve, promise } = future()
+ const p = never()
+ const res = promise.catch(f)
+ resolve(p)
+ assert(isNever(res))
+ }) */
+ })
+
+ describe('map', () => {
+ it('should behave like mapped for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.map(f)
+ resolve(p)
+ return assertSame(p.map(f), res)
+ })
+
+ it('should behave like rejection for reject', () => {
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
+ const res = promise.map(f)
+ resolve(p)
+ return assertSame(p, res)
+ })
+
+ /* it('should have never state for never', () => {
+ const { resolve, promise } = future()
+ const p = never()
+ const res = promise.map(f)
+ resolve(p)
+ assert(isNever(res))
+ }) */
+ })
+
+ describe('ap', () => {
+ it('should behave like apply for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(f)
+ const q = fulfill(1)
+ const res = promise.ap(q)
+ resolve(p)
+ return assertSame(p.ap(q), res)
+ })
+
+ it('should behave like rejected for reject', () => {
+ const { resolve, promise } = future()
+ const p = silenced(reject(f))
+ const res = promise.ap(fulfill(1))
+ resolve(p)
+ return assertSame(p, res)
+ })
+
+ /* it('should have never state for never', () => {
+ const { resolve, promise } = future()
+ const p = never()
+ const res = promise.ap(fulfill(1))
+ resolve(p)
+ assert(isNever(res))
+ }) */
+ })
+
+ describe('chain', () => {
+ it('should behave like chained for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.chain(fp)
+ resolve(p)
+ return assertSame(p.chain(fp), res)
+ })
+
+ it('should behave like rejection chained for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(1)
+ const res = promise.chain(rp)
+ resolve(p)
+ return assertSame(p.chain(rp), res)
+ })
+
+ it('should behave like rejected for reject', () => {
+ const { resolve, promise } = future()
+ const p = silenced(reject(1))
+ const res = promise.chain(fp)
+ resolve(p)
+ return assertSame(p, res)
+ })
+
+ /* it('should have never state for never', () => {
+ const { resolve, promise } = future()
+ const p = never()
+ const res = promise.chain(fp)
+ resolve(p)
+ assert(isNever(res))
+ }) */
+ })
+
+ describe('concat', () => {
+ it('should behave like fulfilled other for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(2)
+ const res = promise.concat(p)
+ resolve(fulfill(1))
+ return assertSame(p, res)
+ })
+
+ it('should behave like rejected other for fulfill', () => {
+ const { resolve, promise } = future()
+ const p = silenced(reject(2))
+ const res = promise.concat(p)
+ resolve(fulfill(1))
+ return assertSame(p, res)
+ })
+
+ it('should behave like fulfilled other for reject', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(2)
+ const res = promise.concat(p)
+ resolve(silenced(reject(1)))
+ return assertSame(p, res)
+ })
+
+ it('should behave like rejected other for reject', () => {
+ const { resolve, promise } = future()
+ const p = silenced(reject(2))
+ const res = promise.concat(p)
+ resolve(silenced(reject(1)))
+ return assertSame(p, res)
+ })
+
+ it('should behave like other for never', () => {
+ const { resolve, promise } = future()
+ const p = fulfill(2)
+ const res = promise.concat(p)
+ resolve(never())
+ return assertSame(p, res)
})
})
})
diff --git a/test/inspect-test.js b/test/inspect-test.js
index c7af240..01d7343 100644
--- a/test/inspect-test.js
+++ b/test/inspect-test.js
@@ -1,6 +1,7 @@
import { describe, it } from 'mocha'
-import { isFulfilled, isRejected, isSettled, isPending, isHandled, isNever, silenceError, getValue, getReason } from '../src/inspect'
-import { resolve, reject, fulfill, never, Future } from '../src/Promise'
+import { resolve, reject, fulfill, never } from '../src/main'
+import { isFulfilled, isRejected, isCancelled, isSettled, isPending, isNever, getValue, getReason, isHandled } from '../src/inspect'
+import { Future, cancel, silenceError } from '../src/Promise'
import assert from 'assert'
describe('inspect', () => {
@@ -13,6 +14,10 @@ describe('inspect', () => {
assert(!isFulfilled(reject()))
})
+ it('should be false for cancelled promise', () => {
+ assert(!isFulfilled(cancel()))
+ })
+
it('should be false for pending promise', () => {
assert(!isFulfilled(new Future()))
})
@@ -27,6 +32,10 @@ describe('inspect', () => {
assert(isRejected(reject()))
})
+ it('should be true for cancelled promise', () => {
+ assert(isRejected(cancel()))
+ })
+
it('should be false for fulfilled promise', () => {
assert(!isRejected(resolve()))
})
@@ -40,6 +49,28 @@ describe('inspect', () => {
})
})
+ describe('isCancelled', () => {
+ it('should be true for cancelled promise', () => {
+ assert(isCancelled(cancel()))
+ })
+
+ it('should be false for fulfilled promise', () => {
+ assert(!isCancelled(resolve()))
+ })
+
+ it('should be false for rejected promise', () => {
+ assert(!isCancelled(reject()))
+ })
+
+ it('should be false for pending promise', () => {
+ assert(!isCancelled(new Future()))
+ })
+
+ it('should be false for never', () => {
+ assert(!isCancelled(never()))
+ })
+ })
+
describe('isSettled', () => {
it('should be true for fulfilled promise', () => {
assert(isSettled(resolve()))
@@ -49,6 +80,10 @@ describe('inspect', () => {
assert(isSettled(reject()))
})
+ it('should be true for cancelled promise', () => {
+ assert(isSettled(cancel()))
+ })
+
it('should be false for pending promise', () => {
assert(!isSettled(new Future()))
})
@@ -67,6 +102,10 @@ describe('inspect', () => {
assert(!isPending(reject()))
})
+ it('should be false for cancelled promise', () => {
+ assert(!isPending(cancel()))
+ })
+
it('should be true for pending promise', () => {
assert(isPending(new Future()))
})
@@ -85,6 +124,10 @@ describe('inspect', () => {
assert(!isNever(reject()))
})
+ it('should be false for cancelled promise', () => {
+ assert(!isNever(cancel()))
+ })
+
it('should be false for pending promise', () => {
assert(!isNever(new Future()))
})
@@ -118,6 +161,10 @@ describe('inspect', () => {
assert(isHandled(p))
})
+ it('should be true for cancelled promise', () => {
+ assert(isHandled(cancel()))
+ })
+
it('should be false for pending promise', () => {
assert(!isHandled(new Future()))
})
@@ -137,6 +184,10 @@ describe('inspect', () => {
assert.throws(() => getValue(reject()))
})
+ it('should throw for cancelled promise', () => {
+ assert.throws(() => getValue(cancel()))
+ })
+
it('should throw for pending promise', () => {
assert.throws(() => getValue(new Future()))
})
@@ -160,6 +211,11 @@ describe('inspect', () => {
assert.strictEqual(x, getReason(reject(x)))
})
+ it('should get reason from cancelled promise', () => {
+ let x = {}
+ assert.strictEqual(x, getReason(cancel(x)))
+ })
+
it('should throw for fulfilled promise', () => {
assert.throws(() => getReason(fulfill()))
})
diff --git a/test/iterable-test.js b/test/iterable-test.js
index 083f9b9..4479347 100644
--- a/test/iterable-test.js
+++ b/test/iterable-test.js
@@ -1,6 +1,6 @@
import { describe, it } from 'mocha'
-import { Future, resolve } from '../src/Promise'
import { resolveIterable } from '../src/iterable'
+import { Future } from '../src/Promise'
import { arrayIterable } from './lib/test-util'
import assert from 'assert'
@@ -14,7 +14,7 @@ describe('iterable', () => {
}
const iterable = arrayIterable([1, 2, 3])
- return resolveIterable(resolve, itemHandler, iterable, new Future())
+ return resolveIterable(itemHandler, iterable, new Future())
.then(assert.ifError, e => assert.strictEqual(error, e))
})
@@ -31,7 +31,7 @@ describe('iterable', () => {
const promise = new Future()
promise._resolve(expected)
- return resolveIterable(resolve, itemHandler, iterable, promise)
+ return resolveIterable(itemHandler, iterable, promise)
.then(x => assert.strictEqual(expected, x))
})
})
diff --git a/test/lib/test-util.js b/test/lib/test-util.js
index f050f15..086f9ca 100644
--- a/test/lib/test-util.js
+++ b/test/lib/test-util.js
@@ -3,7 +3,8 @@
import assert from 'assert'
export function assertSame (ap, bp) {
- return ap.then(a => bp.then(b => assert(a === b)))
+ return ap.then(a => bp.then(b => assert.strictEqual(a, b)),
+ a => bp.then(x => { throw x }, b => assert.strictEqual(a, b)))
}
export function throwingIterable (e) {
@@ -56,3 +57,40 @@ class ThrowingIterator {
throw e
}
}
+
+export class FakeCancelAction {
+ constructor (promise, cb) {
+ this.promise = promise
+ this.isCancelled = 0
+ this.isDestroyed = 0
+ const token = promise.token
+ if (token != null) {
+ token._subscribe(this)
+ }
+ }
+
+ destroy () {
+ this.isDestroyed++
+ this.promise = null
+ }
+
+ cancel (p) {
+ this.isCancelled++
+ if (typeof this.promise._isResolved !== 'function' || this.promise._isResolved()) {
+ this.destroy()
+ }
+ }
+}
+
+export function raceCallbacks (future) {
+ const {resolve, promise} = future()
+ return {
+ ok (x) {
+ setTimeout(resolve, 1, x) // wait for noks
+ },
+ nok (e) {
+ promise._reject(e)
+ },
+ result: promise
+ }
+}
diff --git a/test/never-test.js b/test/never-test.js
index 084f56a..82ef61d 100644
--- a/test/never-test.js
+++ b/test/never-test.js
@@ -1,33 +1,73 @@
import { describe, it } from 'mocha'
-import { never, fulfill } from '../src/main'
+import { never, fulfill, CancelToken } from '../src/main'
import assert from 'assert'
describe('never', () => {
it('then should be identity', () => {
- var p = never()
+ const p = never()
assert.strictEqual(p, p.then(assert.ifError, assert.ifError))
})
+ it('then with token should return the cancellation', () => {
+ const p = never()
+ const token = CancelToken.empty()
+ assert.strictEqual(token.getCancelled(), p.then(assert.ifError, assert.ifError, token))
+ })
+
it('catch should be identity', () => {
- var p = never()
+ const p = never()
assert.strictEqual(p, p.catch(assert.ifError))
})
+ it('catch with token should return the cancellation', () => {
+ const p = never()
+ const token = CancelToken.empty()
+ assert.strictEqual(token.getCancelled(), p.catch(assert.ifError, token))
+ })
+
it('map should be identity', () => {
- var p = never()
+ const p = never()
assert.strictEqual(p, p.map(assert.ifError))
})
+ it('map with token should return the cancellation', () => {
+ const p = never()
+ const token = CancelToken.empty()
+ assert.strictEqual(token.getCancelled(), p.map(assert.ifError, token))
+ })
+
it('ap should be identity', () => {
- var p = never()
- assert.strictEqual(p, p.ap(fulfill()))
+ const p = never()
+ assert.strictEqual(p, p.ap(fulfill(true)))
+ })
+
+ it('ap with token should return the cancellation', () => {
+ const p = never()
+ const token = CancelToken.empty()
+ assert.strictEqual(token.getCancelled(), p.ap(fulfill(true), token))
})
it('chain should be identity', () => {
- var p = never()
+ const p = never()
assert.strictEqual(p, p.chain(fulfill))
})
+ it('chain with token should return the cancellation', () => {
+ const p = never()
+ const token = CancelToken.empty()
+ assert.strictEqual(token.getCancelled(), p.chain(assert.ifError, token))
+ })
+
+ it('finally should be identity', () => {
+ const p = never()
+ assert.strictEqual(p, p.finally(() => {}))
+ })
+
+ it('trifurcate should be identity', () => {
+ const p = never()
+ assert.strictEqual(p, p.trifurcate(assert.ifError, assert.ifError, assert.ifError))
+ })
+
it('_when should not call action', () => {
let fail = () => { throw new Error('never._when called action') }
let action = {
diff --git a/test/of-test.js b/test/of-test.js
index e2bc785..c2c9098 100644
--- a/test/of-test.js
+++ b/test/of-test.js
@@ -1,27 +1,27 @@
import { describe, it } from 'mocha'
-import { Future, reject } from '../src/Promise'
-import { silenceError, getValue } from '../src/inspect'
+import { Promise, reject, getValue } from '../src/main'
+import { silenceError } from '../src/Promise'
import assert from 'assert'
-describe('fulfill', () => {
+describe('of', () => {
it('should wrap value', () => {
const x = {}
- return Future.of(x).then(y => assert(x === y))
+ return Promise.of(x).then(y => assert.strictEqual(x, y))
})
it('should be immediately fulfilled', () => {
- let x = {}
- assert.strictEqual(x, getValue(Future.of(x)))
+ const x = {}
+ assert.strictEqual(x, getValue(Promise.of(x)))
})
it('should wrap promise', () => {
- const x = Future.of({})
- return Future.of(x).then(y => assert(x === y))
+ const x = Promise.of({})
+ return Promise.of(x).then(y => assert.strictEqual(x, y))
})
it('should wrap rejected promise', () => {
const x = reject({})
silenceError(x)
- return Future.of(x).then(y => assert(x === y))
+ return Promise.of(x).then(y => assert.strictEqual(x, y))
})
})
diff --git a/test/race-test.js b/test/race-test.js
index 55448c8..4448e04 100644
--- a/test/race-test.js
+++ b/test/race-test.js
@@ -1,6 +1,5 @@
import { describe, it } from 'mocha'
-import { race, resolve, reject, never } from '../src/main'
-import { isNever } from '../src/inspect'
+import { race, resolve, reject, never, isNever } from '../src/main'
import { throwingIterable } from './lib/test-util'
import assert from 'assert'
diff --git a/test/reject-test.js b/test/reject-test.js
index 1227dd9..46d410d 100644
--- a/test/reject-test.js
+++ b/test/reject-test.js
@@ -1,30 +1,96 @@
import { describe, it } from 'mocha'
-import { fulfill, reject } from '../src/main'
-import { silenceError } from '../src/inspect'
+import { reject, fulfill, CancelToken } from '../src/main'
+import { silenceError } from '../src/Promise'
+import { assertSame } from './lib/test-util'
import assert from 'assert'
describe('reject', () => {
- it('then should be identity without f', () => {
+ it('then should be identity without r callback', () => {
const p = reject(true)
silenceError(p)
assert.strictEqual(p, p.then(assert.ifError))
})
+ it('then with uncancelled token should be identity without r callback', () => {
+ const p = reject(true)
+ silenceError(p)
+ assert.strictEqual(p, p.then(assert.ifError, null, CancelToken.empty()))
+ })
+
+ it('then with cancelled token should behave like cancellation', () => {
+ const p = reject(true)
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return assertSame(token.getCancelled(), p.then(assert.ifError, null, token))
+ })
+
+ it('catch with cancelled token should behave like cancellation', () => {
+ const p = reject(true)
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return assertSame(token.getCancelled(), p.catch(assert.ifError, token))
+ })
+
it('map should be identity', () => {
- var p = reject(true)
+ const p = reject(true)
silenceError(p)
assert.strictEqual(p, p.map(assert.ifError))
})
+ it('map with uncancelled token should be identity', () => {
+ const p = reject(true)
+ silenceError(p)
+ assert.strictEqual(p, p.map(assert.ifError, CancelToken.empty()))
+ })
+
+ it('map with cancelled token should behave like cancellation', () => {
+ const p = reject(true)
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return assertSame(token.getCancelled(), p.map(assert.ifError, token))
+ })
+
it('ap should be identity', () => {
- var p = reject(assert.ifError)
+ const p = reject(assert.ifError)
silenceError(p)
assert.strictEqual(p, p.ap(fulfill(true)))
})
+ it('ap with uncancelled token should be identity', () => {
+ const p = reject(assert.ifError)
+ silenceError(p)
+ assert.strictEqual(p, p.ap(fulfill(true), CancelToken.empty()))
+ })
+
+ it('ap with cancelled token should behave like cancellation', () => {
+ const p = reject(assert.ifError)
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return assertSame(token.getCancelled(), p.ap(fulfill(true), token))
+ })
+
it('chain should be identity', () => {
- var p = reject()
+ const p = reject()
silenceError(p)
assert.strictEqual(p, p.chain(fulfill))
})
+
+ it('chain with uncancelled token should be identity', () => {
+ const p = reject()
+ silenceError(p)
+ assert.strictEqual(p, p.chain(fulfill, CancelToken.empty()))
+ })
+
+ it('chain with cancelled token should behave like cancellation', () => {
+ const p = reject(true)
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return assertSame(token.getCancelled(), p.chain(assert.ifError, token))
+ })
+
+ it('trifurcate should be identity without r callback', () => {
+ const p = reject(true)
+ silenceError(p)
+ assert.strictEqual(p, p.trifurcate(assert.ifError, undefined, assert.ifError))
+ })
})
diff --git a/test/resolve-test.js b/test/resolve-test.js
index 8ae0e13..0710e91 100644
--- a/test/resolve-test.js
+++ b/test/resolve-test.js
@@ -1,41 +1,147 @@
import { describe, it } from 'mocha'
-import { resolve, Future } from '../src/Promise'
+import { resolve, fulfill, reject, future, isCancelled, CancelToken } from '../src/main'
+import { Future, cancel } from '../src/Promise'
+import { assertSame } from './lib/test-util'
import assert from 'assert'
describe('resolve', () => {
it('should reject promise cycle', () => {
- let p = new Future()
+ const p = new Future()
p._resolve(p)
return p.then(assert.ifError, e => assert(e instanceof TypeError))
})
+ it('should reject indirect promise cycle', () => {
+ const p1 = new Future()
+ const p2 = new Future()
+ p1._resolve(p2)
+ p2._resolve(p1)
+ return p1.then(assert.ifError, e => assert(e instanceof TypeError))
+ })
+
describe('thenables', () => {
it('should resolve fulfilled thenable', () => {
- let expected = {}
+ const expected = {}
return resolve({ then: f => f(expected) })
.then(x => assert.strictEqual(expected, x))
})
it('should resolve rejected thenable', () => {
- let expected = {}
+ const expected = {}
return resolve({ then: (f, r) => r(expected) })
.then(assert.ifError, e => assert.strictEqual(expected, e))
})
it('should reject if thenable.then throws', () => {
- let expected = {}
+ const expected = {}
return resolve({ then: () => { throw expected } })
.then(assert.ifError, e => assert.strictEqual(expected, e))
})
it('should reject if accessing thenable.then throws', () => {
- let expected = {}
- let thenable = {
+ const expected = {}
+ const thenable = {
get then () { throw expected }
}
return resolve(thenable)
.then(assert.ifError, e => assert.strictEqual(expected, e))
})
+
+ it('should receive a token', () => {
+ const {token} = CancelToken.source()
+ return resolve({
+ then: (f, r, t) => {
+ assert.strictEqual(t, token)
+ f()
+ }
+ }, token)
+ })
+
+ it('should receive the token of the future it resolves', () => {
+ const {token} = CancelToken.source()
+ const p = new Future(token)
+ p._resolve({
+ then: (f, r, t) => {
+ assert.strictEqual(t, token)
+ f()
+ }
+ })
+ return p
+ })
+ })
+
+ describe('token', () => {
+ it('should return cancellation with cancelled token for true', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ assert.strictEqual(token.getCancelled(), resolve(true, token))
+ })
+
+ it('should return cancellation with cancelled token for future', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ assert.strictEqual(token.getCancelled(), resolve(new Future(), token))
+ })
+
+ it('should return cancellation with cancelled token for fulfill', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ assert.strictEqual(token.getCancelled(), resolve(fulfill(), token))
+ })
+
+ it('should return cancellation with cancelled token for reject', () => {
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ assert.strictEqual(token.getCancelled(), resolve(reject(), token))
+ })
+
+ it('should be identity for future with same token', () => {
+ const {token} = CancelToken.source()
+ const p = new Future(token)
+ assert.strictEqual(p, resolve(p, token))
+ })
+
+ it('should cancel result for unresolved promise', () => {
+ const {token, cancel} = CancelToken.source()
+ const {promise} = future()
+ const p = resolve(promise, token)
+ cancel({})
+ assert(!isCancelled(promise))
+ assert(isCancelled(p))
+ return assertSame(token.getCancelled(), p)
+ })
+
+ it('should cancel result for unresolved promise with different token', () => {
+ const {token, cancel} = CancelToken.source()
+ const {promise} = future(CancelToken.empty())
+ const p = resolve(promise, token)
+ cancel({})
+ assert(!isCancelled(promise))
+ assert(isCancelled(p))
+ return assertSame(token.getCancelled(), p)
+ })
+ })
+
+ it('should be identity for fulfilled promise', () => {
+ const p = fulfill()
+ assert.strictEqual(resolve(p), p)
+ })
+
+ it('should be identity for rejected promise', () => {
+ const p = reject()
+ assert.strictEqual(resolve(p), p)
+ })
+
+ it('should be identity for unresolved promise', () => {
+ const p = future().promise
+ assert.strictEqual(resolve(p), p)
+ })
+
+ it('should reject for cancelled promise', () => {
+ const expected = {}
+ return resolve(cancel(expected)).trifurcate(assert.ifError, e => {
+ assert.strictEqual(e, expected)
+ }, assert.ifError)
})
})
diff --git a/test/settle-test.js b/test/settle-test.js
index 5ae8d2f..797258d 100644
--- a/test/settle-test.js
+++ b/test/settle-test.js
@@ -1,6 +1,5 @@
import { describe, it } from 'mocha'
-import { settle, resolve, reject } from '../src/main'
-import { isFulfilled, isRejected } from '../src/inspect'
+import { settle, resolve, reject, isFulfilled, isRejected } from '../src/main'
import { throwingIterable } from './lib/test-util'
import assert from 'assert'
diff --git a/test/then-test.js b/test/then-test.js
index b3a5d75..4c295d6 100644
--- a/test/then-test.js
+++ b/test/then-test.js
@@ -2,7 +2,7 @@ import { describe, it } from 'mocha'
import { delay, reject } from '../src/main'
import assert from 'assert'
-describe('map', function () {
+describe('then', function () {
it('should not change value when f is not a function', () => {
let expected = {}
return delay(1, expected).then()
@@ -26,4 +26,9 @@ describe('map', function () {
return delay(1).then(reject).then(null, () => { throw expected })
.then(assert.ifError, x => assert.strictEqual(x, expected))
})
+
+ it('should have cycle detection', () => {
+ const p = delay(1).then(() => p)
+ return p.then(assert.ifError, e => assert(e instanceof TypeError))
+ })
})
diff --git a/test/timeout-test.js b/test/timeout-test.js
index fa9f063..29e8339 100644
--- a/test/timeout-test.js
+++ b/test/timeout-test.js
@@ -1,8 +1,7 @@
import { describe, it } from 'mocha'
-import { timeout, delay } from '../src/main'
+import { reject, fulfill, timeout, delay } from '../src/main'
import TimeoutError from '../src/TimeoutError'
-import { Future, reject, fulfill } from '../src/Promise'
-import { silenceError } from '../src/inspect'
+import { Future, silenceError } from '../src/Promise'
import assert from 'assert'
function delayReject (ms, e) {
diff --git a/test/toString-test.js b/test/toString-test.js
index ef9a282..6bcd009 100644
--- a/test/toString-test.js
+++ b/test/toString-test.js
@@ -1,6 +1,6 @@
import { describe, it } from 'mocha'
-import { fulfill, reject, Future, never } from '../src/Promise'
-import { getValue, getReason } from '../src/inspect'
+import { fulfill, reject, never, getValue, getReason } from '../src/main'
+import { Future } from '../src/Promise'
import assert from 'assert'
describe('toString', () => {
diff --git a/test/trifurcate-test.js b/test/trifurcate-test.js
new file mode 100644
index 0000000..8bf221f
--- /dev/null
+++ b/test/trifurcate-test.js
@@ -0,0 +1,536 @@
+import { describe, it } from 'mocha'
+import { future, fulfill, reject, never, CancelToken, all } from '../src/main'
+import { assertSame, raceCallbacks } from './lib/test-util'
+import assert from 'assert'
+
+describe('untilCancel', () => {
+ const testResult = t => f => () => {
+ const token = t()
+ const res = f(token).untilCancel(token)
+ try {
+ assert.strictEqual(res.token, token)
+ } catch (e) {
+ assert.strictEqual(res, token.getCancelled())
+ }
+ }
+ const testUncancelled = testResult(() => CancelToken.empty())
+ const testCancelled = testResult(() => {
+ const {token, cancel} = CancelToken.source()
+ cancel({})
+ return token
+ })
+
+ describe('returns cancellation or promise with token for uncancelled token on', () => {
+ it('never', testUncancelled(() => never()))
+ it('unresolved future', testUncancelled(() => future().promise))
+ it('unresolved future with same token', testUncancelled(token => future(token).promise))
+ it('unresolved future with other token', testUncancelled(() => future(CancelToken.empty()).promise))
+ })
+
+ describe('returns cancellation or promise with token for cancelled token on', () => {
+ it('fulfill', testCancelled(fulfill))
+ it('reject', testCancelled(reject))
+ it('never', testCancelled(never))
+ it('unresolved future', testCancelled(() => future().promise))
+ it('fulfilled future', testCancelled(() => {
+ const f = future()
+ f.resolve(fulfill())
+ return f.promise
+ }))
+ it('rejected future', testCancelled(() => {
+ const f = future()
+ f.resolve(reject())
+ return f.promise
+ }))
+ it('unresolved future with token', testCancelled(token => {
+ const f = future(token)
+ return f.promise
+ }))
+ })
+})
+
+describe('trifurcate', () => {
+ const ful = () => 'f'
+ const rej = () => 'r'
+ const can = () => 'c'
+ it('should behave like then without a token', () => {
+ const a = future()
+ const b = future()
+ const c = future()
+ const d = future()
+ a.resolve(fulfill())
+ b.resolve(reject())
+ const res = all([
+ assertSame(fulfill().trifurcate(ful, rej, can), fulfill().then(ful, rej)),
+ assertSame( reject().trifurcate(ful, rej, can), reject().then(ful, rej)),
+ assertSame(a.promise.trifurcate(ful, rej, can), a.promise.then(ful, rej)),
+ assertSame(b.promise.trifurcate(ful, rej, can), b.promise.then(ful, rej)),
+ assertSame(c.promise.trifurcate(ful, rej, can), c.promise.then(ful, rej)),
+ assertSame(d.promise.trifurcate(ful, rej, can), d.promise.then(ful, rej))
+ ])
+ c.resolve(fulfill())
+ d.resolve(reject())
+ return res
+ })
+
+ const f = x => x + 1
+ const fp = x => fulfill(x + 1)
+ const rp = x => reject(x + 1)
+ const tr = x => { throw x + 1 }
+
+ describe('on fulfilled future', () => {
+ it('should only call the onfulfilled callback', () => {
+ const { ok, nok, result } = raceCallbacks(future)
+ const { resolve, promise } = future(CancelToken.empty())
+ resolve(fulfill(1))
+ promise.trifurcate(ok, nok, nok)
+ return result
+ })
+
+ it('should asynchronously call the callback', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ resolve(fulfill(1))
+ let called = false
+ const res = promise.trifurcate(() => {
+ called = true
+ })
+ assert(!called)
+ return res.then(() => assert(called))
+ })
+
+ it('should behave like the input without callback', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = fulfill(1)
+ resolve(p)
+ return assertSame(p, promise.trifurcate(undefined, assert.ifError, assert.ifError))
+ })
+
+ it('should behave like map', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = fulfill(1)
+ resolve(p)
+ return assertSame(p.map(f), promise.trifurcate(f, assert.ifError, assert.ifError))
+ })
+
+ it('should behave like chain with fulfillment', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = fulfill(1)
+ resolve(p)
+ return assertSame(p.chain(fp), promise.trifurcate(fp, assert.ifError, assert.ifError))
+ })
+
+ it('should behave like chain with rejection', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = fulfill(1)
+ resolve(p)
+ return assertSame(p.chain(rp), promise.trifurcate(rp, assert.ifError, assert.ifError))
+ })
+
+ it('should behave like then with exception', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = fulfill(1)
+ resolve(p)
+ return assertSame(p.then(tr), promise.trifurcate(tr, assert.ifError, assert.ifError))
+ })
+ })
+
+ describe('on rejected future', () => {
+ it('should only call the onrejected callback', () => {
+ const { ok, nok, result } = raceCallbacks(future)
+ const { resolve, promise } = future(CancelToken.empty())
+ resolve(reject(1))
+ promise.trifurcate(nok, ok, nok)
+ return result
+ })
+
+ it('should asynchronously call the callback', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ resolve(reject(1))
+ let called = false
+ const res = promise.trifurcate(undefined, () => {
+ called = true
+ })
+ assert(!called)
+ return res.then(() => assert(called))
+ })
+
+ it('should behave like the input without callback', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = reject(1)
+ resolve(p)
+ return assertSame(p, promise.trifurcate(assert.ifError, undefined, assert.ifError))
+ })
+
+ it('should behave like catch', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = reject(1)
+ resolve(p)
+ return assertSame(p.catch(f), promise.trifurcate(assert.ifError, f, assert.ifError))
+ })
+
+ it('should behave like catch with fulfillment', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = reject(1)
+ resolve(p)
+ return assertSame(p.catch(fp), promise.trifurcate(assert.ifError, fp, assert.ifError))
+ })
+
+ it('should behave like catch with rejection', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = reject(1)
+ resolve(p)
+ return assertSame(p.catch(rp), promise.trifurcate(assert.ifError, rp, assert.ifError))
+ })
+
+ it('should behave like catch with exception', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = reject(1)
+ resolve(p)
+ return assertSame(p.catch(tr), promise.trifurcate(assert.ifError, tr, assert.ifError))
+ })
+ })
+
+ describe('on cancelled future', () => {
+ it('should only call the oncancelled callback', () => {
+ const { ok, nok, result } = raceCallbacks(future)
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ cancel(1)
+ promise.trifurcate(nok, nok, ok)
+ return result
+ })
+
+ it('should asynchronously call the callback', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ cancel(1)
+ let called = false
+ const res = promise.trifurcate(undefined, undefined, () => {
+ called = true
+ })
+ assert(!called)
+ return res.then(() => assert(called))
+ })
+
+ it('should behave like rejected without callback', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ const expected = {}
+ cancel(expected)
+ return assertSame(reject(expected), promise.trifurcate(assert.ifError, assert.ifError, undefined))
+ })
+
+ it('should behave like subscribe', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ cancel(1)
+ return assertSame(token.subscribe(f), promise.trifurcate(assert.ifError, assert.ifError, f))
+ })
+
+ it('should behave like subscribe with fulfillment', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ cancel(1)
+ return assertSame(token.subscribe(fp), promise.trifurcate(assert.ifError, assert.ifError, fp))
+ })
+
+ it('should behave like subscribe with rejection', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ cancel(1)
+ return assertSame(token.subscribe(rp), promise.trifurcate(assert.ifError, assert.ifError, rp))
+ })
+
+ it('should behave like subscribe with exception', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ cancel(1)
+ return assertSame(token.subscribe(tr), promise.trifurcate(assert.ifError, assert.ifError, tr))
+ })
+ })
+
+ describe('on future before fulfilled', () => {
+ it('should only call the onfulfilled callback', () => {
+ const { ok, nok, result } = raceCallbacks(future)
+ const { resolve, promise } = future(CancelToken.empty())
+ promise.trifurcate(ok, nok, nok)
+ resolve(fulfill(1))
+ return result
+ })
+
+ it('should asynchronously call the callback', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ let called = false
+ const res = promise.trifurcate(() => {
+ called = true
+ })
+ assert(!called)
+ resolve(fulfill(1))
+ assert(!called)
+ return res.then(() => assert(called))
+ })
+
+ it('should behave like the input without callback', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = fulfill(1)
+ const res = promise.trifurcate(undefined, assert.ifError, assert.ifError)
+ resolve(p)
+ return assertSame(res, p)
+ })
+
+ it('should behave like map', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = fulfill(1)
+ const res = promise.trifurcate(f, assert.ifError, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.map(f))
+ })
+
+ it('should behave like chain with fulfillment', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = fulfill(1)
+ const res = promise.trifurcate(fp, assert.ifError, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.chain(fp))
+ })
+
+ it('should behave like chain with rejection', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = fulfill(1)
+ const res = promise.trifurcate(rp, assert.ifError, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.chain(rp))
+ })
+
+ it('should behave like then with exception', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = fulfill(1)
+ const res = promise.trifurcate(tr, assert.ifError, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.then(tr))
+ })
+ })
+
+ describe('on future before rejected', () => {
+ it('should only call the onrejected callback', () => {
+ const { ok, nok, result } = raceCallbacks(future)
+ const { resolve, promise } = future(CancelToken.empty())
+ promise.trifurcate(nok, ok, nok)
+ resolve(reject(1))
+ return result
+ })
+
+ it('should asynchronously call the callback', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ let called = false
+ const res = promise.trifurcate(undefined, () => {
+ called = true
+ })
+ assert(!called)
+ resolve(reject(1))
+ assert(!called)
+ return res.then(() => assert(called))
+ })
+
+ it('should behave like the input without callback', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = reject(1)
+ const res = promise.trifurcate(assert.ifError, undefined, assert.ifError)
+ resolve(p)
+ return assertSame(res, p)
+ })
+
+ it('should behave like catch', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = reject(1)
+ const res = promise.trifurcate(assert.ifError, f, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.catch(f))
+ })
+
+ it('should behave like catch with fulfillment', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = reject(1)
+ const res = promise.trifurcate(assert.ifError, fp, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.catch(fp))
+ })
+
+ it('should behave like catch with rejection', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = reject(1)
+ const res = promise.trifurcate(assert.ifError, rp, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.catch(rp))
+ })
+
+ it('should behave like catch with exception', () => {
+ const { resolve, promise } = future(CancelToken.empty())
+ const p = reject(1)
+ const res = promise.trifurcate(assert.ifError, tr, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.catch(tr))
+ })
+ })
+
+ describe('on future before cancelled', () => {
+ it('should only call the oncancelled callback', () => {
+ const { ok, nok, result } = raceCallbacks(future)
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ promise.trifurcate(nok, nok, ok)
+ cancel(1)
+ return result
+ })
+
+ it('should asynchronously call the callback', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ let called = false
+ const res = promise.trifurcate(undefined, undefined, () => {
+ called = true
+ })
+ assert(!called)
+ cancel(1)
+ assert(!called)
+ return res.then(() => assert(called))
+ })
+
+ it('should behave like rejected without callback', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ const expected = {}
+ const res = promise.trifurcate(assert.ifError, assert.ifError, undefined)
+ cancel(expected)
+ return assertSame(res, reject(expected))
+ })
+
+ it('should behave like subscribe', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ const res = promise.trifurcate(assert.ifError, assert.ifError, f)
+ cancel(1)
+ return assertSame(res, token.subscribe(f))
+ })
+
+ it('should behave like subscribe with fulfillment', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ const res = promise.trifurcate(assert.ifError, assert.ifError, fp)
+ cancel(1)
+ return assertSame(res, token.subscribe(fp))
+ })
+
+ it('should behave like subscribe with rejection', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ const res = promise.trifurcate(assert.ifError, assert.ifError, rp)
+ cancel(1)
+ return assertSame(res, token.subscribe(rp))
+ })
+
+ it('should behave like subscribe with exception', () => {
+ const { token, cancel } = CancelToken.source()
+ const { promise } = future(token)
+ const res = promise.trifurcate(assert.ifError, assert.ifError, tr)
+ cancel(1)
+ return assertSame(res, token.subscribe(tr))
+ })
+ })
+
+ const pre = (f, g) => x => (f({}), g(x))
+
+ describe('on future before fulfilled even when cancelled from the callback', () => {
+ it('should only call the onfulfilled callback', () => {
+ const { ok, nok, result } = raceCallbacks(future)
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future(token)
+ promise.trifurcate(pre(cancel, ok), nok, nok)
+ resolve(fulfill(1))
+ return result
+ })
+
+ it('should behave like map', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future(token)
+ const p = fulfill(1)
+ const res = promise.trifurcate(pre(cancel, f), assert.ifError, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.map(f))
+ })
+
+ it('should behave like chain with fulfillment', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future(token)
+ const p = fulfill(1)
+ const res = promise.trifurcate(pre(cancel, fp), assert.ifError, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.chain(fp))
+ })
+
+ it('should behave like chain with rejection', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future(token)
+ const p = fulfill(1)
+ const res = promise.trifurcate(pre(cancel, rp), assert.ifError, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.chain(rp))
+ })
+
+ it('should behave like then with exception', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future(token)
+ const p = fulfill(1)
+ const res = promise.trifurcate(pre(cancel, tr), assert.ifError, assert.ifError)
+ resolve(p)
+ return assertSame(res, p.then(tr))
+ })
+ })
+
+ describe('on future before rejected even when cancelled from the callback', () => {
+ it('should only call the onrejected callback', () => {
+ const { ok, nok, result } = raceCallbacks(future)
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future(token)
+ promise.trifurcate(nok, pre(cancel, ok), nok)
+ resolve(reject(1))
+ return result
+ })
+
+ it('should behave like catch', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future(token)
+ const p = reject(1)
+ const res = promise.trifurcate(assert.ifError, pre(cancel, f), assert.ifError)
+ resolve(p)
+ return assertSame(res, p.catch(f))
+ })
+
+ it('should behave like catch with fulfillment', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future(token)
+ const p = reject(1)
+ const res = promise.trifurcate(assert.ifError, pre(cancel, fp), assert.ifError)
+ resolve(p)
+ return assertSame(res, p.catch(fp))
+ })
+
+ it('should behave like catch with rejection', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future(token)
+ const p = reject(1)
+ const res = promise.trifurcate(assert.ifError, pre(cancel, rp), assert.ifError)
+ resolve(p)
+ return assertSame(res, p.catch(rp))
+ })
+
+ it('should behave like catch with exception', () => {
+ const { token, cancel } = CancelToken.source()
+ const { resolve, promise } = future(token)
+ const p = reject(1)
+ const res = promise.trifurcate(assert.ifError, pre(cancel, tr), assert.ifError)
+ resolve(p)
+ return assertSame(res, p.catch(tr))
+ })
+ })
+})