mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
A handler that only updates gauges has nothing to yield. collect() already dropped undefined results at runtime, now its type says so too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
185 lines
4.1 KiB
TypeScript
185 lines
4.1 KiB
TypeScript
import { logger } from "./logging.js";
|
|
import * as libHelpers from "./helpers.js";
|
|
|
|
export type HookHandler<Args extends unknown[], Result = void> = (...args: Args) => Result | Promise<Result>;
|
|
|
|
export class AsyncHook<
|
|
Args extends unknown[],
|
|
Result = void,
|
|
> {
|
|
private static readonly _timeoutToken = Symbol("timeout");
|
|
private _handlers = new Map<string, HookHandler<Args, Result>>();
|
|
|
|
constructor(
|
|
private _logger = logger,
|
|
private _timeoutMs = 15_000,
|
|
) { }
|
|
|
|
get size() {
|
|
return this._handlers.size;
|
|
}
|
|
|
|
get attached() {
|
|
return this._handlers.keys();
|
|
}
|
|
|
|
/**
|
|
* Stable event listener reference bound to this hook instance.
|
|
*
|
|
* This function is pre-bound and guaranteed to remain referentially stable
|
|
* for the lifetime of the hook instance. It can safely be passed directly
|
|
* into event emitters (e.g. EventEmitter.on / off) without losing context
|
|
* or requiring re-binding.
|
|
*
|
|
* Example:
|
|
* `eventEmitter.on("change", hook.listener);`
|
|
*/
|
|
readonly listener = this.invoke.bind(this);
|
|
|
|
/**
|
|
* Attach a handler to this hook.
|
|
*
|
|
* An error is thrown if another handler with the same name is already attached.
|
|
*/
|
|
attach(
|
|
name: string,
|
|
handler: HookHandler<Args, Result>,
|
|
) {
|
|
if (this._handlers.has(name)) {
|
|
throw new Error(`Handler with name ${name} is already attached`);
|
|
}
|
|
this._handlers.set(name, handler);
|
|
}
|
|
|
|
/**
|
|
* Remove a previously attached handler.
|
|
*
|
|
* If no handler exists, then no action is performed.
|
|
*/
|
|
detach(name: string) {
|
|
this._handlers.delete(name);
|
|
}
|
|
|
|
/**
|
|
* Remove all attached handlers.
|
|
*/
|
|
clear() {
|
|
this._handlers.clear();
|
|
}
|
|
|
|
private async _invokeHandler(
|
|
name: string,
|
|
handler: HookHandler<Args, Result>,
|
|
args: Args,
|
|
): Promise<Result | undefined> {
|
|
try {
|
|
const result = await libHelpers.timeout<
|
|
Result | typeof AsyncHook._timeoutToken
|
|
>(
|
|
Promise.resolve(handler(...args)),
|
|
this._timeoutMs,
|
|
AsyncHook._timeoutToken,
|
|
);
|
|
|
|
if (result === AsyncHook._timeoutToken) {
|
|
throw new Error(`Hook ${name} timed out after ${this._timeoutMs}ms`);
|
|
}
|
|
|
|
return result;
|
|
|
|
} catch (err: any) {
|
|
this._logger.error(`Ignoring error in hook "${name}":\n${err?.stack ?? err}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoke all handlers attached to this hook.
|
|
*
|
|
* Any return values are discarded.
|
|
*/
|
|
async invoke(...args: Args): Promise<void> {
|
|
await Promise.all(
|
|
[...this._handlers.entries()].map(
|
|
([name, handler]) => this._invokeHandler(name, handler, args)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Invoke all handlers attached to this hook.
|
|
*
|
|
* Returns all successful results.
|
|
*/
|
|
async collect(...args: Args): Promise<Exclude<Awaited<Result>, void>[]> {
|
|
const results = await Promise.all(
|
|
[...this._handlers.entries()].map(
|
|
([name, handler]) => this._invokeHandler(name, handler, args)
|
|
)
|
|
);
|
|
|
|
return results.filter(
|
|
(result): result is Exclude<Awaited<Result>, void> => result !== undefined
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Invoke all handlers attached to this hook.
|
|
*
|
|
* Returns all successful results and their source.
|
|
*/
|
|
async collectEntries(...args: Args): Promise<[string, Result][]> {
|
|
const results = await Promise.all(
|
|
[...this._handlers.entries()].map(async ([name, handler]) => {
|
|
const result = await this._invokeHandler(name, handler, args);
|
|
return result !== undefined ? [name, result] as [string, Result] : undefined;
|
|
})
|
|
);
|
|
|
|
return results.filter(
|
|
(entry): entry is [string, Result] => entry !== undefined
|
|
);
|
|
}
|
|
}
|
|
|
|
export class AsyncHookCollection {
|
|
private hooks: AsyncHook<any[], any>[] = [];
|
|
|
|
constructor(
|
|
private _logger = logger,
|
|
) {}
|
|
|
|
protected newHook<
|
|
Args extends unknown[],
|
|
Result = void,
|
|
> (
|
|
_logger = this._logger
|
|
) {
|
|
const hook = new AsyncHook<Args, Result>(_logger);
|
|
this.hooks.push(hook);
|
|
return hook;
|
|
}
|
|
|
|
get size() {
|
|
const attached = new Set(this.hooks.flatMap(hook => [...hook.attached]));
|
|
return attached.size;
|
|
}
|
|
|
|
get attached() {
|
|
const attached = new Set(this.hooks.flatMap(hook => [...hook.attached]));
|
|
return attached.values();
|
|
}
|
|
|
|
detachAll(name: string) {
|
|
for (const hook of this.hooks) {
|
|
hook.detach(name);
|
|
}
|
|
}
|
|
|
|
clearAll() {
|
|
for (const hook of this.hooks) {
|
|
hook.clear();
|
|
}
|
|
}
|
|
}
|