A generalized and simple hooking API for adding extensibility to applications
npm install fun-hooks
bash
npm install fun-hooks
`Motivation
Fun(ctional) Hooks is a generalized and simple hooking API for creating runtime extensible applications. This
function-based approach operates on functions themselves rather than object methods to be fully compatible with purely
functional code, but still maintains some convenience operations for dealing with object-oriented code.The goals of this library are the following (in priority order):
1. Easy debugging
2. Performance
3. Simple but powerful API
4. Limited footprint
$3
If you will be running fun-hooks in an environment that doesn't support the use of Proxy
objects or you are using MooTools, Prototype.js, or some other old school js library that incorrectly patches Array and
Function prototype methods, you should use the latest 0.9.x version of fun-hooks
that includes the appropriate compatibility polyfills.Usage
Hooks follow the same format whether they're sync or async and whether they're before or after hooks; however,
it's important to remember that sync after hooks act on the _return_ result and async after hooks act on the
_callback's_ arguments.$3
- ready : number - (Optional, default: 0 (meaning no ready() call required)) See Ready.
`javascript
import funHooks from "fun-hooks"; // es6 (using webpack or babel)
let funHooks = require("fun-hooks"); // or in node
let funHooks = window.funHooks; // or directly in browser from somewhere like https://unpkg.com/fun-hooks@latest
let createHook = funHooks({
ready: funHooks.ASYNC | funHooks.QUEUE
});
`$3
`javascript
function sum(a, b) {
return a + b;
}let hookedSum = createHook("sync", sum);
// sync
before hooks accept the arguments to sum, and next passes the arguments to sum (or next before hook)
hookedSum.before(function(next, a, b) {
a = a + 1; // modify arguments or do some operation
next(a, b); // call next when you're done
});// sync
after hooks accepts the return result from sum, and next returns the result (or calls next after hook)
hookedSum.after(function(next, result) {
next(result + 1);
});let result = hookedSum(1, 1);
// hookedSum(1, 1) -> hookedSum.before(1, 1) -> sum(2, 1) -> hookedSum.after(3) -> 4
console.log(result); // 4
`_Note: You should always use
sync if you are returning a value. This includes if you are returning a Promise.
69 If you're hooking a function with sync your hooks must call next synchronously (e.g. no ajax). Calling
70 next asynchronously can lead to unpredictable behavior, including an undefined return value and incorrect
71 arguments passed to subsequent hooks or the wrapped function._$3
`javascript
function increment(a, b, callback) {
callback(a + 1, b + 1);
}let hookedIncrement = createHook("async", increment);
// async
before hooks accept the arguments to sum, and next passes the arguments to the next before hook or sum (same as sync)
hookedIncrement.before(function(next, a, b) {
a = a + 1;
next(a, b);
});// async
after hooks accept the arguments sum passed to callback, and next calls sum's actual callback (or next after hook)
hookedIncrement.after(function(next, a, b) {
next(a, b + 1);
});hookedIncrement(1, 1, function(a, b) {
console.log(a, b); // 3, 3
})
// hookedIncrement(1, 1) -> hookedIncrement.before(1, 1) -> increment(2, 1) -> hookedIncrement.after(3, 2) -> callback(3, 3)
`You'll notice no difference above in
sync or async with the before hooks, but the after hooks are dealing with
the callback's parameters in one case and the return result (which will always be a single value) in the other.$3
Hooks can be removed using the remove method. You can either use a match object to remove a specific hook or pass
nothing to remove all hooks.`javascript
function beforeHook() {
console.log("called");
}hookedSum.before(beforeHook);
hookedSum(1, 1); // "called"
hookedSum.getHooks({hook: beforeHook}).remove();
hookedSum(1, 1); // hook not called
hookedSum.before(beforeHook);
hookedSum.after(afterHook);
hookedSum.removeAll(); // remove all before and after hooks
`$3
You can attach as many before or after hooks as you'd like to a function. The order in which the hooks are run is
dependent on the order they're added or an optional priority argument set when creating the hook (which defaults
to a priority of 10).`javascript
hookedSum.before(beforeHook1);
hookedSum.before(beforeHook2, 9);
hookedSum.before(beforeHook3, 11);
hookedSum.after(afterHook1, 8);
hookedSum.after(afterHook2);hookedSum(1, 1); // hookedSum -> beforeHook3 -> beforeHook1 -> beforeHook2 -> sum -> afterHook2 -> afterHook1
`$3
A hook can bail early to skip the other hooks or to skip the hooked function altogether (effectively stubbing it).`javascript
function bailHook(next, a, b) {
next.bail(1, 1);
}hookedIncrement.after(bailHook);
hookedIncrement.after(afterHook2);
// notice
afterHook2 is not called
hookedIncrement(1, 1, function callback(a, b) {}); // hookedIncrement -> increment -> bailHook -> callback(1, 1)hookedIncrement.before(bailHook);
// notice not even the original
increment function is called now
hookedIncrement(1, 1, function callback(a, b) {}); // hookedIncrement -> bailHook -> callback(1, 1)
`If you want to bail completely (i.e. not even call the callback) then just don't call
next.$3
You can get all the hook entries attached to a hooked function using hookedFn.getHooks(). An optional argument
can be passed for matching only specific kinds of hooks: e.g. hookedFn.getHooks({type: "before"}) or
hookedFn.getHooks({hook: myBeforeHook}) to get a specific hook entry.$3
If you want to have a hook that just performs some side-effect before or after the hooked function but does not modify
arguments, just call next and pass-through the arguments without modifying them. It's important that next is
still called with the original arguments so that the hook-chain can continue.`javascript
hookedIncrement.before(function sideEffect(next, ...args) {
console.log("I'm a side-effect!");
next.apply(this, args);
})
`$3
Hooks can be given a name and then accessed using the .get method. This can be useful for defining the
extensible API for your application. _Note: You can also just expose references to the hooked functions themselves,
this is just a convenience. Also, when using named hooks, you can reference the hook by name using .get and add
before and after hooks before the hook itself has actually been created!_Note: For Typescript users, the
.get function requires a type parameter defining the type of hook to expect.
Type helpers are exported as SyncHook and AsyncHook where T is the hooked function signature. If you want
Typescript to infer proper types then you should just expose references to the hooked function themselves._`javascript
// some-applicaiton
import hookFactory from "fun-hooks";
let hook = hookFactory(); // default configurationfunction getItem(id, cb) {
fetchItem(id).then(cb);
}
function getPrice(item) {
return item.price;
}
// works, even though the "item" hook isn't defined until below!
hook.get("item").after(function(next, id) {
console.log("accessing item: " + id);
next(id);
});
hook("async", getItem, "item"); // naming this hook
item
hook("sync", getPrice, "price"); // naming this hook priceexport const getHook = hook.get;
// extending application
import { getHook } from "some-application";
getHook("item").before(function modifyId(next, id) {
let newId = getUpdatedId(id); //
id naming scheme changed... luckily we have this hook available!
next(newId);
});getHook("price").after(function currencyConversion(next, price) {
let newPrice = convert(price, "USD");
next(newPrice);
});
`$3
While functions are the base unit of extension in this library, there is a convenience provided to apply hooks to object
methods if an object is passed to the hook creator. _Note: this will be bound correctly in the hooked function as well
as in the before and after hooks (i.e. this refers to the object instance inside hooks)._`javascript
class Thing {
constructor() {
this.value = 1;
}
setValue(value) {
this.value = value;
}
getValue() {
return this.value;
}
}
hook(Thing.prototype, ["setValue", "getValue"]);Thing.prototype.getValue.after(function(next) {
next(this.value + 2);
});
let myThing = new Thing();
myThing.setValue(1);
console.log(myThing.getValue()); // 3
`_Note:
hook will also walk the prototype chain and find getValue if it were an inherited method._If
["setValue", "getValue"] were omitted then hook would hook the results of
Object.getOwnPropertyNames(Thing.prototype) excluding constructor and any methods marked private with a preceding
underscore (e.g. _privateMethod() {}). Also, if the list of methods to hook is omitted, hook will no longer walk
the prototype chain to avoid creating accidental hooks. Hooked methods are all assumed to be
sync unless otherwise specified.`javascript
hook(Thing.prototype, ["setValue", "sync:getValue" / same as "getValue" /, "async:loadData"]);
`If a third argument,
name, is provided, then the object's hooked methods will be made accessible to the .get
method described above using