0% found this document useful (0 votes)
59 views

Nodejs Controlling Flow

The document discusses flow control in Node.js applications using callbacks. It proposes a simple flow control library called "slide" that uses actors and callbacks to control asynchronous flow. Actors perform actions and pass errors to callbacks, while callbacks handle results. Examples are given for common patterns like asyncMap to run functions in parallel and chain to run them sequentially. The document also discusses reducing boilerplate by binding actor functions from arrays.

Uploaded by

Venkat Bitla
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
59 views

Nodejs Controlling Flow

The document discusses flow control in Node.js applications using callbacks. It proposes a simple flow control library called "slide" that uses actors and callbacks to control asynchronous flow. Actors perform actions and pass errors to callbacks, while callbacks handle results. Examples are given for common patterns like asyncMap to run functions in parallel and chain to run them sequentially. The document also discusses reducing boilerplate by binding actor functions from arrays.

Uploaded by

Venkat Bitla
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 63

Controlling Flow

callbacks made are easy

Thursday, October 28, 2010

EventEmitters are Easy


Responding to events is a solved problem
(At least for JavaScripters)

Very similar to DOM coding thing.on("event", doSomething) easy.


Thursday, October 28, 2010

callbacks are hard


Most common complaint about nodejs:
Ew, callbacks? Ugly nasty nesting indented forever spaghetti code? Just to open a le?! YOUVE GOTTA BE FRICKIN KIDDING ME.

Variant:
Why doesnt this work? <link to severely broken code>
Thursday, October 28, 2010

It's worse than that


Most *objects* in NodeJS are Event
Emitters (http server/client, etc.) (posix API, DNS lookups, etc.)

Most low level *functions* take callbacks. Beyond "hello, world" it gets tricky.
Thursday, October 28, 2010

What's actually hard?


Doing a bunch of things in a specic order. Knowing when stuff is done. Handling failures. Breaking up functionality into parts
(innitely nested inline callbacks)

Thursday, October 28, 2010

Common Mistakes
Abandoning convention and consistency. Putting all callbacks inline. Using libraries without grokking them. Trying to make async code look sync.*
*controversial In my opinion, promises are a perfectly ne way to solve this problem. But they're complicated under the hood, and making async code look sync can cause weird expectations. Also, people like to talk about a "Promise" like it's one kind of thing, when it's actually a very general pattern.
Thursday, October 28, 2010

ow control libraries
There are approximately 7 gillion ow Everyone likes their own the best. Obvious solution: Write your own. Let's do that now.
Thursday, October 28, 2010

control libraries in the node.js ecosystem.

This is a learning exercise


The goal is not to write the ideal ow
control library.

The goal is simplicity and understanding,


with very little magic.

Please dont hold questions for the end. Please do try this at home.
Thursday, October 28, 2010

First Priority: A Really Cool Name


Be descriptive, but not much to describe. So minimal, you can write it in a slide show. http://github.com/isaacs/slide-ow-control npm install slide Hellz.Yeah.
Thursday, October 28, 2010

Dene Conventions
Two kinds of functions:
Actors: Take action Callbacks: Get results

Essentially the continuation pattern.


APIs already, and it's very exible.

Resulting code *looks* similar to bers, but is *much* simpler to implement.

Bonus: node works this way in the lowlevel


Thursday, October 28, 2010

Callbacks
Simple responders Must always be prepared to handle errors!
(That's why it's the rst argument.)

Often inline anonymous, but not always. Can trap and call other callbacks with
Thursday, October 28, 2010

modied data, or to pass errors upwards.

Actors
Last argument is a callback. If any error occurs, and can't be handled,
pass it to the callback and return.

Must not throw. Return value ignored. return x ==> return cb(null, x) throw er ==> return cb(er)
Thursday, October 28, 2010

