npm package: @spine/hook
Description: A hook pattern for your project
Author: Ezequiel S. Pereira
Latest version: 0.2.17

# Spine Hook

A hook utility that makes your project pluggable. Inspired by [Wordpress Plugin API](https://codex.wordpress.org/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](#standard-hook-types)
    * [HookFilterSync](#hookfiltersync)
    * [HookFilter](#hookfilter)
    * [HookActionSync](#hookactionsync)
    * [HookAction](#hookaction)
    * [HookActionSeries](#hookactionseries)
* [Webpack Tapable vs Spine Hook](#webpack-tapable-vs-spine-hook)
* [Inercepting Hooks](#intercepting-hooks)
* [Trace](#trace)
* [Order and Priority](#order-and-priority)
* [`unbind` vs `disable`](#unbind-vs-disable)
* [`addFilter`/`addAction` vs `bind`](#addfilter/addaction-vs-bind)
* [Bootstrap](#bootstrap)
* [Hook Plugin](#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](https://github.com/webpack/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](http://gitlab.com/ezsper.com/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') {}
```