1
mirror of https://github.com/thepeacockproject/Peacock synced 2024-11-29 09:15:11 +01:00
Peacock/components/hooksImpl.ts
2023-09-14 20:01:31 +01:00

252 lines
6.8 KiB
TypeScript

/*
* The Peacock Project - a HITMAN server replacement.
* Copyright (C) 2021-2023 The Peacock Project Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* A HookMap is a helper class for a Map with Hooks.
*
* @example
* const myHookMap = new HookMap(key => new SyncHook())
*
* @example
* hookMap.for("some-key").tap("MyPlugin", (arg) => { })
*
* @example
* const hook = hookMap.for("some-key")
* hook.call("some value", 123456)
*/
export class HookMap<Hook> {
private readonly _map: Map<string, Hook>
public constructor(private readonly _createFunc: (key: string) => Hook) {
this._map = new Map()
}
/**
* Get a hook for the given key.
*
* @param key The hook to get.
* @returns The hook.
*/
public for(key: string): Hook {
if (this._map.has(key)) {
return this._map.get(key)!
}
const hook = this._createFunc(key)
this._map.set(key, hook)
return hook
}
}
/**
* The options for a hook. Will either be just the name (as a string), or an object containing the additional options.
*/
export type TapOptions = string | { name: string; context: boolean }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AsArray<T> = T extends any[] ? T : [T]
/**
* An internal interface containing the properties held by a single taps' container object.
*/
interface Tap<T, R> {
name: string
func: (...args: AsArray<T>) => R
enableContext: boolean
}
/**
* The structure of an intercept.
*
* @see name
* @see call
* @see tap
*/
export interface Intercept<Params, Return> {
/**
* The name of the intercept.
*/
name: string
/**
* A function called just after the hook is called, and before all taps run.
*
* @param context The context object. Can be modified.
* @param params The parameters that the taps will get. Can be modified.
*/
call(context, ...params: AsArray<Params>): void
/**
* A function called when the hook is tapped. Note that it will not be called when an interceptor is registered, since that doesn't count as a tap.
*
* @param name The name of the tap.
* @param func The tap's function.
*/
tap(name: string, func: (...args: AsArray<Params>) => Return): void
}
/**
* The base for a hook, including {@link tap} and {@link intercept} functionality.
*
* @see SyncHook
* @see SyncBailHook
* @see AsyncSeriesHook
*/
export abstract class BaseImpl<Params, Return = void> {
protected _intercepts: Intercept<Params, Return>[]
protected _taps: Tap<Params, Return>[]
/**
* Register an interceptor.
* Interceptors can listen for certain events, and control things like context for them.
*
* @param intercept An object containing the intercept.
* @see Intercept
*/
public intercept(intercept: Intercept<Params, Return>): void {
this._intercepts.push(intercept)
}
/**
* Tap the hook.
*
* @param nameOrOptions A string containing the tap's name, or an object containing the tap's details.
* @param consumer The function that will be called when the hook is.
* @see TapOptions
*/
public tap(
nameOrOptions: TapOptions,
consumer: (...args: AsArray<Params>) => Return,
): void {
const name =
typeof nameOrOptions === "string"
? nameOrOptions
: nameOrOptions.name
const enableContext =
typeof nameOrOptions === "string" ? false : nameOrOptions.context
for (const intercept of this._intercepts) {
if (intercept.tap) {
intercept.tap(name, consumer)
}
}
this._taps.push({
name,
func: consumer,
enableContext,
})
}
public get allTapNames(): string[] {
return this._taps.map((t) => t.name)
}
}
/**
* A hook that runs each tap one-by-one.
*/
export class SyncHook<Params> extends BaseImpl<Params> {
public constructor() {
super()
this._taps = []
this._intercepts = []
}
public call(...params: AsArray<Params>): void {
const context = {}
for (const intercept of this._intercepts) {
if (intercept.call) {
intercept.call(context, ...params)
}
}
for (const tap of this._taps) {
const args = tap.enableContext ? [context, ...params] : [...params]
// @ts-expect-error TypeScript things.
tap.func(...args)
}
return
}
}
/**
* A hook that runs each tap one-by-one until one returns a result.
*/
export class SyncBailHook<Params, Return> extends BaseImpl<Params, Return> {
public constructor() {
super()
this._taps = []
this._intercepts = []
}
public call(...params: AsArray<Params>): Return | null {
const context = {}
for (const intercept of this._intercepts) {
if (intercept.call) {
intercept.call(context, ...params)
}
}
for (const tap of this._taps) {
const args = tap.enableContext ? [context, ...params] : [...params]
// @ts-expect-error TypeScript things.
const result = tap.func(...args)
if (result) {
return result
}
}
return null
}
}
/**
* A hook that runs each tap, one-by-one, in an async context (each tap may be an async function).
*/
export class AsyncSeriesHook<Params> extends BaseImpl<Params, Promise<void>> {
public constructor() {
super()
this._taps = []
this._intercepts = []
}
public async callAsync(...params: AsArray<Params>): Promise<void> {
const context = {}
for (const intercept of this._intercepts) {
if (intercept.call) {
await intercept.call(context, ...params)
}
}
for (const tap of this._taps) {
const args = tap.enableContext ? [context, ...params] : [...params]
// @ts-expect-error TypeScript things.
await tap.func(...args)
}
}
}