0 | |
# Slide - a tiny flow control library
|
|
0 |
# Controlling Flow: callbacks are easy
|
1 | 1 |
|
2 | |
Callbacks are simple and easy if you keep the pattern consistent.
|
|
2 |
## What's actually hard?
|
3 | 3 |
|
4 | |
Check out the [slide presentation](http://github.com/isaacs/slide-flow-control/raw/master/nodejs-controlling-flow.pdf).
|
|
4 |
- Doing a bunch of things in a specific order.
|
|
5 |
- Knowing when stuff is done.
|
|
6 |
- Handling failures.
|
|
7 |
- Breaking up functionality into parts (avoid nested inline callbacks)
|
5 | 8 |
|
6 | |
You'll laugh when you see how little code is actually in this thing.
|
7 | |
It's so not-enterprisey, you won't believe it. It does almost nothing,
|
8 | |
but it's super handy.
|
9 | 9 |
|
10 | |
I actually use an earlier version of this util in
|
11 | |
[a real world program](http://npmjs.org/).
|
|
10 |
## Common Mistakes
|
12 | 11 |
|
13 | |
## Installation
|
|
12 |
- Abandoning convention and consistency.
|
|
13 |
- Putting all callbacks inline.
|
|
14 |
- Using libraries without grokking them.
|
|
15 |
- Trying to make async code look sync.
|
14 | 16 |
|
15 | |
Just copy the files into your project, and use them that way, or
|
16 | |
you can do this:
|
|
17 |
## Define Conventions
|
17 | 18 |
|
18 | |
npm install slide
|
|
19 |
- Two kinds of functions: *actors* take action, *callbacks* get results.
|
|
20 |
- Essentially the continuation pattern. Resulting code *looks* similar
|
|
21 |
to fibers, but is *much* simpler to implement.
|
|
22 |
- Node works this way in the lowlevel APIs already, and it's very flexible.
|
19 | 23 |
|
20 | |
and then:
|
|
24 |
## Callbacks
|
21 | 25 |
|
22 | |
var asyncMap = require("slide").asyncMap
|
23 | |
, chain = require("slide").chain
|
24 | |
// use the power!
|
|
26 |
- Simple responders
|
|
27 |
- Must always be prepared to handle errors, that's why it's the first argument.
|
|
28 |
- Often inline anonymous, but not always.
|
|
29 |
- Can trap and call other callbacks with modified data, or pass errors upwards.
|
25 | 30 |
|
26 | |
Enjoy!
|
|
31 |
## Actors
|
|
32 |
|
|
33 |
- Last argument is a callback.
|
|
34 |
- If any error occurs, and can't be handled, pass it to the callback and return.
|
|
35 |
- Must not throw. Return value ignored.
|
|
36 |
- return x ==> return cb(null, x)
|
|
37 |
- throw er ==> return cb(er)
|
|
38 |
|
|
39 |
```javascript
|
|
40 |
// return true if a path is either
|
|
41 |
// a symlink or a directory.
|
|
42 |
function isLinkOrDir (path, cb) {
|
|
43 |
fs.lstat(path, function (er, s) {
|
|
44 |
if (er) return cb(er)
|
|
45 |
return cb(null, s.isDirectory() || s.isSymbolicLink())
|
|
46 |
})
|
|
47 |
}
|
|
48 |
```
|
|
49 |
|
|
50 |
# asyncMap
|
|
51 |
|
|
52 |
## Usecases
|
|
53 |
|
|
54 |
- I have a list of 10 files, and need to read all of them, and then continue when they're all done.
|
|
55 |
- I have a dozen URLs, and need to fetch them all, and then continue when they're all done.
|
|
56 |
- I have 4 connected users, and need to send a message to all of them, and then continue when that's done.
|
|
57 |
- 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.
|
|
58 |
|
|
59 |
|
|
60 |
## Solution
|
|
61 |
|
|
62 |
```javascript
|
|
63 |
var asyncMap = require("slide").asyncMap
|
|
64 |
function writeFiles (files, what, cb) {
|
|
65 |
asyncMap(files, function (f, cb) {
|
|
66 |
fs.writeFile(f, what, cb)
|
|
67 |
}, cb)
|
|
68 |
}
|
|
69 |
writeFiles([my, file, list], "foo", cb)
|
|
70 |
```
|
|
71 |
|
|
72 |
# chain
|
|
73 |
|
|
74 |
## Usecases
|
|
75 |
|
|
76 |
- I have to do a bunch of things, in order. Get db credentials out of a file,
|
|
77 |
read the data from the db, write that data to another file.
|
|
78 |
- If anything fails, do not continue.
|
|
79 |
- I still have to provide an array of functions, which is a lot of boilerplate,
|
|
80 |
and a pita if your functions take args like
|
|
81 |
|
|
82 |
```javascript
|
|
83 |
function (cb) {
|
|
84 |
blah(a, b, c, cb)
|
|
85 |
}
|
|
86 |
```
|
|
87 |
|
|
88 |
- Results are discarded, which is a bit lame.
|
|
89 |
- No way to branch.
|
|
90 |
|
|
91 |
## Solution
|
|
92 |
|
|
93 |
- reduces boilerplate by converting an array of [fn, args] to an actor
|
|
94 |
that takes no arguments (except cb)
|
|
95 |
- A bit like Function#bind, but tailored for our use-case.
|
|
96 |
- bindActor(obj, "method", a, b, c)
|
|
97 |
- bindActor(fn, a, b, c)
|
|
98 |
- bindActor(obj, fn, a, b, c)
|
|
99 |
- branching, skipping over falsey arguments
|
|
100 |
|
|
101 |
```javascript
|
|
102 |
chain([
|
|
103 |
doThing && [thing, a, b, c]
|
|
104 |
, isFoo && [doFoo, "foo"]
|
|
105 |
, subChain && [chain, [one, two]]
|
|
106 |
], cb)
|
|
107 |
```
|
|
108 |
|
|
109 |
- tracking results: results are stored in an optional array passed as argument,
|
|
110 |
last result is always in results[results.length - 1].
|
|
111 |
- treat chain.first and chain.last as placeholders for the first/last
|
|
112 |
result up until that point.
|
|
113 |
|
|
114 |
|
|
115 |
## Non-trivial example
|
|
116 |
|
|
117 |
- Read number files in a directory
|
|
118 |
- Add the results together
|
|
119 |
- Ping a web service with the result
|
|
120 |
- Write the response to a file
|
|
121 |
- Delete the number files
|
|
122 |
|
|
123 |
```javascript
|
|
124 |
var chain = require("slide").chain
|
|
125 |
function myProgram (cb) {
|
|
126 |
var res = [], last = chain.last, first = chain.first
|
|
127 |
chain([
|
|
128 |
[fs, "readdir", "the-directory"]
|
|
129 |
, [readFiles, "the-directory", last]
|
|
130 |
, [sum, last]
|
|
131 |
, [ping, "POST", "example.com", 80, "/foo", last]
|
|
132 |
, [fs, "writeFile", "result.txt", last]
|
|
133 |
, [rmFiles, "./the-directory", first]
|
|
134 |
], res, cb)
|
|
135 |
}
|
|
136 |
```
|
|
137 |
|
|
138 |
# Conclusion: Convention Profits
|
|
139 |
|
|
140 |
- Consistent API from top to bottom.
|
|
141 |
- Sneak in at any point to inject functionality. Testable, reusable, ...
|
|
142 |
- When ruby and python users whine, you can smile condescendingly.
|