Ecosyste.ms: Awesome

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

Awesome Lists | Featured Topics | Projects

https://github.com/faressoft/flowa

🔥Service level control flow for Node.js
https://github.com/faressoft/flowa

api async control express expressjs flow javascript nodejs parallel promise restful router runner serial series sync task waterfall web

Last synced: about 1 month ago
JSON representation

🔥Service level control flow for Node.js

Awesome Lists containing this project

README

        

Flowa Logo

# Flowa

[![npm](https://img.shields.io/npm/v/flowa.svg)](https://www.npmjs.com/package/flowa)
[![npm](https://img.shields.io/npm/l/flowa.svg)](https://github.com/faressoft/flowa/blob/master/LICENSE)
[![Gitter](https://badges.gitter.im/join_chat.svg)](https://gitter.im/flowa-control-flow/Lobby)
[![Build Status](https://travis-ci.org/faressoft/flowa.svg?branch=master)](https://travis-ci.org/faressoft/flowa)
[![Coverage Status](https://coveralls.io/repos/github/faressoft/flowa/badge.svg?branch=master)](https://coveralls.io/github/faressoft/flowa?branch=master)

> Service level control flow for Node.js

One execution flow that runs mixed sync and async functions that uses either promises or callbacks running in parallel, sequentially or mixed. 🔥 **It can't be easier and more readable !**

# Hint

Check the [suggested way](#use-it-with-express) to use `Flowa` with `Express.js`.

# Demo

Flowa Demo

# Table of Contents

* [Features](#features)
* [Introduction](#introduction)
* [Installation](#installation)
* [Usage](#usage)
* [Shorthand Method](#shorthand-method)
* [Mixed Runners Types](#mixed-runners-types)
* [Promises](#promises)
* [Sync Tasks](#sync-tasks)
* [Terminating The Flow](#terminating-the-flow)
* [Jumping Between Tasks](#jumping-between-tasks)
* [Error Handling](#error-handling)
* [Factory Method](#factory-method)
* [ES6 Coding Style](#es6-coding-style)
* [Use It With Express](#use-it-with-express)
* [Best Practices](#best-practices)
* [Debugging Mode](#debugging-mode)
* [API](#api)
* [Flowa(flow[, name])](#constructor)
* [Flowa.create(flow[, name])](#create)
* [run(context, options)](#run)
* [License](#license)

## 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` or other `compound` tasks. Single tasks are either async or sync functions that are executed and called by passing an object called `context` to allow sharing data between tasks and an optional `callback` function for async tasks that use callbacks instead of promises. 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])`, `Flowa.create(flow[, name])`, or just use the [Shorthand Method](#shorthand-method) it is much easier and recommended if you are not planning to execute the same flow again and again.

```js
var Flowa = require('flowa');

// Define the flow
var flowa = new Flowa({

// Runner type
type: 'serial',

// A task that uses a callback
asyncTaskWithCallback: asyncTaskWithCallback,

// A task that returns a promise
asyncTaskWithPromise: asyncTaskWithPromise,

// A sync task
syncTask: syncTask

});
```

Then we need to execute the flow.

```js
// To be used to share data between the tasks
var context = {};

// Execute the tasks
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.

```js
// A task that uses a callback
function asyncTaskWithCallback(context, callback) {

setTimeout(callback.bind(null, null, 'DummyValue1'), 500);

}

// A task that returns a promise
function asyncTaskWithPromise(context) {

return Promise.resolve('DummyValue2');

}

// A sync task
function syncTask(context) {

return 'DummyValue3';

}
```

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.

```js
Flowa.run({

// Runner type
type: 'serial',

// Do task1
task1: task1,

// Do task2
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.

```js
var flowa = new Flowa({

// Runner type
type: 'serial',

// Do task1
task1: task1,

// Do task2 and task3 in parallel
group1: {

// Runner type
type: 'parallel',

// Do task2
task2: task2,

// Do task3
task3: task3,

// Do task4 and task5 in parallel
group2: {

// Runner type
type: 'serial',

// Do task4
task4: task4,

// Do task5
task5: task5

}

},

// Do task6
task6: task6

});
```

### Promises

You can return promises from your tasks instead of using callbacks. The callbacks will be called internally.

```js
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.

```js
function task1(context) {

// Do something !!

}
```

### Terminating The Flow

You can terminate the flow (skip executing the remaining tasks) by calling the `done` method.

```js
function task1(context, callback) {

this.done();
callback();

}
```

### 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 calling the `jump` method with the task name as first argument to jump into it after executing the current task completely. You can jump into a compound task too.

```js
function task1(context, callback) {

this.jump('task6');
callback();

}
```

### 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.

```js
function task1(context, callback) {

// We are just generating a random boolean value here
context.checkSomething = Math.random() >= 0.5;
callback();

}

/**
* Task
*
* @param {Object} context
* @param {Function} callback
*/
function task2(context, callback) {

if (context.checkSomething) {
return callback();
}

// Retry
this.jump('task1');
callback();

}
```

### 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.

```js
// Using callbacks (Recommended)
function checkUser(context, callback) {
callback(new Error('User is not found'));
}

// Using the `throw` operator
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`.

```js
Flowa.create({

// Runner type
type: 'serial',

// Do task1
task1: task1,

// Do task2
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.

```js
var flowa = new Flowa({

// Runner type
type: 'serial',

// Shorthand format for task1: task1
task1,

// Shorthand format for task2:task2
task2,

// Shorthand format for task3:task3
task3,

// Shorthand format for task4:task4
task4,

// Shorthand format for task5:task5
task5,

// Shorthand format for task6:task6
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.

```js
var express = require('express');
var Flowa = require('./index.js');
var app = express();

/**
* A mapping between services names and their handlers
* @type {Object}
*/
var handlers = {};

/**
* RESTful API services
* @type {Array}
*/
var services = [
{name: 'greeting.get', path: '/greeting/:name', method: 'get'}
];

/**
* Get a function to handle a specific service
*
* @param {String} name the name of the service
* @return {Function}
*/
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);

});

};

}

// Foreach service, define its route and attach a handler
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.

```js
/**
* Generate a greeting message
*
* @author Mohammad Fares
*/

var counter = 0;

/**
* Increment the greeting counter
*
* @param {Object} context
*/
function incrementGreetingCounter(context) {

context.counterValue = ++counter;

}

/**
* Generate a greeting message
*
* @param {Object} context
*/
function generateGreetingMessage(context) {

context.res.send({
message: 'Hello ' + context.req.params.name,
counter: context.counterValue
});

}

module.exports = {

// Runner type
type: 'serial',

// Increment the greeting counter
incrementGreetingCounter: incrementGreetingCounter,

// Generate a greeting message
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.

```js
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



flowa.run(context[, options]) ⇒ Promise

Execute the flow


task.done()

Skip the remaining tasks


task.jump(taskName)

Jump into another task under the same parent after executing the current task


### Note

> A new instance from the class `Task` is created for each execution for each task.

## 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) |

## flowa.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`).
* **autoInjectResults**: Inject the result of each task into the context automatically (type: `Boolean`) (default: `true`).
* **debug**: log the tasks' names in realtime (type: `Boolean`) (default: `false`).
* **debugCallback**: the debug logging function (type: `Boolean`) (default: `console.log`).

## task.done()

Skip the remaining tasks. Check [Terminating The Flow](#terminating-the-flow).

## task.jump(taskName)

Jump into another task under the same parent after executing the current task. Check [Jumping Between Tasks](#jumping-between-tasks).

| Param | Type | Description |
|----------|---------------------|--------------------------------|
| taskName | String | The name of the sibling task |

# License

This project is under the MIT license.