grappling-hook
pre/post hooking enabler
grappling-hook
allows you to add pre/post hooks to objects and prototypes.
A number of modules already exist that allow you to do just the same, but the most popular one (hooks) is no longer maintained.
Also, we wanted a more granular control of the hooking process and the way middleware is called.
NEW:
Installation
$ npm install grappling-hook
Usage
Simply require('grappling-hook')
. Or require('grappling-hook/es6)
if you want the ES6 (yeah, yeah, ES2015 or whatever) version.
From here on grappling-hook
refers to the module itself (i.e. what you get when you require('grappling-hook')
) and GrapplingHook
refers to any GrapplingHook object (i.e. an object which allows you to register pre
and post
middleware, et cetera)
grappling-hook
and GrapplingHook
expose two different API's:
- a consumer-facing API, i.e. it allows you to add middleware functions to pre/post hooks.
- a producer-facing API, i.e. it allows you to create hooks, wrap methods with hooks, et cetera.
Consumer-facing API
Allows you to add/remove middleware functions to hooks. There's 4 types of middleware possible:
synchronous middleware
i.e. the function is executed and the next middleware function in queue will be called immediately.
function () {
}
serially (a)synchronous middleware
i.e. the next middleware function in queue will be called once the current middleware function finishes its (asynchronous) execution.
function (next) {
setTimeout(next, 1000);
}
parallel (a)synchronous middleware
i.e. the next middleware function in queue will be called once the current middleware function signals it, however the whole queue will only be finished once the current middleware function has completed its (a)synchronous execution.
function (next, done) {
setTimeout(next, 500);
setTimeout(done, 1000);
}
thenable middleware (promises)
i.e. the next middleware function in queue will be called once the thenable middleware function has resolved its promise.
function () {
return promise
}
(Sidenote: all consumer-facing methods exist out of a single word)
See:
All three allow you to register middleware functions by either passing them as parameters to the method:
instance.pre('save', notifyUser, checkPermissions, doSomethingElseVeryImportant);
Or (if the grappling-hook instances are setup for thenables) by chaining them with then
:
instance.pre('save')
.then(notifyUser)
.then(checkPermissions)
.then(doSomethingElseVeryImportant)
Additionally see:
Producer-facing API
grappling-hook
provides you with methods to store, retrieve and reuse presets.
All grappling-hook
factory functions allow you to reuse presets, see presets example.
See:
By default GrapplingHook
hooks need to be either explicitly declared with GrapplingHook#allowHooks if you want to call your hooks directly or by wrapping existing methods.
GrapplingHook
objects can have 3 kinds of hooks:
Asynchronous hooks
Asynchronous hooks require a callback as the final parameter. It will be called once all pre and post middleware has finished. When using a wrapped method, the original (unwrapped) method will be called in between the pre and post middleware.
Asynchronous hooks always finish asynchronously, i.e. even if only synchronous middleware has been registered to a hook callback
will always be called asynchronously (next tick at the earliest).
Middleware added to asynchronous hooks can be synchronous, serially asynchronous, parallel asynchronous or thenable. See middleware for more information.
See:
Synchronous hooks
Synchronous hooks do not require a callback and allow the possibility to return values from wrapped methods.
They always finish synchronously, which means consumers are not allowed to register any asynchronous middleware (including thenables) to synchronous hooks.
See:
Thenable hooks
Thenable hooks must return a promise.
They always finish asynchronously, i.e. even if only synchronous middleware has been registered to a thenable hook the promise will be resolved asynchronously.
Middleware added to thenable hooks can be synchronous, serially asynchronous, parallel asynchronous or thenable. See middleware for more information.
See:
In order to create thenable hooks grappling-hook
must be properly setup for creating thenables.
Introspection
You can check if a hook has middleware registered with GrapplingHook#hasMiddleware or you can even access the raw middleware functions through GrapplingHook#getMiddleware.
Examples
mix middleware types
You can mix sync/async serial/parallel and thenable middleware any way you choose (for aynchronous and thenable hooks):
instance.pre('save', function (next) {
console.log('async serial: setup');
setTimeout(function () {
console.log('async serial: done');
next();
}, 100);
}, function () {
console.log('sync: done');
}, function (next, done) {
console.log('async parallel: setup');
setTimeout(function () {
console.log('async parallel: done');
done();
}, 200);
next();
}, function () {
console.log('thenable: setup');
var done;
var promise = new P(function (resolve, fail) {
done = resolve;
});
setTimeout(function () {
console.log('thenable: done');
done();
}, 30);
return promise;
});
async serial: setup
async serial: done
sync: done
async parallel: setup
thenable: setup
thenable: done
async parallel: done
Creating a GrapplingHook
object
You can easily add methods to a new grappling-hook
instance which are automatically ready for hooking up middleware:
var grappling = require('grappling-hook');
var instance = grappling.create();
instance.addHooks({
save: function (done) {
console.log('save!');
done();
}
});
instance.pre('save', function () {
console.log('saving!');
}).post('save', function () {
console.log('saved!');
});
instance.save(function (err) {
console.log('All done!!');
});
saving!
save!
saved!
All done!!
Using an existing object
You can choose to enable hooking for an already existing object with methods:
var grappling = require('grappling-hook');
var instance = {
save: function (done) {
console.log('save!');
done();
}
};
grappling.mixin(instance);
instance.addHooks('save');
instance.pre('save', function () {
console.log('saving!');
}).post('save', function () {
console.log('saved!');
});
instance.save(function (err) {
console.log('All done!!');
});
saving!
save!
saved!
All done!!
Using a 'class'
You can patch a prototype
with grappling-hook
methods:
var grappling = require('grappling-hook');
var MyClass = function () {};
MyClass.prototype.save = function (done) {
console.log('save!');
done();
};
grappling.attach(MyClass);
var instance = new MyClass();
instance.addHooks('save');
instance.pre('save', function () {
console.log('saving!');
}).post('save', function () {
console.log('saved!');
});
instance.save(function (err) {
console.log('All done!!');
});
saving!
save!
saved!
All done!!
Adding hooks to synchronous methods
addSyncHooks
allows you to register methods for enforced synchronized middleware execution:
var grappling = require('grappling-hook');
var instance = {
saveSync: function (filename) {
filename = Date.now() + '-' + filename;
console.log('save', filename);
return filename;
}
};
grappling.mixin(instance);
instance.addSyncHooks('saveSync');
instance.pre('saveSync', function () {
console.log('saving!');
}).post('saveSync', function () {
console.log('saved!');
});
var newName = instance.saveSync('example.txt');
console.log('new name:', newName);
saving!
save 1431264587725-example.txt
saved!
new name: 1431264587725-example.txt
Passing parameters
You can pass any number of parameters to your middleware:
instance.pre('save', function (foo, bar) {
console.log('saving!', foo, bar);
});
instance.callHook('pre:save', 'foo', { bar: 'bar'}, function () {
console.log('done!');
});
saving! foo { bar: 'bar' }
done!
instance.save = function (filename, dir, done) {
done();
}
instance.pre('save', function (filename, dir) {
console.log('saving!', filename, dir);
});
instance.save('README.md', 'docs');
saving! README.md docs
Contexts
By default all middleware is called with the GrapplingHook
instance as an execution context, e.g.:
instance.pre('save', function () {
console.log(this);
});
instance.toString = function () {
return "That's me!!";
};
instance.callSyncHook('pre:save');
That's me!!
However, callHook
, callSyncHook
and callThenableHook
accept a context
parameter to change the scope:
instance.pre('save', function () {
console.log(this);
});
instance.toString = function () {
return "That's me!!";
};
var context = {
toString: function () {
return 'Different context!';
}
};
instance.callSyncHook(context, 'pre:save');
Different context!
All done!!
Lenient mode
By default grappling-hook
throws errors if you try to add middleware to or call a non-existing hook. However if you want to allow more leeway (for instance for dynamic delegated hook registration) you can turn on lenient mode:
var instance = grappling.create({
strict: false
});
Other qualifiers
By default grappling-hook
registers pre
and post
methods, but you can configure other names if you want:
var instance = grappling.create({
qualifiers: {
pre: 'before',
post: 'after'
}
});
instance.addHooks('save');
instance.before('save', fn);
instance.after('save', fn);
instance.save();
There's one caveat: you have to configure both or none.
Setting up thenable hooks
If you want to use thenable hooks, you'll need to provide grappling-hook
with a thenable factory function, since it's promise library agnostic (i.e. you can use it with any promise library you want).
Just to be clear: you do NOT need to provide a thenable factory function in order to allow thenable middleware, this works out of the box.
var P = require('bluebird');
var instance = grappling.create({
createThenable: function (fn) {
return new P(fn);
}
})
instance.addThenableHooks({
save: function (filename) {
var p = new P(function (resolve, reject) {
});
return p;
}
});
instance.save('examples.txt').then(function () {
console.log('Finished!');
});
Error handling
-
Errors thrown in middleware registered to synchronized hooks will bubble through
instance.pre('save', function () {
throw new Error('Oh noes!');
});
instance.callSyncHook('pre:save');
Error: Oh noes!
-
Errors thrown in middleware registered to asynchronous hooks are available as the err
object in the callback
.
instance.pre('save', function () {
throw new Error('Oh noes!');
});
instance.callHook('pre:save', function (err) {
console.log('Error occurred:', err);
});
Error occurred: Error: Oh noes!
-
Errors thrown in middleware registered to thenable hooks trigger the promise's rejectedHandler.
instance.pre('save', function () {
throw new Error('Oh noes!');
});
instance.callThenableHook('pre:save').then(null, function (err) {
console.log('Error occurred:', err);
});
Error occurred: Error: Oh noes!
-
Async middleware can pass errors to their next
(serial or parallel) or done
(parallel only) callbacks, which will be passed as the err
object parameter for asynchronous hooks:
instance.pre('save', function (next) {
next(new Error('Oh noes!'));
});
instance.pre('save', function (next, done) {
next();
done(new Error('Oh noes!'));
});
instance.callHook('pre:save', function (err) {
if (err) {
console.log('An error occurred:', err);
}
});
An error occurred: Oh noes!
-
Async middleware can pass errors to their next
(serial or parallel) or done
(parallel only) callbacks, which will trigger the rejectedHandler of thenable hooks:
instance.pre('save', function (next) {
next(new Error('Oh noes!'));
});
instance.pre('save', function (next, done) {
next();
done(new Error('Oh noes!'));
});
instance.callThenableHook('pre:save').then(null, function (err) {
if (err) {
console.log('An error occurred:', err);
}
});
An error occurred: Oh noes!
-
Thenable middleware can reject their promises, which will be passed as the err
object parameter for asynchronous hooks:
instance.pre('save', function (next) {
var p = new Promise(function (succeed, fail) {
fail('Oh noes!');
});
return p;
});
instance.callHook('pre:save', function (err) {
if (err) {
console.log('An error occurred:', err);
}
});
An error occurred: Oh noes!
-
Thenable middleware can reject their promises, which will trigger the rejectedHandler of thenable hooks:
instance.pre('save', function (next) {
var p = new Promise(function (succeed, fail) {
fail('Oh noes!');
});
return p;
});
instance.callThenableHook('pre:save').then(null, function (err) {
if (err) {
console.log('An error occurred:', err);
}
});
An error occurred: Oh noes!
Presets
You can set and use preset configurations, in order to reuse them in your project.
var presets = {
strict: false,
qualifiers: {
pre: 'before',
post: 'after'
}
};
var grappling = require('grappling-hook');
grappling.set('grappling-hook:examples.presets', presets);
var instance = grappling.create('grappling-hook:examples.presets');
instance.addSyncHooks({
save: function () {
console.log('Saving!');
}
});
instance.before('save', function () {
console.log('Before save!');
}).after('save', function () {
console.log('After save!');
}).save();
Before save!
Saving!
After save!
If you want to override preset configuration options, just pass them to the factory function, as always:
var instance = grappling.create('grappling-hook:examples.presets', {
strict: true
});
With grappling-hook.get you can introspect the configuration options of a preset:
console.log(grappling.get('grappling-hook:examples.presets'));
{
strict: false,
qualifiers: {
pre: 'before',
post: 'after'
}
}
Changelog
See History.md
Contributing
Pull requests welcome. Make sure you use the .editorconfig in your IDE of choice and please adhere to the coding style as defined in .eslintrc.
npm test
for running the testsnpm run lint
for running eslintnpm run test-cov
for churning out test coverage. (We go for 100% here!)npm run docs
for generating the API docs