Actor Example
function actor (some, args, cb) { // last argument is callback // optional args: if (!cb && typeof(args) === "function") cb = args, args = [] // do something, and then: if (failed) cb(new Error( "failed!")) else cb(null, optionalData)

Thursday, October 28, 2010

Actor Example
// return true if a path is either // a symlink or a directory. function isLinkOrDir (path, cb) { fs.lstat(path, function (er, s) { if (er) return cb(er) return cb(null, s.isDirectory() || s.isSymbolicLink()) }) }

Thursday, October 28, 2010

Actor Example
// return true if a path is either // a symlink or a directory. function isLinkOrDir (path, cb) { fs.lstat(path, function (er, s) { if (er) return cb(er) return cb(null, s.isDirectory() || s.isSymbolicLink()) }) }

Thursday, October 28, 2010

Actor Example
// return true if a path is either // a symlink or a directory. function isLinkOrDir (path, cb) { fs.lstat(path, function (er, s) { if (er) return cb(er) return cb(null, s.isDirectory() || s.isSymbolicLink()) }) }

Thursday, October 28, 2010

Actor Example
// return true if a path is either // a symlink or a directory. function isLinkOrDir (path, cb) { fs.lstat(path, function (er, s) { if (er) return cb(er) return cb(null, s.isDirectory() || s.isSymbolicLink()) }) }

Thursday, October 28, 2010

Actor Composition
// return true if a path is either // a symlink or a directory, and also // ends in ".bak" function isLinkDirBak (path, cb) { return isLinkOrDir(path, function (er, ld) { return cb(er, ld && path.substr(-4) === ".bak") }) }

Thursday, October 28, 2010

Actor Composition
// return true if a path is either // a symlink or a directory, and also // ends in ".bak" function isLinkDirBak (path, cb) { return isLinkOrDir(path, function (er, ld) { return cb(er, ld && path.substr(-4) === ".bak") }) }

Thursday, October 28, 2010

Actor Composition
// return true if a path is either // a symlink or a directory, and also // ends in ".bak" function isLinkDirBak (path, cb) { return isLinkOrDir(path, function (er, ld) { return cb(er, ld && path.substr(-4) === ".bak") }) }

Thursday, October 28, 2010

usecase: asyncMap
I have a list of 10 les, and need to read all I have a dozen URLs, and need to fetch
of them, and then continue when they're all done. them all, and then continue when they're all done. a message to all of them, and then continue when that's done.

I have 4 connected users, and need to send


Thursday, October 28, 2010

usecase: asyncMap
I have a list of n things, and I need to

dosomething with all of them, in parallel, and get the results once they're all complete.

Thursday, October 28, 2010

function asyncMap (list, fn, cb_) { var n = list.length , results = [] , errState = null function cb (er, data) { if (errState) return if (er) return cb(errState = er) results.push(data) if (-- n === 0) return cb_(null, results) } list.forEach(function (l) { fn(l, cb) }) }
Thursday, October 28, 2010

function asyncMap (list, fn, cb_) { var n = list.length , results = [] , errState = null function cb (er, data) { if (errState) return if (er) return cb(errState = er) results.push(data) if (-- n === 0) return cb_(null, results) } list.forEach(function (l) { fn(l, cb) }) }
Thursday, October 28, 2010

function asyncMap (list, fn, cb_) { var n = list.length , results = [] , errState = null function cb (er, data) { if (errState) return if (er) return cb(errState = er) results.push(data) if (-- n === 0) return cb_(null, results) } list.forEach(function (l) { fn(l, cb) }) }
Thursday, October 28, 2010

function asyncMap (list, fn, cb_) { var n = list.length , results = [] , errState = null function cb (er, data) { if (errState) return if (er) return cb(errState = er) results.push(data) if (-- n === 0) return cb_(null, results) } list.forEach(function (l) { fn(l, cb) }) }
Thursday, October 28, 2010

usecase: asyncMap
function writeFiles (files, what, cb) { asyncMap( files , function (f, cb) { fs.writeFile(f,what,cb) } , cb ) } writeFiles([my,file,list], "foo", cb)

Thursday, October 28, 2010

asyncMap
note that asyncMap itself is an Actor
function, so you can asyncMap your asyncMaps, dawg. matter, but what if it does?

This implementation is ne if order doesn't

Thursday, October 28, 2010

asyncMap - ordered
close over the array index in the generated
cb function.

match up results to their original index.

Thursday, October 28, 2010

function asyncMap (list, fn, cb_) { var n = list.length , results = [] , errState = null function cbGen (i) { return function cb (er, data) { if (errState) return if (er) return cb(errState = er) results[i] = data if (-- n === 0) return cb_(null, results) }} list.forEach(function (l, i) { fn(l, cbGen(i)) })
Thursday, October 28, 2010

function asyncMap (list, fn, cb_) { var n = list.length , results = [] , errState = null function cbGen (i) { return function cb (er, data) { if (errState) return if (er) return cb(errState = er) results[i] = data if (-- n === 0) return cb_(null, results) }} list.forEach(function (l, i) { fn(l, cbGen(i)) })
Thursday, October 28, 2010

usecase: chain
I have to do a bunch of things, in order. Get If anything fails, do not continue.

db credentials out of a le, read the data from the db, write that data to another le.

Thursday, October 28, 2010

function chain (things, cb) { ;(function LOOP (i, len) { if (i >= len) return cb() things[i](function (er) { if (er) return cb(er) LOOP(i + 1, len) }) })(0, things.length) }

Thursday, October 28, 2010

function chain (things, cb) { ;(function LOOP (i, len) { if (i >= len) return cb() things[i](function (er) { if (er) return cb(er) LOOP(i + 1, len) }) })(0, things.length) }

Thursday, October 28, 2010

function chain (things, cb) { ;(function LOOP (i, len) { if (i >= len) return cb() things[i](function (er) { if (er) return cb(er) LOOP(i + 1, len) }) })(0, things.length) }

Thursday, October 28, 2010

function chain (things, cb) { ;(function LOOP (i, len) { if (i >= len) return cb() things[i](function (er) { if (er) return cb(er) LOOP(i + 1, len) }) })(0, things.length) }

Thursday, October 28, 2010

usecase: chain
Still have to provide an array of functions,
which is a lot of boilerplate, and a pita if your functions take args "function (cb){blah(a,b,c,cb)}"

Results are discarded, which is a bit lame. No way to branch.


Thursday, October 28, 2010

reducing boilerplate
convert an array of [fn, args] to an actor
that takes no arguments (except cb) our use-case.

A bit like Function#bind, but tailored for bindActor(obj, "method", a, b, c)


bindActor(fn, a, b, c) bindActor(obj, fn, a, b, c)

Thursday, October 28, 2010

function bindActor () { var args = Array.prototype.slice.call (arguments) // jswtf. , obj = null , fn if (typeof args[0] === "object") { obj = args.shift() fn = args.shift() if (typeof fn === "string") fn = obj[ fn ] } else fn = args.shift() return function (cb) { fn.apply(obj, args.concat(cb)) } }
Thursday, October 28, 2010

function bindActor () { var args = Array.prototype.slice.call (arguments) // jswtf. , obj = null , fn if (typeof args[0] === "object") { obj = args.shift() fn = args.shift() if (typeof fn === "string") fn = obj[ fn ] } else fn = args.shift() return function (cb) { fn.apply(obj, args.concat(cb)) } }
Thursday, October 28, 2010

function bindActor () { var args = Array.prototype.slice.call (arguments) // jswtf. , obj = null , fn if (typeof args[0] === "object") { obj = args.shift() fn = args.shift() if (typeof fn === "string") fn = obj[ fn ] } else fn = args.shift() return function (cb) { fn.apply(obj, args.concat(cb)) } }
Thursday, October 28, 2010

function bindActor () { var args = Array.prototype.slice.call (arguments) // jswtf. , obj = null , fn if (typeof args[0] === "object") { obj = args.shift() fn = args.shift() if (typeof fn === "string") fn = obj[ fn ] } else fn = args.shift() return function (cb) { fn.apply(obj, args.concat(cb)) } }
Thursday, October 28, 2010

bindActor
Some obvious areas for improvement. They wouldn't t on a slide. Left as an exercise for the reader.

Thursday, October 28, 2010

function chain (things, cb) { ;(function LOOP (i, len) { if (i >= len) return cb() if (Array.isArray(things[i])) things[i] = bindActor.apply (null, things[i]) things[i](function (er) { if (er) return cb(er) LOOP(i + 1, len) }) })(0, things.length) }

Thursday, October 28, 2010

function chain (things, cb) { ;(function LOOP (i, len) { if (i >= len) return cb() if (Array.isArray(things[i])) things[i] = bindActor.apply (null, things[i]) things[i](function (er) { if (er) return cb(er) LOOP(i + 1, len) }) })(0, things.length) }

Thursday, October 28, 2010

chain: branching
Skip over falsey arguments chain([ doThing && [thing,a,b,c]
, isFoo && [doFoo, "foo"] , subChain && [chain, [one, two]] ], cb)

Thursday, October 28, 2010

function chain (things, cb) { ;(function LOOP (i, len) { if (i >= len) return cb() if (Array.isArray(things[i])) things[i] = bindActor.apply (null, things[i]) if (!things[i]) return LOOP(i + 1, len) things[i](function (er) { if (er) return cb(er) LOOP(i + 1, len) }) })(0, things.length) }

Thursday, October 28, 2010

chain: tracking results


Supply an array to keep the results in. If you don't care, don't worry about it. Last result is always in results[results.length - 1] Just for kicks, let's also treat chain.rst and
chain.last as placeholders for the rst/last result up until that point.

Thursday, October 28, 2010

chain.first = {} ; chain.last = {} function chain (things, res, cb) { if (!cb) cb = res , res = [] ;(function LOOP (i, len) { if (i >= len) return cb(null,res) if (Array.isArray(things[i])) things[i] = bindActor.apply(null, things[i].map(function(i){ return (i===chain.first) ? res[0] : (i===chain.last) ? res[res.length - 1] : i })) if (!things[i]) return LOOP(i + 1, len) things[i](function (er, data) { res.push(er || data) if (er) return cb(er, res) LOOP(i + 1, len) }) })(0, things.length) }
Thursday, October 28, 2010

chain.first = {} ; chain.last = {} function chain (things, res, cb) { if (!cb) cb = res , res = [] ;(function LOOP (i, len) { if (i >= len) return cb(null,res) if (Array.isArray(things[i])) things[i] = bindActor.apply(null, things[i].map(function(i){ return (i===chain.first) ? res[0] : (i===chain.last) ? res[res.length - 1] : i })) if (!things[i]) return LOOP(i + 1, len) things[i](function (er, data) { res.push(er || data) if (er) return cb(er, res) LOOP(i + 1, len) }) })(0, things.length) }
Thursday, October 28, 2010

chain.first = {} ; chain.last = {} function chain (things, res, cb) { if (!cb) cb = res , res = [] ;(function LOOP (i, len) { if (i >= len) return cb(null,res) if (Array.isArray(things[i])) things[i] = bindActor.apply(null, things[i].map(function(i){ return (i===chain.first) ? res[0] : (i===chain.last) ? res[res.length - 1] : i })) if (!things[i]) return LOOP(i + 1, len) things[i](function (er, data) { res.push(er || data) if (er) return cb(er, res) LOOP(i + 1, len) }) })(0, things.length) }
Thursday, October 28, 2010

chain.first = {} ; chain.last = {} function chain (things, res, cb) { if (!cb) cb = res , res = [] ;(function LOOP (i, len) { if (i >= len) return cb(null,res) if (Array.isArray(things[i])) things[i] = bindActor.apply(null, things[i].map(function(i){ return (i===chain.first) ? res[0] : (i===chain.last) ? res[res.length - 1] : i })) if (!things[i]) return LOOP(i + 1, len) things[i](function (er, data) { res.push(er || data) if (er) return cb(er, res) LOOP(i + 1, len) }) })(0, things.length) }
Thursday, October 28, 2010

chain.first = {} ; chain.last = {} function chain (things, res, cb) { if (!cb) cb = res , res = [] ;(function LOOP (i, len) { if (i >= len) return cb(null,res) if (Array.isArray(things[i])) things[i] = bindActor.apply(null, things[i].map(function(i){ return (i===chain.first) ? res[0] : (i===chain.last) ? res[res.length - 1] : i })) if (!things[i]) return LOOP(i + 1, len) things[i](function (er, data) { res.push(er || data) if (er) return cb(er, res) LOOP(i + 1, len) }) })(0, things.length) }
Thursday, October 28, 2010

chain.first = {} ; chain.last = {} function chain (things, res, cb) { if (!cb) cb = res , res = [] ;(function LOOP (i, len) { if (i >= len) return cb(null,res) if (Array.isArray(things[i])) things[i] = bindActor.apply(null, things[i].map(function(i){ return (i===chain.first) ? res[0] : (i===chain.last) ? res[res.length - 1] : i })) if (!things[i]) return LOOP(i + 1, len) things[i](function (er, data) { res.push(er || data) if (er) return cb(er, res) LOOP(i + 1, len) Ok, this can't get any }) bigger or it won't t. })(0, things.length) }
Thursday, October 28, 2010

Non-trivial Use Case


Read number les in a directory Add the results together Ping a web service with the result Write the response to a le Delete the number les
Thursday, October 28, 2010

var chain = require("./chain.js") , asyncMap = require("./async-map.js") function myProgram (cb) { var res = [], last = chain.last , first = chain.first chain ( [ [fs, "readdir", "the-directory"] , [readFiles, "the-directory", last] , [sum, last] , [ping, "POST", "example.com", 80 , "/foo", last] , [fs, "writeFile", "result.txt", last] , [rmFiles, "./the-directory", first] ] , res , cb ) )
Thursday, October 28, 2010

var chain = require("./chain.js") , asyncMap = require("./async-map.js") function myProgram (cb) { var res = [], last = chain.last , first = chain.first chain ( [ [fs, "readdir", "the-directory"] , [readFiles, "the-directory", last] , [sum, last] , [ping, "POST", "example.com", 80 , "/foo", last] , [fs, "writeFile", "result.txt", last] , [rmFiles, "./the-directory", first] ] , res , cb ) )
Thursday, October 28, 2010

var chain = require("./chain.js") , asyncMap = require("./async-map.js") function myProgram (cb) { var res = [], last = chain.last , first = chain.first chain ( [ [fs, "readdir", "the-directory"] , [readFiles, "the-directory", last] , [sum, last] , [ping, "POST", "example.com", 80 , "/foo", last] , [fs, "writeFile", "result.txt", last] , [rmFiles, "./the-directory", first] ] , res , cb ) )
Thursday, October 28, 2010

var chain = require("./chain.js") , asyncMap = require("./async-map.js") function myProgram (cb) { var res = [], last = chain.last , first = chain.first chain ( [ [fs, "readdir", "the-directory"] , [readFiles, "the-directory", last] , [sum, last] , [ping, "POST", "example.com", 80 , "/foo", last] , [fs, "writeFile", "result.txt", last] , [rmFiles, "./the-directory", first] ] , res , cb ) )
Thursday, October 28, 2010

var chain = require("./chain.js") , asyncMap = require("./async-map.js") function myProgram (cb) { var res = [], last = chain.last , first = chain.first chain ( [ [fs, "readdir", "the-directory"] , [readFiles, "the-directory", last] , [sum, last] , [ping, "POST", "example.com", 80 , "/foo", last] , [fs, "writeFile", "result.txt", last] , [rmFiles, "./the-directory", first] ] , res , cb ) )
Thursday, October 28, 2010

var chain = require("./chain.js") , asyncMap = require("./async-map.js") function myProgram (cb) { var res = [], last = chain.last , first = chain.first chain ( [ [fs, "readdir", "the-directory"] , [readFiles, "the-directory", last] , [sum, last] , [ping, "POST", "example.com", 80 , "/foo", last] , [fs, "writeFile", "result.txt", last] , [rmFiles, "./the-directory", first] ] , res , cb ) )
Thursday, October 28, 2010

var chain = require("./chain.js") , asyncMap = require("./async-map.js") function myProgram (cb) { var res = [], last = chain.last , first = chain.first chain ( [ [fs, "readdir", "the-directory"] , [readFiles, "the-directory", last] , [sum, last] , [ping, "POST", "example.com", 80 , "/foo", last] , [fs, "writeFile", "result.txt", last] , [rmFiles, "./the-directory", first] ] , res , cb ) )
Thursday, October 28, 2010

Convention Prots
Consistent API from top to bottom. Sneak in at any point to inject functionality.
(testable, reusable, etc.)

When ruby and python users whine, you


can smile condescendingly.

Thursday, October 28, 2010

You might also like