Files
skills/docs/foundation/data/web-workers/src/message-bus/message-bus.md
2026-05-29 08:20:56 +01:00

3.8 KiB

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<T>(topic, payload), broadcast<T>(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<MessageBusEventMap> lifecycle event channel.

Usage: Basic Example

import { MessageBus } from "@suaveplan/web-workers";

const bus = new MessageBus();
bus.subscribe<UserEvent>("user-login", (event) =>
    analytics.track("login", event)
);
bus.publish("user-login", { userId, at: Date.now() });

Usage: Advanced Example

import { MessageBus, WEB_WORKERS_CODES } from "@suaveplan/web-workers";

const bus = new MessageBus();

// Request handler with async work + abort support.
bus.registerHandler<SearchQuery, SearchResult>(
    "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<SearchQuery, SearchResult>(
        "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<T>(topic, handler): () => void.
    • subscribeOnce<T>(topic, handler): () => void.
    • publish<T>(topic, payload): number (delivery count).
    • broadcast<T>(payload): number.
    • registerHandler<TReq, TRes>(topic, handler).
    • request<TReq, TRes>(topic, payload, options?: BusRequestOptions): Promise<TRes>.
      • throws WEB_WORKERS.HANDLER_NOT_FOUND, WEB_WORKERS.REQUEST_TIMEOUT, WEB_WORKERS.REQUEST_ABORTED.
    • subscriberCount(topic): number, getTopics(): string[], clear(): void.
    • readonly events: EventBus<MessageBusEventMap>.
  • type MessageHandler<T> = (payload: T, message: WorkerMessage<T>) => void.
  • type RequestHandler<TReq, TRes> = (payload: TReq) => TRes | Promise<TRes>.
  • 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.