first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
# `message-bus`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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.