fibrous

Easily mix asynchronous and synchronous programming styles in node.js

Fibrous

Easily mix asynchronous and synchronous programming styles in node.js.

A project by Good Eggs at https://github.com/goodeggs/fibrous.

npm install fibrous

Fibrous requires node version 0.6.x or greater.

Would you rather write this:

var updateUser = function(idattributescallback) {
  User.findOne(id, function (erruser) {
    if (err) return callback(err);
    
    user.set(attributes);
    user.save(function(errupdated) {
      if (err) return callback(err);
 
      console.log("Updated", updated);
      callback(null, updated);
    });
  });
});

Or this, which behaves identically to calling code:

var updateUser = fibrous(function(idattributes) {
  user = User.sync.findOne(id);
  user.set(attributes);
  updated = user.sync.save();
  console.log("Updated", updated);
  return updated;
});

Or even better, with coffeescript:

updateUser = fibrous (id, attributes) ->
  user = User.sync.findOne(id)
  user.set(attributes)
  updated = user.sync.save()
  console.log("Updated"updated)
  updated

Using standard node callback-style APIs without fibrous, we write (from the fs docs):

fs.readFile('/etc/passwd', function (errdata) {
  if (err) throw err;
  console.log(data);
});

Using fibrous, we write:

data = fs.sync.readFile('/etc/passwd');
console.log(data);

This is the same as writing:

future = fs.future.readFile('/etc/passwd');
data = future.wait();
console.log(data);

Or for multiple files read asynchronously:

futures = [
  fs.future.readFile('/etc/passwd'),
  fs.future.readFile('/etc/hosts')
];
data = fibrous.wait(futures);
console.log(data[0], data[1]);

Note that fs.sync.readFile is not the same as fs.readFileSync. The 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.

wait and sync (which uses wait 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 = fibrous(function() {
  return fs.sync.readFile('/etc/passwd');
});

is functionally equivalent to:

var asyncFunc = function(callback) {
  fs.readFile('/etc/passwd', function(errdata) {
    if (err) return callback(err);
 
    callback(null, data);
  });
}

With coffeescript, the fibrous version is even cleaner:

asyncFunc = fibrous ->
  fs.sync.readFile('/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.

Fibrous provides connect middleware that ensures that every request runs in a fiber. If you are using express, you'll want to use this middleware.

var express = require('express');
var fibrous = require('fibrous');
 
var app = express.createServer();
 
app.use(fibrous.middleware);
 
app.get('/', function(reqres){
    data = fs.sync.readFile('./index.html', 'utf8');
    res.send(data);
});

Fibrous uses the Future implementation from node-fibers.

future.wait waits for the future to resolve then returns the result while allowing the process to continue. 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.

In the above examples, if readFile produces an error, the fibrous versions (both sync and 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 beforeEach, it, and 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', function() {
  
  it('tests something asynchronous', function() {
    data = fs.sync.readFile('/etc/password');
    expect(data.length).toBeGreaterThan(0);
  });
});

If an asynchronous method called through fibrous returns an error, the spec helper will fail the spec.

Fibrous can make it easier to work with asynchronous methods in the console. It's not convenient to create a fiber to run console commands with sync, but using future is still easier than constructing callbacks in the console.

> 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 (not very long) for the future to complete.

Fibrous mixes future and sync into Function.prototype so you can use them directly as in:

readFile = require('fs').readFile;
data = readFile.sync('/etc/passwd');

Fibrous adds future and sync to 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 = obj.getter
func.future.call(obj, 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.

The first time you call sync or 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.

git clone git://github.com/goodeggs/fibrous.git
npm install
npm test

Fibrous is written in coffeescript with source in src/ compiled to lib/.

Tests are written with jasmine-node in spec/.

Run tests with npm test which will also compile the coffeescript to lib/.

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.