first commmit
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* A lightweight, framework-agnostic pub/sub event bus.
|
||||
*
|
||||
* Used project-wide to decouple:
|
||||
* - UI layer (floating controls) → logic layer (player controller)
|
||||
* - Logic layer (damage system) → UI layer (HUD / feedback)
|
||||
* - Any manager → Logger / BI埋点
|
||||
*
|
||||
* Design notes:
|
||||
* - `emit` is synchronous to avoid a frame of latency for combat events.
|
||||
* - Subscribing the same callback twice is a no-op (idempotent) to avoid
|
||||
* double-fire bugs when views are rebuilt.
|
||||
* - `once` unsubscribes itself after the first invocation.
|
||||
* - Handler errors are caught and forwarded to a user-provided error hook
|
||||
* (defaulting to `console.error`) so that one bad listener cannot break
|
||||
* the rest of the fan-out.
|
||||
*/
|
||||
|
||||
export type EventHandler<TPayload = unknown> = (payload: TPayload) => void;
|
||||
export type ErrorHook = (event: string, err: unknown) => void;
|
||||
|
||||
interface HandlerRecord {
|
||||
fn: EventHandler<any>;
|
||||
once: boolean;
|
||||
}
|
||||
|
||||
export class EventBus {
|
||||
private readonly handlers = new Map<string, HandlerRecord[]>();
|
||||
private errorHook: ErrorHook = (event, err) => {
|
||||
// Fallback error hook; replaced via `setErrorHook` once Logger is
|
||||
// available.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[EventBus] handler for "${event}" threw:`, err);
|
||||
};
|
||||
|
||||
/** Override the default error hook (used by Logger integration). */
|
||||
public setErrorHook(hook: ErrorHook): void {
|
||||
this.errorHook = hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe a handler. Idempotent — the same `fn` cannot be registered
|
||||
* twice for the same event.
|
||||
*/
|
||||
public on<T>(event: string, fn: EventHandler<T>): void {
|
||||
this.register(event, fn as EventHandler<any>, false);
|
||||
}
|
||||
|
||||
/** Subscribe a handler that auto-unsubscribes after one invocation. */
|
||||
public once<T>(event: string, fn: EventHandler<T>): void {
|
||||
this.register(event, fn as EventHandler<any>, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe. If `fn` is omitted, all handlers for `event` are cleared.
|
||||
*/
|
||||
public off<T>(event: string, fn?: EventHandler<T>): void {
|
||||
const list = this.handlers.get(event);
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
if (!fn) {
|
||||
this.handlers.delete(event);
|
||||
return;
|
||||
}
|
||||
const filtered = list.filter((r) => r.fn !== fn);
|
||||
if (filtered.length === 0) {
|
||||
this.handlers.delete(event);
|
||||
} else {
|
||||
this.handlers.set(event, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
/** Synchronously dispatch `payload` to every handler registered for `event`. */
|
||||
public emit<T>(event: string, payload?: T): void {
|
||||
const list = this.handlers.get(event);
|
||||
if (!list || list.length === 0) {
|
||||
return;
|
||||
}
|
||||
// Snapshot first: `once` handlers will mutate `list` via `off`.
|
||||
const snapshot = list.slice();
|
||||
for (const record of snapshot) {
|
||||
try {
|
||||
record.fn(payload as unknown);
|
||||
} catch (err) {
|
||||
this.errorHook(event, err);
|
||||
}
|
||||
if (record.once) {
|
||||
this.off(event, record.fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Return the number of handlers registered for `event`. */
|
||||
public listenerCount(event: string): number {
|
||||
return this.handlers.get(event)?.length ?? 0;
|
||||
}
|
||||
|
||||
/** Remove every handler of every event (used in unit tests / scene unload). */
|
||||
public clear(): void {
|
||||
this.handlers.clear();
|
||||
}
|
||||
|
||||
private register(event: string, fn: EventHandler<any>, once: boolean): void {
|
||||
const list = this.handlers.get(event) ?? [];
|
||||
if (list.some((r) => r.fn === fn)) {
|
||||
return;
|
||||
}
|
||||
list.push({ fn, once });
|
||||
this.handlers.set(event, list);
|
||||
}
|
||||
}
|
||||
|
||||
/** Shared, process-wide event bus. Tests should create a fresh `new EventBus()`. */
|
||||
export const globalEventBus = new EventBus();
|
||||
Reference in New Issue
Block a user