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

@hpx7/delta-pack

v0.2.8

Published

A TypeScript code generator and runtime for binary serialization based on schemas.

Readme

Delta-Pack TypeScript

TypeScript implementation of delta-pack, a compact binary serialization format with efficient delta compression for real-time state synchronization.

Installation

npm install @hpx7/delta-pack

Quick Start

Delta-pack provides three approaches for working with schemas:

  1. Interpreter Mode - Runtime schema parsing with dynamic API
  2. Decorator Mode - Use TypeScript decorators for class-based schemas (builds on interpreter mode)
  3. Codegen Mode - Generate TypeScript code from schemas for compile-time type safety

Interpreter Mode (Recommended for prototyping)

import { ObjectType, StringType, IntType, load, Infer } from "@hpx7/delta-pack";

// Define schema type directly
const Player = ObjectType("Player", {
  id: StringType(),
  name: StringType(),
  score: IntType(),
});

// Infer TypeScript type
type Player = Infer<typeof Player>;
// Result: { id: string; name: string; score: number }

// Load API for the type
const PlayerApi = load(Player);

// Use the API
const player = { id: "p1", name: "Alice", score: 100 };
const encoded = PlayerApi.encode(player);
const decoded = PlayerApi.decode(encoded);

Decorator Mode (Recommended for class-based code)

import { StringType, IntType, loadClass } from "@hpx7/delta-pack";

class Player {
  @StringType()
  id: string = "";

  @StringType()
  name: string = "";

  @IntType()
  score: number = 0;
}

// Load API from decorated class
const PlayerApi = loadClass(Player);

// Use the API
const player = new Player();
player.id = "p1";
player.name = "Alice";
player.score = 100;

const encoded = PlayerApi.encode(player);
const decoded = PlayerApi.decode(encoded);

Codegen Mode (Recommended for production)

Create a YAML schema file (schema.yml):

# yaml-language-server: $schema=https://raw.githubusercontent.com/hpx7/delta-pack/refs/heads/main/schema.json

Player:
  id: string
  name: string
  score: int

Generate TypeScript code using the CLI:

npx delta-pack generate schema.yml -l typescript -o generated.ts

Then use the generated code:

import { Player } from "./generated";

const player: Player = { id: "p1", name: "Alice", score: 100 };
const encoded = Player.encode(player);
const decoded = Player.decode(encoded);

Schema Definition

Schemas can be defined in two ways:

  1. YAML - Human-readable format, useful for defining schemas separately from code. Parse with parseSchemaYml() for interpreter mode.
  2. TypeScript - Define schemas using the type constructor functions. Works with both interpreter and codegen modes. Required for using the Infer<> type utility.

Note: The Infer<> type utility only works with TypeScript-defined schemas, not YAML-parsed schemas, since it requires compile-time type information.

YAML Schema

Create a schema.yml file:

# yaml-language-server: $schema=https://raw.githubusercontent.com/hpx7/delta-pack/refs/heads/main/schema.json

# Enums (list of string literals)
Team:
  - RED
  - BLUE

# Objects
Player:
  id: string
  name: string
  score: int
  team: Team
  position: Position?

Position:
  x: float(precision=0.1)
  y: float(precision=0.1)

# Union types (list of type references)
# A union is a list where all items are defined type names
MoveAction:
  x: int
  y: int

AttackAction:
  targetId: string
  damage: uint # shorthand for int(min=0)

GameAction:
  - MoveAction
  - AttackAction

# Complex types
GameState:
  players: <string, Player>
  actions: GameAction[]
  round: uint
  phase: string

# Bounded integers
BoundedExample:
  health: int(min=0, max=100) # validated in fromJson, encoded as unsigned
  level: int(min=1, max=99)
  temperature: int(min=-50, max=50)

Parse YAML schemas with:

import { parseSchemaYml } from "@hpx7/delta-pack";
import { readFileSync } from "fs";

const schemaYml = readFileSync("schema.yml", "utf8");
const schema = parseSchemaYml(schemaYml);

