Coder Social home page Coder Social logo

flow-js's Introduction

Flow-JS

Overview

Flow-JS provides a continuation-esque construct that makes it much easier to express multi-step asynchronous logic in non-blocking callback-heavy environments like Node.js or javascript in the web browser.

The concept is best explained with an example. The following code uses a simple asynchronous key-store to look-up a user's ID from his username and then sets his email address, first name, and last name.

In this example, the dbGet and dbSet functions are assumed to rely on asynchronous I/O and both take a callback that is called upon completion.

dbGet('userIdOf:bobvance', function(userId) {
	dbSet('user:' + userId + ':email', '[email protected]', function() {
		dbSet('user:' + userId + ':firstName', 'Bob', function() {
			dbSet('user:' + userId + ':lastName', 'Vance', function() {
				okWeAreDone();
			});
		});
	});
});

Notice how every single step requires another nested function definition. A four-step process like the one shown here is fairly awkward. Imagine how painful a 10-step process would be!

One might point out that there is no reason to wait for one dbSet to complete before calling the next, but, assuming we don't want okWeAreDone to be called until all three calls to dbSet are finished, we'd need some logic to manage that:

dbGet('userIdOf:bobvance', function(userId) {
	var completeCount = 0;
	var complete = function() {
		completeCount += 1;
		if (completeCount == 3) {
			okWeAreDone();
		}
	}
	
	dbSet('user:' + userId + ':email', '[email protected]', complete);
	dbSet('user:' + userId + ':firstName', 'Bob', complete);
	dbSet('user:' + userId + ':lastName', 'Vance', complete);
});

Now look at the same example using Flow-JS:

flow.exec(
	function() {
		dbGet('userIdOf:bobvance', this);
		
	},function(userId) {
		dbSet('user:' + userId + ':email', '[email protected]', this.MULTI());
		dbSet('user:' + userId + ':firstName', 'Bob', this.MULTI());
		dbSet('user:' + userId + ':lastName', 'Vance', this.MULTI());
	
	},function() {
		okWeAreDone()
	}
);

A flow consists of a series of functions, each of which is applied with a special this object which serves as a callback to the next function in the series. In cases like our second step, this.MULTI() can be used to generate a callback that won't call the next function until all such callbacks have been called.

Installing

Flow-JS is a CommonJS compatible module. Place the "flow.js" file in any directory listed in your require.paths array and require it like this:

var flow = require('flow')

Or you can just put "flow.js" next to your script and do this:

var flow = require('./flow')

Defining a Flow

flow.define defines a flow given any number of functions as parameters. It returns a function that can be used to execute that flow more than once. Whatever parameters are passed each time that flow is called are passed as the parameters to the first function in the flow.

Each function in the flow is called with a special this object which maintains the state of the flow's execution, acts as a container for saving values for use between functions in the flow, and acts as a callback to the next function in the flow.

Here is an example to make this clear:

// define a flow for renaming a file and then printing its stats
var renameAndStat = flow.define(

	function(fromName, toName) {
		// arguments passed to renameAndStat() will pass through to this first function
		
		this.toName = toName; // save to be used in the next function
		fs.rename(fromName, toName, this);
	
	},function(err) {
		// when fs.rename calls the special "this" callback above, this function will be called
		// whatever arguments fs.rename chooses to pass to the callback will pass through to this function
	
		if (err) throw err;
		
		// the "this" here is the same as in the function above, so this.toName is available
		fs.stat(this.toName, this);
	
	},function(err, stats) {
		// when fs.stat calls the "this" callback above, this function will be called
		// whatever arguments fs.stat chooses to pass to the callback will pass through to this function
		
		if (err) throw err;
		
		sys.puts("stats: " + JSON.stringify(stats));
	}
);

// now renameAndStat can be used more than once
renameAndStat("/tmp/hello1", "/tmp/world1");
renameAndStat("/tmp/hello2", "/tmp/world2");

Executing a Flow Just Once

flow.exec is a convenience function that defines a flow and executes it immediately, passing no arguments to the first function.

