electron-json-rpc 1.0.0

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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +978 -0
  3. package/README.zh-CN.md +978 -0
  4. package/dist/debug.d.mts +92 -0
  5. package/dist/debug.d.mts.map +1 -0
  6. package/dist/debug.mjs +206 -0
  7. package/dist/debug.mjs.map +1 -0
  8. package/dist/error-xVRu7Lxq.mjs +131 -0
  9. package/dist/error-xVRu7Lxq.mjs.map +1 -0
  10. package/dist/event.d.mts +71 -0
  11. package/dist/event.d.mts.map +1 -0
  12. package/dist/event.mjs +60 -0
  13. package/dist/event.mjs.map +1 -0
  14. package/dist/index.d.mts +78 -0
  15. package/dist/index.d.mts.map +1 -0
  16. package/dist/index.mjs +4 -0
  17. package/dist/internal-BZK_0O3n.mjs +23 -0
  18. package/dist/internal-BZK_0O3n.mjs.map +1 -0
  19. package/dist/main.d.mts +151 -0
  20. package/dist/main.d.mts.map +1 -0
  21. package/dist/main.mjs +329 -0
  22. package/dist/main.mjs.map +1 -0
  23. package/dist/preload.d.mts +23 -0
  24. package/dist/preload.d.mts.map +1 -0
  25. package/dist/preload.mjs +417 -0
  26. package/dist/preload.mjs.map +1 -0
  27. package/dist/renderer/builder.d.mts +64 -0
  28. package/dist/renderer/builder.d.mts.map +1 -0
  29. package/dist/renderer/builder.mjs +101 -0
  30. package/dist/renderer/builder.mjs.map +1 -0
  31. package/dist/renderer/client.d.mts +42 -0
  32. package/dist/renderer/client.d.mts.map +1 -0
  33. package/dist/renderer/client.mjs +136 -0
  34. package/dist/renderer/client.mjs.map +1 -0
  35. package/dist/renderer/event.d.mts +17 -0
  36. package/dist/renderer/event.d.mts.map +1 -0
  37. package/dist/renderer/event.mjs +117 -0
  38. package/dist/renderer/event.mjs.map +1 -0
  39. package/dist/renderer/index.d.mts +6 -0
  40. package/dist/renderer/index.mjs +6 -0
  41. package/dist/stream.d.mts +38 -0
  42. package/dist/stream.d.mts.map +1 -0
  43. package/dist/stream.mjs +80 -0
  44. package/dist/stream.mjs.map +1 -0
  45. package/dist/types-BnGse9DF.d.mts +201 -0
  46. package/dist/types-BnGse9DF.d.mts.map +1 -0
  47. package/package.json +92 -0
