npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

node-control-flow

v0.4.0

Published

A control flow library for javascript intended to handle the orchestration of multiple invocations within a data context.

Downloads

44

Readme

Control Flow

travis

NPM

A control flow library for javascript intended to handle the orchestration of multiple invocations within a data context.

Note: This library is released pre-1.0 as the API will most likely change once members of the community provide their input.

Purpose

1. Ability to terminate flow on business errors without using the error callback model.

This is the main reason I built this library, as this was a sore spot when dealing with batch processing and worker models. Often times, based on business requirements, I want to prevent execution flow from continuing but do not want to rely on the error callback pattern or introduce additional code checking between each step (ie: returning a status object and asserting how to continue). By allowing each method to determine their place in the flow, I can encapsulate that logic and reuse it across flows without much effort.

2. Avoid passing data through a callback waterfall

Another pain point when using a library like async directly, is you end up having to implement your own context pattern. This often requires having a global context "object" that is either accessed through scoped invocation for each method, or passed from call to call. That model breaks down as soon you want to break your invocation into separate modules and now you have the concern of sharing state across each.

3. Fork flows to execute the same logic across different data sets

The best example of this would be to pull data from a remote source (like a database), then for each result execute a series of methods. Sounds simple, but to do so without introducing a ton of boilerplate code is a hassle.

Flow Object API

Note: You are not required to use the flow object, you can stick with the standard "callback" argument, however you will only be able to error or continue to the next step, and not control the flow.

next([object])

Continues processing the flow without interrupting, optionally passing additional data to the context.

intercept(function(err, context, callback), [object])

Handle any exceptions that are thrown after the flow continues or in the absence of any error, execute logic when the flow has completed.

Arguments

  • object - An object of key/value pairs to place in the current context.

fork(variableName, items)

Enables fork mode by executing each future method in the flow for every item passed in. The item is made available in the context via varibaleName.

error(err)

As the flow replaces your traditional error callback model, you will use this method to stop execution and passback an error to the flow caller.

ignore()

When invoked within a flow, all future steps will not execute the and flow will invoke the caller callback immediately. When called within a subflow, the behavior is the same unless it is the last step in the flow, in which it will also terminate the main flow. See examples below.

ok([callback])

A wrapper around the okay library, this is a convenience method that is the same as running the example below. If a callback is not passed, flow.next() will be invoked.

externalCall(ok(flow.error, function(data){
	flow.next();
}));

turns into

externalCall(flow.ok(function(data){
	flow.next();
}));

or

externalCall(flow.ok);

Examples

Basic Use Case

var flow = require('node-control-flow');

module.exports = {
	start: function (callback) {
		// run setup code, this typically is used to gather all I/O connections, listen to a queue or schedule via timer
		setTimeout(function(){
			module.exports._execute(var1, var2, var3, callback);
		});
	},
	_execute: function (var1, var2, var3, callback) {
		// we encapsulate our orechestration logic in one place to allow for better testing (ie: mocking out I/O)
	
		var context = {
			var1: var1,
			var2: var2,
			var3: var3
		};

		flow.start(context, [
			module.exports._doThingOne,
			module.exports._doThingTwo,
			module.exports._doThingThree
		], callback);
	},
	_doThingOne: function(var1, flow){
		// we have access to the var1 object from our context
		// flow is always in the context, does not have to be the last parameter
		
		flow.next();
	},
	_doThingTwo: function(var2, var3, flow){
		// as the next called method, we can introduce new items in the context
		
		doDatabaseCall(function(err, data){
			if(err){
				return flow.error(err);
			}
		
			flow.next({
				var4: data
			});
		});
	},
	_doThingThree: function(var4, callback){
		// any method called after _doThingTwo will have access to var4
		// you also are not required to use the flow object, you can use normal callback as well which defaults to using flow.ok()
		
		callback();
	}
}

Flow Interruption

var flow = require('node-control-flow');

module.exports = {
	_execute: function (callback) {
		var context = {};

		flow.start(context, [
			module.exports._doThingOne,
			module.exports._doThingTwo,
			module.exports._doThingThree
		], callback);
	},
	_doThingOne: function(flow){		
		flow.next();
	},
	_doThingTwo: function(flow){
		// because we invoke flow.ignore(), the main flow will stop executing and flow callback will be called
		
		flow.ignore();
	},
	_doThingThree: function(flow){
		// this method would never be called
	}
}

Forked Flows

Forked flows allow you to easily define a single "flow" but have it execute multiple times per item. By default, each item will execute in sequence and complete fully before moving on to the next item.

var flow = require('node-control-flow');

