@igoforth/ws-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 +446 -0
- package/dist/adapters/client.d.ts +117 -0
- package/dist/adapters/client.js +241 -0
- package/dist/adapters/cloudflare-do.d.ts +72 -0
- package/dist/adapters/cloudflare-do.js +192 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/server.d.ts +10 -0
- package/dist/adapters/server.js +122 -0
- package/dist/adapters/types.d.ts +125 -0
- package/dist/adapters/types.js +3 -0
- package/dist/codecs/cbor.d.ts +16 -0
- package/dist/codecs/cbor.js +36 -0
- package/dist/codecs/factory.d.ts +3 -0
- package/dist/codecs/factory.js +3 -0
- package/dist/codecs/index.d.ts +5 -0
- package/dist/codecs/index.js +5 -0
- package/dist/codecs/json.d.ts +4 -0
- package/dist/codecs/json.js +4 -0
- package/dist/codecs/msgpack.d.ts +16 -0
- package/dist/codecs/msgpack.js +34 -0
- package/dist/codecs-BmYG2d_U.js +0 -0
- package/dist/default-BkrMd28n.js +253 -0
- package/dist/default-xDNNMrg0.d.ts +129 -0
- package/dist/durable-MZjkvyS6.js +165 -0
- package/dist/errors-5BfreE63.js +96 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.js +7 -0
- package/dist/factory-3ziwTuZe.js +132 -0
- package/dist/factory-C1v0AEHY.d.ts +101 -0
- package/dist/index-Be7jjS77.d.ts +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/interface-C4S-WCqW.d.ts +120 -0
- package/dist/json-54Z2bIIs.d.ts +22 -0
- package/dist/json-Bshec-bZ.js +41 -0
- package/dist/memory-Bqb3KEVr.js +48 -0
- package/dist/memory-D1nGjzzH.d.ts +41 -0
- package/dist/multi-peer-BAi9yVzp.js +242 -0
- package/dist/peers/default.d.ts +8 -0
- package/dist/peers/default.js +8 -0
- package/dist/peers/durable.d.ts +136 -0
- package/dist/peers/durable.js +9 -0
- package/dist/peers/index.d.ts +10 -0
- package/dist/peers/index.js +9 -0
- package/dist/protocol-DA84zrc2.d.ts +211 -0
- package/dist/protocol-_mpoOPp6.js +192 -0
- package/dist/protocol.d.ts +6 -0
- package/dist/protocol.js +6 -0
- package/dist/reconnect-CGAA_1Gf.js +26 -0
- package/dist/reconnect-DbcN0R_1.d.ts +35 -0
- package/dist/schema-CN5HHHku.d.ts +108 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +43 -0
- package/dist/server-zTjpJpoX.d.ts +209 -0
- package/dist/sql-CCjc6Bid.js +142 -0
- package/dist/sql-DPmHOeZy.d.ts +131 -0
- package/dist/storage/index.d.ts +8 -0
- package/dist/storage/index.js +7 -0
- package/dist/storage/interface.d.ts +3 -0
- package/dist/storage/interface.js +0 -0
- package/dist/storage/memory.d.ts +7 -0
- package/dist/storage/memory.js +6 -0
- package/dist/storage/sql.d.ts +7 -0
- package/dist/storage/sql.js +6 -0
- package/dist/types-Be-qmQu0.d.ts +111 -0
- package/dist/types-D_psiH09.js +13 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +3 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/reconnect.d.ts +2 -0
- package/dist/utils/reconnect.js +3 -0
- package/package.json +156 -0
- package/src/adapters/client.ts +396 -0
- package/src/adapters/cloudflare-do.ts +346 -0
- package/src/adapters/index.ts +16 -0
- package/src/adapters/multi-peer.ts +404 -0
- package/src/adapters/server.ts +192 -0
- package/src/adapters/types.ts +202 -0
- package/src/codecs/cbor.ts +42 -0
- package/src/codecs/factory.ts +210 -0
- package/src/codecs/index.ts +30 -0
- package/src/codecs/json.ts +42 -0
- package/src/codecs/msgpack.ts +36 -0
- package/src/errors.ts +105 -0
- package/src/index.ts +102 -0
- package/src/peers/default.ts +433 -0
- package/src/peers/durable.ts +280 -0
- package/src/peers/index.ts +13 -0
- package/src/protocol.ts +306 -0
- package/src/schema.ts +167 -0
- package/src/storage/index.ts +20 -0
- package/src/storage/interface.ts +146 -0
- package/src/storage/memory.ts +84 -0
- package/src/storage/sql.ts +266 -0
- package/src/types.ts +158 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/reconnect.ts +51 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Peer Base Class
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class for adapters managing multiple RPC peers.
|
|
5
|
+
* Extended by RpcServer and Cloudflare DO adapter.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RpcPeer } from "../peers/default.js";
|
|
9
|
+
import type {
|
|
10
|
+
EventDef,
|
|
11
|
+
InferEventData,
|
|
12
|
+
Provider,
|
|
13
|
+
RpcSchema,
|
|
14
|
+
StringKeys,
|
|
15
|
+
} from "../schema.js";
|
|
16
|
+
import type { IMinWebSocket } from "../types.js";
|
|
17
|
+
import type {
|
|
18
|
+
IMultiAdapterHooks,
|
|
19
|
+
IMultiConnectionAdapter,
|
|
20
|
+
MultiCallOptions,
|
|
21
|
+
MultiCallResult,
|
|
22
|
+
MultiDriver,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for creating a MultiPeerBase subclass
|
|
27
|
+
*/
|
|
28
|
+
export interface MultiPeerOptions<
|
|
29
|
+
TLocalSchema extends RpcSchema,
|
|
30
|
+
TRemoteSchema extends RpcSchema,
|
|
31
|
+
> {
|
|
32
|
+
/** Schema defining local methods we implement */
|
|
33
|
+
localSchema: TLocalSchema;
|
|
34
|
+
/** Schema defining remote methods we can call */
|
|
35
|
+
remoteSchema: TRemoteSchema;
|
|
36
|
+
/** Implementation of local methods */
|
|
37
|
+
provider: Provider<TLocalSchema>;
|
|
38
|
+
/** Default timeout for RPC calls in ms */
|
|
39
|
+
timeout?: number;
|
|
40
|
+
/** Lifecycle hooks */
|
|
41
|
+
hooks?: IMultiAdapterHooks<TLocalSchema, TRemoteSchema>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Abstract base class for multi-peer adapters
|
|
46
|
+
*
|
|
47
|
+
* Provides shared functionality for managing multiple RPC peers:
|
|
48
|
+
* - Driver for calling methods on multiple peers
|
|
49
|
+
* - Broadcast emit to all peers
|
|
50
|
+
* - Peer lookup by ID
|
|
51
|
+
* - Connection count/IDs
|
|
52
|
+
*
|
|
53
|
+
* @typeParam TLocalSchema - Schema for methods/events this side provides
|
|
54
|
+
* @typeParam TRemoteSchema - Schema for methods/events the remote side provides
|
|
55
|
+
* @typeParam TConnection - Connection type (IWebSocket, WebSocket, etc.)
|
|
56
|
+
*/
|
|
57
|
+
export abstract class MultiPeerBase<
|
|
58
|
+
TLocalSchema extends RpcSchema,
|
|
59
|
+
TRemoteSchema extends RpcSchema,
|
|
60
|
+
TConnection,
|
|
61
|
+
> implements IMultiConnectionAdapter<TLocalSchema, TRemoteSchema>
|
|
62
|
+
{
|
|
63
|
+
protected readonly peers = new Map<
|
|
64
|
+
TConnection,
|
|
65
|
+
RpcPeer<TLocalSchema, TRemoteSchema>
|
|
66
|
+
>();
|
|
67
|
+
|
|
68
|
+
/** Local schema */
|
|
69
|
+
public readonly localSchema: TLocalSchema;
|
|
70
|
+
|
|
71
|
+
/** Remote schema */
|
|
72
|
+
public readonly remoteSchema: TRemoteSchema;
|
|
73
|
+
|
|
74
|
+
/** Implementation of local methods */
|
|
75
|
+
public readonly provider: Provider<TLocalSchema>;
|
|
76
|
+
|
|
77
|
+
/** Default timeout for RPC calls */
|
|
78
|
+
public readonly timeout: number;
|
|
79
|
+
|
|
80
|
+
/** Lifecycle hooks */
|
|
81
|
+
public readonly hooks: IMultiAdapterHooks<TLocalSchema, TRemoteSchema>;
|
|
82
|
+
|
|
83
|
+
constructor(options: MultiPeerOptions<TLocalSchema, TRemoteSchema>) {
|
|
84
|
+
this.localSchema = options.localSchema;
|
|
85
|
+
this.remoteSchema = options.remoteSchema;
|
|
86
|
+
this.provider = options.provider;
|
|
87
|
+
this.timeout = options.timeout ?? 30000;
|
|
88
|
+
this.hooks = options.hooks ?? {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// =========================================================================
|
|
92
|
+
// Peer Creation
|
|
93
|
+
// =========================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create an RPC peer for a WebSocket connection.
|
|
97
|
+
* Subclasses can override to customize peer creation.
|
|
98
|
+
*/
|
|
99
|
+
protected createPeer(
|
|
100
|
+
ws: IMinWebSocket,
|
|
101
|
+
): RpcPeer<TLocalSchema, TRemoteSchema> {
|
|
102
|
+
return new RpcPeer<TLocalSchema, TRemoteSchema>({
|
|
103
|
+
ws,
|
|
104
|
+
localSchema: this.localSchema,
|
|
105
|
+
remoteSchema: this.remoteSchema,
|
|
106
|
+
provider: this.provider,
|
|
107
|
+
onEvent: this.hooks.onEvent
|
|
108
|
+
? (event, data) => {
|
|
109
|
+
const peer = this.findPeerByWs(ws);
|
|
110
|
+
if (peer) {
|
|
111
|
+
(
|
|
112
|
+
this.hooks.onEvent as (
|
|
113
|
+
peer: RpcPeer<TLocalSchema, TRemoteSchema>,
|
|
114
|
+
event: string,
|
|
115
|
+
data: unknown,
|
|
116
|
+
) => void
|
|
117
|
+
)(peer, event, data);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
: undefined,
|
|
121
|
+
timeout: this.timeout,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Find peer by WebSocket (internal helper for event routing)
|
|
127
|
+
*/
|
|
128
|
+
private findPeerByWs(
|
|
129
|
+
ws: IMinWebSocket,
|
|
130
|
+
): RpcPeer<TLocalSchema, TRemoteSchema> | null {
|
|
131
|
+
for (const peer of this.peers.values()) {
|
|
132
|
+
if (peer.getWebSocket() === ws) {
|
|
133
|
+
return peer;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// =========================================================================
|
|
140
|
+
// Peer Management
|
|
141
|
+
// =========================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Add a peer (called by subclass when connection established)
|
|
145
|
+
*/
|
|
146
|
+
protected addPeer(
|
|
147
|
+
connection: TConnection,
|
|
148
|
+
peer: RpcPeer<TLocalSchema, TRemoteSchema>,
|
|
149
|
+
): void {
|
|
150
|
+
this.peers.set(connection, peer);
|
|
151
|
+
this.hooks.onConnect?.(peer);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Remove a peer (called by subclass when connection closes)
|
|
156
|
+
*/
|
|
157
|
+
protected removePeer(
|
|
158
|
+
connection: TConnection,
|
|
159
|
+
): RpcPeer<TLocalSchema, TRemoteSchema> | null {
|
|
160
|
+
const peer = this.peers.get(connection);
|
|
161
|
+
if (peer) {
|
|
162
|
+
peer.close();
|
|
163
|
+
this.peers.delete(connection);
|
|
164
|
+
this.hooks.onDisconnect?.(peer);
|
|
165
|
+
return peer;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get peer by connection object
|
|
172
|
+
*/
|
|
173
|
+
public getPeerFor(
|
|
174
|
+
connection: TConnection,
|
|
175
|
+
): RpcPeer<TLocalSchema, TRemoteSchema> | null {
|
|
176
|
+
return this.peers.get(connection) ?? null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get peer by ID
|
|
181
|
+
*/
|
|
182
|
+
public getPeer(id: string): RpcPeer<TLocalSchema, TRemoteSchema> | null {
|
|
183
|
+
for (const peer of this.peers.values()) {
|
|
184
|
+
if (peer.id === id) {
|
|
185
|
+
return peer;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Find peer entry by ID (internal - includes connection)
|
|
193
|
+
*/
|
|
194
|
+
protected findPeerEntry(id: string): {
|
|
195
|
+
peer: RpcPeer<TLocalSchema, TRemoteSchema>;
|
|
196
|
+
connection: TConnection;
|
|
197
|
+
} | null {
|
|
198
|
+
for (const [connection, peer] of this.peers) {
|
|
199
|
+
if (peer.id === id) {
|
|
200
|
+
return { peer, connection };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get all peers
|
|
208
|
+
*/
|
|
209
|
+
public getPeers(): IterableIterator<RpcPeer<TLocalSchema, TRemoteSchema>> {
|
|
210
|
+
return this.peers.values();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get all open peer entries (internal)
|
|
215
|
+
*/
|
|
216
|
+
protected getOpenEntries(): Array<{
|
|
217
|
+
peer: RpcPeer<TLocalSchema, TRemoteSchema>;
|
|
218
|
+
connection: TConnection;
|
|
219
|
+
}> {
|
|
220
|
+
const result: Array<{
|
|
221
|
+
peer: RpcPeer<TLocalSchema, TRemoteSchema>;
|
|
222
|
+
connection: TConnection;
|
|
223
|
+
}> = [];
|
|
224
|
+
for (const [connection, peer] of this.peers) {
|
|
225
|
+
if (peer.isOpen) result.push({ peer, connection });
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// =========================================================================
|
|
231
|
+
// IMultiConnectionAdapter Implementation
|
|
232
|
+
// =========================================================================
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get count of open connections
|
|
236
|
+
*/
|
|
237
|
+
public getConnectionCount(): number {
|
|
238
|
+
let count = 0;
|
|
239
|
+
for (const peer of this.peers.values()) {
|
|
240
|
+
if (peer.isOpen) count++;
|
|
241
|
+
}
|
|
242
|
+
return count;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get all open peer IDs
|
|
247
|
+
*/
|
|
248
|
+
public getConnectionIds(): string[] {
|
|
249
|
+
const ids: string[] = [];
|
|
250
|
+
for (const peer of this.peers.values()) {
|
|
251
|
+
if (peer.isOpen) ids.push(peer.id);
|
|
252
|
+
}
|
|
253
|
+
return ids;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Driver for calling remote methods on connected peers
|
|
258
|
+
*
|
|
259
|
+
* @returns MultiDriver proxy for calling methods on all or specific peers
|
|
260
|
+
*/
|
|
261
|
+
public get driver(): MultiDriver<TRemoteSchema> {
|
|
262
|
+
return this.createMultiDriver();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Emit an event to connected peers
|
|
267
|
+
*
|
|
268
|
+
* @param event - Event name from local schema
|
|
269
|
+
* @param data - Event data matching the schema
|
|
270
|
+
* @param ids - Optional array of peer IDs to emit to (broadcasts to all if omitted)
|
|
271
|
+
*/
|
|
272
|
+
public emit<K extends StringKeys<TLocalSchema["events"]>>(
|
|
273
|
+
event: K,
|
|
274
|
+
data: TLocalSchema["events"] extends Record<string, EventDef>
|
|
275
|
+
? InferEventData<TLocalSchema["events"][K]>
|
|
276
|
+
: never,
|
|
277
|
+
ids?: string[],
|
|
278
|
+
): void {
|
|
279
|
+
const validPeers = ids
|
|
280
|
+
? this.peers.values().filter((p) => ids.includes(p.id) && p.isOpen)
|
|
281
|
+
: this.peers.values().filter((p) => p.isOpen);
|
|
282
|
+
for (const peer of validPeers) peer.emit(event, data);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Close a specific peer by ID
|
|
287
|
+
*
|
|
288
|
+
* @param id - Peer ID to close
|
|
289
|
+
* @returns true if peer was found and closed, false otherwise
|
|
290
|
+
*/
|
|
291
|
+
public closePeer(id: string): boolean {
|
|
292
|
+
const entry = this.findPeerEntry(id);
|
|
293
|
+
if (entry) {
|
|
294
|
+
entry.peer.close();
|
|
295
|
+
this.peers.delete(entry.connection);
|
|
296
|
+
this.hooks.onDisconnect?.(entry.peer);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Close all peers
|
|
304
|
+
*/
|
|
305
|
+
protected closeAll(): void {
|
|
306
|
+
for (const peer of this.peers.values()) {
|
|
307
|
+
peer.close();
|
|
308
|
+
this.hooks.onDisconnect?.(peer);
|
|
309
|
+
}
|
|
310
|
+
this.peers.clear();
|
|
311
|
+
this.hooks.onClose?.();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// =========================================================================
|
|
315
|
+
// Driver Implementation
|
|
316
|
+
// =========================================================================
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Create a driver proxy for calling remote methods on multiple peers
|
|
320
|
+
*/
|
|
321
|
+
private createMultiDriver(): MultiDriver<TRemoteSchema> {
|
|
322
|
+
const methods = this.remoteSchema.methods ?? {};
|
|
323
|
+
const driver: Record<
|
|
324
|
+
string,
|
|
325
|
+
(input: unknown, options?: MultiCallOptions) => Promise<unknown>
|
|
326
|
+
> = {};
|
|
327
|
+
|
|
328
|
+
for (const methodName of Object.keys(methods)) {
|
|
329
|
+
driver[methodName] = async (
|
|
330
|
+
input: unknown,
|
|
331
|
+
options?: MultiCallOptions,
|
|
332
|
+
) => {
|
|
333
|
+
return this.callMethod(methodName, input, options);
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return driver as MultiDriver<TRemoteSchema>;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Call a method on multiple peers with timeout handling
|
|
342
|
+
*
|
|
343
|
+
* @param method - Method name to call
|
|
344
|
+
* @param input - Method input parameters
|
|
345
|
+
* @param options - Call options including target peer IDs and timeout
|
|
346
|
+
* @returns Array of results from each peer (success or error)
|
|
347
|
+
*/
|
|
348
|
+
private async callMethod(
|
|
349
|
+
method: string,
|
|
350
|
+
input: unknown,
|
|
351
|
+
options?: MultiCallOptions,
|
|
352
|
+
): Promise<Array<MultiCallResult<unknown>>> {
|
|
353
|
+
const ids = options?.ids;
|
|
354
|
+
const timeout = options?.timeout ?? this.timeout;
|
|
355
|
+
|
|
356
|
+
// Determine which peers to call
|
|
357
|
+
let targetPeers: Array<RpcPeer<TLocalSchema, TRemoteSchema>>;
|
|
358
|
+
|
|
359
|
+
if (ids === undefined) {
|
|
360
|
+
targetPeers = Array.from(this.peers.values()).filter((p) => p.isOpen);
|
|
361
|
+
} else if (typeof ids === "string") {
|
|
362
|
+
const peer = this.getPeer(ids);
|
|
363
|
+
targetPeers = peer?.isOpen ? [peer] : [];
|
|
364
|
+
} else {
|
|
365
|
+
targetPeers = ids
|
|
366
|
+
.map((id) => this.getPeer(id))
|
|
367
|
+
.filter(
|
|
368
|
+
(p): p is RpcPeer<TLocalSchema, TRemoteSchema> => p?.isOpen === true,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const promises = targetPeers.map(async (peer) => {
|
|
373
|
+
try {
|
|
374
|
+
const peerDriver = peer.driver as Record<
|
|
375
|
+
string,
|
|
376
|
+
(input: unknown) => Promise<unknown>
|
|
377
|
+
>;
|
|
378
|
+
const callPromise = peerDriver[method]!(input);
|
|
379
|
+
|
|
380
|
+
const value = await Promise.race([
|
|
381
|
+
callPromise,
|
|
382
|
+
new Promise<never>((_, reject) =>
|
|
383
|
+
setTimeout(
|
|
384
|
+
() => reject(new Error(`Timeout after ${timeout}ms`)),
|
|
385
|
+
timeout,
|
|
386
|
+
),
|
|
387
|
+
),
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
return { id: peer.id, result: { success: true as const, value } };
|
|
391
|
+
} catch (error) {
|
|
392
|
+
return {
|
|
393
|
+
id: peer.id,
|
|
394
|
+
result: {
|
|
395
|
+
success: false as const,
|
|
396
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return Promise.all(promises);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Server Adapter
|
|
3
|
+
*
|
|
4
|
+
* Manages RpcPeer instances for incoming WebSocket connections.
|
|
5
|
+
* Works with Node.js `ws`, Bun's native WebSocket, or any compatible server.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RpcPeer } from "../peers/default.js";
|
|
9
|
+
import type { Provider, RpcSchema } from "../schema.js";
|
|
10
|
+
import {
|
|
11
|
+
type IRpcOptions,
|
|
12
|
+
type IWebSocket,
|
|
13
|
+
type IWebSocketServer,
|
|
14
|
+
WebSocketReadyState,
|
|
15
|
+
type WebSocketServerOptions,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
import { MultiPeerBase } from "./multi-peer.js";
|
|
18
|
+
import type { IMultiAdapterHooks } from "./types.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for creating an RpcServer
|
|
22
|
+
*/
|
|
23
|
+
export interface RpcServerOptions<
|
|
24
|
+
TLocalSchema extends RpcSchema,
|
|
25
|
+
TRemoteSchema extends RpcSchema,
|
|
26
|
+
> extends IRpcOptions<TLocalSchema, TRemoteSchema> {
|
|
27
|
+
/** Implementation of local methods */
|
|
28
|
+
provider: Provider<TLocalSchema>;
|
|
29
|
+
/** WebSocket server instance or options to create one */
|
|
30
|
+
wss: IWebSocketServer | WebSocketServerOptions;
|
|
31
|
+
/** WebSocket server constructor (defaults to require('ws').WebSocketServer) */
|
|
32
|
+
WebSocketServer?: new (
|
|
33
|
+
options: WebSocketServerOptions,
|
|
34
|
+
) => IWebSocketServer;
|
|
35
|
+
/** Lifecycle hooks */
|
|
36
|
+
hooks?: IMultiAdapterHooks<TLocalSchema, TRemoteSchema>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* RPC Server
|
|
41
|
+
*
|
|
42
|
+
* Manages WebSocket server and client connections with RPC capabilities.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* import { WebSocketServer } from "ws";
|
|
47
|
+
* import { RpcServer } from "@igoforth/ws-rpc/adapters/server";
|
|
48
|
+
*
|
|
49
|
+
* const server = new RpcServer({
|
|
50
|
+
* wss: { port: 8080 },
|
|
51
|
+
* WebSocketServer,
|
|
52
|
+
* localSchema: ServerSchema,
|
|
53
|
+
* remoteSchema: ClientSchema,
|
|
54
|
+
* provider: {
|
|
55
|
+
* getUser: async ({ id }) => ({ name: "John", email: "john@example.com" }),
|
|
56
|
+
* },
|
|
57
|
+
* hooks: {
|
|
58
|
+
* onConnect: (peer) => {
|
|
59
|
+
* console.log(`Client ${peer.id} connected`);
|
|
60
|
+
* peer.driver.ping({}).then(console.log);
|
|
61
|
+
* },
|
|
62
|
+
* onDisconnect: (peer) => console.log(`Client ${peer.id} disconnected`),
|
|
63
|
+
* },
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* // Emit to all clients
|
|
67
|
+
* server.emit("orderUpdated", { orderId: "123", status: "shipped" });
|
|
68
|
+
*
|
|
69
|
+
* // Graceful shutdown
|
|
70
|
+
* process.on("SIGTERM", () => server.close());
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export class RpcServer<
|
|
74
|
+
TLocalSchema extends RpcSchema,
|
|
75
|
+
TRemoteSchema extends RpcSchema,
|
|
76
|
+
> extends MultiPeerBase<TLocalSchema, TRemoteSchema, IWebSocket> {
|
|
77
|
+
private readonly wss: IWebSocketServer;
|
|
78
|
+
|
|
79
|
+
constructor(options: RpcServerOptions<TLocalSchema, TRemoteSchema>) {
|
|
80
|
+
super({
|
|
81
|
+
localSchema: options.localSchema,
|
|
82
|
+
remoteSchema: options.remoteSchema,
|
|
83
|
+
provider: options.provider,
|
|
84
|
+
...(options.timeout !== undefined && { timeout: options.timeout }),
|
|
85
|
+
...(options.hooks !== undefined && { hooks: options.hooks }),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Create or use existing WebSocket server
|
|
89
|
+
if ("on" in options.wss && typeof options.wss.on === "function") {
|
|
90
|
+
this.wss = options.wss as IWebSocketServer;
|
|
91
|
+
} else {
|
|
92
|
+
if (!options.WebSocketServer) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"WebSocketServer constructor required when passing options",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
this.wss = new options.WebSocketServer(
|
|
98
|
+
options.wss as WebSocketServerOptions,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Set up server event handlers
|
|
103
|
+
this.wss.on("connection", (ws) => this.handleConnection(ws));
|
|
104
|
+
this.wss.on("error", (error) => this.hooks.onError?.(null, error));
|
|
105
|
+
this.wss.on("close", () => this.hooks.onClose?.());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// =========================================================================
|
|
109
|
+
// Connection Handling
|
|
110
|
+
// =========================================================================
|
|
111
|
+
|
|
112
|
+
private handleConnection(ws: IWebSocket): void {
|
|
113
|
+
const peer = new RpcPeer({
|
|
114
|
+
ws,
|
|
115
|
+
localSchema: this.localSchema,
|
|
116
|
+
remoteSchema: this.remoteSchema,
|
|
117
|
+
provider: this.provider,
|
|
118
|
+
timeout: this.timeout,
|
|
119
|
+
onEvent: (event, data) => {
|
|
120
|
+
this.hooks.onEvent?.(peer, event, data);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this.addPeer(ws, peer);
|
|
125
|
+
|
|
126
|
+
ws.onmessage = (event) => {
|
|
127
|
+
if (
|
|
128
|
+
typeof event === "object" &&
|
|
129
|
+
event != null &&
|
|
130
|
+
"data" in event &&
|
|
131
|
+
(typeof event.data === "string" || event.data instanceof ArrayBuffer)
|
|
132
|
+
) {
|
|
133
|
+
peer.handleMessage(event.data as string | ArrayBuffer);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
ws.onclose = () => {
|
|
138
|
+
this.removePeer(ws);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
ws.onerror = (event) => {
|
|
142
|
+
const error =
|
|
143
|
+
event instanceof Error
|
|
144
|
+
? event
|
|
145
|
+
: new Error(`WebSocket error for peer ${peer.id}`);
|
|
146
|
+
this.hooks.onError?.(peer, error);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// =========================================================================
|
|
151
|
+
// Server-Specific Methods
|
|
152
|
+
// =========================================================================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Close a peer connection with WebSocket close code/reason
|
|
156
|
+
*
|
|
157
|
+
* @param id - Peer ID to close
|
|
158
|
+
* @param code - WebSocket close code (default: 1000)
|
|
159
|
+
* @param reason - Close reason message (default: "Server disconnect")
|
|
160
|
+
* @returns true if peer was found and closed, false otherwise
|
|
161
|
+
*/
|
|
162
|
+
override closePeer(
|
|
163
|
+
id: string,
|
|
164
|
+
code = 1000,
|
|
165
|
+
reason = "Server disconnect",
|
|
166
|
+
): boolean {
|
|
167
|
+
const entry = this.findPeerEntry(id);
|
|
168
|
+
if (entry) {
|
|
169
|
+
if (entry.connection.readyState !== WebSocketReadyState.CLOSED) {
|
|
170
|
+
entry.connection.close(code, reason);
|
|
171
|
+
}
|
|
172
|
+
return super.closePeer(id);
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Close the server and all client connections
|
|
179
|
+
*
|
|
180
|
+
* @param callback - Optional callback invoked when server is closed
|
|
181
|
+
*/
|
|
182
|
+
close(callback?: (err?: Error) => void): void {
|
|
183
|
+
for (const entry of this.getOpenEntries()) {
|
|
184
|
+
if (entry.connection.readyState !== WebSocketReadyState.CLOSED) {
|
|
185
|
+
entry.connection.close(1001, "Server shutdown");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.closeAll();
|
|
190
|
+
this.wss.close(callback);
|
|
191
|
+
}
|
|
192
|
+
}
|