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.