TypeSafe Event Emitter: Type-Safe Pub/Sub with Middleware for TypeScript

TypeScript··52 views
/**
 * A strongly-typed event emitter that provides full TypeScript type safety for events and their payloads.
 * Unlike traditional event emitters, this implementation ensures:
 * - Event names and payloads are type-checked at compile time
 * - Handlers receive correctly typed event data
 * - Events can only be emitted with the correct payload type
 * - Support for async event handlers
 * - Proper memory management with automatic cleanup
 * - Support for one-time event listeners
 * - Middleware support for event interception
 * 
 * Example usage:
 * ```typescript
 * // Define your events
 * type Events = {
 *   userJoined: { userId: string; timestamp: number };
 *   messageReceived: { text: string; from: string };
 *   typing: undefined;  // Events without payload
 * };
 * 
 * // Create emitter
 * const events = new TypedEventEmitter<Events>();
 * 
 * // Type-safe event handling
 * events.on('userJoined', ({ userId, timestamp }) => {
 *   console.log(`User ${userId} joined at ${timestamp}`);
 * });
 * 
 * // Won't compile if payload doesn't match type
 * events.emit('userJoined', { userId: '123', timestamp: Date.now() }); // ✅
 * events.emit('userJoined', { userId: '123' }); // ❌ Type error
 * ```
 */

type EventMap = Record<string | symbol, any>;
type EventKey<T extends EventMap> = string & keyof T;
type EventReceiver<T> = (params: T) => void | Promise<void>;

interface Middleware<T extends EventMap> {
  pre?: <K extends EventKey<T>>(eventName: K, data: T[K]) => T[K] | false | Promise<T[K] | false>;
  post?: <K extends EventKey<T>>(eventName: K, data: T[K]) => void | Promise<void>;
}

export class TypedEventEmitter<T extends EventMap> {
  private events = new Map<
    EventKey<T>,
    Set<{
      handler: EventReceiver<any>;
      once: boolean;
    }>
  >();
  private middlewares: Middleware<T>[] = [];

  /**
   * Add a middleware to intercept events
   */
  use(middleware: Middleware<T>) {
    this.middlewares.push(middleware);
    return this;
  }

  /**
   * Register an event handler
   */
  on<K extends EventKey<T>>(eventName: K, handler: EventReceiver<T[K]>) {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, new Set());
    }
    this.events.get(eventName)!.add({ handler, once: false });
    return () => this.off(eventName, handler);
  }

  /**
   * Register a one-time event handler
   */
  once<K extends EventKey<T>>(eventName: K, handler: EventReceiver<T[K]>) {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, new Set());
    }
    this.events.get(eventName)!.add({ handler, once: true });
    return () => this.off(eventName, handler);
  }

  /**
   * Remove an event handler
   */
  off<K extends EventKey<T>>(eventName: K, handler: EventReceiver<T[K]>) {
    const handlers = this.events.get(eventName);
    if (handlers) {
      for (const handlerObj of handlers) {
        if (handlerObj.handler === handler) {
          handlers.delete(handlerObj);
          break;
        }
      }
      if (handlers.size === 0) {
        this.events.delete(eventName);
      }
    }
  }

  /**
   * Remove all event handlers for a given event
   */
  removeAllListeners<K extends EventKey<T>>(eventName?: K) {
    if (eventName) {
      this.events.delete(eventName);
    } else {
      this.events.clear();
    }
  }

  /**
   * Emit an event with type-safe payload
   */
  async emit<K extends EventKey<T>>(eventName: K, data: T[K]) {
    const handlers = this.events.get(eventName);
    if (!handlers) return;

    // Run pre-middleware
    for (const middleware of this.middlewares) {
      if (middleware.pre) {
        const result = await middleware.pre(eventName, data);
        if (result === false) return;
        data = result;
      }
    }

    // Execute handlers
    const promises: Promise<void>[] = [];
    const handlersToRemove = new Set<{ handler: EventReceiver<T[K]>; once: boolean }>();

    for (const handlerObj of handlers) {
      if (handlerObj.once) {
        handlersToRemove.add(handlerObj);
      }
      const result = handlerObj.handler(data);
      if (result instanceof Promise) {
        promises.push(result);
      }
    }

    // Remove one-time handlers
    for (const handlerObj of handlersToRemove) {
      this.off(eventName, handlerObj.handler);
    }

    // Wait for all async handlers
    await Promise.all(promises);

    // Run post-middleware
    for (const middleware of this.middlewares) {
      if (middleware.post) {
        await middleware.post(eventName, data);
      }
    }
  }

  /**
   * Get the number of listeners for a given event
   */
  listenerCount<K extends EventKey<T>>(eventName: K): number {
    const handlers = this.events.get(eventName);
    return handlers ? handlers.size : 0;
  }

  /**
   * Check if an event has any listeners
   */
  hasListeners<K extends EventKey<T>>(eventName: K): boolean {
    return this.listenerCount(eventName) > 0;
  }
}