@roomkit/state 1.0.0

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/README.md ADDED
@@ -0,0 +1,356 @@
1
+ # @roomkit/state
2
+
3
+ High-performance state synchronization library inspired by Colyseus Schema.
4
+
5
+ ## Features
6
+
7
+ - 🎯 **Type-Safe**: Decorator-based schema definition with TypeScript support
8
+ - ⚡ **High Performance**: Binary encoding with MessagePack, <1ms for 1000 entities
9
+ - 📦 **Small Payloads**: 75-85% bandwidth reduction with compression
10
+ - 🔄 **Automatic Tracking**: Automatic change detection for all data types
11
+ - 🎮 **Game-Ready**: Tracked collections (Map, Array, Set) with automatic sync
12
+ - 📊 **Delta Updates**: JSON Patch format for incremental state changes
13
+ - 🧩 **Deep Nesting**: Automatic tracking of nested objects and collections
14
+ - 🗜️ **Smart Compression**: Adaptive compression with entropy detection
15
+ - 📦 **Batch Updates**: Queue system to reduce message frequency by 20x
16
+ - 📈 **Performance Monitoring**: Built-in statistics and benchmarking
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pnpm add @roomkit/state
22
+ ```
23
+
24
+ ## Performance Benchmarks
25
+
26
+ | Scale | Players | Encoding Time | Data Size | Compression | Throughput |
27
+ |-------|---------|---------------|-----------|-------------|------------|
28
+ | Small | 10 | 0.087ms | 474 bytes | 58.1% saved | 11,504/sec |
29
+ | Medium| 100 | 0.210ms | 3.3 KB | 70.6% saved | 4,768/sec |
30
+ | Large | 1000 | 0.996ms | 32 KB | 72.5% saved | 1,004/sec |
31
+
32
+ **Delta Updates**: 0.006ms for 3 patches, 158,430 patches/sec
33
+ **Batch Queue**: 20x message reduction, 4.25ms for 5000 patches
34
+ **Change Tracking**: 0.010ms per cycle, 104,261 cycles/sec
35
+
36
+ ## Quick Start
37
+
38
+ ### Using Tracked Collections (Recommended)
39
+
40
+ ```typescript
41
+ import { State, Field, MapField, TrackedMap } from '@roomkit/state';
42
+
43
+ class Player {
44
+ @Field('string') name: string = '';
45
+ @Field('number') score: number = 0;
46
+ }
47
+
48
+ class GameState extends State {
49
+ @MapField(Player) players = new Map<string, Player>();
50
+ @Field('string') status: string = 'waiting';
51
+ }
52
+
53
+ // Usage
54
+ const state = new GameState();
55
+ state.startTracking(); // Automatically converts to TrackedMap
56
+
57
+ // All modifications are automatically tracked
58
+ state.players.set('p1', newPlayer);
59
+ state.status = 'playing';
60
+
61
+ // Get changes
62
+ const patches = state.getPatches(); // JSON Patch format
63
+ const encoded = state.encode(); // Binary MessagePack
64
+
65
+ // Clear changes after sync
66
+ state.clearChanges();
67
+ ```
68
+
69
+ ### Direct Tracked Collections Usage
70
+
71
+ ```typescript
72
+ import { TrackedMap, TrackedArray, TrackedSet } from '@roomkit/state';
73
+
74
+ // TrackedMap
75
+ const players = new TrackedMap<string, Player>();
76
+ players.startTracking();
77
+
78
+ players.onChange((patches) => {
79
+ console.log('Players changed:', patches);
80
+ });
81
+
82
+ players.set('p1', new Player());
83
+ // Triggers onChange with patches
84
+
85
+ // TrackedArray
86
+ const items = new TrackedArray<string>();
87
+ items.startTracking();
88
+ items.push('item1', 'item2');
89
+
90
+ // TrackedSet
91
+ const tags = new TrackedSet<string>();
92
+ tags.startTracking();
93
+ tags.add('tag1');
94
+ ```
95
+
96
+ ### Client-side State Synchronization
97
+
98
+ ```typescript
99
+ import { StateDecoder } from '@roomkit/state/encoding';
100
+
101
+ class ClientGameState {
102
+ players = new Map<string, any>();
103
+ scores: number[] = [];
104
+ status: string = 'waiting';
105
+ round: number = 0;
106
+ }
107
+
108
+ const decoder = new StateDecoder();
109
+ const state = new ClientGameState();
110
+
111
+ // Full state sync
112
+ room.onMessage(MessageId.STATE_FULL, (message) => {
113
+ const decoded = decoder.decode(message.data);
114
+ Object.assign(state, decoded);
115
+ });
116
+
117
+ // Delta updates
118
+ room.onMessage(MessageId.STATE_DELTA, (message) => {
119
+ decoder.applyPatches(state, message.patches);
120
+ });
121
+ ```
122
+
123
+ ## API Reference
124
+
125
+ ### Decorators
126
+
127
+ #### `@Field(type: FieldType)`
128
+ Define a primitive field.
129
+
130
+ ```typescript
131
+ @Field('string') name: string = '';
132
+ @Field('number') health: number = 100;
133
+ @Field('boolean') isAlive: boolean = true;
134
+ ```
135
+
136
+ ### TrackedMap
137
+
138
+ ```typescript
139
+ @MapField(Player) players = new Map<string, Player>();
140
+ ```
141
+
142
+ When `startTracking()` is called, automatically converts to `TrackedMap` which:
143
+ - Tracks `set()`, `delete()`, `clear()` operations
144
+ - Emits change events via `onChange(callback)`
145
+ - Supports batch operations with `batch(fn)`
146
+
147
+ ### TrackedArray
148
+
149
+ ```typescript
150
+ @ArrayField('number') scores: number[] = [];
151
+ ```
152
+
153
+ Automatically converts to `TrackedArray` which tracks:
154
+ - `push()`, `pop()`, `shift()`, `unshift()`
155
+ - `splice()`, `sort()`, `reverse()`
156
+ - Array element assignments
157
+
158
+ ### TrackedSet
159
+
160
+ ```typescript
161
+ @SetField('string') tags = new Set<string>();
162
+ ```
163
+
164
+ Automatically converts to `TrackedSet` which tracks:
165
+ - `add()`, `delete()`, `clear()`
166
+ - All modifications to the set
167
+
168
+ ### State Class
169
+
170
+ #### `startTracking()`
171
+ Start tracking changes to the state.
172
+
173
+ #### `stopTracking()`
174
+ Stop tracking changes.
175
+
176
+ #### `getPatches(): Patch[]`
177
+ Get accumulated changes as JSON Patch operations.
178
+
179
+ #### `clearChanges()`
180
+ Clear accumulated changes.
181
+
182
+ #### `encode(full?: boolean, options?: EncodeOptions): EncodedState`
183
+ Encode state to binary format.
184
+ - `full=true`: Encode entire state
185
+ - `full=false`: Encode only changes (delta)
186
+ - Returns: `{ data, compressed, originalSize, compressedSize, compressionRatio }`
187
+
188
+ #### `clone(): this`
189
+ Create a deep copy of the state.
190
+
191
+ ### Compression Strategy
192
+
193
+ ```typescript
194
+ import { CompressionStrategy, StateEncoder } from '@roomkit/state';
195
+
196
+ // Create custom compression strategy
197
+ const compressionStrategy = new CompressionStrategy({
198
+ threshold: 2048, // Only compress data > 2KB
199
+ minCompressionRatio: 0.7, // Skip if compression ratio > 70%
200
+ level: 9, // Compression level (1-9)
201
+ adaptive: true // Learn from compression history
202
+ });
203
+
204
+ // Use with encoder
205
+ const encoder = new StateEncoder(compressionStrategy);
206
+ const encoded = encoder.encodeFull(state);
207
+
208
+ // Get compression statistics
209
+ const stats = encoder.getCompressionStats();
210
+ console.log(`Compression rate: ${stats.compressionRate * 100}%`);
211
+ console.log(`Average saving: ${stats.averageSaving} bytes`);
212
+ ```
213
+
214
+ ### Batch Update Queue
215
+
216
+ ```typescript
217
+ import { BatchQueue } from '@roomkit/state';
218
+
219
+ const queue = new BatchQueue({
220
+ maxWaitTime: 100, // Flush every 100ms
221
+ maxPatchCount: 50, // Or when 50 patches accumulated
222
+ maxBatchSize: 10240, // Or when 10KB reached
223
+ enablePriority: true // Enable priority queue
224
+ });
225
+
226
+ // Register flush callback
227
+ queue.onFlush((updates) => {
228
+ const patches = updates.flatMap(u => u.patches);
229
+ const encoded = encoder.encodeDelta(patches);
230
+ sendToClients(encoded.data);
231
+ });
232
+
233
+ // Add updates to queue
234
+ setInterval(() => {
235
+ const patches = state.getPatches();
236
+ if (patches.length > 0) {
237
+ queue.enqueue(patches, priority);
238
+ state.clearChanges();
239
+ }
240
+ }, 16); // 60 FPS
241
+
242
+ // Force flush on important events
243
+ queue.forceFlush();
244
+
245
+ // Get statistics
246
+ const stats = queue.getStats();
247
+ console.log(`Batching factor: ${stats.totalEnqueued / stats.totalBatches}x`);
248
+ ```
249
+
250
+ ### Complete Production Example
251
+
252
+ ```typescript
253
+ import {
254
+ State, Field, MapField,
255
+ StateEncoder, StateDecoder,
256
+ CompressionStrategy, BatchQueue
257
+ } from '@roomkit/state';
258
+
259
+ class GameState extends State {
260
+ @MapField(Player) players = new Map();
261
+ @Field('number') tick = 0;
262
+ }
263
+
264
+ // Server setup
265
+ const state = new GameState();
266
+ state.startTracking();
267
+
268
+ const compressionStrategy = new CompressionStrategy({ adaptive: true });
269
+ const encoder = new StateEncoder(compressionStrategy);
270
+ const queue = new BatchQueue({ maxWaitTime: 50 });
271
+
272
+ queue.onFlush((updates) => {
273
+ const patches = updates.flatMap(u => u.patches);
274
+
275
+ // Optimize patches (merge, deduplicate)
276
+ const optimized = BatchQueue.optimizePatches(patches);
277
+
278
+ // Encode with compression
279
+ const encoded = encoder.encodeDelta(optimized);
280
+
281
+ // Send to clients
282
+ broadcast({
283
+ type: 'state_delta',
284
+ data: encoded.data,
285
+ compressed: encoded.compressed
286
+ });
287
+
288
+ // Log stats
289
+ console.log(`Sent ${encoded.compressedSize} bytes (${optimized.length} patches)`);
290
+ });
291
+
292
+ // Game loop
293
+ setInterval(() => {
294
+ // Update game logic
295
+ updateGame(state);
296
+
297
+ // Queue changes
298
+ const patches = state.getPatches();
299
+ if (patches.length > 0) {
300
+ queue.enqueue(patches);
301
+ state.clearChanges();
302
+ }
303
+ }, 16); // 60 FPS
304
+ ```
305
+
306
+ ## Performance Optimization Tips
307
+
308
+ ### 1. Choose Right Compression Threshold
309
+ - **Small messages (< 1KB)**: Don't compress (overhead > benefit)
310
+ - **Medium messages (1-10KB)**: Use level 3-6
311
+ - **Large messages (> 10KB)**: Use level 6-9
312
+
313
+ ### 2. Batch Update Strategy
314
+ - **Real-time games**: 50ms window, prioritize important events
315
+ - **Turn-based games**: 200ms window, accumulate more changes
316
+ - **MMO games**: 100ms window, use priority queue
317
+
318
+ ### 3. Memory Management
319
+ - Call `resetStats()` periodically to avoid stat accumulation
320
+ - Use `forceFlush()` at critical moments
321
+ - Consider state sharding for large-scale applications
322
+
323
+ ## Benchmarking
324
+
325
+ Run the built-in performance tests:
326
+
327
+ ```bash
328
+ cd packages/state
329
+ pnpm benchmark
330
+ ```
331
+
332
+ This will test:
333
+ - Full state encoding (with/without compression)
334
+ - Delta encoding performance
335
+ - Decoding performance
336
+ - Change tracking overhead
337
+ - Batch queue efficiency
338
+ - Compression strategy effectiveness
339
+
340
+ ## Performance
341
+
342
+ - **Full state encoding**: 0.087ms (10 players) → 0.996ms (1000 players)
343
+ - **Delta encoding**: 0.006ms for 3 patches, 158,430 patches/sec
344
+ - **Compression**: 58-73% size reduction for game states
345
+ - **Batch queue**: 20x message frequency reduction
346
+ - **Bandwidth reduction**: 75-85% vs JSON
347
+
348
+ ## Documentation
349
+
350
+ - [Phase 1 Summary](./docs/PHASE1_SUMMARY.md) - Core state system
351
+ - [Phase 2 Summary](./docs/PHASE2_SUMMARY.md) - Tracked collections
352
+ - [Phase 3 Summary](./docs/PHASE3_SUMMARY.md) - Compression & optimization
353
+
354
+ ## License
355
+
356
+ MIT
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @roomkit/state - High-performance state synchronization library
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { State, Field, MapField } from '@roomkit/state';
7
+ *
8
+ * class Player {
9
+ * @Field('string') name: string = '';
10
+ * @Field('number') x: number = 0;
11
+ * }
12
+ *
13
+ * class GameState extends State {
14
+ * @MapField(Player) players = new Map();
15
+ * }
16
+ *
17
+ * const state = new GameState();
18
+ * state.startTracking();
19
+ *
20
+ * // Changes are automatically tracked
21
+ * state.players.set('p1', new Player());
22
+ *
23
+ * // Get patches
24
+ * const patches = state.getPatches();
25
+ *
26
+ * // Encode to binary
27
+ * const encoded = state.encode(true); // full state
28
+ * const delta = state.encode(false); // only changes
29
+ * ```
30
+ */
31
+ export * from './State.js';
32
+ export * from './types.js';
33
+ export * from './decorators/index.js';
34
+ export * from './tracking/ChangeTracker.js';
35
+ export * from './encoding/index.js';
36
+ export * from './collections/index.js';
37
+ export * from './compression/index.js';
38
+ export * from './optimization/index.js';
39
+ export type { Patch, FieldType, FieldMetadata, EncodedState, EncodeOptions, DecodeOptions, TrackingOptions, } from './types.js';
40
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AAGxC,YAAY,EACV,KAAK,EACL,SAAS,EACT,aAAa,EACb,YAAY,EACZ,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /**
3
+ * @roomkit/state - High-performance state synchronization library
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { State, Field, MapField } from '@roomkit/state';
8
+ *
9
+ * class Player {
10
+ * @Field('string') name: string = '';
11
+ * @Field('number') x: number = 0;
12
+ * }
13
+ *
14
+ * class GameState extends State {
15
+ * @MapField(Player) players = new Map();
16
+ * }
17
+ *
18
+ * const state = new GameState();
19
+ * state.startTracking();
20
+ *
21
+ * // Changes are automatically tracked
22
+ * state.players.set('p1', new Player());
23
+ *
24
+ * // Get patches
25
+ * const patches = state.getPatches();
26
+ *
27
+ * // Encode to binary
28
+ * const encoded = state.encode(true); // full state
29
+ * const delta = state.encode(false); // only changes
30
+ * ```
31
+ */
32
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
33
+ if (k2 === undefined) k2 = k;
34
+ var desc = Object.getOwnPropertyDescriptor(m, k);
35
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
36
+ desc = { enumerable: true, get: function() { return m[k]; } };
37
+ }
38
+ Object.defineProperty(o, k2, desc);
39
+ }) : (function(o, m, k, k2) {
40
+ if (k2 === undefined) k2 = k;
41
+ o[k2] = m[k];
42
+ }));
43
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
44
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ __exportStar(require("./State.js"), exports);
48
+ __exportStar(require("./types.js"), exports);
49
+ __exportStar(require("./decorators/index.js"), exports);
50
+ __exportStar(require("./tracking/ChangeTracker.js"), exports);
51
+ __exportStar(require("./encoding/index.js"), exports);
52
+ __exportStar(require("./collections/index.js"), exports);
53
+ __exportStar(require("./compression/index.js"), exports);
54
+ __exportStar(require("./optimization/index.js"), exports);
55
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;;;;;;;;;;;;;;;;AAEH,6CAA2B;AAC3B,6CAA2B;AAC3B,wDAAsC;AACtC,8DAA4C;AAC5C,sDAAoC;AACpC,yDAAuC;AACvC,yDAAuC;AACvC,0DAAwC"}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@roomkit/state",
3
+ "version": "1.0.0",
4
+ "description": "High-performance state synchronization library inspired by Colyseus Schema",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./decorators": {
15
+ "types": "./dist/decorators/index.d.ts",
16
+ "import": "./dist/decorators/index.js"
17
+ },
18
+ "./collections": {
19
+ "types": "./dist/collections/index.d.ts",
20
+ "import": "./dist/collections/index.js"
21
+ },
22
+ "./encoding": {
23
+ "types": "./dist/encoding/index.d.ts",
24
+ "import": "./dist/encoding/index.js"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "build:examples": "tsc --project tsconfig.examples.json",
30
+ "dev": "tsc --watch",
31
+ "clean": "rm -rf dist dist-examples",
32
+ "benchmark": "pnpm build:examples && node dist-examples/benchmarks/performance.js",
33
+ "test": "jest"
34
+ },
35
+ "keywords": [
36
+ "state",
37
+ "synchronization",
38
+ "schema",
39
+ "binary",
40
+ "msgpack",
41
+ "realtime",
42
+ "game"
43
+ ],
44
+ "author": "",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/daxing/gateway-worker.git",
49
+ "directory": "packages/state"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/daxing/gateway-worker/issues"
53
+ },
54
+ "homepage": "https://github.com/daxing/gateway-worker#readme",
55
+ "files": [
56
+ "dist",
57
+ "README.md",
58
+ "LICENSE"
59
+ ],
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "dependencies": {
64
+ "@msgpack/msgpack": "^3.0.0-beta2",
65
+ "fast-json-patch": "^3.1.1",
66
+ "pako": "^2.1.0",
67
+ "reflect-metadata": "^0.2.1"
68
+ },
69
+ "devDependencies": {
70
+ "@types/node": "^20.10.0",
71
+ "@types/pako": "^2.0.3",
72
+ "typescript": "^5.3.3"
73
+ }
74
+ }