@spine/hook

0.2.17 • Public • Published

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

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') {}

Package Sidebar

Install

npm i @spine/hook

Weekly Downloads

25

Version

0.2.17

License

MIT

Unpacked Size

233 kB

Total Files

51

Last publish

Collaborators

  • ezsper