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.
- package/LICENSE +21 -0
- package/README.md +978 -0
- package/README.zh-CN.md +978 -0
- package/dist/debug.d.mts +92 -0
- package/dist/debug.d.mts.map +1 -0
- package/dist/debug.mjs +206 -0
- package/dist/debug.mjs.map +1 -0
- package/dist/error-xVRu7Lxq.mjs +131 -0
- package/dist/error-xVRu7Lxq.mjs.map +1 -0
- package/dist/event.d.mts +71 -0
- package/dist/event.d.mts.map +1 -0
- package/dist/event.mjs +60 -0
- package/dist/event.mjs.map +1 -0
- package/dist/index.d.mts +78 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +4 -0
- package/dist/internal-BZK_0O3n.mjs +23 -0
- package/dist/internal-BZK_0O3n.mjs.map +1 -0
- package/dist/main.d.mts +151 -0
- package/dist/main.d.mts.map +1 -0
- package/dist/main.mjs +329 -0
- package/dist/main.mjs.map +1 -0
- package/dist/preload.d.mts +23 -0
- package/dist/preload.d.mts.map +1 -0
- package/dist/preload.mjs +417 -0
- package/dist/preload.mjs.map +1 -0
- package/dist/renderer/builder.d.mts +64 -0
- package/dist/renderer/builder.d.mts.map +1 -0
- package/dist/renderer/builder.mjs +101 -0
- package/dist/renderer/builder.mjs.map +1 -0
- package/dist/renderer/client.d.mts +42 -0
- package/dist/renderer/client.d.mts.map +1 -0
- package/dist/renderer/client.mjs +136 -0
- package/dist/renderer/client.mjs.map +1 -0
- package/dist/renderer/event.d.mts +17 -0
- package/dist/renderer/event.d.mts.map +1 -0
- package/dist/renderer/event.mjs +117 -0
- package/dist/renderer/event.mjs.map +1 -0
- package/dist/renderer/index.d.mts +6 -0
- package/dist/renderer/index.mjs +6 -0
- package/dist/stream.d.mts +38 -0
- package/dist/stream.d.mts.map +1 -0
- package/dist/stream.mjs +80 -0
- package/dist/stream.mjs.map +1 -0
- package/dist/types-BnGse9DF.d.mts +201 -0
- package/dist/types-BnGse9DF.d.mts.map +1 -0
- 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
|