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,43 @@
1
+ /**
2
+ * Protocol Layer - Type-safe networking primitives
3
+ *
4
+ * This layer enforces:
5
+ * - Type-safe intent definitions (extend Intent interface)
6
+ * - Type-safe snapshot definitions (Snapshot<YourState>)
7
+ * - Memory-efficient encoding (use PooledCodec from core)
8
+ *
9
+ * You provide:
10
+ * - Your intent types
11
+ * - Your state types
12
+ * - Your schemas (for binary encoding)
13
+ * - Codec instances (instantiate once, reuse)
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // Define your types
18
+ * interface MoveIntent extends Intent {
19
+ * kind: 1;
20
+ * tick: number;
21
+ * dx: number;
22
+ * dy: number;
23
+ * }
24
+ *
25
+ * interface GameState {
26
+ * players: Record<number, { x: number; y: number }>;
27
+ * }
28
+ *
29
+ * // Create codecs once (reuse these!)
30
+ * const intentRegistry = new IntentRegistry();
31
+ * intentRegistry.register(1, new PooledCodec(moveSchema));
32
+ *
33
+ * const snapshotCodec = new SnapshotCodec<GameState>(
34
+ * new PooledCodec(stateSchema)
35
+ * );
36
+ *
37
+ * // Use them
38
+ * const buf = intentRegistry.encode(intent);
39
+ * const snapshot = snapshotCodec.decode(buf);
40
+ * ```
41
+ */
42
+ export * from "./intent";
43
+ export * from "./snapshot";
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Intent system for client-to-server actions.
3
+ *
4
+ * Intents are player/AI actions that need to be:
5
+ * 1. Encoded efficiently (binary)
6
+ * 2. Sent over network
7
+ * 3. Decoded on the other end
8
+ * 4. Processed deterministically
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { Intent, IntentRegistry } from './protocol/intent';
13
+ * import { PooledCodec } from '../core/pooled-codec';
14
+ * import { BinaryCodec } from '../core/binary-codec';
15
+ *
16
+ * // 1. Define your intent type
17
+ * interface MoveIntent extends Intent {
18
+ * kind: 1;
19
+ * tick: number;
20
+ * dx: number;
21
+ * dy: number;
22
+ * }
23
+ *
24
+ * // 2. Create registry and register once (reuse this instance)
25
+ * const registry = new IntentRegistry();
26
+ * registry.register(1, new PooledCodec({
27
+ * kind: BinaryCodec.u8,
28
+ * tick: BinaryCodec.u32,
29
+ * dx: BinaryCodec.f32,
30
+ * dy: BinaryCodec.f32,
31
+ * }));
32
+ *
33
+ * // 3. Encode/decode
34
+ * const buf = registry.encode(intent);
35
+ * const decoded = registry.decode(1, buf);
36
+ * ```
37
+ */
38
+ export type { Intent } from "./intent";
39
+ export { IntentRegistry } from "./intent-registry";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Intent system for client-to-server actions.
3
+ *
4
+ * Intents are player/AI actions that need to be:
5
+ * 1. Encoded efficiently (binary)
6
+ * 2. Sent over network
7
+ * 3. Decoded on the other end
8
+ * 4. Processed deterministically
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { Intent, IntentRegistry } from './protocol/intent';
13
+ * import { PooledCodec } from '../core/pooled-codec';
14
+ * import { BinaryCodec } from '../core/binary-codec';
15
+ *
16
+ * // 1. Define your intent type
17
+ * interface MoveIntent extends Intent {
18
+ * kind: 1;
19
+ * tick: number;
20
+ * dx: number;
21
+ * dy: number;
22
+ * }
23
+ *
24
+ * // 2. Create registry and register once (reuse this instance)
25
+ * const registry = new IntentRegistry();
26
+ * registry.register(1, new PooledCodec({
27
+ * kind: BinaryCodec.u8,
28
+ * tick: BinaryCodec.u32,
29
+ * dx: BinaryCodec.f32,
30
+ * dy: BinaryCodec.f32,
31
+ * }));
32
+ *
33
+ * // 3. Encode/decode
34
+ * const buf = registry.encode(intent);
35
+ * const decoded = registry.decode(1, buf);
36
+ * ```
37
+ */
38
+ export { IntentRegistry } from "./intent-registry";
@@ -0,0 +1,54 @@
1
+ import type { Intent } from "./intent";
2
+ /**
3
+ * Generic codec interface (users import from core/pooled-codec)
4
+ */
5
+ export interface Codec<T> {
6
+ encode(value: T): Uint8Array;
7
+ decode(buf: Uint8Array): T;
8
+ }
9
+ /**
10
+ * Registry for mapping intent kinds to their codecs.
11
+ *
12
+ * Users instantiate this once and register their intent types with
13
+ * PooledCodec instances (from core/pooled-codec).
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { IntentRegistry } from './protocol/intent';
18
+ * import { PooledCodec } from './core/pooled-codec';
19
+ * import { BinaryCodec } from './core/binary-codec';
20
+ *
21
+ * const registry = new IntentRegistry();
22
+ *
23
+ * registry.register(1, new PooledCodec({
24
+ * kind: BinaryCodec.u8,
25
+ * tick: BinaryCodec.u32,
26
+ * dx: BinaryCodec.f32,
27
+ * dy: BinaryCodec.f32,
28
+ * }));
29
+ *
30
+ * // Encode/decode
31
+ * const buf = registry.encode(intent);
32
+ * const decoded = registry.decode(1, buf);
33
+ * ```
34
+ */
35
+ export declare class IntentRegistry {
36
+ private codecs;
37
+ /**
38
+ * Register a codec for a specific intent kind.
39
+ * Call this once per intent type at startup.
40
+ */
41
+ register<T extends Intent>(kind: number, codec: Codec<T>): void;
42
+ /**
43
+ * Encode an intent into binary format.
44
+ */
45
+ encode<T extends Intent>(intent: T): Uint8Array;
46
+ /**
47
+ * Decode binary data into an intent.
48
+ */
49
+ decode(kind: number, buf: Uint8Array): Intent;
50
+ has(kind: number): boolean;
51
+ unregister(kind: number): boolean;
52
+ clear(): void;
53
+ getKinds(): number[];
54
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Registry for mapping intent kinds to their codecs.
3
+ *
4
+ * Users instantiate this once and register their intent types with
5
+ * PooledCodec instances (from core/pooled-codec).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { IntentRegistry } from './protocol/intent';
10
+ * import { PooledCodec } from './core/pooled-codec';
11
+ * import { BinaryCodec } from './core/binary-codec';
12
+ *
13
+ * const registry = new IntentRegistry();
14
+ *
15
+ * registry.register(1, new PooledCodec({
16
+ * kind: BinaryCodec.u8,
17
+ * tick: BinaryCodec.u32,
18
+ * dx: BinaryCodec.f32,
19
+ * dy: BinaryCodec.f32,
20
+ * }));
21
+ *
22
+ * // Encode/decode
23
+ * const buf = registry.encode(intent);
24
+ * const decoded = registry.decode(1, buf);
25
+ * ```
26
+ */
27
+ export class IntentRegistry {
28
+ constructor() {
29
+ this.codecs = new Map();
30
+ }
31
+ /**
32
+ * Register a codec for a specific intent kind.
33
+ * Call this once per intent type at startup.
34
+ */
35
+ register(kind, codec) {
36
+ if (this.codecs.has(kind)) {
37
+ throw new Error(`Intent kind ${kind} is already registered`);
38
+ }
39
+ this.codecs.set(kind, codec);
40
+ }
41
+ /**
42
+ * Encode an intent into binary format.
43
+ */
44
+ encode(intent) {
45
+ const codec = this.codecs.get(intent.kind);
46
+ if (!codec) {
47
+ throw new Error(`No codec registered for intent kind ${intent.kind}`);
48
+ }
49
+ return codec.encode(intent);
50
+ }
51
+ /**
52
+ * Decode binary data into an intent.
53
+ */
54
+ decode(kind, buf) {
55
+ const codec = this.codecs.get(kind);
56
+ if (!codec) {
57
+ throw new Error(`No codec registered for intent kind ${kind}`);
58
+ }
59
+ return codec.decode(buf);
60
+ }
61
+ has(kind) {
62
+ return this.codecs.has(kind);
63
+ }
64
+ unregister(kind) {
65
+ return this.codecs.delete(kind);
66
+ }
67
+ clear() {
68
+ this.codecs.clear();
69
+ }
70
+ getKinds() {
71
+ return Array.from(this.codecs.keys());
72
+ }
73
+ }
@@ -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 @@
1
+ export {};
@@ -0,0 +1,44 @@
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
+ export type { Snapshot } from "./snapshot";
42
+ export { applySnapshot } from "./snapshot";
43
+ export { SnapshotCodec } from "./snapshot-codec";
44
+ export { SnapshotRegistry } from "./snapshot-registry";
@@ -0,0 +1,43 @@
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
+ export { applySnapshot } from "./snapshot";
42
+ export { SnapshotCodec } from "./snapshot-codec";
43
+ export { SnapshotRegistry } from "./snapshot-registry";
@@ -0,0 +1,48 @@
1
+ import type { Snapshot } from "./snapshot";
2
+ /**
3
+ * Generic codec interface (users import from core/pooled-codec)
4
+ */
5
+ export interface Codec<T> {
6
+ encode(value: T): Uint8Array;
7
+ decode(buf: Uint8Array): T;
8
+ }
9
+ /**
10
+ * Codec for encoding/decoding snapshots with binary serialization.
11
+ *
12
+ * Users instantiate this once with a PooledCodec for their state schema.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { SnapshotCodec } 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
+ * const snapshotCodec = new SnapshotCodec<GameState>(
25
+ * new PooledCodec({ players: // schema })
26
+ * );
27
+ *
28
+ * // Encode/decode
29
+ * const buf = snapshotCodec.encode(snapshot);
30
+ * const decoded = snapshotCodec.decode(buf);
31
+ * ```
32
+ */
33
+ export declare class SnapshotCodec<T> {
34
+ private updatesCodec;
35
+ /**
36
+ * @param updatesCodec Codec for encoding/decoding the state updates
37
+ */
38
+ constructor(updatesCodec: Codec<Partial<T>>);
39
+ /**
40
+ * Encode a snapshot into binary format.
41
+ * Format: [tick: u32][updates: encoded by updatesCodec]
42
+ */
43
+ encode(snapshot: Snapshot<T>): Uint8Array;
44
+ /**
45
+ * Decode binary data into a snapshot.
46
+ */
47
+ decode(buf: Uint8Array): Snapshot<T>;
48
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Codec for encoding/decoding snapshots with binary serialization.
3
+ *
4
+ * Users instantiate this once with a PooledCodec for their state schema.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { SnapshotCodec } from './protocol/snapshot';
9
+ * import { PooledCodec } from './core/pooled-codec';
10
+ * import { BinaryCodec } from './core/binary-codec';
11
+ *
12
+ * interface GameState {
13
+ * players: Record<number, { x: number; y: number }>;
14
+ * }
15
+ *
16
+ * const snapshotCodec = new SnapshotCodec<GameState>(
17
+ * new PooledCodec({ players: // schema })
18
+ * );
19
+ *
20
+ * // Encode/decode
21
+ * const buf = snapshotCodec.encode(snapshot);
22
+ * const decoded = snapshotCodec.decode(buf);
23
+ * ```
24
+ */
25
+ export class SnapshotCodec {
26
+ /**
27
+ * @param updatesCodec Codec for encoding/decoding the state updates
28
+ */
29
+ constructor(updatesCodec) {
30
+ this.updatesCodec = updatesCodec;
31
+ }
32
+ /**
33
+ * Encode a snapshot into binary format.
34
+ * Format: [tick: u32][updates: encoded by updatesCodec]
35
+ */
36
+ encode(snapshot) {
37
+ const updatesBytes = this.updatesCodec.encode(snapshot.updates);
38
+ const buf = new Uint8Array(4 + updatesBytes.length);
39
+ // Encode tick (4 bytes, little-endian)
40
+ new DataView(buf.buffer).setUint32(0, snapshot.tick, true);
41
+ // Encode updates
42
+ buf.set(updatesBytes, 4);
43
+ return buf;
44
+ }
45
+ /**
46
+ * Decode binary data into a snapshot.
47
+ */
48
+ decode(buf) {
49
+ // Decode tick (first 4 bytes)
50
+ const tick = new DataView(buf.buffer, buf.byteOffset).getUint32(0, true);
51
+ // Decode updates (remaining bytes)
52
+ const updatesBytes = buf.subarray(4);
53
+ const updates = this.updatesCodec.decode(updatesBytes);
54
+ return { tick, updates };
55
+ }
56
+ }
@@ -0,0 +1,100 @@
1
+ import type { Snapshot } from "./snapshot";
2
+ /**
3
+ * Generic codec interface (users import from core/pooled-codec)
4
+ */
5
+ interface Codec<T> {
6
+ encode(value: T): Uint8Array;
7
+ decode(buf: Uint8Array): T;
8
+ }
9
+ /**
10
+ * Registry for multiple snapshot types with different update schemas.
11
+ *
12
+ * This allows efficient delta encoding by only sending specific update types
13
+ * instead of encoding all fields (including empty/nil ones).
14
+ *
15
+ * ## Memory Efficiency
16
+ *
17
+ * The encoding path minimizes allocations:
18
+ * 1. PooledCodec acquires a buffer from its pool (reused across calls)
19
+ * 2. PooledCodec writes data directly to the buffer at sequential offsets
20
+ * 3. SnapshotRegistry creates ONE final buffer: [typeId(1) + tick(4) + updatesBytes]
21
+ * 4. Total allocations per encode: 1 pooled buffer + 1 final buffer
22
+ *
23
+ * Buffer Layout:
24
+ * ```
25
+ * ┌─────────┬────────────┬──────────────────┐
26
+ * │ Type ID │ Tick │ Updates (codec) │
27
+ * │ (u8) │ (u32) │ (variable) │
28
+ * │ 1 byte │ 4 bytes │ N bytes │
29
+ * └─────────┴────────────┴──────────────────┘
30
+ * ```
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { SnapshotRegistry } from './protocol/snapshot';
35
+ * import { PooledCodec } from './core/pooled-codec';
36
+ * import { BinaryCodec } from './core/binary-codec';
37
+ *
38
+ * // Define different update types
39
+ * interface PlayerUpdate {
40
+ * players: Array<{ entityId: number; x: number; y: number }>;
41
+ * }
42
+ *
43
+ * interface ScoreUpdate {
44
+ * score: number;
45
+ * }
46
+ *
47
+ * // Create registry
48
+ * const registry = new SnapshotRegistry<PlayerUpdate | ScoreUpdate>();
49
+ *
50
+ * // Register codecs for each update type
51
+ * registry.register('players', new PooledCodec({
52
+ * players: // schema
53
+ * }));
54
+ *
55
+ * registry.register('score', new PooledCodec({
56
+ * score: BinaryCodec.u32
57
+ * }));
58
+ *
59
+ * // Server: Encode specific update type
60
+ * const buf = registry.encode('players', {
61
+ * tick: 100,
62
+ * updates: { players: [{ entityId: 1, x: 5, y: 10 }] }
63
+ * });
64
+ *
65
+ * // Client: Decode (type is embedded in message)
66
+ * const { type, snapshot } = registry.decode(buf);
67
+ * applySnapshot(state, snapshot);
68
+ * ```
69
+ */
70
+ export declare class SnapshotRegistry<T> {
71
+ private codecs;
72
+ private typeIds;
73
+ private idToType;
74
+ private nextId;
75
+ /**
76
+ * Register a codec for a specific update type.
77
+ * Call this once per update type at startup.
78
+ */
79
+ register<U extends Partial<T>>(type: string, codec: Codec<U>): void;
80
+ /**
81
+ * Encode a snapshot with a specific update type.
82
+ * Format: [typeId: u8][tick: u32][updates: encoded by codec]
83
+ *
84
+ * Efficiently writes to a single buffer:
85
+ * - PooledCodec writes updates to its pooled buffer
86
+ * - We allocate ONE final buffer for [typeId + tick + updates]
87
+ * - Direct memory copy, no intermediate allocations
88
+ */
89
+ encode<U extends Partial<T>>(type: string, snapshot: Snapshot<U>): Uint8Array;
90
+ /**
91
+ * Decode a snapshot and return both the type and the snapshot.
92
+ */
93
+ decode(buf: Uint8Array): {
94
+ type: string;
95
+ snapshot: Snapshot<Partial<T>>;
96
+ };
97
+ has(type: string): boolean;
98
+ getTypes(): string[];
99
+ }
100
+ export {};
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Registry for multiple snapshot types with different update schemas.
3
+ *
4
+ * This allows efficient delta encoding by only sending specific update types
5
+ * instead of encoding all fields (including empty/nil ones).
6
+ *
7
+ * ## Memory Efficiency
8
+ *
9
+ * The encoding path minimizes allocations:
10
+ * 1. PooledCodec acquires a buffer from its pool (reused across calls)
11
+ * 2. PooledCodec writes data directly to the buffer at sequential offsets
12
+ * 3. SnapshotRegistry creates ONE final buffer: [typeId(1) + tick(4) + updatesBytes]
13
+ * 4. Total allocations per encode: 1 pooled buffer + 1 final buffer
14
+ *
15
+ * Buffer Layout:
16
+ * ```
17
+ * ┌─────────┬────────────┬──────────────────┐
18
+ * │ Type ID │ Tick │ Updates (codec) │
19
+ * │ (u8) │ (u32) │ (variable) │
20
+ * │ 1 byte │ 4 bytes │ N bytes │
21
+ * └─────────┴────────────┴──────────────────┘
22
+ * ```
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { SnapshotRegistry } from './protocol/snapshot';
27
+ * import { PooledCodec } from './core/pooled-codec';
28
+ * import { BinaryCodec } from './core/binary-codec';
29
+ *
30
+ * // Define different update types
31
+ * interface PlayerUpdate {
32
+ * players: Array<{ entityId: number; x: number; y: number }>;
33
+ * }
34
+ *
35
+ * interface ScoreUpdate {
36
+ * score: number;
37
+ * }
38
+ *
39
+ * // Create registry
40
+ * const registry = new SnapshotRegistry<PlayerUpdate | ScoreUpdate>();
41
+ *
42
+ * // Register codecs for each update type
43
+ * registry.register('players', new PooledCodec({
44
+ * players: // schema
45
+ * }));
46
+ *
47
+ * registry.register('score', new PooledCodec({
48
+ * score: BinaryCodec.u32
49
+ * }));
50
+ *
51
+ * // Server: Encode specific update type
52
+ * const buf = registry.encode('players', {
53
+ * tick: 100,
54
+ * updates: { players: [{ entityId: 1, x: 5, y: 10 }] }
55
+ * });
56
+ *
57
+ * // Client: Decode (type is embedded in message)
58
+ * const { type, snapshot } = registry.decode(buf);
59
+ * applySnapshot(state, snapshot);
60
+ * ```
61
+ */
62
+ export class SnapshotRegistry {
63
+ constructor() {
64
+ this.codecs = new Map();
65
+ this.typeIds = new Map();
66
+ this.idToType = new Map();
67
+ this.nextId = 0;
68
+ }
69
+ /**
70
+ * Register a codec for a specific update type.
71
+ * Call this once per update type at startup.
72
+ */
73
+ register(type, codec) {
74
+ if (this.codecs.has(type)) {
75
+ throw new Error(`Snapshot type "${type}" is already registered`);
76
+ }
77
+ const typeId = this.nextId++;
78
+ this.codecs.set(type, codec);
79
+ this.typeIds.set(type, typeId);
80
+ this.idToType.set(typeId, type);
81
+ }
82
+ /**
83
+ * Encode a snapshot with a specific update type.
84
+ * Format: [typeId: u8][tick: u32][updates: encoded by codec]
85
+ *
86
+ * Efficiently writes to a single buffer:
87
+ * - PooledCodec writes updates to its pooled buffer
88
+ * - We allocate ONE final buffer for [typeId + tick + updates]
89
+ * - Direct memory copy, no intermediate allocations
90
+ */
91
+ encode(type, snapshot) {
92
+ const codec = this.codecs.get(type);
93
+ const typeId = this.typeIds.get(type);
94
+ if (!codec || typeId === undefined) {
95
+ throw new Error(`No codec registered for snapshot type "${type}"`);
96
+ }
97
+ // Encode updates using the specific codec (acquires from pool)
98
+ const updatesBytes = codec.encode(snapshot.updates);
99
+ // Allocate single buffer for complete message
100
+ const buf = new Uint8Array(1 + 4 + updatesBytes.length);
101
+ // Write type ID (1 byte)
102
+ buf[0] = typeId;
103
+ // Write tick (4 bytes, little-endian)
104
+ new DataView(buf.buffer).setUint32(1, snapshot.tick, true);
105
+ // Write updates (direct copy)
106
+ buf.set(updatesBytes, 5);
107
+ return buf;
108
+ }
109
+ /**
110
+ * Decode a snapshot and return both the type and the snapshot.
111
+ */
112
+ decode(buf) {
113
+ // Decode type ID (first byte)
114
+ const typeId = buf[0];
115
+ const type = this.idToType.get(typeId);
116
+ if (!type) {
117
+ throw new Error(`Unknown snapshot type ID: ${typeId}`);
118
+ }
119
+ const codec = this.codecs.get(type);
120
+ if (!codec) {
121
+ throw new Error(`No codec registered for snapshot type "${type}"`);
122
+ }
123
+ // Decode tick (bytes 1-4)
124
+ const tick = new DataView(buf.buffer, buf.byteOffset + 1).getUint32(0, true);
125
+ // Decode updates (remaining bytes)
126
+ const updatesBytes = buf.subarray(5);
127
+ const updates = codec.decode(updatesBytes);
128
+ return { type, snapshot: { tick, updates } };
129
+ }
130
+ has(type) {
131
+ return this.codecs.has(type);
132
+ }
133
+ getTypes() {
134
+ return Array.from(this.codecs.keys());
135
+ }
136
+ }