Zero-dependency, transport-agnostic RPC for Deno — your TypeScript interface is the contract, and remote calls feel local.
A TypeScript interface is the contract. Zero codegen, zero dependencies, zero ceremony.
rpx makes remote calls feel identical to local ones. Define an interface,
implement it on the server, call it from the client — a Proxy does the rest.
The transport (UDS, TCP, stdio, WebSocket) is a swappable parameter, not an
architectural commitment.
// shared.ts — the interface IS the contract
interface MathService {
add(a: number, b: number): Promise<number>;
stream(from: number): AsyncIterable<number>;
}
// server.ts
import { serve } from "@rpx/core";
await serve<MathService>("/tmp/math.sock", {
add: (a, b) => a + b, // bare value or Promise — both fine
async *stream(from) {
for (let i = from; i < from + 5; i++) yield i;
},
});
// client.ts — feels 100% local
import { connect } from "@rpx/core";
const svc = await connect<MathService>("/tmp/math.sock");
await svc.add(1, 2); // 3
for await (const n of svc.stream(10)) console.log(n); // 10 11 12 13 14deno add jsr:@rpx/coreSame code, different transport. A string is shorthand for a UDS path.
import {
connect,
serve,
tcp,
uds,
websocket,
websocketServer,
} from "@rpx/core";
await serve(uds("/tmp/svc.sock"), impl); // Unix domain socket (default)
await serve(tcp({ port: 9000 }), impl); // TCP
await serve(websocketServer(9001), impl); // WebSocket (browser ↔ server)
const a = await connect<Svc>(uds("/tmp/svc.sock"));
const b = await connect<Svc>(tcp({ hostname: "10.0.0.1", port: 9000 }));
const c = await connect<Svc>(websocket("ws://localhost:9001"));stdio (for spawned child processes / plugins):
import { stdioClient, stdioServer } from "@rpx/core";
await serve(stdioServer(), impl); // inside the child
const svc = await connect<Svc>(stdioClient(childProc)); // in the parentA server cleans up automatically with await using (Deno's
Symbol.asyncDispose):
await using server = await serve(uds("/tmp/svc.sock"), impl);
// ... server.close() runs at end of scopeawait connect<Svc>(t, {
timeout: 30_000, // ms before a call rejects with "Call timed out" (default 30s)
onError: (e) => console.error(e), // transport/decoder errors + connection close
});
await serve(t, impl, {
onError: (e) => console.error(e), // handler throws, decode failures, etc.
maxConcurrentCalls: 1024, // per-connection in-flight cap (default 1024)
});| Export | Description |
|---|---|
serve<T>(t, impl, opts?) |
Start a server. string → UDS path. Returns a server. |
connect<T>(t, opts?) |
Connect; returns a typed proxy T. |
uds(path) |
Unix domain socket transport |
tcp({ hostname?, port }) |
TCP transport |
stdioServer() |
stdio transport — server (in the child process) |
stdioClient(proc) |
stdio transport — client (wraps a Deno.ChildProcess) |
websocket(url) |
WebSocket transport — client |
websocketServer(port) |
WebSocket transport — server |
- Wire format: NDJSON-style messages. Byte-stream transports (UDS/TCP/stdio) add a 4-byte length prefix (16 MB frame cap); WebSocket is already message-framed.
- Multiplexing: every call gets a
crypto.randomUUID(); responses dispatch by id, so many calls and streams share one connection concurrently. - Streaming: return an
AsyncIterableand it streams chunk-by-chunk. Clientbreak(or disconnect) sends acancel, so the server generator'sfinallyruns — no leaks. - Errors: a thrown handler rejects the client call with the same message;
unknown or inherited (
toString,constructor, …) methods reject withMethod not found.
| rpx | Comlink | tRPC | gRPC | |
|---|---|---|---|---|
| Codegen / schema | none | none | router build | .proto |
| Contract | TS interface | TS types | Zod + router | protobuf |
| Transports | UDS/TCP/stdio/WS | postMessage | HTTP/WS | HTTP/2 |
| Streaming | async iterables | manual | yes | yes |
| Dependencies | 0 | 0 | several | many |
Why rpx? Fractal design: the same serve/connect API works between
threads (stdio), processes (UDS), machines (TCP), and browser↔server
(WebSocket). Swap the transport argument; nothing else changes.
deno run --allow-read --allow-write --allow-net app.ts--allow-read/--allow-write: UDS socket file--allow-net: TCP and WebSocket
Out of scope for now: auth/TLS (tunnel TCP), runtime schema validation (TypeScript is the contract), client→server streaming, and a Node adapter. UDS requires Windows 10+ / Unix.