See the main README for complete schema syntax reference.

TypeScript Schema

Define schemas using the type constructor functions. Named types (ObjectType, EnumType, UnionType) take a required name as the first parameter:

import {
  ObjectType,
  StringType,
  IntType,
  UIntType,
  FloatType,
  ArrayType,
  OptionalType,
  RecordType,
  EnumType,
  UnionType,
  ReferenceType,
} from "@hpx7/delta-pack";

// Enum type
const Team = EnumType("Team", ["RED", "BLUE"]);

// Object types - define in dependency order
const Position = ObjectType("Position", {
  x: FloatType({ precision: 0.1 }),
  y: FloatType({ precision: 0.1 }),
});

const Player = ObjectType("Player", {
  id: StringType(),
  name: StringType(),
  score: IntType(),
  team: ReferenceType(Team),
  position: OptionalType(ReferenceType(Position)),
});

// Union type - options are direct type references
const MoveAction = ObjectType("MoveAction", {
  x: IntType(),
  y: IntType(),
});

const AttackAction = ObjectType("AttackAction", {
  targetId: StringType(),
  damage: UIntType(),
});

const GameAction = UnionType("GameAction", [MoveAction, AttackAction]);

// Using unions in other types
const GameState = ObjectType("GameState", {
  players: RecordType(StringType(), ReferenceType(Player)),
  actions: ArrayType(ReferenceType(GameAction)),
  round: UIntType(),
  phase: StringType(),
});

Union value format: Union values use a flattened discriminated union format with _type:

// Creating a union value
const action: GameAction = { _type: "MoveAction", x: 10, y: 20 };

// Type narrowing with discriminant
if (action._type === "MoveAction") {
  console.log(action.x, action.y);
}

Interpreter API

The interpreter mode provides a runtime API for working with schemas.

Loading a Schema

import { ObjectType, StringType, IntType, load, Infer } from "@hpx7/delta-pack";

// Define schema type
const Player = ObjectType("Player", {
  id: StringType(),
  name: StringType(),
  score: IntType(),
});

// Infer TypeScript type
type Player = Infer<typeof Player>;
// Result: { id: string; name: string; score: number }

// Load interpreter API
const PlayerApi = load(Player);

API Methods

Every loaded type provides these methods:

fromJson(obj: Record<string, unknown>): T

Validates and parses JSON data, throwing if invalid. Use this when parsing untrusted or untyped data:

// Parse unvalidated JSON data
const jsonData = JSON.parse(networkResponse);
const player = Player.fromJson(jsonData);

For most cases, prefer using TypeScript types directly:

// Preferred: use TypeScript types for compile-time safety
const player: Player = { id: "p1", name: "Alice", score: 100 };

toJson(obj: T): Record<string, unknown>

Converts an object to JSON-serializable format. Useful for serializing to JSON or sending over HTTP:

const player: Player = { id: "p1", name: "Alice", score: 100 };
const json = Player.toJson(player);
const jsonString = JSON.stringify(json);

Format notes:

  • Maps (RecordType) are converted to plain objects
  • Optional object properties with undefined values are excluded from the JSON
  • Unions are converted to protobuf format: { TypeName: {...} }

Example with unions:

const action: GameAction = { _type: "MoveAction", x: 10, y: 20 };
const json = GameAction.toJson(action);
// Result: { MoveAction: { x: 10, y: 20 } }

The JSON output uses protobuf format ({ TypeName: {...} }) which can be parsed back with fromJson().

encode(obj: T): Uint8Array

Serializes an object to binary format:

const player = { id: "p1", name: "Alice", score: 100 };
const bytes = Player.encode(player);
console.log(`Encoded size: ${bytes.length} bytes`);

decode(bytes: Uint8Array): T

Deserializes binary data back to an object:

const decoded = Player.decode(bytes);
// decoded = { id: 'p1', name: 'Alice', score: 100 }

encodeDiff(oldObj: T, newObj: T): Uint8Array

Encodes only the differences between two objects:

