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,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
+ }