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.
Files changed (110) hide show
  1. package/README.md +430 -2
  2. package/README.zh.md +1650 -0
  3. package/dist/browser-mod.cjs +1 -1
  4. package/dist/browser-mod.d.cts +4 -4
  5. package/dist/browser-mod.d.ts +4 -4
  6. package/dist/browser-mod.js +1 -1
  7. package/dist/browser-mod.js.map +1 -1
  8. package/dist/channel-B-xw0r_-.cjs +6 -0
  9. package/dist/channel-C4KXiIq_.d.cts +519 -0
  10. package/dist/channel-C4KXiIq_.d.cts.map +1 -0
  11. package/dist/channel-CLazgI0u.d.ts +519 -0
  12. package/dist/channel-CLazgI0u.d.ts.map +1 -0
  13. package/dist/channel-Dl81iz1-.js +7 -0
  14. package/dist/channel-Dl81iz1-.js.map +1 -0
  15. package/dist/chrome-extension.cjs +1 -1
  16. package/dist/chrome-extension.d.cts +3 -3
  17. package/dist/chrome-extension.d.ts +3 -3
  18. package/dist/chrome-extension.js +1 -1
  19. package/dist/chrome-extension.js.map +1 -1
  20. package/dist/{deno-DS3TcO_u.d.ts → deno-DLEehrMZ.d.ts} +2 -2
  21. package/dist/{deno-DS3TcO_u.d.ts.map → deno-DLEehrMZ.d.ts.map} +1 -1
  22. package/dist/{deno-BNN8LLrd.d.cts → deno-X3dHX6ii.d.cts} +2 -2
  23. package/dist/{deno-BNN8LLrd.d.cts.map → deno-X3dHX6ii.d.cts.map} +1 -1
  24. package/dist/deno-mod.cjs +1 -1
  25. package/dist/deno-mod.d.cts +4 -4
  26. package/dist/deno-mod.d.ts +4 -4
  27. package/dist/deno-mod.js +1 -1
  28. package/dist/electron-ipc.cjs +1 -1
  29. package/dist/electron-ipc.d.cts +3 -3
  30. package/dist/electron-ipc.d.ts +3 -3
  31. package/dist/electron-ipc.js +1 -1
  32. package/dist/electron-ipc.js.map +1 -1
  33. package/dist/electron.cjs +1 -1
  34. package/dist/electron.d.cts +3 -3
  35. package/dist/electron.d.cts.map +1 -1
  36. package/dist/electron.d.ts +3 -3
  37. package/dist/electron.d.ts.map +1 -1
  38. package/dist/electron.js +1 -1
  39. package/dist/electron.js.map +1 -1
  40. package/dist/{http-DWq6Ez_h.d.cts → http-BXn39Czz.d.cts} +2 -2
  41. package/dist/{http-DWq6Ez_h.d.cts.map → http-BXn39Czz.d.cts.map} +1 -1
  42. package/dist/{http-CcLOuQmc.d.ts → http-DMg5ci8E.d.ts} +2 -2
  43. package/dist/{http-CcLOuQmc.d.ts.map → http-DMg5ci8E.d.ts.map} +1 -1
  44. package/dist/http.cjs +1 -1
  45. package/dist/http.d.cts +3 -3
  46. package/dist/http.d.ts +3 -3
  47. package/dist/http.js +1 -1
  48. package/dist/{interface-DRqrAKo-.d.ts → interface-COwEog13.d.cts} +2 -2
  49. package/dist/interface-COwEog13.d.cts.map +1 -0
  50. package/dist/{interface-DPtHJBBS.d.cts → interface-Cdp6K7Dz.d.ts} +2 -2
  51. package/dist/interface-Cdp6K7Dz.d.ts.map +1 -0
  52. package/dist/{kafka-_Fcc7erL.d.cts → kafka-CEnBN-7e.d.cts} +2 -2
  53. package/dist/{kafka-_Fcc7erL.d.cts.map → kafka-CEnBN-7e.d.cts.map} +1 -1
  54. package/dist/{kafka-BMOHF0lZ.d.ts → kafka-j0MF9lik.d.ts} +2 -2
  55. package/dist/{kafka-BMOHF0lZ.d.ts.map → kafka-j0MF9lik.d.ts.map} +1 -1
  56. package/dist/kafka.d.cts +2 -2
  57. package/dist/kafka.d.ts +2 -2
  58. package/dist/mod.cjs +2 -2
  59. package/dist/mod.d.cts +10 -59
  60. package/dist/mod.d.cts.map +1 -1
  61. package/dist/mod.d.ts +10 -59
  62. package/dist/mod.d.ts.map +1 -1
  63. package/dist/mod.js +2 -2
  64. package/dist/mod.js.map +1 -1
  65. package/dist/{nats-D3XOZAGl.d.ts → nats-C1aVwb8x.d.ts} +2 -2
  66. package/dist/{nats-D3XOZAGl.d.ts.map → nats-C1aVwb8x.d.ts.map} +1 -1
  67. package/dist/{nats-Bkr44tQB.d.cts → nats-c0XYE-Ao.d.cts} +2 -2
  68. package/dist/{nats-Bkr44tQB.d.cts.map → nats-c0XYE-Ao.d.cts.map} +1 -1
  69. package/dist/nats.d.cts +2 -2
  70. package/dist/nats.d.ts +2 -2
  71. package/dist/{rabbitmq-CpeO6XdQ.d.ts → rabbitmq-DWEE5gve.d.ts} +2 -2
  72. package/dist/{rabbitmq-CpeO6XdQ.d.ts.map → rabbitmq-DWEE5gve.d.ts.map} +1 -1
  73. package/dist/{rabbitmq-DAUXsuvL.d.cts → rabbitmq-OGWQJkUQ.d.cts} +2 -2
  74. package/dist/{rabbitmq-DAUXsuvL.d.cts.map → rabbitmq-OGWQJkUQ.d.cts.map} +1 -1
  75. package/dist/rabbitmq.cjs +1 -1
  76. package/dist/rabbitmq.d.cts +4 -4
  77. package/dist/rabbitmq.d.ts +4 -4
  78. package/dist/rabbitmq.js +1 -1
  79. package/dist/{redis-streams-DpbNc20y.d.cts → redis-streams-DeCCm9lZ.d.cts} +2 -2
  80. package/dist/{redis-streams-DpbNc20y.d.cts.map → redis-streams-DeCCm9lZ.d.cts.map} +1 -1
  81. package/dist/{redis-streams-avvO_U2r.d.ts → redis-streams-RKCo8Gip.d.ts} +2 -2
  82. package/dist/{redis-streams-avvO_U2r.d.ts.map → redis-streams-RKCo8Gip.d.ts.map} +1 -1
  83. package/dist/redis-streams.d.cts +2 -2
  84. package/dist/redis-streams.d.ts +2 -2
  85. package/dist/socketio.d.cts +1 -1
  86. package/dist/socketio.d.ts +1 -1
  87. package/dist/tauri-BAtWibFE.js +2 -0
  88. package/dist/tauri-BAtWibFE.js.map +1 -0
  89. package/dist/tauri-Ble8sg9S.d.cts +131 -0
  90. package/dist/tauri-Ble8sg9S.d.cts.map +1 -0
  91. package/dist/tauri-CSGFwEY6.d.ts +131 -0
  92. package/dist/tauri-CSGFwEY6.d.ts.map +1 -0
  93. package/dist/tauri-DRz52iou.cjs +1 -0
  94. package/package.json +23 -20
  95. package/dist/channel-ChGarSnI.d.cts +0 -154
  96. package/dist/channel-ChGarSnI.d.cts.map +0 -1
  97. package/dist/channel-K9w_2vmv.d.ts +0 -154
  98. package/dist/channel-K9w_2vmv.d.ts.map +0 -1
  99. package/dist/channel-LGw9tl8f.js +0 -7
  100. package/dist/channel-LGw9tl8f.js.map +0 -1
  101. package/dist/channel-OGNDLfsd.cjs +0 -6
  102. package/dist/interface-DPtHJBBS.d.cts.map +0 -1
  103. package/dist/interface-DRqrAKo-.d.ts.map +0 -1
  104. package/dist/tauri-CSoj53la.js +0 -2
  105. package/dist/tauri-CSoj53la.js.map +0 -1
  106. package/dist/tauri-DIJzjZwG.d.ts +0 -59
  107. package/dist/tauri-DIJzjZwG.d.ts.map +0 -1
  108. package/dist/tauri-DvS2Czwp.cjs +0 -1
  109. package/dist/tauri-d0yDsUnV.d.cts +0 -59
  110. 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
+ [![kkrpc Tutorial](https://img.youtube.com/vi/CF8lji8eB30/0.jpg)](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
- ## 🚀 Quick Start
197
+ ## Data Validation
171
198
 
172
- ### Installation
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>