promise-toolbox
Essential utils for promises.
Features:
- compatible with all promise implementations
- small (< 150 KB with all dependencies, < 5 KB with gzip)
- nice with ES2015 / ES2016 syntax
Table of contents:
- Cancelation
- Resource management
- Functions
- asyncFn(generator)
- asyncFn.cancelable(generator)
- defer()
- fromCallback(fn, arg1, ..., argn)
- fromEvent(emitter, event, [options]) => Promise
- fromEvents(emitter, successEvents, errorEvents) => Promise
- isPromise(value)
- nodeify(fn)
- pipe(fns)
- pipe(value, ...fns)
- promisify(fn, [ context ]) / promisifyAll(obj)
- retry(fn, [options])
- try(fn)
- wrapApply(fn, args, [thisArg]) / wrapCall(fn, arg, [thisArg])
- Pseudo-methods
Installation of the npm package:
> npm install --save promise-toolbox
Browser
You can directly use the build provided at unpkg.com:
<script src="https://unpkg.com/promise-toolbox@0.8/dist/umd.js"></script>
Usage
Promise support
If your environment may not natively support promises, you should use a polyfill such as native-promise-only.
On Node, if you want to use a specific promise implementation,
Bluebird for instance
to have better performance, you can override the global Promise
variable:
global.Promise = require("bluebird");
Note that it should only be done at the application level, never in
a library!
Imports
You can either import all the tools directly:
import * as PT from "promise-toolbox";
console.log(PT.isPromise(value));
Or import individual tools from the main module:
import { isPromise } from "promise-toolbox";
console.log(isPromise(value));
Each tool is also exported with a p
prefix to work around reserved keywords
and to help differentiate with other tools (like lodash.map
):
import { pCatch, pMap } from "promise-toolbox";
If you are bundling your application (Browserify, Rollup, Webpack, etc.), you
can cherry-pick the tools directly:
import isPromise from "promise-toolbox/isPromise";
import pCatch from "promise-toolbox/catch";
API
Cancelation
This library provides an implementation of CancelToken
from the
cancelable promises specification.
A cancel token is an object which can be passed to asynchronous
functions to represent cancelation state.
import { CancelToken } from "promise-toolbox";
Creation
A cancel token is created by the initiator of the async work and its
cancelation state may be requested at any time.
const token = new CancelToken(cancel => {
$("#some-button").on("click", () => cancel("button clicked"));
});
const { cancel, token } = CancelToken.source();
A list of existing tokens can be passed to source()
to make the created token
follow their cancelation:
const { cancel, token } = CancelToken.source([token1, token2, token3]);
Consumption
The receiver of the token (the function doing the async work) can:
- synchronously check whether cancelation has been requested
- synchronously throw if cancelation has been requested
- register a callback that will be executed if cancelation is requested
- pass the token to subtasks
if (token.reason) {
console.log("cancelation has been requested", token.reason.message);
}
try {
token.throwIfRequested();
} catch (reason) {
console.log("cancelation has been requested", reason.message);
}
token.promise.then(reason => {
console.log("cancelation has been requested", reason.message);
});
subtask(token);
Registering async handlers
Asynchronous handlers are executed on token cancelation and the
promise returned by the cancel
function will wait for all handlers
to settle.
function httpRequest(cancelToken, opts) {
const req = http.request(opts);
req.end();
cancelToken.addHandler(() => {
req.abort();
return fromEvent(req, "close");
});
return fromEvent(req, "response");
}
const { cancel, token } = CancelToken.source();
httpRequest(token, {
hostname: "example.org",
}).then(response => {
});
Promise.resolve(cancel()).then(() => {
});
Is cancel token?
if (CancelToken.isCancelToken(value)) {
console.log("value is a cancel token");
}
@cancelable decorator
Make your async functions cancelable.
If the first argument passed to the cancelable function is not a
cancel token, a new one is created and injected and the returned
promise will have a cancel()
method.
import { cancelable, CancelToken } from "promise-toolbox";
const asyncFunction = cancelable(async ($cancelToken, a, b) => {
$cancelToken.promise.then(() => {
});
});
const source = CancelToken.source();
const promise1 = asyncFunction(source.token, "foo", "bar");
source.cancel("reason");
const promise2 = asyncFunction("foo", "bar");
promise2.cancel("reason");
If the function is a method of a class or an object, you can use
cancelable
as a decorator:
class MyClass {
@cancelable
async asyncMethod($cancelToken, a, b) {
}
}
Resource management
See Bluebird documentation for a good explanation.
Creation
A disposable is a simple object, containing a value and a dispose function:
const disposable = { value: db, dispose: () => db.close() };
As a convenience, you can use the Disposable
class:
import { Disposable } from "promise-toolbox";
const disposable = new Disposable(db, () => db.close());
If the process is more complicated, maybe because this disposable depends on
other disposables, you can use a generator function alongside the
Disposable.factory
decorator:
const getTable = Disposable.factory(async function*() {
const db = yield getDb();
const table = await db.getTable();
try {
yield table;
} finally {
await table.close();
}
});
Combination
Independent disposables can be acquired and disposed in parallel, to achieve
this, you can use Disposable.all
:
const combined = await Disposable.all([disposable1, disposable2]);
Similarly to Promise.all
, the value of such a disposable, is an array whose
values are the values of the disposables combined.
Consumption
To ensure all resources are properly disposed of, disposables must never be
used manually, but via the using
function:
import { using } from "promise-toolbox";
await using(
getTable(),
() => getTable(),
async (table1, table2) => {
}
);
For more complex use cases, just like Disposable.factory
, the handler can be
a generator function:
await using(async function*() {
const table1 = yield getTable();
const table2 = yield getTable();
});
Functions
asyncFn(generator)
Create an async function from a generator function
Similar to Bluebird.coroutine
.
import { asyncFn } from 'promise-toolbox'
const getUserName = asyncFn(function * (db, userId)) {
const user = yield db.getRecord(userId)
return user.name
})
asyncFn.cancelable(generator)
Like asyncFn(generator)
but the created async function supports cancelation.
Similar to CAF.
import { asyncFn, CancelToken } from 'promise-toolbox'
const getUserName = asyncFn.cancelable(function * (cancelToken, db, userId)) {
const user = yield db.getRecord(userId)
return user.name
})
const source = CancelToken.source()
getUserName(source.token, db, userId).then(
name => {
console.log('user name is', name)
},
error => {
console.error(error)
}
)
setTimeout(source.cancel, 5e3)
defer()
Discouraged but sometimes necessary way to create a promise.
import { defer } from "promise-toolbox";
const { promise, resolve } = defer();
promise.then(value => {
console.log(value);
});
resolve(3);
fromCallback(fn, arg1, ..., argn)
Easiest and most efficient way to promisify a function call.
import { fromCallback } from "promise-toolbox";
fromCallback(fs.readFile, "foo.txt").then(content => {
console.log(content);
});
fromCallback(cb => foo("bar", cb, "baz")).then(() => {
});
fromCallback.call(thisArg, fn, ...args).then(() => {
});
fromCallback.call(object, "method", ...args).then(() => {
});
fromEvent(emitter, event, [options]) => Promise
Wait for one event. The first parameter of the emitted event is used
to resolve/reject the promise.
const promise = fromEvent(emitter, "foo", {
array: false,
ignoreErrors: false,
error: "error",
});
promise.then(
value => {
console.log("foo event was emitted with value", value);
},
reason => {
console.error("an error has been emitted", reason);
}
);
fromEvents(emitter, successEvents, errorEvents) => Promise
Wait for one of multiple events. The array of all the parameters of
the emitted event is used to resolve/reject the promise.
The array also has an event
property indicating which event has
been emitted.
fromEvents(emitter, ["foo", "bar"], ["error1", "error2"]).then(
event => {
console.log(
"event %s have been emitted with values",
event.name,
event.args
);
},
reasons => {
console.error(
"error event %s has been emitted with errors",
event.names,
event.args
);
}
);
isPromise(value)
import { isPromise } from "promise-toolbox";
if (isPromise(foo())) {
console.log("foo() returns a promise");
}
nodeify(fn)
From async functions return promises, create new ones taking node-style
callbacks.
import { nodeify } = require('promise-toolbox')
const writable = new Writable({
write: nodeify(async function (chunk, encoding) {
})
})
pipe(fns)
Create a new function from the composition of async functions.
import { pipe } from "promise-toolbox";
const getUserPreferences = pipe(getUser, getPreferences);
pipe(value, ...fns)
Makes value flow through a list of async functions.
import { pipe } from "promise-toolbox";
const output = await pipe(
input,
transform1,
transform2,
transform3
);
promisify(fn, [ context ]) / promisifyAll(obj)
From async functions taking node-style callbacks, create new ones
returning promises.
import fs from "fs";
import { promisify, promisifyAll } from "promise-toolbox";
const readFile = promisify(fs.readFile);
const fsPromise = promisifyAll(fs);
readFile(__filename).then(content => console.log(content));
fsPromise.readFile(__filename).then(content => console.log(content));
retry(fn, [options])
Retries an async function when it fails.
import { retry } from "promise-toolbox";
(async () => {
await retry(
async bail => {
const response = await fetch("https://pokeapi.co/api/v2/pokemon/3/");
if (response.status === 500) {
throw bail(new Error(response.statusText));
}
if (response.status !== 200) {
throw new Error(response.statusText);
}
return response.json();
},
{
delay: 2000,
async onRetry(error) {
},
tries: 3,
retries: 4,
when: { message: "my error message" },
}
);
})().catch(console.error.bind(console));
try(fn)
Starts a chain of promises.
import PromiseToolbox from "promise-toolbox";
const getUserById = id =>
PromiseToolbox.try(() => {
if (typeof id !== "number") {
throw new Error("id must be a number");
}
return db.getUserById(id);
});
Note: similar to Promise.resolve().then(fn)
but calls fn()
synchronously.
wrapApply(fn, args, [thisArg]) / wrapCall(fn, arg, [thisArg])
Wrap a call to a function to always return a promise.
function getUserById(id) {
if (typeof id !== "number") {
throw new TypeError("id must be a number");
}
return db.getUser(id);
}
wrapCall(getUserById, "foo").catch(error => {
});
Pseudo-methods
This function can be used as if they were methods, i.e. by passing the
promise (or promises) as the context.
This is extremely easy using ES2016's bind syntax.
const promises = [Promise.resolve("foo"), Promise.resolve("bar")];
promises::all().then(values => {
console.log(values);
});
If you are still an older version of ECMAScript, fear not: simply pass
the promise (or promises) as the first argument of the .call()
method:
const promises = [Promise.resolve("foo"), Promise.resolve("bar")];
all.call(promises).then(function(values) {
console.log(values);
});
promises::all([ mapper ])
Waits for all promises of a collection to be resolved.
Contrary to the standard Promise.all()
, this function works also
with objects.
import { all } from 'promise-toolbox'
[
Promise.resolve('foo'),
Promise.resolve('bar')
]::all().then(value => {
console.log(value)
// → ['foo', 'bar']
})
{
foo: Promise.resolve('foo'),
bar: Promise.resolve('bar')
}::all().then(value => {
console.log(value)
})
promise::asCallback(cb)
Register a node-style callback on this promise.
import { asCallback } from "promise-toolbox";
function getDataFor(input, callback) {
return dataFromDataBase(input)::asCallback(callback);
}
promise::catch(predicate, cb)
Similar to Promise#catch()
but:
- support predicates
- do not catch
ReferenceError
, SyntaxError
or TypeError
unless
they match a predicate because they are usually programmer errors
and should be handled separately.
somePromise
.then(() => {
return a.b.c.d();
})
::pCatch(TypeError, ReferenceError, reason => {
})
::pCatch(NetworkError, TimeoutError, reason => {
})
::pCatch(reason => {
});
promise::delay(ms, [value])
Delays the resolution of a promise by ms
milliseconds.
Note: the rejection is not delayed.
console.log(await Promise.resolve("500ms passed")::delay(500));
Also works with a value:
console.log(await delay(500, "500ms passed"));
Like setTimeout
in Node, it is possible to
unref
the timer:
await delay(500).unref();
collection::forEach(cb)
Iterates in order over a collection, or promise of collection, which
contains a mix of promises and values, waiting for each call of cb
to be resolved before the next one.
The returned promise will resolve to undefined
when the iteration is
complete.
["foo", Promise.resolve("bar")]::forEach(value => {
console.log(value);
return new Promise(resolve => setTimeout(resolve, 10));
});
promise::ignoreErrors()
Ignore (operational) errors for this promise.
import { ignoreErrors } from "promise-toolbox";
readFileAsync("foo.txt")
.then(content => {
console.log(content);
})
::ignoreErrors();
readFileAsync("foo.txt")
.then(content => {
console.lgo(content);
})
::ignoreErrors();
promise::finally(cb)
Execute a handler regardless of the promise fate. Similar to the
finally
block in synchronous codes.
The resolution value or rejection reason of the initial promise is
forwarded unless the callback rejects.
import { pFinally } from "promise-toolbox";
function ajaxGetAsync(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener("error", reject);
xhr.addEventListener("load", resolve);
xhr.open("GET", url);
xhr.send(null);
})::pFinally(() => {
$("#ajax-loader-animation").hide();
});
}
promise::reflect()
Returns a promise which resolves to an objects which reflects the
resolution of this promise.
import { reflect } from "promise-toolbox";
const inspection = await promise::reflect();
if (inspection.isFulfilled()) {
console.log(inspection.value());
} else {
console.error(inspection.reason());
}
promises::some(count)
Waits for count
promises in a collection to be resolved.
import { some } from "promise-toolbox";
const [first, seconds] = await [
ping("ns1.example.org"),
ping("ns2.example.org"),
ping("ns3.example.org"),
ping("ns4.example.org"),
]::some(2);
promise::suppressUnhandledRejections()
Suppress unhandled rejections, needed when error handlers are attached
asynchronously after the promise has rejected.
Similar to Bluebird#suppressUnhandledRejections()
.
const promise = getUser()::suppressUnhandledRejections();
$(document).on("ready", () => {
promise.catch(error => {
console.error("error while getting user", error);
});
});
promise::tap(onResolved, onRejected)
Like .then()
but the original resolution/rejection is forwarded.
Like ::finally()
, if the callback rejects, it takes over the
original resolution/rejection.
import { tap } from "promise-toolbox";
const promise1 = Promise.resolve(42)::tap(value => {
console.log(value);
});
const promise2 = Promise.reject(42)::tap(null, reason => {
console.error(reason);
});
promise::tapCatch(onRejected)
Alias to promise:tap(null, onRejected)
.
promise::timeout(ms, [cb or rejectionValue])
Call a callback if the promise is still pending after ms
milliseconds. Its resolution/rejection is forwarded.
If the callback is omitted, the returned promise is rejected with a
TimeoutError
.
import { timeout, TimeoutError } from "promise-toolbox";
await doLongOperation()::timeout(100, () => {
return doFallbackOperation();
});
await doLongOperation()::timeout(100);
await doLongOperation()::timeout(
100,
new Error("the long operation has failed")
);
Note: 0
is a special value which disable the timeout, useful if the delay is
configurable in your app.
Development
# Install dependencies
> npm install
# Run the tests
> npm test
# Continuously compile
> npm run dev
# Continuously run the tests
> npm run dev-test
# Build for production
> npm run build
Contributions
Contributions are very welcomed, either on the documentation or on
the code.
You may:
- report any issue
you've encountered;
- fork and create a pull request.
License
ISC © Julien Fontanet