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
|
@@ -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,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
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
|
|
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
|
|
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
|
-
|
|
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
|
*/
|