@roomkit/state
v1.2.1
Published
High-performance state synchronization library with TypeScript 5.0+ decorators
Maintainers
Readme
@roomkit/state
High-performance state synchronization library with TypeScript 5.0+ decorators.
✨ What's New in v1.2.0
- 🎯 TypeScript 5.0+ Decorators: Now using standard decorators (no more
experimentalDecorators!) - ⚡ Better Type Safety: Improved type inference and IDE support
- 📦 Smaller Bundle: Removed
reflect-metadatadependency - 🎮 Zero Breaking Changes: Same API, better internals
Features
- 🎯 Type-Safe: Decorator-based schema definition with TypeScript 5.0+ support
- ⚡ High Performance: Binary encoding with MessagePack, <1ms for 1000 entities
- 📦 Small Payloads: 75-85% bandwidth reduction with compression
- 🔄 Automatic Tracking: Automatic change detection for all data types
- 🎮 Game-Ready: Tracked collections (Map, Array, Set) with automatic sync
- 📊 Delta Updates: JSON Patch format for incremental state changes
- 🧩 Deep Nesting: Automatic tracking of nested objects and collections
- 🗜️ Smart Compression: Adaptive compression with entropy detection
- 📦 Batch Updates: Queue system to reduce message frequency by 20x
- 📈 Performance Monitoring: Built-in statistics and benchmarking
Installation
pnpm add @roomkit/stateRequirements: TypeScript 5.0+ (for decorator support)
Performance Benchmarks
| Scale | Players | Encoding Time | Data Size | Compression | Throughput | |-------|---------|---------------|-----------|-------------|------------| | Small | 10 | 0.087ms | 474 bytes | 58.1% saved | 11,504/sec | | Medium| 100 | 0.210ms | 3.3 KB | 70.6% saved | 4,768/sec | | Large | 1000 | 0.996ms | 32 KB | 72.5% saved | 1,004/sec |
Delta Updates: 0.006ms for 3 patches, 158,430 patches/sec Batch Queue: 20x message reduction, 4.25ms for 5000 patches Change Tracking: 0.010ms per cycle, 104,261 cycles/sec
Quick Start
Using Tracked Collections (Recommended)
import { State, Field, MapField, TrackedMap } from '@roomkit/state';
class Player {
@Field('string') name: string = '';
@Field('number') score: number = 0;
}
class GameState extends State {
@MapField(Player) players = new Map<string, Player>();
@Field('string') status: string = 'waiting';
}
// Usage
const state = new GameState();
state.startTracking(); // Automatically converts to TrackedMap
// All modifications are automatically tracked
state.players.set('p1', newPlayer);
state.status = 'playing';
// Get changes
const patches = state.getPatches(); // JSON Patch format
const encoded = state.encode(); // Binary MessagePack
// Clear changes after sync
state.clearChanges();Direct Tracked Collections Usage
import { TrackedMap, TrackedArray, TrackedSet } from '@roomkit/state';
// TrackedMap
const players = new TrackedMap<string, Player>();
players.startTracking();
players.onChange((patches) => {
console.log('Players changed:', patches);
});
players.set('p1', new Player());
// Triggers onChange with patches
// TrackedArray
const items = new TrackedArray<string>();
items.startTracking();
items.push('item1', 'item2');
// TrackedSet
const tags = new TrackedSet<string>();
tags.startTracking();
tags.add('tag1');Client-side State Synchronization
import { StateDecoder } from '@roomkit/state/encoding';
class ClientGameState {
players = new Map<string, any>();
scores: number[] = [];
status: string = 'waiting';
round: number = 0;
}
const decoder = new StateDecoder();
const state = new ClientGameState();
// Full state sync
room.onMessage(MessageId.STATE_FULL, (message) => {
const decoded = decoder.decode(message.data);
Object.assign(state, decoded);
});
// Delta updates
room.onMessage(MessageId.STATE_DELTA, (message) => {
decoder.applyPatches(state, message.patches);
});API Reference
Decorators
@Field(type: FieldType)
Define a primitive field.
@Field('string') name: string = '';
@Field('number') health: number = 100;
@Field('boolean') isAlive: boolean = true;TrackedMap
@MapField(Player) players = new Map<string, Player>();When startTracking() is called, automatically converts to TrackedMap which:
- Tracks
set(),delete(),clear()operations - Emits change events via
onChange(callback) - Supports batch operations with
batch(fn)
TrackedArray
@ArrayField('number') scores: number[] = [];Automatically converts to TrackedArray which tracks:
push(),pop(),shift(),unshift()splice(),sort(),reverse()- Array element assignments
TrackedSet
@SetField('string') tags = new Set<string>();Automatically converts to TrackedSet which tracks:
add(),delete(),clear()- All modifications to the set
State Class
startTracking()
Start tracking changes to the state.
stopTracking()
Stop tracking changes.
getPatches(): Patch[]
Get accumulated changes as JSON Patch operations.
clearChanges()
Clear accumulated changes.
encode(full?: boolean, options?: EncodeOptions): EncodedState
Encode state to binary format.
full=true: Encode entire statefull=false: Encode only changes (delta)- Returns:
{ data, compressed, originalSize, compressedSize, compressionRatio }
clone(): this
Create a deep copy of the state.
Compression Strategy
import { CompressionStrategy, StateEncoder } from '@roomkit/state';
// Create custom compression strategy
const compressionStrategy = new CompressionStrategy({
threshold: 2048, // Only compress data > 2KB
minCompressionRatio: 0.7, // Skip if compression ratio > 70%
level: 9, // Compression level (1-9)
adaptive: true // Learn from compression history
});
// Use with encoder
const encoder = new StateEncoder(compressionStrategy);
const encoded = encoder.encodeFull(state);
// Get compression statistics
const stats = encoder.getCompressionStats();
console.log(`Compression rate: ${stats.compressionRate * 100}%`);
console.log(`Average saving: ${stats.averageSaving} bytes`);Batch Update Queue
import { BatchQueue } from '@roomkit/state';
const queue = new BatchQueue({
maxWaitTime: 100, // Flush every 100ms
maxPatchCount: 50, // Or when 50 patches accumulated
maxBatchSize: 10240, // Or when 10KB reached
enablePriority: true // Enable priority queue
});
// Register flush callback
queue.onFlush((updates) => {
const patches = updates.flatMap(u => u.patches);
const encoded = encoder.encodeDelta(patches);
sendToClients(encoded.data);
});
// Add updates to queue
setInterval(() => {
const patches = state.getPatches();
if (patches.length > 0) {
queue.enqueue(patches, priority);
state.clearChanges();
}
}, 16); // 60 FPS
// Force flush on important events
queue.forceFlush();
// Get statistics
const stats = queue.getStats();
console.log(`Batching factor: ${stats.totalEnqueued / stats.totalBatches}x`);Complete Production Example
import {
State, Field, MapField,
StateEncoder, StateDecoder,
CompressionStrategy, BatchQueue
} from '@roomkit/state';
class GameState extends State {
@MapField(Player) players = new Map();
@Field('number') tick = 0;
}
// Server setup
const state = new GameState();
state.startTracking();
const compressionStrategy = new CompressionStrategy({ adaptive: true });
const encoder = new StateEncoder(compressionStrategy);
const queue = new BatchQueue({ maxWaitTime: 50 });
queue.onFlush((updates) => {
const patches = updates.flatMap(u => u.patches);
// Optimize patches (merge, deduplicate)
const optimized = BatchQueue.optimizePatches(patches);
// Encode with compression
const encoded = encoder.encodeDelta(optimized);
// Send to clients
broadcast({
type: 'state_delta',
data: encoded.data,
compressed: encoded.compressed
});
// Log stats
console.log(`Sent ${encoded.compressedSize} bytes (${optimized.length} patches)`);
});
// Game loop
setInterval(() => {
// Update game logic
updateGame(state);
// Queue changes
const patches = state.getPatches();
if (patches.length > 0) {
queue.enqueue(patches);
state.clearChanges();
}
}, 16); // 60 FPSPerformance Optimization Tips
1. Choose Right Compression Threshold
- Small messages (< 1KB): Don't compress (overhead > benefit)
- Medium messages (1-10KB): Use level 3-6
- Large messages (> 10KB): Use level 6-9
2. Batch Update Strategy
- Real-time games: 50ms window, prioritize important events
- Turn-based games: 200ms window, accumulate more changes
- MMO games: 100ms window, use priority queue
3. Memory Management
- Call
resetStats()periodically to avoid stat accumulation - Use
forceFlush()at critical moments - Consider state sharding for large-scale applications
Benchmarking
Run the built-in performance tests:
cd packages/state
pnpm benchmarkThis will test:
- Full state encoding (with/without compression)
- Delta encoding performance
- Decoding performance
- Change tracking overhead
- Batch queue efficiency
- Compression strategy effectiveness
Performance
- Full state encoding: 0.087ms (10 players) → 0.996ms (1000 players)
- Delta encoding: 0.006ms for 3 patches, 158,430 patches/sec
- Compression: 58-73% size reduction for game states
- Batch queue: 20x message frequency reduction
- Bandwidth reduction: 75-85% vs JSON
Documentation
- Phase 1 Summary - Core state system
- Phase 2 Summary - Tracked collections
- Phase 3 Summary - Compression & optimization
License
MIT
