Add pre and post middleware hooks to your JavaScript methods.
Installationnpm install hooks
Motivation
Suppose you have a JavaScript object with a save
method.
It would be nice to be able to declare code that runs before save
and after save
. For example, you might want to run validation code before every save
, and you might want to dispatch a job to a background job queue after save
.
One might have an urge to hard code this all into save
, but that turns out to couple all these pieces of functionality (validation, save, and job creation) more tightly than is necessary. For example, what if someone does not want to do background job creation after the logical save?
It is nicer to tack on functionality using what we call pre
and post
hooks. These are functions that you define and that you direct to execute before or after particular methods.
We can use hooks
to add validation and background jobs in the following way:
var hooks = require('hooks')
  , Document = require('./path/to/some/document/constructor');
Â
for (var k in hooks) {
  Document[k] = hooks[k];
}
Â
Document.hook('save', Document.prototype.save);
Â
Document.pre('save', function validate (next) {
 Â
 Â
  if (this.isValid()) next();     Â
                                  Â
  else next(new Error("Invalid"));Â
});
Â
Document.post('save', function createJob (next) {
  this.sendToBackgroundQueue();
  next();
});
If you already have defined Document.prototype
methods for which you want pres and posts, then you do not need to explicitly invoke Document.hook(...)
. Invoking Document.pre(methodName, fn)
or Document.post(methodName, fn)
will automatically and lazily change Document.prototype[methodName]
so that it plays well with hooks
. An equivalent way to implement the previous example is:
Pres and Posts as Middlewarevar hooks = require('hooks')
  , Document = require('./path/to/some/document/constructor');
