kareem
v3.2.0
Published
Next-generation take on pre/post function hooks
Readme
kareem
Re-imagined take on the hooks module, meant to offer additional flexibility in allowing you to execute hooks whenever necessary, as opposed to simply wrapping a single function.
Named for the NBA's 2nd all-time leading scorer Kareem Abdul-Jabbar, known for his mastery of the hook shot
API
pre hooks
Much like hooks, kareem lets you define
pre and post hooks: pre hooks are called before a given function executes.
Unlike hooks, kareem stores hooks and other internal state in a separate
object, rather than relying on inheritance. Furthermore, kareem exposes
an execPre() function that allows you to execute your pre hooks when
appropriate, giving you more fine-grained control over your function hooks.
It runs without any hooks specified
await hooks.execPre('cook', null);It runs basic serial pre hooks
pre hook functions can return a promise that resolves when finished.
let count = 0;
hooks.pre('cook', function() {
++count;
return Promise.resolve();
});
await hooks.execPre('cook', null);
assert.equal(1, count);It can run multiple pre hooks
let count1 = 0;
let count2 = 0;
hooks.pre('cook', function() {
++count1;
return Promise.resolve();
});
hooks.pre('cook', function() {
++count2;
return Promise.resolve();
});
await hooks.execPre('cook', null);
assert.equal(1, count1);
assert.equal(1, count2);It can run fully synchronous pre hooks
If your pre hook function takes no parameters, its assumed to be fully synchronous.
let count1 = 0;
let count2 = 0;
hooks.pre('cook', function() {
++count1;
});
hooks.pre('cook', function() {
++count2;
});
await hooks.execPre('cook', null);
assert.equal(1, count1);
assert.equal(1, count2);It properly attaches context to pre hooks
Pre save hook functions are bound to the second parameter to execPre()
hooks.pre('cook', function() {
this.bacon = 3;
});
hooks.pre('cook', function() {
this.eggs = 4;
});
const obj = { bacon: 0, eggs: 0 };
// In the pre hooks, `this` will refer to `obj`
await hooks.execPre('cook', obj);
assert.equal(3, obj.bacon);
assert.equal(4, obj.eggs);It supports returning a promise
You can also return a promise from your pre hooks instead of calling
next(). When the returned promise resolves, kareem will kick off the
next middleware.
hooks.pre('cook', function() {
return new Promise(resolve => {
setTimeout(() => {
this.bacon = 3;
resolve();
}, 100);
});
});
const obj = { bacon: 0 };
await hooks.execPre('cook', obj);
assert.equal(3, obj.bacon);It supports filtering which hooks to run
You can pass a filter option to execPre() to select which hooks
to run. The filter function receives each hook object and should return
true to run the hook or false to skip it.
const execed = [];
const fn1 = function() { execed.push('first'); };
fn1.skipMe = true;
hooks.pre('cook', fn1);
const fn2 = function() { execed.push('second'); };
hooks.pre('cook', fn2);
// Only runs fn2, skips fn1 because fn1.skipMe is true
await hooks.execPre('cook', null, [], {
filter: hook => !hook.fn.skipMe
});
assert.deepStrictEqual(execed, ['second']);post hooks
It runs without any hooks specified
const [eggs] = await hooks.execPost('cook', null, [1]);
assert.equal(eggs, 1);It executes with parameters passed in
hooks.post('cook', function(eggs, bacon, callback) {
assert.equal(eggs, 1);
assert.equal(bacon, 2);
callback();
});
const [eggs, bacon] = await hooks.execPost('cook', null, [1, 2]);
assert.equal(eggs, 1);
assert.equal(bacon, 2);It can use synchronous post hooks
const execed = {};
hooks.post('cook', function(eggs, bacon) {
execed.first = true;
assert.equal(eggs, 1);
assert.equal(bacon, 2);
});
hooks.post('cook', function(eggs, bacon, callback) {
execed.second = true;
assert.equal(eggs, 1);
assert.equal(bacon, 2);
callback();
});
const [eggs, bacon] = await hooks.execPost('cook', null, [1, 2]);
assert.equal(Object.keys(execed).length, 2);
assert.ok(execed.first);
assert.ok(execed.second);
assert.equal(eggs, 1);
assert.equal(bacon, 2);It supports returning a promise
You can also return a promise from your post hooks instead of calling
next(). When the returned promise resolves, kareem will kick off the
next middleware.
hooks.post('cook', function() {
return new Promise(resolve => {
setTimeout(() => {
this.bacon = 3;
resolve();
}, 100);
});
});
const obj = { bacon: 0 };
await hooks.execPost('cook', obj, [obj]);
assert.equal(obj.bacon, 3);It supports filtering which hooks to run
You can pass a filter option to execPost() to select which hooks
to run. The filter function receives each hook object and should return
true to run the hook or false to skip it.
const execed = [];
const fn1 = function() { execed.push('first'); };
fn1.skipMe = true;
hooks.post('cook', fn1);
const fn2 = function() { execed.push('second'); };
hooks.post('cook', fn2);
// Only runs fn2, skips fn1 because fn1.skipMe is true
await hooks.execPost('cook', null, [], {
filter: hook => !hook.fn.skipMe
});
assert.deepStrictEqual(execed, ['second']);wrap()
It wraps pre and post calls into one call
hooks.pre('cook', function() {
return new Promise(resolve => {
this.bacon = 3;
setTimeout(() => {
resolve();
}, 5);
});
});
hooks.pre('cook', function() {
this.eggs = 4;
return Promise.resolve();
});
hooks.pre('cook', function() {
this.waffles = false;
return Promise.resolve();
});
hooks.post('cook', function(obj) {
obj.tofu = 'no';
});
const obj = { bacon: 0, eggs: 0 };
const args = [obj];
const result = await hooks.wrap(
'cook',
function(o) {
assert.equal(obj.bacon, 3);
assert.equal(obj.eggs, 4);
assert.equal(obj.waffles, false);
assert.equal(obj.tofu, undefined);
return o;
},
obj,
args);
assert.equal(obj.bacon, 3);
assert.equal(obj.eggs, 4);
assert.equal(obj.waffles, false);
assert.equal(obj.tofu, 'no');
assert.equal(result, obj);createWrapper()
It wraps wrap() into a callable function
hooks.pre('cook', function() {
this.bacon = 3;
return Promise.resolve();
});
hooks.pre('cook', function() {
return new Promise(resolve => {
this.eggs = 4;
setTimeout(function() {
resolve();
}, 10);
});
});
hooks.pre('cook', function() {
this.waffles = false;
return Promise.resolve();
});
hooks.post('cook', function(obj) {
obj.tofu = 'no';
});
const obj = { bacon: 0, eggs: 0 };
const cook = hooks.createWrapper(
'cook',
function(o) {
assert.equal(3, obj.bacon);
assert.equal(4, obj.eggs);
assert.equal(false, obj.waffles);
assert.equal(undefined, obj.tofu);
return o;
},
obj);
const result = await cook(obj);
assert.equal(obj.bacon, 3);
assert.equal(obj.eggs, 4);
assert.equal(obj.waffles, false);
assert.equal(obj.tofu, 'no');
assert.equal(result, obj);clone()
It clones a Kareem object
const k1 = new Kareem();
k1.pre('cook', function() {});
k1.post('cook', function() {});
const k2 = k1.clone();
assert.deepEqual(Array.from(k2._pres.keys()), ['cook']);
assert.deepEqual(Array.from(k2._posts.keys()), ['cook']);merge()
It pulls hooks from another Kareem object
const k1 = new Kareem();
const test1 = function() {};
k1.pre('cook', test1);
k1.post('cook', function() {});
const k2 = new Kareem();
const test2 = function() {};
k2.pre('cook', test2);
const k3 = k2.merge(k1);
assert.equal(k3._pres.get('cook').length, 2);
assert.equal(k3._pres.get('cook')[0].fn, test2);
assert.equal(k3._pres.get('cook')[1].fn, test1);
assert.equal(k3._posts.get('cook').length, 1);