murow 0.0.1

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.
Files changed (103) hide show
  1. package/README.md +61 -0
  2. package/dist/core/binary-codec/binary-codec.d.ts +159 -0
  3. package/dist/core/binary-codec/binary-codec.js +336 -0
  4. package/dist/core/binary-codec/index.d.ts +1 -0
  5. package/dist/core/binary-codec/index.js +1 -0
  6. package/dist/core/events/event-system.d.ts +71 -0
  7. package/dist/core/events/event-system.js +88 -0
  8. package/dist/core/events/index.d.ts +1 -0
  9. package/dist/core/events/index.js +1 -0
  10. package/dist/core/fixed-ticker/fixed-ticker.d.ts +105 -0
  11. package/dist/core/fixed-ticker/fixed-ticker.js +91 -0
  12. package/dist/core/fixed-ticker/index.d.ts +1 -0
  13. package/dist/core/fixed-ticker/index.js +1 -0
  14. package/dist/core/generate-id/generate-id.d.ts +21 -0
  15. package/dist/core/generate-id/generate-id.js +25 -0
  16. package/dist/core/generate-id/index.d.ts +1 -0
  17. package/dist/core/generate-id/index.js +1 -0
  18. package/dist/core/index.d.ts +8 -0
  19. package/dist/core/index.js +8 -0
  20. package/dist/core/lerp/index.d.ts +1 -0
  21. package/dist/core/lerp/index.js +1 -0
  22. package/dist/core/lerp/lerp.d.ts +40 -0
  23. package/dist/core/lerp/lerp.js +42 -0
  24. package/dist/core/navmesh/index.d.ts +1 -0
  25. package/dist/core/navmesh/index.js +1 -0
  26. package/dist/core/navmesh/navmesh.d.ts +116 -0
  27. package/dist/core/navmesh/navmesh.js +666 -0
  28. package/dist/core/pooled-codec/index.d.ts +1 -0
  29. package/dist/core/pooled-codec/index.js +1 -0
  30. package/dist/core/pooled-codec/pooled-codec.d.ts +140 -0
  31. package/dist/core/pooled-codec/pooled-codec.js +213 -0
  32. package/dist/core/prediction/index.d.ts +1 -0
  33. package/dist/core/prediction/index.js +1 -0
  34. package/dist/core/prediction/prediction.d.ts +64 -0
  35. package/dist/core/prediction/prediction.js +90 -0
  36. package/dist/core.esm.js +1 -0
  37. package/dist/core.js +1 -0
  38. package/dist/index.d.ts +16 -0
  39. package/dist/index.js +18 -0
  40. package/dist/protocol/index.d.ts +43 -0
  41. package/dist/protocol/index.js +43 -0
  42. package/dist/protocol/intent/index.d.ts +39 -0
  43. package/dist/protocol/intent/index.js +38 -0
  44. package/dist/protocol/intent/intent-registry.d.ts +54 -0
  45. package/dist/protocol/intent/intent-registry.js +73 -0
  46. package/dist/protocol/intent/intent.d.ts +12 -0
  47. package/dist/protocol/intent/intent.js +1 -0
  48. package/dist/protocol/snapshot/index.d.ts +44 -0
  49. package/dist/protocol/snapshot/index.js +43 -0
  50. package/dist/protocol/snapshot/snapshot-codec.d.ts +48 -0
  51. package/dist/protocol/snapshot/snapshot-codec.js +56 -0
  52. package/dist/protocol/snapshot/snapshot-registry.d.ts +100 -0
  53. package/dist/protocol/snapshot/snapshot-registry.js +136 -0
  54. package/dist/protocol/snapshot/snapshot.d.ts +19 -0
  55. package/dist/protocol/snapshot/snapshot.js +30 -0
  56. package/package.json +54 -0
  57. package/src/core/binary-codec/README.md +60 -0
  58. package/src/core/binary-codec/binary-codec.test.ts +300 -0
  59. package/src/core/binary-codec/binary-codec.ts +430 -0
  60. package/src/core/binary-codec/index.ts +1 -0
  61. package/src/core/events/README.md +47 -0
  62. package/src/core/events/event-system.test.ts +243 -0
  63. package/src/core/events/event-system.ts +140 -0
  64. package/src/core/events/index.ts +1 -0
  65. package/src/core/fixed-ticker/README.md +77 -0
  66. package/src/core/fixed-ticker/fixed-ticker.test.ts +151 -0
  67. package/src/core/fixed-ticker/fixed-ticker.ts +158 -0
  68. package/src/core/fixed-ticker/index.ts +1 -0
  69. package/src/core/generate-id/README.md +18 -0
  70. package/src/core/generate-id/generate-id.test.ts +79 -0
  71. package/src/core/generate-id/generate-id.ts +37 -0
  72. package/src/core/generate-id/index.ts +1 -0
  73. package/src/core/index.ts +8 -0
  74. package/src/core/lerp/README.md +79 -0
  75. package/src/core/lerp/index.ts +1 -0
  76. package/src/core/lerp/lerp.test.ts +90 -0
  77. package/src/core/lerp/lerp.ts +42 -0
  78. package/src/core/navmesh/README.md +124 -0
  79. package/src/core/navmesh/index.ts +1 -0
  80. package/src/core/navmesh/navmesh.test.ts +344 -0
  81. package/src/core/navmesh/navmesh.ts +850 -0
  82. package/src/core/pooled-codec/README.md +70 -0
  83. package/src/core/pooled-codec/index.ts +1 -0
  84. package/src/core/pooled-codec/pooled-codec.test.ts +349 -0
  85. package/src/core/pooled-codec/pooled-codec.ts +239 -0
  86. package/src/core/prediction/README.md +64 -0
  87. package/src/core/prediction/index.ts +1 -0
  88. package/src/core/prediction/prediction.test.ts +422 -0
  89. package/src/core/prediction/prediction.ts +101 -0
  90. package/src/index.ts +20 -0
  91. package/src/protocol/README.md +310 -0
  92. package/src/protocol/index.ts +44 -0
  93. package/src/protocol/intent/index.ts +40 -0
  94. package/src/protocol/intent/intent-registry.test.ts +237 -0
  95. package/src/protocol/intent/intent-registry.ts +88 -0
  96. package/src/protocol/intent/intent.ts +12 -0
  97. package/src/protocol/snapshot/index.ts +45 -0
  98. package/src/protocol/snapshot/snapshot-codec.test.ts +138 -0
  99. package/src/protocol/snapshot/snapshot-codec.ts +71 -0
  100. package/src/protocol/snapshot/snapshot-registry.test.ts +302 -0
  101. package/src/protocol/snapshot/snapshot-registry.ts +162 -0
  102. package/src/protocol/snapshot/snapshot.test.ts +76 -0
  103. package/src/protocol/snapshot/snapshot.ts +41 -0
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Base interface for all game intents.
3
+ *
4
+ * Intents represent player or AI actions that need to be processed by the game simulation.
5
+ * They are timestamped with a tick number for deterministic replay and synchronization.
6
+ */
7
+ export interface Intent {
8
+ /** The game tick at which this intent should be processed */
9
+ tick: number;
10
+ /** Numeric identifier for the intent type (used for codec lookup) */
11
+ kind: number;
12
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Snapshot system for server-to-client state updates.
3
+ *
4
+ * Snapshots are delta updates that contain:
5
+ * 1. Server tick number
6
+ * 2. Partial state updates (only what changed)
7
+ *
8
+ * They need to be:
9
+ * 1. Encoded efficiently (binary)
10
+ * 2. Sent over network
11
+ * 3. Decoded on the client
12
+ * 4. Merged into client state
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { Snapshot, SnapshotCodec, applySnapshot } from './protocol/snapshot';
17
+ * import { PooledCodec } from '../core/pooled-codec';
18
+ * import { BinaryCodec } from '../core/binary-codec';
19
+ *
20
+ * interface GameState {
21
+ * players: Record<number, { x: number; y: number }>;
22
+ * }
23
+ *
24
+ * // 1. Create codec once (reuse this instance)
25
+ * const snapshotCodec = new SnapshotCodec<GameState>(
26
+ * new PooledCodec({ players: // ... your schema })
27
+ * );
28
+ *
29
+ * // 2. Server: Encode snapshot
30
+ * const snapshot: Snapshot<GameState> = {
31
+ * tick: 100,
32
+ * updates: { players: { 1: { x: 5, y: 10 } } }
33
+ * };
34
+ * const buf = snapshotCodec.encode(snapshot);
35
+ *
36
+ * // 3. Client: Decode and apply
37
+ * const snapshot = snapshotCodec.decode(buf);
38
+ * applySnapshot(clientState, snapshot);
39
+ * ```
40
+ */
41
+
42
+ export type { Snapshot } from "./snapshot";
43
+ export { applySnapshot } from "./snapshot";
44
+ export { SnapshotCodec } from "./snapshot-codec";
45
+ export { SnapshotRegistry } from "./snapshot-registry";
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { SnapshotCodec } from "./snapshot-codec";
3
+ import { Snapshot } from "./snapshot";
4
+ import { Codec } from "./snapshot-codec";
5
+
6
+ // Simple mock codec for testing
7
+ class MockCodec<T> implements Codec<T> {
8
+ encode(value: T): Uint8Array {
9
+ const json = JSON.stringify(value);
10
+ return new TextEncoder().encode(json);
11
+ }
12
+
13
+ decode(buf: Uint8Array): T {
14
+ const json = new TextDecoder().decode(buf);
15
+ return JSON.parse(json);
16
+ }
17
+ }
18
+
19
+ interface TestState {
20
+ x: number;
21
+ y: number;
22
+ health?: number;
23
+ }
24
+
25
+ describe("SnapshotCodec", () => {
26
+ it("should encode and decode a snapshot", () => {
27
+ const codec = new SnapshotCodec<TestState>(new MockCodec());
28
+ const snapshot: Snapshot<TestState> = {
29
+ tick: 100,
30
+ updates: { x: 10, y: 20 },
31
+ };
32
+
33
+ const buf = codec.encode(snapshot);
34
+ const decoded = codec.decode(buf);
35
+
36
+ expect(decoded.tick).toBe(100);
37
+ expect(decoded.updates.x).toBe(10);
38
+ expect(decoded.updates.y).toBe(20);
39
+ });
40
+
41
+ it("should handle partial updates", () => {
42
+ const codec = new SnapshotCodec<TestState>(new MockCodec());
43
+ const snapshot: Snapshot<TestState> = {
44
+ tick: 50,
45
+ updates: { x: 5 },
46
+ };
47
+
48
+ const buf = codec.encode(snapshot);
49
+ const decoded = codec.decode(buf);
50
+
51
+ expect(decoded.tick).toBe(50);
52
+ expect(decoded.updates.x).toBe(5);
53
+ expect(decoded.updates.y).toBeUndefined();
54
+ });
55
+
56
+ it("should handle large tick numbers", () => {
57
+ const codec = new SnapshotCodec<TestState>(new MockCodec());
58
+ const snapshot: Snapshot<TestState> = {
59
+ tick: 4294967295, // Max u32
60
+ updates: { x: 1, y: 2 },
61
+ };
62
+
63
+ const buf = codec.encode(snapshot);
64
+ const decoded = codec.decode(buf);
65
+
66
+ expect(decoded.tick).toBe(4294967295);
67
+ });
68
+
69
+ it("should preserve tick in binary format", () => {
70
+ const codec = new SnapshotCodec<TestState>(new MockCodec());
71
+ const snapshot: Snapshot<TestState> = {
72
+ tick: 12345,
73
+ updates: { x: 1 },
74
+ };
75
+
76
+ const buf = codec.encode(snapshot);
77
+
78
+ // First 4 bytes should be the tick (little-endian)
79
+ const tick = new DataView(buf.buffer, buf.byteOffset).getUint32(0, true);
80
+ expect(tick).toBe(12345);
81
+ });
82
+
83
+ it("should handle empty updates", () => {
84
+ const codec = new SnapshotCodec<TestState>(new MockCodec());
85
+ const snapshot: Snapshot<TestState> = {
86
+ tick: 100,
87
+ updates: {},
88
+ };
89
+
90
+ const buf = codec.encode(snapshot);
91
+ const decoded = codec.decode(buf);
92
+
93
+ expect(decoded.tick).toBe(100);
94
+ expect(decoded.updates).toEqual({});
95
+ });
96
+
97
+ it("should handle nested state", () => {
98
+ interface NestedState {
99
+ player: {
100
+ position: { x: number; y: number };
101
+ health: number;
102
+ };
103
+ }
104
+
105
+ const codec = new SnapshotCodec<NestedState>(new MockCodec());
106
+ const snapshot: Snapshot<NestedState> = {
107
+ tick: 200,
108
+ updates: {
109
+ player: {
110
+ position: { x: 10, y: 20 },
111
+ health: 80,
112
+ },
113
+ },
114
+ };
115
+
116
+ const buf = codec.encode(snapshot);
117
+ const decoded = codec.decode(buf);
118
+
119
+ expect(decoded.tick).toBe(200);
120
+ expect(decoded.updates.player?.position.x).toBe(10);
121
+ expect(decoded.updates.player?.position.y).toBe(20);
122
+ expect(decoded.updates.player?.health).toBe(80);
123
+ });
124
+
125
+ it("should round-trip multiple times", () => {
126
+ const codec = new SnapshotCodec<TestState>(new MockCodec());
127
+ const original: Snapshot<TestState> = {
128
+ tick: 999,
129
+ updates: { x: 123, y: 456, health: 78 },
130
+ };
131
+
132
+ for (let i = 0; i < 10; i++) {
133
+ const buf = codec.encode(original);
134
+ const decoded = codec.decode(buf);
135
+ expect(decoded).toEqual(original);
136
+ }
137
+ });
138
+ });
@@ -0,0 +1,71 @@
1
+ import type { Snapshot } from "./snapshot";
2
+
3
+ /**
4
+ * Generic codec interface (users import from core/pooled-codec)
5
+ */
6
+ export interface Codec<T> {
7
+ encode(value: T): Uint8Array;
8
+ decode(buf: Uint8Array): T;
9
+ }
10
+
11
+ /**
12
+ * Codec for encoding/decoding snapshots with binary serialization.
13
+ *
14
+ * Users instantiate this once with a PooledCodec for their state schema.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { SnapshotCodec } from './protocol/snapshot';
19
+ * import { PooledCodec } from './core/pooled-codec';
20
+ * import { BinaryCodec } from './core/binary-codec';
21
+ *
22
+ * interface GameState {
23
+ * players: Record<number, { x: number; y: number }>;
24
+ * }
25
+ *
26
+ * const snapshotCodec = new SnapshotCodec<GameState>(
27
+ * new PooledCodec({ players: // schema })
28
+ * );
29
+ *
30
+ * // Encode/decode
31
+ * const buf = snapshotCodec.encode(snapshot);
32
+ * const decoded = snapshotCodec.decode(buf);
33
+ * ```
34
+ */
35
+ export class SnapshotCodec<T> {
36
+ /**
37
+ * @param updatesCodec Codec for encoding/decoding the state updates
38
+ */
39
+ constructor(private updatesCodec: Codec<Partial<T>>) {}
40
+
41
+ /**
42
+ * Encode a snapshot into binary format.
43
+ * Format: [tick: u32][updates: encoded by updatesCodec]
44
+ */
45
+ encode(snapshot: Snapshot<T>): Uint8Array {
46
+ const updatesBytes = this.updatesCodec.encode(snapshot.updates);
47
+ const buf = new Uint8Array(4 + updatesBytes.length);
48
+
49
+ // Encode tick (4 bytes, little-endian)
50
+ new DataView(buf.buffer).setUint32(0, snapshot.tick, true);
51
+
52
+ // Encode updates
53
+ buf.set(updatesBytes, 4);
54
+
55
+ return buf;
56
+ }
57
+
58
+ /**
59
+ * Decode binary data into a snapshot.
60
+ */
61
+ decode(buf: Uint8Array): Snapshot<T> {
62
+ // Decode tick (first 4 bytes)
63
+ const tick = new DataView(buf.buffer, buf.byteOffset).getUint32(0, true);
64
+
65
+ // Decode updates (remaining bytes)
66
+ const updatesBytes = buf.subarray(4);
67
+ const updates = this.updatesCodec.decode(updatesBytes);
68
+
69
+ return { tick, updates };
70
+ }
71
+ }
@@ -0,0 +1,302 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { SnapshotRegistry } from "./snapshot-registry";
3
+ import { Snapshot } from "./snapshot";
4
+
5
+ // Mock codec for testing
6
+ class MockCodec<T> {
7
+ encode(value: T): Uint8Array {
8
+ const json = JSON.stringify(value);
9
+ return new TextEncoder().encode(json);
10
+ }
11
+
12
+ decode(buf: Uint8Array): T {
13
+ const json = new TextDecoder().decode(buf);
14
+ return JSON.parse(json);
15
+ }
16
+ }
17
+
18
+ // Test update types
19
+ interface PlayerUpdate {
20
+ players: Array<{ entityId: number; x: number; y: number }>;
21
+ }
22
+
23
+ interface ScoreUpdate {
24
+ score: number;
25
+ }
26
+
27
+ interface ProjectileUpdate {
28
+ projectiles: Array<{ id: number; x: number; y: number }>;
29
+ }
30
+
31
+ type GameUpdate = PlayerUpdate | ScoreUpdate | ProjectileUpdate;
32
+
33
+ describe("SnapshotRegistry", () => {
34
+ let registry: SnapshotRegistry<GameUpdate>;
35
+
36
+ beforeEach(() => {
37
+ registry = new SnapshotRegistry<GameUpdate>();
38
+ });
39
+
40
+ describe("register", () => {
41
+ it("should register a codec for a snapshot type", () => {
42
+ registry.register("players", new MockCodec<PlayerUpdate>());
43
+ expect(registry.has("players")).toBe(true);
44
+ });
45
+
46
+ it("should throw error when registering duplicate type", () => {
47
+ registry.register("players", new MockCodec<PlayerUpdate>());
48
+ expect(() => registry.register("players", new MockCodec<PlayerUpdate>())).toThrow(
49
+ 'Snapshot type "players" is already registered'
50
+ );
51
+ });
52
+
53
+ it("should allow registering multiple types", () => {
54
+ registry.register("players", new MockCodec<PlayerUpdate>());
55
+ registry.register("score", new MockCodec<ScoreUpdate>());
56
+ registry.register("projectiles", new MockCodec<ProjectileUpdate>());
57
+
58
+ expect(registry.has("players")).toBe(true);
59
+ expect(registry.has("score")).toBe(true);
60
+ expect(registry.has("projectiles")).toBe(true);
61
+ });
62
+ });
63
+
64
+ describe("encode", () => {
65
+ beforeEach(() => {
66
+ registry.register("players", new MockCodec<PlayerUpdate>());
67
+ registry.register("score", new MockCodec<ScoreUpdate>());
68
+ });
69
+
70
+ it("should encode a snapshot with type ID", () => {
71
+ const snapshot: Snapshot<PlayerUpdate> = {
72
+ tick: 100,
73
+ updates: {
74
+ players: [
75
+ { entityId: 1, x: 10, y: 20 },
76
+ ],
77
+ },
78
+ };
79
+
80
+ const buf = registry.encode("players", snapshot);
81
+
82
+ expect(buf).toBeInstanceOf(Uint8Array);
83
+ expect(buf.length).toBeGreaterThan(5); // type(1) + tick(4) + data
84
+ expect(buf[0]).toBe(0); // First registered type gets ID 0
85
+ });
86
+
87
+ it("should encode different types with different type IDs", () => {
88
+ const playerSnapshot: Snapshot<PlayerUpdate> = {
89
+ tick: 100,
90
+ updates: { players: [{ entityId: 1, x: 10, y: 20 }] },
91
+ };
92
+
93
+ const scoreSnapshot: Snapshot<ScoreUpdate> = {
94
+ tick: 101,
95
+ updates: { score: 50 },
96
+ };
97
+
98
+ const buf1 = registry.encode("players", playerSnapshot);
99
+ const buf2 = registry.encode("score", scoreSnapshot);
100
+
101
+ expect(buf1[0]).toBe(0); // players = ID 0
102
+ expect(buf2[0]).toBe(1); // score = ID 1
103
+ });
104
+
105
+ it("should throw error when encoding unregistered type", () => {
106
+ const snapshot: Snapshot<ProjectileUpdate> = {
107
+ tick: 100,
108
+ updates: { projectiles: [] },
109
+ };
110
+
111
+ expect(() => registry.encode("projectiles", snapshot)).toThrow(
112
+ 'No codec registered for snapshot type "projectiles"'
113
+ );
114
+ });
115
+
116
+ it("should preserve tick number in encoding", () => {
117
+ const snapshot: Snapshot<ScoreUpdate> = {
118
+ tick: 12345,
119
+ updates: { score: 100 },
120
+ };
121
+
122
+ const buf = registry.encode("score", snapshot);
123
+
124
+ // Tick is at bytes 1-4 (after type ID)
125
+ const tick = new DataView(buf.buffer, buf.byteOffset + 1).getUint32(0, true);
126
+ expect(tick).toBe(12345);
127
+ });
128
+ });
129
+
130
+ describe("decode", () => {
131
+ beforeEach(() => {
132
+ registry.register("players", new MockCodec<PlayerUpdate>());
133
+ registry.register("score", new MockCodec<ScoreUpdate>());
134
+ });
135
+
136
+ it("should decode a snapshot and return type", () => {
137
+ const original: Snapshot<PlayerUpdate> = {
138
+ tick: 100,
139
+ updates: {
140
+ players: [
141
+ { entityId: 1, x: 10, y: 20 },
142
+ ],
143
+ },
144
+ };
145
+
146
+ const buf = registry.encode("players", original);
147
+ const { type, snapshot } = registry.decode(buf);
148
+
149
+ expect(type).toBe("players");
150
+ expect(snapshot.tick).toBe(100);
151
+ expect(snapshot.updates).toEqual(original.updates);
152
+ });
153
+
154
+ it("should decode different snapshot types correctly", () => {
155
+ const playerSnapshot: Snapshot<PlayerUpdate> = {
156
+ tick: 100,
157
+ updates: { players: [{ entityId: 1, x: 10, y: 20 }] },
158
+ };
159
+
160
+ const scoreSnapshot: Snapshot<ScoreUpdate> = {
161
+ tick: 101,
162
+ updates: { score: 50 },
163
+ };
164
+
165
+ const buf1 = registry.encode("players", playerSnapshot);
166
+ const buf2 = registry.encode("score", scoreSnapshot);
167
+
168
+ const decoded1 = registry.decode(buf1);
169
+ const decoded2 = registry.decode(buf2);
170
+
171
+ expect(decoded1.type).toBe("players");
172
+ expect(decoded1.snapshot.updates).toEqual(playerSnapshot.updates);
173
+
174
+ expect(decoded2.type).toBe("score");
175
+ expect(decoded2.snapshot.updates).toEqual(scoreSnapshot.updates);
176
+ });
177
+
178
+ it("should throw error when decoding unknown type ID", () => {
179
+ const buf = new Uint8Array([99, 0, 0, 0, 100]); // Unknown type ID 99
180
+
181
+ expect(() => registry.decode(buf)).toThrow("Unknown snapshot type ID: 99");
182
+ });
183
+ });
184
+
185
+ describe("round-trip encoding/decoding", () => {
186
+ beforeEach(() => {
187
+ registry.register("players", new MockCodec<PlayerUpdate>());
188
+ registry.register("score", new MockCodec<ScoreUpdate>());
189
+ registry.register("projectiles", new MockCodec<ProjectileUpdate>());
190
+ });
191
+
192
+ it("should preserve data through encode/decode cycle", () => {
193
+ const original: Snapshot<PlayerUpdate> = {
194
+ tick: 500,
195
+ updates: {
196
+ players: [
197
+ { entityId: 1, x: 100, y: 200 },
198
+ { entityId: 2, x: 300, y: 400 },
199
+ ],
200
+ },
201
+ };
202
+
203
+ const buf = registry.encode("players", original);
204
+ const { type, snapshot } = registry.decode(buf);
205
+
206
+ expect(type).toBe("players");
207
+ expect(snapshot).toEqual(original);
208
+ });
209
+
210
+ it("should handle multiple round-trips", () => {
211
+ const original: Snapshot<ScoreUpdate> = {
212
+ tick: 1000,
213
+ updates: { score: 99999 },
214
+ };
215
+
216
+ for (let i = 0; i < 10; i++) {
217
+ const buf = registry.encode("score", original);
218
+ const { type, snapshot } = registry.decode(buf);
219
+ expect(type).toBe("score");
220
+ expect(snapshot).toEqual(original);
221
+ }
222
+ });
223
+
224
+ it("should handle arrays in updates", () => {
225
+ const original: Snapshot<ProjectileUpdate> = {
226
+ tick: 250,
227
+ updates: {
228
+ projectiles: [
229
+ { id: 1, x: 10, y: 20 },
230
+ { id: 2, x: 30, y: 40 },
231
+ { id: 3, x: 50, y: 60 },
232
+ ],
233
+ },
234
+ };
235
+
236
+ const buf = registry.encode("projectiles", original);
237
+ const { type, snapshot } = registry.decode(buf);
238
+
239
+ expect(type).toBe("projectiles");
240
+ expect(snapshot.updates).toEqual(original.updates);
241
+ });
242
+ });
243
+
244
+ describe("getTypes", () => {
245
+ it("should return empty array when no types registered", () => {
246
+ expect(registry.getTypes()).toEqual([]);
247
+ });
248
+
249
+ it("should return all registered types", () => {
250
+ registry.register("players", new MockCodec<PlayerUpdate>());
251
+ registry.register("score", new MockCodec<ScoreUpdate>());
252
+
253
+ const types = registry.getTypes();
254
+ expect(types).toContain("players");
255
+ expect(types).toContain("score");
256
+ expect(types.length).toBe(2);
257
+ });
258
+ });
259
+
260
+ describe("integration scenarios", () => {
261
+ beforeEach(() => {
262
+ registry.register("players", new MockCodec<PlayerUpdate>());
263
+ registry.register("score", new MockCodec<ScoreUpdate>());
264
+ });
265
+
266
+ it("should allow sending only specific updates", () => {
267
+ // Server only sends player updates this tick
268
+ const playerBuf = registry.encode("players", {
269
+ tick: 100,
270
+ updates: { players: [{ entityId: 1, x: 5, y: 10 }] },
271
+ });
272
+
273
+ // Next tick, only send score
274
+ const scoreBuf = registry.encode("score", {
275
+ tick: 101,
276
+ updates: { score: 100 },
277
+ });
278
+
279
+ // Client can decode both
280
+ const decoded1 = registry.decode(playerBuf);
281
+ const decoded2 = registry.decode(scoreBuf);
282
+
283
+ expect(decoded1.type).toBe("players");
284
+ expect(decoded2.type).toBe("score");
285
+ });
286
+
287
+ it("should maintain type safety with unions", () => {
288
+ const snapshot: Snapshot<PlayerUpdate> = {
289
+ tick: 100,
290
+ updates: { players: [{ entityId: 1, x: 5, y: 10 }] },
291
+ };
292
+
293
+ const buf = registry.encode("players", snapshot);
294
+ const { type, snapshot: decoded } = registry.decode(buf);
295
+
296
+ // Type narrowing based on type field
297
+ if (type === "players") {
298
+ expect(decoded.updates.players).toBeDefined();
299
+ }
300
+ });
301
+ });
302
+ });