Â
for (var k in hooks) {
  Document[k] = hooks[k];
}
Â
Document.prototype.save = function () {
 Â
};
Â
Document.pre('save', function validate (next) {
 Â
 Â
  if (this.isValid()) next();     Â
                                  Â
  else next(new Error("Invalid"));Â
});
Â
Document.post('save', function createJob (next) {
  this.sendToBackgroundQueue();
  next();
});
We structure pres and posts as middleware to give you maximum flexibility:
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.pre
and post
are chainable, so you can define multiple via:
Document.pre('save', function (next) {
  console.log("hello");
  next();
}).pre('save', function (next) {
  console.log("world");
  next();
});
Â
Document.post('save', function (next) {
  console.log("hello");
  next();
}).post('save', function (next) {
  console.log("world");
  next();
});
As soon as one pre finishes executing, the next one will be invoked, and so on.
Error HandlingYou can define a default error handler by passing a 2nd function as the 3rd argument to hook
:
Document.hook('set', function (path, val) {
  this[path] = val;
}, function (err) {
 Â
  console.error(err);
});
Then, we can pass errors to this handler from a pre or post middleware function:
Document.pre('set', function (next, path, val) {
  next(new Error());
});
If you do not set up a default handler, then hooks
makes the default handler that just throws the Error
.
The default error handler can be over-rided on a per method invocation basis.
If the main method that you are surrounding with pre and post middleware expects its last argument to be a function with callback signature function (error, ...)
, then that callback becomes the error handler, over-riding the default error handler you may have set up.
Mutating Arguments via MiddlewareDocument.hook('save', function (callback) {
 Â
  ...
});
Â
var doc = new Document();
doc.save( function (err, saved) {
 Â
  if (err) console.error(err);
Â
 Â
});
pre
and post
middleware can also accept the intended arguments for the method they augment. This is useful if you want to mutate the arguments before passing them along to the next middleware and eventually pass a mutated arguments list to the main method itself.
As a simple example, let's define a method set
that just sets a key, value pair. If we want to namespace the key, we can do so by adding a pre
middleware hook that runs before set
, alters the arguments by namespacing the key
argument, and passes them onto set
:
Document.hook('set', function (key, val) {
  this[key] = val;
});
Document.pre('set', function (next, key, val) {
  next('namespace-' + key, val);
});
var doc = new Document();
doc.set('hello', 'world');
console.log(doc.hello);Â
console.log(doc['namespace-hello']);Â
As you can see above, we pass arguments via next
.
If you are not mutating the arguments, then you can pass zero arguments to next
, and the next middleware function will still have access to the arguments.
Document.hook('set', function (key, val) {
  this[key] = val;
});
Document.pre('set', function (next, key, val) {
 Â
  next();Â
});
Document.pre('set', function (next, key, val) {
 Â
  next();
});
Finally, you can add arguments that downstream middleware can also see:
Post middlewareDocument.hook('set', function (key, val) {
 Â
  var options = arguments[2];Â
                             Â
  console.log(options);Â
  this[key] = val;
});
Document.pre('set', function pre1 (next, key, val) {
 Â
  console.log(arguments.length);Â
  next(key, val, {debug: true});
});
Document.pre('set', function pre2 (next, key, val, options) {
  console.log(arguments.length);Â
  console.log(options);Â
  next();
});
Document.pre('set', function pre3 (next, key, val, options) {
 Â
  console.log(arguments.length);Â
  console.log(options);Â
  next();
});
Â
var doc = new Document()
doc.set('hey', 'there');
Post middleware intercepts the callback originally sent to the asynchronous function you have hooked to.
This means that the following chain of execution will occur in a typical save
operation:
(1) doc.save -> (2) pre --(next)--> (3) save calls back -> (4) post --(next)--> (5) targetFn
Illustrated below:
Document.pre('save', function (next) {
this.key = "value";
next();
});
// Post handler occurs before `set` calls back. This is useful if we need to grab something
// async before `set` finishes.
Document.post('set', function (next) {
var me = this;
getSomethingAsync(function(value){ // let's assume it returns "Hello Async"
me.key2 = value;
next();
});
});
var doc = new Document();
doc.save(function(err){
console.log(this.key); // "value" - this value was saved
console.log(this.key2); // "Hello Async" - this value was *not* saved
}
Post middleware must call next()
or execution will stop.
pre
middleware
All middleware up to this point has been "serial" middleware -- i.e., middleware whose logic is executed as a serial chain.
Some scenarios call for parallel middleware -- i.e., middleware that can wait for several asynchronous services at once to respond.
For instance, you may only want to save a Document only after you have checked that the Document is valid according to two different remote services.
We accomplish asynchronous middleware by adding a second kind of flow control callback (the only flow control callback so far has been next
), called done
.
next
passes control to the next middleware in the chaindone
keeps track of how many parallel middleware have invoked done
and passes control to the target method when ALL parallel middleware have invoked done
. If you pass an Error
to done
, then the error is handled, and the main method that is wrapped by pres and posts will not get invoked.We declare pre middleware that is parallel by passing a 3rd boolean argument to our pre
definition method.
We illustrate via the parallel validation example mentioned above:
Document.hook('save', function targetFn (callback) {
 Â
 Â
 Â
});
Â
                    Â
Document.pre('save', true, function preOne (next, doneOne, callback) {
  remoteServiceOne.validate(this.serialize(), function (err, isValid) {
   Â
   Â
    if (err) return doneOne(err);
    if (isValid) doneOne();
  });
  next();Â
});
Â
Document.pre('save', true, function preTwo (next, doneTwo, callback) {
  remoteServiceTwo.validate(this.serialize(), function (err, isValid) {
    if (err) return doneTwo(err);
    if (isValid) doneTwo();
  });
  next();
});
Â
Document.pre('save', function preThree (next, callback) {
  next();
});
Â
var doc = new Document();
doc.save( function (err, doc) {
 Â
});
In the above example, flow control may happen in the following way:
(1) doc.save -> (2) preOne --(next)--> (3) preTwo --(next)--> (4) preThree --(next)--> (wait for dones to invoke) -> (5) doneTwo -> (6) doneOne -> (7) targetFn
So what's happening is that:
doc.save(...)
next()
s to the preTwo middleware.next()
s to the preThree middleware.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.doneTwo
inside the callback to remoteServiceTwo.doneOne
inside the callback to remoteServiceOne.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).You can remove a particular pre associated with a hook:
Document.pre('set', someFn);
Document.removePre('set', someFn);
And you can also remove all pres associated with a hook: Document.removePre('set'); // Removes all declared pre
s on the hook 'set'
To run the tests: make test
Contributors LicenseMIT License
AuthorBrian Noguchi
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4