mind scratch

thinking...

Craig W

I'm a software developer, writer and editor. But on a personal note, I'm a husband and the father of a few great kids.
           
 

Flow Control in npm

Static Version

Flow Control in npm

Flow control is a popular subject in NodeJS. Since most of us learned synchronous object-oriented programming patterns in school, it can be a bit of a shift to really leverage asynchronous functional programming for all it can do.

As it turns out, a great way to leverage huge chunks of the node API is to build a package manager. npm has to do a lot of stuff with the file system, child processes, and HTTP requests to the registry. Fetching and building packages is a lot of "laundry list" programming. That is, the algorithms are very simple (fetch this file, put it over there, run that script, etc.), but there's a lot to do, and you've gotta make sure everything gets done right (and in the right order).

To keep this from getting out of hand, I've leveraged two fairly low-level patterns that are found throughout the NodeJS API, but which I haven't seen documented anywhere yet, and which I'm calling "the Action/Callback pattern".

Action functions

Action functions can take a variable amount of arguments, but the last argument is always a callback function. It MUST call that cb exactly one time, once it's done doing stuff.

Callback functions

Callback functions can take any number of arguments, but the first argument is always an error or null.

Example

You've probably seen this example:

read-file-example.js
fs.readFile("/etc/passwd", function (er, data) {  if (er) throw er  // doSomething(data)})

fs.readFile is an action function. The function being passed to it, function (er, data)... is a callback function.

Consistent Patterns are Consistent

Every function in npm, if it does something async, does it using this mechanism.

Every callback function in npm, will expect an error argument as the first argument, so calling it like that will always bubble the error up.

Polymorphism Wins

Because of this consistent pattern, there is a lot of room for powerful creativity.

asyncMap(list, fn, cb)

Let's say you have a list of filenames, and you have to remove each one. A pretty typical use case in npm, but once you generalize "do this to those", you start noticing nails in need of a hammer.

asyncMap is the answer for that. (The current async-map.js in npm is a bit more complicated, because it allows you to specify a list of functions rather than just one.)

async-map.js
function asyncMap (list, fn, cb_) {  if (typeof cb_ !== "function") throw new Error(    "No callback provided to asyncMap")  var data = []    , errState = null    , l = list.length  if (!l) return cb_(null, [])  function cb (er, d) {    if (errState) return    if (arguments.length > 1) data = data.concat(d)    if (er) return cb_(errState = er, data)    else if (-- a === 0) cb_(errState, data)  }  // expect the supplied cb function to be called  // "n" times for each thing in the array.  list.forEach(function (ar) { fn(ar, cb) })}

Note that the top-level cb_ function is called with an array of all the results. So, data is being proxied up, as well as errors. (That's why it's called asyncMap, rather than asyncForEach.)

So, now we can simply do this:

remove-file-list.js
function removeFileList(fileList, cb) {  asyncMap(fileList, rm, cb)}

Proxying

You catch that? An Action function can pass its own cb over to another Action function if it doesn't need to do anything with success or failure. We could also have done something like this:

proxying.js
function removeFileList (fileList, cb) {  asyncMap(fileList, rm, function (er) {    if (er) log("Failed to remove fileList!")    cb(er)  })}

chain(fn1, fn2, ..., cb)

There are other cases where you want to make sure that a list of functions are called in a specific order. For instance, we need to make sure that all preinstall scripts are done running before starting in on the install scripts. That's where chain comes in handy.

chain takes a list of function arguments which take a callback, and a final argument which is the ultimate callback function.

chain.js
function chain () {  var steps = Array.prototype.slice.call(arguments)    , cb_ = steps.pop()    , n = 0    , l = steps.length  function cb (er) {    if (er) return cb_(er)    if (++ n === l) return cb_()    steps[n](cb)  }  steps[n](cb)}

chain 2

This is great, but it requires using Function#bind if we want to pass arguments to those functions other than a callback. This API would be nice:

chain2-usage.js
chain( [fn1, a, b]     , [obj, "method", x, y, z]     , function (cb) { doSomething(1,2,3,cb) }     , cb     )

Basically, each argument is one of:

  • a function which will be called with a single cb argument
  • an array containing a function and 0 or more arguments
  • an array containing an object, a method name, and 0 or more arguments
  • a falsey value, so we can do stuff like: foo && [ doFoo, foo, bar ]

Revamped to handle this calling style, chain looks like this:

chain2.js
function chain () {  var steps = Array.prototype.slice.call(arguments)    , cb_ = steps.pop()    , n = 0    , l = steps.length  nextStep(cb)  function cb (er) {    if (er) return cb_(er)    if (++ n === l) return cb_()    nextStep(cb)  }  function nextStep (cb) {    var s = steps[n]    // skip over falsey members    if (!s) return cb()    // simple function    if (typeof s === "function") return s(cb)    if (!Array.isArray(s)) throw new Error(      "Invalid thing in chain: "+s)    var obj = null      , fn = s.shift()    if (typeof fn === "object") {      // [obj, "method", some, args]      obj = fn      fn = obj[s.shift()]    }    if (typeof fn !== "function") throw new Error(      "Invalid thing in chain: "+typeof(fn))    fn.apply(obj, s.concat(cb))  }}

Mix and Match

Because everything in npm (and a lot of things in node) use this pattern, you can mix very high-level operations with very low-level operations. Why, even chain and asyncMap are "action" functions, so they can take any vanilla callback, or be arguments to one another.

This is without any sophisticated "async flow control" library. Just a few short JavaScript functions and an adherence to a few simple patterns.

As long as you stick to these patterns, you can use any Action function in any asyncMap or chain call, and make sure that all Action functions call their cb exactly once.

Get creative! This language is powerful, and it's not that hard to do interesting things with it.

View the discussion thread.blog comments powered byDisqus

Posted
 

Goodbye MySpace

Just cancelled my old myspace account...haven't logged into it in almost a year and I just saw news they re-designed part of it. I logged in, was still bored so I cancelled. Facebook might be next...I really don't use it. My online social life pretty much consists of: github.com, buzz, twitter (only via posterous.com) and maybe forrst (still exploring).

Thanks MySpace....good luck.
Posted
 

post from postme

Hello, my name is postme.
via postme
Posted
 

More postme

I’m continuing to work on postme, a small app that allows me to explore node.js and the Posterous API.

Posted
 

Hello, my name is postme

postme is my playground for exploring the posterous API and using node.js

Posted
 

posterous API test with node.js

this post was created using the posterous API and node.js

Posted
 

Notes: Introduction to Node.js

I recently watched the Introduction to NodeJS video which was a presentation by Ryan Dahl about node.js.

It provided some great information about where node.js came from, it’s design goals and where it’s headed. Here are some quick notes I took while watching:

Goal

provide a purely evented, non-blocking infrastructure to script highly concurrent programs.

Design Goals

  • no function should directly perform I/O (mostly true, not totally)
  • to recv. info from disk/network/process, there must be a callback low-level
  • stream everything, never force the buffering of data
  • do not remove functionality present at POSIX layer. For example, support half-closed TCP connections.
  • built-in support for most important protocols: DNS, HTTP, TLS
  • support many HTTP features: chunked encoding, pipelined messages, hanging requests for comet applications
  • API should be familiar to client-side JS programmers and old school UNIX hackers
  • be platform independent
  • make it enjoyale

Misc. Notes

  • only one execution stack
  • javascript can access the main thread
  • C layer can use multiple threads
  • Threads should only be used by experts, so you’ll have to write C to interact with the thread pool provided by libeio if you want them.
  • Jail users in non-blocking environment
  • don’t allow I/O trickery (i.e. co-routines)

Road Map

  • use it for entire websites as frameworks mature
  • security: set it behind a stable web server
  • load balancing: not necessary
  • example stack: Internet <—–> NGINX <—–> Node
  • when node is stable…some day: Internet <—–> Node

And just a little code snippet that demonstrates printing “hello world!”, where “world!” gets printed after 2 seconds.

var sys = require('sys');

setTimeout(function() {
    sys.puts('world!');
}, 2000);

sys.print('hello ');
Posted
 

Interesting Workspace Ideas

Interesting ideas for finding workspace:

https://www.noisebridge.net/wiki/Noisebridge

http://citizenspace.us/

 

makes me wish I lived in the bay area. does the east coast have anything like these?

Posted
 

Reading Code

This week I'm going to focus solely on reading code. I've looked at blog posts and even played around with node.js myself, but now I want to see exactly what others are doing with it.

There are so many projects on github I should be entertained for a while before I start working on brownbag.

Posted
 

Joose and node.js

Just saw that Joose can now be used a module within a node.js app....Joose looks cool, maybe with my new node.js interest, i'll try and add it into my playground :)

http://groups.google.com/group/joose-js/browse_thread/thread/30a61052b68f6485

Joose Home: http://code.google.com/p/joose-js/
Posted
Theme by Cory Watilo.
More great Posterous themes at themes.posterous.com.