Skip to content
/ creed Public
forked from briancavalier/creed

Sophisticated and functionally-minded promise library with advanced features: coroutines, cancellation, ES2015 iterables, fantasy-land algebraics

License

Notifications You must be signed in to change notification settings

bergus/creed

 
 

Repository files navigation

creed :: async

Join the chat at https://gitter.im/briancavalier/creed

Sophisticated and functionally-minded async with advanced features: promises, cancellation, coroutines, ES2015 iterables, 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. It empowers you to direct the execution flow in detail through cancellation of callbacks.

You can also use babel and the babel-creed-async plugin to write ES7 async functions backed by creed coroutines.

Promises/A+ Fantasy Land Build Status Coverage Status

Example

Using creed coroutines, ES2015, and FP to solve the async-problem:

import { runNode, all, coroutine } from 'creed';
import { readFile } from 'fs';
import { join } from 'path';

// joinPath :: String -> String -> String
const joinPath = init => tail => join(init, tail);

// readFileP :: String -> String -> Promise Error Buffer
const readFileP = encoding => file => runNode(readFile, file, {encoding});

// pipe :: (a -> b) -> (b -> c) -> (a -> c)
const pipe = (f, g) => x => g(f(x));

// concatFiles :: String -> Promise Error String
const concatFiles = coroutine(function* (dir) {
    const readUtf8P = pipe(joinPath(dir), readFileP('utf8'));

    const index = yield readUtf8P('index.txt');
    const results = yield all(index.match(/^.*(?=\n)/gm).map(readUtf8P));
    return results.join('');
});

const main = process => concatFiles(process.argv[2])
    .then(s => process.stdout.write(s));

main(process);

Get it

npm install --save creed

bower install --save creed

As a module:

// ES2015
import { resolve, reject, all, ... } from 'creed';

// Node/CommonJS
var creed = require('creed');

// AMD
define(['creed'], function(creed) { ... });

As window.creed:

<!-- Browser global: window.creed -->
<script src="creed/dist/creed.js"></script>

Try it

Creed will work anywhere ES5 works. Here's how to try it.

Creed is REPL friendly, with instant and obvious feedback. Try it out in JSBin or using ES2015 with babel, or try it in a REPL:

Note that although babel supports ES2015 import statements, the babel-node REPL doesn't. Use let + require in the REPL instead.

npm install creed
npm install -g babel-node
babel-node
> let { resolve, delay, all, race } = require('creed');
'use strict'
> resolve('hello');
Promise { fulfilled: hello }
> all([1, 2, 3].map(resolve));
Promise { fulfilled: 1,2,3 }
> let p = delay(1000, 'done!'); p
Promise { pending }
... wait 1 second ...
> p
Promise { fulfilled: done! }
> race([delay(100, 'no'), 'winner']);
Promise { fulfilled: winner }

Errors & debugging

By design, uncaught creed promise errors are fatal. They will crash your program, forcing you to fix or .catch them. You can override this behaviour by registering your own error event listener.

Consider this small program, which contains a ReferenceError.

import { all, runNode } from 'creed';
import { readFile } from 'fs';

const readFileP = file => runNode(readFile, file);

const readFilesP = files => all(files.map(readFileP));

const append = (head, tail) => head + fail; // Oops, typo will throw ReferenceError

// Calling append() from nested promise causes
// a ReferenceError, but it is not being caught
readFilesP(process.argv.slice(2))
    .map(contents => contents.reduce(append, ''))
    .then(s => console.log(s));

Running this program (e.g. using babel-node) causes a fatal error, exiting the process with a stack trace:

> babel-node experiments/errors.js file1 file2 ...
/Users/brian/Projects/creed/dist/creed.js:583
		throw e.value;
		^

ReferenceError: fail is not defined
    at append (/Users/brian/Projects/creed/experiments/errors.js:8:39)
    at Array.reduce (native)
    at /Users/brian/Projects/creed/experiments/errors.js:11:31
    at Map.applyMap [as apply] (/Users/brian/Projects/creed/dist/creed.js:342:17)
    at Map.fulfilled (/Users/brian/Projects/creed/dist/creed.js:360:19)
    at Future._runAction (/Users/brian/Projects/creed/dist/creed.js:791:17)
    at Future.run (/Users/brian/Projects/creed/dist/creed.js:724:14)
    at TaskQueue._drain (/Users/brian/Projects/creed/dist/creed.js:158:10)
    at /Users/brian/Projects/creed/dist/creed.js:143:18
    at doNTCallback0 (node.js:407:9)

Debug events

