core/events.tstypescript
/**
 * Type-safe event bus for SDK communication
 */

import type { EventMap } from '@/types/global';

/**
 * Singleton event bus behind `sdk.on` and `sdk.off`. Typed by {@link EventMap}, so a handler receives the payload its event name declares.
 *
 * @category Events
 */
export class EventBus {
  private static instance: EventBus;
  private listeners = new Map<keyof EventMap, Set<Function>>();

  private constructor() {}

  public static getInstance(): EventBus {
    if (!EventBus.instance) {
      EventBus.instance = new EventBus();
    }
    return EventBus.instance;
  }

  /**
   * Registers `handler` for `event` and returns a function that removes it again.
   *
   * Prefer the returned function over {@link EventBus.off}: `off` needs the exact
   * handler reference, so an inline arrow passed straight to `on` would otherwise be
   * impossible to remove. Calling the returned function twice is harmless, as is
   * calling it after an equivalent `off`.
   *
   * Inside an enhancer, use `this.on(...)` instead — it keeps this unsubscribe and
   * runs it on `destroy()`. Registering here directly leaks the handler for the
   * lifetime of the page, because the bus is a singleton.
   *
   * Handlers are held in a `Set`, so registering the *same function reference*
   * twice registers it once, and the first unsubscribe removes it for both callers.
   *
   * @example
   * const stop = EventBus.getInstance().on('cart:item-added', ({ packageId }) => {
   *   console.log('added', packageId);
   * });
   * stop(); // handler removed
   */
  public on<K extends keyof EventMap>(
    event: K,
    handler: (data: EventMap[K]) => void
  ): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(handler);
    return () => this.off(event, handler);
  }

  public emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
    const handlers = this.listeners.get(event);
    if (handlers) {
      handlers.forEach(handler => {
        try {
          (handler as (data: EventMap[K]) => void)(data);
        } catch (error) {
          console.error(`Event handler error for ${String(event)}:`, error);
        }
      });
    }
  }

  public off<K extends keyof EventMap>(event: K, handler: Function): void {
    const handlers = this.listeners.get(event);
    if (handlers) {
      handlers.delete(handler);
    }
  }

  public removeAllListeners<K extends keyof EventMap>(event?: K): void {
    if (event) {
      this.listeners.delete(event);
    } else {
      this.listeners.clear();
    }
  }
}