# `main-side` _(no submodules)_ ## Purpose Mounts a main-process `@suaveplan/events` `EventBus` on a `MessagePort` via the `@suaveplan/api-worker-rpc` server so worker-tier plugins can publish to and subscribe from the same shared bus. The host owns the `EventBus`; this module installs the RPC handlers and lifecycle. Three RPC methods are exposed: `emit`, `emitAsync`, and the streaming `subscribe`. Subscriptions return an `AsyncIterable` that yields one chunk per main-side event until the worker cancels (or `mode: "once"` ends after the first delivery). ## Features - Wires `emit` / `emitAsync` / `subscribe` over the canonical worker-RPC protocol — no bespoke wire format. - Per-subscription `highWaterMark` cap with `EVENTS_PROXY.BACKPRESSURE_EXCEEDED` rejection when the worker stops draining. - Payloads are Zod-validated and structured-clone-probed on encode; unsafe payloads surface as `EVENTS_PROXY.UNSERIALISABLE_PAYLOAD` BEFORE they touch the port. - `EventsProxyError` instances are serialised onto the wire by `api-worker-rpc`'s `errorToEnvelope` so the worker side reconstructs typed errors. - `dispose()` shuts the underlying `WorkerRpcServer` down; in-flight `subscribe` streams emit `stream-end` via the goodbye flow. ## Usage: Basic ```ts import { EventBus } from "@suaveplan/events"; import { createEventBusProxyServer } from "@suaveplan/api-events-worker-proxy/main-side"; import { createInProcessPortPair } from "@suaveplan/api-worker-rpc"; const bus = new EventBus<{ "billing.invoice.created": { id: string } }>(); const { clientPort, serverPort, close } = createInProcessPortPair(); const server = createEventBusProxyServer(serverPort, bus); // Pass `clientPort` to a worker; tear down with `server.dispose()` + close(). ``` ## Usage: Advanced ```ts import { EventBus } from "@suaveplan/events"; import { createEventBusProxyServer } from "@suaveplan/api-events-worker-proxy/main-side"; import { createInProcessPortPair } from "@suaveplan/api-worker-rpc"; type Map = { "drive.chunk.delivered": { chunkId: string } }; const bus = new EventBus(); const { clientPort, serverPort } = createInProcessPortPair(); const server = createEventBusProxyServer(serverPort, bus); // In-process subscribers keep receiving events as usual. bus.on("drive.chunk.delivered", ({ chunkId }) => console.log("main-side observed", chunkId) ); process.on("beforeExit", () => server.dispose()); ``` ## API Reference - `createEventBusProxyServer(port: PortLike, bus: EventBus): EventBusProxyServer`. - `EventBusProxyServer` — `{ dispose(reason?: string): void; readonly activeSubscriptionCount: number; }`. ## Implementation Notes The handlers object passed to `serveWorkerRpc` declares three methods. `emit` and `emitAsync` parse positional params (`[topic, payload, traceId?, timeoutMs?]`), decode the wire envelope with `decodeEnvelope`, and forward to `bus.emit` / `bus.emitAsync`. The streaming `subscribe` handler builds an `AsyncIterable` whose body subscribes to the bus, enqueues payloads on each event, and yields them on each iteration. Backpressure is enforced by checking `queue.length` against the worker-supplied high-water mark; over-budget events surface `BACKPRESSURE_EXCEEDED` and end the stream. Cancellation is wired through `AbortSignal`: the server's `cancel` frame triggers the per-call signal, which the iterator observes via `signal.aborted` plus a `wake()` callback so the pending `await` resolves immediately. The `unsubscribe` happens in the iterator's `finally` block so it runs on both normal completion and cancellation.