Creed supports global window events in browsers, and process events in Node, similar to Node's 'uncaughtException' event. This allows applications to register a handler to receive events from all promise implementations that support these global events.

The events are:

  • 'unhandledRejection': fired when an unhandled rejection is detected
  • 'rejectionHandled': fired when rejection previously reported via an 'unhandledRejection' event becomes handled

Node global process events

The following example shows how to use global process events in Node.js to implement simple debug output. The parameters passed to the process event handlers:

  • reason - the rejection reason, typically an Error instance.
  • promise - the promise that was rejected. This can be used to correlate corresponding unhandledRejection and rejectionHandled events for the same promise.
process.on('unhandledRejection', reportRejection);
process.on('rejectionHandled', reportHandled);

function reportRejection(error, promise) {
	// Implement whatever logic your application requires
	// Log or record error state, etc.
}

function reportHandled(promise) {
	// Implement whatever logic your application requires
	// Log that error has been handled, etc.
}

Browser window events

The following example shows how to use global window events in browsers to implement simple debug output. The event object has the following extra properties:

  • event.detail.reason - the rejection reason (typically an Error instance)
  • event.detail.promise - the promise that was rejected. This can be used to correlate corresponding unhandledRejection and rejectionHandled events for the same promise.
window.addEventListener('unhandledRejection', event => {
	// Calling preventDefault() suppresses default rejection logging
	// in favor of your own.
	event.preventDefault();
	reportRejection(event.detail.reason, event.detail.promise);
}, false);

window.addEventListener('rejectionHandled', event => {
	// Calling preventDefault() suppresses default rejection logging
	// in favor of your own.
	event.preventDefault();
	reportHandled(event.detail.promise);
}, false);

function reportRejection(error, promise) {
	// Implement whatever logic your application requires
	// Log or record error state, etc.
}

function reportHandled(promise) {
	// Implement whatever logic your application requires
	// Log that error has been handled, etc.
}

API

Run async tasks

coroutine :: GeneratorFunction a → (...* → Promise e a)

Create an async coroutine from a promise-yielding generator function.

import { coroutine } from 'creed';

function fetchTextFromUrl(url) {
    // Fetch the text and return a promise for it
    return promise;
}

// Make an async coroutine from a generator function
let getUserProfile = coroutine(function* (userId) {
    try {
        let profileUrl = yield getUserProfileUrlFromDB(userId);
        let text = yield fetchTextFromUrl(profileUrl);
        return text;
    } catch(e) {
        return getDefaultText();
    }
});

// Call it
getUserProfile(123)
    .then(profile => console.log(profile));

For cancellation of coroutines see the cancellation docs.

fromNode :: NodeApi e a → (...* → Promise e a)

type NodeApi e a = ...* → Nodeback e a → ()
type Nodeback e a = e → a → ()

Turn a Node API into a promise API

import { fromNode } from 'creed';
import { readFile } from 'fs';

// Make a promised version of fs.readFile
let readFileP = fromNode(readFile);

readFileP('theFile.txt', 'utf8')
    .then(contents => console.log(contents));

runNode :: NodeApi e a → ...* → Promise e a

type NodeApi e a = ...* → Nodeback e a → ()
type Nodeback e a = e → a → ()

Run a Node API and return a promise for its result.

import { runNode } from 'creed';
import { readFile } from 'fs';

runNode(readFile, 'theFile.txt', 'utf8')
    .then(contents => console.log(contents));

runPromise :: Producer e a → ...* → Promise e a

type Producer e a = (...* → Resolve e a → Reject e → ())
type Resolve e a = a|Thenable e a → ()
type Reject e = e → ()

Run a function to produce a promised result.

import { runPromise } from 'creed';

/* Run a function, passing in a url parameter */
let p = runPromise((url, resolve, reject) => {
    var xhr = new XMLHttpRequest;
    xhr.addEventListener("error", reject);
    xhr.addEventListener("load", resolve);
    xhr.open("GET", url);
    xhr.send(null);
}, 'http://...'); // inject url parameter

p.then(result => console.log(result));

Parameter passing also makes it easy to create reusable tasks that don't rely on closures and scope chain capturing.

import { runPromise } from 'creed';

function xhrGet(url, resolve, reject) => {
    var xhr = new XMLHttpRequest;
    xhr.addEventListener("error", reject);
    xhr.addEventListener("load", resolve);
    xhr.open("GET", url);
    xhr.send(null);
}

runPromise(xhrGet, 'http://...')
    .then(result => console.log(result));

