Files
2026-05-27 23:29:53 +01:00

3.5 KiB

client

(no submodules)

Purpose

Caller-side binding for the worker-RPC protocol. createRawWorkerRpcClient returns a low-level object exposing .call(method, params, opts) which returns a Promise<R>; createWorkerRpcClient<T> wraps that in a typed Proxy<T> so plugin authors invoke remote methods as if they were local async methods on a typed handle.

The client handles every cross-cutting concern: monotonic correlation-id allocation, request/response matching, optional AbortSignal forwarding via cancel frames, optional per-call timeout, budget-enforced backpressure, outbound queue management, structured-clone safety pre-checks, and inbound frame validation with version negotiation.

Features

  • Typed Proxy<T> so await client.method(arg) works without a generated stub.
  • AbortSignal per call; aborting sends a cancel frame and rejects the pending promise with an AbortError.
  • Per-call timeout via opts.timeoutMs or defaultTimeoutMs.
  • callBudget cap (default 1024) — synchronous-throw exhaustion with WORKER_RPC.BACKPRESSURE.
  • Pre-send assertCloneable walk rejecting functions, class instances, and symbols at the call site instead of letting the platform throw a DataCloneError later.
  • Inbound frames validated via the Zod protocol schema; invalid payloads and version mismatches are surfaced through the onProtocolError observer (never crash the listener).
  • onStreamFrame hook to multiplex stream frames into the streaming layer.

Usage: Basic

import { createWorkerRpcClient } from "@suaveplan/api-worker-rpc/client";

interface Calc {
    add(a: number, b: number): number;
}

const calc = createWorkerRpcClient<Calc>(workerPort);
const sum = await calc.add(2, 3);
calc.close();

Usage: Advanced

import { createRawWorkerRpcClient } from "@suaveplan/api-worker-rpc/client";

const client = createRawWorkerRpcClient(port, {
    callBudget: 256,
    defaultTimeoutMs: 5_000,
    onProtocolError: (err) => logger.warn({ err }, "rpc protocol error"),
});

const ac = new AbortController();
setTimeout(() => ac.abort(), 200);
try {
    const result = await client.call("longRunning", [42], { signal: ac.signal });
} catch (err) {
    if (err instanceof Error && err.name === "AbortError") {
        // caller cancelled
    }
}
client.close("shutdown");

API Reference

  • createRawWorkerRpcClient(port, opts?): WorkerRpcClient — low-level binding.
  • createWorkerRpcClient<T>(port, opts?): WorkerProxyClient<T> — typed proxy whose __raw__ field exposes the underlying client and whose close(reason?) tears it down.
  • interface WorkerRpcClient { call, send, cancel, nextId, registry, inFlight, close }.
  • interface WorkerRpcClientOptions { idGenerator?, callBudget?, outboundQueue?, defaultTimeoutMs?, onStreamFrame?, onProtocolError? }.
  • assertCloneable(value, path?): void — re-exported from ./cloneable-guard.

Implementation Notes

  • The proxy filters reserved keys (__raw__, close) so they do NOT become remote methods.
  • call acquires a budget slot synchronously before allocating the id so exhaustion fails at the call site, not later in the microtask queue.
  • The inbound listener catches parseFrame errors and routes them to onProtocolError rather than throwing — a malformed frame should never crash the host loop.
  • Cancel frames sent during abort do not require an ack from the server (best-effort cooperative cancel).