const oldPlayer = { id: "p1", name: "Alice", score: 100 };
const newPlayer = { id: "p1", name: "Alice", score: 150 };

const diff = Player.encodeDiff(oldPlayer, newPlayer);
console.log(`Diff size: ${diff.length} bytes`); // Much smaller!

decodeDiff(oldObj: T, diffBytes: Uint8Array): T

Applies a diff to reconstruct the new object:

const reconstructed = Player.decodeDiff(oldPlayer, diff);
// reconstructed = { id: 'p1', name: 'Alice', score: 150 }

equals(a: T, b: T): boolean

Deep equality comparison with appropriate tolerance for floats:

const isEqual = Player.equals(player1, player2);

For quantized floats (with precision), equality uses quantized value comparison. For non-quantized floats, equality uses epsilon-based comparison (0.00001 tolerance).

clone(obj: T): T

Creates a deep clone of an object:

const player1 = { id: "p1", name: "Alice", score: 100 };
const player2 = Player.clone(player1);

// Modifying the clone doesn't affect the original
player2.score = 200;
console.log(player1.score); // 100
console.log(player2.score); // 200

Important notes:

  • Creates deep copies of all nested objects, arrays, and maps
  • Primitives (strings, numbers, booleans) are copied by value
  • The _dirty field is not preserved in clones (clones always start clean)
  • Useful for creating modified copies without mutating the original state

Decorator API

The decorator mode provides a class-based API using TypeScript decorators. Schemas are inferred from decorated classes at runtime.

Decorators

The same type functions used to build schemas (StringType(), IntType(), ArrayType(), etc.) also work as property decorators. See Type Reference for the complete list.

Additional decorator-specific features:

| Function | Description | | ----------------------- | --------------------------------------------- | | EnumType("Name", []) | Define an enum type | | UnionType("Name", []) | Define a union from a list of variant classes | | ReferenceType(ref) | Reference a class, enum, or union |

Referencing classes, enums, and unions:

  • Use ReferenceType(ClassName) to reference another class
  • Use EnumType("Name", [...]) to define an enum, then ReferenceType(EnumDef) to use it
  • Use UnionType("Name", [ClassA, ClassB]) to define a union, then ReferenceType(UnionDef) to use it

Example

import {
  StringType,
  IntType,
  UIntType,
  FloatType,
  BooleanType,
  ReferenceType,
  ArrayType,
  RecordType,
  OptionalType,
  EnumType,
  UnionType,
  Infer,
  loadClass,
} from "@hpx7/delta-pack";

// Enum type
const Team = EnumType("Team", ["RED", "BLUE"]);
type Team = Infer<typeof Team>;

// Nested object
class Position {
  @FloatType({ precision: 0.1 }) x: number = 0;
  @FloatType({ precision: 0.1 }) y: number = 0;
}

// Union types - define variant classes, then combine with UnionType
class MoveAction {
  @IntType() x: number = 0;
  @IntType() y: number = 0;
}

class AttackAction {
  @StringType() targetId: string = "";
  @UIntType() damage: number = 0;
}

// Define union type - combines variant classes
const GameAction = UnionType("GameAction", [MoveAction, AttackAction]);
type GameAction = Infer<typeof GameAction>;

// Main class
class Player {
  @StringType() id: string = "";
  @StringType() name: string = "";
  @IntType() score: number = 0;
  @BooleanType() isOnline: boolean = false;

  @ReferenceType(Position) position: Position = new Position();
  @ReferenceType(Team) team: Team = Team.RED;

  @ArrayType(StringType()) tags: string[] = [];
  @ArrayType(ReferenceType(Position)) waypoints: Position[] = [];
  @RecordType(StringType(), IntType()) inventory: Map<string, number> = new Map();

  @OptionalType(StringType()) nickname?: string;
  @OptionalType(ReferenceType(GameAction)) lastAction?: GameAction;
}

// Load API and use
const PlayerApi = loadClass(Player);
const player = new Player();
player.name = "Alice";
const encoded = PlayerApi.encode(player);
const decoded = PlayerApi.decode(encoded);

