npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@roomkit/state

v1.2.1

Published

High-performance state synchronization library with TypeScript 5.0+ decorators

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

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)

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 state
  • full=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 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:

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

License

MIT