# @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-metadata` dependency
- 🎮 **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

```bash
pnpm add @roomkit/state
```

**Requirements**: 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)

```typescript
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

```typescript
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

```typescript
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.

```typescript
@Field('string') name: string = '';
@Field('number') health: number = 100;
@Field('boolean') isAlive: boolean = true;
```

### TrackedMap

```typescript
@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

```typescript
@ArrayField('number') scores: number[] = [];
```

Automatically converts to `TrackedArray` which tracks:
- `push()`, `pop()`, `shift()`, `unshift()`
- `splice()`, `sort()`, `reverse()`
- Array element assignments

### TrackedSet

```typescript
@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 state
- `full=false`: Encode only changes (delta)
- Returns: `{ data, compressed, originalSize, compressedSize, compressionRatio }`

#### `clone(): this`
Create a deep copy of the state.

### Compression Strategy

```typescript
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

```typescript
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

```typescript
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 FPS
```

## Performance 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:

```bash
cd packages/state
pnpm benchmark
```

This 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](./docs/PHASE1_SUMMARY.md) - Core state system
- [Phase 2 Summary](./docs/PHASE2_SUMMARY.md) - Tracked collections
- [Phase 3 Summary](./docs/PHASE3_SUMMARY.md) - Compression & optimization

## License

MIT
