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?)withtimeoutandsignal. - Returns delivery counts from
publish/broadcastso callers know how many subscribers received the message. subscriberCount,getTopics(only topics with live subscribers),clear(drop everything + emitbus: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.
- throws
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.