module.exports = {
	_execute: function (callback) {
		var context = {};

		flow.start(context, [
			module.exports._firstStep,
			module.exports._forkStep,
			module.exports._multipleOne,
            module.exports._multipleTwo
		], callback);
	},
	_firstStep: function(flow){		
		flow.next();
	},
	_forkStep: function(flow){
		flow.fork('item', ['item1', 'item2']);
	},
	_multipleOne: function(item, flow){
		// on the first past this will be called with item = 'item1', after _multipleTwo, this will be called again with item = 'item2'
		flow.next();
	},
	_multipleTwo: function(item, flow){
		// when item = 'item1', flow.next() will jump back to _mulltipleOne and continue with item = 'item2'. When item = 'item2', the main flow will complete.
		flow.next();
	}
}

Subflows

A subflow behaves just like a normal flow but it comes with a few features:

  1. The context is forked in a subflow and any data added to it will be discarded when continuing outside the flow
  2. A subflow can be interrupted in isolation without affecting the main flow UNLESS the last method in the flow initatives the ignore.

Subflow Execute + Continue Main Flow

var flow = require('node-control-flow');

module.exports = {
	_execute: function (callback) {
		var context = {};

		flow.start(context, [
			module.exports._firstStep,
			[module.exports._shouldSubflowExecute, module.exports._doSubflowLogic],
			module.exports._finalStep
		], callback);
	},
	_firstStep: function(flow){		
		flow.next();
	},
	_shouldSubflowExecute: function(flow){
		// we have an opportunity to abort the subflow via flow.ignore(), and continue the main flow, but in this example we continue
		
		flow.next({
			subflowData: 'foobar'
		});
	},
	_doSubflowLogic: function(subflowData, flow){
		// this will be invoked because _shouldSubflowExecute called flow.next() instead of flow.ignore()
		
		flow.next();
	},
	_finalStep: function(flow){
		// subflowData is not available as an argument to this step
		// this will call because the subflow executed without issue
		flow.next();
	}
}

Subflow Interrupt + Continue Main Flow

var flow = require('node-control-flow');

module.exports = {
	_execute: function (callback) {
		var context = {};

		flow.start(context, [
			module.exports._firstStep,
			[module.exports._shouldSubflowExecute, module.exports._doSubflowLogic],
			module.exports._finalStep
		], callback);
	},
	_firstStep: function(flow){
		flow.next();
	},
	_shouldSubflowExecute: function(flow){
		// any method that calls flow.ignore() in a subflow will still allow the main flow to continue unless it is the last step in the subflow
		
		flow.ignore();
	},
	_doSubflowLogic: function(flow){
		// this will not be invoked because the previous step called flow.ignore()
	},
	_finalStep: function(flow){
		// this will call because the subflow executed without issue
		flow.next();
	}
}

Subflow Interrupt + Stop Main Flow

var flow = require('node-control-flow');

module.exports = {
	_execute: function (callback) {
		var context = {};

		flow.start(context, [
			module.exports._firstStep,
			[module.exports._shouldSubflowExecute, module.exports._doSubflowLogic],
			module.exports._finalStep
		], callback);
	},
	_firstStep: function(flow){		
		flow.next();
	},
	_shouldSubflowExecute: function(flow){
		flow.next();
	},
	_doSubflowLogic: function(flow){
		// this will cause the entire flow to stop immediately
		
		flow.ignore();
	},
	_finalStep: function(flow){
		// this will not be invoked because the last step in the previous subflow called flow.ignore()
		flow.next();
	}
}

Interceptors

Some times you want execute code regardless of the outcome for a particular flow or subflow. To do this, simply pass a function to the next method instead of data.

Intercept Exception

In this example, you can do some additional logic before passing error back up the chain.

var flow = require('node-control-flow');

module.exports = {
	_execute: function (callback) {
		var context = {};

		flow.start(context, [
			module.exports._interceptor,
			module.exports._finalStep
		], callback);
	},
	_interceptor: function(flow){		
		flow.intercept(function(err, context, interceptCallback){
			console.error(err); // Log error
			
			// pass it back up the flow stack, will invoke callback(err)
			interceptCallback(err);
		},{
		    foo: "bar" // similar to calling .next(), you can pass data along the chain
		});
	},
	_finalStep: function(flow){
		// after this is invoked, the interceptor function above is called
		flow.error(true);
	}
}

Swallow Exceptions

In this example, you can ignore any errors passed from the flow and complete successfully.

var flow = require('node-control-flow');

module.exports = {
	_execute: function (callback) {
		var context = {};

		flow.start(context, [
			module.exports._interceptor,
			module.exports._finalStep
		], callback);
	},
	_interceptor: function(flow){		
		flow.intercept(function(err, context, interceptCallback){
			// we ignore "err" and the flow ends without issue
			interceptCallback();
		});
	},
	_finalStep: function(flow){
		// after this is invoked, the interceptor function above is called
		flow.error(true);
	}
}