Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/luciotato/waitfor

Sequential programming for node.js, end of callback hell / pyramid of doom
https://github.com/luciotato/waitfor

Last synced: 11 days ago
JSON representation

Sequential programming for node.js, end of callback hell / pyramid of doom

Awesome Lists containing this project

README

        

Wait.for
=======
Sequential programming for node.js, end of callback hell.

Simple, straightforward abstraction over [Fibers](https://github.com/laverdet/node-fibers).

By using **wait.for**, you can call any nodejs standard async function in sequential/Sync mode, waiting for result data,
without blocking node's event loop (thanks to fibers)

A nodejs standard async function is a function in which the last parameter is a callback: function(err,data)

Advantages:
* Avoid callback hell / pyramid of doom
* Simpler, sequential programming when required, without blocking node's event loop (thanks to fibers)
* Simpler, try-catch exception programming. (default callback handler is: if (err) throw err; else return data)
* You can also launch multiple parallel non-concurrent fibers.
* No multi-threaded debugging nightmares, only one fiber running at a given time (thanks to fibers)
* Can use any node-standard async function with callback(err,data) as last parameter.
* Plays along with node programming style. Write your async functions with callback(err,data), but use them in sequential/SYNC mode when required.
* Plays along with node cluster. You design for one thread/processor, then scale with cluster on multicores.

##NEWS

###Aug-2013 - Wait.for-ES6 based on ES6-generators

I've developed ***a version based on JavaScript upcoming ES6-Harmony generators***. It's not based on node-fibers.
***Surprisingly***, ES6 based implementation of *wait.for(asyncFn)* is almost a no-op, you can even completely omit it. *Warning: Bleeding edge*. Check [Wait.for-ES6] (https://github.com/luciotato/waitfor-ES6)

---------------

Install:
----
```
npm install wait.for
```

Proper Use:
----
You need to be in a Fiber to be able to use wait.for. The ideal place to launch a fiber
is when a request arrives, to handle it:

```js
var server = http.createServer(
function(req, res){
console.log('req!');
wait.launchFiber(handler,req,res); //handle in a fiber, keep node spinning
}).listen(8000);
```

then,at *function handler(req,res)* and every function you call from there,
you'll be able to use wait.for(ayncFn...

Minimal running example
----
```js
var wait = require('wait.for');

function anyStandardAsync(param, callback){
setTimeout( function(){
callback(null,'hi '+param);
}, 5000);
};

function testFunction(){
console.log('fiber start');
var result = wait.for(anyStandardAsync,'test');
console.log('function returned:', result);
console.log('fiber end');
};

console.log('app start');
wait.launchFiber(testFunction);
console.log('after launch');
```

Basic Usage Example with Express.js
----
```js
var wait = require('wait.for');
var express = require('express');
var app = express();

// in a Fiber
function handleGet(req, res){
res.send( wait.for(fs.readFile,'largeFile.html') );
}

app.get('/', function(req,res){
wait.launchFiber(handleGet, req, res); //handle in a fiber, keep node spinning
});

app.listen(3000);
```

Cradle/couchdb Usage Example
----
see [cradle example](/examples/waitfor-cradle.js)

Generic Usage:
------------
```js
var wait=require('wait.for');

// launch a new fiber
wait.launchFiber(my_sequential_function, arg,arg,...)

// in a fiber.. We can wait for async functions
function my_sequential_function(arg,arg...){
// call async_function(arg1), wait for result, return data
var myObj = wait.for(async_function, arg1);
// call myObj.querydata(arg1,arg2), wait for result, return data
var myObjData = wait.forMethod(myObj,'queryData', arg1, arg2);
console.log(myObjData.toString());
}
```

-------------
##Notes on non-standard callbacks. e.g.: connection.query from mysql, database.prepare on node-sqlite3

wait.for expects standardized callbacks.
A standardized callback always returns (err,data) in that order.

A solution for the sql.query method and other non-standard callbacks
is to create a wrapper function standardizing the callback, e.g.:
```js
connection.prototype.q = function(sql, params, stdCallback){
this.query(sql,params, function(err,rows,columns){
return stdCallback(err,{rows:rows,columns:columns});
});
}
```
usage:
```js
try {
var result = wait.forMethod(connection, "q", options.sql, options.params);
console.log(result.rows);
console.log(result.columns);
}
catch(err) {
console.log(err);
}
```
e.g.: node-sqlite3's [database.prepare](https://github.com/mapbox/node-sqlite3/wiki/API)

```js
var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database(':memory:');

db.prototype.prep = function(sql, stdCallback){
var stmt = this.prepare(sql, function(err){
return stdCallback(err, stmt);
});
}

var stmt = wait.forMethod (db, 'prep', "INSERT OR REPLACE INTO foo (a,b,c) VALUES (?,?,?)");
```

More Examples:
-

DNS testing, *using pure node.js* (a little of callback hell):
```js
var dns = require("dns");

function test(){
dns.resolve4("google.com", function(err, addresses) {
if (err) throw err;
for (var i = 0; i < addresses.length; i++) {
var a = addresses[i];
dns.reverse(a, function (err, data) {
if (err) throw err;
console.log("reverse for " + a + ": " + JSON.stringify(data));
});
};
});
}

test();
```

***THE SAME CODE***, using **wait.for** (sequential):
```javascript
var dns = require("dns"), wait=require('wait.for');

function test(){
var addresses = wait.for(dns.resolve4,"google.com");
for (var i = 0; i < addresses.length; i++) {
var a = addresses[i];
console.log("reverse for " + a + ": " + JSON.stringify(wait.for(dns.reverse,a)));
}
}

wait.launchFiber(test);
```

Database example (pseudocode)
--
*using pure node.js* (a callback hell):
```js
var db = require("some-db-abstraction");

function handleWithdrawal(req,res){
try {
var amount=req.param("amount");
db.select("* from sessions where session_id=?",req.param("session_id"),function(err,sessiondata) {
if (err) throw err;
db.select("* from accounts where user_id=?",sessiondata.user_ID),function(err,accountdata) {
if (err) throw err;
if (accountdata.balance < amount) throw new Error('insufficient funds');
db.execute("withdrawal(?,?)",accountdata.ID,req.param("amount"), function(err,data) {
if (err) throw err;
res.write("withdrawal OK, amount: "+ req.param("amount"));
db.select("balance from accounts where account_id=?", accountdata.ID,function(err,balance) {
if (err) throw err;
res.end("your current balance is " + balance.amount);
});
});
});
});
}
catch(err) {
res.end("Withdrawal error: " + err.message);
}
}
```
Note: The above code, although it looks like it will catch the exceptions, **it will not**.
Catching exceptions with callback hell adds a lot of pain, and i'm not sure if you will have the 'res' parameter
to respond to the user. If somebody like to fix this example... be my guest.

***THE SAME CODE***, using **wait.for** (sequential logic - sequential programming):
```js
var db = require("some-db-abstraction"), wait=require('wait.for');

function handleWithdrawal(req,res){
try {
var amount=req.param("amount");
sessiondata = wait.forMethod(db,"select","* from session where session_id=?",req.param("session_id"));
accountdata = wait.forMethod(db,"select","* from accounts where user_id=?",sessiondata.user_ID);
if (accountdata.balance < amount) throw new Error('insufficient funds');
wait.forMethod(db,"execute","withdrawal(?,?)",accountdata.ID,req.param("amount"));
res.write("withdrawal OK, amount: "+ req.param("amount"));
balance = wait.forMethod(db,"select","balance from accounts where account_id=?", accountdata.ID);
res.end("your current balance is " + balance.amount);
}
catch(err) {
res.end("Withdrawal error: " + err.message);
}
}
```

Note: Exceptions will be catched as expected.
db methods (db.select, db.execute) will be called with this=db

-------------

##How does wait.launchFiber works?

`wait.launchFiber(genFn,param1,param2)` starts executing the `function genFn` *as a fiber-generator* until a "yield" (wait.for) is found, then `wait.launchFiber` execute the "yielded" value (a call to an async function), and links generator's "next" with the async callback(err,data), so when the async finishes and the callback is called, the fiber/generator "continues" after the `var x =wait.for(...)`.

Parallel Extensions
----------

-------------
###wait.parallel.launch(functions:Array)

Note: must be in a Fiber

####input:
* functions: Array = [[func,arg,arg],[func,arg,arg],...]

wait.parallel.launch expects an array of [[func,arg,arg..],[func,arg,arg..],...] and then launches a fiber for each function call, in parallel, and waits for all the fibers to complete.

The functions to be called ***should not be async functions***.

Each called sync function will be executed in it's own fiber, and this sync function should/can use `data=wait.for(..)` internally in order to call async functions.

####actions:
-launchs a fiber for each func
-the fiber does `resultArray[index] = func.apply(undefined,args)`

####returns:
- array with a result for each function
- do not "returns" until all fibers complete
- throws if error

-------------
###wait.parallel.map(arr:Array, mappedFn:function)

Note: must be in a Fiber

####input:
- arr: Array
- mappedFn = function(item,index,arr)
-- mappedFn should return converted item. Since we're in a fiber
-- mappedFn can use wait.for and also throw/try/catch

####returns:
- array with converted items
- do not "returns" until all fibers complete
- throws if error

-------------
###wait.parallel.filter(arr:Array, itemTestFn:function)

Note: must be in a Fiber

####input:
- arr: Array
- itemTestFn = function(item,index,arr)
-- itemTestFn should return true|false. Since we're in a fiber
-- itemTestFn can use wait.for and also throw/try/catch

####returns
- array with items where itemTestFn() returned true
- do not "returns" until all fibers complete
- throws if error

-------------
Parallel Usage Example:
see:
- [parallel-tests](/parallel-tests.js)