| name | kkrpc |
| description | Build bidirectional RPC systems in TypeScript with kkrpc. Create RPC channels, expose APIs, use multiple transports (stdio, WebSocket, HTTP), handle callbacks, property access, and errors across Node.js, Deno, Bun, and browsers. |
| version | 1.0.0 |
| license | MIT |
| metadata | {"author":"kkrpc","domain":"typescript-rpc","tags":["rpc","typescript","bidirectional","ipc","websocket","stdio","cross-runtime"]} |
| compatibility | Works in Node.js, Deno, Bun, and browsers with appropriate entry points |
kkrpc - TypeScript RPC Library
Build bidirectional RPC systems in TypeScript with full type safety and multiple transport options.
Installation
npm install kkrpc
pnpm add kkrpc
import { RPCChannel } from "jsr:@kunkun/kkrpc"
Quick Start
Basic RPC Setup
Server (expose API):
import { NodeIo, RPCChannel } from "kkrpc"
const api = {
greet: (name: string) => `Hello, ${name}!`,
add: (a: number, b: number) => a + b,
counter: 42
}
const rpc = new RPCChannel(new NodeIo(process.stdin, process.stdout), { expose: api })
Client (consume API):
import { spawn } from "child_process"
import { NodeIo, RPCChannel } from "kkrpc"
const worker = spawn("bun", ["server.ts"])
const rpc = new RPCChannel(new NodeIo(worker.stdout, worker.stdin))
const api = rpc.getAPI<typeof api>()
console.log(await api.greet("World"))
console.log(await api.add(5, 3))
console.log(await api.counter)
Core Concepts
RPCChannel
The main class that manages bidirectional communication:
class RPCChannel<LocalAPI extends Record<string, any>, RemoteAPI extends Record<string, any>> {
constructor(
io: IoInterface,
options?: {
expose?: LocalAPI
serialization?: { version: "json" | "superjson" }
validators?: RPCValidators<LocalAPI>
}
)
getAPI(): RemoteAPI
expose(api: LocalAPI): void
}
IoInterface (Transports)
Transport adapters for different environments:
| Transport | Class | Environment |
|---|
| stdio | NodeIo, DenoIo, BunIo | Process-to-process |
| WebSocket | WebSocketClientIO, WebSocketServerIO | Network |
| HTTP | HTTPClientIO, HTTPServerIO | Web APIs |
| Worker | WorkerParentIO, WorkerChildIO | Web Workers |
| postMessage | IframeParentIO, IframeChildIO | iframes |
| Chrome Extension | ChromePortIO | Chrome extensions |
| Electron | ElectronIpcMainIO, ElectronIpcRendererIO | Electron |
| Message Queues | RabbitMQIO, KafkaIO, RedisStreamsIO, NatsIO | Distributed |
API Definition Patterns
Pattern 1: Inline API (Simple)
const api = {
greet: (name: string) => `Hello, ${name}!`,
add: (a: number, b: number) => a + b
}
type API = typeof api
const rpc = new RPCChannel<API, API>(io, { expose: api })
const remote = rpc.getAPI()
Pattern 2: Interface-First (Recommended)
interface MathAPI {
add(a: number, b: number): Promise<number>
multiply(a: number, b: number): Promise<number>
}
interface MyAPI {
math: MathAPI
greet(name: string): Promise<string>
}
const api: MyAPI = {
math: {
add: async (a, b) => a + b,
multiply: async (a, b) => a * b
},
greet: async (name) => `Hello, ${name}!`
}
const rpc = new RPCChannel<MyAPI, MyAPI>(io, { expose: api })
Pattern 3: Nested APIs
interface API {
math: {
basic: {
add(a: number, b: number): Promise<number>
subtract(a: number, b: number): Promise<number>
}
advanced: {
pow(base: number, exp: number): Promise<number>
sqrt(n: number): Promise<number>
}
}
}
const result = await api.math.advanced.pow(2, 10)
Transport Examples
Stdio (Process Communication)
import { spawn } from "child_process"
import { NodeIo, RPCChannel } from "kkrpc"
const child = spawn("bun", ["worker.ts"])
const io = new NodeIo(child.stdout, child.stdin)
const rpc = new RPCChannel<LocalAPI, RemoteAPI>(io, { expose: localApi })
const api = rpc.getAPI()
WebSocket
import { RPCChannel, WebSocketClientIO, WebSocketServerIO } from "kkrpc"
wss.on("connection", (ws) => {
const io = new WebSocketServerIO(ws)
const rpc = new RPCChannel<API, API>(io, { expose: api })
})
const ws = new WebSocket("ws://localhost:3000")
const io = new WebSocketClientIO({ ws })
const rpc = new RPCChannel<{}, API>(io)
const api = rpc.getAPI()
Web Worker
import { WorkerParentIO, WorkerChildIO, RPCChannel } from "kkrpc/browser"
const worker = new Worker("./worker.ts", { type: "module" })
const io = new WorkerParentIO(worker)
const rpc = new RPCChannel<LocalAPI, RemoteAPI>(io, { expose: localApi })
const io = new WorkerChildIO()
const rpc = new RPCChannel<RemoteAPI, LocalAPI>(io, { expose: api })
HTTP
import { HTTPClientIO, HTTPServerIO, RPCChannel } from "kkrpc"
const serverIO = new HTTPServerIO()
const serverRPC = new RPCChannel<API, API>(serverIO, { expose: api })
Bun.serve({
async fetch(req) {
if (new URL(req.url).pathname === "/rpc") {
const response = await serverIO.handleRequest(await req.text())
return new Response(response, {
headers: { "Content-Type": "application/json" }
})
}
return new Response("Not found", { status: 404 })
}
})
const clientIO = new HTTPClientIO({ url: "http://localhost:3000/rpc" })
const clientRPC = new RPCChannel<{}, API>(clientIO)
Advanced Features
Callback Functions
Send functions as arguments that can be invoked remotely:
interface API {
processData(data: string, onProgress: (percent: number) => void): Promise<string>
}
const api: API = {
processData: async (data, onProgress) => {
for (let i = 0; i <= 100; i += 10) {
onProgress(i)
await sleep(100)
}
return `Processed: ${data}`
}
}
const result = await api.processData("my-data", (progress) => {
console.log(`Progress: ${progress}%`)
})
Property Access
Access and mutate remote properties:
interface API {
counter: number
settings: {
theme: string
notifications: { enabled: boolean }
}
}
const count = await api.counter
const theme = await api.settings.theme
api.counter = 100
api.settings.theme = "dark"
Enhanced Error Handling
Errors preserve name, message, stack, and custom properties:
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public code: number
) {
super(message)
this.name = "ValidationError"
}
}
type API = {
validateUser(data: unknown): Promise<void>
}
try {
await api.validateUser({})
} catch (error) {
console.log(error.name)
console.log(error.message)
console.log(error.field)
console.log(error.code)
}
Validation (Optional)
Use Standard Schema (Zod, Valibot, ArkType) for runtime validation:
import { RPCChannel, type RPCValidators } from "kkrpc"
import { z } from "zod"
type MathAPI = {
add(a: number, b: number): Promise<number>
}
const api: MathAPI = {
add: async (a, b) => a + b
}
const validators: RPCValidators<MathAPI> = {
add: {
input: z.tuple([z.number(), z.number()]),
output: z.number()
}
}
const rpc = new RPCChannel(io, { expose: api, validators })
Transferable Objects (Browser)
Zero-copy transfer of large binary data:
import { RPCChannel, transfer, WorkerParentIO } from "kkrpc/browser"
interface API {
processBuffer(buffer: ArrayBuffer): Promise<number>
}
const worker = new Worker("worker.js")
const io = new WorkerParentIO(worker)
const rpc = new RPCChannel<{}, API>(io)
const api = rpc.getAPI()
const buffer = new ArrayBuffer(10 * 1024 * 1024)
const result = await api.processBuffer(transfer(buffer, [buffer]))
Serialization Options
JSON (Default for Interop)
const rpc = new RPCChannel(io, {
expose: api,
serialization: { version: "json" }
})
- Standard JSON
- Works with all interop languages
- No Date, Map, Set, BigInt support
SuperJSON (Default, TypeScript-only)
const rpc = new RPCChannel(io, {
expose: api,
serialization: { version: "superjson" }
})
- Supports Date, Map, Set, BigInt, Uint8Array
- TypeScript-to-TypeScript only
- Auto-detected by receiver
Common Patterns
Bidirectional Communication
Both sides expose APIs:
interface API_A {
compute(data: number[]): Promise<number>
}
interface API_B {
notify(message: string): Promise<void>
}
const apiA: API_A = {
compute: async (data) => data.reduce((a, b) => a + b, 0)
}
const rpc = new RPCChannel<API_A, API_B>(io, { expose: apiA })
const apiB = rpc.getAPI()
await apiB.notify("Computation complete")
Dynamic API Exposure
Change exposed API at runtime:
const rpc = new RPCChannel(io)
rpc.expose(newApi)
Cleanup
Destroy connections when done:
io.destroy()
Environment-Specific Entry Points
| Environment | Import Path |
|---|
| Node.js | kkrpc |
| Deno | kkrpc/deno or jsr:@kunkun/kkrpc |
| Bun | kkrpc |
| Browser | kkrpc/browser |
| Chrome Extension | kkrpc/chrome-extension |
import { RPCChannel, WorkerParentIO } from "kkrpc/browser"
import { RPCChannel, DenoIo } from "kkrpc/deno"
import { RPCChannel, ChromePortIO } from "kkrpc/chrome-extension"
Error Reference
| Error | Cause | Solution |
|---|
RPCValidationError | Input/output validation failed | Check validation schema |
TimeoutError | Request timed out | Increase timeout or check connection |
TransportClosed | Connection closed unexpectedly | Check transport health |
Testing
Test with Reference Implementation
const api = {
math: { add: (a: number, b: number) => a + b },
echo: <T>(v: T) => v,
counter: 42
}
Unit Test Pattern
import { expect, test } from "bun:test"
import { NodeIo, RPCChannel } from "kkrpc"
test("basic RPC call", async () => {
const api = { add: (a: number, b: number) => a + b }
const { port1, port2 } = new MessageChannel()
const serverIO = new NodeIo(port1)
const clientIO = new NodeIo(port2)
new RPCChannel<typeof api, {}>(serverIO, { expose: api })
const client = new RPCChannel<{}, typeof api>(clientIO)
const result = await client.getAPI().add(2, 3)
expect(result).toBe(5)
})
Best Practices
- Use Interface-First: Define interfaces before implementations for type safety
- Handle Errors: Always wrap RPC calls in try-catch
- Clean Up: Destroy transports when components unmount
- Validate Inputs: Use validators for public APIs
- Choose Serialization: Use JSON for interop, superjson for TS-only
- Transfer Large Data: Use
transfer() for ArrayBuffers in browsers
- Namespace APIs: Use nested objects to organize methods
References
- Package:
packages/kkrpc/
- Core source:
packages/kkrpc/src/
- Serialization:
packages/kkrpc/src/serialization.ts
- Adapters:
packages/kkrpc/src/adapters/
- Interop guide:
skills/interop/SKILL.md