Advanced patterns (nested containers, number modifiers):

class AdvancedExample {
  @ArrayType(UIntType()) unsignedInts: number[] = [];
  @ArrayType(FloatType({ precision: 0.01 })) floats: number[] = [];
  @ArrayType(ArrayType(IntType())) matrix: number[][] = [];
  @RecordType(StringType(), ArrayType(IntType())) vectorsById: Map<string, number[]> = new Map();
  @OptionalType(ReferenceType(Player)) partner?: Player; // Self-reference
}

Working with Union Types

With the decorator API, union types are defined using UnionType() to combine variant classes:

// Define variant classes (regular decorated classes)
class JoinMessage {
  @StringType() name: string = "";
}

class InputMessage {
  @ReferenceType(ClientInput) input: ClientInput = new ClientInput();
}

// Define union type - combines variants
const ClientMessage = UnionType("ClientMessage", [JoinMessage, InputMessage]);
type ClientMessage = Infer<typeof ClientMessage>;

// Load the API from the union
const ClientMessageApi = loadClass(ClientMessage);

Encoding: Pass variant class instances directly:

const joinMsg = new JoinMessage();
joinMsg.name = "Alice";
const encoded = ClientMessageApi.encode(joinMsg);

Decoding: Returns variant class instances. Use instanceof to narrow the type:

const message = ClientMessageApi.decode(encoded);

if (message instanceof JoinMessage) {
  console.log(message.name); // TypeScript knows this is JoinMessage
} else if (message instanceof InputMessage) {
  console.log(message.input);
}

buildSchema()

If you need access to the generated schema (e.g., for inspection or to use with load() directly), use buildSchema():

import { buildSchema, load } from "@hpx7/delta-pack";

const schema = buildSchema(Player);
console.log(schema); // { Player: { type: "object", properties: { ... }, name: "Player" } }

// Use with load() for more control
const PlayerApi = load(schema["Player"]);

Requirements

tsconfig.json: Enable "experimentalDecorators": true

Class requirements:

  1. Parameterless constructor (instantiable with new Class())
  2. Decorated properties must have default values (discovered via Object.keys(new Class()))
  3. At least one property must have a type decorator
  4. Unique class names (used as schema identifiers)

Note: Properties without decorators are ignored (not serialized). Methods are also allowed. This lets you add helper methods or transient state to your classes.

API Methods

loadClass() returns the same API as load() in interpreter mode:

| Method | Description | | ----------------------- | ------------------------------------ | | encode(obj) | Serialize to binary | | decode(bytes) | Deserialize from binary | | encodeDiff(old, new) | Encode delta between two states | | decodeDiff(old, diff) | Apply delta to reconstruct new state | | equals(a, b) | Deep equality comparison | | clone(obj) | Deep clone | | toJson(obj) | Convert to JSON-serializable format | | fromJson(obj) | Validate and parse JSON data |

Key difference: In decorator mode, decode, decodeDiff, clone, and fromJson return proper class instances—not plain objects. This includes union variants (e.g., decoding a ClientMessage returns a JoinMessage or InputMessage instance).

Schema API vs Decorator API

| Aspect | Schema API | Decorator API | | ----------------- | -------------------------------------------- | ------------------------------------------- | | Type constructors | StringType(), ArrayType(), etc. | Same - work as decorators too | | Object types | ObjectType("Name", { ... }) | Class with decorated properties | | Enums | EnumType("Name", [...]) | Same - EnumType("Name", [...]) | | Unions | UnionType("Name", [TypeA, TypeB]) | UnionType("Name", [ClassA, ClassB]) | | References | Type references: ReferenceType(PlayerType) | Class/enum/union refs: ReferenceType(ref) | | Loading | load(RootType) | loadClass(Class) or loadClass(UnionDef) | | Return types | Plain objects | Class instances with methods |

Codegen API