Here's a simple example very similar to the one above:

flow.exec(
	function() {
		fs.rename("/tmp/hello", "/tmp/world", this);
	},function(err) {
		if (err) throw err;
		fs.stat("/tmp/world", this)
	},function(err, stats) {
		if (err) throw err;
		sys.puts("stats: " + JSON.stringify(stats));
	}
);

Multiplexing

Sometimes, it makes sense for a step in a flow to initiate several asynchronous tasks and then wait for all of those tasks to finish before continuing to the next step in the flow. This can be accomplished by passing this.MULTI() as the callback rather than just this.

Here is an example of this.MULTI() in action (repeated from the overview):

flow.exec(
	function() {
		dbGet('userIdOf:bobvance', this);
		
	},function(userId) {
		dbSet('user:' + userId + ':email', '[email protected]', this.MULTI());
		dbSet('user:' + userId + ':firstName', 'Bob', this.MULTI());
		dbSet('user:' + userId + ':lastName', 'Vance', this.MULTI());
	
	},function() {
		okWeAreDone()
	}
);

You can identify the results of a function by passing a result identifier to MULTI. The results of a function can retrieved using this key in the final step. The result will be a single value if callback receives 0 or 1 argument, otherwise it will be an array of arguments passed to the callback.

Example:

flow.exec(
	function() {
		dbGet('userIdOf:bobvance', this.MULTI('bob'));
		dbGet('userIdOf:joohndoe', this.MULTI('john'));
	},function(results) {
	  dbSet('user:' + results['bob'] + ':email', '[email protected]');
	  dbSet('user:' + results['john'] + ':email', '[email protected]');
	  okWeAreDone();
	}
);

In many cases, you may simply discard the arguments passed to each of the callbacks generated by this.MULTI(), but if you need them, they are accessible as an array of arguments objects passed as the first argument of the next function. Each arguments object will be appended to the array as it is received, so the order will be unpredictable for most asynchronous APIs.

Here's a quick example that checks for errors:

flow.exec(
	function() {
		fs.rename("/tmp/a", "/tmp/1", this.MULTI());
		fs.rename("/tmp/b", "/tmp/2", this.MULTI());
		fs.rename("/tmp/c", "/tmp/3", this.MULTI());
	
	},function(argsArray) {
		argsArray.forEach(function(args){
			if (args[0]) then throw args[0];
		});
	}
);

serialForEach

Flow-JS comes with a convience function called flow.serialForEach which can be used to apply an asynchronous function to each element in an array of values serially:

flow.serialForEach([1, 2, 3, 4], function(val) {
	keystore.increment("counter", val, this);
},function(error, newVal) {
	if (error) throw error;
	sys.puts('newVal: ' + newVal);
},function() {
	sys.puts('This is the end!');
});

flow.serialForEach takes an array-like object, a function to be called for each item in the array, a function that receives the callback values after each iteration, and a function that is called after the entire process is finished. Both of the second two functions are optional.

flow.serialForEach is actually implemented with flow.define.

