Easily mix asynchronous and synchronous programming styles in node.js
Easily mix asynchronous and synchronous programming styles in node.js.
A project by Good Eggs at https://github.com/goodeggs/fibrous.
- Easy-to-follow flow control for both serial and parallel execution
- Complete stack traces, even for exceptions thrown within callbacks
- No boilerplate code for error and exception handling
- Conforms to standard node async API
Fibrous requires node version 0.6.x or greater.
npm install fibrous
Would you rather write this:
varUserfindOneidif err return callbackerr;usersetattributes;usersaveif err return callbackerr;console.log"Updated" updated;callbacknull updated;;;;
Or this, which behaves identically to calling code:
var updateUser = fibroususer = UsersyncfindOneid;usersetattributes;updated = usersyncsave;console.log"Updated" updated;return updated;;
Or even better, with CoffeeScript:
updateUser = fibroususer = UsersyncfindOneidusersetattributesupdated = usersyncsaveconsolelog"Updated"updatedupdated
Using standard node callback-style APIs without fibrous, we write (from the fs docs):
fsreadFile'/etc/passwd'if err throw err;console.logdata;;
Using fibrous, we write:
data = fssyncreadFile'/etc/passwd';console.logdata;
This is the same as writing:
future = fsfuturereadFile'/etc/passwd';data = futurewait;console.logdata;
Or for multiple files read asynchronously:
futures =fsfuturereadFile'/etc/passwd'fsfuturereadFile'/etc/hosts';data = fibrouswaitfutures;console.logdata0 data1;
fs.sync.readFile is not the same as
latter blocks while the former allows the process to continue while
waiting for the file read to complete.
Fibrous uses node-fibers behind the scenes.
sync (which uses
internally) require that they are called within a fiber. Fibrous
provides two easy ways to do this.
Pass any function to
fibrous and it returns a function that
conforms to standard node async APIs with a callback as the last
argument. The callback expects
err as the first argument and the function
result as the second. Any exception thrown will be passed to the
callback as an error.
var asynFunc = fibrousreturn fssyncreadFile'/etc/passwd';;
is functionally equivalent to:
varfsreadFile'/etc/passwd'if err return callbackerr;callbacknull data;;
With coffeescript, the fibrous version is even cleaner:
asyncFunc = fibrous ->fssyncreadFile'/etc/passwd'
fibrous ensures that the passed function is
running in an existing fiber (from higher up the call stack) or will
create a new fiber if one does not already exist.
var express = require'express';var fibrous = require'fibrous';var app = express;appusefibrousmiddleware;appget'/'data = fssyncreadFile'./index.html' 'utf8';ressenddata;;
fibrous.run is a utility function that creates a fibrous function then executes it.
Provide a callback to handle any errors and the return value of the passed function (if you need it). If you don't provide a callback and there is an error, run will throw the error which will produce an uncaught exception. That may be okay for quick and dirty work but is probably a bad idea in production code.
fibrousrunvar data = fssyncreadFile'/etc/passwd';console.logdatatoString;return data;console.log"Handle both async and sync errors here" err;;
Sometimes you need to wait for a callback to happen that does not conform to
err, result format (for example streams). In this case the following pattern works well:
var stream = <your stream>streamon'close'callbacknull code;;var code = waitsync;
In the above examples, if
readFile produces an error, the fibrous versions
wait) will throw an exception. Additionally, the stack
trace will include the stack of the calling code unlike exceptions
typically thrown from within callback.
Fibrous provides a test helper for jasmine-node
that ensures that
afterEach run in a fiber.
Require it in your shared
spec_helper file or in the spec files where
you want to use fibrous.
require'fibrous/lib/jasmine_spec_helper';describe'My Spec'it'tests something asynchronous'data = fssyncreadFile'/etc/password';expectdatalengthtoBeGreaterThan0;;;
If an asynchronous method called through fibrous produces an error, the spec helper will fail the spec.
If you write a helper for other testing frameworks, we'd love to include it in the project.
Fibrous makes it much easier to work with asynchronous methods in an interactive console, or REPL.
If you find yourself in an interactive session, you can require fibrous so that
you can use
> fs = require('fs');> require('fibrous');> data = fs.future.readFile('/etc/passwd', 'utf8');> data.get()
In this example,
data.get() will return the result of the future,
provided you have waited long enough for the future to complete.
(The time it takes to type the next line is almost always long enough.)
You can't use
sync in the above scenario because a fiber has not been created
so you can't call
wait on a future.
Fibrous does provide a bin script that creates a new interactive console where each command
is run in a fiber so you can use sync. If you install fibrous with
npm install -g fibrous
./node_modules/.bin on your path, you can just run:
$ fibrousStarting fibrous node REPL...> fs = require('fs');> data = fs.sync.readFile('/etc/passwd', 'utf8');> console.log(data);### User Database#...
Or for a CoffeeScript REPL:
$ fibrous -c [or --coffee]Starting fibrous coffee REPL...coffee> fs = require 'fs'coffee> data = fs.sync.readFile '/etc/passwd', 'utf8'coffee> console.log data### User Database#...
The first time you call
future on an object, it builds the sync
and future proxies so if you add a method to the object later, it will
not be proxied.
You might be getting an error in Express that you are not in context of a fiber even after adding
fibrous.middleware to your stack. This can happen if you added it before
express.bodyParser(). Here's an example:
// might not workappusefibrousmiddleware;appuseexpressbodyParser;// orappusefibrousmiddleware;appuseexpressjson;// should workappuseexpressbodyParser;appusefibrousmiddleware;// orappuseexpressjson;appusefibrousmiddleware;
Fibrous uses the
Future implementation from node-fibers.
future.wait waits for the future to resolve then returns the result while allowing the process
fibrous.wait accepts a single future, multiple future arguments or an array of futures.
It returns the result of the future if passed just one, or an array of
results if passed multiple.
future.get returns the result of the resolved future or throws an
exception if not yet resolved.
Function.prototype so you can
use them directly as in:
readFile = require'fs'readFile;data = readFilesync'/etc/passwd';
Object.prototype correctly so they
are not enumerable.
These proxy methods also ignore all getters, even those that may return functions. If you need to call a getter with fibrous that returns an asynchronous function, you can do:
func = objgetterfuncfuturecallobj args
Some people don't like libraries that mix in to Object.prototype and Function.prototype. If that's how you feel, then fibrous is probably not for you. We've been careful to mix in 'right' so that we don't change property enumeration and find that the benefits of having sync and future available without explicitly wrapping objects or functions are worth the philosophical tradeoffs.
git clone git://github.com/goodeggs/fibrous.gitnpm installnpm test
Fibrous is written in coffeescript with
src/ compiled to
Tests are written with jasmine-node in
Run tests with
npm test which will also compile the coffeescript to
Pull requests are welcome. Please provide tests for your changes and features. Thanks!
(The MIT License)
Copyright (c) 2012 Good Eggs, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.