Back to docs
Architecture

Event Bus Design

A typed, in-process pub/sub backbone for decoupling Meridian subsystems — licensing, analytics, UI state, and IPC — without circular dependencies.

Why an event bus?

Meridian's loader, dashboard, and agent all emit lifecycle signals — license validated, heartbeat tick, tamper detected. A central bus lets each module subscribe to exactly what it needs without importing every other module. The result is a flat dependency graph where producers and consumers only know about the bus interface.

Core contract

type EventMap = {
  "license:validated":   { key: string; tier: number };
  "heartbeat:ok":        { latencyMs: number };
  "tamper:detected":     { reason: string };
  "ui:toast":            { message: string; variant: "info" | "error" };
};

type Handler<E> = (payload: E) => void;

interface Bus {
  on<K extends keyof EventMap>(e: K, h: Handler<EventMap[K]>): () => void;
  emit<K extends keyof EventMap>(e: K, p: EventMap[K]): void;
}

Every event is a string literal key mapped to a typed payload.on() returns an unsubscribe function so React effects can clean up automatically.

Implementation sketch

A single Map of event name → Set<Handler> lives in a module-scoped closure. Emit iterates the set synchronously; handlers that throw are caught and logged but never kill the bus. For React, a thinuseBus hook wraps useEffect to subscribe/unsubscribe on mount.

Rules

  • No async handlers — keep the bus synchronous; push async work to a queue if needed.
  • One bus instance per process. Singleton pattern via module-level const.
  • Events are fire-and-forget. No return values, no request/response.
  • Payloads are plain objects — never pass mutable refs across subscribers.