package/README.md ADDED
@@ -0,0 +1,978 @@
1
+ # electron-json-rpc
2
+
3
+ [English](./README.md) | [简体中文](./README.zh-CN.md)
4
+
5
+ A type-safe IPC library for Electron built on the JSON-RPC 2.0 protocol. Define your API once, get full type inference across main process, preload script, and renderer process. Supports streaming, event bus, queued requests with retry, and validation.
6
+
7
+ > **Status**: This project is currently in **beta**. The API is subject to change. Feedback and contributions are welcome!
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ bun add electron-json-rpc
13
+ # or
14
+ npm install electron-json-rpc
15
+ ```
16
+
17
+ ## Features
18
+
19
+ - **JSON-RPC 2.0 compliant** - Standard request/response protocol
20
+ - **Type-safe** - Full TypeScript support with typed method definitions
21
+ - **Event Bus** - Built-in publish-subscribe pattern for real-time events
22
+ - **Validation** - Generic validator interface compatible with any validation library
23
+ - **Streaming** - Web standard `ReadableStream` support for server-sent streams
24
+ - **Notifications** - One-way calls without response
25
+ - **Timeout handling** - Configurable timeout for RPC calls
26
+ - **Batch requests** - Support for multiple requests in a single call
27
+
28
+ ## Quick Start
29
+
30
+ ### Main Process
31
+
32
+ ```typescript
33
+ import { app, BrowserWindow } from "electron";
34
+ import { RpcServer } from "electron-json-rpc/main";
35
+
36
+ const rpc = new RpcServer();
37
+
38
+ // Register methods
39
+ rpc.register("add", (a: number, b: number) => a + b);
40
+ rpc.register("greet", async (name: string) => {
41
+ return `Hello, ${name}!`;
42
+ });
43
+
44
+ // Start listening
45
+ rpc.listen();
46
+ ```
47
+
48
+ ### Preload Script
49
+
50
+ Two modes are supported: **Auto Proxy Mode** (recommended) and **Whitelist Mode**.
51
+
52
+ #### Auto Proxy Mode (Recommended)
53
+
54
+ No need to define method names - call any method registered in the main process directly:
55
+
56
+ ```typescript
57
+ import { exposeRpcApi } from "electron-json-rpc/preload";
58
+ import { contextBridge, ipcRenderer } from "electron";
59
+
60
+ exposeRpcApi({
61
+ contextBridge,
62
+ ipcRenderer,
63
+ });
64
+ ```
65
+
66
+ In the renderer process, you can call any method directly:
67
+
68
+ ```typescript
69
+ // Call methods directly without pre-definition
70
+ const sum = await window.rpc.add(1, 2);
71
+ const greeting = await window.rpc.greet("World");
72
+
73
+ // Or use the generic call method
74
+ const result = await window.rpc.call("methodName", arg1, arg2);
75
+
76
+ // Send a notification (no response)
77
+ window.rpc.log("Hello from renderer!");
78
+ ```
79
+
80
+ #### Whitelist Mode (More Secure)
81
+
82
+ Only expose specific methods:
83
+
84
+ ```typescript
85
+ import { exposeRpcApi } from "electron-json-rpc/preload";
86
+ import { contextBridge, ipcRenderer } from "electron";
87
+
88
+ exposeRpcApi({
89
+ contextBridge,
90
+ ipcRenderer,
91
+ methods: ["add", "greet"], // Only expose these methods
92
+ });
93
+ ```
94
+
95
+ In the renderer process:
96
+
97
+ ```typescript
98
+ // Whitelisted methods can be called directly
99
+ const sum = await window.rpc.add(1, 2);
100
+ const greeting = await window.rpc.greet("World");
101
+
102
+ // Non-whitelisted methods require the call method
103
+ const result = await window.rpc.call("otherMethod", arg1, arg2);
104
+ ```
105
+
106
+ ### Renderer Process
107
+
108
+ ```typescript
109
+ import { createRpcClient } from "electron-json-rpc/renderer";
110
+
111
+ const rpc = createRpcClient();
112
+
113
+ // Call a method
114
+ const result = await rpc.call("add", 1, 2);
115
+ console.log(result); // 3
116
+
117
+ // Send a notification (no response)
118
+ rpc.notify("log", "Hello from renderer!");
119
+ ```
120
+
121
+ ## Communication Modes
122
+
123
+ This library supports three main communication modes. Choose based on your security and type-safety requirements:
124
+
125
+ | Mode | Preload Definition | Renderer Usage | Security | Type-Safe | Use Case |
126
+ | ---------------- | ---------------------------------------------------------------- | ----------------------------------- | --------------------------- | --------: | ---------------------- |
127
+ | **Auto Proxy** | `exposeRpcApi({ contextBridge, ipcRenderer })` | `await window.rpc.anyMethod()` | ⚠️ Any method callable | No | Quick prototyping |
128
+ | **Whitelist** | `exposeRpcApi({ contextBridge, ipcRenderer, methods: ["add"] })` | `await window.rpc.add()` | ✅ Only whitelisted methods | No | Production recommended |
129
+ | **Typed Client** | Any mode | `const api = defineRpcApi<MyApi>()` | ✅ Depends on preload | Yes | Large projects |
130
+
131
+ ### Auto Proxy Mode (Simplest)
132
+
133
+ ```typescript
134
+ // Preload - no method definitions needed
135
+ exposeRpcApi({ contextBridge, ipcRenderer });
136
+
137
+ // Renderer - call any method directly
138
+ await window.rpc.add(1, 2);
139
+ ```
140
+
141
+ ### Whitelist Mode (Production Recommended)
142
+
143
+ ```typescript
144
+ // Preload - only expose specified methods
145
+ exposeRpcApi({ contextBridge, ipcRenderer, methods: ["add", "greet"] });
146
+
147
+ // Renderer - whitelisted methods called directly
148
+ await window.rpc.add(1, 2);
149
+ ```
150
+
151
+ ### Typed Client (Full Type Safety)
152
+
153
+ ```typescript
154
+ // Renderer - define interface for full type safety
155
+ const api = defineRpcApi<{
156
+ add: (a: number, b: number) => number;
157
+ log: (msg: string) => void;
158
+ }>();
159
+
160
+ await api.add(1, 2); // Fully typed!
161
+ ```
162
+
163
+ ## Typed API
164
+
165
+ Define your API interface for full type safety:
166
+
167
+ ```typescript
168
+ // Define your API types
169
+ type AppApi = {
170
+ add: (a: number, b: number) => number;
171
+ greet: (name: string) => Promise<string>;
172
+ log: (message: string) => void; // notification
173
+ };
174
+
175
+ // Create typed client
176
+ import { createTypedRpcClient } from "electron-json-rpc/renderer";
177
+
178
+ const rpc = createTypedRpcClient<AppApi>();
179
+
180
+ // Fully typed!
181
+ const sum = await rpc.add(1, 2);
182
+ const greeting = await rpc.greet("World");
183
+ rpc.log("This is a notification");
184
+ ```
185
+
186
+ ### Interface-Style API (defineRpcApi)
187
+
188
+ For a more semantic API definition, use `defineRpcApi`:
189
+
190
+ ```typescript
191
+ import { defineRpcApi } from "electron-json-rpc/renderer";
192
+
193
+ // Define your API interface
194
+ interface AppApi {
195
+ getUserList(): Promise<{ id: string; name: string }[]>;
196
+ getUser(id: string): Promise<{ id: string; name: string }>;
197
+ deleteUser(id: string): Promise<void>;
198
+ log(message: string): void; // notification
199
+ dataStream(count: number): ReadableStream<number>;
200
+ }
201
+
202
+ const api = defineRpcApi<AppApi>({ timeout: 10000 });
203
+
204
+ // Fully typed usage
205
+ const users = await api.getUserList();
206
+ const user = await api.getUser("123");
207
+ await api.deleteUser("123");
208
+ api.log("Done"); // notification (void return)
209
+
210
+ // Stream support
211
+ for await (const n of api.dataStream(10)) {
212
+ console.log(n);
213
+ }
214
+ ```
215
+
216
+ ### Builder Pattern (createRpc)
217
+
218
+ Build your API using a fluent builder pattern with type inference:
219
+
220
+ ```typescript
221
+ import { createRpc } from "electron-json-rpc/renderer";
222
+
223
+ interface User {
224
+ id: string;
225
+ name: string;
226
+ }
227
+
228
+ const api = createRpc({ timeout: 10000 })
229
+ .add("getUserList", () => Promise<User[]>())
230
+ .add("getUser", (id: string) => Promise<User>())
231
+ .add("deleteUser", (id: string) => Promise<void>())
232
+ .add("log", (message: string) => {}) // void return = notification
233
+ .stream("dataStream", (count: number) => new ReadableStream<number>())
234
+ .build();
235
+
236
+ // Fully typed usage - types inferred from handler signatures
237
+ const users = await api.getUserList();
238
+ const user = await api.getUser("123");
239
+ await api.deleteUser("123");
240
+ api.log("Done"); // notification (void)
241
+
242
+ // Stream support
243
+ for await (const n of api.dataStream(10)) {
244
+ console.log(n);
245
+ }
246
+ ```
247
+
248
+ ## Request Queue
249
+
250
+ For applications that need to handle unreliable connections or busy main processes, the queued RPC client provides automatic request queuing with retry logic.
251
+
252
+ ### Basic Usage
253
+
254
+ ```typescript
255
+ import { createQueuedRpcClient } from "electron-json-rpc/renderer";
256
+
257
+ const rpc = createQueuedRpcClient({
258
+ maxSize: 50, // Maximum queue size
259
+ fullBehavior: "evictOldest", // What to do when queue is full
260
+ timeout: 10000, // Request timeout in ms
261
+ });
262
+
263
+ // Call a method - will be queued if main process is busy
264
+ const result = await rpc.call("getData", id);
265
+
266
+ // Send a notification (not queued - fire and forget)
267
+ rpc.notify("log", "Hello from renderer!");
268
+
269
+ // Get queue status
270
+ console.log(rpc.getQueueStatus());
271
+ // { pending: 2, active: 1, maxSize: 50, isPaused: false, isConnected: true }
272
+ ```
273
+
274
+ ### Queue Configuration
275
+
276
+ ```typescript
277
+ const rpc = createQueuedRpcClient({
278
+ // Queue size settings
279
+ maxSize: 100,
280
+ fullBehavior: "reject", // 'reject' | 'evict' | 'evictOldest'
281
+
282
+ // Retry settings
283
+ retry: {
284
+ maxAttempts: 3, // Maximum retry attempts
285
+ backoff: "exponential", // 'fixed' | 'exponential'
286
+ initialDelay: 1000, // Initial delay in ms
287
+ maxDelay: 10000, // Maximum delay in ms
288
+ },
289
+
290
+ // Connection health settings
291
+ healthCheck: true, // Enable connection health check
292
+ healthCheckInterval: 5000, // Health check interval in ms
293
+ });
294
+ ```
295
+
296
+ ### Queue Full Behavior
297
+
298
+ When the queue reaches maximum size:
299
+
300
+ - **`"reject"`** (default): Throw `RpcQueueFullError` for new requests
301
+ - **`"evict"`**: Evict the current request being added
302
+ - **`"evictOldest"`**: Remove the oldest request from the queue
303
+
304
+ ```typescript
305
+ // Reject mode (default)
306
+ const rpc = createQueuedRpcClient({
307
+ maxSize: 10,
308
+ fullBehavior: "reject",
309
+ });
310
+
311
+ try {
312
+ await rpc.call("someMethod");
313
+ } catch (error) {
314
+ if (error.name === "RpcQueueFullError") {
315
+ console.log("Queue is full!");
316
+ }
317
+ }
318
+ ```
319
+
320
+ ### Queue Control Methods
321
+
322
+ ```typescript
323
+ const rpc = createQueuedRpcClient();
324
+
325
+ // Check if queue is healthy (connected and not paused)
326
+ if (rpc.isQueueHealthy()) {
327
+ await rpc.call("someMethod");
328
+ }
329
+
330
+ // Get detailed queue status
331
+ const status = rpc.getQueueStatus();
332
+ console.log(`Pending: ${status.pending}, Active: ${status.active}`);
333
+
334
+ // Pause queue processing (incoming requests will queue)
335
+ rpc.pauseQueue();
336
+
337
+ // Resume queue processing
338
+ rpc.resumeQueue();
339
+
340
+ // Clear all pending requests
341
+ rpc.clearQueue();
342
+ ```
343
+
344
+ ### Retry Strategy
345
+
346
+ The queue automatically retries failed requests based on the configured strategy:
347
+
348
+ ```typescript
349
+ const rpc = createQueuedRpcClient({
350
+ retry: {
351
+ maxAttempts: 3,
352
+ backoff: "exponential", // or "fixed"
353
+ initialDelay: 1000,
354
+ maxDelay: 10000,
355
+ },
356
+ });
357
+
358
+ // Exponential backoff: 1000ms, 2000ms, 4000ms, ...
359
+ // Fixed backoff: 1000ms, 1000ms, 1000ms, ...
360
+ ```
361
+
362
+ Requests are retried on:
363
+
364
+ - Timeout errors (`RpcTimeoutError`)
365
+ - Connection errors (`RpcConnectionError`)
366
+
367
+ ### Error Handling
368
+
369
+ ```typescript
370
+ import {
371
+ RpcQueueFullError,
372
+ RpcConnectionError,
373
+ RpcQueueEvictedError,
374
+ isQueueFullError,
375
+ isConnectionError,
376
+ isQueueEvictedError,
377
+ } from "electron-json-rpc/renderer";
378
+
379
+ const rpc = createQueuedRpcClient();
380
+
381
+ try {
382
+ await rpc.call("someMethod");
383
+ } catch (error) {
384
+ if (isQueueFullError(error)) {
385
+ console.log(`Queue full: ${error.currentSize}/${error.maxSize}`);
386
+ } else if (isConnectionError(error)) {
387
+ console.log(`Connection lost: ${error.message}`);
388
+ } else if (isQueueEvictedError(error)) {
389
+ console.log(`Request evicted: ${error.reason}`);
390
+ }
391
+ }
392
+ ```
393
+
394
+ ## Debug Logging
395
+
396
+ The renderer client provides built-in debug logging for monitoring RPC requests and responses. This is useful for development and troubleshooting.
397
+
398
+ ### Global Debug Mode
399
+
400
+ Enable debug logging for all RPC clients globally:
401
+
402
+ ```typescript
403
+ import { setRpcDebug, isRpcDebug } from "electron-json-rpc/renderer";
404
+
405
+ // Enable debug logging for all clients
406
+ setRpcDebug(true);
407
+
408
+ // Check if debug is enabled
409
+ if (isRpcDebug()) {
410
+ console.log("Debug mode is active");
411
+ }
412
+
413
+ // Disable debug logging
414
+ setRpcDebug(false);
415
+ ```
416
+
417
+ ### Per-Client Debug Option
418
+
419
+ Enable debug logging for a specific client:
420
+
421
+ ```typescript
422
+ import { createRpcClient } from "electron-json-rpc/renderer";
423
+
424
+ const rpc = createRpcClient({
425
+ debug: true, // Enable debug logging for this client
426
+ timeout: 10000,
427
+ });
428
+
429
+ // Works with all client types
430
+ const api = createTypedRpcClient<MyApi>({ debug: true });
431
+ const events = createEventBus<AppEvents>({ debug: true });
432
+ ```
433
+
434
+ ### Custom Logger
435
+
436
+ Provide a custom logger function for full control over debug output:
437
+
438
+ ```typescript
439
+ import { createRpcClient, type RpcLogger } from "electron-json-rpc/renderer";
440
+
441
+ const customLogger: RpcLogger = (entry) => {
442
+ const { type, method, params, result, error, duration, requestId } = entry;
443
+
444
+ console.log(`[RPC ${type.toUpperCase()}] ${method}`, {
445
+ params,
446
+ result,
447
+ error,
448
+ duration,
449
+ requestId,
450
+ });
451
+ };
452
+
453
+ const rpc = createRpcClient({
454
+ logger: customLogger,
455
+ });
456
+ ```
457
+
458
+ ### Default Logger Output
459
+
460
+ When using the default logger (with `debug: true`), you'll see formatted console output:
461
+
462
+ ```
463
+ [RPC] 10:30:15.123 → request add [#1]
464
+ params: [1, 2]
465
+
466
+ [RPC] 10:30:15.145 ← response add [#1] (22ms)
467
+ params: [1, 2]
468
+ result: 3
469
+
470
+ [RPC] 10:30:16.234 → notify log
471
+ params: ['Hello']
472
+
473
+ [RPC] 10:30:17.456 → request getUser [#2]
474
+ params: ['123']
475
+
476
+ [RPC] 10:30:17.567 ← error getUser [#2] (111ms)
477
+ params: ['123']
478
+ error: Method not found
479
+ ```
480
+
481
+ ### Log Entry Types
482
+
483
+ The logger receives entries with the following types:
484
+
485
+ - **`request`** - Outgoing RPC request
486
+ - **`response`** - Successful RPC response
487
+ - **`error`** - Failed RPC request
488
+ - **`notify`** - One-way notification
489
+ - **`stream`** - Stream request
490
+ - **`event`** - Event bus event received
491
+
492
+ ## Event Bus
493
+
494
+ The event bus enables real-time communication from the main process to renderer processes using a publish-subscribe pattern.
495
+
496
+ ### Main Process
497
+
498
+ ```typescript
499
+ import { RpcServer } from "electron-json-rpc/main";
500
+
501
+ const rpc = new RpcServer();
502
+ rpc.listen();
503
+
504
+ // Publish events to all subscribed renderers
505
+ rpc.publish("user-updated", { id: "123", name: "John" });
506
+ rpc.publish("data-changed", { items: [1, 2, 3] });
507
+
508
+ // Check subscriber counts
509
+ console.log(rpc.getEventSubscribers());
510
+ // { "user-updated": 2, "data-changed": 1 }
511
+ ```
512
+
513
+ ### Renderer Process
514
+
515
+ ```typescript
516
+ // Using the exposed API (via preload)
517
+ const unsubscribe = window.rpc.on("user-updated", (data) => {
518
+ console.log("User updated:", data);
519
+ });
520
+
521
+ // Unsubscribe using the returned function
522
+ unsubscribe();
523
+
524
+ // Or unsubscribe manually
525
+ window.rpc.off("user-updated");
526
+
527
+ // Subscribe once (auto-unsubscribe after first event)
528
+ window.rpc.once("notification", (data) => {
529
+ console.log("Got notification:", data);
530
+ });
531
+ ```
532
+
533
+ ### Typed Event Bus
534
+
535
+ For full type safety, use `createEventBus` with event definitions:
536
+
537
+ ```typescript
538
+ import { createEventBus } from "electron-json-rpc/renderer";
539
+
540
+ // Define your events
541
+ interface AppEvents {
542
+ "user-updated": { id: string; name: string };
543
+ "data-changed": { items: number[] };
544
+ notification: { message: string; type: "info" | "warning" };
545
+ }
546
+
547
+ const events = createEventBus<AppEvents>();
548
+
549
+ // Fully typed!
550
+ const unsubscribe = events.on("user-updated", (data) => {
551
+ console.log(data.name); // TypeScript knows this is string
552
+ });
553
+
554
+ // Subscribe once
555
+ events.once("data-changed", (data) => {
556
+ console.log(data.items); // number[]
557
+ });
558
+
559
+ // Unsubscribe
560
+ unsubscribe();
561
+ ```
562
+
563
+ ### Event Bus Methods
564
+
565
+ **Main Process (`RpcServer`):**
566
+
567
+ - `publish(eventName, data?)` - Publish an event to all subscribed renderers
568
+ - `getEventSubscribers()` - Get subscriber counts for each event
569
+
570
+ **Renderer Process:**
571
+
572
+ - `on(eventName, callback)` - Subscribe to an event, returns unsubscribe function
573
+ - `off(eventName, callback?)` - Unsubscribe from an event (or all callbacks for the event)
574
+ - `once(eventName, callback)` - Subscribe once, auto-unsubscribe after first event
575
+
576
+ ## Streaming
577
+
578
+ Stream data from main process to renderer using Web standard `ReadableStream`:
579
+
580
+ ### Main Process
581
+
582
+ ```typescript
583
+ import { RpcServer } from "electron-json-rpc/main";
584
+
585
+ const rpc = new RpcServer();
586
+
587
+ // Register a stream method
588
+ rpc.registerStream("counter", async (count: number) => {
589
+ return new ReadableStream({
590
+ async start(controller) {
591
+ for (let i = 0; i < count; i++) {
592
+ controller.enqueue({ index: i, value: i * 2 });
593
+ // Simulate async work
594
+ await new Promise((r) => setTimeout(r, 100));
595
+ }
596
+ controller.close();
597
+ },
598
+ });
599
+ });
600
+
601
+ // Stream from fetch or other async source
602
+ rpc.registerStream("fetchData", async (url: string) => {
603
+ const response = await fetch(url);
604
+ return response.body!;
605
+ });
606
+
607
+ rpc.listen();
608
+ ```
609
+
610
+ ### Renderer Process
611
+
612
+ ```typescript
613
+ import { createRpcClient } from "electron-json-rpc/renderer";
614
+
615
+ const rpc = createRpcClient();
616
+
617
+ // Using for-await-of (recommended)
618
+ for await (const chunk of rpc.stream("counter", 10)) {
619
+ console.log(chunk); // { index: 0, value: 0 }, { index: 1, value: 2 }, ...
620
+ }
621
+
622
+ // Using reader directly
623
+ const stream = rpc.stream("counter", 5);
624
+ const reader = stream.getReader();
625
+ while (true) {
626
+ const { done, value } = await reader.read();
627
+ if (done) break;
628
+ console.log(value);
629
+ }
630
+ reader.release();
631
+
632
+ // Pipe to Response
633
+ const response = new Response(rpc.stream("fetchData", "https://api.example.com/data"));
634
+ const blob = await response.blob();
635
+ ```
636
+
637
+ ### Stream Utilities
638
+
639
+ ```typescript
640
+ import { asyncGeneratorToStream, iterableToStream } from "electron-json-rpc/stream";
641
+
642
+ // Convert async generator to stream
643
+ rpc.registerStream("numbers", () => {
644
+ return asyncGeneratorToStream(async function* () {
645
+ for (let i = 0; i < 10; i++) {
646
+ yield i;
647
+ await new Promise((r) => setTimeout(r, 100));
648
+ }
649
+ });
650
+ });
651
+
652
+ // Convert array/iterable to stream
653
+ rpc.registerStream("items", () => {
654
+ return iterableToStream([1, 2, 3, 4, 5]);
655
+ });
656
+ ```
657
+
658
+ ## Validation
659
+
660
+ You can add parameter validation to any RPC method. The library provides a generic validator interface that works with any validation library.
661
+
662
+ ### Custom Validator
663
+
664
+ ```typescript
665
+ import { RpcServer } from "electron-json-rpc/main";
666
+
667
+ const rpc = new RpcServer();
668
+
669
+ // Simple custom validator
670
+ rpc.register("divide", (a: number, b: number) => a / b, {
671
+ validate: (params) => {
672
+ const [, divisor] = params as [number, number];
673
+ if (divisor === 0) {
674
+ throw new Error("Cannot divide by zero");
675
+ }
676
+ },
677
+ });
678
+ ```
679
+
680
+ ### With Zod
681
+
682
+ ```typescript
683
+ import { z } from "zod";
684
+ import { RpcServer } from "electron-json-rpc/main";
685
+
686
+ const rpc = new RpcServer();
687
+
688
+ const userSchema = z.object({
689
+ name: z.string().min(1),
690
+ age: z.number().min(0).max(150),
691
+ });
692
+
693
+ rpc.register(
694
+ "createUser",
695
+ async (user: unknown) => {
696
+ // user is already validated
697
+ return db.users.create(user);
698
+ },
699
+ {
700
+ validate: (params) => {
701
+ const result = userSchema.safeParse(params[0]);
702
+ if (!result.success) {
703
+ throw new Error(result.error.errors[0].message);
704
+ }
705
+ },
706
+ description: "Create a new user",
707
+ },
708
+ );
709
+ ```
710
+
711
+ ### With TypeBox
712
+
713
+ ```typescript
714
+ import { Type, type Static } from "@sinclair/typebox";
715
+ import { Value } from "@sinclair/typebox/value";
716
+ import { RpcServer } from "electron-json-rpc/main";
717
+
718
+ const rpc = new RpcServer();
719
+
720
+ const UserSchema = Type.Object({
721
+ name: Type.String({ minLength: 1 }),
722
+ age: Type.Number({ minimum: 0, maximum: 150 }),
723
+ });
724
+
725
+ type User = Static<typeof UserSchema>;
726
+
727
+ rpc.register(
728
+ "createUser",
729
+ async (user: User) => {
730
+ return db.users.create(user);
731
+ },
732
+ {
733
+ validate: (params) => {
734
+ const errors = Value.Errors(UserSchema, params[0]);
735
+ if (errors.length > 0) {
736
+ throw new Error([...errors][0].message);
737
+ }
738
+ },
739
+ },
740
+ );
741
+ ```
742
+
743
+ ### With Ajv
744
+
745
+ ```typescript
746
+ import Ajv from "ajv";
747
+ import { RpcServer } from "electron-json-rpc/main";
748
+
749
+ const rpc = new RpcServer();
750
+ const ajv = new Ajv();
751
+
752
+ const validateUser = ajv.compile({
753
+ type: "object",
754
+ properties: {
755
+ name: { type: "string", minLength: 1 },
756
+ age: { type: "number", minimum: 0, maximum: 150 },
757
+ },
758
+ required: ["name", "age"],
759
+ additionalProperties: false,
760
+ });
761
+
762
+ rpc.register(
763
+ "createUser",
764
+ async (user) => {
765
+ return db.users.create(user);
766
+ },
767
+ {
768
+ validate: (params) => {
769
+ if (!validateUser(params[0])) {
770
+ throw new Error(ajv.errorsText(validateUser.errors));
771
+ }
772
+ },
773
+ },
774
+ );
775
+ ```
776
+
777
+ ## API Reference
778
+
779
+ ### Main Process (`electron-json-rpc/main`)
780
+
781
+ #### `RpcServer`
782
+
783
+ ```typescript
784
+ const rpc = new RpcServer();
785
+ ```
786
+
787
+ **Methods:**
788
+
789
+ - `register(name: string, handler: Function, options?)` - Register a RPC method
790
+ - `options.validate?: (params: unknown[]) => void | Promise<void>` - Validator function
791
+ - `options.description?: string` - Method description
792
+ - `registerStream(name: string, handler: Function, options?)` - Register a stream method
793
+ - Handler should return a `ReadableStream`
794
+ - `publish(eventName: string, data?)` - Publish an event to all subscribed renderers
795
+ - `getEventSubscribers(): Record<string, number>` - Get subscriber counts for each event
796
+ - `unregister(name: string)` - Unregister a method
797
+ - `has(name: string): boolean` - Check if method exists
798
+ - `getMethodNames(): string[]` - Get all registered method names
799
+ - `listen()` - Start listening for IPC messages
800
+ - `dispose()` - Stop listening and cleanup
801
+
802
+ ### Preload (`electron-json-rpc/preload`)
803
+
804
+ #### `exposeRpcApi(options)`
805
+
806
+ Exposes the RPC API to the renderer process.
807
+
808
+ ```typescript
809
+ exposeRpcApi({
810
+ contextBridge,
811
+ ipcRenderer,
812
+ methods: ["method1", "method2"], // Optional whitelist
813
+ apiName: "rpc", // Default: 'rpc'
814
+ });
815
+ ```
816
+
817
+ #### `createPreloadClient(ipcRenderer, timeout?)`
818
+
819
+ Create a client for use in preload scripts (without exposing to renderer).
820
+
821
+ ### Renderer (`electron-json-rpc/renderer`)
822
+
823
+ #### `createRpcClient(options?)`
824
+
825
+ Create an untyped RPC client.
826
+
827
+ ```typescript
828
+ const rpc = createRpcClient({
829
+ timeout: 10000, // Default: 30000ms
830
+ apiName: "rpc", // Default: 'rpc'
831
+ });
832
+
833
+ await rpc.call("methodName", arg1, arg2);
834
+ rpc.notify("notificationMethod", arg1);
835
+ rpc.stream("streamMethod", arg1); // Returns ReadableStream
836
+ ```
837
+
838
+ #### `createTypedRpcClient<T>(options?)`
839
+
840
+ Create a typed RPC client with full type safety.
841
+
842
+ ```typescript
843
+ type MyApi = {
844
+ foo: (x: number) => string;
845
+ };
846
+
847
+ const rpc = createTypedRpcClient<MyApi>();
848
+ await rpc.foo(42);
849
+ ```
850
+
851
+ #### `defineRpcApi<T>(options?)`
852
+
853
+ Define a typed RPC API from an interface (alias for `createTypedRpcClient` with semantic naming).
854
+
855
+ ```typescript
856
+ interface AppApi {
857
+ getUser(id: string): Promise<User>;
858
+ log(msg: string): void;
859
+ }
860
+
861
+ const api = defineRpcApi<AppApi>();
862
+ await api.getUser("123");
863
+ api.log("hello");
864
+ ```
865
+
866
+ #### `createRpc(options?)`
867
+
868
+ Create a fluent API builder with type inference.
869
+
870
+ ```typescript
871
+ const api = createRpc()
872
+ .add("getUser", (id: string) => Promise<User>())
873
+ .add("log", (msg: string) => {})
874
+ .stream("dataStream", (n: number) => new ReadableStream<number>())
875
+ .build();
876
+
877
+ await api.getUser("123");
878
+ api.log("hello");
879
+ ```
880
+
881
+ #### `useRpcProxy(options?)`
882
+
883
+ Use the proxy exposed by preload (if methods whitelist was provided).
884
+
885
+ #### `createQueuedRpcClient(options?)`
886
+
887
+ Create a queued RPC client with automatic retry and connection health checking.
888
+
889
+ ```typescript
890
+ const rpc = createQueuedRpcClient({
891
+ maxSize: 100, // Maximum queue size (default: 100)
892
+ fullBehavior: "reject", // 'reject' | 'evict' | 'evictOldest'
893
+ timeout: 30000, // Request timeout in ms (default: 30000)
894
+ retry: {
895
+ maxAttempts: 3, // Maximum retry attempts (default: 3)
896
+ backoff: "exponential", // 'fixed' | 'exponential'
897
+ initialDelay: 1000, // Initial delay in ms (default: 1000)
898
+ maxDelay: 10000, // Maximum delay in ms (default: 10000)
899
+ },
900
+ healthCheck: true, // Enable health check (default: true)
901
+ healthCheckInterval: 5000, // Health check interval in ms (default: 5000)
902
+ apiName: "rpc", // API name (default: 'rpc')
903
+ });
904
+
905
+ // Queue control methods
906
+ rpc.getQueueStatus(); // Returns QueueStatus
907
+ rpc.clearQueue(); // Clear all pending requests
908
+ rpc.pauseQueue(); // Pause queue processing
909
+ rpc.resumeQueue(); // Resume queue processing
910
+ rpc.isQueueHealthy(); // Returns true if connected and not paused
911
+ ```
912
+
913
+ #### `createEventBus<T>(options?)`
914
+
915
+ Create a typed event bus for real-time events from main process.
916
+
917
+ ```typescript
918
+ interface AppEvents {
919
+ "user-updated": { id: string; name: string };
920
+ "data-changed": { items: number[] };
921
+ }
922
+
923
+ const events = createEventBus<AppEvents>();
924
+
925
+ // Subscribe with full type safety
926
+ const unsubscribe = events.on("user-updated", (data) => {
927
+ console.log(data.name); // TypeScript knows this is string
928
+ });
929
+
930
+ // Unsubscribe
931
+ unsubscribe();
932
+
933
+ // Subscribe once
934
+ events.once("data-changed", (data) => {
935
+ console.log(data.items);
936
+ });
937
+ ```
938
+
939
+ ## Error Handling
940
+
941
+ JSON-RPC errors are returned with standard error codes:
942
+
943
+ | Code | Message |
944
+ | ------ | ---------------- |
945
+ | -32700 | Parse error |
946
+ | -32600 | Invalid Request |
947
+ | -32601 | Method not found |
948
+ | -32602 | Invalid params |
949
+ | -32603 | Internal error |
950
+
951
+ Timeout errors use a custom `RpcTimeoutError` class.
952
+
953
+ ## Bundle Size
954
+
955
+ ESM bundles:
956
+
957
+ | Package | gzip |
958
+ | ---------------- | ------- |
959
+ | Preload | 3.95 kB |
960
+ | Main | 2.97 kB |
961
+ | Queue | 1.99 kB |
962
+ | Debug | 1.50 kB |
963
+ | Renderer/client | 1.14 kB |
964
+ | Renderer/builder | 1.21 kB |
965
+ | Renderer/event | 1.15 kB |
966
+ | Renderer/queue | 0.93 kB |
967
+ | Stream | 0.72 kB |
968
+ | Event | 0.43 kB |
969
+
970
+ ## Requirements
971
+
972
+ - **Electron**: >= 18.0.0 (recommended >= 32.0.0)
973
+ - **Node.js**: >= 16.9.0
974
+ - **TypeScript**: >= 5.0.0 (if using TypeScript)
975
+
976
+ ## License
977
+
978
+ MIT