murow 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/net/client.d.ts +51 -0
- package/dist/net/client.js +123 -2
- package/dist/net/server.d.ts +67 -0
- package/dist/net/server.js +164 -2
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/rpc/define-rpc.d.ts +72 -0
- package/dist/protocol/rpc/define-rpc.js +84 -0
- package/dist/protocol/rpc/index.d.ts +3 -0
- package/dist/protocol/rpc/index.js +3 -0
- package/dist/protocol/rpc/rpc-registry.d.ts +105 -0
- package/dist/protocol/rpc/rpc-registry.js +142 -0
- package/dist/protocol/rpc/rpc.d.ts +34 -0
- package/dist/protocol/rpc/rpc.js +12 -0
- package/package.json +1 -1
- package/src/net/README.md +79 -4
- package/src/net/client.ts +143 -2
- package/src/net/server.ts +196 -2
- package/src/protocol/README.md +91 -4
- package/src/protocol/index.ts +1 -0
- package/src/protocol/rpc/define-rpc.test.ts +141 -0
- package/src/protocol/rpc/define-rpc.ts +113 -0
- package/src/protocol/rpc/index.ts +3 -0
- package/src/protocol/rpc/rpc-registry.test.ts +168 -0
- package/src/protocol/rpc/rpc-registry.ts +156 -0
- package/src/protocol/rpc/rpc.ts +37 -0
package/dist/net/client.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { IntentRegistry } from "../protocol/intent/intent-registry";
|
|
|
2
2
|
import type { SnapshotRegistry } from "../protocol/snapshot/snapshot-registry";
|
|
3
3
|
import type { Snapshot } from "../protocol/snapshot/snapshot";
|
|
4
4
|
import type { Intent } from "../protocol/intent/intent";
|
|
5
|
+
import type { RpcRegistry } from "../protocol/rpc/rpc-registry";
|
|
6
|
+
import type { DefinedRpc } from "../protocol/rpc/rpc";
|
|
5
7
|
import { type TransportAdapter, type NetworkConfig } from "./types";
|
|
6
8
|
/**
|
|
7
9
|
* Configuration for ClientNetwork
|
|
@@ -13,6 +15,8 @@ export interface ClientNetworkConfig<TSnapshots> {
|
|
|
13
15
|
intentRegistry: IntentRegistry;
|
|
14
16
|
/** Snapshot registry for decoding server snapshots */
|
|
15
17
|
snapshotRegistry: SnapshotRegistry<TSnapshots>;
|
|
18
|
+
/** RPC registry for bidirectional remote procedure calls (optional) */
|
|
19
|
+
rpcRegistry?: RpcRegistry;
|
|
16
20
|
/** Network configuration */
|
|
17
21
|
config?: NetworkConfig;
|
|
18
22
|
}
|
|
@@ -42,9 +46,12 @@ export declare class ClientNetwork<TSnapshots = unknown> {
|
|
|
42
46
|
private transport;
|
|
43
47
|
private intentRegistry;
|
|
44
48
|
private snapshotRegistry;
|
|
49
|
+
private rpcRegistry?;
|
|
45
50
|
private config;
|
|
46
51
|
/** Snapshot type handlers: type -> handler[] (supports multiple handlers) */
|
|
47
52
|
private snapshotHandlers;
|
|
53
|
+
/** RPC method handlers: method -> handler[] (supports multiple handlers) */
|
|
54
|
+
private rpcHandlers;
|
|
48
55
|
/** Connection lifecycle handlers */
|
|
49
56
|
private connectHandlers;
|
|
50
57
|
private disconnectHandlers;
|
|
@@ -98,6 +105,46 @@ export declare class ClientNetwork<TSnapshots = unknown> {
|
|
|
98
105
|
* @returns Unsubscribe function to remove this handler
|
|
99
106
|
*/
|
|
100
107
|
onSnapshot<T extends Partial<TSnapshots>>(type: string, handler: (snapshot: Snapshot<T>) => void): () => void;
|
|
108
|
+
/**
|
|
109
|
+
* Send an RPC to the server (type-safe)
|
|
110
|
+
*
|
|
111
|
+
* @template TSchema The RPC data type
|
|
112
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
113
|
+
* @param data The RPC data to send
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const BuyItem = defineRpc({
|
|
118
|
+
* method: 'buyItem',
|
|
119
|
+
* schema: { itemId: BinaryCodec.string(32) }
|
|
120
|
+
* });
|
|
121
|
+
*
|
|
122
|
+
* client.sendRpc(BuyItem, { itemId: 'long_sword' });
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
sendRpc<TSchema extends Record<string, any>>(rpc: DefinedRpc<TSchema>, data: TSchema): void;
|
|
126
|
+
/**
|
|
127
|
+
* Register a handler for incoming RPCs from the server (type-safe)
|
|
128
|
+
* Supports multiple handlers per RPC method
|
|
129
|
+
*
|
|
130
|
+
* @template TSchema The RPC data type
|
|
131
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
132
|
+
* @param handler Callback function to handle the RPC
|
|
133
|
+
* @returns Unsubscribe function to remove this handler
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const MatchCountdown = defineRpc({
|
|
138
|
+
* method: 'matchCountdown',
|
|
139
|
+
* schema: { secondsRemaining: BinaryCodec.u8 }
|
|
140
|
+
* });
|
|
141
|
+
*
|
|
142
|
+
* client.onRpc(MatchCountdown, (rpc) => {
|
|
143
|
+
* console.log(`Match starting in ${rpc.secondsRemaining}s`);
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
onRpc<TSchema extends Record<string, any>>(rpc: DefinedRpc<TSchema>, handler: (data: TSchema) => void): () => void;
|
|
101
148
|
/**
|
|
102
149
|
* Register a handler for connection events
|
|
103
150
|
*/
|
|
@@ -138,6 +185,10 @@ export declare class ClientNetwork<TSnapshots = unknown> {
|
|
|
138
185
|
* Decode and handle a snapshot from server
|
|
139
186
|
*/
|
|
140
187
|
private handleSnapshot;
|
|
188
|
+
/**
|
|
189
|
+
* Handle incoming RPC message from server
|
|
190
|
+
*/
|
|
191
|
+
private handleRpc;
|
|
141
192
|
/**
|
|
142
193
|
* Handle disconnection from server
|
|
143
194
|
*/
|
package/dist/net/client.js
CHANGED
|
@@ -25,6 +25,8 @@ export class ClientNetwork {
|
|
|
25
25
|
constructor(config) {
|
|
26
26
|
/** Snapshot type handlers: type -> handler[] (supports multiple handlers) */
|
|
27
27
|
this.snapshotHandlers = new Map();
|
|
28
|
+
/** RPC method handlers: method -> handler[] (supports multiple handlers) */
|
|
29
|
+
this.rpcHandlers = new Map();
|
|
28
30
|
/** Connection lifecycle handlers */
|
|
29
31
|
this.connectHandlers = [];
|
|
30
32
|
this.disconnectHandlers = [];
|
|
@@ -43,6 +45,7 @@ export class ClientNetwork {
|
|
|
43
45
|
this.transport = config.transport;
|
|
44
46
|
this.intentRegistry = config.intentRegistry;
|
|
45
47
|
this.snapshotRegistry = config.snapshotRegistry;
|
|
48
|
+
this.rpcRegistry = config.rpcRegistry;
|
|
46
49
|
this.config = {
|
|
47
50
|
maxMessageSize: config.config?.maxMessageSize ?? 65536,
|
|
48
51
|
debug: config.config?.debug ?? false,
|
|
@@ -149,6 +152,93 @@ export class ClientNetwork {
|
|
|
149
152
|
}
|
|
150
153
|
};
|
|
151
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Send an RPC to the server (type-safe)
|
|
157
|
+
*
|
|
158
|
+
* @template TSchema The RPC data type
|
|
159
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
160
|
+
* @param data The RPC data to send
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* const BuyItem = defineRpc({
|
|
165
|
+
* method: 'buyItem',
|
|
166
|
+
* schema: { itemId: BinaryCodec.string(32) }
|
|
167
|
+
* });
|
|
168
|
+
*
|
|
169
|
+
* client.sendRpc(BuyItem, { itemId: 'long_sword' });
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
sendRpc(rpc, data) {
|
|
173
|
+
if (!this.rpcRegistry) {
|
|
174
|
+
throw new Error('RpcRegistry not configured. Pass rpcRegistry to ClientNetworkConfig.');
|
|
175
|
+
}
|
|
176
|
+
if (!this.connected) {
|
|
177
|
+
this.log("Cannot send RPC: not connected");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Client-side rate limiting
|
|
181
|
+
if (!this.checkRateLimit()) {
|
|
182
|
+
this.log("Rate limit exceeded, dropping RPC");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
// Encode RPC
|
|
187
|
+
const rpcData = this.rpcRegistry.encode(rpc, data);
|
|
188
|
+
// Wrap with message type header
|
|
189
|
+
const message = new Uint8Array(1 + rpcData.byteLength);
|
|
190
|
+
message[0] = MessageType.CUSTOM;
|
|
191
|
+
message.set(rpcData, 1);
|
|
192
|
+
// Send to server
|
|
193
|
+
this.transport.send(message);
|
|
194
|
+
this.log(`Sent RPC (method: ${rpc.method})`);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
this.log(`Failed to send RPC: ${error}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Register a handler for incoming RPCs from the server (type-safe)
|
|
202
|
+
* Supports multiple handlers per RPC method
|
|
203
|
+
*
|
|
204
|
+
* @template TSchema The RPC data type
|
|
205
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
206
|
+
* @param handler Callback function to handle the RPC
|
|
207
|
+
* @returns Unsubscribe function to remove this handler
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* const MatchCountdown = defineRpc({
|
|
212
|
+
* method: 'matchCountdown',
|
|
213
|
+
* schema: { secondsRemaining: BinaryCodec.u8 }
|
|
214
|
+
* });
|
|
215
|
+
*
|
|
216
|
+
* client.onRpc(MatchCountdown, (rpc) => {
|
|
217
|
+
* console.log(`Match starting in ${rpc.secondsRemaining}s`);
|
|
218
|
+
* });
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
onRpc(rpc, handler) {
|
|
222
|
+
if (!this.rpcRegistry) {
|
|
223
|
+
throw new Error('RpcRegistry not configured. Pass rpcRegistry to ClientNetworkConfig.');
|
|
224
|
+
}
|
|
225
|
+
let handlers = this.rpcHandlers.get(rpc.method);
|
|
226
|
+
if (!handlers) {
|
|
227
|
+
handlers = [];
|
|
228
|
+
this.rpcHandlers.set(rpc.method, handlers);
|
|
229
|
+
}
|
|
230
|
+
handlers.push(handler);
|
|
231
|
+
// Return unsubscribe function
|
|
232
|
+
return () => {
|
|
233
|
+
const handlers = this.rpcHandlers.get(rpc.method);
|
|
234
|
+
if (handlers) {
|
|
235
|
+
const index = handlers.indexOf(handler);
|
|
236
|
+
if (index > -1) {
|
|
237
|
+
handlers.splice(index, 1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
152
242
|
/**
|
|
153
243
|
* Register a handler for connection events
|
|
154
244
|
*/
|
|
@@ -278,8 +368,7 @@ export class ClientNetwork {
|
|
|
278
368
|
this.log("Received heartbeat from server");
|
|
279
369
|
break;
|
|
280
370
|
case MessageType.CUSTOM:
|
|
281
|
-
|
|
282
|
-
this.log("Received custom message from server");
|
|
371
|
+
this.handleRpc(payload);
|
|
283
372
|
break;
|
|
284
373
|
default:
|
|
285
374
|
this.log(`Unknown message type: ${messageType}`);
|
|
@@ -313,6 +402,38 @@ export class ClientNetwork {
|
|
|
313
402
|
this.log(`Failed to decode snapshot: ${error}`);
|
|
314
403
|
}
|
|
315
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* Handle incoming RPC message from server
|
|
407
|
+
*/
|
|
408
|
+
handleRpc(data) {
|
|
409
|
+
if (!this.rpcRegistry) {
|
|
410
|
+
this.log("Received RPC but RpcRegistry not configured");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
// Decode using RPC registry (returns { method, data })
|
|
415
|
+
const decoded = this.rpcRegistry.decode(data);
|
|
416
|
+
this.log(`Received RPC (method: ${decoded.method})`);
|
|
417
|
+
// Call all method-specific handlers if registered
|
|
418
|
+
const handlers = this.rpcHandlers.get(decoded.method);
|
|
419
|
+
if (handlers && handlers.length > 0) {
|
|
420
|
+
for (const handler of handlers) {
|
|
421
|
+
try {
|
|
422
|
+
handler(decoded.data);
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
this.log(`Error in RPC handler: ${error}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
this.log(`No handler registered for RPC method: ${decoded.method}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
this.log(`Failed to decode RPC: ${error}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
316
437
|
/**
|
|
317
438
|
* Handle disconnection from server
|
|
318
439
|
*/
|
package/dist/net/server.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { IntentRegistry } from "../protocol/intent/intent-registry";
|
|
|
2
2
|
import type { SnapshotRegistry } from "../protocol/snapshot/snapshot-registry";
|
|
3
3
|
import type { Snapshot } from "../protocol/snapshot/snapshot";
|
|
4
4
|
import type { Intent } from "../protocol/intent/intent";
|
|
5
|
+
import type { RpcRegistry } from "../protocol/rpc/rpc-registry";
|
|
6
|
+
import type { DefinedRpc } from "../protocol/rpc/rpc";
|
|
5
7
|
import { MessagePriority, type PeerState, type ServerTransportAdapter, type TransportAdapter, type NetworkConfig } from "./types";
|
|
6
8
|
import { DefinedIntent } from "../protocol";
|
|
7
9
|
/**
|
|
@@ -14,6 +16,8 @@ export interface ServerNetworkConfig<TPeer extends TransportAdapter, TSnapshots>
|
|
|
14
16
|
intentRegistry: IntentRegistry;
|
|
15
17
|
/** Factory to create per-peer snapshot registries */
|
|
16
18
|
createPeerSnapshotRegistry: () => SnapshotRegistry<TSnapshots>;
|
|
19
|
+
/** RPC registry for bidirectional remote procedure calls (optional) */
|
|
20
|
+
rpcRegistry?: RpcRegistry;
|
|
17
21
|
/** Network configuration */
|
|
18
22
|
config?: NetworkConfig;
|
|
19
23
|
}
|
|
@@ -70,6 +74,7 @@ export declare class ServerNetwork<TPeer extends TransportAdapter = TransportAda
|
|
|
70
74
|
private transport;
|
|
71
75
|
private intentRegistry;
|
|
72
76
|
private createPeerSnapshotRegistry;
|
|
77
|
+
private rpcRegistry?;
|
|
73
78
|
private config;
|
|
74
79
|
/** Per-peer state tracking */
|
|
75
80
|
private peers;
|
|
@@ -83,6 +88,8 @@ export declare class ServerNetwork<TPeer extends TransportAdapter = TransportAda
|
|
|
83
88
|
private intentHandlers;
|
|
84
89
|
/** Global intent handler called for ALL intents before specific handlers */
|
|
85
90
|
private anyIntentHandlers;
|
|
91
|
+
/** RPC method handlers: method -> handler[] (supports multiple handlers) */
|
|
92
|
+
private rpcHandlers;
|
|
86
93
|
/** Connection lifecycle handlers */
|
|
87
94
|
private connectionHandlers;
|
|
88
95
|
private disconnectionHandlers;
|
|
@@ -133,6 +140,62 @@ export declare class ServerNetwork<TPeer extends TransportAdapter = TransportAda
|
|
|
133
140
|
* Register a handler for disconnections
|
|
134
141
|
*/
|
|
135
142
|
onDisconnection(handler: (peerId: string) => void): void;
|
|
143
|
+
/**
|
|
144
|
+
* Send an RPC to a specific peer (type-safe)
|
|
145
|
+
*
|
|
146
|
+
* @template TSchema The RPC data type
|
|
147
|
+
* @param peerId The peer to send to
|
|
148
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
149
|
+
* @param data The RPC data to send
|
|
150
|
+
* @param priority Message priority (default: NORMAL)
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* const MatchCountdown = defineRpc({
|
|
155
|
+
* method: 'matchCountdown',
|
|
156
|
+
* schema: { secondsRemaining: BinaryCodec.u8 }
|
|
157
|
+
* });
|
|
158
|
+
*
|
|
159
|
+
* server.sendRpc(peerId, MatchCountdown, { secondsRemaining: 10 });
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
sendRpc<TSchema extends Record<string, any>>(peerId: string, rpc: DefinedRpc<TSchema>, data: TSchema, priority?: MessagePriority): void;
|
|
163
|
+
/**
|
|
164
|
+
* Send an RPC to all connected peers (broadcast)
|
|
165
|
+
*
|
|
166
|
+
* @template TSchema The RPC data type
|
|
167
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
168
|
+
* @param data The RPC data to send
|
|
169
|
+
* @param priority Message priority (default: NORMAL)
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* server.sendRpcBroadcast(MatchCountdown, { secondsRemaining: 3 });
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
sendRpcBroadcast<TSchema extends Record<string, any>>(rpc: DefinedRpc<TSchema>, data: TSchema, priority?: MessagePriority): void;
|
|
177
|
+
/**
|
|
178
|
+
* Register a handler for incoming RPCs from clients (type-safe)
|
|
179
|
+
* Supports multiple handlers per RPC method
|
|
180
|
+
*
|
|
181
|
+
* @template TSchema The RPC data type
|
|
182
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
183
|
+
* @param handler Callback function to handle the RPC
|
|
184
|
+
* @returns Unsubscribe function to remove this handler
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* const BuyItem = defineRpc({
|
|
189
|
+
* method: 'buyItem',
|
|
190
|
+
* schema: { itemId: BinaryCodec.string(32) }
|
|
191
|
+
* });
|
|
192
|
+
*
|
|
193
|
+
* server.onRpc(BuyItem, (peerId, rpc) => {
|
|
194
|
+
* console.log(`${peerId} wants to buy ${rpc.itemId}`);
|
|
195
|
+
* });
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
onRpc<TSchema extends Record<string, any>>(rpc: DefinedRpc<TSchema>, handler: (peerId: string, data: TSchema) => void): () => void;
|
|
136
199
|
/**
|
|
137
200
|
* Send a snapshot to a specific peer using their dedicated snapshot registry (type-safe)
|
|
138
201
|
* @template T The specific snapshot update type
|
|
@@ -297,6 +360,10 @@ export declare class ServerNetwork<TPeer extends TransportAdapter = TransportAda
|
|
|
297
360
|
* Decode and handle an intent from a peer
|
|
298
361
|
*/
|
|
299
362
|
private handleIntent;
|
|
363
|
+
/**
|
|
364
|
+
* Handle incoming RPC message from a peer
|
|
365
|
+
*/
|
|
366
|
+
private handleRpc;
|
|
300
367
|
/**
|
|
301
368
|
* Check rate limit for a peer
|
|
302
369
|
* Returns true if message should be processed, false if rate limit exceeded
|
package/dist/net/server.js
CHANGED
|
@@ -63,6 +63,8 @@ export class ServerNetwork {
|
|
|
63
63
|
this.intentHandlers = new Map();
|
|
64
64
|
/** Global intent handler called for ALL intents before specific handlers */
|
|
65
65
|
this.anyIntentHandlers = [];
|
|
66
|
+
/** RPC method handlers: method -> handler[] (supports multiple handlers) */
|
|
67
|
+
this.rpcHandlers = new Map();
|
|
66
68
|
/** Connection lifecycle handlers */
|
|
67
69
|
this.connectionHandlers = [];
|
|
68
70
|
this.disconnectionHandlers = [];
|
|
@@ -73,6 +75,7 @@ export class ServerNetwork {
|
|
|
73
75
|
this.transport = config.transport;
|
|
74
76
|
this.intentRegistry = config.intentRegistry;
|
|
75
77
|
this.createPeerSnapshotRegistry = config.createPeerSnapshotRegistry;
|
|
78
|
+
this.rpcRegistry = config.rpcRegistry;
|
|
76
79
|
this.config = {
|
|
77
80
|
maxMessageSize: config.config?.maxMessageSize ?? 65536,
|
|
78
81
|
debug: config.config?.debug ?? false,
|
|
@@ -173,6 +176,129 @@ export class ServerNetwork {
|
|
|
173
176
|
onDisconnection(handler) {
|
|
174
177
|
this.disconnectionHandlers.push(handler);
|
|
175
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Send an RPC to a specific peer (type-safe)
|
|
181
|
+
*
|
|
182
|
+
* @template TSchema The RPC data type
|
|
183
|
+
* @param peerId The peer to send to
|
|
184
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
185
|
+
* @param data The RPC data to send
|
|
186
|
+
* @param priority Message priority (default: NORMAL)
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* const MatchCountdown = defineRpc({
|
|
191
|
+
* method: 'matchCountdown',
|
|
192
|
+
* schema: { secondsRemaining: BinaryCodec.u8 }
|
|
193
|
+
* });
|
|
194
|
+
*
|
|
195
|
+
* server.sendRpc(peerId, MatchCountdown, { secondsRemaining: 10 });
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
sendRpc(peerId, rpc, data, priority = MessagePriority.NORMAL) {
|
|
199
|
+
if (!this.rpcRegistry) {
|
|
200
|
+
throw new Error('RpcRegistry not configured. Pass rpcRegistry to ServerNetworkConfig.');
|
|
201
|
+
}
|
|
202
|
+
const peer = this.peers.get(peerId);
|
|
203
|
+
if (!peer) {
|
|
204
|
+
this.log(`Cannot send RPC to unknown peer: ${peerId}`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
// Encode RPC
|
|
209
|
+
const rpcData = this.rpcRegistry.encode(rpc, data);
|
|
210
|
+
// Wrap with message type header (use pool if enabled)
|
|
211
|
+
let message;
|
|
212
|
+
if (this.messagePool) {
|
|
213
|
+
message = this.messagePool.wrap(MessageType.CUSTOM, rpcData);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
message = new Uint8Array(1 + rpcData.byteLength);
|
|
217
|
+
message[0] = MessageType.CUSTOM;
|
|
218
|
+
message.set(rpcData, 1);
|
|
219
|
+
}
|
|
220
|
+
// Check backpressure and queue if necessary
|
|
221
|
+
if (peer.isBackpressured || peer.sendQueue.length > 0) {
|
|
222
|
+
// Peer is experiencing backpressure, queue the message with priority
|
|
223
|
+
this.queueMessage(peer, message, priority);
|
|
224
|
+
// Release pooled buffer since we copied it in queueMessage
|
|
225
|
+
if (this.messagePool) {
|
|
226
|
+
this.messagePool.release(message);
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Try to send immediately
|
|
231
|
+
this.sendMessageToPeer(peer, message);
|
|
232
|
+
// Release pooled buffer after send
|
|
233
|
+
if (this.messagePool) {
|
|
234
|
+
this.messagePool.release(message);
|
|
235
|
+
}
|
|
236
|
+
this.log(`Sent RPC (method: ${rpc.method}) to peer: ${peerId}`);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
this.log(`Failed to send RPC to peer ${peerId}: ${error}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Send an RPC to all connected peers (broadcast)
|
|
244
|
+
*
|
|
245
|
+
* @template TSchema The RPC data type
|
|
246
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
247
|
+
* @param data The RPC data to send
|
|
248
|
+
* @param priority Message priority (default: NORMAL)
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* server.sendRpcBroadcast(MatchCountdown, { secondsRemaining: 3 });
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
sendRpcBroadcast(rpc, data, priority = MessagePriority.NORMAL) {
|
|
256
|
+
for (const peerId of this.getPeerIds()) {
|
|
257
|
+
this.sendRpc(peerId, rpc, data, priority);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Register a handler for incoming RPCs from clients (type-safe)
|
|
262
|
+
* Supports multiple handlers per RPC method
|
|
263
|
+
*
|
|
264
|
+
* @template TSchema The RPC data type
|
|
265
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
266
|
+
* @param handler Callback function to handle the RPC
|
|
267
|
+
* @returns Unsubscribe function to remove this handler
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```ts
|
|
271
|
+
* const BuyItem = defineRpc({
|
|
272
|
+
* method: 'buyItem',
|
|
273
|
+
* schema: { itemId: BinaryCodec.string(32) }
|
|
274
|
+
* });
|
|
275
|
+
*
|
|
276
|
+
* server.onRpc(BuyItem, (peerId, rpc) => {
|
|
277
|
+
* console.log(`${peerId} wants to buy ${rpc.itemId}`);
|
|
278
|
+
* });
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
onRpc(rpc, handler) {
|
|
282
|
+
if (!this.rpcRegistry) {
|
|
283
|
+
throw new Error('RpcRegistry not configured. Pass rpcRegistry to ServerNetworkConfig.');
|
|
284
|
+
}
|
|
285
|
+
let handlers = this.rpcHandlers.get(rpc.method);
|
|
286
|
+
if (!handlers) {
|
|
287
|
+
handlers = [];
|
|
288
|
+
this.rpcHandlers.set(rpc.method, handlers);
|
|
289
|
+
}
|
|
290
|
+
handlers.push(handler);
|
|
291
|
+
// Return unsubscribe function
|
|
292
|
+
return () => {
|
|
293
|
+
const handlers = this.rpcHandlers.get(rpc.method);
|
|
294
|
+
if (handlers) {
|
|
295
|
+
const index = handlers.indexOf(handler);
|
|
296
|
+
if (index > -1) {
|
|
297
|
+
handlers.splice(index, 1);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
176
302
|
/**
|
|
177
303
|
* Send a snapshot to a specific peer using their dedicated snapshot registry (type-safe)
|
|
178
304
|
* @template T The specific snapshot update type
|
|
@@ -685,8 +811,7 @@ export class ServerNetwork {
|
|
|
685
811
|
this.log(`Received heartbeat from peer ${peerId}`);
|
|
686
812
|
break;
|
|
687
813
|
case MessageType.CUSTOM:
|
|
688
|
-
|
|
689
|
-
this.log(`Received custom message from peer ${peerId}`);
|
|
814
|
+
this.handleRpc(peerId, payload);
|
|
690
815
|
break;
|
|
691
816
|
default:
|
|
692
817
|
this.log(`Unknown message type ${messageType} from peer ${peerId}`);
|
|
@@ -737,6 +862,43 @@ export class ServerNetwork {
|
|
|
737
862
|
this.log(`Failed to decode intent from peer ${peerId}: ${error}`);
|
|
738
863
|
}
|
|
739
864
|
}
|
|
865
|
+
/**
|
|
866
|
+
* Handle incoming RPC message from a peer
|
|
867
|
+
*/
|
|
868
|
+
handleRpc(peerId, data) {
|
|
869
|
+
if (!this.rpcRegistry) {
|
|
870
|
+
this.log("Received RPC but RpcRegistry not configured");
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
// Rate limiting check
|
|
874
|
+
if (!this.checkRateLimit(peerId)) {
|
|
875
|
+
this.log(`Rate limit exceeded for peer ${peerId}, dropping RPC`);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
// Decode using RPC registry (returns { method, data })
|
|
880
|
+
const decoded = this.rpcRegistry.decode(data);
|
|
881
|
+
this.log(`Received RPC (method: ${decoded.method}) from peer ${peerId}`);
|
|
882
|
+
// Call all method-specific handlers if registered
|
|
883
|
+
const handlers = this.rpcHandlers.get(decoded.method);
|
|
884
|
+
if (handlers && handlers.length > 0) {
|
|
885
|
+
for (const handler of handlers) {
|
|
886
|
+
try {
|
|
887
|
+
handler(peerId, decoded.data);
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
this.log(`Error in RPC handler: ${error}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
this.log(`No handler registered for RPC method: ${decoded.method}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
this.log(`Failed to decode RPC from peer ${peerId}: ${error}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
740
902
|
/**
|
|
741
903
|
* Check rate limit for a peer
|
|
742
904
|
* Returns true if message should be processed, false if rate limit exceeded
|
package/dist/protocol/index.d.ts
CHANGED
package/dist/protocol/index.js
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { DefinedRpc } from "./rpc";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for defining an RPC type.
|
|
4
|
+
* @template S The schema type describing the RPC's data fields
|
|
5
|
+
*/
|
|
6
|
+
export interface RpcDefinition<S extends Record<string, any>> {
|
|
7
|
+
/** Method name for this RPC (must be unique) */
|
|
8
|
+
method: string;
|
|
9
|
+
/** Schema describing the RPC's data fields */
|
|
10
|
+
schema: S;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Infers the TypeScript type from an RPC schema.
|
|
14
|
+
* @template S The schema type
|
|
15
|
+
*/
|
|
16
|
+
export type InferRpcType<S extends Record<string, any>> = {
|
|
17
|
+
[P in keyof S]: S[P] extends {
|
|
18
|
+
read(dv: DataView, o: number): infer R;
|
|
19
|
+
} ? R : never;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Define a type-safe RPC with automatic schema generation.
|
|
23
|
+
*
|
|
24
|
+
* RPCs are bidirectional one-off events/commands for:
|
|
25
|
+
* - Meta-game events (achievements, notifications)
|
|
26
|
+
* - Match lifecycle (countdown, results)
|
|
27
|
+
* - Request/response patterns
|
|
28
|
+
* - System announcements
|
|
29
|
+
*
|
|
30
|
+
* NOT for game state synchronization (use Snapshots) or player inputs (use Intents)
|
|
31
|
+
*
|
|
32
|
+
* @template S The schema type
|
|
33
|
+
* @param definition RPC configuration with method and schema
|
|
34
|
+
* @returns A DefinedRpc object with method and codec
|
|
35
|
+
*
|
|
36
|
+
* @example Server → Client RPC
|
|
37
|
+
* ```ts
|
|
38
|
+
* const MatchCountdown = defineRpc({
|
|
39
|
+
* method: 'matchCountdown',
|
|
40
|
+
* schema: {
|
|
41
|
+
* secondsRemaining: BinaryCodec.u8,
|
|
42
|
+
* }
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // Server sends
|
|
46
|
+
* server.sendRpcBroadcast(MatchCountdown, { secondsRemaining: 10 });
|
|
47
|
+
*
|
|
48
|
+
* // Client receives
|
|
49
|
+
* client.onRpc(MatchCountdown, (rpc) => {
|
|
50
|
+
* console.log(`Match starting in ${rpc.secondsRemaining}s`);
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example Client → Server RPC
|
|
55
|
+
* ```ts
|
|
56
|
+
* const BuyItem = defineRpc({
|
|
57
|
+
* method: 'buyItem',
|
|
58
|
+
* schema: {
|
|
59
|
+
* itemId: BinaryCodec.string(32),
|
|
60
|
+
* }
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* // Client sends
|
|
64
|
+
* client.sendRpc(BuyItem, { itemId: 'long_sword' });
|
|
65
|
+
*
|
|
66
|
+
* // Server receives
|
|
67
|
+
* server.onRpc(BuyItem, (peerId, rpc) => {
|
|
68
|
+
* console.log(`${peerId} wants to buy ${rpc.itemId}`);
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare function defineRpc<S extends Record<string, any>>(definition: RpcDefinition<S>): DefinedRpc<InferRpcType<S>>;
|