Flowa
Service level control flow for Node.js
Hint
Check the suggested way to use Flowa
with Express.js
.
Demo
Table of Contents
Features
- Writing more readable code for complex logic.
- Works with promises or callbacks.
- Works with sync or async tasks.
- Serial or parallel execution.
- No more callback hells.
- Jumping between tasks.
- Proper error handling.
- Timeouts.
Introduction
Each flow
is a set of tasks
. It starts by a compound task
which is basically a task that groups a set of single tasks
. Single tasks are async functions that are executed and called by passing an object called context
to allow sharing data between tasks and a callback
function. Each compound task's sub tasks are executed by a runner
that can be a serial
execution (default type) or a parallel
execution.
Installation
npm install --save flowa
Usage
We need to create a new Flowa object with our flow using new Flowa(flow[, name])
or Flowa.create(flow[, name])
.
var Flowa = require('flowa');
var flowa = new Flowa({
type: 'serial',
task1: task1,
task2: task2,
task3: task3
});
Then we need to execute the flow.
var context = {};
flowa.run(context).then(function(result) {
console.log(result);
}).catch(function(error) {
console.error(error);
});
And don't forget to write the code for your tasks.
function task1(context, callback) {
context.task1 = 1;
console.log('Executing task 1');
setTimeout(callback, 500);
}
function task2(context) {
context.task2 = 2;
console.log('Executing task 2');
return new Promise();
}
function task3(context) {
context.task3 = 3;
console.log('Executing task 3');
}
Just put the 3 blocks of code together in one script and they will run smoothly.
Shorthand Method
Is it possible to create a flow and execute it using a single function .run()
that belongs to the Flowa class.
Flowa.run({
type: 'serial',
task1: task1,
task2: task2
}).then(function(result) {
console.log(result);
}).catch(function(error) {
console.error(error);
});
Mixed Runners Types
There are no limitations about mixing the runners types. Add type
to the compound tasks to specify the runner type. But remember, it is not a good idea to make things too complex.
var flowa = new Flowa({
type: 'serial',
task1: task1,
group1: {
type: 'parallel',
task2: task2,
task3: task3,
group2: {
type: 'serial',
task4: task4,
task5: task5
}
},
task6: task6
});
Promises
You can return promises from your tasks instead of using callbacks. The callbacks will be called internally.
function task1(context) {
return new Promise(function(resolve, reject) {
resolve();
});
}
Sync Tasks
You can use sync tasks that doesn't return a promise and doesn't take a second callback argument. The callbacks will be called internally.
function task1(context) {
}
Jumping Between Tasks
You can jump forward and backward between tasks that belong to the same parent task and the runner type is serial
by passing the name of the task as the second argument to the callback function or as a resolved value if you use promises instead. You can jump into a compound task too.
function task1(context, callback) {
callback(null, 'task6');
}
Or with promises
function task1(context) {
return new Promise(function(resolve, reject) {
resolve('task6');
});
}
Loop and Retry
Since we have the ability to jump backward and forward, we can implement a task to try something and another task to check the result to decide either to jump back to the previous task or continue.
function task1(context, callback) {
context.checkSomething = Math.random() >= 0.5;
callback();
}
function task2(context, callback) {
if (context.checkSomething) {
return callback();
}
callback(null, 'task1');
}
Error Handling
The thrown errors and the errors passed as a first argument to the callback function can be handled by attaching a .catch()
to the returend promise from run()
method.
function checkUser(context, callback) {
callback(new Error('User is not found'));
}
function checkUser(context, callback) {
throw new Error('User is not found');
}
Factory Method
Is it possible to create a new Flowa object by calling .create()
method instead of using new Flowa
.
Flowa.create({
type: 'serial',
task1: task1,
task2: task2
}).run(context).then(function(result) {
console.log(result);
}).catch(function(error) {
console.error(error);
});
ES6 Coding Style
You can use the shorthand syntax for naming the tasks by their functions names.
var flowa = new Flowa({
type: 'serial',
task1,
task2,
task3,
task4,
task5,
task6
});
Use It With Express
You can use Flowa
to make more readable and maintainable express.js
services.
App.js
To initilize your web server and load your services.
Note: No need to change the code, just add more services at the line 16.
var express = require('express');
var Flowa = require('./index.js');
var app = express();
var handlers = {};
var services = [
{name: 'greeting.get', path: '/greeting/:name', method: 'get'}
];
function getServiceHandler(name) {
return function(req, res) {
var handler = handlers[name];
var context = {req: req, res: res};
handler.run(context).then(function() {
res.end();
}).catch(function(error) {
if (res.headersSent) {
return res.end();
}
res.status(500).send({
error: 'Something went wrong !'
});
console.error(error);
});
};
}
services.forEach(function(route) {
handlers[route.name] = new Flowa(require('./' + route.name)),
app[route.method](route.path, getServiceHandler(route.name));
});
app.listen(3000, console.log.bind(null, 'listening ...'));
Greeting.get.js
An example of a service.
var counter = 0;
function incrementGreetingCounter(context, callback) {
context.counterValue = ++counter;
callback();
}
function generateGreetingMessage(context, callback) {
context.res.send({
message: 'Hello ' + context.req.params.name,
counter: context.counterValue
});
callback();
}
module.exports = {
type: 'serial',
incrementGreetingCounter: incrementGreetingCounter,
generateGreetingMessage: generateGreetingMessage
};
Best Practices
- Stick with one coding style.
- Define your flow object in a separated object or better in a separated module.
- Add comments for each task to get a quick overview about all the tasks at one place.
- Each single task should do literally only one task.
- Specifiy the runners types.
Debugging Mode
To watch how the tasks being executed in realtime, you can activate the debug logging via the debug
option.
flowa.run(context, {debug: true});
API
- Flowa(flow[, name])
To create Flowa objects
- Flowa.create(flow[, name]) ⇒
Flowa
A factory method to create Flowa objects
- Flowa.run(flow[, context, options]) ⇒
Promise
Create a flow and execute it
- run(context[, options]) ⇒
Promise
Execute the flow
Flowa(flow[, name])
To create Flowa objects.
Param | Type | Description |
---|
flow | Object | A compound task |
name | String | A name for the flow (Optional) |
Flowa.create(flow[, name]) ⇒ Flowa
A factory method to create Flowa objects.
Returns: Flowa
- a new Flowa object
Param | Type | Description |
---|
flow | Object | A compound task |
name | String | A name for the flow (Optional) |
Flowa.run(flow[, context, options]) ⇒ Promise
Create a flow and execute it.
Returns: Promise
- resolve with the passed context object
Param | Type | Description |
---|
flow | Object | A compound task |
context | Object | A shared object between the tasks (Optional) (default: {}) |
options | Object | (Optional) |
run(context, options) ⇒ Promise
Execute the flow. The Flowa object can be defined once and executed as many as you need.
Returns: Promise
- resolve with the passed context object
Param | Type | Description |
---|
context | Object | A shared object between the tasks (Optional) (default: {}) |
options | Object | (Optional) |
Options:
- timeout: a timeout for the flow in milliseconds. The promise will be rejected with an error object that has (code:
ETIMEDOUT
) if the timeout is exeeded (type: Number
). - taskTimeout: a timeout for the single tasks in milliseconds. The promise will be rejected with an error object that has (code:
ETIMEDOUT
) if the timeout is exeeded (type: Number
). - debug: log the tasks' names in realtime (type:
Boolean
). - debugCallback: the debug logging function (type:
Boolean
) (default: console.log
).
License
This project is under the MIT license.