Created
August 22, 2021 13:49
-
-
Save Akryum/c18f10e16a24b86dcb2c9c11cdf6fc47 to your computer and use it in GitHub Desktop.
Type-safe hooks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Hookable } from '../src/hookable' | |
describe('hookable', () => { | |
test('hook with one callback', async () => { | |
const hooks = new Hookable<{ | |
invert:(value: boolean) => boolean | Promise<boolean> | |
}>() | |
hooks.hook('invert', value => !value) | |
const res = await hooks.callHook('invert', true) | |
expect(res).toBe(false) | |
}) | |
test('hook with multiple callbacks', async () => { | |
const hooks = new Hookable<{ | |
increment:(value: number) => number | Promise<number> | |
}>() | |
hooks.hook('increment', value => value + 1) | |
hooks.hook('increment', async value => Promise.resolve(value + 1)) | |
hooks.hook('increment', value => value + 2) | |
const res = await hooks.callHook('increment', 0) | |
expect(res).toBe(4) | |
}) | |
test('multiple hooks', async () => { | |
const hooks = new Hookable<{ | |
decrement:(value: number) => number | Promise<number> | |
increment:(value: number) => number | Promise<number> | |
}>() | |
hooks.hook('increment', value => value + 1) | |
hooks.hook('decrement', value => value - 1) | |
let res = await hooks.callHook('increment', 0) | |
res = await hooks.callHook('decrement', res) | |
expect(res).toBe(0) | |
}) | |
test('hook with multiple parameters', async () => { | |
const hooks = new Hookable<{ | |
increment:(value: number, count?: number) => number | |
}>() | |
hooks.hook('increment', (value, count) => value + (count ?? 1)) | |
expect(await hooks.callHook('increment', 0)).toBe(1) | |
expect(await hooks.callHook('increment', 0, 2)).toBe(2) | |
}) | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T | |
export type HookMap<THooks> = Partial<Record<keyof THooks, THooks[keyof THooks][]>> | |
export class Hookable<THooks extends Record<string, (...args: any[]) => any>> { | |
_hooks: HookMap<THooks> | |
constructor () { | |
this._hooks = {} | |
} | |
/** | |
* Register a callback to a specific hook. | |
*/ | |
hook<Key extends keyof THooks> (name: Key, fn: THooks[Key]): void { | |
if (!name || typeof fn !== 'function') { | |
return | |
} | |
this._hooks[name] = this._hooks[name] || [] | |
this._hooks[name].push(fn) | |
} | |
/** | |
* Call all callbacks for a hook. They are called sequentially with async support. | |
*/ | |
async callHook<Key extends keyof THooks> ( | |
name: Key, | |
...args: Parameters<THooks[Key]> | |
): Promise<Awaited<ReturnType<THooks[Key]>>> { | |
if (!this._hooks[name]) { | |
return args[0] | |
} | |
try { | |
let result: Awaited<ReturnType<THooks[Key]>> = args[0] | |
for (const fn of this._hooks[name]) { | |
const returned = await fn(result, ...args.slice(1)) | |
if (typeof returned !== 'undefined') { | |
result = returned | |
} | |
} | |
return result | |
} catch (err) { | |
// @ts-expect-error The 'error' hook might not exist in THooks | |
name !== 'error' && await this.callHook('error', err) | |
console.error(err) | |
throw err | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment