Files
skills/docs/api-stack/api/api-events-worker-proxy/src/main-side/main-side.md
2026-05-29 08:20:56 +01:00

3.6 KiB

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<unknown> 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

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

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<Map>();
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<TMap>(port: PortLike, bus: EventBus<TMap>): 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<unknown> 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.