@spine/hook
v0.2.17
Published
A hook pattern for your project
Downloads
35
Maintainers
Readme
Spine Hook
A hook utility that makes your project pluggable. Inspired by Wordpress Plugin API.
Key aspects
- Fast
- Friendly type definitions
- Highly extendable
- Easy to use
- Custom hooks
- Priority
- Before / After ordering
- Enable / Disable plugins
- Public iteration methods
- Interception / Trace
Summary
- Standard Hook Types
- Webpack Tapable vs Spine Hook
- Inercepting Hooks
- Trace
- Order and Priority
unbind
vsdisable
addFilter
/addAction
vsbind
- Bootstrap
- Hook Plugin
Standard Hook Types
Spine Hook comes with standard hook types that should cover any strategy
HookFilterSync
Filters a value "synchronously", like a waterfall it passes the result of each filter to the other.
import { HookFilterSync } from '@erect/hook/HookFilterSync';
const hook = HookFilterSync.template((value: number, factor: number) => value);
hook.addFilter('multiply', (value, factor) => value * factor); // 3 * 4 = 12
hook.addFilter('sum', (value, factor) => value + factor); // 12 + 4 = 16
hook.addFilter('divide', (value, factor) => value / factor); // 16 / 4 = 4
const value = hook.filter(3, 4);
console.log(value); // 4
HookFilter
Filters a value "asynchronously", like a waterfall it passes the result of each filter to the other.
import { HookFilter } from '@erect/hook/HookFilter';
const hook = HookFilter.template((value: number, factor: number) => value);
hook.addFilter('multiply', async (value, factor) => value * factor); // 3 * 4 = 12
hook.addFilter('sum', (value, factor) => value + factor); // 12 + 4 = 16
hook.addFilter('divide', (value, factor) => Promise.resolve(value / factor)); // 16 / 4 = 4
hook.filter(3, 4)
.then(value => {
console.log(value); // 4
});
HookActionSync
Execute multiple actions against defined params "synchronously" without catching any values
import { HookActionSync } from '@erect/hook/HookActionSync';
const hook = HookActionSync.template((value: number, factor: number) => {});
hook.addAction('multiply', 'context', (context, value, factor) => {
context.multiply = value * factor;
});
hook.addAction('divide', 'iterator', (iterator, value, factor) => {
iterator.context.divide = value / factor;
});
hook.addAction('sum', false, (context, value, factor) => {
context.sum = value + factor;
});
const context: any = {};
hook.doPassing(context, 4, 2);
console.log(context);
// { multiply: 12, divide: 2, sum: 6 }
// On sync order "is guaranteed"
HookAction
Execute multiple actions against defined params "asynchronously" in parallel without catching any results
import { HookAction } from '@erect/hook/HookAction';
const hook = HookAction.template((value: number, factor: number) => {});
hook.addAction('multiply', 'context', (context, value, factor) => {
return new Promise(resolve => {
setTimeout(() => {
context.multiply = value * factor;
resolve();
}, 10);
});
});
hook.addAction('divide', 'iterator', async (iterator, value, factor) => {
iterator.context.divide = value / factor;
});
hook.addAction('sum', false, (context, value, factor) => {
context.sum = value + factor;
});
const context: any = {};
hook.doPassing(context, 4, 2)
.then(() => {
console.log(context);
// { divide: 2, sum: 6, [multiply]: 12 }
// On async (not series) order "is not guaranteed"
});
HookActionSeries
Execute multiple actions against defined params "asynchronously" in series without catching any results
import { HookActionSeries } from '@erect/hook/HookActionSeries';
const hook = HookActionSeries.template((value: number, factor: number) => {});
hook.addAction('multiply', false, (context, value, factor) => {
return new Promise(resolve => {
setTimeout(() => {
context.multiply = value * factor;
resolve();
}, 10);
});
});
hook.addAction('divide', true, async (iterator, value, factor) => {
iterator.context.divide = value / factor;
});
hook.addAction('sum', false, (context, value, factor) => {
context.sum = value + factor;
});
const context: any = {};
hook.doPassing(context, 4, 2)
.then(() => {
console.log(context);
// { multiply: 12, divide: 2, sum: 6 }
// On async series order "is guaranteed"
});
Webpack Tapable vs Spine Hook
Tapable is the Webpack hook module used for its plugins.
- Tapable is slower;
- Tapable don't support priority or ordering;
- On Tapable not every method can bail, on Spine Hook all methods can
iterator.bail()
; - Tapable typings are confusing in comparison;
- Tapable don't offer plugin navigation through iteration;
- Tapable cannot disable a plugin at runtime;
Intercepting hooks
Hook enables you to observe its execution with the Interceptor API, the following objects can be intercepted
- Plugin
- Hook
- HookIterator
import { HookActionSync } from '@spine/hook/HookActionSync';
const myHook = new HookActionSync.template((value: string) => value);
if (process.env.NODE_ENV !== 'production') {
myHook.intercept({
bind(plugin) {
plugin.intercept({
unbind(hook) {
if (myHook === hook) {
console.log(`Plugin "${plugin.name}" was unbind`);
}
},
});
console.log(`Plugin "${plugin.name}" was bind`);
},
iterate(iterator) {
iterator.intercept({
call(value) {
console.log(`Called with value ${value}`);
},
});
},
});
}
myHook.addAction('MyPlugin', (value) => {});
myHook.removeAction('MyPlugin');
myHook.do(10);
Trace
Trace is a simple library that uses Interceptor API to prints hook execution
import { HookActionSync } from '@spine/hook/HookActionSync';
import { trace } from '@spine/hook/trace';
const myHook = new HookActionSync.template((myValue: string) => myValue);
if (process.env.NODE_ENV !== 'production') {
trace(myHook, 'myHook');
}
myHook.addAction('plugin1', (myValue) => {
// do something
});
// outputs:
// myHook: Plugin "plugin1" was binded with (mode: default, priority: default 30)
// at %fileName%:%lineNumber%:%columnNumber%
myHook.do(10);
// outputs:
// myHook: Iterator#0 created
// at %fileName%:%lineNumber%:%columnNumber%
// myHook: Iterator#0 called with (myValue: 10)
// at %fileName%:%lineNumber%:%columnNumber%
Order and Priority
Optionally you can bind plugins to your hooks with order (before / after) and priority.
Take note that all plugins have a priority, if not set, a default priority will be considered, if all plugins have the same priority, no priority sorting will be applied.
In other hand order ensures that a plugin will be executed before or after another plugin, if in the execution state the ordering plugin is not present, the plugin that is matching the order will fallback to its default priority.
Note that before and after have no priority in itself, priority is only used when no order match is applied
hook.bind(pluginName, caller, order[]?, priority?);
hook.bind(pluginName, caller, order?, priority?);
hook.bind(pluginName, caller, priority?);
hook.bind(pluginName, mode, caller, order[]?, priority?);
hook.bind(pluginName, mode, caller, order?, priority?);
hook.bind(pluginName, mode, caller, priority?);
Usage Example
import * as http from 'http';
import { HookAction } from '@spine/hook/HookAction';
const middlewareHook = HookAction.template((req: http.IncomingMessage, res: http.ServerResponse) => {});
const errorMiddlewareHook = HookAction.template((error: Error, req: http.IncomingMessage, res: http.ServerResponse) => {});
middlewareHook.addAction('auth', true, (iterator, req, res) => {
// do something for auth
let authorized = true;
if (!authorized) {
if (!iterator.goto('forbidden')) {
iterator.bail(new Error('Not Authorized'));
}
}
}, { after: 'serveStatic' }, 5); // priorityFallback of 5 (if no "serveStatic" plugin is found)
middlewareHook.addAction('serveStatic', (req, res) => {
// do something for static files serving
}, 2); // priority 2
middlewareHook.addAction('render', true, (iterator, req, res) => {
// render application
let rendered = false;
if (rendered) {
iterator.bail();
}
}); // default priority of 10
middlewareHook.addAction('csp', (req, res) => {
// do something for something security police
}, { before: 'render' }); // default for priorityFallback is 10 (if no "render" plugin is found)
middlewareHook.addAction('notFound', true, (iterator, req, res) => {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.write('Not Found');
iterator.bail();
});
middlewareHook.addAction('forbidden', true, (iterator, req, res) => {
res.writeHead(403, { 'Content-Type': 'text/html' });
res.write('Forbidden Error');
iterator.bail();
});
errorMiddlewareHook.addAction('default', true, (iterator, error, res, res) => {
console.error(error);
res.writeHead(500, { 'Content-Type': 'text/html' });
res.write('<h1>ServerFaultError</h1>');
iterator.bail();
});
http.createServer((req, res) => {
middlewareHook.do(req, res)
// the execution order will be
// middleware: serveStatic
// middleware: auth
// middleware: csp
// middleware: render
// middleware: notFound
// middlreare: forbidden
.catch(async error => {
return errorMiddlewareHook.do(error, req, res);
});
}).listen(3000);
unbind
vs disable
You can bind/unbind a plugin and you can also enable/disable a plugin. The difference between the two is that by unbinding a plugin the one will be removed from hook iteration, that is every order (before / after) applied to the plugin won't be matched anymore and it will will fallback to priority, also unbind forces an order refreshing something that disable don't.
iterator.disable('plugin name')
disables plugin inside iteration (if not already executed by iterator),plugin.disable()
disables plugin for all iterations.
addFilter
/addAction
vs bind
addFilter
and addAction
are just an alias for bind
, the only benefit of using them is the readability of
indicating the type of hook is being performed.
Bootstrap
Use Spine Bootstrap so your hooks are always executed after hooks are binded.
Hook Plugin
Use plugin context to easily prefix listeners
const listener1 = hook.addAction(['myPlugin', 'foo'], () => {});
if (listener1.binded) {}
const myPlugin = { hookContext: 'myPlugin' }
const listener2 = hook.addAction([myPlugin, 'foo'], () => {}); // replaces listener1
if (!listener1.binded) {}
if (listener2 === hook.get(['myPlugin', 'foo'])) {}
if (listener2.name === 'foo' && listener2.context === myPlugin.hookContext) {}
if (listener2.name === 'foo' && listener2.contextValue === myPlugin) {}
if (listener1.name === 'foo' && listener1.contextValue === 'myPlugin') {}