Promise-oriented coroutines for node.js.
npm install f-promise
The f-promise
API consists in 2 calls: wait
and run
.
result = wait(promise)
: waits on a promise and returns its result (or throws if the promise is rejected).promise = run(fn)
: runs a function as a coroutine and returns a promise for the function's result.
Constraint: wait
may only be called from a coroutine (a function which is executed by run
, directly or indirectly, through one of its callers).
import { wait, run } from 'f-promise';
import * as fs from 'mz/fs';
import { join } from 'path';
function diskUsage(dir) {
return wait(fs.readdir(dir)).reduce((size, name) => {
const sub = join(dir, name);
const stat = wait(fs.stat(sub));
if (stat.isDirectory()) return size + diskUsage(sub);
else if (stat.isFile()) return size + stat.size;
else return size;
}, 0);
}
function printDiskUsage(dir) {
console.log(`${dir}: ${diskUsage(dir)}`);
}
run(() => printDiskUsage(process.cwd()))
.then(() => {}, err => { console.error(err); });
Note: this is not a very efficient implementation because the logic is completely serialized.
To understand the benefits of f-promise
, let us compare the example above with the ES7 async/await
equivalent:
import * as fs from 'mz/fs';
import { join } from 'path';
async function diskUsage(dir) {
var size = 0;
for (var name of await fs.readdir(dir)) {
const sub = join(dir, name);
const stat = await fs.stat(sub);
if (stat.isDirectory()) size += await diskUsage(sub);
else if (stat.isFile()) size += stat.size;
}
return size;
}
async function printDiskUsage(dir) {
console.log(`${dir}: ${await diskUsage(dir)}`);
}
printDiskUsage(process.cwd())
.then(() => {}, err => { console.error(err); });
Two observations:
- Async is contagious:
printDiskUsage
must be marked asasync
because it needs toawait
ondiskUsage
. This is not dramatic in this simple example but in a large code base this translates into a proliferation ofasync/await
keywords throughout the code. - ES7 async/await does not play well with array methods (
forEach
,map
,reduce
, ...) because you cannot useawait
inside the callbacks of these methods. You have to write the loop differently, withfor ... of ...
orPromise.all
.
f-promise
solves these problems:
- Functions that wait on async operations are not marked with
async
; they are normal JavaScript functions.async/await
keywords don't invade the code. wait
plays well with array methods, and with other APIs that expect synchronous callbacks.
Coroutines have other advantages, like providing complete meaningful stacktraces without any overhead.
TypeScript is fully supported.
You can also use f-promise
with callback APIs.
So you don't absolutely need wrappers like mz/fs
, you can directly call node's fs
API:
import { wait } from 'f-promise';
// promise style
import * as mzfs from 'mz/fs';
const readdir = path => wait(mzfs.readdir(path));
// callback style
import * as fs from 'fs';
const readdir = path => wait(cb => fs.readdir(path, cb));
These goodies solve some common problems and offer an easy upgrade path from streamline.js (which bundled a similar API).
fun = fpromise.funnel(max)
limits the number of concurrent executions of a given code block.
The funnel
function is typically used with the following pattern:
import { funnel } from 'f-promise';
// somewhere
var myFunnel = funnel(10); // create a funnel that only allows 10 concurrent executions.
// elsewhere
myFunnel(() => { /* code with at most 10 concurrent executions */ });
The funnel
function can also be used to implement critical sections. Just set funnel's max
parameter to 1.
If max
is set to 0, a default number of parallel executions is allowed.
This default number can be read and set via funnel.defaultSize
.
If max
is negative, the funnel does not limit the level of parallelism.
The funnel can be closed with fun.close()
.
When a funnel is closed, the operations that are still in the funnel will continue but their callbacks
won't be called, and no other operation will enter the funnel.
hs = fpromise.handshake()
allocates a simple semaphore that can be used to do simple handshakes between two tasks.
The returned handshake object has two methods:
hs.wait()
: waits untilhs
is notified.
hs.notify()
: notifieshs
(without waiting for an acknowledgement) Note:wait
calls are not queued. An exception is thrown if wait is called while anotherwait
is pending.q = fpromise.queue(options)
allocates a queue which may be used to send data asynchronously between two tasks.
Themax
option can be set to control the maximum queue length.
Whenmax
has been reachedq.put(data)
discards data and returns false. The returned queue has the following methods:
data = q.read()
: dequeues an item from the queue. Waits if no element is available.
q.write(data)
: queues an item. Waits if the queue is full.
ok = q.put(data)
: queues an item synchronously. Returns true if the queue accepted it, false otherwise.
q.end()
: ends the queue. This is the synchronous equivalent ofq.write(undefined)
data = q.peek()
: returns the first item, without dequeuing it. Returnsundefined
if the queue is empty.
array = q.contents()
: returns a copy of the queue's contents.
q.adjust(fn[, thisObj])
: adjusts the contents of the queue by callingnewContents = fn(oldContents)
.
q.length
: number of items currently in the queue.
-
cx = fpromise.context()
returns the current context. -
fn = fpromise.withContext(fn, cx)
wraps a function so that it executes with contextcx
(or a wrapper around current context ifcx
is falsy). The previous context will be restored when the function returns (or throws).
returns the wrapped function.
-
results = fpromise.map(collection, fn)
creates as many coroutines withfn
as items incollection
and wait for them to finish to return result array. -
fpromise.sleep(ms)
suspends current coroutine forms
milliseconds. -
ok = fpromise.canWait()
returns whetherwait
calls are allowed (whether we are called from arun
). -
wrapped = fpromise.eventHandler(handler)
wrapshandler
so that it can callwait
.
the wrapped handler will execute on the current fiber if canWait() is true. otherwise it will berun
on a new fiber (without waiting for its completion)
Three policies available for error stack trace handling:
fast
: stack traces are not changed. Call history might be difficult to read; cost less.whole
: stack traces due to async tasks errors inwait()
are concatenate with the current coroutine stack. This allow to have a complete history call (including f-promise traces).- default: stack traces are like
whole
policy, but clean up to remove f-promise noise.
The policy can be set with FPROMISE_STACK_TRACES
environment variable.
Any value other than fast
and whole
are consider as default policy.
MIT.
f-promise
is just a thin layer. All the hard work is done by the fibers
library.
The absence of async/await
markers in code that calls asynchronous APIs is unusual in JavaScript (and considered harmful by some).
But this is the norm in other languages. Basically f-promise
enables goroutines in JavaScript.