new Promise :: Producer e a [→ CancelToken e] → Promise e a

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 :: [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.

import { future, reject, CancelToken } from 'creed';

// Fulfill
let { resolve, promise } = future();
resolve(123);
promise.then(x => console.log(x)); //=> 123

// Resolve to another promise
let anotherPromise = ...;
let { resolve, promise } = future();
resolve(anotherPromise); //=> make promise's fate the same as anotherPromise's

// Reject
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 [→ CancelToken e] → Promise e a

Coerce a value or thenable to a promise. If the optional cancellation token is passed, it will be associated to the promise.

import { resolve } from 'creed';

resolve(123)
    .then(x => console.log(x)); //=> 123

resolve(resolve(123))
    .then(x => console.log(x)); //=> 123
    
resolve(jQuery.get('http://...')) // coerce any thenable
    .then(x => console.log(x)); //=> 123

fulfill :: a → Promise e a

Lift a value into a promise.

import { fulfill, resolve } from 'creed';

fulfill(123)
    .then(x => console.log(x)); //=> 123
    
// Note the difference from resolve
fulfill(fulfill(123))
    .then(x => console.log(x)); //=> '[object Promise { fulfilled: 123 }]'

resolve(fulfill(123))
    .then(x => console.log(x)); //=> 123

Promise.of :: a → Promise e a

Alias for fulfill, completing the Fantasy-land Applicative.

reject :: Error e ⇒ e → Promise e a

Make a rejected promise for an error.

import { reject } from 'creed';

reject(new TypeError('oops!'))
    .catch(e => console.log(e.message)); //=> oops!

never :: () → Promise e a

Make a promise that remains pending forever.

import { never } from 'creed';

never()
    .then(x => console.log(x)); // nothing logged, ever

Note: never consumes virtually no resources. It does not hold references to any functions passed to then, map, chain, etc.

Promise.empty :: () → Promise e a

Alias for never, completing the Fantasy-land Monoid.

Transform promises

.then :: Promise e a → (a → b|Thenable e b) → (e → b|Thenable e b) [→ CancelToken e] → Promise e b

Promises/A+ then. 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.

import { resolve } from 'creed';

resolve(1)
    .then(x => x + 1) // return a transformed value
    .then(y => console.log(y)); //=> 2

resolve(1)
    .then(x => resolve(x + 1)) // return transformed promise
    .then(y => console.log(y)); //=> 2

.catch :: Promise e a → (e → b|Thenable e b) [→ CancelToken e] → Promise e b

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.

import { reject, resolve } from 'creed';

reject(new Error('oops!'))
    .catch(e => 123) // recover by returning a new value
    .then(x => console.log(x)); //=> 123

reject(new Error('oops!'))
    .catch(e => resolve(123)) // recover by returning a promise
    .then(x => console.log(x)); //=> 123

.map :: Promise e a → (a → b) [→ CancelToken e] → Promise e b

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.

import { resolve } from 'creed';

resolve(1)
    .map(x => x + 1) // return a transformed value
    .then(y => console.log(y)); //=> 2

.ap :: Promise e (a → b) → Promise e a [→ CancelToken e] → Promise e b

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.

import { resolve } from 'creed';

resolve(x => x + 1)
    .ap(resolve(123))
    .then(y => console.log(y)); //=> 124

resolve(x => y => x+y)
    .ap(resolve(1))
    .ap(resolve(123))
    .then(y => console.log(y)); //=> 124

.chain :: Promise e a → (a → Promise e b) [→ CancelToken e] → Promise e b

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.

let profileText = getUserProfileUrlFromDB(userId)
    .chain(fetchTextFromUrl);

profileText.then(text => console.log(text)); //=> <user profile text>

.concat :: Promise e a → Promise e a → Promise e a

Fantasy-land Semigroup. Returns a promise equivalent to the earlier of two promises. Preference is given to the callee promise in the case that both promises have already settled.

import { delay, fulfill } from 'creed';

delay(200, 'bar').concat(delay(100, 'foo'))
    .then(x => console.log(x)); //=> 'foo'

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.

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.

Control time

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.

import { delay, reject, CancelToken } from 'creed';

delay(5000, 'hi')
    .then(x => console.log(x)); //=> 'hi' after 5 seconds

delay(5000, delay(1000, 'hi'))
    .then(x => console.log(x)); //=> 'hi' after 6 seconds

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

Create a promise that will reject after a specified time unless it settles earlier.

import { delay } from 'creed';

timeout(2000, delay(1000, 'hi'))
    .then(x => console.log(x)); //=> 'hi' after 1 second

timeout(1000, delay(2000, 'hi')); //=> TimeoutError after 1 second

Resolve Iterables

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 (Array a)

Await all promises from an Iterable. Returns a promise that fulfills with an array containing all input promise fulfillment values, or rejects if at least one input promise rejects.

import { all, resolve } from 'creed';

all([resolve(123), resolve(456)])
    .then(x => console.log(x)); //=> [123, 456]

let promises = new Set();
promises.add(resolve(123));
promises.add(resolve(456));

all(promises)
    .then(x => console.log(x)); //=> [123, 456]

function *generator() {
    yield resolve(123);
    yield resolve(456);
}

all(generator())
    .then(x => console.log(x)); //=> [123, 456]

race :: Iterable (Promise e a) → Promise e a

Returns a promise equivalent to the input promise that settles earliest. If there are input promises that are already settled or settle simultaneously, race prefers the one encountered first in the iteration order.

Note: As per the ES6-spec, racing an empty iterable returns never()

import { race, resolve, reject, delay, isNever } from 'creed';

race([delay(100, 123), resolve(456)])
    .then(x => console.log(x)); //=> 456

race([resolve(123), reject(456)])
    .then(x => console.log(x)); //=> 123

race([delay(100, 123), reject(new Error('oops'))])
    .catch(e => console.log(e)); //=> [Error: oops]

isNever(race([])); //=> true

any :: Iterable (Promise e a) → Promise e a

Returns a promise equivalent to the input promise that fulfills earliest. If all input promises reject, the returned promise rejects.

Note the differences from race().

import { any, resolve, reject, delay, isNever } from 'creed';

any([delay(100, 123), resolve(456)])
    .then(x => console.log(x)); //=> 123

any([resolve(123), reject(456)])
    .then(x => console.log(x)); //=> 123

any([reject(new Error('foo')), reject(new Error('bar'))])
    .catch(e => console.log(e)); //=> [RangeError: No fulfilled promises in input]

any([])
    .catch(e => console.log(e)); //=> [RangeError: No fulfilled promises in input]

settle :: Iterable (Promise e a) → Promise e (Array (Promise e a))

Returns a promise that fulfills with an array of settled promises.

import { settle, resolve, reject, isFulfilled, getValue } from 'creed';

// Find all the fulfilled promises in an iterable
settle([resolve(123), reject(new Error('oops')), resolve(456)])
    .map(settled => settled.filter(isFulfilled).map(getValue))
    .then(fulfilled => console.log(fulfilled)); //=> [ 123, 456 ]

Inspect

isFulfilled :: Promise e a → boolean

Returns true if the promise is fulfilled.

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

isRejected :: Promise e a → boolean

Returns true if the promise is rejected.

import { isRejected, resolve, reject, delay, never } from 'creed';

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

isSettled :: Promise e a → boolean

Returns true if the promise is either fulfilled or rejected.

import { isSettled, resolve, reject, delay, never } from 'creed';

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

isPending :: Promise e a → boolean

Returns true if the promise is pending (not yet fulfilled or rejected).

import { isPending, resolve, reject, delay, never } from 'creed';

isPending(resolve(123));        //=> false
isPending(reject(new Error())); //=> false
isPending(delay(0, 123));       //=> false
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 forever. In practice, this means that the promise is one that was returned by never() or a promise that has been resolved to such.

import { isNever, resolve, reject, delay, never, race } from 'creed';

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
isNever(race([]));             //=> true

getValue :: Promise e a → a

Extract the value of a fulfilled promise. Throws if called on a pending or rejected promise, so check first with isFulfilled.

import { getValue, resolve, reject, never } from 'creed';

getValue(resolve(123)); //=> 123
getValue(reject());     //=> throws TypeError
getValue(never());      //=> throws TypeError

getReason :: Promise e a → e

Extract the reason of a rejected promise. Throws if called on a pending or fulfilled promise, so check first with isRejected.

import { getReason, resolve, reject, never } from 'creed';

getReason(resolve(123));      //=> throws TypeError
getReason(reject('because')); //=> 'because'
getReason(never());           //=> throws TypeError

Polyfill

shim :: () → PromiseConstructor|undefined

Polyfill the global Promise constructor with the creed Promise constructor. If there was a pre-existing global Promise, it is returned.

import { shim } from 'creed';

// Install creed's ES2015-compliant Promise as global
let NativePromise = shim();

// Create a creed promise
Promise.resolve(123);

About

Sophisticated and functionally-minded promise library with advanced features: coroutines, cancellation, ES2015 iterables, fantasy-land algebraics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 99.7%
  • Shell 0.3%