Thanks to John Wright for suggesting the idea! (http://github.com/mrjjwright)

flow-js's People

Contributors

endeepak avatar mrjjwright avatar printercu avatar siebertm avatar willconant avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

flow-js's Issues

How to support success and failure callbacks from single async call

I really like the way the code for flow-js looks, but I am having trouble wrapping my head around how I could call an async method that takes two callback methods. One for success and one for failure.

Example method:

myRequest: function(successCb, failureCb) { ... }

I would need a flow that chooses the next method to hit in the flow based upon the success or failure. Something like:

flow.exec(
    function() {    // step 1
        myRequest(this.FN1(), this.FN2())
    },function(successParams) {  // step 2
        // do something
    },function(errParams) {  // step 1 error
        if (err) throw err;
    },function(param) {   // step 3
       // do something more
    }
);

So on success the flow would be: step1, step2, step3.
On failure it would be: step1, step1 error, throw.

Is this possible?

flow.exec doesn't seem to work properly on Windows Phone 7.5 or IE7

Not sure if you are interested in supporting these browsers (I am !) Great little library you have here.

In my code below, the second function in the flow gets called before both callbacks when on IE, but in Chrome, the iPad, etc it works fine.

// in parallel start the transition and the XHR
var count = 0;
flow.exec(
  function() {
    // Start the CSS transition to hide
    var transMulti = this.MULTI();
    $control.parent().addClassTrans('removed', function() { count++; transMulti(); });

    // In parallel, tell the server to hide facets
    var xhrMulti = this.MULTI();
    x$(document).xhr(url, {
      data: 'facet=' + facetName,
      async: true,
      method: 'post',
      callback: function() { count++; xhrMulti(); }
    });                  
  },
  function() {          
    if (count != 2)
      alert('fucked: ' + count);
    // if we're selected, then hide our value pane
    if (selected) 
      hideValues();

    // actually remove it, now that its hidden
    $control.parent().remove();
    // fetch new facets            
    updateSuggested();
  }
);

Conditional flow control

Is any kind of conditional flow control available? E.g. in step 3, if A, then go to step 5, else if B then go to step 7, otherwise go on with step 4.

More complex serialForEach example

Hello,

I'm trying to put together a more complex serialForEach example, which includes "jumping" manually to next step, based on the example in issue #3.
step 1: search for the value
step 2: if found, just go to next step. If not, search again using different parameters, and go to next step using that value


flow.serialForEach(["refe", "lkajdflkjwernm", "crypto", "poidfgmnermn"], function(val) {
  console.log("Searching for " + val)
      $.ajax({
                type: "GET",
        url: 'https://api.stackexchange.com/2.1/badges?key=U4DMV*8nvpm3EOpvf69Rxw((&site=stackoverflow&order=desc&sort=rank&filter=default',
        data: {inname: val},
                success: this
      })

},function(search_result) {
  console.log('search_result')  
  console.log(search_result)

  var next = this
  if (search_result.items.length > 0) {
    console.log('found')  
    next(search_result.items[0])
  } else {
    console.log('NOT found')  
    //just get the latest badge
      $.ajax({
                type: "GET",
        url: 'https://api.stackexchange.com/2.1/badges?key=U4DMV*8nvpm3EOpvf69Rxw((&site=stackoverflow&order=desc&sort=rank&filter=default',
        data: {},
                success: this
      })

  }

},function(badge) {
  console.log('badge')  
  console.log(badge)  



},function() {
    console.log('This is the end!');
});

There are multiple issues with this sample:

  1. the 'badge' step isn't called with additional data passed as parameters. 'badge' is always undefined.
  2. processing stops after 2nd value in serialForEach array
  3. 'This is the end!' never makes it

How should the code look for this to work?

Thanks

Old sources in npm package

Thank you for the great library.
The only problem there: On npm repository in package.json you have version = 0.2.2, but actually it's not.
Please check number of lines inside flow.js and test.js files (may be others).
I think version number was updated, but there are old sources in repository!
Thanks!

Using flow-js with promises?

Hello,

Can flow-js also be used with promises? If so, how? I've tried leveraging this.MULTI() but the next function never fires:

const flow = require('flow');

function generateRandomNumber () {
  return new Promise(function (resolve, reject) {
    var randomNumber = Math.floor((Math.random() * 10) + 1)
    if (randomNumber <= 5) {
      resolve(randomNumber)
    } else {
      reject(randomNumber)
    }
  })
}

/*
generateRandomNumber().then(function(result) {
	console.log('Success: ' + result)
}).catch(function(error) {
	console.log('Error: ' + error)
})//*/

flow.exec(
	function(){
		generateRandomNumber().then(this.MULTI("RES1")).catch(this.MULTI("REJ1"));
		generateRandomNumber().then(this.MULTI("RES2")).catch(this.MULTI("REJ2"));
		
	},function(argsArray){
		debugger;
		console.log("this is the data: ",JSON.stringify(argsArray));
	}
);
console.log("EOF");

It works really well for function() callbacks, though I'd love to also leverage this for promises if possible.

need to pass some additional data to this.MULTI()

When issuing multiple parallel requests and this.MULTI(), one can get the request results with argsArray.
However, I need to pass some additional data, besides the data returned by the callbacks. Something like
fs.rename("/tmp/c", "/tmp/3", this.MULTI(myData));

or
fs.rename("/tmp/c", "/tmp/3", this.MULTI(), myData );

I'll use myData in the final function to figure out at what result I'm looking at.

Add browser support info to README

Would be helpful to users considering this library to have readily-available browser support information.

Examples:

  • Simply mention that any browser that supports HTML File API (maybe also just link to caniuse.com?)
  • I see there are tests, so could simply list what browsers those tests are run against (and whether that's a manual or automated process)
  • If none of the above really fit, then add a manually maintained list or helpful information.

Modify to work with node's async interface

You could modify this slightly to work with node's async interface where the first parameter to the callback is the error message. Then it would work with all the built-in node async functions out of the box.

Maintaining access to scope vars in flow.exec

I'm trying to avoid async callback hell by creating a flow for a more complex set of API calls. Currently I have something like this:
//var lookupSQL can't go here

	flow.exec(
		function(){
			// see if user is in lookup table
			var lookupSQL = "SELECT * FROM ?? WHERE slackUserID = ?";
			pool.query(lookupSQL, [usersLUTable, message.user], this.MULTI("lookup_user"));

			// need to pass these on, or they won't be visible in the next function() call
			this.MULTI("params")(bot, message, callback, pool, this);
		},function(results){
			var bot = results.params[0],
				message = results.params[1],
				callback = results.params[2];
								
			if(results.lookup_user[0]){
				// error in SQL query
			}
			
			var results = results.lookup_user[1];
			var isKnownUser = false;
			if(results.length == 0){
				// 
			}else{
				isKnownUser = true;
			}
			
		    bot.api.users.info({user: message.user}, this.MULTI("userInfo"));
			
			// pass the same info to the next sync function				
			this.MULTI("params")(bot, message, callback, isKnownUser);
		},function(results){
			var bot = results.params[0],
				message = results.params[1],
				callback = results.params[2],
				isKnownUser = results.params[3],
				userInfo = results.userInfo;
		}
	);

I've noticed that I can't seem to maintain access to many both local and global variables & functions during each callback. For example, the global params in the first function() of the flow work as I want them to:

  • usersLUTable
  • message
  • pool
  • bot

However, in the next callback these will return "undefined", so I have to maintain them by manually adding & storing them through a custom this.MULTI("params") call. I have to do this in each step of the flow. Is this really the right way of doing this, or have I missed something obvious? It doesn't seem right to manually maintain that each step.

Thanks

`MULTI()` does not apply `resultId` to the results array

We're using Backbone models and collections in combination with flow-js to sync API calls. As per your example, MULTI() takes a parameter that allows it to identify a result when multiple async methods are required to complete before continuing. However:

flow.exec(
  ->
    aCb = @MULTI 'a'
    bCb = @MULTI 'b'
    aModel.fetch success: aCb, error: aCb
    bModel.fetch success: bCb, error: bCb
  (results) ->
    console.debug results # => [Array[2], Array[2]]
)

The code returns a numerically indexed array instead, so your example of results['a'] and results['b'] return undefined.

Cannot load Flow.JS using Require.JS

I have Require.JS in my JS folder, along side Flow.JS.

I have added the script tag to load Require.JS, then in another script, called:

var flow = require('./flow');

As per the Readme, but it doesn't work.

I get:

Uncaught Error: Module name "flow" has not been loaded yet for context: _. Use require([])
http://requirejs.org/docs/errors.html#notloaded

require.js:8 Uncaught Error: Mismatched anonymous define() module: function (){return c}
http://requirejs.org/docs/errors.html#mismatch

I then tried:

var flow = require(['./flow']);

But I got:

Uncaught Error: Mismatched anonymous define() module: function (){return c}
http://requirejs.org/docs/errors.html#mismatch

Can I get some help on installing this? Ideally without Require.JS, as I am not using it (apart form to try and load this library!)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.