haken
v0.1.0
Published
Typesafe hooks for any context
Downloads
180
Readme
Normally, running a function yields a single result. With HAKEN, you can run a function in a specific context, allowing the function to register some hooks, which you can then call in response to future events.
// Setup:
import { buildHooksContext } from 'haken'
const { acceptHooks, hook } = buildHooksContext()
export const onMessage = hook('onMessage')
// Use in functions:
export const createHistory = () => {
const history = []
onMessage(msg => history.push({ date: new Date(), msg }))
return history
}
// Use those functions in a hookable context:
const [history, {hooks}] = acceptHooks(() => createHistory())
hooks.onMessage && source.addListener('message', hooks.onMessage)
Contents
Who is this for?
If you are writing a framework, or providing some form of inversion of control, and want to allow user functions (written by someone else) to be able to hook into various aspects of your host code / environment, HAKEN can come in handy.
Why?
What HAKEN does can also be achieved by using classes instead of functions. A user function can return an instance (perhaps it is a constructor), which provides methods that your host code can then invoke in response to later events.
The main difference between OOP and the hooks pattern is flexibility and composability: with hooks, it is much easier to bundle repeating patterns of logic into custom hooks and easily re-use them, while in OOP, specifically in a single-inheritance model, this level of composition quickly turns into a headache.
export const useGreeter = () => {
onMessage(msg => {
if (msg.toLowerCase().startsWith('hello')) {
console.log('Hellow to you sir!')
}
})
}
A custom hook is analogous to a custom base class you can inherit from, and registering a hook is equivalent to overriding a parent method. From this perspective, for example, with hooks you can:
- Inherit from different base classes, who override different methods
- Override a parent method multiple times
- Conditionally inherit from some base class
Which makes the hooks pattern even more flexible than OOP with multiple inheritance (this is in part due to the scope of hooks being more limited than generic methods: they are supposed to run some side-effect in response to some event, which means you can trivially combine them and disambiguate them).
Installation
import { buildHooksContext } from 'https://esm.sh/haken'
Or
npm i haken
Usage
Step 1: build a hooks context and expose its functions to user land:
import { buildHooksContext } from 'haken'
// build the context
const { acceptHooks, hook } = buildHooksContext()
// expose the hooks
export const onMessage = hook('onMessage')
export const onClose = hook('onClose')
export const onError = hook('onError')
Step 2: use the exposed hooks in user land (or allow your users to):
export function setupLogger() {
onMessage(msg => console.log('received: ' + msg))
onClose(() => console.log('closed!')
}
Step 3: run user land functions with acceptHooks()
and hook their hooks.
const [result, { hooks }] = acceptHooks(() => setupLogger())
hooks.onMessage && socket.addEventListener('msg', hooks.onMessage)
hooks.onClose && socket.addEventListener('close', hooks.onClose)
hooks.onError && socket.addEventListener('error', hooks.onError)
Meta
Custom hooks might need some additional metadata about the host context. Provide such metadata as the second argument to acceptHooks()
:
const [result, { hooks }] = acceptHooks(
() => setupLogger(),
{ socket } // 👉 this is the metadata
)
For accessing this metadata, use the hooksMeta()
function returned by buildHooksContext()
. It is recommended to provide wrapper functions for such access instead of providing direct, uncontrolled access to the metadata.
const { acceptHooks, hook, hooksMeta } = buildHooksContext()
export const onMessage = hook('onMessage')
export const onClose = hook('onClose')
export const onError = hook('onError')
// 👇 user functions and custom hooks can call this to access
// the current socket.
export const currentSocket = () => hooksMeta().socket
User functions or custom hooks might also add some metadata of their own, which you can check by reading the meta
key returned by acceptHooks()
function:
const [result, {
hooks,
meta // 👉 metadata, possibly modified by custom hooks
}] = acceptHooks(() => setupLogger(), { socket })
Type Safety
Provide the types of hooks you want for your hooks context as a type argument to buildHooksContext()
:
type Hooks = {
onMessage: (msg: string) => void,
onClose: () => void,
onError: (err: any, callsite: Callsite) => void,
}
const { acceptHooks, hook, hooksMeta } = buildHooksContext<Hooks>()
You can also enforce the type of the metadata, by passing a second type argument:
type Hooks = {
onMessage: (msg: string) => void,
onClose: () => void,
onError: (err: any, callsite: Callsite) => void,
}
type Meta = {
socket: WebSocket
}
const { acceptHooks, hook, hooksMeta } = buildHooksContext<Hooks, Meta>()
Contribution
You need node, NPM to start and git to start.
# clone the code
git clone [email protected]:loreanvictor/haken.git
# install stuff
npm i
Make sure all checks are successful on your PRs. This includes all tests passing, high code coverage, correct typings and abiding all the linting rules. The code is typed with TypeScript, Jest is used for testing and coverage reports, ESLint and TypeScript ESLint are used for linting. Subsequently, IDE integrations for TypeScript and ESLint would make your life much easier (for example, VSCode supports TypeScript out of the box and has this nice ESLint plugin), but you could also use the following commands:
# run tests
npm test
# check code coverage
npm run coverage
# run linter
npm run lint
# run type checker
npm run typecheck