@codehz/ts-rpc
v0.1.0
Published
Transport-agnostic、双向对象能力的 TypeScript RPC 库。
Readme
ts-rpc
Transport-agnostic、双向对象能力的 TypeScript RPC 库。
- 传输无关:只依赖一个
RpcTransport接口(send/receive),可接驳 WebSocket、MessagePort、Worker、HTTP 或任意自定义通道 - 双向对象引用:两端各自暴露 main 对象,可互相调用方法、传递函数、传递
RpcTarget实例,引用自动回收 - Promise pipelining:可直接链式调用尚未返回的远端 capability,多级调用只占用一个往返窗口
- 流桥接:
ReadableStream/WritableStream跨 RPC 透传,writer.write(chunk)的 Promise 在对端实际完成时才 resolve - 构建发布:使用
tsdown生成 ESM +.d.ts+ sourcemap,运行时import时自动注入流桥接钩子 - 运行时:Bun
安装
bun install如需生成发布产物:
bun run build快速开始
import { RpcSession, RpcTarget, createTransportPair } from "ts-rpc";
// 1) 定义服务端 API:继承 RpcTarget 让对象按引用传递
class Calculator extends RpcTarget {
add(a: number, b: number) { return a + b; }
async slowAdd(a: number, b: number): Promise<number> { return a + b; }
}
// 2) 构造一对互连的 RpcTransport(这里用内存对,实际可换成 WebSocket 等)
const [clientTransport, serverTransport] = createTransportPair();
// 3) 各建一个 RpcSession,服务端传入 main 对象
const server = new RpcSession(serverTransport, new Calculator());
const client = new RpcSession(clientTransport);
// 4) 客户端拿到对端 main 的代理 stub,像本地一样调用
const remote = client.getRemoteMain<Calculator>();
console.log(await remote.add(2, 3)); // 5
console.log(await remote.slowAdd(40, 2)); // 42Promise pipelining
远端方法返回 RpcTarget(或函数 capability)时,无需先等待中间结果:
class Profile extends RpcTarget {
getName() { return "Ada"; }
}
class Users extends RpcTarget {
async getProfile(): Promise<Profile> {
return new Profile();
}
}
const remote = client.getRemoteMain<Users>();
const name = await remote.getProfile().getName();getName 调用会在 getProfile 返回前立即发送。每次调用结果都会成为一个隐式 export,
因此未决结果与普通 capability 共用同一套 import/export 引用计数和 release 回收机制。
当前原型只支持 capability 方法调用;普通值属性投影、stream pipelining,以及把
未 await 的 RpcPromise 直接作为参数传递均不支持。
RpcPromise 支持显式资源管理。若不再需要通过某个未决结果继续 pipeline,可使用
using 或调用其 [Symbol.dispose]();这只释放未来寻址能力,不会取消已发送的调用,
也不会阻止该 Promise 自身最终 resolve/reject。
核心概念
RpcTransport — 唯一的传输抽象
interface RpcTransport {
send(message: unknown): void | Promise<void>;
receive(): Promise<unknown>;
abort?(reason: unknown): void;
}消息是不透明对象(内部为 expression tree,可被 JSON.stringify 或 structuredClone)。实现 RpcTransport 时自由选择底层编码:
RpcTransport 必须可靠且保持 FIFO:多次 send() 的消息需按调用顺序被对端 receive()
取得。Session 不会等待前一次异步 send() 完成后再发送下一条消息。
- WebSocket 文本帧:用
createJsonStringTransport包装ws.send/ws.onmessage - MessagePort / Worker:直接
postMessage/event.data,structuredClone 透传 - 自定义二进制:CBOR / MessagePack 等由用户加持
RpcTarget — 按引用传递
继承 RpcTarget 的实例、任意函数、内置 ReadableStream / WritableStream 才按引用传递;Date、Error、Uint8Array 等走值拷贝。Map / Set 不支持,需先转为普通对象/数组。
双向调用与回调
两端都能向对端传递对象和函数,对端可反向调用:
class ServerAPI extends RpcTarget {
// 接收客户端传入的函数 stub,在服务端反向调用
async callFn(fn: (x: number) => number, x: number): Promise<number> {
return fn(x);
}
}
const server = new RpcSession(serverTransport, new ServerAPI());
const client = new RpcSession(clientTransport);
const remote = client.getRemoteMain<ServerAPI>();
// 客户端把本地函数传过去,服务端调用它
const result = await remote.callFn((x: number) => x * 10, 5);
console.log(result); // 50流
class FileReceiver extends RpcTarget {
async save(stream: WritableStream<string>): Promise<void> {
const writer = stream.getWriter();
await writer.write("hello");
await writer.write("world");
await writer.close();
}
}
// 客户端创建本地 WritableStream,传给服务端写入
const localStream = new WritableStream<string>({
write(chunk) { console.log("收到:", chunk); },
});
const remote = client.getRemoteMain<FileReceiver>();
await remote.save(localStream);
// 输出:收到: hello / 收到: worldwriter.write(chunk) 的 Promise 在对端实际写入/读取完成时才 resolve——真正的端到端 ack。chunk 中可包含 RpcTarget 对象或函数,引用由 FinalizationRegistry 在 GC 时自动回收。
API 一览
| 导出 | 用途 |
|------|------|
| RpcTransport | 传输接口(用户实现) |
| RpcSession | 在 RpcTransport 上建立双向 RPC 会话 |
| RpcTarget | 标记基类,继承后按引用传递 |
| RpcStub | 对象代理,new RpcStub(obj) 包装本地对象 |
| RpcPromise | 可 await、同时可继续调用未决 capability 的 Promise-like 类型 |
| serialize / deserialize | 纯值版本(无会话时往返简单数据) |
| encodeMessage / decodeMessage / createJsonStringTransport | 接驳字符串通道 |
| createTransportPair | 测试用内存 RpcTransport 对 |
| RpcAbortError | 会话中止错误 |
运行与测试
bun run build # 生成 dist/ 发布产物(tsdown)
bun run build:watch # 监听并持续构建
bun test # 运行全部测试
bun test test/index.test.ts # 运行指定测试文件
bun run src/index.ts # 运行入口
bunx tsc --noEmit # 类型检查发布产物
构建后会生成:
dist/index.js:ESM 入口dist/index.d.ts:类型声明dist/index.js.map:源码映射
package.json 的 exports / module / types 已指向 dist,发布时只包含 dist/。
许可
MIT
