🦆 It's ducks, taps, ducks with taps. What more would you want?
This module provides a bunch of Hooks you can use to let other code tap into.
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncBounceHook,
SyncLoopBailHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook,
AsyncSeriesBounceHook,
AsyncLoopBailHook
} = require('duck-taps')
Install it with yarn:
yarn add duck-taps
Or with npm:
npm install duck-taps
These are the hooks that are included:
Basic usage is very straightforward:
const { SyncHook } = require('duck-taps')
const hook = new SyncHook() // create a new Hook instance
hook.tap('Logger', arg => { // register a named tap (Logger) with the hook
console.log(arg)
})
hook.call('Hello World') // call the hook with the argument 'Hello World'
And a basic typescript example with type declaration:
import { SyncHook } from 'duck-taps'
const hook = new SyncHook<[string]>()
hook.tap('Logger', arg => { // arg is now type string
})
hook.call('Hello World') // you can only assign 1 argument now and it has to be a string
See Typescript for more awesome features with typescript
To register a tap ( plugin ) with the hook, you can use the hook.tap()
method.
There are 3 ways you can do this:
This is propably the most basic way to register a tap, you just provide the handler function to the .tap()
method.
hook.tap(() => { /* functionality here */ })
This creates an anonymous tap, that has no name, so it cannot be addressed by name directly.
The second way is to provide a name and a handler function
hook.tap('Name of tap', () => { /* functionality here */ })
This creates a named tap. You can later use this name to insert taps before or after the tap. This way of creating a tap recreates the tapable behavior.
This is the most customizable way to create a tap. You pass in a config object detailing what function you want to put and where in the tap chain you want to put it.
hook.tap({
name: 'Name of tap',
fn: () => { /* functionality here */ }
})
The biggest under-the hood change from tapable is the fact that duck-taps uses hook phases, each hooks can define custom phases that plugins can tap into.
The only hooks to take advantage of this currently are the bounce hooks, where the in-order part is one phase ( the execution
phase ) and the reverse-order part is another ( the post
phase ).
You can either provide a config object that specifies the phases or use the hook.phase()
method. Normal registration still works though, you function will just get added to all phases.
hook.phase('execute', () => {}) // Registered just for the `'execute'` phase.
hook.phase('weird', () => {}) // If you pass a phase in that doesnt exist, is will get ignored.
All phases the hook has are specified in hook.phases
.
There are some standard phases Hooks adopted:
- execute Is the standard phase, all hooks with just a single phase will just use this.
- pre Some hooks might use this phase to allow you to setup stuff, before the execution phase.
- post For cleaning up stuff after the execution phase or whatever you might have to do. The Bounce hooks you this phase when going up the tap chain again.
The basic hooks are all synchronous. This is fine for small tap chains, but for larger ones or taps that are async functions or return promises this is not ideal, that's why there are also asynchronous hooks. Calling them returns a promise that resolves once the taps are done.
Synchronous hooks can be identified by the prefix Sync
before the class name,
while asynchronous hooks can be identified by the Async
prefix.
Async hooks also support parallel execution, the AsyncParallelHook
and AsyncSeriesBailHook
execute all taps in parallel, while all hooks with the AsyncSeries
prefix execute all asnyc taps in sequence.
Sync hooks can only be invoked with the method #call()
, you have to provide all the arguments you want and it will call all of the taps. This is blocking so you would not want to have a large amount of computationally intensive taps on the hook.
hook.call('arg1','arg2')
Async hooks can only be invoked with the method #promise()
which calls all the taps with the provided arguments and resolves once it is done.
hook.promise('arg1','arg2')
There are 4 types of synchonous hooks ( technically 4 ) that all execute the taps in sequence. They only differ in the way they handle the return values:
- Hook
The SyncHook
executes all taps in sequence. It does not return anything.
The async counterpart is the AsyncSeriesHook
it also accepts async functions and promises.
- Bail
Like the SyncHook
the SyncBailHook
executes all the taps in sequence, but if one of the taps returns anything the bail hook will stop executing the remaining ones and return the value the tap returned.
The async counterparts to this hook are the AsyncSeriesBailHook
and AsyncParallelBailHook
- Waterfall
A waterfall hook also calls each tapped function in a row. Unlike the basic hook, it passes a return value from each function to the next function.
- Bounce
A bounce hook works basically like a two way waterfall hook. When the hook gets called it first executes all taps in sequnce passing the return value from tap to tap. But once it reaches the end, it 'turns around' and starts executing the taps in backwards order. The last hook in the tap chain is the bounce tap. It gets only called once.
bounceHook.tap(console.log('A'))
bounceHook.tap(console.log('B'))
bounceHook.tap(console.log('C'))
bounceHook.call() // will log like this:
// A
// B
// C
// B
// A
This is useful for request handlers for example.
- Loop
WARNING: :excalamtion:
⚠️ The looping hook type is very dangerous, it can quickly lead to infite loops if you're not careful!
A looping hook executes all the taps in sequence but loops once it reaches the end of the tap chain. This continues until one of the taps returns anything.
Because of the danger of the loop hook more types than the Bail types have not been implemented yet.
When using this module with typescript you'd want to specify what types your arguments can be and how many in total. This can be easily done since every class is a typescript generic:
// you can provide the types of the arguments in the brackets.
const hook = new AsyncSeriesHook<[string,number]>()
hook.promise('string',1) // This works.
hook.promise('string') // This doesn't since 2 arguments are required.
hook.promise(1,'string') // This also doesn't work because the order is wrong
hook.promise('string',1,whatever) // You can also not provide more arguments than specified.
When using the #phase()
method you also have autocompletion for the possible phases:
const hook1 = new SyncHook<[]>()
hook1.phase('execute', () => {}) // This can only be execute since it is the only phase the SyncHook has.
const hook2 = new SyncBounceHook<[]>()
// Here the phase can be either `'execute'` or `'post'` because the SyncBounceHook has two phases.
hook2.phase('execute', () => {})
hook2.phase('post', () => {})
The complete Api documenation can be found here: API Docs
If you feel like there is a type of hook that would be nice to have, I'd welcome a pull request! You can also just open an issue if you want to suggest a feature or want to report a bug!