type EventCallback = (...args: unknown[]) => void; type AnyEventCallback = (event: string, ...args: unknown[]) => void; export interface RuntimeEventMap { "runtime:ready": { startedAt: number }; "scene:changed": { current: string; previous?: string }; "game:rendered": undefined; "game:frame-rendered": undefined; } export class RuntimeEvents { private events = new Map>(); private anyEventCallbacks = new Set(); on(event: string, callback: EventCallback): () => void { if (!this.events.has(event)) { this.events.set(event, new Set()); } this.events.get(event)!.add(callback); return () => this.off(event, callback); } off(event: string, callback: EventCallback): void { const callbacks = this.events.get(event); if (!callbacks) { return; } callbacks.delete(callback); if (callbacks.size === 0) { this.events.delete(event); } } once(event: string, callback: EventCallback): () => void { const wrapped: EventCallback = (...args) => { try { callback(...args); } finally { this.off(event, wrapped); } }; return this.on(event, wrapped); } emit(event: string, ...args: unknown[]): void { this.anyEventCallbacks.forEach((cb) => { try { cb(event, ...args); } catch (error) { console.error(`RuntimeEvents: tap listener failed for "${event}"`, error); } }); const callbacks = this.events.get(event); if (!callbacks) { return; } callbacks.forEach((cb) => { try { cb(...args); } catch (error) { console.error(`RuntimeEvents: listener failed for "${event}"`, error); } }); } clear(): void { this.events.clear(); this.anyEventCallbacks.clear(); } getEventCount(): number { return this.events.size; } onAny(callback: AnyEventCallback): () => void { this.anyEventCallbacks.add(callback); return () => { this.anyEventCallbacks.delete(callback); }; } }