The codegen mode generates TypeScript code from schemas for compile-time type safety. Code generation is handled by the @hpx7/delta-pack-cli package.

Generating Code

# Install CLI globally or use npx
npm install -g @hpx7/delta-pack-cli

# Generate from YAML schema
delta-pack generate schema.yml -l typescript -o generated.ts

# Or use npx
npx delta-pack generate schema.yml -l typescript -o generated.ts

For programmatic code generation, import from the CLI package:

import { codegenTypescript } from "@hpx7/delta-pack-cli/codegen";
import { parseSchemaYml } from "@hpx7/delta-pack";
import { readFileSync, writeFileSync } from "fs";

const schemaYml = readFileSync("schema.yml", "utf8");
const schema = parseSchemaYml(schemaYml);
const code = codegenTypescript(schema);
writeFileSync("generated.ts", code);

Using Generated Code

The generated code exports TypeScript types and runtime objects:

import { Player, GameState } from "./generated";

// TypeScript types are available
const player: Player = {
  id: "p1",
  name: "Alice",
  score: 100,
};

// Runtime objects provide the same API as interpreter mode
const encoded = Player.encode(player);
const decoded = Player.decode(encoded);

Generated API

The generated code provides the same methods as interpreter mode:

  • Player.fromJson(obj) - Validate and parse JSON data
  • Player.toJson(obj) - Convert to JSON-serializable format
  • Player.encode(obj) - Serialize to binary
  • Player.decode(bytes) - Deserialize from binary
  • Player.encodeDiff(old, new) - Encode delta
  • Player.decodeDiff(old, diff) - Apply delta
  • Player.equals(a, b) - Deep equality
  • Player.clone(obj) - Deep clone object
  • Player.default() - Default instance

Complete Example

Multiplayer Game State Sync

Using Interpreter Mode:

import {
  ObjectType,
  StringType,
  UIntType,
  FloatType,
  IntType,
  EnumType,
  ReferenceType,
  RecordType,
  load,
  Infer,
} from "@hpx7/delta-pack";

// Define schema types
const Team = EnumType("Team", ["RED", "BLUE"]);

const Position = ObjectType("Position", {
  x: FloatType(),
  y: FloatType(),
});

const Player = ObjectType("Player", {
  id: StringType(),
  username: StringType(),
  team: ReferenceType(Team),
  position: ReferenceType(Position),
  health: UIntType(),
  score: IntType(),
});

const GameState = ObjectType("GameState", {
  players: RecordType(StringType(), ReferenceType(Player)),
  round: UIntType(),
  timeRemaining: FloatType(),
});

// Infer types
type GameStateType = Infer<typeof GameState>;

// Load API
const GameStateApi = load(GameState);

// Create initial state
const state1: GameStateType = {
  players: new Map([
    [
      "p1",
      {
        id: "p1",
        username: "Alice",
        team: "RED",
        position: { x: 100, y: 100 },
        health: 100,
        score: 0,
      },
    ],
  ]),
  round: 1,
  timeRemaining: 600.0,
};

// Updated state (player moved)
const state2: GameStateType = {
  ...state1,
  players: new Map([["p1", { ...state1.players.get("p1")!, position: { x: 105, y: 102 } }]]),
  timeRemaining: 599.0,
};

// Delta encoding
const diff = GameStateApi.encodeDiff(state1, state2);
const reconstructed = GameStateApi.decodeDiff(state1, diff);

Using Decorator Mode:

import {
  StringType,
  UIntType,
  FloatType,
  IntType,
  ReferenceType,
  RecordType,
  EnumType,
  Infer,
  loadClass,
} from "@hpx7/delta-pack";

const Team = EnumType("Team", ["RED", "BLUE"]);
type Team = Infer<typeof Team>;

class Position {
  @FloatType() x: number = 0;
  @FloatType() y: number = 0;
}

class Player {
  @StringType() id: string = "";
  @StringType() username: string = "";
  @ReferenceType(Team) team: Team = "RED";
  @ReferenceType(Position) position: Position = new Position();
  @UIntType() health: number = 0;
  @IntType() score: number = 0;
}

