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,168 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { RpcRegistry } from "./rpc-registry";
|
|
3
|
+
import { defineRpc } from "./define-rpc";
|
|
4
|
+
import { BinaryCodec } from "../../core/binary-codec";
|
|
5
|
+
|
|
6
|
+
// Define RPCs for testing
|
|
7
|
+
const MockRpc = defineRpc({
|
|
8
|
+
method: 'mockRpc',
|
|
9
|
+
schema: {
|
|
10
|
+
value: BinaryCodec.u32,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const AnotherRpc = defineRpc({
|
|
15
|
+
method: 'anotherRpc',
|
|
16
|
+
schema: {
|
|
17
|
+
data: BinaryCodec.string(64),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
type MockRpcType = typeof MockRpc._type;
|
|
22
|
+
type AnotherRpcType = typeof AnotherRpc._type;
|
|
23
|
+
|
|
24
|
+
describe("RpcRegistry", () => {
|
|
25
|
+
let registry: RpcRegistry;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
registry = new RpcRegistry();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("register", () => {
|
|
32
|
+
it("should register an RPC", () => {
|
|
33
|
+
registry.register(MockRpc);
|
|
34
|
+
expect(registry.has('mockRpc')).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should throw error when registering duplicate method", () => {
|
|
38
|
+
registry.register(MockRpc);
|
|
39
|
+
expect(() => registry.register(MockRpc)).toThrow(
|
|
40
|
+
'RPC "mockRpc" is already registered'
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should allow registering multiple different RPCs", () => {
|
|
45
|
+
registry.register(MockRpc);
|
|
46
|
+
registry.register(AnotherRpc);
|
|
47
|
+
expect(registry.has('mockRpc')).toBe(true);
|
|
48
|
+
expect(registry.has('anotherRpc')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should assign sequential method IDs", () => {
|
|
52
|
+
registry.register(MockRpc);
|
|
53
|
+
registry.register(AnotherRpc);
|
|
54
|
+
|
|
55
|
+
expect(registry.getMethodId('mockRpc')).toBe(0);
|
|
56
|
+
expect(registry.getMethodId('anotherRpc')).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("encode", () => {
|
|
61
|
+
it("should encode an RPC using registered codec", () => {
|
|
62
|
+
registry.register(MockRpc);
|
|
63
|
+
const data: MockRpcType = { value: 42 };
|
|
64
|
+
const buf = registry.encode(MockRpc, data);
|
|
65
|
+
|
|
66
|
+
expect(buf).toBeInstanceOf(Uint8Array);
|
|
67
|
+
expect(buf.byteLength).toBeGreaterThan(2); // At least methodId (2 bytes) + data
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should throw error when encoding unregistered RPC", () => {
|
|
71
|
+
const data: MockRpcType = { value: 42 };
|
|
72
|
+
expect(() => registry.encode(MockRpc, data)).toThrow(
|
|
73
|
+
'RPC "mockRpc" is not registered'
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should encode different RPC types correctly", () => {
|
|
78
|
+
registry.register(MockRpc);
|
|
79
|
+
registry.register(AnotherRpc);
|
|
80
|
+
|
|
81
|
+
const data1: MockRpcType = { value: 42 };
|
|
82
|
+
const data2: AnotherRpcType = { data: "test" };
|
|
83
|
+
|
|
84
|
+
const buf1 = registry.encode(MockRpc, data1);
|
|
85
|
+
const buf2 = registry.encode(AnotherRpc, data2);
|
|
86
|
+
|
|
87
|
+
expect(buf1).toBeInstanceOf(Uint8Array);
|
|
88
|
+
expect(buf2).toBeInstanceOf(Uint8Array);
|
|
89
|
+
expect(buf1[0]).not.toBe(buf2[0]); // Different method IDs
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("decode", () => {
|
|
94
|
+
it("should decode an RPC using registered codec", () => {
|
|
95
|
+
registry.register(MockRpc);
|
|
96
|
+
const data: MockRpcType = { value: 42 };
|
|
97
|
+
const encoded = registry.encode(MockRpc, data);
|
|
98
|
+
const decoded = registry.decode(encoded);
|
|
99
|
+
|
|
100
|
+
expect(decoded.method).toBe('mockRpc');
|
|
101
|
+
expect(decoded.data.value).toBe(42);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should throw error when decoding unknown method ID", () => {
|
|
105
|
+
// Create buffer with invalid method ID
|
|
106
|
+
const buf = new Uint8Array([0xFF, 0xFF]);
|
|
107
|
+
|
|
108
|
+
expect(() => registry.decode(buf)).toThrow(
|
|
109
|
+
'Unknown RPC method ID: 65535'
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should throw error when buffer is too small", () => {
|
|
114
|
+
const buf = new Uint8Array([0x00]); // Only 1 byte, needs at least 2
|
|
115
|
+
|
|
116
|
+
expect(() => registry.decode(buf)).toThrow(
|
|
117
|
+
'Buffer too small for RPC message'
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should roundtrip encode/decode correctly", () => {
|
|
122
|
+
registry.register(MockRpc);
|
|
123
|
+
registry.register(AnotherRpc);
|
|
124
|
+
|
|
125
|
+
const data1: MockRpcType = { value: 12345 };
|
|
126
|
+
const data2: AnotherRpcType = { data: "hello world" };
|
|
127
|
+
|
|
128
|
+
const encoded1 = registry.encode(MockRpc, data1);
|
|
129
|
+
const encoded2 = registry.encode(AnotherRpc, data2);
|
|
130
|
+
|
|
131
|
+
const decoded1 = registry.decode(encoded1);
|
|
132
|
+
const decoded2 = registry.decode(encoded2);
|
|
133
|
+
|
|
134
|
+
expect(decoded1.method).toBe('mockRpc');
|
|
135
|
+
expect(decoded1.data.value).toBe(12345);
|
|
136
|
+
|
|
137
|
+
expect(decoded2.method).toBe('anotherRpc');
|
|
138
|
+
expect(decoded2.data.data).toBe('hello world');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("getMethods", () => {
|
|
143
|
+
it("should return empty array when no RPCs registered", () => {
|
|
144
|
+
expect(registry.getMethods()).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should return all registered method names", () => {
|
|
148
|
+
registry.register(MockRpc);
|
|
149
|
+
registry.register(AnotherRpc);
|
|
150
|
+
|
|
151
|
+
const methods = registry.getMethods();
|
|
152
|
+
expect(methods).toContain('mockRpc');
|
|
153
|
+
expect(methods).toContain('anotherRpc');
|
|
154
|
+
expect(methods.length).toBe(2);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("getMethodId", () => {
|
|
159
|
+
it("should return undefined for unregistered method", () => {
|
|
160
|
+
expect(registry.getMethodId('unknown')).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should return correct method ID", () => {
|
|
164
|
+
registry.register(MockRpc);
|
|
165
|
+
expect(registry.getMethodId('mockRpc')).toBe(0);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { DefinedRpc, RpcCodec } from './rpc';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Registry for managing RPC definitions with binary encoding/decoding
|
|
5
|
+
*
|
|
6
|
+
* Maps RPC method names to numeric IDs for efficient binary protocol:
|
|
7
|
+
* - Method names are assigned sequential IDs (0, 1, 2, ...)
|
|
8
|
+
* - Binary format: [methodId: u16][data: variable]
|
|
9
|
+
* - Supports bidirectional RPCs (client ↔ server)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const registry = new RpcRegistry();
|
|
14
|
+
*
|
|
15
|
+
* // Register RPCs
|
|
16
|
+
* registry.register(MatchCountdown);
|
|
17
|
+
* registry.register(BuyItem);
|
|
18
|
+
*
|
|
19
|
+
* // Encode RPC to binary
|
|
20
|
+
* const binary = registry.encode(MatchCountdown, { secondsRemaining: 10 });
|
|
21
|
+
*
|
|
22
|
+
* // Decode RPC from binary
|
|
23
|
+
* const { method, data } = registry.decode(binary);
|
|
24
|
+
* console.log(method); // 'matchCountdown'
|
|
25
|
+
* console.log(data.secondsRemaining); // 10
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class RpcRegistry {
|
|
29
|
+
private codecs = new Map<string, RpcCodec<any>>();
|
|
30
|
+
private methodToId = new Map<string, number>();
|
|
31
|
+
private idToMethod = new Map<number, string>();
|
|
32
|
+
private nextId = 0;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register an RPC definition
|
|
36
|
+
*
|
|
37
|
+
* @param rpc The RPC definition created by defineRpc()
|
|
38
|
+
* @throws Error if method is already registered
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* const MatchCountdown = defineRpc({
|
|
43
|
+
* method: 'matchCountdown',
|
|
44
|
+
* schema: { secondsRemaining: BinaryCodec.u8 }
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* registry.register(MatchCountdown);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
register<TSchema extends Record<string, any>>(rpc: DefinedRpc<TSchema>): void {
|
|
51
|
+
if (this.codecs.has(rpc.method)) {
|
|
52
|
+
throw new Error(`RPC "${rpc.method}" is already registered`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const id = this.nextId++;
|
|
56
|
+
this.codecs.set(rpc.method, rpc.codec);
|
|
57
|
+
this.methodToId.set(rpc.method, id);
|
|
58
|
+
this.idToMethod.set(id, rpc.method);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Encode an RPC to binary format
|
|
63
|
+
*
|
|
64
|
+
* Binary format: [methodId: u16][data: variable]
|
|
65
|
+
*
|
|
66
|
+
* @param rpc The RPC definition
|
|
67
|
+
* @param data The RPC data to encode
|
|
68
|
+
* @returns Encoded binary data
|
|
69
|
+
* @throws Error if RPC is not registered
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const binary = registry.encode(MatchCountdown, {
|
|
74
|
+
* secondsRemaining: 10
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
encode<TSchema extends Record<string, any>>(rpc: DefinedRpc<TSchema>, data: TSchema): Uint8Array {
|
|
79
|
+
const codec = this.codecs.get(rpc.method);
|
|
80
|
+
if (!codec) {
|
|
81
|
+
throw new Error(`RPC "${rpc.method}" is not registered`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const methodId = this.methodToId.get(rpc.method)!;
|
|
85
|
+
const encodedData = codec.encode(data);
|
|
86
|
+
|
|
87
|
+
// Message format: [methodId: u16][data]
|
|
88
|
+
const buffer = new Uint8Array(2 + encodedData.byteLength);
|
|
89
|
+
new DataView(buffer.buffer).setUint16(0, methodId, true);
|
|
90
|
+
buffer.set(new Uint8Array(encodedData), 2);
|
|
91
|
+
|
|
92
|
+
return buffer;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Decode an RPC from binary format
|
|
97
|
+
*
|
|
98
|
+
* @param buffer Binary data to decode
|
|
99
|
+
* @returns Object with method name and decoded data
|
|
100
|
+
* @throws Error if method ID is unknown
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* const { method, data } = registry.decode(binary);
|
|
105
|
+
* console.log(method); // 'matchCountdown'
|
|
106
|
+
* console.log(data.secondsRemaining); // 10
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
decode(buffer: Uint8Array): { method: string; data: any } {
|
|
110
|
+
if (buffer.byteLength < 2) {
|
|
111
|
+
throw new Error('Buffer too small for RPC message');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset);
|
|
115
|
+
const methodId = view.getUint16(0, true);
|
|
116
|
+
|
|
117
|
+
const method = this.idToMethod.get(methodId);
|
|
118
|
+
if (!method) {
|
|
119
|
+
throw new Error(`Unknown RPC method ID: ${methodId}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const codec = this.codecs.get(method)!;
|
|
123
|
+
const data = codec.decode(buffer.slice(2));
|
|
124
|
+
|
|
125
|
+
return { method, data };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if an RPC method is registered
|
|
130
|
+
*
|
|
131
|
+
* @param method Method name to check
|
|
132
|
+
* @returns True if registered, false otherwise
|
|
133
|
+
*/
|
|
134
|
+
has(method: string): boolean {
|
|
135
|
+
return this.codecs.has(method);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get all registered RPC method names
|
|
140
|
+
*
|
|
141
|
+
* @returns Array of method names
|
|
142
|
+
*/
|
|
143
|
+
getMethods(): string[] {
|
|
144
|
+
return Array.from(this.codecs.keys());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get the numeric ID for a method
|
|
149
|
+
*
|
|
150
|
+
* @param method Method name
|
|
151
|
+
* @returns Method ID or undefined if not registered
|
|
152
|
+
*/
|
|
153
|
+
getMethodId(method: string): number | undefined {
|
|
154
|
+
return this.methodToId.get(method);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
/**
|
|
14
|
+
* Codec interface for encoding/decoding RPCs
|
|
15
|
+
*/
|
|
16
|
+
export interface RpcCodec<T> {
|
|
17
|
+
encode(value: T): Uint8Array;
|
|
18
|
+
decode(buf: Uint8Array): T;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Runtime RPC message structure
|
|
23
|
+
*/
|
|
24
|
+
export interface Rpc<T = unknown> {
|
|
25
|
+
method: string;
|
|
26
|
+
data: T;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Compile-time RPC definition with type safety
|
|
31
|
+
* Created by defineRpc() helper
|
|
32
|
+
*/
|
|
33
|
+
export interface DefinedRpc<TSchema extends Record<string, any>> {
|
|
34
|
+
method: string;
|
|
35
|
+
codec: RpcCodec<TSchema>;
|
|
36
|
+
_type?: TSchema; // Phantom type for inference
|
|
37
|
+
}
|