wate
v0.4.0
Published
Wrangle callbacks without Promises.
Downloads
12
Readme
/$$
| $$
/$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$
| $$ | $$ | $$ |____ $$|_ $$_/ /$$__ $$
| $$ | $$ | $$ /$$$$$$$ | $$ | $$$$$$$$
| $$ | $$ | $$ /$$__ $$ | $$ /$$| $$_____/
| $$$$$/$$$$/| $$$$$$$ | $$$$/| $$$$$$$
\_____/\___/ \_______/ \___/ \_______/
Wate is a small (2.2kb minified and gzipped), fast, full-featured, easy-to-debug control flow library for JavaScript and TypeScript. Rather than using Promise-based libraries that swallow errors and lose stack traces in production, use Wate to manage callbacks inside of lightweight Futures.
Wate also exposes functions that convert to and from Promises and Futures, so you can interop with Promise libraries if needed.
Install It
npm install --save wate
If you're using TypeScript, the type definitions should Just Work and be
imported into your project automatically thanks to the "typings"
definition
in the package.json
.
Examples
Wrap a Node function to make it return Futures:
const fs = require('fs');
function readFile(filename, encoding) {
return wate.make((callback) => {
fs.readFile(filename, encoding, callback);
});
}
Transform a value as soon as it's loaded:
const xmlParser = require('xml2json');
// Read the file using the fn we defined above
const file = readFile('config.xml', 'utf-8');
// Transform to JSON
const json = wate.transform(file, (xmlText) => xmlParser.toJson(xmlText));
// Print the JSON
json.done((err, jsonVal) => {
console.log(jsonVal);
});
Read some files concurrently and print them when you're done:
// Read in parallel
const proust = readFile('proust.txt', 'utf-8');
const hemingway = readFile('hemingway.txt', 'utf-8');
// Get all the of values out of the futures once they've loaded
wate.splat([ proust, hemingway ], (proustText, hemingwayText) => {
// Print the values
console.log('proust said', proustText);
console.log('hemingway said', hemingwayText);
});
Read and parse some files concurrently, and operate on them both when you're done:
function loadJson(filename) {
const file = readFile(filename, 'utf-8');
return wate.transform(file, JSON.parse);
}
// Read and parse the two files in parallel
const conf = loadJson('config.json');
const overrides = loadJson('overrides.json');
// Combine the parsed hashes once they've loaded
const fullConf = wate.transform([conf, overrides], (confHash, overrideHash) => {
return _.extend({}, confHash, overrideHash);
});
// Print the combined hash when it's ready
fullConf.done((err, val) => {
console.log(val);
});
API
Futures
Futures expose two methods:
.done(callback)
Given a callback of the form function(err, val) {}
, calls the callback with
its error and value states when it resolves. For example:
// Returns a future for the file
const fileFuture = readFile('test.txt', 'utf-8');
fileFuture.done((err, text) => {
// Ordinary Node-style callback.
if(!err) console.log(text);
});
.catch(callback)
Given a callback of the form function(err) {}
, calls the callback with its
error if the future resolves to an error. For example:
const fileFuture = readFile('test.txt', 'utf-8');
fileFuture.catch((err) => {
// Only runs if there's an error
});
There isn't a corresponding method that only runs when the future succeeds, because if you're going to ignore errors you should be explicit about it.
Composing Futures
wate.all(futures)
Returns a Future that waits until all the given futures are successful, or errors out as soon as the first given Future does. For example:
const proust = readFile('proust.txt', 'utf-8');
const hemingway = readFile('hemingway.txt', 'utf-8');
wate.all([ proust, hemingway ]).done((err, texts) => {
if(!err) {
// Note: the below is a silly way to do this, and Wate provides better
// syntax for this so that you wouldn't write your code this way. But for
// the sake of a gradual introduction:
console.log(texts[0]); // proust
console.log(texts[1]); // hemingway
}
});
wate.none(futures)
Returns a Future that collects all of the errors and returns them in an array
as the err
parameters to its done
function, or succeeds if any of the
futures succeed.
const planA = runPlan('a');
const planB = runPlan('b');
wate.none([ planA, planB ]).done((allFailures, succeeded) => {
if(!succeeded) {
console.log("we're doomed");
console.log(allFailures);
}
});
wate.settled(futures)
Returns a Future that waits until all of the given futures have either errored
out or succeeded. The future will resolve to a list of Result objects of the
form { value: val, error: err }
. For example:
wate.settled([ a, b, c ]).done((_, results) => {
results.forEach((result) => {
if(result.error) console.log('error', result.error);
else console.log('success', result.value);
});
});
wate.firstValue(futures)
alias: wate.first
Returns a Future that resolves to the first value that any of the given futures resolve to, or to an array of all of the errors if none of the futures are successful. For example:
wate.firstValue([ a, b, c ]).done((errors, first) => {
if(!errors) console.log('value of first to finish successfully:', first);
else console.log('all futures errored:', errors);
});
wate.firstError(futures)
Returns a Future that resolves to the first error that any of the given Futures error out with, or to an array of all of the values if none of the Futures error out. For example:
wate.firstError([ a, b, c ]).done((values, firstError) => {
if(firstError) console.log('first error', firstError);
else console.log('no errors, values were:', values);
});
wate.lastValue(futures)
alias: wate.last
Returns a Future that resolves to the last value that the given Futures resolve to, or an array or all of the errors if none of the futures are successful. For example:
wate.lastValue([ a, b, c ]).done((errors, lastValue) => {
if(!errors) console.log('last value to succeed was:', lastValue);
else console.log('all futures errored:', errors);
});
wate.concatValues
alias: wate.concat
Given an array of Futures that resolve to arrays of values, returns a Future that resolves to the concatenation of all of the values. For example:
const a = wate.value([ 10, 20 ]);
const b = wate.value([ 30, 40 ]);
const c = wate.value([ 50, 60 ]);
wate.concat([ a, b, c ]).done((err, values) => {
// values is [ 10, 20, 30, 40, 50, 60 ]
});
wate.concatErrors
Similar to wate.concatValues
, but for errors.
Working with Futures
wate.splatValues(future, callback)
alias: wate.spreadValues
Given a Future that will resolve (if successful) to an array of values, calls the callback with the values as an argument list. Returns the given future. For example:
const red = readFile('red.txt', 'utf-8');
const blue = readFile('blue.txt', 'utf-8');
wate.splatValues(wate.all([ red, blue ]), (redText, blueText) => {
console.log(redText);
console.log(blueText);
});
wate.splatErrors(future, callback)
alias: wate.spreadErrors
Given a Future that will resolve (on errors) to an array of errors, calls the callback with the errors as an argument list. Returns the given future. For example:
const explode = readFile('none.txt', 'utf-8');
const alsoExplode = readFile('none.txt', 'utf-8');
wate.splatErrors(wate.firstValue([ explode, alsoExplode ]), (err1, err2) => {
console.log(err1);
console.log(err2);
});
wate.splat(futures, callback)
aliases: wate.splatAll
, wate.spreadAll
, wate.spread
Given an array of futures and a callback, calls the callback with an argument list of values if the futures all succeed. The values will be passed into the callback in the order that the futures were passed in the array. Returns a future that succeeds if all of the futures succeed, or fails if any of them fail. For example:
wate.splat([ a, b, c ], (aValue, bValue, cValue) => {
console.log('a:', aValue);
console.log('b:', bValue);
console.log('c:', cValue);
});
This is just a convenient wrapper around a common pattern:
wate.splatValues(wate.all([ a, b, c ]), (aValue, bValue, cValue) => {
console.log('a:', aValue);
console.log('b:', bValue);
console.log('c:', cValue);
});
Since the all
future is returned, this allows for easy error handling:
wate.splat([ a, b, c ], (aValue, bValue, cValue) => {
// handle values here
}).catch((err) => {
// handle errors here
});
wate.transformValue(future, mapper)
aliases: wate.bindValue
Given a future, returns a future that will resolve to the value of the given future, transformed by the given mapper function. For example:
const fileContents = readFile('config.json', 'utf-8');
const config = wate.transformValue(fileContents, (text) => {
return JSON.parse(text);
});
// Or, more succinctly:
const config = wate.transformValue(fileContents, JSON.parse);
wate.transformError(future, callback)
alias: wate.bindError
Similar to wate.transformValue
, but transforms errors.
wate.transformValues(futures, callback)
Given an array of futures and a callback of the form function(...args) {}
,
passes each of the resolved futures' values into the callback in the order the
futures appear in the array and returns a future that will resolve to the value
of the callback. For example:
const conf = wate.transform(readFile('config.json', 'utf-8'), JSON.parse);
const overrides = wate.transform(readFile('overrides.json', 'utf-8'), JSON.parse);
const fullConf = wate.transformValues([conf, overrides], (confHash, overrideHash) => {
return _.extend({}, confHash, overrideHash);
});
wate.transformErrors(futures, callback)
Similar to wate.transformValues
, but operates on errors instead of values.
wate.transform(future|futures, callback)
Convenience function: if given a single future and a callback, proxies to
wate.transformValue
. If given an array of futures and a callback, proxies to
wate.transformValues
.
wate.unwrapValue(future)
alias: wate.unwrap
Given a future that resolves to another future, unwraps the inner future.
const urlToLoad = readFile("url-to-load.txt", "utf-8");
const networkFuture = wate.transform(urlToLoad, (url) => {
// For transform() calls, ordinarily we return a value. Here, however, we're
// returning a future, since we need to make an async request to get
// the data. That means "networkFuture" future will actually resolve to...
// another future.
return wate.make((callback) => {
networkRequest(url, callback);
});
});
// Hence, we unwrap the outer future to get at the inner future:
const network = wate.unwrap(networkFuture);
// In practice, you'd probably just use the flatTransform convenience
// function rather than using both transform and unwrap, like so:
const network = wate.flatTransform(urlToLoad, (url) => {
return wate.make((callback) => {
networkRequest(url, callback);
});
});
wate.unwrapError(future)
Similar to unwrapValue
, but unwraps a future returned as an error rather than a
future that's returned as a value.
wate.flatten(future)
Maximally un-nests the future's resolved value. If you have multiple nested
futures, this will return a future that resolves to the innermost future's
value (or error out if the futures error out), whereas unwrap
only un-nests a
single layer. For example:
const ultraNested = wate.value(wate.value(wate.value(10)));
// The following resolves to 10
const flattened = wate.flatten(ultraNested);
// The following resolves to a future that resolves to 10
const unwrapped = wate.unwrap(ultraNested);
wate.flatTransform(future|futures, transformFunction)
alias: wate.flatBind
Composes the transform
and flatten
calls. Runs flatten both on the input
and the output, so you can pass it nested futures and also return nested
futures, and have them transparently un-nest. For example:
const urlToLoad = readFile('url-to-load.txt', 'utf-8');
const network = wate.flatTransform(urlToLoad, (url) => {
return wate.make((callback) => {
networkRequest(url, callback);
});
});
network.done((err, val) => {
// assuming success, val is the value of the network call
});
Since it composes transform
, it similarly also works with arrays of futures:
const conf = wate.transform(readFile('config.json', 'utf-8'), JSON.parse);
const overrides = wate.transform(readFile('overrides.json', 'utf-8'), JSON.parse);
const pidfile = wate.flatTransform([conf, overrides], (confHash, overrideHash) => {
const conf = _.extend({}, confHash, overrideHash);
return readFile(conf["pidfile"], "utf-8");
});
pidfile.done((err, val) => {
// assuming success, val is the contents of the pidfile
});
wate.invert(future)
Given a future that resolves to an error, returns a future that resolves to that error as its value. Similarly, given a future that resolves to a value, returns a future that resolves to the value as its error. For example:
const val = wate.value(10);
const inverted = wate.invert(val);
inverted.done((err) => {
// err is 10 here
});
Creating Futures
wate.make(builder)
Given a callback of the form function(callback) {}
, returns a Future that
resolves to whatever the callback is called with. For example:
const fs = require('fs');
const fileFuture = wate.make((callback) => {
fs.readFile('test.txt', 'utf-8', callback);
});
fileFuture.done((err, text) => {
if(!err) console.log(text);
});
wate.value(val)
Creates a future of a raw value. For example:
const future = wate.value(10);
wate.error(err)
Creates a failed future given a raw error (or any JS value). For example:
const future = wate.error("an error string");
wate.fromDOMElement(domElement)
Given a DOM element that emits 'load'
and 'error'
events, returns a future
that resolves to a null error and the element if it loads, or an error if the
element fails to load. For example:
const image = new Image();
image.src = 'test.png';
const imageFuture = wate.fromDOMElement(image);
// Append the image if it loads
imageFuture.done((err, image) => {
if(!err) document.body.appendChild(image);
});
Promise Interop
wate.fromPromise(promise)
Turns a Promise into a Wate Future. For example:
const promise = somePromiseReturningFn();
const future = wate.fromPromise(promise);
wate.toPromise(future)
Turns a Future into a Promises/A compatible promise. Note that the Promises/A+ spec mandates interop with Promises/A, so this should work even with Promises/A+ libraries. For example:
const future = wate.value(10);
const promise = wate.toPromise(future);