class GameState {
  @RecordType(StringType(), ReferenceType(Player)) players: Map<string, Player> = new Map();
  @UIntType() round: number = 0;
  @FloatType() timeRemaining: number = 0;
}

// Load API
const GameStateApi = loadClass(GameState);

// Create initial state
const state1 = new GameState();
state1.round = 1;
state1.timeRemaining = 600.0;

const player = new Player();
player.id = "p1";
player.username = "Alice";
player.team = "RED";
player.position.x = 100;
player.position.y = 100;
player.health = 100;
state1.players.set("p1", player);

// Updated state (player moved)
const state2 = GameStateApi.clone(state1);
state2.players.get("p1")!.position.x = 105;
state2.players.get("p1")!.position.y = 102;
state2.timeRemaining = 599.0;

// Delta encoding
const diff = GameStateApi.encodeDiff(state1, state2);
const reconstructed = GameStateApi.decodeDiff(state1, diff);

Using Codegen Mode:

Create a schema file (game.schema.yml):

# yaml-language-server: $schema=https://raw.githubusercontent.com/hpx7/delta-pack/refs/heads/main/schema.json

Team:
  - RED
  - BLUE

Position:
  x: float
  y: float

Player:
  id: string
  username: string
  team: Team
  position: Position
  health: uint
  score: int

GameState:
  players: <string, Player>
  round: uint
  timeRemaining: float

Generate the code:

npx delta-pack generate game.schema.yml -l typescript -o generated.ts

Then use the generated code:

import { GameState, Player } from "./generated";

// TypeScript types are available at compile time
const state: GameState = GameState.default();
state.players.set("p1", {
  id: "p1",
  username: "Alice",
  team: "RED",
  position: { x: 100, y: 100 },
  health: 100,
  score: 0,
});

// Same API as interpreter mode
const encoded = GameState.encode(state);
const decoded = GameState.decode(encoded);

Performance Tips

Delta Compression

Delta encoding is most effective when:

  • State changes are incremental (only a few fields change per update)
  • You send updates frequently (e.g., 60 times per second in games)
  • Objects are medium to large (>50 bytes)

Typical bandwidth savings:

  • Position-only updates: 90-95% smaller
  • Single field changes: 85-90% smaller
  • Multiple field changes: 70-85% smaller

Dirty Tracking with track()

For maximum encodeDiff performance, use track() to automatically track which fields have changed. This allows delta encoding to skip comparison checks entirely:

import { track, clearTracking } from "@hpx7/delta-pack";

// Wrap state with tracking
const state = track({
  tick: 0,
  player: { x: 0, y: 0 },
  players: new Map([["p1", { x: 0, y: 0 }]]),
});

// Mutations are automatically tracked
state.tick = 1; // Marks "tick" dirty
state.player.x = 100; // Marks "x" on player, "player" on state
state.players.get("p1")!.x = 50; // Marks "x" on player, "p1" on players, "players" on state

// Efficient delta encoding - only changed fields are compared
const diff = GameStateApi.encodeDiff(oldState, state);

// Clear tracking for next update cycle
clearTracking(state);

Deep tracking: Changes to nested objects, arrays, and maps automatically propagate up to parents. This works with:

  • Nested objects: state.player.x = 100 marks both player.x and state.player
  • Arrays: state.items[0].value = 99 marks the item, index 0, and state.items
  • Maps: state.players.get("p1")!.x = 50 marks the player, key "p1", and state.players
  • Array methods: push, pop, shift, unshift, splice all mark appropriate indices

Game loop pattern:

class Game {
  private state = track(new GameState());

  tick() {
    // Mutations are automatically tracked
    this.state.tick++;
    for (const [id, input] of this.inputs) {
      const player = this.state.players.get(id)!;
      player.x += input.vx;
      player.y += input.vy;
    }
  }

