kkrpc 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +430 -2
- package/README.zh.md +1650 -0
- package/dist/browser-mod.cjs +1 -1
- package/dist/browser-mod.d.cts +4 -4
- package/dist/browser-mod.d.ts +4 -4
- package/dist/browser-mod.js +1 -1
- package/dist/browser-mod.js.map +1 -1
- package/dist/channel-B-xw0r_-.cjs +6 -0
- package/dist/channel-C4KXiIq_.d.cts +519 -0
- package/dist/channel-C4KXiIq_.d.cts.map +1 -0
- package/dist/channel-CLazgI0u.d.ts +519 -0
- package/dist/channel-CLazgI0u.d.ts.map +1 -0
- package/dist/channel-Dl81iz1-.js +7 -0
- package/dist/channel-Dl81iz1-.js.map +1 -0
- package/dist/chrome-extension.cjs +1 -1
- package/dist/chrome-extension.d.cts +3 -3
- package/dist/chrome-extension.d.ts +3 -3
- package/dist/chrome-extension.js +1 -1
- package/dist/chrome-extension.js.map +1 -1
- package/dist/{deno-DS3TcO_u.d.ts → deno-DLEehrMZ.d.ts} +2 -2
- package/dist/{deno-DS3TcO_u.d.ts.map → deno-DLEehrMZ.d.ts.map} +1 -1
- package/dist/{deno-BNN8LLrd.d.cts → deno-X3dHX6ii.d.cts} +2 -2
- package/dist/{deno-BNN8LLrd.d.cts.map → deno-X3dHX6ii.d.cts.map} +1 -1
- package/dist/deno-mod.cjs +1 -1
- package/dist/deno-mod.d.cts +4 -4
- package/dist/deno-mod.d.ts +4 -4
- package/dist/deno-mod.js +1 -1
- package/dist/electron-ipc.cjs +1 -1
- package/dist/electron-ipc.d.cts +3 -3
- package/dist/electron-ipc.d.ts +3 -3
- package/dist/electron-ipc.js +1 -1
- package/dist/electron-ipc.js.map +1 -1
- package/dist/electron.cjs +1 -1
- package/dist/electron.d.cts +3 -3
- package/dist/electron.d.cts.map +1 -1
- package/dist/electron.d.ts +3 -3
- package/dist/electron.d.ts.map +1 -1
- package/dist/electron.js +1 -1
- package/dist/electron.js.map +1 -1
- package/dist/{http-DWq6Ez_h.d.cts → http-BXn39Czz.d.cts} +2 -2
- package/dist/{http-DWq6Ez_h.d.cts.map → http-BXn39Czz.d.cts.map} +1 -1
- package/dist/{http-CcLOuQmc.d.ts → http-DMg5ci8E.d.ts} +2 -2
- package/dist/{http-CcLOuQmc.d.ts.map → http-DMg5ci8E.d.ts.map} +1 -1
- package/dist/http.cjs +1 -1
- package/dist/http.d.cts +3 -3
- package/dist/http.d.ts +3 -3
- package/dist/http.js +1 -1
- package/dist/{interface-DRqrAKo-.d.ts → interface-COwEog13.d.cts} +2 -2
- package/dist/interface-COwEog13.d.cts.map +1 -0
- package/dist/{interface-DPtHJBBS.d.cts → interface-Cdp6K7Dz.d.ts} +2 -2
- package/dist/interface-Cdp6K7Dz.d.ts.map +1 -0
- package/dist/{kafka-_Fcc7erL.d.cts → kafka-CEnBN-7e.d.cts} +2 -2
- package/dist/{kafka-_Fcc7erL.d.cts.map → kafka-CEnBN-7e.d.cts.map} +1 -1
- package/dist/{kafka-BMOHF0lZ.d.ts → kafka-j0MF9lik.d.ts} +2 -2
- package/dist/{kafka-BMOHF0lZ.d.ts.map → kafka-j0MF9lik.d.ts.map} +1 -1
- package/dist/kafka.d.cts +2 -2
- package/dist/kafka.d.ts +2 -2
- package/dist/mod.cjs +2 -2
- package/dist/mod.d.cts +10 -59
- package/dist/mod.d.cts.map +1 -1
- package/dist/mod.d.ts +10 -59
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +2 -2
- package/dist/mod.js.map +1 -1
- package/dist/{nats-D3XOZAGl.d.ts → nats-C1aVwb8x.d.ts} +2 -2
- package/dist/{nats-D3XOZAGl.d.ts.map → nats-C1aVwb8x.d.ts.map} +1 -1
- package/dist/{nats-Bkr44tQB.d.cts → nats-c0XYE-Ao.d.cts} +2 -2
- package/dist/{nats-Bkr44tQB.d.cts.map → nats-c0XYE-Ao.d.cts.map} +1 -1
- package/dist/nats.d.cts +2 -2
- package/dist/nats.d.ts +2 -2
- package/dist/{rabbitmq-CpeO6XdQ.d.ts → rabbitmq-DWEE5gve.d.ts} +2 -2
- package/dist/{rabbitmq-CpeO6XdQ.d.ts.map → rabbitmq-DWEE5gve.d.ts.map} +1 -1
- package/dist/{rabbitmq-DAUXsuvL.d.cts → rabbitmq-OGWQJkUQ.d.cts} +2 -2
- package/dist/{rabbitmq-DAUXsuvL.d.cts.map → rabbitmq-OGWQJkUQ.d.cts.map} +1 -1
- package/dist/rabbitmq.cjs +1 -1
- package/dist/rabbitmq.d.cts +4 -4
- package/dist/rabbitmq.d.ts +4 -4
- package/dist/rabbitmq.js +1 -1
- package/dist/{redis-streams-DpbNc20y.d.cts → redis-streams-DeCCm9lZ.d.cts} +2 -2
- package/dist/{redis-streams-DpbNc20y.d.cts.map → redis-streams-DeCCm9lZ.d.cts.map} +1 -1
- package/dist/{redis-streams-avvO_U2r.d.ts → redis-streams-RKCo8Gip.d.ts} +2 -2
- package/dist/{redis-streams-avvO_U2r.d.ts.map → redis-streams-RKCo8Gip.d.ts.map} +1 -1
- package/dist/redis-streams.d.cts +2 -2
- package/dist/redis-streams.d.ts +2 -2
- package/dist/socketio.d.cts +1 -1
- package/dist/socketio.d.ts +1 -1
- package/dist/tauri-BAtWibFE.js +2 -0
- package/dist/tauri-BAtWibFE.js.map +1 -0
- package/dist/tauri-Ble8sg9S.d.cts +131 -0
- package/dist/tauri-Ble8sg9S.d.cts.map +1 -0
- package/dist/tauri-CSGFwEY6.d.ts +131 -0
- package/dist/tauri-CSGFwEY6.d.ts.map +1 -0
- package/dist/tauri-DRz52iou.cjs +1 -0
- package/package.json +23 -20
- package/dist/channel-ChGarSnI.d.cts +0 -154
- package/dist/channel-ChGarSnI.d.cts.map +0 -1
- package/dist/channel-K9w_2vmv.d.ts +0 -154
- package/dist/channel-K9w_2vmv.d.ts.map +0 -1
- package/dist/channel-LGw9tl8f.js +0 -7
- package/dist/channel-LGw9tl8f.js.map +0 -1
- package/dist/channel-OGNDLfsd.cjs +0 -6
- package/dist/interface-DPtHJBBS.d.cts.map +0 -1
- package/dist/interface-DRqrAKo-.d.ts.map +0 -1
- package/dist/tauri-CSoj53la.js +0 -2
- package/dist/tauri-CSoj53la.js.map +0 -1
- package/dist/tauri-DIJzjZwG.d.ts +0 -59
- package/dist/tauri-DIJzjZwG.d.ts.map +0 -1
- package/dist/tauri-DvS2Czwp.cjs +0 -1
- package/dist/tauri-d0yDsUnV.d.cts +0 -59
- package/dist/tauri-d0yDsUnV.d.cts.map +0 -1
package/README.md
CHANGED
|
@@ -29,6 +29,29 @@ Call remote functions as if they were local, with full TypeScript type safety an
|
|
|
29
29
|
|
|
30
30
|
[**Quick Start**](#-quick-start) • [**Documentation**](https://kunkunsh.github.io/kkrpc/) • [**Examples**](#-examples) • [**API Reference**](https://jsr.io/@kunkun/kkrpc/doc) • [**LLM Docs**](https://docs.kkrpc.kunkun.sh/llms.txt) • [**中文文档**](./README.zh.md)
|
|
31
31
|
|
|
32
|
+
## 🎥 Video Tutorial
|
|
33
|
+
|
|
34
|
+
[](https://youtu.be/CF8lji8eB30)
|
|
35
|
+
|
|
36
|
+
Watch this video for a comprehensive introduction to kkrpc and how to use it in your projects.
|
|
37
|
+
|
|
38
|
+
## 🤖 AI Support
|
|
39
|
+
|
|
40
|
+
Working with kkrpc in your AI-powered editor? Add these skills to your Claude Code configuration to get intelligent assistance:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Copy kkrpc skills to your global Claude Code skills folder
|
|
44
|
+
cp -r skills/kkrpc ~/.claude/skills/
|
|
45
|
+
cp -r skills/interop ~/.claude/skills/
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This provides your AI assistant with:
|
|
49
|
+
|
|
50
|
+
- **kkrpc skill**: How to use kkrpc in TypeScript projects
|
|
51
|
+
- **interop skill**: How to implement kkrpc clients/servers in other languages (Go, Python, Rust, Swift)
|
|
52
|
+
|
|
53
|
+
See [`skills/`](./skills/) directory for details.
|
|
54
|
+
|
|
32
55
|
<div align="center">
|
|
33
56
|
|
|
34
57
|
<img src="https://imgur.com/19XswxO.jpg" style="max-width: 800px; width: 100%; margin-bottom: 20px;"/>
|
|
@@ -61,6 +84,10 @@ kkrpc stands out in the crowded RPC landscape by offering **true cross-runtime c
|
|
|
61
84
|
| **🔗 Nested Calls** | Deep method chaining like `api.math.operations.calculate()` |
|
|
62
85
|
| **📦 Auto Serialization** | Intelligent JSON/superjson detection |
|
|
63
86
|
| **⚡ Zero Config** | No schema files or code generation required |
|
|
87
|
+
| **🔒 Data Validation** | Optional runtime validation with Zod, Valibot, ArkType, etc. |
|
|
88
|
+
| **🔌 Middleware** | Interceptor chain for logging, auth, timing, and more |
|
|
89
|
+
| **⏱️ Request Timeout** | Auto-reject pending calls after a configurable deadline |
|
|
90
|
+
| **🔁 Streaming** | Return `AsyncIterable` from methods, consume with `for await` |
|
|
64
91
|
| **🚀 Transferable Objects** | Zero-copy transfers for large data (40-100x faster) |
|
|
65
92
|
|
|
66
93
|
</div>
|
|
@@ -167,11 +194,188 @@ const rpc = new RPCChannel(io, {
|
|
|
167
194
|
|
|
168
195
|
For backward compatibility, the receiving side will automatically detect the serialization format so older clients can communicate with newer servers and vice versa.
|
|
169
196
|
|
|
170
|
-
##
|
|
197
|
+
## Data Validation
|
|
171
198
|
|
|
172
|
-
|
|
199
|
+
kkrpc supports optional runtime validation of RPC inputs and outputs using any [Standard Schema](https://standardschema.dev)-compatible library (Zod, Valibot, ArkType, etc.). Validation is fully opt-in — without it, kkrpc behaves exactly as before.
|
|
200
|
+
|
|
201
|
+
There are two approaches:
|
|
202
|
+
|
|
203
|
+
### Type-first (add validators to existing code)
|
|
204
|
+
|
|
205
|
+
Define your API as usual, then add a `validators` map that mirrors the API shape:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { RPCChannel, type RPCValidators } from "kkrpc"
|
|
209
|
+
import { z } from "zod"
|
|
210
|
+
|
|
211
|
+
type MathAPI = {
|
|
212
|
+
add(a: number, b: number): Promise<number>
|
|
213
|
+
divide(a: number, b: number): Promise<number>
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const api: MathAPI = {
|
|
217
|
+
add: async (a, b) => a + b,
|
|
218
|
+
divide: async (a, b) => a / b
|
|
219
|
+
}
|
|
173
220
|
|
|
221
|
+
const validators: RPCValidators<MathAPI> = {
|
|
222
|
+
add: {
|
|
223
|
+
input: z.tuple([z.number(), z.number()]),
|
|
224
|
+
output: z.number()
|
|
225
|
+
},
|
|
226
|
+
divide: {
|
|
227
|
+
input: z.tuple([z.number(), z.number().refine((n) => n !== 0, "Divisor cannot be zero")]),
|
|
228
|
+
output: z.number()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
new RPCChannel(io, { expose: api, validators })
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Schema-first (types inferred from schemas)
|
|
236
|
+
|
|
237
|
+
Use `defineMethod` and `defineAPI` to define your API with schemas — types are inferred automatically:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { defineAPI, defineMethod, extractValidators, RPCChannel, type InferAPI } from "kkrpc"
|
|
241
|
+
import { z } from "zod"
|
|
242
|
+
|
|
243
|
+
const api = defineAPI({
|
|
244
|
+
add: defineMethod(
|
|
245
|
+
{ input: z.tuple([z.number(), z.number()]), output: z.number() },
|
|
246
|
+
async (a, b) => a + b // a, b are typed as number
|
|
247
|
+
),
|
|
248
|
+
greet: defineMethod(
|
|
249
|
+
{ input: z.tuple([z.string()]), output: z.string() },
|
|
250
|
+
async (name) => `Hello, ${name}!`
|
|
251
|
+
)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
type MyAPI = InferAPI<typeof api>
|
|
255
|
+
|
|
256
|
+
new RPCChannel(io, { expose: api, validators: extractValidators(api) })
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Validation errors
|
|
260
|
+
|
|
261
|
+
When validation fails, the caller receives an `RPCValidationError` with structured issue details:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { isRPCValidationError } from "kkrpc"
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await api.add("not", "numbers") // wrong types
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (isRPCValidationError(error)) {
|
|
270
|
+
error.phase // "input" or "output"
|
|
271
|
+
error.method // "add"
|
|
272
|
+
error.issues // [{ message: "Expected number, received string", path: [0] }]
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
174
276
|
|
|
277
|
+
Validators support nested APIs (`math.divide`), custom refinements (`.email()`, `.min(1)`, `.refine()`), and output validation. Since kkrpc is bidirectional, both sides can independently validate their own exposed API.
|
|
278
|
+
|
|
279
|
+
## Middleware / Interceptors
|
|
280
|
+
|
|
281
|
+
kkrpc supports an optional interceptor chain that wraps handler invocation on the receiving side. Interceptors use the standard onion model (like Koa or tRPC) — each interceptor calls `next()` to proceed, and can inspect args, transform return values, measure timing, or throw to abort.
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { RPCChannel, type RPCInterceptor } from "kkrpc"
|
|
285
|
+
|
|
286
|
+
const logger: RPCInterceptor = async (ctx, next) => {
|
|
287
|
+
console.log(`→ ${ctx.method}`, ctx.args)
|
|
288
|
+
const result = await next()
|
|
289
|
+
console.log(`← ${ctx.method}`, result)
|
|
290
|
+
return result
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const auth: RPCInterceptor = async (ctx, next) => {
|
|
294
|
+
if (ctx.method.startsWith("admin.")) throw new Error("Unauthorized")
|
|
295
|
+
return next()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
new RPCChannel(io, {
|
|
299
|
+
expose: api,
|
|
300
|
+
interceptors: [logger, auth]
|
|
301
|
+
})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Interceptors run **after** input validation and **before** output validation, so they always see clean, validated data. Each interceptor receives a `ctx` object with `method`, `args`, and a shared `state` bag for passing data between interceptors. When no interceptors are configured, there is zero overhead.
|
|
305
|
+
|
|
306
|
+
## Request Timeout
|
|
307
|
+
|
|
308
|
+
kkrpc supports optional request timeouts to prevent pending calls from hanging forever if the remote side crashes or the transport drops:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { isRPCTimeoutError, RPCChannel } from "kkrpc"
|
|
312
|
+
|
|
313
|
+
const rpc = new RPCChannel(io, {
|
|
314
|
+
expose: api,
|
|
315
|
+
timeout: 5000 // 5 second timeout for all outgoing calls
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
await api.slowOperation()
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (isRPCTimeoutError(error)) {
|
|
322
|
+
console.log(error.method) // "slowOperation"
|
|
323
|
+
console.log(error.timeoutMs) // 5000
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
When `destroy()` is called, all pending requests are immediately rejected with `"RPC channel destroyed"`. The default is `0` (no timeout).
|
|
329
|
+
|
|
330
|
+
## Streaming / AsyncIterable
|
|
331
|
+
|
|
332
|
+
kkrpc supports first-class streaming via `AsyncIterable`. If an RPC method returns an `AsyncIterable` (e.g. an async generator), the values are streamed chunk-by-chunk to the consumer, who can read them with `for await...of`:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
// Server: return an async generator
|
|
336
|
+
const api = {
|
|
337
|
+
async *countdown(from: number) {
|
|
338
|
+
for (let i = from; i >= 0; i--) {
|
|
339
|
+
yield i
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
async *watchFiles(path: string) {
|
|
343
|
+
const watcher = fs.watch(path)
|
|
344
|
+
try {
|
|
345
|
+
for await (const event of watcher) {
|
|
346
|
+
yield event
|
|
347
|
+
}
|
|
348
|
+
} finally {
|
|
349
|
+
watcher.close()
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
new RPCChannel(io, { expose: api })
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
// Client: consume with for-await-of
|
|
359
|
+
const api = rpc.getAPI()
|
|
360
|
+
|
|
361
|
+
const values: number[] = []
|
|
362
|
+
for await (const n of await api.countdown(5)) {
|
|
363
|
+
values.push(n)
|
|
364
|
+
}
|
|
365
|
+
// values = [5, 4, 3, 2, 1, 0]
|
|
366
|
+
|
|
367
|
+
// Consumer cancellation: break stops the producer
|
|
368
|
+
for await (const event of await api.watchFiles("/tmp")) {
|
|
369
|
+
console.log(event)
|
|
370
|
+
if (shouldStop) break // sends cancel signal to producer
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Streaming works alongside regular methods, interceptors, and validation. Producer errors propagate to the consumer, and `break` sends a cancel signal back to stop production. When `destroy()` is called, all active streams are cleaned up.
|
|
375
|
+
|
|
376
|
+
## 🚀 Quick Start
|
|
377
|
+
|
|
378
|
+
### Installation
|
|
175
379
|
|
|
176
380
|
```bash
|
|
177
381
|
# npm
|
|
@@ -275,6 +479,140 @@ console.log(await api.counter) // 42
|
|
|
275
479
|
console.log(await api.settings.theme) // "dark"
|
|
276
480
|
```
|
|
277
481
|
|
|
482
|
+
### Validation Example (Type-first)
|
|
483
|
+
|
|
484
|
+
Add runtime validation to an existing API using the `validators` option:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
// api.ts
|
|
488
|
+
import type { RPCValidators } from "kkrpc"
|
|
489
|
+
import { z } from "zod"
|
|
490
|
+
|
|
491
|
+
export type API = {
|
|
492
|
+
add(a: number, b: number): Promise<number>
|
|
493
|
+
createUser(user: {
|
|
494
|
+
name: string
|
|
495
|
+
email: string
|
|
496
|
+
}): Promise<{ id: string; name: string; email: string }>
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export const api: API = {
|
|
500
|
+
add: async (a, b) => a + b,
|
|
501
|
+
createUser: async (user) => ({ id: crypto.randomUUID(), ...user })
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export const validators: RPCValidators<API> = {
|
|
505
|
+
add: {
|
|
506
|
+
input: z.tuple([z.number(), z.number()]),
|
|
507
|
+
output: z.number()
|
|
508
|
+
},
|
|
509
|
+
createUser: {
|
|
510
|
+
input: z.tuple([z.object({ name: z.string().min(1), email: z.string().email() })]),
|
|
511
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
// server.ts
|
|
518
|
+
import { RPCChannel, WebSocketServerIO } from "kkrpc"
|
|
519
|
+
import { api, validators, type API } from "./api"
|
|
520
|
+
|
|
521
|
+
wss.on("connection", (ws) => {
|
|
522
|
+
const io = new WebSocketServerIO(ws)
|
|
523
|
+
new RPCChannel<API, API>(io, { expose: api, validators })
|
|
524
|
+
})
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
// client.ts
|
|
529
|
+
import { isRPCValidationError, RPCChannel, WebSocketClientIO } from "kkrpc"
|
|
530
|
+
import type { API } from "./api"
|
|
531
|
+
|
|
532
|
+
const io = new WebSocketClientIO({ url: "ws://localhost:3000" })
|
|
533
|
+
const rpc = new RPCChannel<{}, API>(io)
|
|
534
|
+
const api = rpc.getAPI()
|
|
535
|
+
|
|
536
|
+
// Valid calls work as usual
|
|
537
|
+
console.log(await api.add(1, 2)) // 3
|
|
538
|
+
|
|
539
|
+
// Invalid calls throw RPCValidationError
|
|
540
|
+
try {
|
|
541
|
+
await api.createUser({ name: "", email: "not-an-email" })
|
|
542
|
+
} catch (error) {
|
|
543
|
+
if (isRPCValidationError(error)) {
|
|
544
|
+
console.log(error.phase) // "input"
|
|
545
|
+
console.log(error.issues) // validation issues from Zod
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Validation Example (Schema-first)
|
|
551
|
+
|
|
552
|
+
Define your API with schemas — types are inferred automatically, no separate type definition needed:
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
// api.ts
|
|
556
|
+
import { defineAPI, defineMethod, extractValidators, type InferAPI } from "kkrpc"
|
|
557
|
+
import { z } from "zod"
|
|
558
|
+
|
|
559
|
+
export const api = defineAPI({
|
|
560
|
+
add: defineMethod(
|
|
561
|
+
{ input: z.tuple([z.number(), z.number()]), output: z.number() },
|
|
562
|
+
async (a, b) => a + b
|
|
563
|
+
),
|
|
564
|
+
greet: defineMethod(
|
|
565
|
+
{ input: z.tuple([z.string()]), output: z.string() },
|
|
566
|
+
async (name) => `Hello, ${name}!`
|
|
567
|
+
),
|
|
568
|
+
math: {
|
|
569
|
+
divide: defineMethod(
|
|
570
|
+
{
|
|
571
|
+
input: z.tuple([z.number(), z.number().refine((n) => n !== 0, "Cannot divide by zero")]),
|
|
572
|
+
output: z.number()
|
|
573
|
+
},
|
|
574
|
+
async (a, b) => a / b
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
export type API = InferAPI<typeof api>
|
|
580
|
+
export const validators = extractValidators(api)
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
// server.ts
|
|
585
|
+
import { RPCChannel, WebSocketServerIO } from "kkrpc"
|
|
586
|
+
import { api, validators } from "./api"
|
|
587
|
+
|
|
588
|
+
wss.on("connection", (ws) => {
|
|
589
|
+
const io = new WebSocketServerIO(ws)
|
|
590
|
+
new RPCChannel(io, { expose: api, validators })
|
|
591
|
+
})
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
// client.ts
|
|
596
|
+
import { isRPCValidationError, RPCChannel, WebSocketClientIO } from "kkrpc"
|
|
597
|
+
import type { API } from "./api"
|
|
598
|
+
|
|
599
|
+
const io = new WebSocketClientIO({ url: "ws://localhost:3000" })
|
|
600
|
+
const rpc = new RPCChannel<{}, API>(io)
|
|
601
|
+
const api = rpc.getAPI()
|
|
602
|
+
|
|
603
|
+
console.log(await api.greet("World")) // "Hello, World!"
|
|
604
|
+
console.log(await api.math.divide(10, 2)) // 5
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
await api.math.divide(10, 0)
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (isRPCValidationError(error)) {
|
|
610
|
+
console.log(error.method) // "math.divide"
|
|
611
|
+
console.log(error.issues[0].message) // "Cannot divide by zero"
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
278
616
|
### Enhanced Error Preservation
|
|
279
617
|
|
|
280
618
|
kkrpc preserves complete error information across RPC boundaries:
|
|
@@ -1250,6 +1588,92 @@ const rpc = new RPCChannel<WorkerAPI, {}>(io, {
|
|
|
1250
1588
|
- **Multiple channels**: Can create multiple relays on different IPC channels
|
|
1251
1589
|
- **Composable**: Can chain relays through multiple processes
|
|
1252
1590
|
|
|
1591
|
+
## 📊 Benchmarks
|
|
1592
|
+
|
|
1593
|
+
kkrpc includes comprehensive benchmarks to measure throughput and data transfer performance across different transports and runtimes.
|
|
1594
|
+
|
|
1595
|
+
### Running Benchmarks
|
|
1596
|
+
|
|
1597
|
+
```bash
|
|
1598
|
+
# Run all benchmarks
|
|
1599
|
+
bun test __tests__/stdio-benchmark.test.ts
|
|
1600
|
+
bun test __tests__/websocket-benchmark.test.ts
|
|
1601
|
+
bun test __tests__/stdio-large-data-benchmark.test.ts
|
|
1602
|
+
bun test __tests__/websocket-large-data-benchmark.test.ts
|
|
1603
|
+
|
|
1604
|
+
# Or run all tests including benchmarks
|
|
1605
|
+
bun test
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
### Benchmark Design
|
|
1609
|
+
|
|
1610
|
+
The benchmarks are designed to measure two key aspects of RPC performance:
|
|
1611
|
+
|
|
1612
|
+
1. **Call Throughput** (`stdio-benchmark.test.ts`, `websocket-benchmark.test.ts`)
|
|
1613
|
+
- **Sequential Operations**: Measures latency per call when making blocking calls one after another
|
|
1614
|
+
- **Concurrent Operations**: Measures throughput when making many calls in parallel using `Promise.all`
|
|
1615
|
+
- **Batch Operations**: Tests batching multiple operations into a single RPC call
|
|
1616
|
+
- **Latency Distribution**: Pings the server 1,000 times to calculate min/avg/p99/max latency
|
|
1617
|
+
|
|
1618
|
+
2. **Data Transfer Throughput** (`stdio-large-data-benchmark.test.ts`, `websocket-large-data-benchmark.test.ts`)
|
|
1619
|
+
- **Upload**: Client sends large data payloads to the server
|
|
1620
|
+
- **Download**: Server generates and sends large data to the client
|
|
1621
|
+
- **Echo**: Bidirectional transfer (client sends, server echoes back)
|
|
1622
|
+
- Tests various payload sizes: 1KB, 10KB, 100KB, 1MB, 10MB
|
|
1623
|
+
|
|
1624
|
+
### Benchmark Results
|
|
1625
|
+
|
|
1626
|
+
Results from running on a MacBook Pro (Apple Silicon):
|
|
1627
|
+
|
|
1628
|
+
#### Stdio Adapter (Process-to-Process)
|
|
1629
|
+
|
|
1630
|
+
| Runtime | Operation | Calls/sec | Latency (avg) |
|
|
1631
|
+
| ----------- | --------------- | ----------------- | ------------- |
|
|
1632
|
+
| **Bun** | Sequential Echo | 22,234 | 0.046ms |
|
|
1633
|
+
| **Bun** | Concurrent Echo | 151,069 | - |
|
|
1634
|
+
| **Bun** | Batch (100 ops) | 453,042 effective | - |
|
|
1635
|
+
| **Node.js** | Sequential Echo | 23,985 | 0.038ms |
|
|
1636
|
+
| **Node.js** | Concurrent Echo | 145,516 | - |
|
|
1637
|
+
| **Deno** | Sequential Echo | 20,028 | 0.047ms |
|
|
1638
|
+
| **Deno** | Concurrent Echo | 123,079 | - |
|
|
1639
|
+
|
|
1640
|
+
#### Stdio Large Data Transfer
|
|
1641
|
+
|
|
1642
|
+
| Runtime | Operation | 1MB Payload | 10MB Payload |
|
|
1643
|
+
| ----------- | ------------ | ----------- | ------------ |
|
|
1644
|
+
| **Bun** | Upload | ~1,010 MB/s | ~658 MB/s |
|
|
1645
|
+
| **Bun** | Download | ~134 MB/s | ~132 MB/s |
|
|
1646
|
+
| **Bun** | Echo (100KB) | ~1,132 MB/s | - |
|
|
1647
|
+
| **Node.js** | Upload | ~382 MB/s | ~92 MB/s |
|
|
1648
|
+
| **Node.js** | Download | ~75 MB/s | ~30 MB/s |
|
|
1649
|
+
| **Deno** | Upload | ~358 MB/s | ~91 MB/s |
|
|
1650
|
+
| **Deno** | Download | ~74 MB/s | ~33 MB/s |
|
|
1651
|
+
|
|
1652
|
+
#### WebSocket Adapter
|
|
1653
|
+
|
|
1654
|
+
| Operation | Calls/sec | Latency (avg) |
|
|
1655
|
+
| --------------- | ----------------- | ------------- |
|
|
1656
|
+
| Sequential Echo | 22,314 | 0.040ms |
|
|
1657
|
+
| Concurrent Echo | 74,954 | - |
|
|
1658
|
+
| Batch (100 ops) | 483,318 effective | - |
|
|
1659
|
+
|
|
1660
|
+
#### WebSocket Large Data Transfer
|
|
1661
|
+
|
|
1662
|
+
| Operation | 1MB Payload | 10MB Payload |
|
|
1663
|
+
| ------------ | ----------- | ------------ |
|
|
1664
|
+
| Upload | ~577 MB/s | ~927 MB/s |
|
|
1665
|
+
| Download | ~137 MB/s | ~149 MB/s |
|
|
1666
|
+
| Echo (100KB) | ~799 MB/s | - |
|
|
1667
|
+
|
|
1668
|
+
### Key Findings
|
|
1669
|
+
|
|
1670
|
+
- **Bun** consistently outperforms Node.js and Deno for stdio communication, especially for large data transfers
|
|
1671
|
+
- **Stdio** is significantly faster than WebSocket for local process communication (2-3x higher throughput)
|
|
1672
|
+
- **Concurrent operations** achieve 6-7x higher throughput than sequential operations
|
|
1673
|
+
- **Batching** is highly effective - 100 operations per batch achieves 400K+ effective calls/sec
|
|
1674
|
+
- **Upload** is faster than download due to JSON serialization overhead on the response path
|
|
1675
|
+
- **WebSocket** performance is excellent for network communication, nearly matching stdio for small payloads
|
|
1676
|
+
|
|
1253
1677
|
## 🆚 Comparison with Alternatives
|
|
1254
1678
|
|
|
1255
1679
|
<div align="center">
|
|
@@ -1264,6 +1688,10 @@ const rpc = new RPCChannel<WorkerAPI, {}>(io, {
|
|
|
1264
1688
|
| **Property Access** | ✅ Remote getters/setters | ❌ Methods only | ❌ Methods only |
|
|
1265
1689
|
| **Zero Config** | ✅ No code generation | ✅ No code generation | ✅ No code generation |
|
|
1266
1690
|
| **Callbacks** | ✅ Function parameters | ❌ No callbacks | ✅ Function parameters |
|
|
1691
|
+
| **Data Validation** | ✅ Optional, any Standard Schema library | ✅ Built-in Zod support | ❌ Not supported |
|
|
1692
|
+
| **Middleware** | ✅ Interceptor chain (onion model) | ✅ `.use()` middleware | ❌ Not supported |
|
|
1693
|
+
| **Request Timeout** | ✅ Per-channel timeout + destroy cleanup | ❌ Not built-in | ❌ Not supported |
|
|
1694
|
+
| **Streaming** | ✅ AsyncIterable with cancel + error propagation | ✅ SSE subscriptions | ❌ Not supported |
|
|
1267
1695
|
| **Transferable Objects** | ✅ Zero-copy transfers (40-100x faster) | ❌ Not supported | ✅ Basic support |
|
|
1268
1696
|
|
|
1269
1697
|
</div>
|