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,162 @@
1
+ import type { Snapshot } from "./snapshot";
2
+
3
+ /**
4
+ * Generic codec interface (users import from core/pooled-codec)
5
+ */
6
+ interface Codec<T> {
7
+ encode(value: T): Uint8Array;
8
+ decode(buf: Uint8Array): T;
9
+ }
10
+
11
+ /**
12
+ * Registry for multiple snapshot types with different update schemas.
13
+ *
14
+ * This allows efficient delta encoding by only sending specific update types
15
+ * instead of encoding all fields (including empty/nil ones).
16
+ *
17
+ * ## Memory Efficiency
18
+ *
19
+ * The encoding path minimizes allocations:
20
+ * 1. PooledCodec acquires a buffer from its pool (reused across calls)
21
+ * 2. PooledCodec writes data directly to the buffer at sequential offsets
22
+ * 3. SnapshotRegistry creates ONE final buffer: [typeId(1) + tick(4) + updatesBytes]
23
+ * 4. Total allocations per encode: 1 pooled buffer + 1 final buffer
24
+ *
25
+ * Buffer Layout:
26
+ * ```
27
+ * ┌─────────┬────────────┬──────────────────┐
28
+ * │ Type ID │ Tick │ Updates (codec) │
29
+ * │ (u8) │ (u32) │ (variable) │
30
+ * │ 1 byte │ 4 bytes │ N bytes │
31
+ * └─────────┴────────────┴──────────────────┘
32
+ * ```
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { SnapshotRegistry } from './protocol/snapshot';
37
+ * import { PooledCodec } from './core/pooled-codec';
38
+ * import { BinaryCodec } from './core/binary-codec';
39
+ *
40
+ * // Define different update types
41
+ * interface PlayerUpdate {
42
+ * players: Array<{ entityId: number; x: number; y: number }>;
43
+ * }
44
+ *
45
+ * interface ScoreUpdate {
46
+ * score: number;
47
+ * }
48
+ *
49
+ * // Create registry
50
+ * const registry = new SnapshotRegistry<PlayerUpdate | ScoreUpdate>();
51
+ *
52
+ * // Register codecs for each update type
53
+ * registry.register('players', new PooledCodec({
54
+ * players: // schema
55
+ * }));
56
+ *
57
+ * registry.register('score', new PooledCodec({
58
+ * score: BinaryCodec.u32
59
+ * }));
60
+ *
61
+ * // Server: Encode specific update type
62
+ * const buf = registry.encode('players', {
63
+ * tick: 100,
64
+ * updates: { players: [{ entityId: 1, x: 5, y: 10 }] }
65
+ * });
66
+ *
67
+ * // Client: Decode (type is embedded in message)
68
+ * const { type, snapshot } = registry.decode(buf);
69
+ * applySnapshot(state, snapshot);
70
+ * ```
71
+ */
72
+ export class SnapshotRegistry<T> {
73
+ private codecs = new Map<string, Codec<any>>();
74
+ private typeIds = new Map<string, number>();
75
+ private idToType = new Map<number, string>();
76
+ private nextId = 0;
77
+
78
+ /**
79
+ * Register a codec for a specific update type.
80
+ * Call this once per update type at startup.
81
+ */
82
+ register<U extends Partial<T>>(type: string, codec: Codec<U>): void {
83
+ if (this.codecs.has(type)) {
84
+ throw new Error(`Snapshot type "${type}" is already registered`);
85
+ }
86
+
87
+ const typeId = this.nextId++;
88
+ this.codecs.set(type, codec);
89
+ this.typeIds.set(type, typeId);
90
+ this.idToType.set(typeId, type);
91
+ }
92
+
93
+ /**
94
+ * Encode a snapshot with a specific update type.
95
+ * Format: [typeId: u8][tick: u32][updates: encoded by codec]
96
+ *
97
+ * Efficiently writes to a single buffer:
98
+ * - PooledCodec writes updates to its pooled buffer
99
+ * - We allocate ONE final buffer for [typeId + tick + updates]
100
+ * - Direct memory copy, no intermediate allocations
101
+ */
102
+ encode<U extends Partial<T>>(type: string, snapshot: Snapshot<U>): Uint8Array {
103
+ const codec = this.codecs.get(type);
104
+ const typeId = this.typeIds.get(type);
105
+
106
+ if (!codec || typeId === undefined) {
107
+ throw new Error(`No codec registered for snapshot type "${type}"`);
108
+ }
109
+
110
+ // Encode updates using the specific codec (acquires from pool)
111
+ const updatesBytes = codec.encode(snapshot.updates);
112
+
113
+ // Allocate single buffer for complete message
114
+ const buf = new Uint8Array(1 + 4 + updatesBytes.length);
115
+
116
+ // Write type ID (1 byte)
117
+ buf[0] = typeId;
118
+
119
+ // Write tick (4 bytes, little-endian)
120
+ new DataView(buf.buffer).setUint32(1, snapshot.tick, true);
121
+
122
+ // Write updates (direct copy)
123
+ buf.set(updatesBytes, 5);
124
+
125
+ return buf;
126
+ }
127
+
128
+ /**
129
+ * Decode a snapshot and return both the type and the snapshot.
130
+ */
131
+ decode(buf: Uint8Array): { type: string; snapshot: Snapshot<Partial<T>> } {
132
+ // Decode type ID (first byte)
133
+ const typeId = buf[0];
134
+ const type = this.idToType.get(typeId);
135
+
136
+ if (!type) {
137
+ throw new Error(`Unknown snapshot type ID: ${typeId}`);
138
+ }
139
+
140
+ const codec = this.codecs.get(type);
141
+ if (!codec) {
142
+ throw new Error(`No codec registered for snapshot type "${type}"`);
143
+ }
144
+
145
+ // Decode tick (bytes 1-4)
146
+ const tick = new DataView(buf.buffer, buf.byteOffset + 1).getUint32(0, true);
147
+
148
+ // Decode updates (remaining bytes)
149
+ const updatesBytes = buf.subarray(5);
150
+ const updates = codec.decode(updatesBytes);
151
+
152
+ return { type, snapshot: { tick, updates } };
153
+ }
154
+
155
+ has(type: string): boolean {
156
+ return this.codecs.has(type);
157
+ }
158
+
159
+ getTypes(): string[] {
160
+ return Array.from(this.codecs.keys());
161
+ }
162
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { Snapshot, applySnapshot } from "./snapshot";
3
+
4
+ interface TestState {
5
+ x: number;
6
+ y: number;
7
+ health?: number;
8
+ nested?: {
9
+ a: number;
10
+ b?: number;
11
+ };
12
+ items?: string[];
13
+ }
14
+
15
+ describe("Snapshot", () => {
16
+ describe("applySnapshot", () => {
17
+ it("should apply simple property updates", () => {
18
+ const state: TestState = { x: 0, y: 0 };
19
+ const snapshot: Snapshot<TestState> = {
20
+ tick: 100,
21
+ updates: { x: 10, y: 20 },
22
+ };
23
+
24
+ applySnapshot(state, snapshot);
25
+ expect(state.x).toBe(10);
26
+ expect(state.y).toBe(20);
27
+ });
28
+
29
+ it("should apply partial updates without affecting other properties", () => {
30
+ const state: TestState = { x: 0, y: 0, health: 100 };
31
+ const snapshot: Snapshot<TestState> = {
32
+ tick: 100,
33
+ updates: { x: 10 },
34
+ };
35
+
36
+ applySnapshot(state, snapshot);
37
+ expect(state.x).toBe(10);
38
+ expect(state.y).toBe(0);
39
+ expect(state.health).toBe(100);
40
+ });
41
+
42
+ it("should handle nested object updates", () => {
43
+ const state: TestState = { x: 0, y: 0, nested: { a: 1, b: 2 } };
44
+ const snapshot: Snapshot<TestState> = {
45
+ tick: 100,
46
+ updates: { nested: { a: 10 } },
47
+ };
48
+
49
+ applySnapshot(state, snapshot);
50
+ expect(state.nested?.a).toBe(10);
51
+ expect(state.nested?.b).toBe(2);
52
+ });
53
+
54
+ it("should replace arrays entirely", () => {
55
+ const state: TestState = { x: 0, y: 0, items: ["a", "b", "c"] };
56
+ const snapshot: Snapshot<TestState> = {
57
+ tick: 100,
58
+ updates: { items: ["x", "y"] },
59
+ };
60
+
61
+ applySnapshot(state, snapshot);
62
+ expect(state.items).toEqual(["x", "y"]);
63
+ });
64
+
65
+ it("should handle null values", () => {
66
+ const state: TestState = { x: 0, y: 0, health: 100 };
67
+ const snapshot: Snapshot<TestState> = {
68
+ tick: 100,
69
+ updates: { health: null as any },
70
+ };
71
+
72
+ applySnapshot(state, snapshot);
73
+ expect(state.health).toBeNull();
74
+ });
75
+ });
76
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Server-authoritative snapshot with delta updates.
3
+ *
4
+ * @template T The full state type
5
+ */
6
+ export interface Snapshot<T> {
7
+ /** Server tick when this snapshot was created */
8
+ tick: number;
9
+ /** Partial state updates (only what changed) */
10
+ updates: Partial<T>;
11
+ }
12
+
13
+ /**
14
+ * Apply snapshot updates to state (mutating).
15
+ * Deep merges objects, replaces arrays.
16
+ *
17
+ * @param state State to update
18
+ * @param snapshot Snapshot to apply
19
+ */
20
+ export function applySnapshot<T>(state: T, snapshot: Snapshot<T>): void {
21
+ deepMerge(state, snapshot.updates);
22
+ }
23
+
24
+ function deepMerge<T>(target: T, update: Partial<T>): void {
25
+ for (const key in update) {
26
+ if (!Object.prototype.hasOwnProperty.call(update, key)) continue;
27
+
28
+ const updateValue = update[key];
29
+ const targetValue = target[key];
30
+
31
+ if (updateValue === null || updateValue === undefined) {
32
+ target[key] = updateValue as T[Extract<keyof T, string>];
33
+ } else if (Array.isArray(updateValue)) {
34
+ target[key] = updateValue as T[Extract<keyof T, string>];
35
+ } else if (typeof updateValue === "object" && typeof targetValue === "object") {
36
+ deepMerge(targetValue as any, updateValue as any);
37
+ } else {
38
+ target[key] = updateValue as T[Extract<keyof T, string>];
39
+ }
40
+ }
41
+ }