  broadcast() {
    for (const client of this.clients) {
      const diff = GameStateApi.encodeDiff(client.lastState, this.state);
      client.send(diff);
      client.lastState = GameStateApi.clone(this.state);
    }
    clearTracking(this.state); // Reset for next tick
  }
}

The Tracked<T> type: The track() function returns a Tracked<T> which recursively adds _dirty sets at every level of the object tree.

When to use tracking:

  • High-frequency updates (e.g., 20-60 times per second)
  • Large state objects where full comparison is expensive
  • Mutable state that changes incrementally each tick

Manual _dirty (alternative): You can also set _dirty directly without using track():

// Objects: track changed fields
player.score = 150;
player._dirty = new Set(["score"]);

// Arrays: track changed indices
items[5] = newItem;
items._dirty = new Set([5]);

// Maps: track changed keys
players.set("p1", updatedPlayer);
players._dirty = new Set(["p1"]);

The _dirty field is:

  • Optional: If absent, full comparison is performed
  • Type-safe: Set<keyof T> for objects, Set<number> for arrays, Set<K> for maps
  • Included in types: Both codegen and interpreter types include _dirty
  • Not serialized: The _dirty field is never encoded in the binary format

Quantized Floats

Use precision for floats to reduce size:

const Position = ObjectType("Position", {
  x: FloatType({ precision: 0.1 }), // ~10cm precision
  y: FloatType({ precision: 0.1 }),
});

This enables delta compression to skip encoding unchanged floats even if they differ slightly due to floating-point imprecision.

String Dictionary

Strings are automatically deduplicated within each encoding operation. Reuse common strings (player IDs, item names, etc.) to benefit from dictionary compression.

Map vs Array

  • Use RecordType (maps) when entities have unique IDs
  • Use ArrayType when order matters or IDs aren't meaningful

Maps enable efficient delta encoding for entity collections (only changed entities are encoded).

Examples

See the examples/ directory for complete examples:

  • examples/primitives/ - Basic primitive types
  • examples/user/ - User profile with unions and optionals
  • examples/game/ - Multiplayer game with complex state

Each example includes:

  • schema.yml - Schema definition
  • state1.json, state2.json, ... - Example states demonstrating delta compression

Type Reference

Primitive Types

| Function | TypeScript Type | Description | | -------------------------- | --------------- | ------------------------------------------- | | StringType() | string | UTF-8 encoded string | | IntType() | number | Variable-length signed integer (zigzag) | | IntType({ min }) | number | Bounded integer, encoded as unsigned offset | | IntType({ min, max }) | number | Bounded integer with validation | | UIntType() | number | Shorthand for IntType({ min: 0 }) | | FloatType() | number | 32-bit IEEE 754 float | | FloatType({ precision }) | number | Quantized float with specified precision | | BooleanType() | boolean | Single bit boolean |

Bounded integers: When min is specified, values are encoded as value - min using unsigned varints, which is more compact for values near the minimum. The max parameter adds validation in fromJson() but doesn't affect encoding.

Container Types

| Function | TypeScript Type | Description | | ------------------ | ---------------- | ------------------------------------ | | ArrayType(T) | T[] | Array of type T | | OptionalType(T) | T \| undefined | Optional value of type T | | RecordType(K, V) | Map<K, V> | Map with key type K and value type V |

Complex Types

| Function | TypeScript Type | Description | | ----------------------------- | ------------------------ | -------------------------------- | | ObjectType("Name", { ... }) | { ... } | Object with defined properties | | EnumType("Name", [...]) | Union of string literals | Enumerated string values | | UnionType("Name", [...]) | { _type: string } & T | Tagged union of named types | | ReferenceType(Type) | Named type | Reference to another named type | | SelfReferenceType() | Self | Reference to the containing type |

Development

Running Tests

npm test              # Run all tests
npm run test:coverage # Run with coverage report
npm run test:ui       # Open Vitest UI

Type Checking

npm run typecheck

Formatting

npm run format        # Format code
npm run format:check  # Check formatting

API Documentation

For detailed API documentation and schema syntax, see the main README.

License

MIT