fix(01-02): replace Node EventEmitter with browser-compatible implementation

Node's 'events' module doesn't work in browsers. Replaced with custom
Map-based implementation that provides the same API but works in both
Node.js and browser environments.

All 98 tests still pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 00:09:45 +07:00
parent d0a39fc031
commit 4adca72e3a
+24 -12
View File
@@ -1,13 +1,11 @@
// src/game/EventEmitter.ts - Typed event emitter wrapper
// src/game/EventEmitter.ts - Typed event emitter (browser-compatible)
/**
* TypedEventEmitter provides a type-safe wrapper around Node's EventEmitter.
* It ensures event names and payloads are correctly typed.
* TypedEventEmitter provides a type-safe event system that works in browsers.
* Custom implementation - no Node.js dependencies.
*/
import { EventEmitter } from 'events';
export class TypedEventEmitter<T extends object> {
private emitter = new EventEmitter();
private listeners = new Map<keyof T, Set<(data: T[keyof T]) => void>>();
/**
* Register a listener for an event
@@ -16,7 +14,10 @@ export class TypedEventEmitter<T extends object> {
* @returns this for chaining
*/
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): this {
this.emitter.on(event as string, listener);
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener as (data: T[keyof T]) => void);
return this;
}
@@ -27,8 +28,11 @@ export class TypedEventEmitter<T extends object> {
* @returns this for chaining
*/
once<K extends keyof T>(event: K, listener: (data: T[K]) => void): this {
this.emitter.once(event as string, listener);
return this;
const onceWrapper = (data: T[K]) => {
this.off(event, onceWrapper);
listener(data);
};
return this.on(event, onceWrapper);
}
/**
@@ -38,7 +42,12 @@ export class TypedEventEmitter<T extends object> {
* @returns true if listeners were called, false otherwise
*/
emit<K extends keyof T>(event: K, data: T[K]): boolean {
return this.emitter.emit(event as string, data);
const eventListeners = this.listeners.get(event);
if (!eventListeners || eventListeners.size === 0) {
return false;
}
eventListeners.forEach(listener => listener(data as T[keyof T]));
return true;
}
/**
@@ -48,7 +57,10 @@ export class TypedEventEmitter<T extends object> {
* @returns this for chaining
*/
off<K extends keyof T>(event: K, listener: (data: T[K]) => void): this {
this.emitter.off(event as string, listener);
const eventListeners = this.listeners.get(event);
if (eventListeners) {
eventListeners.delete(listener as (data: T[keyof T]) => void);
}
return this;
}
@@ -58,7 +70,7 @@ export class TypedEventEmitter<T extends object> {
* @returns this for chaining
*/
removeAllListeners<K extends keyof T>(event: K): this {
this.emitter.removeAllListeners(event as string);
this.listeners.delete(event);
return this;
}
}