Skip to content
riagenticPublic

About

Zero-dependency, transport-agnostic RPC for Deno — your TypeScript interface is the contract, and remote calls feel local.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

15 Commits

Folders and files

Repository files navigation

@rpx/core

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 14

Install

deno add jsr:@rpx/core

Swap transports

Same 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 parent

Lifecycle

A 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 scope

Options

await 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)
});

API

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

How it works

  • 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 AsyncIterable and it streams chunk-by-chunk. Client break (or disconnect) sends a cancel, so the server generator's finally runs — no leaks.
  • Errors: a thrown handler rejects the client call with the same message; unknown or inherited (toString, constructor, …) methods reject with Method not found.

vs. alternatives

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.

Permissions

deno run --allow-read --allow-write --allow-net app.ts
  • --allow-read / --allow-write: UDS socket file
  • --allow-net: TCP and WebSocket

Scope (v0.1)

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.

About

Zero-dependency, transport-agnostic RPC for Deno — your TypeScript interface is the contract, and remote calls feel local.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages