# `message-bus` _(no submodules)_ ## Purpose `message-bus.ts` declares `MessageBus`, an in-process publish/subscribe + request/response message bus. It's the right tool when you want to decouple modules without actually crossing a worker boundary — every listener and handler runs synchronously on the main thread. The bus complements the worker managers: managers handle cross-thread RPC over `postMessage`, while the bus handles same-thread routing. Modules that produce events (e.g. an UI store) publish to a topic, and modules that consume them (e.g. an analytics pipeline) subscribe. ## Features - Topic-based pub/sub: `subscribe(topic, handler)`, `subscribeOnce`, `publish(topic, payload)`, `broadcast(payload)`. - Request/response: `registerHandler(topic, handler)`, `request(topic, payload, options?)` with `timeout` and `signal`. - Returns delivery counts from `publish` / `broadcast` so callers know how many subscribers received the message. - `subscriberCount`, `getTopics` (only topics with live subscribers), `clear` (drop everything + emit `bus:cleared`). - `EventBus` lifecycle event channel. ## Usage: Basic Example ```ts import { MessageBus } from "@suaveplan/web-workers"; const bus = new MessageBus(); bus.subscribe("user-login", (event) => analytics.track("login", event) ); bus.publish("user-login", { userId, at: Date.now() }); ``` ## Usage: Advanced Example ```ts import { MessageBus, WEB_WORKERS_CODES } from "@suaveplan/web-workers"; const bus = new MessageBus(); // Request handler with async work + abort support. bus.registerHandler( "search.execute", async (query) => { const docs = await searchIndex.query(query); return { hits: docs.length, results: docs }; } ); const ctrl = new AbortController(); try { const result = await bus.request( "search.execute", { text: "hello", filter: { lang: "en" } }, { signal: ctrl.signal, timeout: 5_000 } ); renderResults(result); } catch (err) { if ( err instanceof Error && (err as { code?: string }).code === WEB_WORKERS_CODES.REQUEST_TIMEOUT ) { renderTimeout(); } } ``` ## API Reference - `class MessageBus`. - `constructor(options?: MessageBusOptions)`. - `subscribe(topic, handler): () => void`. - `subscribeOnce(topic, handler): () => void`. - `publish(topic, payload): number` (delivery count). - `broadcast(payload): number`. - `registerHandler(topic, handler)`. - `request(topic, payload, options?: BusRequestOptions): Promise`. - throws `WEB_WORKERS.HANDLER_NOT_FOUND`, `WEB_WORKERS.REQUEST_TIMEOUT`, `WEB_WORKERS.REQUEST_ABORTED`. - `subscriberCount(topic): number`, `getTopics(): string[]`, `clear(): void`. - `readonly events: EventBus`. - `type MessageHandler = (payload: T, message: WorkerMessage) => void`. - `type RequestHandler = (payload: TReq) => TRes | Promise`. - `interface BusRequestOptions { timeout?: number; signal?: AbortSignal }`. ## Implementation Notes The bus uses two separate maps: `subscriptions` (per-topic array of once/persistent handlers) and `requestHandlers` (single handler per topic). This mirrors the upstream API so consumers can pub/sub and request/respond without coupling the two channels. `subscribeOnce` is implemented by marking the entry `once: true` and filtering it out on the publish loop — the unsubscribe closure returned by `subscribeOnce` removes the entry proactively if the consumer wants to cancel before the first publish. `request` is `async` to keep the type contract clean, but the handler is invoked synchronously inside the promise body so consumers can rely on the handler running before the next microtask.