98fe85bdedc188e300d38209ef113fa4a1620fb0
[aai/esr-gui.git] /
1 hooks
2 ============
3
4 Add pre and post middleware hooks to your JavaScript methods.
5
6 ## Installation
7     npm install hooks
8
9 ## Motivation
10 Suppose you have a JavaScript object with a `save` method.
11
12 It would be nice to be able to declare code that runs before `save` and after `save`.
13 For example, you might want to run validation code before every `save`,
14 and you might want to dispatch a job to a background job queue after `save`.
15
16 One might have an urge to hard code this all into `save`, but that turns out to
17 couple all these pieces of functionality (validation, save, and job creation) more
18 tightly than is necessary. For example, what if someone does not want to do background
19 job creation after the logical save?
20
21 It is nicer to tack on functionality using what we call `pre` and `post` hooks. These
22 are functions that you define and that you direct to execute before or after particular
23 methods.
24
25 ## Example
26 We can use `hooks` to add validation and background jobs in the following way:
27
28 ```javascript
29 var hooks = require('hooks')
30   , Document = require('./path/to/some/document/constructor');
31
32 // Add hooks' methods: `hook`, `pre`, and `post`
33 for (var k in hooks) {
34   Document[k] = hooks[k];
35 }
36
37 // Define a new method that is able to invoke pre and post middleware
38 Document.hook('save', Document.prototype.save);
39
40 // Define a middleware function to be invoked before 'save'
41 Document.pre('save', function validate (next) {
42   // The `this` context inside of `pre` and `post` functions
43   // is the Document instance
44   if (this.isValid()) next();      // next() passes control to the next middleware
45                                    // or to the target method itself
46   else next(new Error("Invalid")); // next(error) invokes an error callback
47 });
48
49 // Define a middleware function to be invoked after 'save'
50 Document.post('save', function createJob (next) {
51   this.sendToBackgroundQueue();
52   next();
53 });
54 ```
55
56 If you already have defined `Document.prototype` methods for which you want pres and posts,
57 then you do not need to explicitly invoke `Document.hook(...)`. Invoking `Document.pre(methodName, fn)`
58 or `Document.post(methodName, fn)` will automatically and lazily change `Document.prototype[methodName]`
59 so that it plays well with `hooks`. An equivalent way to implement the previous example is:
60
61 ```javascript
62 var hooks = require('hooks')
63   , Document = require('./path/to/some/document/constructor');
64
65 // Add hooks' methods: `hook`, `pre`, and `post`
66 for (var k in hooks) {
67   Document[k] = hooks[k];
68 }
69
70 Document.prototype.save = function () {
71   // ...
72 };
73
74 // Define a middleware function to be invoked before 'save'
75 Document.pre('save', function validate (next) {
76   // The `this` context inside of `pre` and `post` functions
77   // is the Document instance
78   if (this.isValid()) next();      // next() passes control to the next middleware
79                                    // or to the target method itself
80   else next(new Error("Invalid")); // next(error) invokes an error callback
81 });
82
83 // Define a middleware function to be invoked after 'save'
84 Document.post('save', function createJob (next) {
85   this.sendToBackgroundQueue();
86   next();
87 });
88 ```
89
90 ## Pres and Posts as Middleware
91 We structure pres and posts as middleware to give you maximum flexibility:
92
93 1. You can define **multiple** pres (or posts) for a single method.
94 2. These pres (or posts) are then executed as a chain of methods.
95 3. Any functions in this middleware chain can choose to halt the chain's execution by `next`ing an Error from that middleware function. If this occurs, then none of the other middleware in the chain will execute, and the main method (e.g., `save`) will not execute. This is nice, for example, when we don't want a document to save if it is invalid.
96
97 ## Defining multiple pres (or posts)
98 `pre` and `post` are chainable, so you can define multiple via:
99 ```javascript
100 Document.pre('save', function (next) {
101   console.log("hello");
102   next();
103 }).pre('save', function (next) {
104   console.log("world");
105   next();
106 });
107
108 Document.post('save', function (next) {
109   console.log("hello");
110   next();
111 }).post('save', function (next) {
112   console.log("world");
113   next();
114 });
115 ```
116
117 As soon as one pre finishes executing, the next one will be invoked, and so on.
118
119 ## Error Handling
120 You can define a default error handler by passing a 2nd function as the 3rd argument to `hook`:
121 ```javascript
122 Document.hook('set', function (path, val) {
123   this[path] = val;
124 }, function (err) {
125   // Handler the error here
126   console.error(err);
127 });
128 ```
129
130 Then, we can pass errors to this handler from a pre or post middleware function:
131 ```javascript
132 Document.pre('set', function (next, path, val) {
133   next(new Error());
134 });
135 ```
136
137 If you do not set up a default handler, then `hooks` makes the default handler that just throws the `Error`.
138
139 The default error handler can be over-rided on a per method invocation basis.
140
141 If the main method that you are surrounding with pre and post middleware expects its last argument to be a function
142 with callback signature `function (error, ...)`, then that callback becomes the error handler, over-riding the default
143 error handler you may have set up.
144
145 ```javascript
146 Document.hook('save', function (callback) {
147   // Save logic goes here
148   ...
149 });
150
151 var doc = new Document();
152 doc.save( function (err, saved) {
153   // We can pass err via `next` in any of our pre or post middleware functions
154   if (err) console.error(err);
155
156   // Rest of callback logic follows ...
157 });
158 ```
159
160 ## Mutating Arguments via Middleware
161 `pre` and `post` middleware can also accept the intended arguments for the method
162 they augment. This is useful if you want to mutate the arguments before passing
163 them along to the next middleware and eventually pass a mutated arguments list to
164 the main method itself.
165
166 As a simple example, let's define a method `set` that just sets a key, value pair.
167 If we want to namespace the key, we can do so by adding a `pre` middleware hook
168 that runs before `set`, alters the arguments by namespacing the `key` argument, and passes them onto `set`:
169
170 ```javascript
171 Document.hook('set', function (key, val) {
172   this[key] = val;
173 });
174 Document.pre('set', function (next, key, val) {
175   next('namespace-' + key, val);
176 });
177 var doc = new Document();
178 doc.set('hello', 'world');
179 console.log(doc.hello); // undefined
180 console.log(doc['namespace-hello']); // 'world'
181 ```
182
183 As you can see above, we pass arguments via `next`.
184
185 If you are not mutating the arguments, then you can pass zero arguments
186 to `next`, and the next middleware function will still have access
187 to the arguments.
188
189 ```javascript
190 Document.hook('set', function (key, val) {
191   this[key] = val;
192 });
193 Document.pre('set', function (next, key, val) {
194   // I have access to key and val here
195   next(); // We don't need to pass anything to next
196 });
197 Document.pre('set', function (next, key, val) {
198   // And I still have access to the original key and val here
199   next();
200 });
201 ```
202
203 Finally, you can add arguments that downstream middleware can also see:
204
205 ```javascript
206 // Note that in the definition of `set`, there is no 3rd argument, options
207 Document.hook('set', function (key, val) {
208   // But...
209   var options = arguments[2]; // ...I have access to an options argument
210                               // because of pre function pre2 (defined below)
211   console.log(options); // '{debug: true}'
212   this[key] = val;
213 });
214 Document.pre('set', function pre1 (next, key, val) {
215   // I only have access to key and val arguments
216   console.log(arguments.length); // 3
217   next(key, val, {debug: true});
218 });
219 Document.pre('set', function pre2 (next, key, val, options) {
220   console.log(arguments.length); // 4
221   console.log(options); // '{ debug: true}'
222   next();
223 });
224 Document.pre('set', function pre3 (next, key, val, options) {
225   // I still have access to key, val, AND the options argument introduced via the preceding middleware
226   console.log(arguments.length); // 4
227   console.log(options); // '{ debug: true}'
228   next();
229 });
230
231 var doc = new Document()
232 doc.set('hey', 'there');
233 ```
234
235 ## Post middleware
236
237 Post middleware intercepts the callback originally sent to the asynchronous function you have hooked to.
238
239 This means that the following chain of execution will occur in a typical `save` operation:
240
241 (1) doc.save -> (2) pre --(next)--> (3) save calls back -> (4) post --(next)--> (5) targetFn
242
243 Illustrated below:
244
245 ```
246 Document.pre('save', function (next) {
247   this.key = "value";
248   next();
249 });
250 // Post handler occurs before `set` calls back. This is useful if we need to grab something
251 // async before `set` finishes.
252 Document.post('set', function (next) {
253   var me = this;
254   getSomethingAsync(function(value){ // let's assume it returns "Hello Async"
255     me.key2 = value;
256     next();
257   });
258 });
259
260 var doc = new Document();
261 doc.save(function(err){
262   console.log(this.key);  // "value" - this value was saved
263   console.log(this.key2); // "Hello Async" - this value was *not* saved
264 }
265
266 ```
267
268 Post middleware must call `next()` or execution will stop.
269
270 ## Parallel `pre` middleware
271
272 All middleware up to this point has been "serial" middleware -- i.e., middleware whose logic
273 is executed as a serial chain.
274
275 Some scenarios call for parallel middleware -- i.e., middleware that can wait for several
276 asynchronous services at once to respond.
277
278 For instance, you may only want to save a Document only after you have checked
279 that the Document is valid according to two different remote services.
280
281 We accomplish asynchronous middleware by adding a second kind of flow control callback
282 (the only flow control callback so far has been `next`), called `done`.
283
284 - `next` passes control to the next middleware in the chain
285 - `done` keeps track of how many parallel middleware have invoked `done` and passes
286    control to the target method when ALL parallel middleware have invoked `done`. If
287    you pass an `Error` to `done`, then the error is handled, and the main method that is
288    wrapped by pres and posts will not get invoked.
289
290 We declare pre middleware that is parallel by passing a 3rd boolean argument to our `pre`
291 definition method.
292
293 We illustrate via the parallel validation example mentioned above:
294
295 ```javascript
296 Document.hook('save', function targetFn (callback) {
297   // Save logic goes here
298   // ...
299   // This only gets run once the two `done`s are both invoked via preOne and preTwo.
300 });
301
302                      // true marks this as parallel middleware
303 Document.pre('save', true, function preOne (next, doneOne, callback) {
304   remoteServiceOne.validate(this.serialize(), function (err, isValid) {
305     // The code in here will probably be run after the `next` below this block
306     // and could possibly be run after the console.log("Hola") in `preTwo
307     if (err) return doneOne(err);
308     if (isValid) doneOne();
309   });
310   next(); // Pass control to the next middleware
311 });
312
313 // We will suppose that we need 2 different remote services to validate our document
314 Document.pre('save', true, function preTwo (next, doneTwo, callback) {
315   remoteServiceTwo.validate(this.serialize(), function (err, isValid) {
316     if (err) return doneTwo(err);
317     if (isValid) doneTwo();
318   });
319   next();
320 });
321
322 // While preOne and preTwo are parallel, preThree is a serial pre middleware
323 Document.pre('save', function preThree (next, callback) {
324   next();
325 });
326
327 var doc = new Document();
328 doc.save( function (err, doc) {
329   // Do stuff with the saved doc here...
330 });
331 ```
332
333 In the above example, flow control may happen in the following way:
334
335 (1) doc.save -> (2) preOne --(next)--> (3) preTwo --(next)--> (4) preThree --(next)--> (wait for dones to invoke) -> (5) doneTwo -> (6) doneOne -> (7) targetFn
336
337 So what's happening is that:
338
339 1. You call `doc.save(...)`
340 2. First, your preOne middleware gets executed. It makes a remote call to the validation service and `next()`s to the preTwo middleware.
341 3. Now, your preTwo middleware gets executed. It makes a remote call to another validation service and `next()`s to the preThree middleware.
342 4. Your preThree middleware gets executed. It immediately `next()`s. But nothing else gets executing until both `doneOne` and `doneTwo` are invoked inside the callbacks handling the response from the two valiation services.
343 5. We will suppose that validation remoteServiceTwo returns a response to us first. In this case, we call `doneTwo` inside the callback to remoteServiceTwo.
344 6. Some fractions of a second later, remoteServiceOne returns a response to us. In this case, we call `doneOne` inside the callback to remoteServiceOne.
345 7. `hooks` implementation keeps track of how many parallel middleware has been defined per target function. It detects that both asynchronous pre middlewares (`preOne` and `preTwo`) have finally called their `done` functions (`doneOne` and `doneTwo`), so the implementation finally invokes our `targetFn` (i.e., our core `save` business logic).
346
347 ## Removing Pres
348
349 You can remove a particular pre associated with a hook:
350
351     Document.pre('set', someFn);
352     Document.removePre('set', someFn);
353
354 And you can also remove all pres associated with a hook:
355     Document.removePre('set'); // Removes all declared `pre`s on the hook 'set'
356
357 ## Tests
358 To run the tests:
359     make test
360
361 ### Contributors
362 - [Brian Noguchi](https://github.com/bnoguchi)
363
364 ### License
365 MIT License
366
367 ---
368 ### Author
369 Brian Noguchi