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.
@@ -0,0 +1,84 @@
1
+ import { BinaryCodec } from "../../core/binary-codec";
2
+ /**
3
+ * Simple codec implementation that uses BinaryCodec for encoding/decoding.
4
+ * @template T The RPC data type
5
+ */
6
+ class RpcCodecImpl {
7
+ constructor(schema) {
8
+ this.schema = schema;
9
+ }
10
+ encode(value) {
11
+ return BinaryCodec.encode(this.schema, value);
12
+ }
13
+ decode(buf) {
14
+ // Create a target object with nil values
15
+ const target = {};
16
+ for (const key of Object.keys(this.schema)) {
17
+ const field = this.schema[key];
18
+ target[key] = field.toNil();
19
+ }
20
+ return BinaryCodec.decode(this.schema, buf, target);
21
+ }
22
+ }
23
+ /**
24
+ * Define a type-safe RPC with automatic schema generation.
25
+ *
26
+ * RPCs are bidirectional one-off events/commands for:
27
+ * - Meta-game events (achievements, notifications)
28
+ * - Match lifecycle (countdown, results)
29
+ * - Request/response patterns
30
+ * - System announcements
31
+ *
32
+ * NOT for game state synchronization (use Snapshots) or player inputs (use Intents)
33
+ *
34
+ * @template S The schema type
35
+ * @param definition RPC configuration with method and schema
36
+ * @returns A DefinedRpc object with method and codec
37
+ *
38
+ * @example Server → Client RPC
39
+ * ```ts
40
+ * const MatchCountdown = defineRpc({
41
+ * method: 'matchCountdown',
42
+ * schema: {
43
+ * secondsRemaining: BinaryCodec.u8,
44
+ * }
45
+ * });
46
+ *
47
+ * // Server sends
48
+ * server.sendRpcBroadcast(MatchCountdown, { secondsRemaining: 10 });
49
+ *
50
+ * // Client receives
51
+ * client.onRpc(MatchCountdown, (rpc) => {
52
+ * console.log(`Match starting in ${rpc.secondsRemaining}s`);
53
+ * });
54
+ * ```
55
+ *
56
+ * @example Client → Server RPC
57
+ * ```ts
58
+ * const BuyItem = defineRpc({
59
+ * method: 'buyItem',
60
+ * schema: {
61
+ * itemId: BinaryCodec.string(32),
62
+ * }
63
+ * });
64
+ *
65
+ * // Client sends
66
+ * client.sendRpc(BuyItem, { itemId: 'long_sword' });
67
+ *
68
+ * // Server receives
69
+ * server.onRpc(BuyItem, (peerId, rpc) => {
70
+ * console.log(`${peerId} wants to buy ${rpc.itemId}`);
71
+ * });
72
+ * ```
73
+ */
74
+ export function defineRpc(definition) {
75
+ // Create the schema from definition
76
+ const schema = definition.schema;
77
+ // Create the codec using BinaryCodec
78
+ const codec = new RpcCodecImpl(schema);
79
+ return {
80
+ method: definition.method,
81
+ codec,
82
+ _type: undefined, // Phantom type for inference
83
+ };
84
+ }
@@ -0,0 +1,3 @@
1
+ export * from './rpc';
2
+ export * from './define-rpc';
3
+ export * from './rpc-registry';
@@ -0,0 +1,3 @@
1
+ export * from './rpc';
2
+ export * from './define-rpc';
3
+ export * from './rpc-registry';
@@ -0,0 +1,105 @@
1
+ import type { DefinedRpc } from './rpc';
2
+ /**
3
+ * Registry for managing RPC definitions with binary encoding/decoding
4
+ *
5
+ * Maps RPC method names to numeric IDs for efficient binary protocol:
6
+ * - Method names are assigned sequential IDs (0, 1, 2, ...)
7
+ * - Binary format: [methodId: u16][data: variable]
8
+ * - Supports bidirectional RPCs (client ↔ server)
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const registry = new RpcRegistry();
13
+ *
14
+ * // Register RPCs
15
+ * registry.register(MatchCountdown);
16
+ * registry.register(BuyItem);
17
+ *
18
+ * // Encode RPC to binary
19
+ * const binary = registry.encode(MatchCountdown, { secondsRemaining: 10 });
20
+ *
21
+ * // Decode RPC from binary
22
+ * const { method, data } = registry.decode(binary);
23
+ * console.log(method); // 'matchCountdown'
24
+ * console.log(data.secondsRemaining); // 10
25
+ * ```
26
+ */
27
+ export declare class RpcRegistry {
28
+ private codecs;
29
+ private methodToId;
30
+ private idToMethod;
31
+ private nextId;
32
+ /**
33
+ * Register an RPC definition
34
+ *
35
+ * @param rpc The RPC definition created by defineRpc()
36
+ * @throws Error if method is already registered
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const MatchCountdown = defineRpc({
41
+ * method: 'matchCountdown',
42
+ * schema: { secondsRemaining: BinaryCodec.u8 }
43
+ * });
44
+ *
45
+ * registry.register(MatchCountdown);
46
+ * ```
47
+ */
48
+ register<TSchema extends Record<string, any>>(rpc: DefinedRpc<TSchema>): void;
49
+ /**
50
+ * Encode an RPC to binary format
51
+ *
52
+ * Binary format: [methodId: u16][data: variable]
53
+ *
54
+ * @param rpc The RPC definition
55
+ * @param data The RPC data to encode
56
+ * @returns Encoded binary data
57
+ * @throws Error if RPC is not registered
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const binary = registry.encode(MatchCountdown, {
62
+ * secondsRemaining: 10
63
+ * });
64
+ * ```
65
+ */
66
+ encode<TSchema extends Record<string, any>>(rpc: DefinedRpc<TSchema>, data: TSchema): Uint8Array;
67
+ /**
68
+ * Decode an RPC from binary format
69
+ *
70
+ * @param buffer Binary data to decode
71
+ * @returns Object with method name and decoded data
72
+ * @throws Error if method ID is unknown
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const { method, data } = registry.decode(binary);
77
+ * console.log(method); // 'matchCountdown'
78
+ * console.log(data.secondsRemaining); // 10
79
+ * ```
80
+ */
81
+ decode(buffer: Uint8Array): {
82
+ method: string;
83
+ data: any;
84
+ };
85
+ /**
86
+ * Check if an RPC method is registered
87
+ *
88
+ * @param method Method name to check
89
+ * @returns True if registered, false otherwise
90
+ */
91
+ has(method: string): boolean;
92
+ /**
93
+ * Get all registered RPC method names
94
+ *
95
+ * @returns Array of method names
96
+ */
97
+ getMethods(): string[];
98
+ /**
99
+ * Get the numeric ID for a method
100
+ *
101
+ * @param method Method name
102
+ * @returns Method ID or undefined if not registered
103
+ */
104
+ getMethodId(method: string): number | undefined;
105
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Registry for managing RPC definitions with binary encoding/decoding
3
+ *
4
+ * Maps RPC method names to numeric IDs for efficient binary protocol:
5
+ * - Method names are assigned sequential IDs (0, 1, 2, ...)
6
+ * - Binary format: [methodId: u16][data: variable]
7
+ * - Supports bidirectional RPCs (client ↔ server)
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const registry = new RpcRegistry();
12
+ *
13
+ * // Register RPCs
14
+ * registry.register(MatchCountdown);
15
+ * registry.register(BuyItem);
16
+ *
17
+ * // Encode RPC to binary
18
+ * const binary = registry.encode(MatchCountdown, { secondsRemaining: 10 });
19
+ *
20
+ * // Decode RPC from binary
21
+ * const { method, data } = registry.decode(binary);
22
+ * console.log(method); // 'matchCountdown'
23
+ * console.log(data.secondsRemaining); // 10
24
+ * ```
25
+ */
26
+ export class RpcRegistry {
27
+ constructor() {
28
+ this.codecs = new Map();
29
+ this.methodToId = new Map();
30
+ this.idToMethod = new Map();
31
+ this.nextId = 0;
32
+ }
33
+ /**
34
+ * Register an RPC definition
35
+ *
36
+ * @param rpc The RPC definition created by defineRpc()
37
+ * @throws Error if method is already registered
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const MatchCountdown = defineRpc({
42
+ * method: 'matchCountdown',
43
+ * schema: { secondsRemaining: BinaryCodec.u8 }
44
+ * });
45
+ *
46
+ * registry.register(MatchCountdown);
47
+ * ```
48
+ */
49
+ register(rpc) {
50
+ if (this.codecs.has(rpc.method)) {
51
+ throw new Error(`RPC "${rpc.method}" is already registered`);
52
+ }
53
+ const id = this.nextId++;
54
+ this.codecs.set(rpc.method, rpc.codec);
55
+ this.methodToId.set(rpc.method, id);
56
+ this.idToMethod.set(id, rpc.method);
57
+ }
58
+ /**
59
+ * Encode an RPC to binary format
60
+ *
61
+ * Binary format: [methodId: u16][data: variable]
62
+ *
63
+ * @param rpc The RPC definition
64
+ * @param data The RPC data to encode
65
+ * @returns Encoded binary data
66
+ * @throws Error if RPC is not registered
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const binary = registry.encode(MatchCountdown, {
71
+ * secondsRemaining: 10
72
+ * });
73
+ * ```
74
+ */
75
+ encode(rpc, data) {
76
+ const codec = this.codecs.get(rpc.method);
77
+ if (!codec) {
78
+ throw new Error(`RPC "${rpc.method}" is not registered`);
79
+ }
80
+ const methodId = this.methodToId.get(rpc.method);
81
+ const encodedData = codec.encode(data);
82
+ // Message format: [methodId: u16][data]
83
+ const buffer = new Uint8Array(2 + encodedData.byteLength);
84
+ new DataView(buffer.buffer).setUint16(0, methodId, true);
85
+ buffer.set(new Uint8Array(encodedData), 2);
86
+ return buffer;
87
+ }
88
+ /**
89
+ * Decode an RPC from binary format
90
+ *
91
+ * @param buffer Binary data to decode
92
+ * @returns Object with method name and decoded data
93
+ * @throws Error if method ID is unknown
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const { method, data } = registry.decode(binary);
98
+ * console.log(method); // 'matchCountdown'
99
+ * console.log(data.secondsRemaining); // 10
100
+ * ```
101
+ */
102
+ decode(buffer) {
103
+ if (buffer.byteLength < 2) {
104
+ throw new Error('Buffer too small for RPC message');
105
+ }
106
+ const view = new DataView(buffer.buffer, buffer.byteOffset);
107
+ const methodId = view.getUint16(0, true);
108
+ const method = this.idToMethod.get(methodId);
109
+ if (!method) {
110
+ throw new Error(`Unknown RPC method ID: ${methodId}`);
111
+ }
112
+ const codec = this.codecs.get(method);
113
+ const data = codec.decode(buffer.slice(2));
114
+ return { method, data };
115
+ }
116
+ /**
117
+ * Check if an RPC method is registered
118
+ *
119
+ * @param method Method name to check
120
+ * @returns True if registered, false otherwise
121
+ */
122
+ has(method) {
123
+ return this.codecs.has(method);
124
+ }
125
+ /**
126
+ * Get all registered RPC method names
127
+ *
128
+ * @returns Array of method names
129
+ */
130
+ getMethods() {
131
+ return Array.from(this.codecs.keys());
132
+ }
133
+ /**
134
+ * Get the numeric ID for a method
135
+ *
136
+ * @param method Method name
137
+ * @returns Method ID or undefined if not registered
138
+ */
139
+ getMethodId(method) {
140
+ return this.methodToId.get(method);
141
+ }
142
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * RPC (Remote Procedure Call) type definitions
3
+ *
4
+ * RPCs are bidirectional one-off events/commands for:
5
+ * - Meta-game events (achievements, notifications)
6
+ * - Match lifecycle (countdown, results)
7
+ * - Request/response patterns
8
+ * - System announcements
9
+ *
10
+ * NOT for game state synchronization (use Snapshots) or player inputs (use Intents)
11
+ */
12
+ /**
13
+ * Codec interface for encoding/decoding RPCs
14
+ */
15
+ export interface RpcCodec<T> {
16
+ encode(value: T): Uint8Array;
17
+ decode(buf: Uint8Array): T;
18
+ }
19
+ /**
20
+ * Runtime RPC message structure
21
+ */
22
+ export interface Rpc<T = unknown> {
23
+ method: string;
24
+ data: T;
25
+ }
26
+ /**
27
+ * Compile-time RPC definition with type safety
28
+ * Created by defineRpc() helper
29
+ */
30
+ export interface DefinedRpc<TSchema extends Record<string, any>> {
31
+ method: string;
32
+ codec: RpcCodec<TSchema>;
33
+ _type?: TSchema;
34
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * RPC (Remote Procedure Call) type definitions
3
+ *
4
+ * RPCs are bidirectional one-off events/commands for:
5
+ * - Meta-game events (achievements, notifications)
6
+ * - Match lifecycle (countdown, results)
7
+ * - Request/response patterns
8
+ * - System announcements
9
+ *
10
+ * NOT for game state synchronization (use Snapshots) or player inputs (use Intents)
11
+ */
12
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "murow",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "A lightweight TypeScript game engine for server-authoritative multiplayer games",
5
5
  "main": "dist/core.js",
6
6
  "module": "dist/core.esm.js",
package/src/net/README.md CHANGED
@@ -18,8 +18,9 @@ Transport-agnostic networking layer for multiplayer games. Provides generic clie
18
18
  - Custom transports
19
19
 
20
20
  - **Type-Safe Protocol** - Integrates with `@mococa/protocol`:
21
- - Intent encoding/decoding
22
- - Snapshot encoding/decoding
21
+ - Intent encoding/decoding (client inputs)
22
+ - Snapshot encoding/decoding (state sync)
23
+ - RPC encoding/decoding (one-off events)
23
24
  - Binary serialization
24
25
 
25
26
  - **Connection Lifecycle** - Built-in handling for:
@@ -34,15 +35,21 @@ Transport-agnostic networking layer for multiplayer games. Provides generic clie
34
35
  │ Game Client │ │ Game Server │
35
36
  └──────┬──────┘ └──────┬──────┘
36
37
  │ │
37
- │ Intents (MoveIntent, etc)
38
+ │ Intents (MoveIntent)
38
39
  ├─────────────────────────────────>│
39
40
  │ │
40
41
  │ Snapshots (GameState) │
41
42
  │<─────────────────────────────────┤
42
43
  │ │
44
+ │ RPCs (BuyItem) │
45
+ ├─────────────────────────────────>│
46
+ │ │
47
+ │ RPCs (MatchCountdown) │
48
+ │<─────────────────────────────────┤
49
+ │ │
43
50
  ▼ ▼
44
51
  ┌─────────────┐ ┌─────────────┐
45
- │ ClientNetwork ServerNetwork
52
+ │ ClientNetwork│ │ServerNetwork│
46
53
  │ Class │ │ Class │
47
54
  └──────┬──────┘ └──────┬──────┘
48
55
  │ │
@@ -167,6 +174,74 @@ client.sendIntent({
167
174
  });
168
175
  ```
169
176
 
177
+ ## RPCs (Remote Procedure Calls)
178
+
179
+ For one-off events that don't fit intents (inputs) or snapshots (state sync), use RPCs. See [Protocol README](../protocol/README.md#rpcs-remote-procedure-calls) for full documentation.
180
+
181
+ ### Quick Example
182
+
183
+ ```typescript
184
+ import { defineRpc, RpcRegistry } from '@mococa/gamedev-utils';
185
+
186
+ // Define RPCs
187
+ const MatchCountdown = defineRpc({
188
+ method: 'matchCountdown',
189
+ schema: {
190
+ secondsRemaining: BinaryCodec.u8,
191
+ }
192
+ });
193
+
194
+ const BuyItem = defineRpc({
195
+ method: 'buyItem',
196
+ schema: {
197
+ itemId: BinaryCodec.string(32),
198
+ }
199
+ });
200
+
201
+ // Register RPCs
202
+ const rpcRegistry = new RpcRegistry();
203
+ rpcRegistry.register(MatchCountdown);
204
+ rpcRegistry.register(BuyItem);
205
+
206
+ // Add to client/server config
207
+ const client = new ClientNetwork({
208
+ transport,
209
+ intentRegistry,
210
+ snapshotRegistry,
211
+ rpcRegistry, // ← Optional
212
+ });
213
+
214
+ // Client: Send RPC to server
215
+ client.sendRpc(BuyItem, { itemId: 'long_sword' });
216
+
217
+ // Client: Receive RPC from server
218
+ client.onRpc(MatchCountdown, (rpc) => {
219
+ showCountdownUI(rpc.secondsRemaining);
220
+ });
221
+
222
+ // Server: Receive RPC from client
223
+ server.onRpc(BuyItem, (peerId, rpc) => {
224
+ handlePurchase(peerId, rpc.itemId);
225
+ });
226
+
227
+ // Server: Send RPC to specific client
228
+ server.sendRpc(peerId, MatchCountdown, { secondsRemaining: 10 });
229
+
230
+ // Server: Broadcast RPC to all clients
231
+ server.sendRpcBroadcast(MatchCountdown, { secondsRemaining: 3 });
232
+ ```
233
+
234
+ **When to use:**
235
+ - ✅ Match lifecycle events (countdown, pause, end)
236
+ - ✅ Meta-game events (achievements, notifications)
237
+ - ✅ Chat messages
238
+ - ✅ UI feedback (purchase confirmations, errors)
239
+
240
+ **When NOT to use:**
241
+ - ❌ Game state (use Snapshots)
242
+ - ❌ Player inputs (use Intents)
243
+ - ❌ Anything late joiners need to know
244
+
170
245
  ## Per-Peer Snapshot Registries
171
246
 
172
247
  The killer feature! Each peer automatically gets their own snapshot registry via the factory function, enabling powerful optimizations:
package/src/net/client.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 { MessageType, type TransportAdapter, type NetworkConfig } from "./types";
6
8
 
7
9
  /**
@@ -17,6 +19,9 @@ export interface ClientNetworkConfig<TSnapshots> {
17
19
  /** Snapshot registry for decoding server snapshots */
18
20
  snapshotRegistry: SnapshotRegistry<TSnapshots>;
19
21
 
22
+ /** RPC registry for bidirectional remote procedure calls (optional) */
23
+ rpcRegistry?: RpcRegistry;
24
+
20
25
  /** Network configuration */
21
26
  config?: NetworkConfig;
22
27
  }
@@ -47,11 +52,15 @@ export class ClientNetwork<TSnapshots = unknown> {
47
52
  private transport: TransportAdapter;
48
53
  private intentRegistry: IntentRegistry;
49
54
  private snapshotRegistry: SnapshotRegistry<TSnapshots>;
55
+ private rpcRegistry?: RpcRegistry;
50
56
  private config: Required<NetworkConfig>;
51
57
 
52
58
  /** Snapshot type handlers: type -> handler[] (supports multiple handlers) */
53
59
  private snapshotHandlers = new Map<string, Array<(snapshot: Snapshot<any>) => void>>();
54
60
 
61
+ /** RPC method handlers: method -> handler[] (supports multiple handlers) */
62
+ private rpcHandlers = new Map<string, Array<(data: any) => void>>();
63
+
55
64
  /** Connection lifecycle handlers */
56
65
  private connectHandlers: Array<() => void> = [];
57
66
  private disconnectHandlers: Array<() => void> = [];
@@ -77,6 +86,7 @@ export class ClientNetwork<TSnapshots = unknown> {
77
86
  this.transport = config.transport;
78
87
  this.intentRegistry = config.intentRegistry;
79
88
  this.snapshotRegistry = config.snapshotRegistry;
89
+ this.rpcRegistry = config.rpcRegistry;
80
90
  this.config = {
81
91
  maxMessageSize: config.config?.maxMessageSize ?? 65536,
82
92
  debug: config.config?.debug ?? false,
@@ -204,6 +214,105 @@ export class ClientNetwork<TSnapshots = unknown> {
204
214
  };
205
215
  }
206
216
 
217
+ /**
218
+ * Send an RPC to the server (type-safe)
219
+ *
220
+ * @template TSchema The RPC data type
221
+ * @param rpc The RPC definition created by defineRpc()
222
+ * @param data The RPC data to send
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * const BuyItem = defineRpc({
227
+ * method: 'buyItem',
228
+ * schema: { itemId: BinaryCodec.string(32) }
229
+ * });
230
+ *
231
+ * client.sendRpc(BuyItem, { itemId: 'long_sword' });
232
+ * ```
233
+ */
234
+ sendRpc<TSchema extends Record<string, any>>(rpc: DefinedRpc<TSchema>, data: TSchema): void {
235
+ if (!this.rpcRegistry) {
236
+ throw new Error('RpcRegistry not configured. Pass rpcRegistry to ClientNetworkConfig.');
237
+ }
238
+
239
+ if (!this.connected) {
240
+ this.log("Cannot send RPC: not connected");
241
+ return;
242
+ }
243
+
244
+ // Client-side rate limiting
245
+ if (!this.checkRateLimit()) {
246
+ this.log("Rate limit exceeded, dropping RPC");
247
+ return;
248
+ }
249
+
250
+ try {
251
+ // Encode RPC
252
+ const rpcData = this.rpcRegistry.encode(rpc, data);
253
+
254
+ // Wrap with message type header
255
+ const message = new Uint8Array(1 + rpcData.byteLength);
256
+ message[0] = MessageType.CUSTOM;
257
+ message.set(rpcData, 1);
258
+
259
+ // Send to server
260
+ this.transport.send(message);
261
+
262
+ this.log(`Sent RPC (method: ${rpc.method})`);
263
+ } catch (error) {
264
+ this.log(`Failed to send RPC: ${error}`);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Register a handler for incoming RPCs from the server (type-safe)
270
+ * Supports multiple handlers per RPC method
271
+ *
272
+ * @template TSchema The RPC data type
273
+ * @param rpc The RPC definition created by defineRpc()
274
+ * @param handler Callback function to handle the RPC
275
+ * @returns Unsubscribe function to remove this handler
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * const MatchCountdown = defineRpc({
280
+ * method: 'matchCountdown',
281
+ * schema: { secondsRemaining: BinaryCodec.u8 }
282
+ * });
283
+ *
284
+ * client.onRpc(MatchCountdown, (rpc) => {
285
+ * console.log(`Match starting in ${rpc.secondsRemaining}s`);
286
+ * });
287
+ * ```
288
+ */
289
+ onRpc<TSchema extends Record<string, any>>(
290
+ rpc: DefinedRpc<TSchema>,
291
+ handler: (data: TSchema) => void
292
+ ): () => void {
293
+ if (!this.rpcRegistry) {
294
+ throw new Error('RpcRegistry not configured. Pass rpcRegistry to ClientNetworkConfig.');
295
+ }
296
+
297
+ let handlers = this.rpcHandlers.get(rpc.method);
298
+ if (!handlers) {
299
+ handlers = [];
300
+ this.rpcHandlers.set(rpc.method, handlers);
301
+ }
302
+ handlers.push(handler as (data: any) => void);
303
+
304
+ // Return unsubscribe function
305
+ return () => {
306
+ const handlers = this.rpcHandlers.get(rpc.method);
307
+ if (handlers) {
308
+ const index = handlers.indexOf(handler as (data: any) => void);
309
+ if (index > -1) {
310
+ handlers.splice(index, 1);
311
+ }
312
+ }
313
+ };
314
+ }
315
+
207
316
  /**
208
317
  * Register a handler for connection events
209
318
  */
@@ -349,8 +458,7 @@ export class ClientNetwork<TSnapshots = unknown> {
349
458
  this.log("Received heartbeat from server");
350
459
  break;
351
460
  case MessageType.CUSTOM:
352
- // Could add custom message handlers here
353
- this.log("Received custom message from server");
461
+ this.handleRpc(payload);
354
462
  break;
355
463
  default:
356
464
  this.log(`Unknown message type: ${messageType}`);
@@ -385,6 +493,39 @@ export class ClientNetwork<TSnapshots = unknown> {
385
493
  }
386
494
  }
387
495
 
496
+ /**
497
+ * Handle incoming RPC message from server
498
+ */
499
+ private handleRpc(data: Uint8Array): void {
500
+ if (!this.rpcRegistry) {
501
+ this.log("Received RPC but RpcRegistry not configured");
502
+ return;
503
+ }
504
+
505
+ try {
506
+ // Decode using RPC registry (returns { method, data })
507
+ const decoded = this.rpcRegistry.decode(data);
508
+
509
+ this.log(`Received RPC (method: ${decoded.method})`);
510
+
511
+ // Call all method-specific handlers if registered
512
+ const handlers = this.rpcHandlers.get(decoded.method);
513
+ if (handlers && handlers.length > 0) {
514
+ for (const handler of handlers) {
515
+ try {
516
+ handler(decoded.data);
517
+ } catch (error) {
518
+ this.log(`Error in RPC handler: ${error}`);
519
+ }
520
+ }
521
+ } else {
522
+ this.log(`No handler registered for RPC method: ${decoded.method}`);
523
+ }
524
+ } catch (error) {
525
+ this.log(`Failed to decode RPC: ${error}`);
526
+ }
527
+ }
528
+
388
529
  /**
389
530
  * Handle disconnection from server
390
531
  */