first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

134
docs/foundation/README.md Normal file
View File

@@ -0,0 +1,134 @@
# `foundation`
Foundational primitives — types, errors, configuration, data structures, and cross-cutting utilities.
<!-- begin:auto-docs -->
## Subcategories
9 subcategories, 62 packages.
### `barcode`
1-D barcode generation and parsing.
| Package | Description |
|---|---|
| [`@suaveplan/barcode`](./barcode/README.md) | Runtime-agnostic 1D barcode generation for seven symbologies — Code 128B, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, and ITF. Outputs inline SVG strings and binary… |
| [`@suaveplan/qrcode`](./qrcode/README.md) | QR code generation producing SVG, pixel matrix, terminal (Unicode blocks), and data URL representations. Implements QR code versions 1-10, error correction lev… |
### `bitpacker`
Bit-level binary packing, schemas, compression, and streaming codecs.
| Package | Description |
|---|---|
| [`@suaveplan/bitpacker`](./bitpacker/README.md) | Core bit-level binary packing library. Provides `BitWriter`, `BitReader`, `BitPacker`, and a pluggable strategy system for schema-driven serialization. |
| [`@suaveplan/bitpacker-compressed`](./bitpacker-compressed/README.md) | Compression pipeline integration for bitpacker. |
| [`@suaveplan/bitpacker-crypto`](./bitpacker-crypto/README.md) | Bit-packer-aware crypto primitives. The shared interfaces (`Authority`, `DomainTag`, `AuthorityVerifier`, `AeadOps`) live in `@suaveplan/types`; the canonical… |
| [`@suaveplan/bitpacker-delta`](./bitpacker-delta/README.md) | Delta encoding utilities for efficient repeated state transmission. Detects changes between successive values and encodes differences rather than full values. |
| [`@suaveplan/bitpacker-game`](./bitpacker-game/README.md) | Game state utilities: quantization, prediction, interpolation, and client-server reconciliation. |
| [`@suaveplan/bitpacker-network`](./bitpacker-network/README.md) | Network simulation utilities for testing bitpacker under realistic network conditions. |
| [`@suaveplan/bitpacker-schema`](./bitpacker-schema/README.md) | Schema registry and validation for bitpacker. |
| [`@suaveplan/bitpacker-strategies`](./bitpacker-strategies/README.md) | Built-in packing strategies for `@suaveplan/bitpacker`. Provides strategies for numbers, strings, booleans, objects, arrays, enums, optional fields, oneOf unio… |
| [`@suaveplan/bitpacker-streaming`](./bitpacker-streaming/README.md) | Streaming APIs for bitpacker including chunked packing/unpacking and true TransformStream support with framing. |
| [`@suaveplan/bitpacker-validation`](./bitpacker-validation/README.md) | Safety limits and validation reports for bitpacker schemas. |
### `core`
Core foundation — types, errors, configuration, schema registry, and utilities.
| Package | Description |
|---|---|
| [`@suaveplan/config`](./config/README.md) | Layered configuration management for Suaveplan. Resolves an ordered set of typed layers (defaults, file, object, env, CLI) in priority order, deep-merges the r… |
| [`@suaveplan/env`](./env/README.md) | Type-safe, schema-first environment variable loading for Node.js and edge runtimes. Define a schema of typed field builders, point it at your sources, and rece… |
| [`@suaveplan/error`](./error/README.md) | Foundation error substrate for Suaveplan. Every non-testing `@suaveplan/*` package extends this library's `SuaveplanError` base class and uses its stable code… |
| [`@suaveplan/schema-registry`](./schema-registry/README.md) | Versioned schema registry with forward/backward/full compatibility checking for evolving data contracts. Register schemas by subject name, enforce evolution ru… |
| [`@suaveplan/types`](./types/README.md) | Types-only foundation package. Zero runtime code, zero dependencies, zero bundle bytes. Provides the canonical shared interfaces that allow every `@suaveplan/*… |
| [`@suaveplan/typescript-config`](./typescript-config/README.md) | Shared TypeScript configuration presets for Suaveplan. This package ships composable JSON presets consumed via `"extends"` across every workspace package — no… |
| [`@suaveplan/utils`](./utils/README.md) | Zero-external-dependency utility library for Suaveplan. Covers debug helpers, deep object manipulation, platform detection, IEEE 754 float packing, pseudo-rand… |
### `data`
Data manipulation, masking, deduplication, diffing, and byte utilities.
| Package | Description |
|---|---|
| [`@suaveplan/bytes`](./bytes/README.md) | Low-level byte utilities for Suaveplan: lossless hex/base64/UTF-8 encoding, `Uint8Array` manipulation, and UUID conversion. No native addons; depends only on p… |
| [`@suaveplan/clipboard`](./clipboard/README.md) | Enterprise Clipboard API abstraction for browser-tier consumers. Wraps the async Clipboard API with a textarea + `execCommand` fallback, a paste-event read fal… |
| [`@suaveplan/data-masking`](./data-masking/README.md) | PII/sensitive data masking for objects and strings with field-level rules, nested traversal, eight masking strategies, and OpenTelemetry instrumentation. |
| [`@suaveplan/dedup`](./dedup/README.md) | Entity deduplication engine for detecting and merging duplicate records in any data set. Given a list of objects with varying representations of the same real-… |
| [`@suaveplan/diff`](./diff/README.md) | Zero-dependency RFC 6902 JSON Patch engine for computing, applying, and inverting structural differences between JSON documents. The `diff(a, b)` function prod… |
| [`@suaveplan/json`](./json/README.md) | Safe, enterprise-grade JSON utilities with prototype-pollution protection, circular-reference detection, and OpenTelemetry instrumentation. |
| [`@suaveplan/media`](./media/README.md) | Universal media detection, metadata extraction, and constraint validation. Combines a pluggable magic-byte `SignatureRegistry` with the MIME database from `@su… |
| [`@suaveplan/mime`](./mime/README.md) | Structured MIME-type detection, parsing, and registry for the Suaveplan platform. Provides RFC 7231 Content-Type parsing, magic-byte file signature detection,… |
| [`@suaveplan/packer`](./packer/README.md) | Binary packing/unpacking utilities. Two modes: |
| [`@suaveplan/packer-browser`](./packer-browser/README.md) | Browser-tier runtime for `@suaveplan/packer` archives. Ships a Service Worker interceptor, a `PackedResourceManager` (LRU + Blob-URL bookkeeping + ref-count +… |
| [`@suaveplan/pagination`](./pagination/README.md) | Universal pagination primitives for cursor-based, offset-based, and keyset-based (seek) pagination strategies. The package covers the full pagination surface:… |
| [`@suaveplan/pwa`](./pwa/README.md) | Enterprise browser-tier PWA primitives for Suaveplan applications. |
| [`@suaveplan/react-packer`](./react-packer/README.md) | React 19 companion for `@suaveplan/packer-browser`. Ships `usePackedResource`, `usePackedImage`, `usePackedAudio`, `usePackedVideo`, and `usePackedFont` hooks… |
| [`@suaveplan/serialize`](./serialize/README.md) | Round-trip-preserving structured serialization for JavaScript's rich type system. The standard `JSON.stringify`/`JSON.parse` pair silently drops `Date` objects… |
| [`@suaveplan/templates`](./templates/README.md) | A plugin-based string template engine for TypeScript. Resolves `{variable}` and `{variable:transform}` tokens in strings using a priority-sorted, regex-scan pi… |
| [`@suaveplan/validation`](./validation/README.md) | Runtime schema validation with a Zod-compatible API. This package is a self-contained parse engine — it does not import Zod or any other validation library — s… |
| [`@suaveplan/web-workers`](./web-workers/README.md) | Enterprise browser-tier runtime for managing Web Workers, SharedWorkers, and fixed-size worker pools with typed RPC, AbortSignal-aware cancellation, transfer-l… |
### `etl`
Extract-Transform-Load pipeline engine with typed stages.
| Package | Description |
|---|---|
| [`@suaveplan/etl`](./etl/README.md) | Extract-Transform-Load pipeline engine with typed stages, error handling, observable progress, and OTel instrumentation. |
### `feature-flags`
Feature-flag evaluation engine with percentage and attribute targeting.
| Package | Description |
|---|---|
| [`@suaveplan/feature-flags`](./feature-flags/README.md) | Provider-agnostic feature flag evaluation engine for controlling feature rollout without code deploys. The package ships a `FlagManager` high-level façade, a `… |
### `flow`
Finite state machines, CRDTs, idempotency, and CBOR serialization.
| Package | Description |
|---|---|
| [`@suaveplan/cbor`](./cbor/README.md) | RFC 8949 (CBOR) encoder + decoder, RFC 8742 (CBOR Sequences), pull-style streaming decoder (`AsyncIterable<Uint8Array>` / `ReadableStream<Uint8Array>`), RFC 90… |
| [`@suaveplan/crdt`](./crdt/README.md) | Conflict-free Replicated Data Types (CRDTs) for distributed systems. Provides eight composable CRDT primitives plus a vector-clock and a sync manager that coor… |
| [`@suaveplan/fsm`](./fsm/README.md) | Typed, generic finite state machine (FSM) for TypeScript. Ships a synchronous-by-default interpreter with guard predicates, ordered transition actions, context… |
| [`@suaveplan/idempotency`](./idempotency/README.md) | Idempotency key guard and deduplication for async operations. The package provides a complete `pending → completed \| failed` state machine, three store backend… |
| [`@suaveplan/job-scheduler`](./job-scheduler/README.md) | Universal in-process job scheduler with cron + queued runs, retries, timeouts, concurrency control, and OpenTelemetry observability. Built on `@suaveplan/cron`… |
| [`@suaveplan/queue`](./queue/README.md) | Enterprise-grade queue management: memory/priority/delayed queues, concurrent workers with retry and dead-letter forwarding, job state machines, and OTel instr… |
| [`@suaveplan/queue-distribution`](./queue-distribution/README.md) | Worker-pool distribution layer: load-balancing strategies, worker pool management, manager with retry and health checks, in-process `LocalWorker`. Transport-ag… |
| [`@suaveplan/queue-distribution-http`](./queue-distribution-http/README.md) | HTTP transport plugin for `@suaveplan/queue-distribution`. Ships `HttpWorkerCommunicator` (a fetch-backed `WorkerCommunicator` with a Zod-validated wire envelo… |
| [`@suaveplan/queue-distribution-worker-thread`](./queue-distribution-worker-thread/README.md) | Web-Worker / Node `worker_threads` transport plugin for `@suaveplan/queue-distribution`. Ships `WorkerThreadCommunicator` (a `ThreadWorkerLike`-based message b… |
| [`@suaveplan/rate-limiter`](./rate-limiter/README.md) | Enterprise rate limiter with three algorithms, rule-based routing, tenant multipliers, pluggable storage, and OTel instrumentation. |
| [`@suaveplan/retry`](./retry/README.md) | Retry strategies and circuit-breaker for async operations. The package provides constant, linear, and exponential backoff with four jitter modes (full, equal,… |
| [`@suaveplan/rules`](./rules/README.md) | Universal rule engine + validation system. Comprehensive `RuleEngine` with async evaluation, depth limit, AbortSignal, plugin registration; fluent `RuleBuilder… |
| [`@suaveplan/rules-hono`](./rules-hono/README.md) | Hono middleware wrapping `@suaveplan/rules`. Six middleware factories — `ruleMiddleware`, `validationMiddleware`, `accessControl`, `responseValidation`, `rateL… |
| [`@suaveplan/streams`](./streams/README.md) | WHATWG-Streams-based primitives: backpressure-first transforms, zero-copy `Uint8Array` operations, `AbortSignal` integration end-to-end, structured errors, OTe… |
| [`@suaveplan/streams-chunk-queue`](./streams-chunk-queue/README.md) | Universal out-of-order chunk reconstitution. Accepts chunks for a file in any order and exposes a sequential `ReadableStream<Uint8Array>` consumer surface, com… |
| [`@suaveplan/streams-metadata`](./streams-metadata/README.md) | Universal stream transforms that detect MIME type and extract EXIF/IPTC/XMP image metadata from buffered `Uint8Array` payloads. Three composable transforms (`M… |
| [`@suaveplan/undo`](./undo/README.md) | Enterprise-grade undo/redo engine with branching history, async commands, history compression, selective undo, and command merging. Built for collaborative edi… |
| [`@suaveplan/workflow`](./workflow/README.md) | Workflow and pipeline orchestration: sequential and parallel step execution, conditional branching with guard functions, retry strategies, timeouts, saga-patte… |
### `random`
Pseudo-random generators, noise functions, and deterministic sequences.
| Package | Description |
|---|---|
| [`@suaveplan/noise`](./noise/README.md) | Seeded spatial noise fields for procedural content generation. Provides six field types — Perlin, Simplex (2D/3D/4D), Value, Worley/cellular (F1/F2/F2-F1 modes… |
| [`@suaveplan/pseudo-random`](./pseudo-random/README.md) | Deterministic, seeded pseudo-random number generators (PRNGs) conforming to the `Rng` interface from `@suaveplan/types`. Ships four algorithms — Mulberry32, SF… |
| [`@suaveplan/random`](./random/README.md) | High-level cryptographically secure randomness for application use. This is the everyday random package — it wraps the CSPRNG with ergonomic helpers for every… |
### `timing`
Cron, stopwatches, throttling, debouncing, and scheduling.
| Package | Description |
|---|---|
| [`@suaveplan/cron`](./cron/README.md) | Universal cron expression parser, next-fire calculator with timezone + DST awareness, and an opt-in lightweight in-process scheduler. Zero external cron depend… |
| [`@suaveplan/datetime`](./datetime/README.md) | Timezone-aware date/time utilities covering the most common application-layer date operations that the native `Date` API leaves unaddressed. The package provid… |
| [`@suaveplan/timing`](./timing/README.md) | Timing and scheduling utilities for application-layer time management. The package covers measurement (stopwatch with lap recording), rate control (throttle an… |
<!-- end:auto-docs -->

View File

@@ -0,0 +1,3 @@
# `foundation`
Foundational primitives — types, errors, configuration, data structures, and cross-cutting utilities.

View File

@@ -0,0 +1,14 @@
# `barcode`
1-D barcode generation and parsing.
<!-- begin:auto-docs -->
## Packages in `barcode`
2 packages.
| Package | Description |
|---|---|
| [`@suaveplan/barcode`](./barcode/README.md) | Runtime-agnostic 1D barcode generation for seven symbologies — Code 128B, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, and ITF. Outputs inline SVG strings and binary… |
| [`@suaveplan/qrcode`](./qrcode/README.md) | QR code generation producing SVG, pixel matrix, terminal (Unicode blocks), and data URL representations. Implements QR code versions 1-10, error correction lev… |
<!-- end:auto-docs -->

View File

@@ -0,0 +1 @@
1-D barcode generation and parsing.

View File

@@ -0,0 +1,132 @@
# @suaveplan/barcode - Barcode generation and parsing for common 1D symbologies: Code 128, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, ITF. Outputs SVG strings and pixel arrays.
## Summary
Runtime-agnostic 1D barcode generation for seven symbologies — Code 128B, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, and ITF. Outputs inline SVG strings and binary bar-pattern arrays with no canvas, no DOM, and no Node.js built-ins required, making it safe to use in Bun, Deno, edge runtimes, and browsers alike. Includes check-digit calculation, barcode validation, and human-readable text rendering. Use this package to generate product labels, shipping labels (ITF/GTIN-14), retail barcodes (EAN/UPC), and document barcodes (Code 128). For 2D barcodes (QR codes) use `@suaveplan/qrcode`. This package does not support barcode scanning/decoding from images — it is generation-only.
<!-- begin:auto-docs -->
## Documentation tree
- [encoder](./src/core/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [encoder](./src/core/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validator](./src/core/validator/validator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validator](./src/core/validator/validator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/barcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [barcode-error](./src/errors/barcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [code128](./src/symbologies/code128.md) — Implements Code 128 Subset B encoding for the full printable ASCII range (characters 32–127). This module produces a bi…
- [code39](./src/symbologies/code39.md) — Implements Code 39 barcode encoding for the standard 43-character set: digits 0–9, uppercase letters A–Z, and the speci…
- [ean13](./src/symbologies/ean13.md) — Implements EAN-13 (International Article Number, 13-digit variant) barcode encoding following the ISO/IEC 15420 and GS1…
- [ean8](./src/symbologies/ean8.md) — Implements EAN-8 barcode encoding for small retail packages where space constraints make EAN-13 impractical. EAN-8 enco…
- [itf](./src/symbologies/itf.md) — Implements ITF (Interleaved 2 of 5) barcode encoding for numeric-only data. ITF is widely used in logistics, shipping,…
- [upca](./src/symbologies/upca.md) — Implements UPC-A barcode encoding and validation. UPC-A is the primary barcode format used in North American retail, en…
- [upce](./src/symbologies/upce.md) — Implements UPC-E barcode encoding for the compressed 6-digit UPC symbology used on small retail packages where space fo…
<!-- end:auto-docs --> - [encoder](./src/core/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [validator](./src/core/validator/validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [validator](./src/core/validator/validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/barcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [barcode-error](./src/errors/barcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [code128](./src/symbologies/code128.md) — Implements Code 128 Subset B encoding for the full printable ASCII range (characters 32–127). This module produces a bi…
- [code39](./src/symbologies/code39.md) — Implements Code 39 barcode encoding for the standard 43-character set: digits 0–9, uppercase letters A–Z, and the speci…
- [ean13](./src/symbologies/ean13.md) — Implements EAN-13 (International Article Number, 13-digit variant) barcode encoding following the ISO/IEC 15420 and GS1…
- [ean8](./src/symbologies/ean8.md) — Implements EAN-8 barcode encoding for small retail packages where space constraints make EAN-13 impractical. EAN-8 enco…
- [itf](./src/symbologies/itf.md) — Implements ITF (Interleaved 2 of 5) barcode encoding for numeric-only data. ITF is widely used in logistics, shipping,…
- [upca](./src/symbologies/upca.md) — Implements UPC-A barcode encoding and validation. UPC-A is the primary barcode format used in North American retail, en…
- [upce](./src/symbologies/upce.md) — Implements UPC-E barcode encoding for the compressed 6-digit UPC symbology used on small retail packages where space fo…
- [encoder](./src/core/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [validator](./src/core/validator/validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [validator](./src/core/validator/validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/barcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [barcode-error](./src/errors/barcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [code128](./src/symbologies/code128.md) — Implements Code 128 Subset B encoding for the full printable ASCII range (characters 32–127). This module produces a bi…
- [code39](./src/symbologies/code39.md) — Implements Code 39 barcode encoding for the standard 43-character set: digits 0–9, uppercase letters A–Z, and the speci…
- [ean13](./src/symbologies/ean13.md) — Implements EAN-13 (International Article Number, 13-digit variant) barcode encoding following the ISO/IEC 15420 and GS1…
- [ean8](./src/symbologies/ean8.md) — Implements EAN-8 barcode encoding for small retail packages where space constraints make EAN-13 impractical. EAN-8 enco…
- [itf](./src/symbologies/itf.md) — Implements ITF (Interleaved 2 of 5) barcode encoding for numeric-only data. ITF is widely used in logistics, shipping,…
- [upca](./src/symbologies/upca.md) — Implements UPC-A barcode encoding and validation. UPC-A is the primary barcode format used in North American retail, en…
- [upce](./src/symbologies/upce.md) — Implements UPC-E barcode encoding for the compressed 6-digit UPC symbology used on small retail packages where space fo…
- [encoder](./src/core/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [validator](./src/core/validator/validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [validator](./src/core/validator/validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/barcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [barcode-error](./src/errors/barcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [code128](./src/symbologies/code128.md) — Implements Code 128 Subset B encoding for the full printable ASCII range (characters 32–127). This module produces a bi…
- [code39](./src/symbologies/code39.md) — Implements Code 39 barcode encoding for the standard 43-character set: digits 0–9, uppercase letters A–Z, and the speci…
- [ean13](./src/symbologies/ean13.md) — Implements EAN-13 (International Article Number, 13-digit variant) barcode encoding following the ISO/IEC 15420 and GS1…
- [ean8](./src/symbologies/ean8.md) — Implements EAN-8 barcode encoding for small retail packages where space constraints make EAN-13 impractical. EAN-8 enco…
- [itf](./src/symbologies/itf.md) — Implements ITF (Interleaved 2 of 5) barcode encoding for numeric-only data. ITF is widely used in logistics, shipping,…
- [upca](./src/symbologies/upca.md) — Implements UPC-A barcode encoding and validation. UPC-A is the primary barcode format used in North American retail, en…
- [upce](./src/symbologies/upce.md) — Implements UPC-E barcode encoding for the compressed 6-digit UPC symbology used on small retail packages where space fo…
- [encoder](./src/core/encoder/encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [validator](./src/core/validator/validator.md) — <!-- begin:auto-docs --> _(no submodules)_ - [validator](./src/core/validator/validator.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/barcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [barcode-error](./src/errors/barcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [code128](./src/symbologies/code128.md) — Implements Code 128 Subset B encoding for the full printable ASCII range (characters 32–127). This module produces a bi…
- [code39](./src/symbologies/code39.md) — Implements Code 39 barcode encoding for the standard 43-character set: digits 0–9, uppercase letters A–Z, and the speci…
- [ean13](./src/symbologies/ean13.md) — Implements EAN-13 (International Article Number, 13-digit variant) barcode encoding following the ISO/IEC 15420 and GS1…
- [ean8](./src/symbologies/ean8.md) — Implements EAN-8 barcode encoding for small retail packages where space constraints make EAN-13 impractical. EAN-8 enco…
- [itf](./src/symbologies/itf.md) — Implements ITF (Interleaved 2 of 5) barcode encoding for numeric-only data. ITF is widely used in logistics, shipping,…
- [upca](./src/symbologies/upca.md) — Implements UPC-A barcode encoding and validation. UPC-A is the primary barcode format used in North American retail, en…
- [upce](./src/symbologies/upce.md) — Implements UPC-E barcode encoding for the compressed 6-digit UPC symbology used on small retail packages where space fo…
## Symbologies
| Symbology | Key | Description |
|-----------|-----|-------------|
| Code 128B | `code128` | ASCII 32–127; variable length; most versatile 1D format |
| Code 39 | `code39` | 0–9, A–Z, special chars; industrial/logistics |
| EAN-13 | `ean13` | 13-digit retail standard (ISBN-13, ISSN-13) |
| EAN-8 | `ean8` | 8-digit compact retail for small packages |
| UPC-A | `upca` | 12-digit North American retail |
| UPC-E | `upce` | 8-digit compressed UPC-A for small packages |
| ITF | `itf` | Interleaved 2 of 5; even-digit numeric; logistics/GTIN-14 |
## Install
```bash
bun add @suaveplan/barcode
```
## Quick Start
```ts
import { encode } from "@suaveplan/barcode";
const result = encode("Hello World", "code128");
console.log(result.svg); // <svg ...>...</svg>
console.log(result.width); // pixel width
```
## API
### `encode(data, symbology, options?): BarcodeResult`
Main encoding function. Dispatches to the per-symbology encoder and renders an SVG.
```ts
import { encode } from "@suaveplan/barcode";
const result = encode("5901234123457", "ean13", {
width: 3, // bar width multiplier (default 2)
height: 80, // bar height in pixels (default 100)
margin: 15, // quiet zone (default 10)
includeText: true, // render human-readable text (default true)
color: "#000000",
background: "#ffffff",
});
```
### `validateBarcode(data, symbology): boolean`
```ts
import { validateBarcode } from "@suaveplan/barcode";
validateBarcode("5901234123457", "ean13"); // true
```
### `calculateCheckDigit(data, symbology): string`
```ts
import { calculateCheckDigit } from "@suaveplan/barcode";
calculateCheckDigit("590123412345", "ean13"); // "7"
```
### `parse(data, symbology): ParseResult`
```ts
import { parse } from "@suaveplan/barcode";
const r = parse("5901234123457", "ean13");
// { symbology: "ean13", data: "...", valid: true, checkDigit: "7" }
```
## Module Docs
- [`src/errors/barcode-error.md`](src/errors/barcode-error.md) — Error class and code registry
- [`src/symbologies/code128.md`](src/symbologies/code128.md) — Code 128B encoding
- [`src/symbologies/code39.md`](src/symbologies/code39.md) — Code 39 encoding
- [`src/symbologies/ean13.md`](src/symbologies/ean13.md) — EAN-13 encoding
- [`src/symbologies/ean8.md`](src/symbologies/ean8.md) — EAN-8 encoding
- [`src/symbologies/upca.md`](src/symbologies/upca.md) — UPC-A encoding
- [`src/symbologies/upce.md`](src/symbologies/upce.md) — UPC-E encoding
- [`src/symbologies/itf.md`](src/symbologies/itf.md) — ITF encoding
- [`src/core/encoder/encoder.md`](src/core/encoder/encoder.md) — Central encoder and SVG renderer
- [`src/core/validator/validator.md`](src/core/validator/validator.md) — Validation and parsing facade

View File

@@ -0,0 +1,87 @@
# encoder
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the central barcode encoding pipeline for `@suaveplan/barcode`. It dispatches input data to the correct per-symbology encoder, then renders the resulting binary bar pattern into a complete, self-contained SVG string. The encoder is the primary entry point for most consumer use cases — callers only need `encode(data, symbology, options)`.
## Features
- `encode(data, symbology, options?)` — full pipeline: dispatch → binary encoding → SVG rendering.
- `renderSvg(encoded, data, opts)` — exposed for testing and advanced pipelines that need SVG rendering without re-encoding.
- Run-length encoding of consecutive bits for compact `<rect>` output.
- XML-safe text rendering via `escapeXml`.
- Configurable bar width, bar height, margin, text inclusion, and colours via `BarcodeOptions`.
- Defaults: width=2, height=100, margin=10, includeText=true, color=#000000, background=#ffffff.
- Throws `BarcodeError` with stable codes on invalid input or unsupported symbology.
## Usage: Basic Example
```ts
import { encode } from "@suaveplan/barcode/encoder";
const result = encode("Hello World", "code128");
console.log(result.svg); // complete <svg>...</svg> string
console.log(result.width); // total pixel width
console.log(result.encoded); // binary bar pattern
```
## Usage: Advanced Example
```ts
import { encode, renderSvg } from "@suaveplan/barcode/encoder";
// Custom rendering options
const result = encode("5901234123457", "ean13", {
width: 3,
height: 80,
margin: 15,
includeText: false,
color: "#1a1a2e",
background: "#e8f4f8",
});
// Separate render pass with modified options
const customSvg = renderSvg(result.encoded, result.data, {
width: 1,
height: 50,
margin: 5,
includeText: true,
color: "#000000",
background: "#ffffff",
});
```
## API Reference
### `encode(data: string, symbology: BarcodeSymbology, options?: BarcodeOptions): BarcodeResult`
Main entry point for barcode generation.
| Parameter | Type | Description |
|-----------|------|-------------|
| `data` | `string` | Raw data to encode |
| `symbology` | `BarcodeSymbology` | Target symbology (`"code128"`, `"ean13"`, etc.) |
| `options` | `BarcodeOptions` | Optional rendering configuration |
Returns `BarcodeResult` with fields: `symbology`, `data`, `encoded`, `svg`, `width`, `height`.
**Throws** `BarcodeError(ENCODE_FAILED)` if the underlying encoder fails, or `BarcodeError(UNSUPPORTED_SYMBOLOGY)` for unknown symbology values.
### `renderSvg(encoded: string, data: string, opts: Required<BarcodeOptions>): string`
Renders a pre-computed binary string to SVG.
| Parameter | Description |
|-----------|-------------|
| `encoded` | Binary string: `'1'` = bar, `'0'` = space |
| `data` | Human-readable label text (rendered below bars when `includeText` is true) |
| `opts` | Fully-resolved options (no optional fields) |
Returns a complete `<svg>` element string suitable for embedding in HTML or writing to a file.
## Implementation Notes
Run-length encoding groups consecutive identical bits before emitting `<rect>` elements, reducing the number of SVG elements for long runs (e.g. quiet zones, wide bars). Text rendering uses `escapeXml` to prevent SVG injection when the data string contains `<`, `>`, `"`, `'`, or `&`. The background rectangle is always rendered first so bar rectangles appear on top. The total height includes bar height + optional text height (14px) + two margin widths (top and bottom).

View File

@@ -0,0 +1,78 @@
# validator
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides cross-symbology barcode validation, check-digit calculation, and structured parsing for the `@suaveplan/barcode` package. While each symbology module exposes its own `validate` function, this module serves as a unified facade so callers can perform validation without needing to import individual symbology modules. It also exposes a `calculateCheckDigit` utility for EAN/UPC families and a `parse` function that returns a structured result including validity status and extracted check digit.
## Features
- `validateBarcode(data, symbology)` — dispatches to per-symbology validator, returns `boolean`.
- `calculateCheckDigit(data, symbology)` — GS1 check digit for EAN-13, EAN-8, and UPC-A.
- `parse(data, symbology)` — returns `ParseResult` with `valid`, `checkDigit` (where applicable).
- Exhaustive switch guards with `never` type for compile-time completeness.
- Throws `BarcodeError` with stable codes for unsupported symbologies or invalid input.
## Usage: Basic Example
```ts
import { validateBarcode, calculateCheckDigit } from "@suaveplan/barcode/validator";
const valid = validateBarcode("5901234123457", "ean13"); // true
const check = calculateCheckDigit("590123412345", "ean13"); // "7"
```
## Usage: Advanced Example
```ts
import { parse } from "@suaveplan/barcode/validator";
const result = parse("5901234123457", "ean13");
// {
// symbology: "ean13",
// data: "5901234123457",
// valid: true,
// checkDigit: "7"
// }
const invalid = parse("5901234123458", "ean13");
// { valid: false, checkDigit: "8" }
// Code 128 parse — no check digit extracted
const code128 = parse("HELLO", "code128");
// { symbology: "code128", data: "HELLO", valid: true }
```
## API Reference
### `validateBarcode(data: string, symbology: BarcodeSymbology): boolean`
Dispatches to the appropriate per-symbology `validate` function. Returns `true` if `data` is valid for `symbology`. Does not throw for validation failures (only throws for unsupported symbology).
### `calculateCheckDigit(data: string, symbology: 'ean13' | 'ean8' | 'upca'): string`
Computes the check digit for EAN/UPC symbologies.
| Parameter | Requirement |
|-----------|-------------|
| `data` | 12 digits for `ean13`/`upca`, 7 digits for `ean8` |
| `symbology` | Must be `'ean13'`, `'ean8'`, or `'upca'` |
Returns check digit as a `string` character `'0'`–`'9'`. **Throws** `BarcodeError(INVALID_DATA)` for non-numeric input or wrong digit count.
### `parse(data: string, symbology: BarcodeSymbology): ParseResult`
Returns a structured `ParseResult`:
| Field | Type | Description |
|-------|------|-------------|
| `symbology` | `BarcodeSymbology` | The symbology used |
| `data` | `string` | The input data string |
| `valid` | `boolean` | Whether data is valid for the symbology |
| `checkDigit` | `string \| undefined` | Last digit for EAN/UPC types; absent for Code 128/39/ITF |
## Implementation Notes
The `parse` function extracts the check digit by position convention: last digit of EAN-13 (13th), EAN-8 (8th), UPC-A (12th), and UPC-E (8th). The `checkDigit` field is only present when the data string has the expected length. Exhaustive switch statements with `never` fall-through guards ensure TypeScript enforces full coverage when new symbologies are added to the `BarcodeSymbology` union type.

View File

@@ -0,0 +1,82 @@
# barcode-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the structured error class and stable error-code registry used throughout the `@suaveplan/barcode` package. As a leaf dependency with no `@suaveplan/error` runtime dependency, this module is fully self-contained while preserving the same structural contract (named `code` field, typed constants) expected by consumers of the wider Suaveplan error ecosystem.
## Features
- `BARCODE_CODES` — an `as const` object of stable string codes, suitable for `switch`/`if` guards without importing magic numbers.
- `BarcodeCode` — union type derived from `BARCODE_CODES` so callers get exhaustive checking.
- `BarcodeError extends Error` — standard JS error subclass with `name = "BarcodeError"` and a typed `code` field; fully serialisable across `structuredClone`, `JSON.stringify`, and Worker boundaries.
## Usage: Basic Example
```ts
import { BarcodeError, BARCODE_CODES } from "@suaveplan/barcode/errors";
throw new BarcodeError(
"EAN-13 requires exactly 13 digits",
BARCODE_CODES.INVALID_DATA,
);
```
## Usage: Advanced Example
```ts
import { BarcodeError, BARCODE_CODES, type BarcodeCode } from "@suaveplan/barcode/errors";
function handleBarcodeError(err: unknown): void {
if (err instanceof BarcodeError) {
switch (err.code) {
case BARCODE_CODES.INVALID_DATA:
console.error("Bad input:", err.message);
break;
case BARCODE_CODES.UNSUPPORTED_SYMBOLOGY:
console.error("Unknown symbology:", err.message);
break;
case BARCODE_CODES.ENCODE_FAILED:
console.error("Encoding failed:", err.message);
break;
}
}
}
```
## API Reference
### `BARCODE_CODES`
`const` object with three keys:
| Key | Value |
|-----|-------|
| `INVALID_DATA` | `"BARCODE.INVALID_DATA"` |
| `UNSUPPORTED_SYMBOLOGY` | `"BARCODE.UNSUPPORTED_SYMBOLOGY"` |
| `ENCODE_FAILED` | `"BARCODE.ENCODE_FAILED"` |
### `BarcodeCode`
Derived union type: `"BARCODE.INVALID_DATA" | "BARCODE.UNSUPPORTED_SYMBOLOGY" | "BARCODE.ENCODE_FAILED"`.
### `BarcodeError`
```ts
class BarcodeError extends Error {
readonly code: BarcodeCode;
constructor(message: string, code: BarcodeCode);
}
```
| Field | Type | Description |
|-------|------|-------------|
| `name` | `"BarcodeError"` | Set in constructor; identifies error type. |
| `code` | `BarcodeCode` | Stable machine-readable code for programmatic handling. |
| `message` | `string` | Human-readable description of the problem. |
## Implementation Notes
The module is deliberately framework-free and has zero runtime imports. The `as const` assertion on `BARCODE_CODES` gives TypeScript narrowing without any extra overhead. Extending the native `Error` class means `instanceof BarcodeError` checks work across realm boundaries (Worker ↔ main thread) as long as the same class reference is used. The `code` field is marked `readonly` to prevent accidental mutation after construction.

View File

@@ -0,0 +1,68 @@
# code128
## Purpose
Implements Code 128 Subset B encoding for the full printable ASCII range (characters 32–127). This module produces a binary bar-pattern string that faithfully represents the ISO/IEC 15417 standard: start symbol, per-character 11-bit patterns from the Code 128B table, mod-103 check symbol, and the 13-bit stop symbol. It is the most versatile 1D symbology in common use and handles arbitrary printable text without requiring special character-set switching.
## Features
- Full 96-entry Code 128B character table (ASCII 32–127).
- `encode(data)` — returns a binary `'0'/'1'` string ready for SVG rendering.
- `validate(data)` — pre-flight check before encoding, returns `boolean`.
- `checkValue(data)` — computes the raw numeric check value (0–102) independently.
- Throws `BarcodeError` with stable codes on invalid input or encoding failure.
- Zero external dependencies; pure computation.
## Usage: Basic Example
```ts
import { encode, validate } from "@suaveplan/barcode/symbologies";
if (validate("Hello-123")) {
const bits = encode("Hello-123");
console.log(bits.startsWith("11010010000")); // true — Code B start symbol
}
```
## Usage: Advanced Example
```ts
import { encode, checkValue } from "@suaveplan/barcode/symbologies";
// Compute check digit independently for audit purposes
const cv = checkValue("ABC");
console.log(`Check value: ${cv}`); // deterministic numeric 0–102
// Encode and inspect structure
const bits = encode("ABC");
const startSymbol = bits.slice(0, 11); // "11010010000" (START B)
const stopSymbol = bits.slice(-13); // "1100011101011" (STOP)
console.log(startSymbol, stopSymbol);
```
## API Reference
### `validate(data: string): boolean`
Returns `true` if every character in `data` has a code point in the range 32–127 (inclusive) and `data.length > 0`. Returns `false` otherwise. Does not throw.
### `encode(data: string): string`
Encodes `data` into a binary string using Code 128 Subset B.
| Segment | Description |
|---------|-------------|
| START B | `11010010000` |
| Data chars | Each character → 11-bit table lookup |
| Check symbol | Derived from mod-103 weighted sum |
| STOP | `1100011101011` |
**Throws** `BarcodeError(BARCODE_CODES.INVALID_DATA)` if any character is outside ASCII 32–127.
### `checkValue(data: string): number`
Returns the integer check value (0–102) computed as `(START_B_VALUE + Σ tableIndex_i × (i+1)) mod 103`. Useful for verification without full encode overhead.
## Implementation Notes
The 108-entry table is declared as a `readonly string[]` constant. Characters in Subset B are indexed as `charCode - 32`, so ASCII space (32) → index 0, `~` (126) → index 94, DEL (127) → index 95. Indices 96–107 cover special function/control values. The check digit accumulates `(i + 1)` as the positional weight (1-based), consistent with the ISO spec. The stop symbol is a fixed 13-bit pattern not present in the main table (it has a different width) and is simply concatenated verbatim.

View File

@@ -0,0 +1,63 @@
# code39
## Purpose
Implements Code 39 barcode encoding for the standard 43-character set: digits 0–9, uppercase letters A–Z, and the special characters space, hyphen, period, dollar, slash, plus, and percent. Code 39 is a variable-length, self-checking, discrete symbology widely used in industrial and logistics applications. This module auto-upper-cases input for ergonomic developer experience and produces a binary bar-pattern string ready for SVG rendering.
## Features
- `encode(data)` — returns a binary `'0'/'1'` string with START, payload, inter-character gaps, and STOP symbols.
- `validate(data)` — checks every character against the legal Code 39 set; returns `boolean`.
- Auto-upper-case normalisation during encoding.
- Narrow inter-character gap (`'0'`) inserted between every symbol pair.
- Throws `BarcodeError` with stable codes on invalid input.
- Zero external dependencies.
## Usage: Basic Example
```ts
import { encode, validate } from "@suaveplan/barcode/symbologies";
if (validate("HELLO-123")) {
const bits = encode("HELLO-123");
// bits begins with Code 39 start (*) pattern
}
```
## Usage: Advanced Example
```ts
import { encode } from "@suaveplan/barcode/symbologies";
// Lower-case is automatically normalised to upper-case
const bits = encode("hello world");
// Equivalent to encode("HELLO WORLD")
// Inspect start/stop boundaries
const startPattern = "100101101101";
console.log(bits.startsWith(startPattern)); // true
console.log(bits.endsWith(startPattern)); // true (same * symbol for stop)
```
## API Reference
### `validate(data: string): boolean`
Returns `true` if every character in `data.toUpperCase()` exists in the Code 39 character set (0–9, A–Z, space, `-`, `.`, `$`, `/`, `+`, `%`) and `data.length > 0`. The `*` start/stop character is not valid in payloads. Returns `false` without throwing.
### `encode(data: string): string`
Encodes `data` into a binary string.
| Segment | Description |
|---------|-------------|
| START | `*` symbol pattern |
| Gap | Narrow `'0'` inter-character gap |
| Data | Each character → 12-bit lookup + narrow gap |
| STOP | `*` symbol pattern (no trailing gap) |
Each character produces a 12-bit pattern (9 element bits + 3 implicit bar/space transitions from narrow/wide encoding). **Throws** `BarcodeError(BARCODE_CODES.INVALID_DATA)` if any character is not in the allowed set.
## Implementation Notes
Code 39 patterns in this implementation use a compact 12-character binary representation where narrow elements are `'0'` and wide elements are `'1'`. Pattern ordering follows the standard bar-space-bar-space-bar-space-bar-space-bar sequence. Inter-character gaps are appended after each symbol including START, but not after STOP. Input is normalised to upper case before validation so lowercase ASCII letters map correctly.

View File

@@ -0,0 +1,77 @@
# ean13
## Purpose
Implements EAN-13 (International Article Number, 13-digit variant) barcode encoding following the ISO/IEC 15420 and GS1 specifications. EAN-13 is the global standard for retail product identification and is recognised by virtually all point-of-sale systems worldwide. This module handles the full encoding pipeline: check digit computation, first-digit parity selection, L-code/G-code left-side encoding, R-code right-side encoding, and assembly of guard bars.
## Features
- `encode(data)` — accepts 12 or 13 digit strings; auto-appends check digit for 12-digit input.
- `validate(data)` — verifies length, digit-only content, and check digit correctness.
- `computeCheckDigit(digits)` — standalone GS1 Luhn-weight check digit calculator (weights 1 and 3).
- Full 3-zone encoding: left guard `101`, 6 L/G-code digits, centre guard `01010`, 6 R-code digits, right guard `101`.
- First-digit parity table derived from GS1 specification.
- Throws `BarcodeError` with stable codes on invalid input.
## Usage: Basic Example
```ts
import { encode, validate } from "@suaveplan/barcode/symbologies";
// Validate a known EAN-13
console.log(validate("5901234123457")); // true
// Encode (auto-computes check if 12 digits provided)
const bits = encode("590123412345");
```
## Usage: Advanced Example
```ts
import { encode, computeCheckDigit } from "@suaveplan/barcode/symbologies";
// Compute and verify a check digit independently
const digits = [5, 9, 0, 1, 2, 3, 4, 1, 2, 3, 4, 5];
const check = computeCheckDigit(digits);
console.log(check); // 7
// Encode a full 13-digit code
const bits = encode("5901234123457");
// Structure: 101 + 6×7-bit left + 01010 + 6×7-bit right + 101
console.log(bits.startsWith("101")); // true
console.log(bits.length); // 95
```
## API Reference
### `computeCheckDigit(digits: readonly number[]): number`
Computes the EAN/UPC Luhn check digit for an array of 12 digit values (0–9). Applies alternating weights 1 and 3 starting with weight 1 for the first digit, sums the products, and returns `(10 - (sum % 10)) % 10`.
### `validate(data: string): boolean`
Returns `true` if:
- `data` matches `/^\d{13}$/`
- the 13th digit equals `computeCheckDigit(first 12 digits)`
Returns `false` without throwing.
### `encode(data: string): string`
Encodes an EAN-13 barcode into a binary string.
| Segment | Width | Description |
|---------|-------|-------------|
| Left guard | 3 | `101` |
| Left digits 1–6 | 42 | 6 × 7-bit L/G-code per first-digit parity |
| Centre guard | 5 | `01010` |
| Right digits 7–12 | 42 | 6 × 7-bit R-code |
| Right guard | 3 | `101` |
| **Total** | **95** | |
Accepts 12-digit strings (check digit auto-appended) or 13-digit strings. **Throws** `BarcodeError(BARCODE_CODES.INVALID_DATA)` for non-digit input or wrong length.
## Implementation Notes
The first-digit parity table follows the GS1 General Specifications. L-codes have odd parity (start with `0`), G-codes have even parity (bitwise complement of R-code, start with `0`), and R-codes have even parity (start with `1`). The first digit of EAN-13 is encoded implicitly in the left-side parity pattern — it does not appear as a bar itself. The total encoded width is exactly 95 modules.

View File

@@ -0,0 +1,61 @@
# ean8
## Purpose
Implements EAN-8 barcode encoding for small retail packages where space constraints make EAN-13 impractical. EAN-8 encodes 8 digits (7 data + 1 check digit) and uses the same GS1 Luhn check-digit algorithm as EAN-13 but with a simplified left-side encoding (all L-code, no G-code parity switching). This module delegates check-digit computation to the `ean13` module to avoid duplication.
## Features
- `encode(data)` — accepts 7 or 8 digit strings; auto-appends check digit for 7-digit input.
- `validate(data)` — verifies 8-digit format and check digit correctness.
- Left guard `101`, centre guard `01010`, right guard `101`.
- Left 4 digits encoded in L-code; right 4 digits in R-code.
- Throws `BarcodeError` with stable codes on invalid input.
## Usage: Basic Example
```ts
import { encode, validate } from "@suaveplan/barcode/symbologies";
console.log(validate("96385074")); // true
const bits = encode("9638507"); // auto-appends check digit 4
```
## Usage: Advanced Example
```ts
import { encode } from "@suaveplan/barcode/symbologies";
// Encode with explicit 8-digit input
const bits = encode("96385074");
// Structure: 101 + 4×7-bit L-code + 01010 + 4×7-bit R-code + 101
console.log(bits.startsWith("101")); // true
console.log(bits.length); // 67
```
## API Reference
### `validate(data: string): boolean`
Returns `true` if `data` matches `/^\d{8}$/` and the 8th digit equals the computed check digit for the first 7. Returns `false` otherwise.
### `encode(data: string): string`
Encodes an EAN-8 barcode into a binary string.
| Segment | Width | Description |
|---------|-------|-------------|
| Left guard | 3 | `101` |
| Left digits 1–4 | 28 | 4 × 7-bit L-code |
| Centre guard | 5 | `01010` |
| Right digits 5–8 | 28 | 4 × 7-bit R-code |
| Right guard | 3 | `101` |
| **Total** | **67** | |
Accepts 7-digit strings (check digit auto-appended) or 8-digit strings. **Throws** `BarcodeError(BARCODE_CODES.INVALID_DATA)` for non-digit input or wrong length.
## Implementation Notes
EAN-8 uses L-code for all left-side digits (no first-digit parity switching). This simplification is valid because the 8-digit format carries enough data in the guard structure to maintain optical orientation detection. The L-code and R-code tables in this module are duplicated from `ean13.ts` rather than imported to keep the module self-contained for bundle-splitting scenarios where only EAN-8 is used.

View File

@@ -0,0 +1,60 @@
# itf
## Purpose
Implements ITF (Interleaved 2 of 5) barcode encoding for numeric-only data. ITF is widely used in logistics, shipping, and inventory applications because it achieves high density by interleaving two digit streams — one encoded in bars and one in spaces — simultaneously. This module encodes digit pairs into alternating bar/space elements following the ISO/IEC 16390 standard, with start and stop guards.
## Features
- `encode(data)` — encodes an even-length digit string into a binary bar pattern.
- `validate(data)` — verifies digit-only content and even length requirement.
- Narrow element = 1 module, wide element = 3 modules (3:1 ratio).
- Start guard `0000`, stop guard `100`.
- Each digit pair interleaved: 5 bar elements (first digit) × 5 space elements (second digit).
- Throws `BarcodeError` with stable codes on invalid input.
## Usage: Basic Example
```ts
import { encode, validate } from "@suaveplan/barcode/symbologies";
console.log(validate("12345678")); // true
console.log(validate("1234567")); // false — odd length
const bits = encode("12345678");
```
## Usage: Advanced Example
```ts
import { encode } from "@suaveplan/barcode/symbologies";
// Encode a 14-digit GTIN (common ITF-14 use case)
const bits = encode("12345678901231");
// ITF structure: 0000 (start) + pairs + 100 (stop)
console.log(bits.startsWith("0000")); // true
console.log(bits.endsWith("100")); // true
```
## API Reference
### `validate(data: string): boolean`
Returns `true` if `data` matches `/^\d+$/` and `data.length` is even and greater than zero. Returns `false` without throwing.
### `encode(data: string): string`
Encodes ITF data into a binary string. Processes digits in pairs: the first digit of each pair drives bar widths and the second drives space widths.
| Segment | Description |
|---------|-------------|
| Start | `0000` (4 narrow modules) |
| Data | Each pair → 5 interleaved bar+space segments (1 or 3 modules each) |
| Stop | `100` (wide bar + 2 narrow) |
**Throws** `BarcodeError(BARCODE_CODES.INVALID_DATA)` for non-digit input or odd-length data.
## Implementation Notes
The ITF_DIGIT table uses boolean arrays (false = narrow, true = wide) for each digit 0–9, with exactly 2 wide elements per digit. The interleaving algorithm iterates through the 5 element positions and alternates: barWidth repeated `1` characters then spaceWidth repeated `0` characters. This produces a continuous binary string where `1` represents a bar and `0` represents a space, directly usable by the SVG renderer. The 3:1 wide-to-narrow ratio is standard for ITF; some implementations use 2.5:1 but 3:1 provides better scanner compatibility.

View File

@@ -0,0 +1,56 @@
# upca
## Purpose
Implements UPC-A barcode encoding and validation. UPC-A is the primary barcode format used in North American retail, encoding 12 digits (11 data + 1 check). It is structurally identical to EAN-13 with a leading `0` digit prepended, so this module delegates encoding to the EAN-13 encoder to avoid duplication and ensure consistent output.
## Features
- `encode(data)` — accepts 11 or 12 digit UPC-A strings; auto-appends check digit for 11-digit input.
- `validate(data)` — verifies 12-digit format and check digit correctness.
- Delegates to `ean13.encode` by prepending `'0'` — same 95-module binary output.
- Throws `BarcodeError` with stable codes on invalid input.
## Usage: Basic Example
```ts
import { encode, validate } from "@suaveplan/barcode/symbologies";
console.log(validate("036000291452")); // true
const bits = encode("036000291452");
```
## Usage: Advanced Example
```ts
import { encode } from "@suaveplan/barcode/symbologies";
// 11-digit input: check digit is computed and appended automatically
const bits = encode("03600029145"); // computes check digit 2
console.log(bits.length); // 95 (same as EAN-13)
// UPC-A is EAN-13 with a leading 0; scanner output is equivalent
```
## API Reference
### `validate(data: string): boolean`
Returns `true` if `data` matches `/^\d{12}$/` and the 12th digit equals the computed check digit for the first 11. Returns `false` without throwing.
### `encode(data: string): string`
Encodes a UPC-A barcode by prepending `'0'` and calling `encodeEan13`.
| Input | Behaviour |
|-------|-----------|
| 11 digits | Check digit auto-computed and appended |
| 12 digits | Used as-is; validated before encoding |
| Other | `BarcodeError(INVALID_DATA)` thrown |
Output is 95 modules (identical to EAN-13). **Throws** `BarcodeError(BARCODE_CODES.INVALID_DATA)` for non-digit input, wrong length, or invalid check digit.
## Implementation Notes
UPC-A's equivalence with EAN-13 (first digit `0`) is standardised in GS1 specifications. Delegating to `ean13.encode` guarantees bit-identical output for any equivalent UPC-A/EAN-13 pair, simplifying scanner compatibility. The `computeCheckDigit` import from `ean13` reuses the same GS1 Luhn algorithm without duplication.

View File

@@ -0,0 +1,59 @@
# upce
## Purpose
Implements UPC-E barcode encoding for the compressed 6-digit UPC symbology used on small retail packages where space for UPC-A is unavailable. UPC-E suppresses trailing zeros from UPC-A to achieve a compact 8-module (number system + 6 data + check) format. This module encodes the standard 8-digit UPC-E form, selecting per-digit L-code or G-code based on the GS1 parity table driven by the check digit.
## Features
- `encode(data)` — encodes an 8-digit UPC-E string into a binary bar pattern.
- `validate(data)` — verifies 8-digit format and number system digit (0 or 1).
- Parity selection via the standard GS1 UPC-E parity table indexed by check digit.
- Start guard `101`, end guard `010101` (6-bar reverse parity guard).
- Throws `BarcodeError` with stable codes on invalid input.
## Usage: Basic Example
```ts
import { encode, validate } from "@suaveplan/barcode/symbologies";
console.log(validate("01234565")); // true (8-digit UPC-E)
const bits = encode("01234565");
```
## Usage: Advanced Example
```ts
import { encode } from "@suaveplan/barcode/symbologies";
const bits = encode("01234565");
// Structure: 101 (start) + 6×7-bit mixed L/G parity + 010101 (end)
console.log(bits.startsWith("101")); // true
console.log(bits.endsWith("010101")); // true
console.log(bits.length); // 51
```
## API Reference
### `validate(data: string): boolean`
Returns `true` if `data` matches `/^\d{8}$/` and the first digit (number system) is `'0'` or `'1'`. Returns `false` otherwise. Full check-digit validation against UPC-A expansion is not performed here.
### `encode(data: string): string`
Encodes a UPC-E barcode.
| Segment | Width | Description |
|---------|-------|-------------|
| Start guard | 3 | `101` |
| 6 data digits | 42 | 6 × 7-bit L or G-code per parity table |
| End guard | 6 | `010101` |
| **Total** | **51** | |
**Throws** `BarcodeError(BARCODE_CODES.INVALID_DATA)` for non-8-digit input or invalid number system digit.
## Implementation Notes
The parity table maps the check digit (0–9) to a 6-character string of `'O'` (odd/L-code) and `'E'` (even/G-code) symbols. For number system digit `0`, the table is applied directly; for digit `1`, the same table is used (GS1 specifies complemented parities, but the standard encoding in most scanners treats both identically for the compressed form). The end guard uses 6 modules rather than the usual 3, distinguishing UPC-E from UPC-A optically.

View File

@@ -0,0 +1,142 @@
# barcode.types
## Purpose
Shared TypeScript type definitions for the `@suaveplan/barcode` package. These types form the public contract between all modules: the symbology encoder, the binary validator, and the SVG renderer all accept and return values described here. Centralising the types in one file ensures consistent interfaces across the package and provides a single authoritative reference for consumers.
## Features
- `BarcodeSymbology` — union of every supported 1D symbology literal string
- `BarcodeOptions` — optional rendering settings passed to the `encode` function
- `BarcodeResult` — the structured return value from a successful `encode` call
- `ParseResult` — the structured return value from the `parse` validator call
- Zero runtime code — types-only, no overhead at runtime
## Usage: Basic Example
```typescript
import type { BarcodeSymbology, BarcodeResult } from "@suaveplan/barcode/types";
const symbology: BarcodeSymbology = "ean13";
function displayResult(result: BarcodeResult): void {
const container = document.getElementById("barcode");
if (container) {
const img = document.createElement("img");
img.src = `data:image/svg+xml;utf8,${encodeURIComponent(result.svg)}`;
container.appendChild(img);
}
}
```
## Usage: Advanced Example
```typescript
import type {
BarcodeOptions,
BarcodeResult,
ParseResult,
BarcodeSymbology,
} from "@suaveplan/barcode/types";
import { encode, parse } from "@suaveplan/barcode";
const SYMBOLOGIES: BarcodeSymbology[] = [
"ean13", "ean8", "upca", "upce", "code128", "code39", "itf",
];
function renderWithOptions(
data: string,
sym: BarcodeSymbology,
opts: BarcodeOptions
): BarcodeResult {
return encode(data, sym, opts);
}
function validateAndParse(
data: string,
sym: BarcodeSymbology
): ParseResult {
return parse(data, sym);
}
const result: BarcodeResult = renderWithOptions("5901234123457", "ean13", {
width: 300,
height: 80,
includeText: true,
margin: 10,
color: "#000000",
background: "#ffffff",
});
const parsed: ParseResult = validateAndParse("5901234123457", "ean13");
// parsed.valid === true, parsed.checkDigit === "7"
```
## API Reference
### `BarcodeSymbology`
```typescript
type BarcodeSymbology =
| "code128"
| "code39"
| "ean13"
| "ean8"
| "upca"
| "upce"
| "itf";
```
Union of all supported barcode symbology identifiers. Pass one of these string literals wherever a symbology argument is required.
### `BarcodeOptions`
```typescript
interface BarcodeOptions {
width?: number; // SVG width in pixels (default: 200)
height?: number; // SVG height in pixels (default: 100)
includeText?: boolean; // Render human-readable text below bars (default: true)
margin?: number; // Quiet-zone margin in pixels (default: 10)
color?: string; // Bar colour as CSS colour string (default: "#000000")
background?: string; // Background colour as CSS colour string (default: "#ffffff")
}
```
All fields are optional. Defaults are applied inside `encode` when fields are omitted.
### `BarcodeResult`
```typescript
interface BarcodeResult {
symbology: BarcodeSymbology; // Symbology used
data: string; // Original input data string
svg: string; // Complete SVG document as a string
binary: string; // Binary bar pattern ("1" = bar, "0" = space)
valid: boolean; // Whether check digit (if any) is correct
}
```
Returned by `encode()`. The `svg` field is a self-contained SVG document with a root `<svg>` element including namespace and `viewBox`. The `binary` field exposes the raw bit string for custom rendering pipelines.
### `ParseResult`
```typescript
interface ParseResult {
symbology: BarcodeSymbology; // Symbology used during parse
data: string; // Input data string
valid: boolean; // Whether the data is valid for the symbology
checkDigit?: string; // Extracted check digit, if applicable to the symbology
}
```
Returned by `parse()`. The `checkDigit` field is present only for symbologies that have a numeric check digit (EAN-13, EAN-8, UPC-A, UPC-E). It is `undefined` for Code 128, Code 39, and ITF.
## Implementation Notes
These are pure TypeScript `interface` and `type` declarations with no associated runtime code. The compiled output for this file is an empty module — all information is erased at transpile time. This means the types impose zero bundle cost.
The `BarcodeOptions` interface intentionally uses all-optional fields to support progressive disclosure: callers can pass an empty object `{}` or omit the argument entirely and receive a fully-rendered barcode with sensible defaults. This avoids forcing boilerplate for the common case while still allowing fine-grained control.
`ParseResult.checkDigit` is typed as `string | undefined` rather than `number | undefined` because check digits are single decimal characters extracted from the original input string. Keeping them as strings avoids ambiguity for leading-zero scenarios and matches the pattern used throughout the validator module.
The `binary` field on `BarcodeResult` is a `"0"` / `"1"` string rather than a `Uint8Array` because human readability and debuggability outweigh the marginal memory advantage of a bit array for single-barcode generation. Consumers who need bit-level storage can trivially convert with `Array.from(result.binary).map(Number)`.

View File

@@ -0,0 +1,131 @@
# @suaveplan/qrcode - QR code generation producing SVG, pixel matrix, terminal (Unicode blocks), and data URL representations. Implements QR code versions 1-10, error correction levels L/M/Q/H, automatic version and mode selection.
## Summary
QR code generation producing SVG, pixel matrix, terminal (Unicode blocks), and data URL representations. Implements QR code versions 1-10, error correction levels L/M/Q/H, automatic version and mode selection.
<!-- begin:auto-docs -->
## Documentation tree
- [core](./src/core/qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- data-encoder — <!-- begin:auto-docs --> _(no submodu…
- [encoder](./src/core/encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [data-encoder](./src/core/encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [mask](./src/core/mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [masking](./src/core/mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [matrix](./src/core/matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [matrix-builder](./src/core/matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [reed-solomon](./src/core/reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [reed-solomon](./src/core/reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [renderer](./src/core/renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [svg-renderer](./src/core/renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [tables](./src/core/tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [tables](./src/core/tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [qrcode](./src/core/qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- data-encoder — <!-- begin:auto-docs --> _(no submodu…
- [errors](./src/errors/qrcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [qrcode-error](./src/errors/qrcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [encoder](./src/core/encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [data-encoder](./src/core/encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask](./src/core/mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [masking](./src/core/mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix](./src/core/matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix-builder](./src/core/matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./src/core/reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./src/core/reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [renderer](./src/core/renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [svg-renderer](./src/core/renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./src/core/tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./src/core/tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode](./src/core/qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/qrcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode-error](./src/errors/qrcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- data-encoder — <!-- begin:au…
- [encoder](./src/core/encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [data-encoder](./src/core/encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask](./src/core/mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [masking](./src/core/mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix](./src/core/matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix-builder](./src/core/matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./src/core/reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./src/core/reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [renderer](./src/core/renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [svg-renderer](./src/core/renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./src/core/tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./src/core/tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode](./src/core/qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_- data-encoder — <!-- begin:au…
- [errors](./src/errors/qrcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode-error](./src/errors/qrcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- data-encoder — <!-- begin:auto-docs --> _(no submod…
- [encoder](./src/core/encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [data-encoder](./src/core/encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask](./src/core/mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [masking](./src/core/mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix](./src/core/matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix-builder](./src/core/matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./src/core/reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./src/core/reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [renderer](./src/core/renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [svg-renderer](./src/core/renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./src/core/tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./src/core/tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode](./src/core/qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_- data-encoder — <!-- begin:auto-docs --> _(no submod…
- [errors](./src/errors/qrcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode-error](./src/errors/qrcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [encoder](./src/core/encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_ - [data-encoder](./src/core/encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask](./src/core/mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_ - [masking](./src/core/mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix](./src/core/matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_ - [matrix-builder](./src/core/matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./src/core/reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_ - [reed-solomon](./src/core/reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [renderer](./src/core/renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [svg-renderer](./src/core/renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./src/core/tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tables](./src/core/tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode](./src/core/qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/qrcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [qrcode-error](./src/errors/qrcode-error.md) — <!-- begin:auto-docs --> _(no submodules)_
## Installation
```bash
bun add @suaveplan/qrcode
```
## Quick Start
```ts
import { generate } from "@suaveplan/qrcode";
const result = generate("https://suaveplan.io");
console.log(result.svg); // Inline SVG string
console.log(result.dataUrl); // data:image/svg+xml;base64,...
console.log(result.terminal); // Unicode half-block for CLI display
console.log(result.version); // e.g. 2
console.log(result.size); // e.g. 25 (version 2 = 25×25)
```
## API
### `generate(data, options?)`
Generates a complete QR code for any UTF-8 string.
```ts
import { generate } from "@suaveplan/qrcode";
const result = generate("Hello World", {
errorCorrectionLevel: "H", // 'L' | 'M' | 'Q' | 'H', default 'M'
version: 2, // 1-10, auto-selected if omitted
margin: 4, // quiet zone in modules, default 4
color: "#000000", // dark module color, default '#000000'
background: "#ffffff", // light module color, default '#ffffff'
scale: 10, // pixels per module in SVG, default 10
});
```
**Returns** `QRCodeResult`:
| Field | Type | Description |
|-------|------|-------------|
| `version` | `number` | QR version used (1-10) |
| `errorCorrectionLevel` | `ErrorCorrectionLevel` | EC level used |
| `mode` | `QRMode` | Encoding mode (numeric/alphanumeric/byte) |
| `matrix` | `boolean[][]` | Module grid — `true` = dark |
| `size` | `number` | Matrix dimension (size × size) |
| `svg` | `string` | Complete SVG markup |
| `dataUrl` | `string` | `data:image/svg+xml;base64,...` |
| `terminal` | `string` | Unicode half-block terminal representation |
### Errors
```ts
import { QRCodeError, QRCODE_CODES } from "@suaveplan/qrcode/errors";
try {
generate("", { version: 1, errorCorrectionLevel: "H" });
} catch (e) {
if (e instanceof QRCodeError) {
switch (e.code) {
case QRCODE_CODES.INVALID_DATA:
// Empty input, invalid characters, or out-of-range version
break;
case QRCODE_CODES.VERSION_TOO_SMALL:
// Data too large for the specified version (or all versions 1-10)
break;
case QRCODE_CODES.ENCODE_FAILED:
// Internal encoding pipeline failure
break;
}
}
}
```
## Module Exports
| Subpath | Contents |
|---------|----------|
| `@suaveplan/qrcode` | `generate`, all types, all sub-module exports |
| `@suaveplan/qrcode/errors` | `QRCodeError`, `QRCODE_CODES`, `QRCodeCode` |
| `@suaveplan/qrcode/core/tables` | Lookup tables and helpers |
| `@suaveplan/qrcode/core/encoder` | `detectMode`, `encodeData`, `BitBuffer`, `encodeUtf8` |
| `@suaveplan/qrcode/core/reed-solomon` | `rsEncode`, `interleaveBlocks`, `splitBlocks`, GF arithmetic |
| `@suaveplan/qrcode/core/matrix` | `buildMatrix` |
| `@suaveplan/qrcode/core/mask` | `selectMask`, `penaltyScore` |
| `@suaveplan/qrcode/core/renderer` | `renderSvg`, `renderDataUrl`, `renderTerminal` |
## Co-located Documentation
- [errors/qrcode-error.md](src/errors/qrcode-error.md)
- [core/tables/tables.md](src/core/tables/tables.md)
- [core/encoder/data-encoder.md](src/core/encoder/data-encoder.md)
- [core/reed-solomon/reed-solomon.md](src/core/reed-solomon/reed-solomon.md)
- [core/matrix/matrix-builder.md](src/core/matrix/matrix-builder.md)
- [core/mask/masking.md](src/core/mask/masking.md)
- [core/renderer/svg-renderer.md](src/core/renderer/svg-renderer.md)
- [core/qrcode.md](src/core/qrcode.md)

View File

@@ -0,0 +1,83 @@
# data-encoder
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements mode detection and bit-stream construction for QR code data encoding. Given a string, it selects the most compact encoding mode (numeric, alphanumeric, or byte), builds the complete bit stream including mode indicator and character count indicator, appends the terminator and padding codewords per ISO/IEC 18004:2015, and returns the final array of data codewords ready for Reed-Solomon processing.
The encoder also exports a `BitBuffer` class (an efficient bit-oriented writer) and the `encodeUtf8` helper, both used internally and by tests to verify intermediate encoding state.
## Features
- Automatic mode detection: numeric → alphanumeric → byte in priority order
- `BitBuffer` accumulates arbitrary-width integers into a packed byte array
- Numeric encoding: groups of 3 digits → 10 bits, 2 → 7 bits, 1 → 4 bits
- Alphanumeric encoding: pairs → 11 bits (first×45+second), single → 6 bits
- Byte encoding: UTF-8 with full surrogate-pair handling for code points above U+FFFF
- Terminator bits (up to 4 zero bits), byte-boundary padding, and pad codeword alternation (0xEC/0x11)
- Character count indicator bit length selected by version range (1-9, 10-26, 27-40)
- Throws `QRCodeError(QRCODE_CODES.INVALID_DATA)` for empty input or invalid alphanumeric characters
## Usage: Basic Example
```ts
import { detectMode, encodeData } from "@suaveplan/qrcode/core/encoder";
const mode = detectMode("01234567"); // "numeric"
const codewords = encodeData("01234567", mode, 1, 19);
// codewords is an array of 19 bytes ready for RS processing
```
## Usage: Advanced Example
```ts
import { BitBuffer, detectMode, encodeData, encodeUtf8 } from "@suaveplan/qrcode/core/encoder";
// Verify intermediate bit stream for "HELLO WORLD"
const buf = new BitBuffer();
buf.put(0b0010, 4); // alphanumeric mode indicator
buf.put(11, 9); // character count (9 bits for version 1)
// ... encode each pair
console.log(buf.bitLength);
// Byte mode with emoji
const mode = detectMode("Hello 🌍");
const cws = encodeData("Hello 🌍", mode, 2, 28);
// UTF-8 bytes for the emoji are correctly split across codewords
```
## API Reference
### `class BitBuffer`
**`put(value: number, numBits: number): void`** — Appends the `numBits` least-significant bits of `value` (MSB first).
**`putBit(bit: boolean): void`** — Appends a single bit.
**`get bitLength(): number`** — Total bits written so far.
**`get bytes(): number[]`** — The backing byte array. Length is `Math.ceil(bitLength / 8)`.
### `detectMode(data: string): QRMode`
Returns the most compact QR encoding mode for `data`: `"numeric"` if all digits, `"alphanumeric"` if all characters are in the QR alphanumeric set, otherwise `"byte"`.
### `encodeData(data, mode, version, totalDataCW): number[]`
Encodes `data` in `mode` for the given `version` into exactly `totalDataCW` bytes, including mode indicator, character count, payload, terminator, and padding.
**Throws** `QRCodeError(QRCODE_CODES.INVALID_DATA)` if `data` is empty or contains characters invalid for the specified mode.
### `encodeUtf8(str: string): number[]`
Converts a JavaScript string to a UTF-8 byte array, handling Basic Multilingual Plane and supplementary code points via surrogate pairs.
## Implementation Notes
`BitBuffer` packs bits MSB-first into a byte array. The `put` method loops from the most-significant bit of `value` down to 0, using a shift and mask to extract each bit. This matches the QR spec's bit ordering convention.
Numeric encoding computes `parseInt` on slices — safe because the mode guard guarantees only digit characters. Alphanumeric encoding throws on characters absent from `ALPHANUMERIC_TABLE` rather than silently skipping them, since a misdetected mode would corrupt the scan result.
The pad byte sequence alternates 0xEC → 0x11 → 0xEC → ... as required by the QR specification. The terminator is clamped to `min(4, remaining bits)` to handle the edge case where the payload exactly fills the codeword capacity.

View File

@@ -0,0 +1,72 @@
# masking
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements QR code mask pattern evaluation and optimal mask selection per ISO/IEC 18004:2015 Section 7.8. Masking improves the readability of QR codes by preventing large uniform regions that confuse scanners. The module evaluates all 8 candidate masks against four penalty rules and selects the mask yielding the lowest total penalty score.
The penalty rules measure four specific undesirable patterns: long consecutive same-color runs, 2×2 same-color blocks, finder-like 1:1:3:1:1 patterns that confuse the scanner's finder detection, and skewed proportions of dark versus light modules.
## Features
- All four ISO penalty rules implemented with correct weight constants (N1=3, N2=3, N3=40, N4=10)
- Penalty rule 1: consecutive same-color runs ≥ 5 in rows and columns
- Penalty rule 2: 2×2 same-color blocks
- Penalty rule 3: finder-like patterns (both forward and reverse orientations)
- Penalty rule 4: dark module proportion deviation from 50%
- `penaltyScore` for single-matrix scoring (useful for testing and external tools)
- `selectMask` evaluates all 8 masked matrices and returns the lowest-penalty matrix with its mask index
## Usage: Basic Example
```ts
import { selectMask } from "@suaveplan/qrcode/core/mask";
const { maskPattern, matrix } = selectMask(1, "M", interleavedCW);
// matrix is the boolean[][] ready for rendering
// maskPattern is 0-7, recorded for format information encoding
```
## Usage: Advanced Example
```ts
import { selectMask, penaltyScore } from "@suaveplan/qrcode/core/mask";
import { buildMatrix } from "@suaveplan/qrcode/core/matrix";
// Force a specific mask and score it manually
const matrix = buildMatrix(2, "Q", 3, data);
const score = penaltyScore(matrix);
console.log(`Mask 3 penalty: ${score}`);
// Or let selectMask choose
const best = selectMask(2, "Q", data);
console.log(`Best mask: ${best.maskPattern}`);
```
## API Reference
### `penaltyScore(matrix: boolean[][]): number`
Computes the total penalty score for a boolean module matrix. Lower is better. Used internally by `selectMask` and available for external inspection.
### `selectMask(version, ecLevel, data): { maskPattern: number; matrix: boolean[][] }`
Evaluates all 8 mask patterns by calling `buildMatrix` for each, scores each matrix with `penaltyScore`, and returns the result with the minimum penalty.
**Parameters:**
- `version: number` — QR version 1-10
- `ecLevel: ErrorCorrectionLevel` — error correction level
- `data: number[]` — interleaved codeword bytes
**Returns:** `{ maskPattern: number; matrix: boolean[][] }` where `maskPattern` is 0-7 and `matrix` is the final boolean matrix ready for rendering.
## Implementation Notes
Penalty rule 3 uses two reference patterns from the QR spec: `1011101 0000` and `0000 1011101`, both of length 11 modules. Each occurrence in any row or column contributes N3=40 to the total. The column check extracts each column into a temporary array to allow the same pattern-matching logic as rows; this avoids code duplication at the cost of one allocation per column per pattern evaluation.
Penalty rule 4 computes the percentage of dark modules, finds the nearest 5% multiples below and above, takes the absolute deviation from 50% for each divided by 5, and multiplies the minimum by N4=10. This matches the ISO specification's "step" approach.
`selectMask` builds all 8 matrices independently (8 calls to `buildMatrix`). This is correct per spec since the mask is applied inside `buildMatrix`; the format information encoding also varies by mask, so each candidate is a fully formed QR matrix.

View File

@@ -0,0 +1,68 @@
# matrix-builder
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Constructs the complete QR code module matrix by placing all functional patterns and data modules according to ISO/IEC 18004:2015. The builder operates on a numeric grid (0=light, 1=dark, 2=reserved, 255=unset), progressively filling in finder patterns, timing patterns, alignment patterns, format information reservations, and finally the data codewords. After data placement, a mask pattern is applied and format information is written into the previously reserved areas.
The output is a `boolean[][]` where `true` represents a dark module and `false` a light module, providing a clean, format-independent representation consumed by renderers and the masking scorer.
## Features
- Three finder pattern placements (7×7) with separator strips
- Horizontal and vertical timing patterns on row/column 6
- Version-adaptive alignment pattern placement (versions 2-10) with overlap detection for finder pattern corners
- Format information area reservation before data placement
- Two-column zigzag data placement with column-6 skip per QR spec
- All 8 mask patterns (ISO conditions 0-7)
- Format information writing (15-bit pre-computed BCH values from tables module)
- Sentinel-based reservation ensures function modules are never overwritten by data
## Usage: Basic Example
```ts
import { buildMatrix } from "@suaveplan/qrcode/core/matrix";
const matrix = buildMatrix(1, "M", 2, interleavedCWBytes);
// matrix[0][0] === true // top-left finder dark corner
// matrix.length === 21 // version 1 is 21×21
```
## Usage: Advanced Example
```ts
import { buildMatrix } from "@suaveplan/qrcode/core/matrix";
// Build all 8 masked variants for penalty scoring
const candidates = Array.from({ length: 8 }, (_, mask) =>
buildMatrix(version, ecLevel, mask, interleaved)
);
// Feed candidates to masking scorer to pick the best one
```
## API Reference
### `buildMatrix(version, ecLevel, maskPattern, data): boolean[][]`
Constructs the complete QR matrix.
**Parameters:**
- `version: number` — QR version 1-10
- `ecLevel: ErrorCorrectionLevel` — `"L" | "M" | "Q" | "H"`
- `maskPattern: number` — mask index 0-7
- `data: number[]` — interleaved codeword bytes from `interleaveBlocks`
**Returns:** `boolean[][]` of size `(version * 4 + 17) × (version * 4 + 17)`. `true` = dark module.
## Implementation Notes
The matrix uses a `Uint8Array[]` internally with four sentinel values: 255 (unset), 0 (light), 1 (dark), 2 (reserved/function). Data placement only writes to cells with value 255; all function pattern cells are set before data placement begins. The mask is applied only to non-reserved, non-unset cells, so function modules remain stable.
Format information is written last to overwrite the reserved-sentinel cells with their final bit values. The dark module at `(size-8, 8)` is placed during format reservation and never overwritten.
Alignment pattern placement skips three positions that would overlap the finder pattern corners: `(6,6)`, `(6, last)`, and `(last, 6)`. This matches the QR spec's exception table for versions 7 and above but is also applied to versions 2-6 where those positions do not occur in the position table.
The data zigzag alternates upward and downward column pairs of width 2, stepping right-to-left. Column 6 (timing column) is skipped by treating the strip at column 7 as a width-3 step instead of width-2.

View File

@@ -0,0 +1,102 @@
# qrcode
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [data-encoder](./encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask](./mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_ - [masking](./mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix](./matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_ - [matrix-builder](./matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_ - [reed-solomon](./reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [renderer](./renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [svg-renderer](./renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tables](./tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode](./qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_- data-encoder — <!-- begin:auto-docs --> _(no submodu…
- [data-encoder](./encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask](./mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [masking](./mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix](./matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix-builder](./matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [renderer](./renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [svg-renderer](./renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode](./qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_- [data-encoder](./encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask](./mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [masking](./mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix](./matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix-builder](./matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [renderer](./renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [svg-renderer](./renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode](./qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_- data-encoder — <!-- begin:au…
- [data-encoder](./encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask](./mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [masking](./mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix](./matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix-builder](./matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [renderer](./renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [svg-renderer](./renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode](./qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_- data-encoder — <!-- begin:auto-docs --> _(no submod…
- [data-encoder](./encoder/data-encoder.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask](./mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_ - [masking](./mask/masking.md) — <!-- begin:auto-docs --> _(no submodules)_- [matrix](./matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_ - [matrix-builder](./matrix/matrix-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [reed-solomon](./reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_ - [reed-solomon](./reed-solomon/reed-solomon.md) — <!-- begin:auto-docs --> _(no submodules)_- [renderer](./renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [svg-renderer](./renderer/svg-renderer.md) — <!-- begin:auto-docs --> _(no submodules)_- [tables](./tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tables](./tables/tables.md) — <!-- begin:auto-docs --> _(no submodules)_- [qrcode](./qrcode.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
Top-level orchestrator for the `@suaveplan/qrcode` package. The `generate` function is the single public entry point that combines all pipeline stages: mode detection, version selection, data encoding, Reed-Solomon error correction, block interleaving, mask scoring, matrix construction, and rendering. Callers provide a string and optional configuration; the function returns a `QRCodeResult` with all four output representations populated.
OpenTelemetry span instrumentation is applied to the full generation pipeline via a graceful dynamic import: if `@opentelemetry/api` is available at runtime it records span attributes (version, EC level, mode, mask pattern) and status; if OTel is absent the function runs without tracing and without throwing.
## Features
- Single `generate(data, options?)` function covering the complete QR code pipeline
- Automatic version selection (tries versions 1-10 in order, picks the smallest that fits)
- Explicit version override with capacity validation and informative error
- EC level defaults to `'M'` if not specified
- Returns `QRCodeResult` with version, EC level, mode, matrix, size, SVG, dataUrl, terminal
- OTel span with attributes: `qrcode.version`, `qrcode.ecLevel`, `qrcode.mode`, `qrcode.maskPattern`
- Throws `QRCodeError` with stable codes for empty input, oversized data, and encoding failures
## Usage: Basic Example
```ts
import { generate } from "@suaveplan/qrcode";
const result = generate("https://suaveplan.io");
console.log(result.svg); // SVG string
console.log(result.terminal); // Unicode block output
console.log(result.version); // e.g. 3
```
## Usage: Advanced Example
```ts
import { generate } from "@suaveplan/qrcode";
const result = generate("HELLO WORLD", {
errorCorrectionLevel: "H",
version: 2,
scale: 6,
margin: 2,
color: "#1a1a2e",
background: "#ffffff",
});
// Embed as img src
const img = document.createElement("img");
img.src = result.dataUrl;
// Use raw matrix for custom rendering
for (let row = 0; row < result.size; row++) {
for (let col = 0; col < result.size; col++) {
const dark = result.matrix[row][col];
// draw(col * 10, row * 10, dark);
}
}
```
## API Reference
### `generate(data: string, options?: QRCodeOptions): QRCodeResult`
Generates a QR code for `data`.
**Parameters:**
- `data` — non-empty string to encode; any UTF-8 content
- `options.errorCorrectionLevel` — `"L" | "M" | "Q" | "H"`, default `"M"`
- `options.version` — QR version 1-10, auto-selected if omitted
- `options.margin` — quiet zone in modules, default 4
- `options.color` — dark module color string, default `"#000000"`
- `options.background` — light module color string, default `"#ffffff"`
- `options.scale` — pixels per module in SVG output, default 10
**Returns:** `QRCodeResult`
- `version: number` — version used
- `errorCorrectionLevel: ErrorCorrectionLevel` — level used
- `mode: QRMode` — encoding mode selected
- `matrix: boolean[][]` — module grid, true=dark
- `size: number` — matrix dimension
- `svg: string` — SVG markup
- `dataUrl: string` — `data:image/svg+xml;base64,...`
- `terminal: string` — Unicode half-block representation
**Throws:** `QRCodeError` with:
- `QRCODE_CODES.INVALID_DATA` — empty input or out-of-range version
- `QRCODE_CODES.VERSION_TOO_SMALL` — data too large for the version or for all versions 1-10
## Implementation Notes
Version auto-selection measures byte length for byte mode and character count for numeric and alphanumeric modes. This matches the QR spec's capacity tables, which are defined in terms of characters for non-byte modes and bytes for byte mode.
The OTel integration uses a top-level `await import()` to avoid bundling issues when OTel is a peer dependency. If the import fails (not installed), the function silently proceeds without tracing. Span status is set to `OK` on success and `ERROR` with a recorded exception on failure.
The mask selection loop in `selectMask` builds all 8 matrices and picks the minimum penalty. For versions up to 10 (maximum 57×57 modules), this executes in well under 1ms on modern hardware.

View File

@@ -0,0 +1,94 @@
# reed-solomon
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements Reed-Solomon error correction over GF(256) for QR code generation. The finite field uses the primitive polynomial x⁸+x⁴+x³+x²+1 (0x11D) as specified in ISO/IEC 18004:2015 Annex A. This module provides the complete RS pipeline: GF(256) arithmetic primitives, generator polynomial construction, codeword computation via polynomial long division, block splitting, and final codeword interleaving.
Reed-Solomon allows QR scanners to reconstruct the original data even when part of the code is damaged or obscured. The number of correctable errors scales with the error correction level: L corrects ~7%, M ~15%, Q ~25%, H ~30% of total codewords.
## Features
- GF(256) exp/log table construction at module load (O(255) init, O(1) multiply)
- `gfMul`, `gfDiv`, `gfAdd`, `gfPow` arithmetic primitives
- `polyMul` for polynomial multiplication in GF(256)
- `generatorPoly(n)` builds the RS generator polynomial g(x) = ∏(x − αⁱ) for i=0..n-1
- `rsEncode(data, nEC)` performs polynomial long division to produce EC codewords
- `splitBlocks` partitions data codewords by the block group structure from `BLOCK_TABLE`
- `interleaveBlocks` interleaves data and EC codewords across all blocks in the column order required by the QR matrix data placement algorithm
## Usage: Basic Example
```ts
import { rsEncode } from "@suaveplan/qrcode/core/reed-solomon";
// Compute 10 EC codewords for a V1-M data block
const dataCW = [32, 91, 11, 120, 209, 114, 220, 77, 67, 64, 236, 17, 236, 17, 236, 17];
const ecCW = rsEncode(dataCW, 10);
// ecCW is an array of 10 bytes
```
## Usage: Advanced Example
```ts
import {
splitBlocks,
interleaveBlocks,
gfMul,
generatorPoly,
} from "@suaveplan/qrcode/core/reed-solomon";
import { BLOCK_TABLE, EC_LEVEL_INDEX } from "@suaveplan/qrcode/core/tables";
const blockInfo = BLOCK_TABLE[4][EC_LEVEL_INDEX["M"]]; // V5 M
const dataBlocks = splitBlocks(allDataCW, blockInfo.blocks);
const finalCW = interleaveBlocks(dataBlocks, blockInfo.ecCWPerBlock);
// finalCW is ready to be placed into the QR matrix
```
## API Reference
### `gfMul(a: number, b: number): number`
Multiplies two GF(256) elements using the exp/log tables. Returns 0 if either operand is 0.
### `gfDiv(a: number, b: number): number`
Divides `a` by `b` in GF(256). Throws `RangeError` if `b === 0`.
### `gfAdd(a: number, b: number): number`
Adds (XORs) two GF(256) elements. Subtraction is identical in GF(2^n).
### `gfPow(x: number, power: number): number`
Computes `α^(log(x) * power) mod 255` using the exp/log tables.
### `polyMul(p: Uint8Array, q: Uint8Array): Uint8Array`
Multiplies two GF(256) polynomials. Coefficient arrays are indexed by degree (index 0 = constant term).
### `generatorPoly(nECCodewords: number): Uint8Array`
Builds the RS generator polynomial for `nECCodewords` EC codewords. Cached implicitly by the caller since the same block always uses the same EC count.
### `rsEncode(data: number[], nECCodewords: number): number[]`
Computes `nECCodewords` Reed-Solomon error correction codewords for `data`. Returns only the EC bytes (not the original data).
### `splitBlocks(dataCW, blocks): number[][]`
Splits a flat data codeword array into individual block arrays according to the block group structure from `BLOCK_TABLE`. Groups with `count: 0` are skipped.
### `interleaveBlocks(dataBlocks, ecCWPerBlock): number[]`
Produces the final codeword sequence by interleaving all block data columns, then all block EC columns. This column-major ordering is required by the QR matrix data placement algorithm.
## Implementation Notes
The exp/log tables are computed once at module initialisation time via an IIFE. The GF(256) multiplication is therefore a single array lookup pair (two reads + one modular add), making it the fastest possible approach for this field size.
The polynomial long division in `rsEncode` operates in-place on a pre-allocated message array of length `data.length + nECCodewords`, avoiding dynamic allocation inside the inner loop. After processing, only the last `nECCodewords` bytes are returned as the EC codewords.
The interleaver handles unequal block sizes (one group with `dataCW` bytes, a second group with `dataCW + 1` bytes for versions like V5) by iterating up to the maximum block length and skipping blocks that have been exhausted. This matches the QR spec's data placement order exactly.

View File

@@ -0,0 +1,78 @@
# svg-renderer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Converts a QR code module matrix (`boolean[][]`) into human- and machine-readable output formats. Three representations are produced: an SVG string for web embedding and print quality at any scale, a base64 data URL for use in HTML `img` src attributes without external file references, and a Unicode half-block terminal string for command-line display and debugging.
All renderers accept the same `QRCodeOptions` object, using the `margin`, `color`, `background`, and `scale` fields to control appearance. This keeps the rendering API uniform across output formats and allows the top-level `generate` function to call all three with a single options object.
## Features
- `renderSvg`: inline SVG with configurable pixel scale, margin, dark/light colors; uses `<rect>` elements for maximum compatibility (no `<path>`)
- `renderDataUrl`: wraps `renderSvg` output as `data:image/svg+xml;base64,...` for `<img src>` embedding
- `renderTerminal`: packs 2 module rows per terminal line using Unicode half-block characters (UP HALF BLOCK, LOWER HALF BLOCK, FULL BLOCK, space), with configurable quiet zone
- XML attribute value escaping (`&`, `<`, `>`, `"`, `'`) prevents SVG injection from user-supplied color strings
- Quiet zone (margin) applied identically in all three renderers for visual consistency
## Usage: Basic Example
```ts
import { renderSvg } from "@suaveplan/qrcode/core/renderer";
const svg = renderSvg(matrix, { scale: 4, margin: 2 });
// Returns a complete SVG string, 100x100px for a 21-module V1 QR
```
## Usage: Advanced Example
```ts
import { renderSvg, renderDataUrl, renderTerminal } from "@suaveplan/qrcode/core/renderer";
const opts = { color: "#1a1a2e", background: "#f0f0f0", scale: 8, margin: 4 };
const svg = renderSvg(matrix, opts);
const url = renderDataUrl(matrix, opts);
const cli = renderTerminal(matrix, { margin: 2 });
// Set on an img element src attribute
const imgSrc = url;
// Print in terminal
console.log(cli);
```
## API Reference
### `renderSvg(matrix: boolean[][], options?: QRCodeOptions): string`
Renders the matrix to an SVG string. Each dark module becomes a `<rect>` element. A background `<rect>` covers the entire canvas including the quiet zone.
**Options used:** `scale` (pixels per module, default 10), `margin` (quiet zone in modules, default 4), `color` (dark module fill, default `#000000`), `background` (light fill, default `#ffffff`).
### `renderDataUrl(matrix: boolean[][], options?: QRCodeOptions): string`
Calls `renderSvg` and encodes the result as a UTF-8-safe base64 data URL: `data:image/svg+xml;base64,...`.
### `renderTerminal(matrix: boolean[][], options?: QRCodeOptions): string`
Renders the matrix to a multi-line string using Unicode half-block characters. Two module rows are collapsed into one terminal row. Options used: `margin` (quiet zone, default 4).
**Terminal character mapping:**
- U+2588 FULL BLOCK: top dark, bottom dark
- U+2580 UPPER HALF BLOCK: top dark, bottom light
- U+2584 LOWER HALF BLOCK: top light, bottom dark
- U+0020 SPACE: top light, bottom light
## Implementation Notes
`renderSvg` generates one `<rect>` per dark module rather than using `<path>` commands. While this produces larger SVG text for dense QR codes, it is universally compatible across SVG 1.1 viewers, email clients, and PDF renderers. The `viewBox` attribute ensures the SVG scales correctly in responsive CSS layouts.
`renderDataUrl` uses `btoa(unescape(encodeURIComponent(svg)))` to handle Unicode characters in user-supplied color strings before base64 encoding, avoiding `btoa` failures on non-Latin characters.
`renderTerminal` pads the matrix with `margin` rows/columns of light modules before processing, then iterates in pairs of rows. If the matrix has an odd total padded height, the final row is treated as if followed by a light row.
The `escapeXml` function is applied to color strings in the SVG output to prevent injection via crafted color values. Colors should also be validated at the options level before reaching the renderer.

View File

@@ -0,0 +1,107 @@
# tables
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Centralises every constant lookup table required by the QR code encoding pipeline. All data is sourced directly from ISO/IEC 18004:2015 and arranged for O(1) access by version (1-indexed) and error correction level (L/M/Q/H). Having a single canonical table module eliminates drift between the encoder, Reed-Solomon generator, matrix builder, and format information writer: they all read from one authoritative source.
The module deliberately avoids any computation at import time beyond trivial object/array literals so tree-shaking is maximally effective; unused capacity tables are eliminated at build time.
## Features
- Byte, numeric, and alphanumeric capacity tables for versions 1-10 × 4 EC levels
- Full block structure table (`BLOCK_TABLE`) describing data/EC codeword counts and group layouts for interleaved data blocks
- Alignment pattern centre positions for versions 1-10
- Alphanumeric character value lookup (O(1) string → integer)
- Mode indicator nibbles and character count indicator bit lengths
- Pre-computed format information 15-bit strings (BCH(15,5) + XOR mask)
- `charCountBits(mode, version)` helper for version-range dispatch
- `qrSize(version)` helper computing module matrix dimension
## Usage: Basic Example
```ts
import { BYTE_CAPACITY, EC_LEVEL_INDEX } from "@suaveplan/qrcode/core/tables";
const ecIdx = EC_LEVEL_INDEX["M"]; // 1
const version = 1;
const maxBytes = BYTE_CAPACITY[version - 1][ecIdx]; // 14
```
## Usage: Advanced Example
```ts
import {
BLOCK_TABLE,
EC_LEVEL_INDEX,
FORMAT_INFO_STRINGS,
ALPHANUMERIC_TABLE,
} from "@suaveplan/qrcode/core/tables";
// Look up block structure for V3 M
const blockInfo = BLOCK_TABLE[2][EC_LEVEL_INDEX["M"]];
// blockInfo.ecCWPerBlock === 26, blockInfo.blocks === [{ count: 1, dataCW: 44 }]
// Encode alphanumeric pair
const charVal = ALPHANUMERIC_TABLE["A"] * 45 + ALPHANUMERIC_TABLE["B"];
// charVal === 10 * 45 + 11 === 461
// Read pre-computed format info for EC=M, mask=2
const fmtBits = FORMAT_INFO_STRINGS[1 * 8 + 2]; // 0x5e7c
```
## API Reference
### `EC_LEVEL_INDEX`
`Record<ErrorCorrectionLevel, number>` mapping `L→0`, `M→1`, `Q→2`, `H→3`. Used as the second dimension index into all 2D tables.
### `BYTE_CAPACITY`
`ReadonlyArray<readonly [number,number,number,number]>`. Byte-mode capacity in bytes. First index is `version - 1`, second is `EC_LEVEL_INDEX[level]`.
### `NUMERIC_CAPACITY`
Same shape as `BYTE_CAPACITY` for numeric-mode character counts.
### `ALPHANUMERIC_CAPACITY`
Same shape as `BYTE_CAPACITY` for alphanumeric-mode character counts.
### `BLOCK_TABLE`
`ReadonlyArray<readonly [BlockInfo, BlockInfo, BlockInfo, BlockInfo]>`. Full interleaving block structure. `BlockInfo` has `{ totalDataCW, ecCWPerBlock, blocks: Array<{ count, dataCW }> }`. First index is `version - 1`, second is `EC_LEVEL_INDEX[level]`.
### `ALIGNMENT_POSITIONS`
`ReadonlyArray<ReadonlyArray<number>>`. Centre coordinates of alignment patterns. First index is `version - 1`; version 1 returns an empty array.
### `ALPHANUMERIC_TABLE`
`Readonly<Record<string, number>>`. Maps each of the 45 alphanumeric characters to its QR spec integer value.
### `MODE_INDICATOR`
`Record<string, number>`. Maps mode name strings to 4-bit mode indicator values.
### `CHAR_COUNT_BITS`
`Record<string, readonly [number, number, number]>`. Character count indicator lengths for three version ranges per mode.
### `FORMAT_INFO_STRINGS`
`ReadonlyArray<number>`. 32 pre-computed 15-bit format information values. Index as `ecLevelIndex * 8 + maskPattern`.
### `charCountBits(mode: string, version: number): number`
Returns the character count indicator bit length for the given mode and version number.
### `qrSize(version: number): number`
Returns the module matrix dimension: `version * 4 + 17`.
## Implementation Notes
All capacity and block tables are lifted verbatim from Annex I of ISO/IEC 18004:2015. Format information strings were independently verified against the reference implementation by cross-checking against the QR code generator specification Table C.1. The `BLOCK_TABLE` entries include zero-count groups (count: 0) to simplify iteration logic in the matrix builder — the builder skips groups with count 0. Version 10 support completes the "versions 1-10" stated scope; versions above 10 are rejected at the API boundary.

View File

@@ -0,0 +1,90 @@
# qrcode-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`QRCodeError` is the typed error class for the entire `@suaveplan/qrcode` package. It extends the native `Error` class with a stable `code` property drawn from `QRCODE_CODES`, allowing callers to discriminate failure modes programmatically without string parsing. The class intentionally avoids the `@suaveplan/error` dependency so this leaf package remains dependency-minimal: only `zod`, `tslib`, and `@opentelemetry/api` are required at runtime.
The three error codes cover every failure surface of the QR code generation pipeline: invalid input data, insufficient version capacity for the payload, and unexpected encoding pipeline failures. All three are stable across patch releases.
## Features
- `QRCODE_CODES` frozen registry with three stable error codes
- `QRCodeCode` union type derived from the registry
- `QRCodeError` extends `Error` with a narrow `code: QRCodeCode` property
- `instanceof` discrimination for structured error handling
- `name` property set to `"QRCodeError"` for logging and stack trace clarity
## Usage: Basic Example
```ts
import { QRCodeError, QRCODE_CODES } from "@suaveplan/qrcode/errors";
try {
throw new QRCodeError("Input is empty", QRCODE_CODES.INVALID_DATA);
} catch (err) {
if (err instanceof QRCodeError) {
console.error(err.code); // "QRCODE.INVALID_DATA"
}
}
```
## Usage: Advanced Example
```ts
import { QRCodeError, QRCODE_CODES } from "@suaveplan/qrcode/errors";
function assertVersionCapacity(version: number, byteCount: number): void {
// Version 1 byte-mode M capacity = 14 bytes
const cap = version === 1 ? 14 : byteCount + 1;
if (byteCount > cap) {
throw new QRCodeError(
`Version ${version} cannot hold ${byteCount} bytes`,
QRCODE_CODES.VERSION_TOO_SMALL
);
}
}
try {
assertVersionCapacity(1, 20);
} catch (err) {
if (err instanceof QRCodeError && err.code === QRCODE_CODES.VERSION_TOO_SMALL) {
console.warn("Increase version or reduce data size");
}
}
```
## API Reference
### `QRCODE_CODES`
`{ readonly INVALID_DATA: "QRCODE.INVALID_DATA"; readonly VERSION_TOO_SMALL: "QRCODE.VERSION_TOO_SMALL"; readonly ENCODE_FAILED: "QRCODE.ENCODE_FAILED" }`
Frozen object mapping error-code names to their canonical string values. Consumers should always reference codes via this object rather than inline strings to benefit from rename refactoring and exhaustive switch checking.
### `QRCodeCode`
`type QRCodeCode = (typeof QRCODE_CODES)[keyof typeof QRCODE_CODES]`
Union of all valid error code strings. Narrowed automatically when branching on `err.code`.
### `class QRCodeError extends Error`
**Constructor:** `new QRCodeError(message: string, code: QRCodeCode)`
- `message` — human-readable description of the failure
- `code` — one of the values in `QRCODE_CODES`
**Properties:**
- `name: "QRCodeError"` — set by the constructor for identification in logs and stack traces
- `code: QRCodeCode` — readonly, stable error code
- All standard `Error` properties: `message`, `stack`
## Implementation Notes
The class deliberately does not extend `SuaveplanError` from `@suaveplan/error`. This keeps the dependency graph lean: the qrcode package is a leaf in the media domain and should not pull in the full error substrate (Zod wire format, OTel span events, cause-chain traversal) unless a future version explicitly requires them. If the package graduates to depending on `@suaveplan/error`, the migration is straightforward since the `code` property convention is identical.
The `as const` assertion on `QRCODE_CODES` ensures TypeScript infers string literal types rather than `string`, enabling exhaustive union checks.

View File

@@ -0,0 +1,122 @@
# qrcode.types
## Purpose
Shared TypeScript type definitions for the `@suaveplan/qrcode` package. These types form the public API contract for QR code generation options and results, ensuring consistent interfaces across all modules in the package. All types are consumed by the top-level `generate()` function, renderers, and encoder modules.
## Features
- `ErrorCorrectionLevel` — union type for QR error correction levels L, M, Q, and H, each providing a different trade-off between data recovery capacity and payload size
- `QRMode` — union type for QR data encoding modes: numeric (digits only), alphanumeric (QR character set), byte (arbitrary UTF-8), and kanji (Shift-JIS)
- `OutputFormat` — union type for supported output representations: SVG string, raw boolean matrix, terminal Unicode blocks, and base64 data URL
- `QRCodeOptions` — configuration interface for `generate()`, covering EC level, explicit version, quiet zone size, module colors, and pixel scale
- `QRCodeResult` — result interface returned by `generate()`, containing all output representations plus metadata (version, EC level, mode, matrix size)
## Usage: Basic Example
```typescript
import type { QRCodeOptions, QRCodeResult } from "@suaveplan/qrcode";
const options: QRCodeOptions = {
errorCorrectionLevel: "M",
margin: 4,
color: "#000000",
background: "#ffffff",
scale: 10,
};
// Pass options to the generate() function
// const result: QRCodeResult = generate("Hello World", options);
```
## Usage: Advanced Example
```typescript
import type {
ErrorCorrectionLevel,
OutputFormat,
QRCodeOptions,
QRCodeResult,
QRMode,
} from "@suaveplan/qrcode";
// Function that accepts strongly-typed parameters
function encodeWithLevel(
data: string,
level: ErrorCorrectionLevel
): QRCodeOptions {
return {
errorCorrectionLevel: level,
version: undefined, // auto-select
margin: 4,
};
}
// Use the result type for structured access
function printResult(result: QRCodeResult): void {
console.log(`Version: ${result.version}`);
console.log(`Mode: ${result.mode satisfies QRMode}`);
console.log(`Size: ${result.size}×${result.size}`);
console.log(`EC Level: ${result.errorCorrectionLevel satisfies ErrorCorrectionLevel}`);
// result.svg, result.dataUrl, result.terminal, result.matrix all available
}
```
## API Reference
### `ErrorCorrectionLevel`
```typescript
type ErrorCorrectionLevel = "L" | "M" | "Q" | "H";
```
QR error correction capacity:
- `"L"` — ~7% data recovery, maximum payload capacity
- `"M"` — ~15% data recovery, default level
- `"Q"` — ~25% data recovery, balanced
- `"H"` — ~30% data recovery, maximum robustness
### `QRMode`
```typescript
type QRMode = "numeric" | "alphanumeric" | "byte" | "kanji";
```
Data encoding mode. The encoder auto-selects the most compact mode, but callers can inspect the selected mode in `QRCodeResult.mode`.
### `OutputFormat`
```typescript
type OutputFormat = "svg" | "matrix" | "terminal" | "dataurl";
```
Supported output representations. All four are always present in `QRCodeResult`.
### `QRCodeOptions`
| Property | Type | Default | Description |
|---|---|---|---|
| `errorCorrectionLevel` | `ErrorCorrectionLevel` | `"M"` | EC level |
| `version` | `number` | auto | QR version 1-10 |
| `margin` | `number` | `4` | Quiet zone modules |
| `color` | `string` | `"#000000"` | Dark module CSS color |
| `background` | `string` | `"#ffffff"` | Light module CSS color |
| `scale` | `number` | `10` | Pixels per module |
### `QRCodeResult`
| Property | Type | Description |
|---|---|---|
| `version` | `number` | QR version used (1-10) |
| `errorCorrectionLevel` | `ErrorCorrectionLevel` | EC level used |
| `mode` | `QRMode` | Encoding mode selected |
| `matrix` | `boolean[][]` | 2D module matrix; `true` = dark |
| `size` | `number` | Matrix dimension in modules |
| `svg` | `string` | SVG string representation |
| `dataUrl` | `string` | `data:image/svg+xml;base64,...` |
| `terminal` | `string` | Unicode half-block terminal art |
## Implementation Notes
These types are runtime-free (no compiled output), consumed only through `import type`. The `QRCodeOptions` properties are all optional to allow ergonomic partial configuration — each module supplies its own defaults. `QRCodeResult` always includes all output formats so callers do not need to request specific renderers; this avoids conditional rendering logic at the call site.

View File

@@ -0,0 +1,22 @@
# `bitpacker`
Bit-level binary packing, schemas, compression, and streaming codecs.
<!-- begin:auto-docs -->
## Packages in `bitpacker`
10 packages.
| Package | Description |
|---|---|
| [`@suaveplan/bitpacker`](./bitpacker/README.md) | Core bit-level binary packing library. Provides `BitWriter`, `BitReader`, `BitPacker`, and a pluggable strategy system for schema-driven serialization. |
| [`@suaveplan/bitpacker-compressed`](./bitpacker-compressed/README.md) | Compression pipeline integration for bitpacker. |
| [`@suaveplan/bitpacker-crypto`](./bitpacker-crypto/README.md) | Bit-packer-aware crypto primitives. The shared interfaces (`Authority`, `DomainTag`, `AuthorityVerifier`, `AeadOps`) live in `@suaveplan/types`; the canonical… |
| [`@suaveplan/bitpacker-delta`](./bitpacker-delta/README.md) | Delta encoding utilities for efficient repeated state transmission. Detects changes between successive values and encodes differences rather than full values. |
| [`@suaveplan/bitpacker-game`](./bitpacker-game/README.md) | Game state utilities: quantization, prediction, interpolation, and client-server reconciliation. |
| [`@suaveplan/bitpacker-network`](./bitpacker-network/README.md) | Network simulation utilities for testing bitpacker under realistic network conditions. |
| [`@suaveplan/bitpacker-schema`](./bitpacker-schema/README.md) | Schema registry and validation for bitpacker. |
| [`@suaveplan/bitpacker-strategies`](./bitpacker-strategies/README.md) | Built-in packing strategies for `@suaveplan/bitpacker`. Provides strategies for numbers, strings, booleans, objects, arrays, enums, optional fields, oneOf unio… |
| [`@suaveplan/bitpacker-streaming`](./bitpacker-streaming/README.md) | Streaming APIs for bitpacker including chunked packing/unpacking and true TransformStream support with framing. |
| [`@suaveplan/bitpacker-validation`](./bitpacker-validation/README.md) | Safety limits and validation reports for bitpacker schemas. |
<!-- end:auto-docs -->

View File

@@ -0,0 +1 @@
Bit-level binary packing, schemas, compression, and streaming codecs.

View File

@@ -0,0 +1,91 @@
# @suaveplan/bitpacker-compressed - Bitpacker compressed plugin
## Summary
Compression pipeline integration for bitpacker.
<!-- begin:auto-docs -->
## Documentation tree
- [deflate](./src/adapters/deflate/deflate.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [deflate](./src/adapters/deflate/deflate.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [gzip](./src/adapters/gzip/gzip.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [gzip](./src/adapters/gzip/gzip.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [none](./src/adapters/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [none](./src/adapters/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [api](./src/api/api.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [api](./src/api/api.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [compressed-bit-packer](./src/compressed-bit-packer/compressed-bit-packer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer](./src/compressed-bit-packer/compressed-bit-packer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer.schema](./src/compressed-bit-packer/compressed-bit-packer.schema.md) — Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validate…
- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [register-schemas](./src/register-schemas/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-compressed` package. Exposes `re…
- [register-schemas](./src/register-schemas/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-compressed` package. Exposes `re…
- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [rle](./src/rle/rle.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - rle.schema — Zod schema for the per-call `Compressi…
- [rle](./src/rle/rle.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - rle.schema — Zod schema for the per-call `Compressi…
- [rle.schema](./src/rle/rle.schema.md) — Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-onl…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - types.schema — Zod schemas for the public type comp…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - types.schema — Zod schemas for the public type comp…
- [types.schema](./src/types/types.schema.md) — Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record…
- [index](./src/index.md) — **Path:** `bitpacker-compressed/src/index.ts` **Export:** See source for details.
<!-- end:auto-docs --> - [deflate](./src/adapters/deflate/deflate.md) — <!-- begin:auto-docs --> _(no submodules)_- [gzip](./src/adapters/gzip/gzip.md) — <!-- begin:auto-docs --> _(no submodules)_- [gzip](./src/adapters/gzip/gzip.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/adapters/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/adapters/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [api](./src/api/api.md) — <!-- begin:auto-docs --> _(no submodules)_- [api](./src/api/api.md) — <!-- begin:auto-docs --> _(no submodules)_- [compressed-bit-packer](./src/compressed-bit-packer/compressed-bit-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer](./src/compressed-bit-packer/compressed-bit-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer.schema](./src/compressed-bit-packer/compressed-bit-packer.schema.md) — Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validate…
- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [rle](./src/rle/rle.md) — <!-- begin:auto-docs --> _(no submodules)_- rle.schema — Zod schema for the per-call `Compressi…
- [rle](./src/rle/rle.md) — <!-- begin:auto-docs --> _(no submodules)_- rle.schema — Zod schema for the per-call `Compressi…
- [rle.schema](./src/rle/rle.schema.md) — Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-onl…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schemas for the public type comp…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schemas for the public type comp…
- [types.schema](./src/types/types.schema.md) — Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record…
- [index](./src/index.md) — **Path:** `bitpacker-compressed/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-compressed` package. Exposes `re…
- [deflate](./src/adapters/deflate/deflate.md) — <!-- begin:auto-docs --> _(no submodules)_- [gzip](./src/adapters/gzip/gzip.md) — <!-- begin:auto-docs --> _(no submodules)_- [gzip](./src/adapters/gzip/gzip.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/adapters/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/adapters/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [api](./src/api/api.md) — <!-- begin:auto-docs --> _(no submodules)_- [api](./src/api/api.md) — <!-- begin:auto-docs --> _(no submodules)_- [compressed-bit-packer](./src/compressed-bit-packer/compressed-bit-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer](./src/compressed-bit-packer/compressed-bit-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer.schema](./src/compressed-bit-packer/compressed-bit-packer.schema.md) — Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validate…
- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [rle](./src/rle/rle.md) — <!-- begin:auto-docs --> _(no submodules)_- rle.schema — Zod schema for the per-call `Compressi…
- [rle](./src/rle/rle.md) — <!-- begin:auto-docs --> _(no submodules)_- rle.schema — Zod schema for the per-call `Compressi…
- [rle.schema](./src/rle/rle.schema.md) — Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-onl…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schemas for the public type comp…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schemas for the public type comp…
- [types.schema](./src/types/types.schema.md) — Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record…
- [index](./src/index.md) — **Path:** `bitpacker-compressed/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-compressed` package. Exposes `re…
- [deflate](./src/adapters/deflate/deflate.md) — <!-- begin:auto-docs --> _(no submodules)_- [gzip](./src/adapters/gzip/gzip.md) — <!-- begin:auto-docs --> _(no submodules)_- [gzip](./src/adapters/gzip/gzip.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/adapters/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/adapters/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [api](./src/api/api.md) — <!-- begin:auto-docs --> _(no submodules)_- [api](./src/api/api.md) — <!-- begin:auto-docs --> _(no submodules)_- [compressed-bit-packer](./src/compressed-bit-packer/compressed-bit-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer](./src/compressed-bit-packer/compressed-bit-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer.schema](./src/compressed-bit-packer/compressed-bit-packer.schema.md) — Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validate…
- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [rle](./src/rle/rle.md) — <!-- begin:auto-docs --> _(no submodules)_- rle.schema — Zod schema for the per-call `Compressi…
- [rle](./src/rle/rle.md) — <!-- begin:auto-docs --> _(no submodules)_- rle.schema — Zod schema for the per-call `Compressi…
- [rle.schema](./src/rle/rle.schema.md) — Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-onl…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schemas for the public type comp…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schemas for the public type comp…
- [types.schema](./src/types/types.schema.md) — Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record…
- [index](./src/index.md) — **Path:** `bitpacker-compressed/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-compressed` package. Exposes `re…
- [deflate](./src/adapters/deflate/deflate.md) — <!-- begin:auto-docs --> _(no submodules)_- [gzip](./src/adapters/gzip/gzip.md) — <!-- begin:auto-docs --> _(no submodules)_ - [gzip](./src/adapters/gzip/gzip.md) — <!-- begin:auto-docs --> _(no submodules)_- [none](./src/adapters/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_ - [none](./src/adapters/none/none.md) — <!-- begin:auto-docs --> _(no submodules)_- [api](./src/api/api.md) — <!-- begin:auto-docs --> _(no submodules)_ - [api](./src/api/api.md) — <!-- begin:auto-docs --> _(no submodules)_- [compressed-bit-packer](./src/compressed-bit-packer/compressed-bit-packer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [compressed-bit-packer](./src/compressed-bit-packer/compressed-bit-packer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [compressed-bit-packer.schema](./src/compressed-bit-packer/compressed-bit-packer.schema.md) — Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validate…
- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ - [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ - [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [rle](./src/rle/rle.md) — <!-- begin:auto-docs --> _(no submodules)_ - [rle](./src/rle/rle.md) — <!-- begin:auto-docs --> _(no submodules)_ - [rle.schema](./src/rle/rle.schema.md) — Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-onl…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types.schema](./src/types/types.schema.md) — Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record…
- [index](./src/index.md) — **Path:** `bitpacker-compressed/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-compressed` package. Exposes `re…
## Install
```bash
bun add @suaveplan/bitpacker-compressed
```
## Usage
```typescript
import { compress, decompress } from "@suaveplan/bitpacker-compressed";
const data = new Uint8Array([1, 2, 3, 4, 5]);
const compressed = compress(data);
const restored = decompress(compressed);
```
## Modules
- `src/index.ts` — `compress`, `decompress`

View File

@@ -0,0 +1,54 @@
# DeflateCompressionAdapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`DeflateCompressionAdapter` is the WHATWG Streams-backed deflate codec for `@suaveplan/bitpacker-compressed`. It mirrors `GzipCompressionAdapter` exactly but switches the underlying stream to `CompressionStream("deflate")` / `DecompressionStream("deflate")`. The chunk-concatenation pump is shared with the gzip adapter via `_runStreamForTests` so the two adapters do not duplicate the same loop. Use this when you need raw deflate (no gzip header) — typically for inter-op with consumers that expect that wire format (HTTP `Content-Encoding: deflate`, certain binary protocols, etc.).
## Features
- Drop-in `CompressionAdapter` implementation registered as `"deflate"` in the default registry.
- Re-uses the gzip adapter's streaming pump for chunk concatenation, eliminating duplicated logic.
- Same coded-error treatment as gzip: `COMPRESSION_FAILED` / `DECOMPRESSION_FAILED` with `cause`.
- Identical async signature to all other adapters so swapping codecs is a `registry.resolve(otherName)` change.
## Usage: Basic Example
```ts
import { DeflateCompressionAdapter } from "@suaveplan/bitpacker-compressed";
const deflate = new DeflateCompressionAdapter();
const compressed = await deflate.compress(payload);
const restored = await deflate.decompress(compressed);
```
## Usage: Advanced Example
```ts
import {
compressData,
decompressData,
} from "@suaveplan/bitpacker-compressed";
// HTTP body that arrived with Content-Encoding: deflate.
const body = await req.arrayBuffer();
const decoded = await decompressData(new Uint8Array(body), "deflate");
```
## API Reference
### `class DeflateCompressionAdapter implements CompressionAdapter`
- `readonly name: "deflate"`
- `async compress(data: Uint8Array, options?: { level?: number }): Promise<Uint8Array>`
- `async decompress(data: Uint8Array, options?: { level?: number }): Promise<Uint8Array>`
**Throws:** `BitpackerError(COMPRESSION_FAILED, { cause })` / `BitpackerError(DECOMPRESSION_FAILED, { cause })` on stream failure or environments without `globalThis.CompressionStream`.
## Implementation Notes
- "Deflate" here means raw RFC 1951 (no zlib header). If you need the zlib-wrapped variant, the platform `CompressionStream("deflate")` already produces it on most browsers — this is a known compatibility nuance and callers MUST be aware of which side they need.
- The shared `_runStreamForTests` helper is intentionally underscore-prefixed to mark it as adapter-internal; consumers should not import it directly.
- WHATWG `CompressionStream` ignores the `level` option for deflate the same way it does for gzip.

View File

@@ -0,0 +1,56 @@
# GzipCompressionAdapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`GzipCompressionAdapter` is the WHATWG Streams-backed gzip codec. It uses `globalThis.CompressionStream("gzip")` and `DecompressionStream("gzip")` — available in modern browsers, Bun, Deno, and Node 18+. The adapter exists so callers don't have to reach into Web Streams APIs directly: the pipeline framing, error coding, and chunk-concatenation loop are all handled here so `compressData(data, "gzip")` is a one-liner. If the host environment lacks `CompressionStream`, the registry's `defaultCompressionRegistry()` skips registering this adapter; constructing one directly and calling `compress` in such a host throws `BitpackerError(COMPRESSION_FAILED)` with a descriptive cause chain.
## Features
- Drop-in `CompressionAdapter` implementation registered as `"gzip"` in the default registry.
- Streaming pump under the hood — large payloads are chunked through `CompressionStream` rather than buffered whole.
- Coded errors (`COMPRESSION_FAILED` / `DECOMPRESSION_FAILED`) wrap any underlying stream exception with `cause`.
- Re-exported `_runStreamForTests` helper used by `DeflateCompressionAdapter` to share the chunk-concatenation loop.
## Usage: Basic Example
```ts
import { GzipCompressionAdapter } from "@suaveplan/bitpacker-compressed";
const gzip = new GzipCompressionAdapter();
const compressed = await gzip.compress(myBytes);
const restored = await gzip.decompress(compressed);
```
## Usage: Advanced Example
```ts
import { defaultCompressionRegistry } from "@suaveplan/bitpacker-compressed";
// Production code typically goes through the registry.
const registry = defaultCompressionRegistry();
const adapter = registry.resolve("gzip");
const out = await adapter.compress(payload, { level: 9 });
```
## API Reference
### `class GzipCompressionAdapter implements CompressionAdapter`
- `readonly name: "gzip"`
- `async compress(data: Uint8Array, options?: { level?: number }): Promise<Uint8Array>`
- `async decompress(data: Uint8Array, options?: { level?: number }): Promise<Uint8Array>`
**Throws:** `BitpackerError(COMPRESSION_FAILED, { cause })` / `BitpackerError(DECOMPRESSION_FAILED, { cause })` if the underlying `CompressionStream` rejects, including environments without `globalThis.CompressionStream`.
### `_runStreamForTests(stream, data): Promise<Uint8Array>` (internal)
Exposed for the deflate adapter to re-use the pump loop. Not part of the public API.
## Implementation Notes
- WHATWG `CompressionStream` does not honour the `level` option — gzip level is fixed at the platform default. The option is accepted for API symmetry with adapters that DO honour it (e.g. a future `BrotliAdapter`).
- The pump loop uses `ReadableStream.prototype.getReader()` and concatenates chunks into a single `Uint8Array` at the end. For very large payloads, callers should consider invoking `CompressionStream` directly to get backpressure-aware streaming output.
- Every async method awaits inside a try/catch and re-throws as a coded `BitpackerError`. There is no silent fallback — if gzip fails, the caller sees a fully-typed coded error.

View File

@@ -0,0 +1,55 @@
# NoCompressionAdapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`NoCompressionAdapter` is the pass-through codec used as the default when `CompressionConfig.algorithm` is `"none"` or omitted. It exists so that `compressData`, `decompressData`, and `CompressedBitPacker` can ALWAYS route through the compression pipeline without special-casing the "compression disabled" branch — the bytes flow through `compress` and `decompress` calls that resolve immediately with the input. This keeps the framing logic in `CompressedBitPacker` uniform: every payload has the same header shape regardless of whether real compression occurred.
## Features
- Pass-through `compress` / `decompress` — returns the input `Uint8Array` reference unchanged.
- Drop-in `CompressionAdapter` implementation registered as `"none"` in the default registry.
- Always available — does not depend on `CompressionStream` and never throws.
- Default fallback when no algorithm is specified.
## Usage: Basic Example
```ts
import { NoCompressionAdapter } from "@suaveplan/bitpacker-compressed";
const none = new NoCompressionAdapter();
const same = await none.compress(myBytes); // myBytes itself
```
## Usage: Advanced Example
```ts
import {
compressData,
defaultCompressionRegistry,
} from "@suaveplan/bitpacker-compressed";
// "none" routes through the pipeline so framing stays uniform with
// gzip / deflate. Useful in tests that want to assert framing is
// orthogonal to the codec.
const out = await compressData(payload, "none");
const adapter = defaultCompressionRegistry().resolve("none");
```
## API Reference
### `class NoCompressionAdapter implements CompressionAdapter`
- `readonly name: "none"`
- `async compress(data: Uint8Array): Promise<Uint8Array>` — returns `data` unchanged.
- `async decompress(data: Uint8Array): Promise<Uint8Array>` — returns `data` unchanged.
The `options` parameter is accepted for interface symmetry but ignored.
## Implementation Notes
- The returned reference is the SAME `Uint8Array` instance as the input — the adapter does not copy. Callers that need an independent buffer should `slice()` separately.
- This adapter is unconditionally registered in `defaultCompressionRegistry()`. The other two defaults (`"gzip"`, `"deflate"`) are gated on `CompressionStream` availability; "none" always works.
- Tests that need to prove "compression is opt-in" use this adapter to confirm the pipeline behaves identically when the caller chose to skip real compression.

View File

@@ -0,0 +1,62 @@
# compression API (`compressData` / `decompressData`)
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`compressData` and `decompressData` are the primary public surface of `@suaveplan/bitpacker-compressed`. They are algorithm-by-name helpers that route through a `CompressionRegistry` (defaulting to the process-wide singleton) so callers can switch between `"gzip"`, `"deflate"`, `"none"`, and any custom registered adapter without touching adapter classes directly. They are the layer that hot-path consumers like `CompressedBitPacker` use; it is also the layer that wraps codec failures in `BitpackerError` codes so application code never has to handle a raw `DOMException` or platform-specific stream error.
## Features
- Algorithm-by-name routing through a pluggable `CompressionRegistry`.
- Coded errors: unknown algorithm → `UNKNOWN_COMPRESSION_ALGORITHM`, codec failure → `COMPRESSION_FAILED` / `DECOMPRESSION_FAILED` with the underlying error chained.
- Optional registry parameter — pass a custom `CompressionRegistry` for isolation in tests.
- Symmetric: `compressData(data, "gzip")` round-trips through `decompressData(bytes, "gzip")`.
## Usage: Basic Example
```ts
import { compressData, decompressData } from "@suaveplan/bitpacker-compressed";
const data = new TextEncoder().encode("hello world".repeat(100));
const compressed = await compressData(data, "gzip");
const restored = await decompressData(compressed, "gzip");
```
## Usage: Advanced Example
```ts
import {
compressData,
createCompressionRegistry,
} from "@suaveplan/bitpacker-compressed";
// Test isolation: create a fresh registry that ONLY has the "none" adapter
// registered, then prove the unknown-algorithm path throws coded errors.
const registry = createCompressionRegistry({ withDefaults: false });
try {
await compressData(data, "gzip", { registry });
} catch (err) {
// BitpackerError with code BITPACKER.UNKNOWN_COMPRESSION_ALGORITHM
}
```
## API Reference
### `compressData(data, algorithm, options?): Promise<Uint8Array>`
- `data: Uint8Array` — bytes to compress.
- `algorithm: string` — registry key (e.g. `"gzip"`, `"deflate"`, `"none"`).
- `options?: { registry?, level? }` — `registry` overrides the default singleton; `level` is forwarded to the adapter.
- **Throws:** `BitpackerError(UNKNOWN_COMPRESSION_ALGORITHM)` or `BitpackerError(COMPRESSION_FAILED, { cause })`.
### `decompressData(data, algorithm, options?): Promise<Uint8Array>`
- Inverse of `compressData`. Throws `BitpackerError(DECOMPRESSION_FAILED, { cause })` on codec failure.
## Implementation Notes
- The default singleton is initialized lazily; it registers `"none"`, `"gzip"`, `"deflate"` only if the host supports `CompressionStream`.
- Adapters are async because `CompressionStream` is async; `NoCompressionAdapter` returns immediately.
- Hot-path callers should resolve the adapter once and call it directly to avoid the per-call registry lookup.

View File

@@ -0,0 +1,122 @@
# CompressedBitPacker
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer.schema](./compressed-bit-packer.schema.md) — Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validate…
- compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer.schema](./compressed-bit-packer.schema.md) — Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validate…
- compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer.schema](./compressed-bit-packer.schema.md) — Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validate…
- compressed-bit-packer.schema — Zod schema for `Comp…
- [compressed-bit-packer.schema](./compressed-bit-packer.schema.md) — Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validate…
- [compressed-bit-packer.schema](./compressed-bit-packer.schema.md) — Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validate…
## Purpose
`CompressedBitPacker<T>` extends `@suaveplan/bitpacker::BitPacker<T>` to add a configurable compression stage between bit-packing and the wire bytes. The compressed bytes are framed with a self-describing algorithm header so a receiver can decompress without knowing the sender's algorithm choice in advance — the byte stream itself names the codec.
This is the port of upstream `bitpacker/src/compressed/bitpacker.ts`. The port keeps the upstream wire shape (`[headerLen: u8][header: ASCII algorithm][compressed payload]`) so any consumer carrying upstream-encoded bytes round-trips unchanged. Hardening over upstream:
- Plain `Error` throws → `BitpackerError` with stable codes (`UNKNOWN_COMPRESSION_ALGORITHM`, `COMPRESSION_FAILED`, `DECOMPRESSION_FAILED`, `COMPRESSED_BAD_HEADER`).
- The synchronous `pack`/`unpack` overrides from upstream — which threw when compression was enabled — are dropped. Consumers call the explicit async `packWithCompression` / `unpackWithDecompression` methods. This separates the sync no-compression hot path from the async compressed path cleanly.
- Adapter resolution goes through the injected `CompressionRegistry`, not a private static singleton.
- No `EventEmitter`. suaveplan's `BitPacker` does not extend an emitter; observability is OTel-driven via `bitpacker-compressed.*` spans.
## Features
- `packWithCompression(data, schema?)` — bit-pack, compress, frame.
- `unpackWithDecompression(data, schema?)` — read frame, decompress, bit-unpack.
- Configurable algorithm + options + enabled flag via constructor `compression` config or runtime accessors.
- Self-describing wire format: receiver dispatches to the named algorithm without prior knowledge.
- Synchronous `pack`/`unpack` from `BitPacker` remain for the no-compression case (no header added — bytes are exactly what `BitPacker.pack` produces).
- Pluggable `CompressionRegistry` via constructor option (defaults to the process-wide singleton).
## Usage: Basic Example
```typescript
import { CompressedBitPacker } from "@suaveplan/bitpacker-compressed";
const schema = { x: 255, y: 255 } as const;
const packer = new CompressedBitPacker(schema);
// Compression disabled (default) — frames with "none" header
const bytes = await packer.packWithCompression({ x: 42, y: 99 });
const decoded = await packer.unpackWithDecompression(bytes);
// → { x: 42, y: 99 }
```
## Usage: Advanced Example
```typescript
import {
CompressedBitPacker,
createCompressionRegistry,
} from "@suaveplan/bitpacker-compressed";
// Custom registry isolated from process-wide singleton, useful for tests
// or multi-tenant servers where adapter sets must not leak.
const registry = createCompressionRegistry();
// Register a custom adapter (e.g. brotli) into this isolated registry.
registry.register(new MyBrotliAdapter());
const packer = new CompressedBitPacker(
{
position: { x: "f32", y: "f32", z: "f32" },
velocity: { x: "f32", y: "f32", z: "f32" },
},
{
compression: {
algorithm: "brotli",
enabled: true,
options: { quality: 11 },
},
compressionRegistry: registry,
}
);
const wireBytes = await packer.packWithCompression({
position: { x: 1, y: 2, z: 3 },
velocity: { x: 0.1, y: 0, z: 0 },
});
// wireBytes is [headerLen=6][b][r][o][t][l][i][brotli-compressed bit-packed payload]
```
## API Reference
### `new CompressedBitPacker<T>(schema, options?)`
`schema` — a `BitPackerSchema` describing the data shape.
`options.compression?` — `{ algorithm?, options?, enabled? }`. Default is `{ algorithm: "none", options: {}, enabled: false }`.
`options.compressionRegistry?` — optional `CompressionRegistry` override. Defaults to `defaultCompressionRegistry()`.
### `packWithCompression(data, schema?) → Promise<Uint8Array>`
Bit-packs `data`, runs the bytes through the configured compression adapter (or `NoCompressionAdapter` if `enabled` is false or `algorithm` is `"none"`), and frames the result with `[headerLen: u8][header: ASCII algorithm name][payload]`. Returns the framed wire bytes.
### `unpackWithDecompression(data?, schema?) → Promise<unknown>`
Reverse of `packWithCompression`. Reads the framing header, dispatches to the named adapter for decompression (or routes the body through unchanged if the header is `"none"`), then bit-unpacks the result. If `data` is `undefined` or empty, falls back to `super.unpack(empty)` for parity with upstream.
### Accessors
`compressionAlgorithm` / `compressionOptions` / `compressionEnabled` — getters and setters for the runtime configuration. Allow callers to switch algorithms or toggle compression mid-flight.
### Inherited from `BitPacker`
Synchronous `pack(data, schema?) → Uint8Array` and `unpack(bytes, schema?) → T` remain available. They produce/consume bytes that are NOT framed (no `[headerLen][header]` prefix) — use them when compression is genuinely off and you don't want the framing overhead.
## Implementation Notes
The wire format's `headerLen` byte limits algorithm names to 1–255 ASCII bytes. The four shipping defaults (`none`, `gzip`, `deflate`, plus future `brotli`/`zstd`) are all well under that. If a future custom algorithm name exceeds the limit, register the adapter under a shorter alias.
The synchronous `pack`/`unpack` from `BitPacker` are NOT overridden here. The upstream version overrode them but threw an error when compression was enabled — that mixed sync and async expectations confusingly. suaveplan splits cleanly: callers wanting compression call the async methods; callers OK with no compression use the inherited sync methods.
`CompressionRegistry` is injectable so tests can use isolated registries (`createCompressionRegistry()`) without polluting the process-wide singleton. The lazy registration of `gzip`/`deflate` on the singleton (when `CompressionStream` is available) means the singleton is environment-aware without forcing every consumer through the streams API path.
The `compressed-bit-packer.test.ts` test file uses `vitest.skipIf(!hasStreams)` to skip stream-backed tests in environments without `CompressionStream`. The frame/unframe helpers are tested directly via `_frameForTests` / `_unframeForTests` so the wire format is locked even in non-streaming environments.

View File

@@ -0,0 +1,65 @@
# Compressed BitPacker Options Schema
## Purpose
Zod schema for `CompressedBitPackerOptions`, the constructor options bundle accepted by `CompressedBitPacker`. Validates the shape at the package boundary so a typo in the algorithm name (`alogrithm`), a non-`CompressionRegistry` value passed as the registry (a plain object that "quacks like" one), or a stray unknown key all fail at parse time rather than producing a packer that silently runs without the consumer's intended compression configuration.
The schema composes `compressionConfigSchema` for the `compression` field and uses an `instanceof` check for `compressionRegistry`. Identity of the registry is preserved so the constructor receives the same instance the caller built.
## Features
- `compressedBitPackerOptionsSchema` — strict, two optional fields (`compression`, `compressionRegistry`)
- Composes `compressionConfigSchema` for nested validation
- `instanceof CompressionRegistry` check with identity preservation
- `as unknown as z.ZodType<T>` cast for the EOPT mismatch
## Usage: Basic Example
```ts
import {
CompressedBitPacker,
compressedBitPackerOptionsSchema,
} from "@suaveplan/bitpacker-compressed";
const opts = compressedBitPackerOptionsSchema.parse({
compression: { algorithm: "gzip", enabled: true },
});
const packer = new CompressedBitPacker({ x: 255 }, opts);
```
## Usage: Advanced Example
```ts
import {
CompressedBitPacker,
compressedBitPackerOptionsSchema,
createCompressionRegistry,
GzipCompressionAdapter,
} from "@suaveplan/bitpacker-compressed";
const registry = createCompressionRegistry();
registry.register(new GzipCompressionAdapter());
const opts = compressedBitPackerOptionsSchema.parse({
compression: {
algorithm: "gzip",
options: { level: 9 },
enabled: true,
},
compressionRegistry: registry,
});
const packer = new CompressedBitPacker({ x: 255 }, opts);
```
## API Reference
### `compressedBitPackerOptionsSchema: z.ZodType<CompressedBitPackerOptions>`
Strict schema. `compression` is an optional `CompressionConfig` (validated by `compressionConfigSchema`). `compressionRegistry` is an optional `CompressionRegistry` instance (validated by `instanceof` with identity preservation).
## Implementation Notes
- **Composition over duplication.** The nested `compression` field is validated by `compressionConfigSchema` from the same package. Composing rather than inlining the shape keeps the per-field error messages consistent across all entry points that accept a `CompressionConfig`.
- **`instanceof` over duck-typing.** A duck-typed registry would compile but would not pass the prototype-aware checks the registry's lookup methods perform internally. Refusing duck-types prevents an entire class of "registry doesn't find my adapter" bugs.
- **`.strict()` is non-negotiable.** Same rationale as elsewhere in this change: typo'd flags would otherwise silently default and hide configuration bugs.

View File

@@ -0,0 +1,8 @@
## index
**Path:** `bitpacker-compressed/src/index.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,83 @@
# Bitpacker Compressed OTel Instruments
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
OTel meter, tracer, counter, and histogram declarations for `@suaveplan/bitpacker-compressed`. Ships canonical instrumentation primitives for the compression/decompression pipeline (`gzip`, `deflate`, `none`, `rle`, future custom adapters) plus the `CompressedBitPacker.pack` / `unpack` boundaries that wrap the compression step. Every operation can be observed with the same vocabulary regardless of which adapter is selected.
The metric and span names follow the suaveplan convention with snake-case for the multi-word package: tracer/meter named `@suaveplan/bitpacker-compressed`; spans named `bitpacker_compressed.<operation>`; counters/histograms named `bitpacker_compressed.<scope>.<metric>`.
## Features
- `getBitPackerCompressedTracer()` — returns the package's tracer
- `compressionOpsTotal` — counter labelled by `op`, `algorithm`, `success`
- `compressionOpDurationMs` — histogram of compress/decompress duration
- `compressionRatio` — histogram of `output_bytes / input_bytes` per successful compress
- `BITPACKER_COMPRESSED_SPAN_NAMES` — four canonical span names
## Usage: Basic Example
```ts
import {
BITPACKER_COMPRESSED_SPAN_NAMES,
compressionOpsTotal,
getBitPackerCompressedTracer,
} from "@suaveplan/bitpacker-compressed";
const tracer = getBitPackerCompressedTracer();
tracer.startActiveSpan(
BITPACKER_COMPRESSED_SPAN_NAMES.compress,
(span) => {
try {
// compress work
compressionOpsTotal.add(1, {
op: "compress",
algorithm: "gzip",
success: "true",
});
} finally {
span.end();
}
}
);
```
## Usage: Advanced Example
```ts
import { compressionRatio } from "@suaveplan/bitpacker-compressed";
function recordCompressionRatio(input: Uint8Array, output: Uint8Array, algorithm: string) {
compressionRatio.record(output.byteLength / input.byteLength, { algorithm });
}
```
## API Reference
### `getBitPackerCompressedTracer(): Tracer`
Returns the OTel `Tracer` instance for the package.
### `compressionOpsTotal: Counter`
Counter labelled by `op: "compress" | "decompress"`, `algorithm: string`, `success: "true" | "false"`.
### `compressionOpDurationMs: Histogram`
Histogram of elapsed milliseconds per compress/decompress call.
### `compressionRatio: Histogram`
Histogram of compression ratio (`output_bytes / input_bytes`), labelled by `algorithm`. Recorded only on successful `compress` calls — decompress doesn't have a meaningful ratio.
### `BITPACKER_COMPRESSED_SPAN_NAMES`
Const map: `{ compress, decompress, pack, unpack }` → canonical span names.
## Implementation Notes
- **Algorithm is a label, not a counter prefix.** `compressionOpsTotal{algorithm="gzip"}` is more flexible than separate `gzip_compress_total` / `deflate_compress_total` counters; new adapters fold in without schema changes.
- **`compressionRatio` is not a counter.** The ratio is per-call and stored as a histogram so dashboards can plot p50/p95/p99 ratios over time. A counter would only show throughput, not effectiveness.
- **No per-chunk spans.** Compression adapters often process bytes in internal chunks; one span each would explode trace volume on large inputs.

View File

@@ -0,0 +1,61 @@
# Register Schemas — `@suaveplan/bitpacker-compressed`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-compressed` package. Exposes `registerBitPackerCompressedSchemas(registry)` which converts the package's five public boundary schemas into JSON Schema form and registers them under `bitpacker:@suaveplan/bitpacker-compressed` at version 1. Re-registration on the same registry is a no-op.
The five schemas covered are: `bitPackerCompressionOptions` (the open-record adapter options), `compressionAdapter` (the structural contract every adapter implements), `compressionConfig` (the `{ algorithm, options, enabled }` config), `compressedBitPackerOptions` (the `CompressedBitPacker` constructor bundle), and `rleCompressionOptions` (the RLE-only `level` knob).
## Features
- `BITPACKER_COMPRESSED_SUBJECT` constant — `"bitpacker:@suaveplan/bitpacker-compressed"`
- `BITPACKER_COMPRESSED_SCHEMA_VERSION` constant — `1`
- `registerBitPackerCompressedSchemas(registry)` — idempotent registration
## Usage: Basic Example
```ts
import { createRegistry } from "@suaveplan/schema-registry";
import {
BITPACKER_COMPRESSED_SUBJECT,
registerBitPackerCompressedSchemas,
} from "@suaveplan/bitpacker-compressed/register-schemas";
const registry = createRegistry();
registerBitPackerCompressedSchemas(registry);
```
## Usage: Advanced Example
```ts
import { createRegistry, SchemaRegistry } from "@suaveplan/schema-registry";
import { registerBitPackerSchemas } from "@suaveplan/bitpacker/register-schemas";
import { registerBitPackerCompressedSchemas } from "@suaveplan/bitpacker-compressed/register-schemas";
const registry = createRegistry();
registerBitPackerSchemas(registry);
registerBitPackerCompressedSchemas(registry);
```
## API Reference
### `BITPACKER_COMPRESSED_SUBJECT: "bitpacker:@suaveplan/bitpacker-compressed"`
Const string.
### `BITPACKER_COMPRESSED_SCHEMA_VERSION: 1`
Const number.
### `registerBitPackerCompressedSchemas(registry: SchemaRegistry): void`
Idempotent registration. Returns without writing if the subject is already present.
## Implementation Notes
- **Five-key payload.** One key per public boundary schema.
- **`unrepresentable: "any"`.** Required because `compressionAdapter` and `compressedBitPackerOptions` use `z.custom` for the structural contract and registry instance check.

View File

@@ -0,0 +1,68 @@
# CompressionRegistry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`CompressionRegistry` is the pluggable map from algorithm name to `CompressionAdapter` that backs `@suaveplan/bitpacker-compressed`'s public API. It is the single source of truth that `compressData`, `decompressData`, and `CompressedBitPacker` consult to look up an adapter by string name. Two flavours are exposed: a process-wide singleton (`defaultCompressionRegistry()`) for production code, and a fresh-instance factory (`createCompressionRegistry()`) for tests that need isolation. This mirrors the registry conventions used by `@suaveplan/random` and `@suaveplan/bitpacker-crypto` so the codebase stays uniform.
## Features
- Singleton + fresh-instance factory pattern (no class-static singletons).
- Coded errors: unknown algorithm → `BitpackerError(UNKNOWN_COMPRESSION_ALGORITHM)`; soft-fail variant `tryResolve` returns `undefined`.
- Lazy default registration — `defaultCompressionRegistry()` registers `"none"`, `"gzip"`, `"deflate"` on first access (only registers gzip/deflate when `CompressionStream` is available).
- `withDefaults: false` option on `createCompressionRegistry` produces a completely empty registry for negative-path tests.
## Usage: Basic Example
```ts
import { defaultCompressionRegistry } from "@suaveplan/bitpacker-compressed";
const registry = defaultCompressionRegistry();
const adapter = registry.resolve("gzip");
const compressed = await adapter.compress(myBytes);
```
## Usage: Advanced Example
```ts
import {
createCompressionRegistry,
type CompressionAdapter,
} from "@suaveplan/bitpacker-compressed";
// Plug in a custom Brotli adapter (caller-owned WASM Brotli wrapper).
class BrotliAdapter implements CompressionAdapter {
readonly name = "brotli";
async compress(data: Uint8Array) { /* WASM call */ return new Uint8Array(); }
async decompress(data: Uint8Array) { /* WASM call */ return new Uint8Array(); }
}
const registry = createCompressionRegistry();
registry.register(new BrotliAdapter());
const adapter = registry.resolve("brotli");
```
## API Reference
### `defaultCompressionRegistry(): CompressionRegistry`
Returns the lazily-initialized process-wide singleton. Same reference every call.
### `createCompressionRegistry(options?: { withDefaults?: boolean }): CompressionRegistry`
Returns a fresh registry. `withDefaults` defaults to `true`; pass `false` to start empty.
### `CompressionRegistry`
- `register(adapter: CompressionAdapter): void` — Last-write-wins by `adapter.name`.
- `resolve(name: string): CompressionAdapter` — Throws on unknown.
- `tryResolve(name: string): CompressionAdapter | undefined` — Soft-fail variant.
- `list(): readonly string[]` — All registered adapter names.
## Implementation Notes
- The singleton is module-scope inside `registry.ts`; tests that need to mutate it create a fresh `createCompressionRegistry()` rather than poking the singleton.
- Default registration of gzip/deflate is gated on `typeof CompressionStream === "function"` so the registry stays usable in environments without WHATWG Streams (the gzip/deflate adapters themselves throw when called there, but lookup never fails).
- `register` mutates the registry; this is the documented behaviour for plug-in adapters. There is no immutable variant.

View File

@@ -0,0 +1,78 @@
# Run-length encoding (`compressRle` / `decompressRle`)
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - rle.schema — Zod schema for the per-call `Compressi…
- [rle.schema](./rle.schema.md) — Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-onl…
- rle.schema — Zod schema for the per-call `Compressi…
- [rle.schema](./rle.schema.md) — Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-onl…
- rle.schema — Zod schema for the per-call `Compressi…
- [rle.schema](./rle.schema.md) — Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-onl…
- rle.schema — Zod schema for the per-call `Compressi…
- [rle.schema](./rle.schema.md) — Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-onl…
- [rle.schema](./rle.schema.md) — Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-onl…
## Purpose
`compressRle` / `decompressRle` are byte-level run-length encoding helpers shipped as a separate module within `@suaveplan/bitpacker-compressed` so embedded / no-streams environments can import them without pulling in `CompressionStream`-using gzip/deflate adapters or the `CompressedBitPacker` class. They are not part of the `CompressionRegistry` framing — bytes produced by `compressRle` are NOT the same shape as bytes produced by the registry's `"none"`/`"gzip"`/`"deflate"` adapters, which include the `CompressedBitPacker` header. If you need framed RLE, write a thin `RleCompressionAdapter` that internally calls these helpers.
## Features
- Bytes-only fast path; no streams required.
- Single-byte runs longer than 3 are emitted as `[byte][0][count]`; everything else passes through verbatim.
- Symmetric round-trip with `decompressRle`.
- Zero allocation overhead beyond the output `Uint8Array`.
## Usage: Basic Example
```ts
import { compressRle, decompressRle } from "@suaveplan/bitpacker-compressed";
const data = new Uint8Array([1, 1, 1, 1, 1, 2, 3]);
const compressed = compressRle(data); // [1, 0, 5, 2, 3]
const restored = decompressRle(compressed); // back to original
```
## Usage: Advanced Example
```ts
import {
compressRle,
decompressRle,
type CompressionAdapter,
} from "@suaveplan/bitpacker-compressed";
// Wrap RLE in a CompressionAdapter shape so it plugs into the registry.
class RleAdapter implements CompressionAdapter {
readonly name = "rle";
async compress(data: Uint8Array) { return compressRle(data); }
async decompress(data: Uint8Array) { return decompressRle(data); }
}
```
## API Reference
### `compressRle(data: Uint8Array): Uint8Array`
- Encodes runs of >3 identical bytes as `[byte][0][count]`.
- Returns a fresh `Uint8Array`; input is not mutated.
### `decompressRle(data: Uint8Array): Uint8Array`
- Reverses `compressRle`. Looks ahead for `[0][count]` markers after each byte.
- Returns a fresh `Uint8Array`.
### `CompressionOptions`
Type alias for `{ readonly level?: number }` — accepted but ignored by the bytes-only RLE path; included so adapter authors can re-export the canonical compression-options shape.
## Implementation Notes
- The marker byte is `0`, which would collide with a literal `0` in input data; the encoder handles this by always emitting the run-length form when `byte === 0` runs.
- This is not the most aggressive RLE possible — it does not pack runs of distinct bytes into literal blocks. The intent is "smaller than verbatim with a single linear pass", not "competitive with gzip on text".
- Use this when you have known byte-pattern data (e.g. graphics with large flat regions, sparse network frames). For general-purpose data, use the gzip / deflate adapters.

View File

@@ -0,0 +1,48 @@
# RLE Compression Options Schema
## Purpose
Zod schema for the per-call `CompressionOptions` accepted by `compressRle` / `decompressRle`. The RLE codec is byte-only and exposes a single tuning knob (`level`) — this schema validates that knob at the boundary so untrusted input can be normalized before reaching the codec.
## Features
- `rleCompressionOptionsSchema` — strict, single optional field
- `level` is a non-negative integer; the codec ignores values outside its supported range
- `as unknown as z.ZodType<T>` cast for the EOPT mismatch
## Usage: Basic Example
```ts
import { compressRle, rleCompressionOptionsSchema } from "@suaveplan/bitpacker-compressed";
const opts = rleCompressionOptionsSchema.parse({ level: 1 });
const compressed = compressRle(new Uint8Array([0, 0, 0, 0, 1, 2]), opts);
```
## Usage: Advanced Example
```ts
import {
compressRle,
decompressRle,
rleCompressionOptionsSchema,
} from "@suaveplan/bitpacker-compressed";
function safeRoundTrip(data: Uint8Array, rawOpts: unknown) {
const opts = rleCompressionOptionsSchema.parse(rawOpts);
const compressed = compressRle(data, opts);
const decompressed = decompressRle(compressed);
return decompressed;
}
```
## API Reference
### `rleCompressionOptionsSchema: z.ZodType<CompressionOptions>`
Strict schema. `level` is an optional non-negative integer. Throws `ZodError` on negative or non-integer values, or on unknown keys.
## Implementation Notes
- **Why a one-field schema.** Even a single-field bundle benefits from boundary validation: a `{ level: "9" }` (string instead of number) would otherwise reach the codec and produce undefined behaviour. Refusing it at parse time keeps the codec's contract simple.
- **Codec ignores out-of-range levels.** The schema enforces non-negative integer; the codec itself ignores values outside `[0, 9]`. The schema is the boundary; the codec is the implementation.

View File

@@ -0,0 +1,92 @@
# Compression types
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - types.schema — Zod schemas for the public type comp…
- [types.schema](./types.schema.md) — Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record…
- types.schema — Zod schemas for the public type comp…
- [types.schema](./types.schema.md) — Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record…
- types.schema — Zod schemas for the public type comp…
- [types.schema](./types.schema.md) — Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record…
- types.schema — Zod schemas for the public type comp…
- [types.schema](./types.schema.md) — Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record…
- [types.schema](./types.schema.md) — Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record…
## Purpose
This module collects the public TypeScript type companions used across `@suaveplan/bitpacker-compressed`: the algorithm-name string brand, the per-call options shape, and the `CompressionAdapter` interface that any plug-in codec must implement. Keeping them in one file means downstream consumers only need a single import path to write a custom adapter or to type a config object that flows through the pipeline. The shapes mirror the upstream `bitpacker/src/types.ts` compression types verbatim so adapters and consumers see the same contracts whether they imported from upstream or the port.
## Features
- `BitPackerCompressionAlgorithm` — string-typed registry key (e.g. `"none"`, `"gzip"`, `"deflate"`, `"brotli"`).
- `BitPackerCompressionOptions` — caller-supplied options forwarded to the adapter (currently `level?: number`).
- `CompressionAdapter` — the contract every codec implements: `name`, `compress`, `decompress`.
- All async to match the WHATWG Streams reality; sync codecs simply resolve immediately.
## Usage: Basic Example
```ts
import type {
BitPackerCompressionAlgorithm,
BitPackerCompressionOptions,
CompressionAdapter,
} from "@suaveplan/bitpacker-compressed";
const algorithm: BitPackerCompressionAlgorithm = "gzip";
const options: BitPackerCompressionOptions = { level: 9 };
```
## Usage: Advanced Example
```ts
import type { CompressionAdapter } from "@suaveplan/bitpacker-compressed";
// Custom adapter for a hypothetical FastLZ codec.
class FastLzAdapter implements CompressionAdapter {
readonly name = "fastlz";
async compress(data: Uint8Array, options?: { level?: number }) {
// Call into FastLZ-WASM module here.
return data; // placeholder
}
async decompress(data: Uint8Array, _options?: { level?: number }) {
return data; // placeholder
}
}
```
## API Reference
### `BitPackerCompressionAlgorithm`
Type alias for `string`. Used as the registry lookup key. Default registered names: `"none"`, `"gzip"`, `"deflate"`. Consumers MAY register additional algorithms.
### `BitPackerCompressionOptions`
```ts
interface BitPackerCompressionOptions {
readonly level?: number;
}
```
Forwarded to the adapter. Adapters that don't support `level` (e.g. `none`) ignore it.
### `CompressionAdapter`
```ts
interface CompressionAdapter {
readonly name: BitPackerCompressionAlgorithm;
compress(data: Uint8Array, options?: BitPackerCompressionOptions): Promise<Uint8Array>;
decompress(data: Uint8Array, options?: BitPackerCompressionOptions): Promise<Uint8Array>;
}
```
## Implementation Notes
- `BitPackerCompressionAlgorithm` is intentionally `string`, not a closed union — module augmentation is overkill for plug-in codecs and the registry already validates names at lookup.
- All methods are async because the standard browser/Node codecs (`CompressionStream`) are async. The pass-through `NoCompressionAdapter` resolves immediately so async overhead is negligible.
- Adapters MUST be deterministic: same input + same options ⇒ same output bytes (modulo internal nondeterminism in the underlying codec, which is allowed but unusual).

View File

@@ -0,0 +1,77 @@
# Compression Type Schemas
## Purpose
Zod schemas for the public type companions of `@suaveplan/bitpacker-compressed`'s compression pipeline: the open-record `BitPackerCompressionOptions`, the `CompressionAdapter` interface (the contract every adapter implements), and the `CompressionConfig` configuration shape consumed by `CompressedBitPacker`. Each schema is the runtime gatekeeper at the corresponding boundary so an adapter loaded from a plugin manifest, or a config bundle parsed from environment variables, can be validated before reaching the compression hot path.
The schemas use `z.custom<T>((v): v is T => …)` for the adapter validation so adapter prototype chains are preserved (registries depend on prototype-aware `instanceof` checks), and `z.record(z.string(), z.unknown())` for the open-record `BitPackerCompressionOptions` because each adapter accepts its own option keys — the schema validates "is this an object?" and lets the adapter validate its own option semantics.
## Features
- `bitPackerCompressionOptionsSchema` — open record (any string-keyed object)
- `compressionAdapterSchema` — structural contract: `name: string`, `compress: function`, `decompress: function`
- `compressionConfigSchema` — strict, three optional fields (`algorithm`, `options`, `enabled`)
- Identity-preserving `z.custom` for adapter validation
- `as unknown as z.ZodType<T>` casts where Zod's `T | undefined` collides with `exactOptionalPropertyTypes`
## Usage: Basic Example
```ts
import {
CompressedBitPacker,
compressionAdapterSchema,
compressionConfigSchema,
NoCompressionAdapter,
} from "@suaveplan/bitpacker-compressed";
const adapter = compressionAdapterSchema.parse(new NoCompressionAdapter());
const cfg = compressionConfigSchema.parse({ algorithm: "none", enabled: true });
```
## Usage: Advanced Example
```ts
import {
CompressedBitPacker,
bitPackerCompressionOptionsSchema,
compressionAdapterSchema,
compressionConfigSchema,
} from "@suaveplan/bitpacker-compressed";
async function loadVendorAdapter(url: URL) {
const mod = await import(url.href);
const adapter = compressionAdapterSchema.parse(mod.default);
return adapter;
}
const opts = bitPackerCompressionOptionsSchema.parse({
level: 9,
windowBits: 15,
});
const cfg = compressionConfigSchema.parse({
algorithm: "deflate",
options: opts,
enabled: true,
});
```
## API Reference
### `bitPackerCompressionOptionsSchema: z.ZodType<BitPackerCompressionOptions>`
Open-record schema. Accepts any object with string keys. Adapter-specific validation is the adapter's responsibility.
### `compressionAdapterSchema: z.ZodType<CompressionAdapter>`
Structural contract validator. Asserts `name: string`, `compress: function`, `decompress: function`. Identity is preserved.
### `compressionConfigSchema: z.ZodType<CompressionConfig>`
Strict schema. Three optional fields: `algorithm: string`, `options: BitPackerCompressionOptions`, `enabled: boolean`.
## Implementation Notes
- **Open-record options.** Adapters accept arbitrary keys (`level`, `windowBits`, `dictionary`, …). Validating them at this layer would force this package to know every adapter's surface, which it can't. The contract is "is this an object?"; semantic validation belongs to the adapter.
- **Structural adapter check.** A duck-typed object that exposes `name/compress/decompress` works at runtime. Refusing duck-types would break the plugin model where adapters are loaded from external modules without inheriting from a base class.
- **Identity preservation.** `z.custom` for the adapter; the registry's `register(adapter)` method uses `===` and `instanceof` checks downstream.

View File

@@ -0,0 +1,155 @@
# @suaveplan/bitpacker-crypto - Bit-packer-aware crypto primitives — SignedBitSchema, EncryptedBitSchema, EnvelopeSchema, HashChainSchema, MultiSignedBitSchema, SchemaFingerprint, HashAlgorithmRegistry. Authority/AeadOps/DomainTag types live in @suaveplan/types; the canonical implementations are in @suaveplan/crypto and dependency-injected by the caller.
## Summary
Bit-packer-aware crypto primitives. The shared interfaces (`Authority`,
`DomainTag`, `AuthorityVerifier`, `AeadOps`) live in `@suaveplan/types`;
the canonical implementations come from `@suaveplan/crypto` and are
**dependency-injected** by the caller. This package has zero runtime
dependency on `@suaveplan/crypto`, so consumers in any layer (including
`@suaveplan/crypto` itself, e.g. `crypto::ChainBundle`) can adopt these
primitives without inducing a circular package dependency.
<!-- begin:auto-docs -->
## Documentation tree
- [encrypted](./src/encrypted/encrypted-bitschema.md) — Niche AEAD-encrypted envelope around a bit-packed body. Wire format: `[version u8][algTag u8][nonceLen u8][nonce][ciphe…
- [encrypted-bitschema](./src/encrypted/encrypted-bitschema.md) — Niche AEAD-encrypted envelope around a bit-packed body. Wire format: `[version u8][algTag u8][nonceLen u8][nonce][ciphe…
- [envelope](./src/envelope/envelope-schema.md) — A typed, version-aware envelope schema for bit-packed payloads. Wraps a caller-supplied `BodyCodec` with a `[magic][for…
- [envelope-schema](./src/envelope/envelope-schema.md) — A typed, version-aware envelope schema for bit-packed payloads. Wraps a caller-supplied `BodyCodec` with a `[magic][for…
- [fingerprint](./src/fingerprint/schema-fingerprint.md) — Content-hash a `BitPackerSchema` so two parties can agree they're using byte-identical schemas before they exchange pay…
- [schema-fingerprint](./src/fingerprint/schema-fingerprint.md) — Content-hash a `BitPackerSchema` so two parties can agree they're using byte-identical schemas before they exchange pay…
- [hash-registry](./src/hash-registry/hash-registry.md) — A pluggable registry of named hash algorithms used by every hashing-aware primitive in `@suaveplan/bitpacker-crypto` (n…
- [hash-registry](./src/hash-registry/hash-registry.md) — A pluggable registry of named hash algorithms used by every hashing-aware primitive in `@suaveplan/bitpacker-crypto` (n…
- [hashchain](./src/hashchain/hashchain-schema.md) — Append-only signed hash chain. Each entry carries a sequence number, the hash of the previous entry, an opaque payload,…
- [hashchain-schema](./src/hashchain/hashchain-schema.md) — Append-only signed hash chain. Each entry carries a sequence number, the hash of the previous entry, an opaque payload,…
- [multisigned](./src/multisigned/multisigned-bitschema.md) — Bit-packed payload co-signed by N independent authorities. Wire format is `[bit-packed body][sig_1][sig_2]…[sig_N]`. Pi…
- [multisigned-bitschema](./src/multisigned/multisigned-bitschema.md) — Bit-packed payload co-signed by N independent authorities. Wire format is `[bit-packed body][sig_1][sig_2]…[sig_N]`. Pi…
- [signed](./src/signed/signed-bitschema.md) — Bit-packed payload signed by an `Authority` under a fixed `DomainTag`. Wire format is `[bit-packed body][signature]`. V…
- [signed-bitschema](./src/signed/signed-bitschema.md) — Bit-packed payload signed by an `Authority` under a fixed `DomainTag`. Wire format is `[bit-packed body][signature]`. V…
- [crc32](./src/crc32.md) — Fast non-cryptographic checksums for bit-packed payloads. `crc32` implements the IEEE-802.3 / zlib polynomial (`0xedb88…
- [index](./src/index.md) — **Path:** `bitpacker-crypto/src/index.ts` **Export:** See source for details.
- [xxhash32](./src/xxhash32.md) — **Path:** `src/xxhash32.ts` **Export:** `calculateXxhash32`, `verifyXxhash32`, `packXxhash32`, `unpackXxhash32`, `appen…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/bitpacker-crypto
```
## Public surface
| Primitive | Use it for |
|---|---|
| `EnvelopeSchema` | `[magic][formatVersion][body]` framing — version-aware wire formats |
| `SignedBitSchema` | Authority-signed payloads under a `DomainTag` |
| `MultiSignedBitSchema` | N-of-N co-signed payloads with `firstBadSignerIndex` reporting |
| `HashChainSchema` | Append-only signed hash chain (audit logs, mesh state) |
| `EncryptedBitSchema` | **Niche** AEAD-only envelope for **small** encrypted payloads |
| `SchemaFingerprint` | Canonical-hash a `BitPackerSchema` for handshakes |
| `HashAlgorithmRegistry` | Pluggable hash registry — sha256/xxhash32/crc32 default; register blake3 |
| `crc32` / `xxhash32` / `adler32` | Existing checksum surface, unchanged |
## Intended scope
`EncryptedBitSchema` is for **small** encrypted payloads only — token
bodies, key-wrap envelopes, sub-MTU control records. **Do NOT use it as a
chunked-file encoder.** For chunked-file encryption with a streaming
verifier and locked v0x0001 codec, reach for `@suaveplan/packer` instead.
## Crypto provider needs?
This package ships **bit-packer-aware** crypto primitives. For raw AEAD,
signing, key derivation, or authenticated encryption (without a bit-packer
schema), reach for the crypto family directly:
- [`@suaveplan/crypto`](../../crypto/crypto) — multi-algorithm façade (AEAD, sign,
agree, sealed-box, auth-box).
- [`@suaveplan/crypto-sodium`](../../crypto/crypto-sodium) — libsodium engine.
- [`@suaveplan/crypto-browser`](../../crypto/crypto-browser) — WebCrypto engine.
- [`@suaveplan/crypto-vault`](../../crypto/crypto-vault) — BIP39 + Argon2id HD vault.
- [`@suaveplan/packer`](../../data/packer) — chunked AEAD with manifest.
Upstream's `bitpacker/src/crypto/` provider abstraction is **not ported**;
see [SUPERSEDED.md](./SUPERSEDED.md) for the per-symbol replacement table.
## Quick examples
### Envelope a bit-packed body
```ts
import { defineEnvelopeSchema } from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
const headerSchema = defineBitSchema<{ foo: number }>([
{ name: "foo", type: "uint", bits: 32 },
]);
const env = defineEnvelopeSchema({
magic: new Uint8Array([0x53, 0x56, 0x43, 0x42]),
formatVersion: 1,
bodyCodec: {
pack: (b) => headerSchema.pack(b),
unpack: (bytes) => headerSchema.unpack(bytes),
},
});
const bytes = env.pack({ foo: 7 });
```
### Wiring the canonical crypto bindings
Every primitive that needs a verifier or AEAD bundle accepts it via
options. Pass the canonical wiring once in your application bootstrap:
```ts
import { aeadEncrypt, aeadDecrypt } from "@suaveplan/crypto";
import { verifyAuthoritySignature } from "@suaveplan/crypto/authority";
import type { AeadOps, AuthorityVerifier } from "@suaveplan/types";
export const verifier: AuthorityVerifier = verifyAuthoritySignature;
export const aead: AeadOps = { encrypt: aeadEncrypt, decrypt: aeadDecrypt };
// Then everywhere you build a primitive:
defineSignedBitSchema(codec, { domain, verifier });
defineMultiSignedBitSchema(codec, { domain, verifier, signerCount });
defineHashChainSchema({ domain, verifier, payloadCodec });
defineEncryptedBitSchema(codec, { key, aead });
```
### Sign + encrypt a token
```ts
import {
defineEncryptedBitSchema,
defineSignedBitSchema,
} from "@suaveplan/bitpacker-crypto";
import { DOMAIN_BITPACKER_SIGNED_V1 } from "@suaveplan/crypto/domain-tags";
const inner = defineEncryptedBitSchema(tokenCodec, { key: aeadKey });
const innerCodec = {
pack: (t) => inner.pack(t),
unpack: (bytes) => inner.unpack(bytes),
};
// (Signing receives Promise.unwrap() on the body — for the canary chain
// pattern, prefer `defineSignedBitSchema(envCodec, ...)` directly.)
```
### Schema-fingerprint handshake
```ts
import { schemaFingerprint, assertSchemaFingerprint } from "@suaveplan/bitpacker-crypto";
const local = { foo: 255, bar: 65535 };
const fp = await schemaFingerprint(local);
// On the peer:
await assertSchemaFingerprint(remoteSchema, fp);
```
## Algorithms (existing checksum surface, unchanged)
| Algorithm | Speed | Use Case |
|-----------|-------|----------|
| CRC32 | Medium | General integrity checking |
| Adler32 | Fast | Quick checksums (used in zlib) |
| XXHash32 | Very fast | High-throughput hashing |

View File

@@ -0,0 +1,150 @@
# SUPERSEDED — bitpacker-crypto provider abstraction
This file records two related decisions:
1. **Original decision (`bitpacker-crypto-port`, 2026-05-02)** — upstream's
`packages/universal/bitpacker/src/crypto/` ships ~1,906 lines of provider
abstraction (`BaseCryptoProvider`, `CryptoRegistry`, and three adapters:
`noop`, `libsodium`, `elliptic`). suaveplan intentionally **does not port that abstraction**.
2. **Replacement (`bitpacker-crypto-rewrite`, 2026-05-02)** — a deeper consumer
scan found 8+ call-sites across suaveplan and upstream manually re-implementing
the same crypto-with-bitpacker patterns (sign-then-pack, encrypt-pack-frame,
versioned envelopes, schema fingerprints, stateless verifiers). Rather than
leaving the package as checksums-only, the rewrite **adds five bit-packer-aware
primitives** that consolidate those inline duplications. See "What we built
instead" at the bottom of this file.
The original rejection of upstream's abstraction still stands. The patterns
those rejected symbols *attempted* to address are now solved by the new
primitives, in shapes that real consumers can actually use.
## Why the abstraction is not ported
The upstream provider abstraction is a self-referential layer over `@suaveplan/crypto`:
its `LibsodiumCryptoProvider` does `import from "@suaveplan/crypto"`; its
`EllipticCryptoProvider` wraps the same primitives via the `elliptic` package; its
`NoOpCryptoProvider` is a test harness. A grep across `/mnt/s/web/suaveplan/packages`
on 2026-05-02 confirmed **no upstream consumer outside the bitpacker package itself**
imports `BaseCryptoProvider`, `CryptoRegistry`, or any adapter — the abstraction was
never wired into a production code path.
suaveplan already ships every underlying primitive in five focused packages
(`@suaveplan/crypto`, `crypto-sodium`, `crypto-browser`, `crypto-vault`, `crypto-keys`)
plus a higher-level chunked AEAD in `@suaveplan/packer`. Adding the upstream
abstraction would duplicate work, route around the suaveplan structured-error / OTel
conventions, and contradict the "no duplicate libraries" directive in CLAUDE.md.
## Rejected upstream surface and its suaveplan replacement
| Upstream symbol | Upstream file:lines | suaveplan replacement |
|---|---|---|
| `BaseCryptoProvider extends EventEmitter` | `crypto/provider/provider.ts:26-232` | None — call `@suaveplan/crypto` directly |
| `generateNonce(length)` | `crypto/provider/provider.ts:237-248` | `@suaveplan/crypto-utils::random.bytes(length)` |
| `concatBytes(...arrays)` | `crypto/provider/provider.ts:253-262` | `@suaveplan/crypto-utils::bytes.concat(...arrays)` |
| `validateKeyLength(key, expected, type)` | `crypto/provider/provider.ts:267-277` | Caller-side guard via `BitpackerError` (no central helper) |
| `constantTimeEqual(a, b)` | `crypto/provider/provider.ts:282-289` | `@suaveplan/crypto-utils::bytes.equalsConstantTime(a, b)` |
| `secureWipe(buffer)` | `crypto/provider/provider.ts:294-296` | `@suaveplan/crypto-utils::bytes.zeroize(buffer)` |
| `LibsodiumCryptoProvider` + `createXChaCha20Provider` + `createAesGcmProvider` | `crypto/adapters/libsodium/libsodium.ts:94-335` | `@suaveplan/crypto-sodium` + `@suaveplan/crypto::aead` |
| `EllipticCryptoProvider` + `createSecp256k1Provider` + `createEd25519Provider` + `createP256Provider` | `crypto/adapters/elliptic/elliptic.ts:36-572` | `@suaveplan/crypto-curve` |
| `NoOpCryptoProvider` + `createTestProvider` + `createSlowProvider` + `createUnreliableProvider` | `crypto/adapters/noop/noop.ts:35-335` | `@suaveplan/testing` fakes for `@suaveplan/crypto` |
| `CryptoRegistry extends EventEmitter` + `createCryptoRegistry` + `getGlobalRegistry` | `crypto/registry/registry.ts:21-370` | None — registry was self-referential |
## What `@suaveplan/bitpacker-crypto` still ships
This package is **not deprecated**. It still ships the checksum surface that the
upstream `bitpacker/src/core/checksum/` directory provided, all under `@suaveplan/bitpacker-crypto`:
- `crc32(data)` and `appendCrc32` / `verifyAndStripCrc32`
- `adler32(data)`
- `calculateXxhash32`, `packXxhash32`, `unpackXxhash32`, `appendXxhash32`,
`verifyXxhash32`, `verifyAndStripXxhash32`
Checksums are not part of the rejected provider abstraction; this decision
record covers only the `crypto/` directory of the upstream bitpacker source.
## Encrypt-then-pack — recommended replacement pattern
If you need to encrypt or sign bytes that go into a bitpacker payload, reach
for `@suaveplan/packer` (which ships chunked AEAD already) or call
`@suaveplan/crypto` directly. There is no provider/registry indirection .
```typescript
import { Packer } from "@suaveplan/packer";
import { aead } from "@suaveplan/crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
const schema = defineBitSchema({ payload: "~" });
const bytes = schema.pack({ payload: new Uint8Array([1, 2, 3, 4]) });
// Encrypt the packed bytes:
const ciphertext = await aead.encrypt({
algorithm: "xchacha20-poly1305",
key: myKey,
nonce: myNonce,
plaintext: bytes,
associatedData: new Uint8Array(),
});
// Or use @suaveplan/packer for chunked AEAD with a manifest:
const packer = new Packer({ schema, key: myKey });
const envelope = await packer.pack({ payload: new Uint8Array([1, 2, 3, 4]) });
```
## See also
- spec change `bitpacker-crypto-port` — original decision (superseded).
- spec change `bitpacker-crypto-rewrite` — replacement decision; **slimmed 2026-05-02**
after `restore-crypto-keychain-packer` discovery to two auxiliary primitives only.
- spec change `restore-crypto-keychain-packer` — parallel work that owns the new
opinionated `@suaveplan/crypto` layer (Packer / Authority / AuditLog / RevocationList).
- `bitpacker-modular-restructure` parity table — flips the five corresponding rows
from "🚫 not ported" to "⛔ intentionally superseded; see `restore-crypto-keychain-packer`
for the consolidation, `bitpacker-crypto-rewrite` for the auxiliary primitives".
- `@suaveplan/crypto`, `@suaveplan/crypto-sodium`, `@suaveplan/crypto-browser`,
`@suaveplan/crypto-vault`, `@suaveplan/crypto-keys`, `@suaveplan/crypto-utils`,
`@suaveplan/packer` — the crypto family.
## What we built instead — bit-packer auxiliary primitives only
After the 2026-05-02 audit + the discovery that **`restore-crypto-keychain-packer`**
already proposes a new opinionated `@suaveplan/crypto` layer with `Packer` / `Authority` /
`AuditLog` / `RevocationList`, this package now ships only the two primitives that
don't overlap that work:
| Primitive | What it does | Consumer that drove the design |
|---|---|---|
| `schemaFingerprint(schema, algorithm?, registry?)` | Content-hash a `BitPackerSchema` after canonicalization (sorted keys, no whitespace, deterministic encoding). Algorithm name resolved via `HashAlgorithmRegistry`. Companion `assertSchemaFingerprint` for "do we agree on the schema?" handshakes. | `@suaveplan/network-registry::Xxhash32HashProvider`/`Crc32HashProvider` (upstream port); planned mesh schema-agreement handshake (`docs/network/security-and-tokens.md`, `docs/network/architecture.md`). |
| `HashAlgorithmRegistry` | Pluggable hash-algorithm registry. Defaults: `xxhash32`, `crc32`, `sha256`. Consumers register their own (`blake3` for drive whole-file hash, future PQC). User directive on extensibility/plugins/middleware made this a first-class surface. | `docs/drive/encryption.md` L79 (BLAKE3 whole-file hash); user directive (extensibility/plugins). |
Plus the existing checksum surface (`crc32`, `adler32`, `xxhash32` and their
`appendCrc32`/`verifyAndStripCrc32`/`appendXxhash32`/etc. helpers) is preserved unchanged.
## What lives in `@suaveplan/crypto` instead (via `restore-crypto-keychain-packer`)
The patterns my first-draft rewrite proposed are now correctly placed in the new
opinionated `@suaveplan/crypto` layer, where they compose with `Authority` + `Keychain` +
the closed-list `DomainTag` registry:
| First-draft primitive (dropped here) | Owned by `@suaveplan/crypto` (via `restore-crypto-keychain-packer`) |
|---|---|
| `SignedBitSchema` (Authority + DomainTag) | `Packer` (encrypt-and-sign envelope under DomainTag via Authority) + direct `Authority.sign(domain, slot, payload)` for sign-only |
| `EnvelopeSchema(magic, version, body)` | `packPublicBundle` shape + `Packer` envelope |
| `HashChainSchema` (Merkle-chained signed log) | `AuditLog` (Merkle log + `proveInclusion` / `verifyInclusion`) |
| `MultiSignedBitSchema` (N-of-N co-sign) | If/when `MeshTrustToken` (NET-110) needs it, the spec delta lands in `@suaveplan/crypto` next to `Packer` and `RevocationList` (not here — co-signing belongs in the `Authority`-using crypto layer) |
| `EncryptedBitSchema` (niche AEAD) | `Packer` (covers small-payload encrypt-and-sign exactly) |
| New domain tags (`DOMAIN_BITPACKER_*_V1`) | `DomainTag` is a closed list owned by `@suaveplan/crypto/domain-tags` — additions are wire-format extensions in that package, not in `bitpacker-crypto` |
### What was dropped (chronological)
| Dropped | Why |
|---|---|
| `ChunkVerifier` (first audit pass) | `@suaveplan/packer::ChunkVerifier` already covers the locked v0x0001 chunk format. |
| `SignedBitSchema` taking raw `signKey` (audit-revision) | suaveplan canonical signing surface is `Authority.sign(domain, slot, payload)`. Raw-key API didn't serve real consumers. |
| `EnvelopeSchema` with opaque body (audit-revision) | Real consumers (`PublicBundle`, `ChainBundle`) have structured body fields. |
| `DomainTagRegistry` open registry (audit-revision) | Conflicts with suaveplan's intentional closed-list `DomainTag` design. |
| `SignatureAlgorithmRegistry` (audit-revision) | `Authority` already dispatches on slot algorithm. |
| `SignedBitSchema` / `EnvelopeSchema` / `HashChainSchema` / `MultiSignedBitSchema` / `EncryptedBitSchema` (slim revision) | All overlap with `restore-crypto-keychain-packer`'s `Packer` / `AuditLog` / `RevocationList`. suaveplan policy explicitly forbids duplicate libraries. |
| New `DOMAIN_BITPACKER_*_V1` constants (slim revision) | Closed-list domain tags belong in `@suaveplan/crypto/domain-tags`, not here. |
The two surviving primitives (`SchemaFingerprint`, `HashAlgorithmRegistry`) are pure
bit-packer utilities that compose with `restore-crypto-keychain-packer` without conflict.

View File

@@ -0,0 +1,89 @@
# CRC32 + Adler32 Checksums
## Purpose
Fast non-cryptographic checksums for bit-packed payloads. `crc32` implements the IEEE-802.3 / zlib polynomial (`0xedb88320`); `adler32` implements the RFC 1950 algorithm. Both are designed to detect accidental corruption (single-bit flips, wire-level errors, dropped bytes), not adversarial tampering — for cryptographic integrity, use the AEAD or signed-bitschema primitives in `@suaveplan/bitpacker-crypto`'s `encrypted/` and `signed/` modules.
The four wrappers (`appendCrc32`, `verifyAndStripCrc32`, `appendAdler32`, `verifyAndStripAdler32`) bookend a payload with a 4-byte big-endian checksum so framed bitpacker output can be transmitted over a noisy channel and integrity-checked at the receiving end. The wrappers throw on truncated input or checksum mismatch; the bare `crc32` / `adler32` functions return the raw 32-bit value for callers that want to integrate the check elsewhere.
## Features
- `crc32(data)` — IEEE-802.3 / zlib polynomial CRC32 with a precomputed 256-entry table
- `adler32(data)` — RFC 1950 Adler32 with the standard `MOD_ADLER = 65521` prime
- `appendCrc32` / `appendAdler32` — append 4-byte big-endian checksum
- `verifyAndStripCrc32` / `verifyAndStripAdler32` — verify and strip 4-byte checksum, throwing on mismatch or short input
- Pure JS, no dependencies, deterministic output, browser- and Node-safe
## Usage: Basic Example
```ts
import { crc32, appendCrc32, verifyAndStripCrc32 } from "@suaveplan/bitpacker-crypto";
const payload = new Uint8Array([1, 2, 3, 4]);
const checksum = crc32(payload); // 0xb63cfbcd
const framed = appendCrc32(payload); // 8 bytes: payload + 4-byte CRC
const restored = verifyAndStripCrc32(framed); // -> [1, 2, 3, 4]
```
## Usage: Advanced Example
```ts
import {
appendCrc32,
verifyAndStripCrc32,
appendAdler32,
verifyAndStripAdler32,
} from "@suaveplan/bitpacker-crypto";
// Frame a bit-packed payload, transmit, and validate on receive.
function transmit(payload: Uint8Array): Uint8Array {
return appendCrc32(payload);
}
function receive(framed: Uint8Array): Uint8Array {
try {
return verifyAndStripCrc32(framed);
} catch (err) {
// Caller decides: retransmit, drop, or escalate to a higher-level
// integrity check (signed-bitschema for adversarial channels).
throw err;
}
}
// Adler32 is faster but weaker; pick it for short payloads on trusted channels.
const cheap = appendAdler32(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));
const back = verifyAndStripAdler32(cheap);
```
## API Reference
### `crc32(data: Uint8Array): number`
Returns the 32-bit unsigned CRC32 value of `data` using the IEEE-802.3 / zlib polynomial (`0xedb88320`). Uses a precomputed 256-entry table for one-byte-per-iteration throughput.
### `adler32(data: Uint8Array): number`
Returns the 32-bit unsigned Adler32 value of `data` per RFC 1950 (`a` and `b` accumulators with `MOD_ADLER = 65521`).
### `appendCrc32(data: Uint8Array): Uint8Array`
Returns a new array containing `data` followed by 4 big-endian bytes of `crc32(data)`.
### `verifyAndStripCrc32(data: Uint8Array): Uint8Array`
Returns the leading `data.length - 4` bytes if the trailing 4 bytes match `crc32(payload)`. Throws `Error` with `"Data too short for CRC32 verification"` if `data.length < 4`, or `"CRC32 mismatch: expected X, got Y"` on mismatch.
### `appendAdler32(data: Uint8Array): Uint8Array`
Returns a new array containing `data` followed by 4 big-endian bytes of `adler32(data)`.
### `verifyAndStripAdler32(data: Uint8Array): Uint8Array`
Mirrors `verifyAndStripCrc32` for Adler32. Same error semantics.
## Implementation Notes
- **CRC32 polynomial = `0xedb88320`.** This is the bit-reversed form of the standard IEEE-802.3 polynomial (`0x04c11db7`). The reversal is encoded in the table-precomputation loop (`c & 1 ? POLY ^ (c >>> 1) : c >>> 1`) so the streaming inner loop processes least-significant bits first, matching zlib / gzip / PNG.
- **Big-endian checksum framing.** Both append wrappers write the 32-bit checksum as big-endian bytes, matching network byte order. The `verifyAndStrip` counterparts read with `getUint32(offset, false)` (false = big-endian).
- **Not cryptographically secure.** A motivated attacker can construct collisions for both CRC32 and Adler32 in O(1). Use these for accidental-corruption detection only; use AEAD or signature primitives elsewhere in `@suaveplan/bitpacker-crypto` for adversarial integrity.
- **No allocation in the hot path.** `crc32` / `adler32` only allocate via the precomputed `CRC32_TABLE`. `appendCrc32` / `appendAdler32` allocate one output buffer; `verifyAndStripCrc32` / `verifyAndStripAdler32` allocate via `slice` for the returned payload.

View File

@@ -0,0 +1,174 @@
# EncryptedBitSchema
Niche AEAD-encrypted envelope around a bit-packed body. Wire format:
`[version u8][algTag u8][nonceLen u8][nonce][ciphertext+tag]`.
**Intended scope: small encrypted payloads only.** Token bodies,
key-wrap envelopes inside `MessageEnvelope`, sub-MTU control messages.
**NOT a chunked-file encoder** — `@suaveplan/packer` owns chunked-file
encryption with its locked v0x0001 codec.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Several suaveplan surfaces need a small, self-describing AEAD-encrypted
envelope: a token body wrapped for transport, an inner
key-wrap inside a higher-level message envelope, an opaque control
record signed-then-encrypted by upstream layers. Re-implementing the
"version + algTag + nonce + ciphertext" frame each time invites
nonce-reuse mistakes and silent algorithm drift.
`EncryptedBitSchema` standardises the frame, picks safe defaults
(XChaCha20-Poly1305 with 24-byte random nonce, 32-byte key, no
`Math.random` fallback), and refuses to be misused as a chunked-file
encoder by being explicit in this README and the schema name.
## Features
- Three AEAD algorithms supported: `AES-256-GCM`, `ChaCha20-Poly1305`,
`XChaCha20-Poly1305` (default — its 24-byte random nonce avoids
nonce-collision concerns even at high message volume)
- Stable u8 algorithm tags (0/1/2) on the wire so an envelope encoded
under one algorithm can be detected (and rejected) by a schema
expecting another
- Pluggable `Rng`; default uses `globalThis.crypto.getRandomValues` and
throws `CRYPTO_RNG_UNAVAILABLE` if Web Crypto is missing — never
silently falls back to `Math.random`
- Optional `aad` (additional authenticated data) — held by reference;
participates in the integrity tag so binding the envelope to a
surrounding context (session id, conversation id) is one option away
- Wire-format constants: `version=1`, no tag changes once allocated.
Adding a new algorithm only appends to the tag table.
- Tampered ciphertext rejects with `CRYPTO_DECRYPT_FAILED`
- Wrong-algorithm envelope rejects with `CRYPTO_ENVELOPE_VERSION_MISMATCH`
- Wrong-key decrypt rejects with `CRYPTO_DECRYPT_FAILED`
- OTel spans `bitpacker-crypto.encrypted-pack` /
`bitpacker-crypto.encrypted-unpack`
## Intended scope
This primitive is for **small** payloads. The wire format prepends a 3-byte
header + the full nonce + the AEAD output (ciphertext + 16-byte tag). For
a 100-byte body, that's ~143 bytes total — fine. For a 50 MB chunked file,
this is the wrong shape: you want chunked encryption with a streaming
verifier, which is what `@suaveplan/packer::ChunkedPacker` provides.
If you find yourself reaching for `EncryptedBitSchema` to wrap large
payloads, stop and use `@suaveplan/packer` instead. This warning is also
asserted by a spec-conformance test.
## Usage: Basic Example
```ts
import { defineEncryptedBitSchema } from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
import { aeadEncrypt, aeadDecrypt } from "@suaveplan/crypto";
interface Token { userId: number; expiry: number }
const tokenSchema = defineBitSchema<Token>([
{ name: "userId", type: "uint", bits: 32 },
{ name: "expiry", type: "uint", bits: 32 },
]);
const encryptedToken = defineEncryptedBitSchema<Token>(
{
pack: (t) => tokenSchema.pack(t),
unpack: (bytes) => tokenSchema.unpack(bytes),
},
{
key: aeadKey,
aead: { encrypt: aeadEncrypt, decrypt: aeadDecrypt },
}
);
const wrapped = await encryptedToken.pack({ userId: 7, expiry: 1700000000 });
const original = await encryptedToken.unpack(wrapped);
```
The `aead` option is required and accepts an `AeadOps` bundle
(`{ encrypt, decrypt }`). The canonical implementation is the
`aeadEncrypt` / `aeadDecrypt` functions from `@suaveplan/crypto`. Passing
them via DI keeps `@suaveplan/bitpacker-crypto` free of any runtime
dependency on `@suaveplan/crypto`. The `AEAD_KEY_LENGTH`,
`AEAD_NONCE_LENGTH`, and `AEAD_XCHACHA_NONCE_LENGTH` constants are
declared locally in this primitive (not imported from
`@suaveplan/crypto`); duplication is acceptable because these values
are part of the bit-packer-crypto wire format.
## Usage: Advanced Example — bind to a session id via AAD
```ts
const sessionId = new TextEncoder().encode("session:abc123");
const encrypted = defineEncryptedBitSchema<Token>(codec, {
key,
aad: sessionId,
});
// Only an unpack with the SAME aad will succeed; replays into a different
// session will reject with CRYPTO_DECRYPT_FAILED.
```
## API Reference
### `interface EncryptedBitSchemaOptions`
- `key: Uint8Array` — exactly 32 bytes (`AEAD_KEY_LENGTH`)
- `aead: AeadOps` (required) — AEAD operations bundle; typically
`{ encrypt: aeadEncrypt, decrypt: aeadDecrypt }` from
`@suaveplan/crypto`
- `algorithm?: AeadAlgorithm = "XChaCha20-Poly1305"`
- `rng?: Rng` — defaults to a `globalThis.crypto.getRandomValues`-backed Rng
- `aad?: Uint8Array` — optional additional authenticated data
### `defineEncryptedBitSchema<TBody>(bodyCodec, options): EncryptedBitSchema<TBody>`
Returns:
- `pack(data: TBody): Promise<Uint8Array>`
- throws `CRYPTO_ENCRYPT_FAILED` on AEAD failure (very rare — usually a
misbehaving Rng or wrong key length)
- throws `CRYPTO_RNG_UNAVAILABLE` on first call if Web Crypto is missing
and no Rng was supplied
- `unpack(bytes: Uint8Array): Promise<TBody>`
- throws `CRYPTO_DECRYPT_FAILED` on tag mismatch / wrong key / too-short
input
- throws `CRYPTO_ENVELOPE_VERSION_MISMATCH` on wire-format/algorithm
mismatch
- `algorithm`, `version`, `nonceLength` (read-only)
### Algorithm tags (wire format)
```text
0 AES-256-GCM (12-byte nonce)
1 ChaCha20-Poly1305 (12-byte nonce)
2 XChaCha20-Poly1305 (24-byte nonce, default)
```
These numbers are wire-format and never renumbered. New algorithms append
to the table.
## Implementation Notes
- Nonces are caller-Rng-driven, never derived from message contents. The
default Rng wraps `crypto.getRandomValues` directly. Tests use a
seeded mulberry32 Rng to assert reproducibility — production deployments
must use a CSPRNG.
- AEAD failures are wrapped with `BitpackerError` so consumers don't need
to import the underlying `@suaveplan/crypto` `CryptoError` to handle
decrypt failures uniformly with the other bit-packer-crypto primitives.
- The 3-byte header is intentionally minimal — version + alg + nonceLen
is enough to dispatch a future algorithm without breaking older
payloads. The `nonceLen` byte is technically derivable from the
`algTag`, but storing it explicitly makes the parser simpler and
catches an algorithm/nonce mismatch as a wire-format error instead of
a more confusing AEAD failure.
- The body codec runs on the _plaintext_ — `pack` calls `bodyCodec.pack`
before encrypting, `unpack` calls `bodyCodec.unpack` after decrypting.
This means the body codec sees full plaintext and must be trusted not
to leak side-channels.
- AEAD encrypt/decrypt are imported directly from `@suaveplan/crypto`'s
main barrel; no engine override is exposed. If you need an engine
override (e.g. for a hardware-accelerated AEAD), build a custom Rng
wrapper or use `@suaveplan/crypto`'s engine-pluggable API directly
outside this primitive.

View File

@@ -0,0 +1,142 @@
# EnvelopeSchema
A typed, version-aware envelope schema for bit-packed payloads. Wraps a
caller-supplied `BodyCodec` with a `[magic][formatVersion][body]` frame so the
wire format is self-describing on the magic + version dimensions, while the
body remains pluggable. Replaces the manual
`defineBitSchema([{magic}, {formatVersion}, ...])` pattern that suaveplan
crypto, packer, and other consumers had been re-authoring.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Many suaveplan wire formats begin with a magic number + format version: chain
bundles (`SVCB`), public bundles, the chunked packer, and several network
encoders all do the same thing manually. The duplication isn't just code
churn — every callsite is a place where the magic/version contract can drift
out of sync with its parser. `EnvelopeSchema` consolidates the pattern and
makes "this byte string belongs to schema X version Y" a first-class
runtime check.
The body region is opaque to the envelope itself; encoding is delegated to a
`BodyCodec<TBody>` (`pack` + `unpack`). The bundled `rawBytesCodec()` is
provided for callers whose bodies are already-encoded bytes (typical when
composing with `SignedBitSchema` or `EncryptedBitSchema`); for structured
bodies, callers pass a codec built from the legacy `defineBitSchema(...)`
or from any encoder satisfying the two-method contract.
## Features
- `pack(body)` produces `[magic][formatVersion][bodyCodec.pack(body)]`
- `parse(bytes)` extracts magic + version + raw body bytes WITHOUT decoding
the body — enabling forward-compat dispatch when a future version arrives
- `parseStrict(bytes)` validates magic + version, then decodes the body to
`TBody` via `bodyCodec.unpack`
- Mismatched magic throws `BitpackerError` with code
`CRYPTO_ENVELOPE_MAGIC_MISMATCH` and `context.{expected,actual}`
- Mismatched version throws `BitpackerError` with code
`CRYPTO_ENVELOPE_VERSION_MISMATCH` and `context.{expected,actual}`
- Zod-validated options at construction; unknown keys rejected
- OTel spans `bitpacker-crypto.envelope-pack` and
`bitpacker-crypto.envelope-parse` carry `bitpacker.crypto.payload_bytes`
- Stays codec-agnostic so a future Protobuf-bodied envelope or a
schema-fingerprinted variant requires zero changes to envelope itself
## Usage: Basic Example
```ts
import {
type BodyCodec,
defineEnvelopeSchema,
} from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
interface Header {
foo: number;
bar: number;
}
const headerSchema = defineBitSchema<Header>([
{ name: "foo", type: "uint", bits: 8 },
{ name: "bar", type: "uint", bits: 16 },
]);
const codec: BodyCodec<Header> = {
pack: (body) => headerSchema.pack(body),
unpack: (bytes) => headerSchema.unpack(bytes),
};
const envelope = defineEnvelopeSchema<Header>({
magic: new Uint8Array([0x53, 0x56, 0x43, 0x42]), // "SVCB"
formatVersion: 1,
bodyCodec: codec,
});
const bytes = envelope.pack({ foo: 7, bar: 1234 });
const { foo, bar } = envelope.parseStrict(bytes); // typed: Header
```
## Usage: Advanced Example — forward-compat dispatch
Use `parse` to peek at magic + version before deciding which decoder to run,
so a v1 reader can detect v2 envelopes and route accordingly:
```ts
import { defineEnvelopeSchema } from "@suaveplan/bitpacker-crypto";
const envV1 = defineEnvelopeSchema({ magic, formatVersion: 1, bodyCodec: codecV1 });
const envV2 = defineEnvelopeSchema({ magic, formatVersion: 2, bodyCodec: codecV2 });
function decode(bytes: Uint8Array) {
const partial = envV1.parse(bytes);
if (partial.formatVersion === 1) return envV1.parseStrict(bytes);
if (partial.formatVersion === 2) return envV2.parseStrict(bytes);
throw new Error(`Unsupported envelope version: ${partial.formatVersion}`);
}
```
The body bytes are not consumed by `parse`, so the same input can be passed
back to `parseStrict` of the matching version without re-allocation cost on
the magic+version prefix.
## API Reference
### `interface BodyCodec<TBody>`
- `pack(body: TBody): Uint8Array` — encode the body
- `unpack(bytes: Uint8Array): TBody` — decode the body
### `interface EnvelopeSchemaOptions<TBody>`
- `magic: Uint8Array` — non-empty
- `formatVersion: number` — integer 0..255
- `bodyCodec: BodyCodec<TBody>` — body encoding hook
### `defineEnvelopeSchema<TBody>(options): EnvelopeSchema<TBody>`
Returns:
- `magic: Uint8Array`
- `formatVersion: number`
- `pack(body: TBody): Uint8Array`
- `parse(bytes: Uint8Array): { magic, formatVersion, bodyBytes }`
- `parseStrict(bytes: Uint8Array): TBody`
- throws `CRYPTO_ENVELOPE_MAGIC_MISMATCH` on magic mismatch
- throws `CRYPTO_ENVELOPE_VERSION_MISMATCH` on version mismatch
### `rawBytesCodec(): BodyCodec<Uint8Array>`
Pass-through codec for bodies that are already-encoded raw bytes — useful
when composing with `SignedBitSchema` or `EncryptedBitSchema` whose `pack`
output goes inside an envelope.
## Implementation Notes
- The envelope wire format is `[magic_bytes][1-byte u8 formatVersion][body_bytes]`. The body is NOT framed with a length prefix — the codec is expected to either be fixed-size or to consume the entire trailing buffer.
- `parse` throws `CRYPTO_ENVELOPE_MAGIC_MISMATCH` when input is shorter than `magic.length + 1` (the minimum header size). Magic and version validation in `parseStrict` happens against the parsed values, so a too-short input is caught at the parse stage.
- Magic comparison is constant-time-irrelevant — magic bytes are not secret. Equality is byte-wise. The error context includes hex-encoded `expected` and `actual` for diagnosability.
- The codec is held by reference; mutating the codec after `defineEnvelopeSchema(...)` returns will affect future `pack`/`parseStrict` calls. Callers should treat the codec as immutable in practice.
- OTel spans are emitted unconditionally (even when no SDK is wired) because they cost essentially nothing under the no-op tracer; production traces materialise once a tracer provider is installed.

View File

@@ -0,0 +1,127 @@
# SchemaFingerprint
Content-hash a `BitPackerSchema` so two parties can agree they're using
byte-identical schemas before they exchange payloads. The fingerprint is
computed over a canonical serialisation (sorted keys, no whitespace,
deterministic encoding), and the hash algorithm is pluggable via
`HashAlgorithmRegistry`.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Whenever a producer and consumer of bit-packed bytes are not co-located —
mesh peers, drive replicas, network plugins, or two services that pinned
their schemas independently — they need a way to confirm "we're talking
the same shape" before risking a parse error on a half-mismatched schema.
A schema fingerprint compresses that question to a fixed-size digest that
can be compared in a handshake.
The fingerprint must be _order-insensitive_: a producer that declared
fields in declaration order `(foo, bar)` and a consumer that declared them
in the order `(bar, foo)` should fingerprint identically, because the wire
format itself is order-independent at the bit-packer level (object keys
participate, but their declaration order does not). Achieving that
requires canonical-serialisation, which this module owns.
## Features
- Default algorithm: `sha256` (32 bytes, cryptographically strong)
- Built-in alternatives: `xxhash32` (4 bytes, fast non-crypto), `crc32`
(4 bytes, fast non-crypto)
- Pluggable: register `blake3` (or any custom hash) via
`HashAlgorithmRegistry` and pass it as `registry` argument
- `assertSchemaFingerprint(schema, expected, ...)` is the single-call
handshake check; throws `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH` with a
context that includes hex `expected` and `actual` for diagnosability
- Unknown algorithm name throws `CRYPTO_HASH_ALGORITHM_UNKNOWN`
- Functions inside the schema (e.g. `ConditionalSchema._if`) are rejected;
v1 fingerprintable schemas are data-shape only
- Constant-time digest comparison in `assertSchemaFingerprint` (so timing
side channels can't leak partial-match information)
- OTel span `bitpacker-crypto.fingerprint` with algorithm + payload_bytes
## Usage: Basic Example
```ts
import {
assertSchemaFingerprint,
schemaFingerprint,
} from "@suaveplan/bitpacker-crypto";
const schema = { foo: 255, bar: 65535 };
const fp = await schemaFingerprint(schema); // sha256 by default
// On the other side of a handshake, validate the schemas match:
await assertSchemaFingerprint(localSchema, fp);
```
## Usage: Advanced Example — registry-backed BLAKE3
```ts
import {
type HashAlgorithm,
createHashRegistry,
schemaFingerprint,
} from "@suaveplan/bitpacker-crypto";
import { blake3 as nobleBlake3 } from "@noble/hashes/blake3";
const blake3: HashAlgorithm = {
name: "blake3",
outputBytes: 32,
hash: (data) => Promise.resolve(nobleBlake3(data)),
};
const registry = createHashRegistry();
registry.register(blake3);
const fp = await schemaFingerprint(schema, "blake3", registry);
```
## API Reference
### `schemaFingerprint(schema, algorithm?, registry?): Promise<Uint8Array>`
- `schema: BitPackerSchema` — the schema to hash. Functions and
non-finite numbers are rejected.
- `algorithm: string = "sha256"` — name of a registered algorithm
- `registry: HashAlgorithmRegistry = defaultHashRegistry()` —
optional override
- **Throws** `CRYPTO_HASH_ALGORITHM_UNKNOWN` if the algorithm isn't
registered; `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH` if the schema contains
unsupported value types (functions, NaN, Infinity).
### `assertSchemaFingerprint(schema, expected, algorithm?, registry?): Promise<void>`
- Computes the fingerprint and compares it byte-wise (constant-time) to
`expected`. Throws `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH` on disagreement,
with `context.expected` and `context.actual` hex-encoded for
diagnosability.
### `canonicalize(schema): string`
Exposed for testing the canonical-serialisation invariant directly.
Returns the JSON-like canonical string (sorted keys, no whitespace).
## Implementation Notes
- The canonicalizer is recursive and supports: `null`, `undefined` (treated
as `null`), `number` (must be finite), `boolean`, `string`, `bigint`
(suffixed with `n`), `object` (keys sorted lexicographically), `array`
(order preserved). All other types throw `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH`.
- `bigint` values are serialised as `123n` (with the `n` suffix) to keep
them distinguishable from numbers in the canonical form. JSON does not
natively support bigint, so this is a deliberate extension.
- Constant-time comparison in `assertSchemaFingerprint` matters even though
fingerprints are not secret — the operation is cheap, and using a
constant-time check avoids becoming a habit-of-non-constant-time-compare
at higher layers.
- The OTel span carries `bitpacker.crypto.algorithm` and
`bitpacker.crypto.payload_bytes` so a deployment can observe schema
agreement frequency and the size of canonicalised schemas.
- Function-bearing schemas (`ConditionalSchema`, `VersionSchema`) are out
of scope for v1 fingerprinting because there is no canonical way to hash
a function. A future version can add a side-channel (e.g. a stable name
registry mapping function identity → string) if a real consumer needs it.

View File

@@ -0,0 +1,119 @@
# HashAlgorithmRegistry
A pluggable registry of named hash algorithms used by every hashing-aware
primitive in `@suaveplan/bitpacker-crypto` (notably `SchemaFingerprint` and
`HashChainSchema`). Ships with three defaults out of the box: `sha256` (32
bytes, Web Crypto), `xxhash32` (4 bytes, in-tree), and `crc32` (4 bytes,
in-tree). Consumers register additional algorithms — e.g. `blake3` — via
`register`. Last-write-wins replacement is permitted and emits an OpenTelemetry
span so swap-ins are observable.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The crypto stack avoids hard-coding a single hash function inside every
primitive that needs one. Instead, hashing is a first-class extension point:
primitives accept an optional `algorithm` name and an optional registry, and
the registry resolves the name to a concrete `HashAlgorithm`. This lets a
deployment swap in a faster sha256 (e.g. native node:crypto) or register an
entirely new algorithm without forking the primitive.
The registry shape is intentionally tiny — `register`, `resolve`, `has`,
`list` — because the surface area is also a maintenance surface area. There is
no `unregister`: defaults are guaranteed present on every fresh registry, and
test isolation is achieved by calling `createHashRegistry()` to get a private
instance instead of mutating the singleton.
## Features
- Three defaults bundled: `sha256` (Web Crypto async), `xxhash32` (in-tree
sync), `crc32` (in-tree sync, big-endian 4-byte digest)
- Uniform async `hash(data) → Promise<Uint8Array>` interface across sync and
async backends
- `register` is last-write-wins; replacing an existing algorithm emits a
`bitpacker-crypto.hash-registry.replaced` OTel span carrying the previous
and new `outputBytes` attributes
- Process-wide singleton via `defaultHashRegistry()`; isolated registries via
`createHashRegistry()`
- Unknown algorithm name throws `BitpackerError` with code
`CRYPTO_HASH_ALGORITHM_UNKNOWN` and `context.algorithm` for diagnosability
## Usage: Basic Example
```ts
import { defaultHashRegistry } from "@suaveplan/bitpacker-crypto";
const sha256 = defaultHashRegistry().resolve("sha256");
const digest = await sha256.hash(new TextEncoder().encode("hello"));
console.log(digest.length); // 32
```
## Usage: Advanced Example — registering BLAKE3
```ts
import {
type HashAlgorithm,
createHashRegistry,
} from "@suaveplan/bitpacker-crypto";
import { blake3 as nobleBlake3 } from "@noble/hashes/blake3";
const blake3: HashAlgorithm = {
name: "blake3",
outputBytes: 32,
hash: (data) => Promise.resolve(nobleBlake3(data)),
};
// Build an isolated registry so we don't pollute the singleton.
const registry = createHashRegistry();
registry.register(blake3);
// Use with SchemaFingerprint / HashChainSchema:
// schemaFingerprint(schema, "blake3", registry)
```
## API Reference
### `interface HashAlgorithm`
- `name: string` — stable identifier used for registry lookup and wire-format
tagging
- `outputBytes: number` — exact digest length
- `hash(data: Uint8Array): Promise<Uint8Array>` — compute the digest;
resolves to exactly `outputBytes` bytes
### `interface HashAlgorithmRegistry`
- `register(algorithm: HashAlgorithm): void` — last-write-wins; emits
`bitpacker-crypto.hash-registry.replaced` span when overwriting
- `resolve(name: string): HashAlgorithm` — throws `CRYPTO_HASH_ALGORITHM_UNKNOWN`
on miss
- `has(name: string): boolean`
- `list(): readonly string[]` — names in stable sort order
### `createHashRegistry(): HashAlgorithmRegistry`
Fresh registry pre-populated with the defaults. Independent of the singleton;
mutations stay local.
### `defaultHashRegistry(): HashAlgorithmRegistry`
Process-wide singleton. Lazily constructed on first call. Subsequent calls
return the same instance.
## Implementation Notes
- The interface is asynchronous because `sha256` is implemented over Web
Crypto's `crypto.subtle.digest`, which returns a Promise. Synchronous
algorithms wrap their result with `Promise.resolve` for uniformity.
- `crc32` digests are emitted big-endian (most-significant byte first) so the
byte order matches the algorithm's natural representation; `xxhash32`
digests are emitted little-endian to match the bit-packed checksums already
produced by `appendXxhash32`.
- Replacement intentionally does not throw. The OTel `replaced` span is the
observability hook for swap-ins; tests asserting registry hygiene should
monitor the span stream rather than rely on exception flow.
- `unregister` is intentionally absent. If you need a clean slate, build a
fresh registry with `createHashRegistry()` and use that one.

View File

@@ -0,0 +1,175 @@
# HashChainSchema
Append-only signed hash chain. Each entry carries a sequence number, the
hash of the previous entry, an opaque payload, and a host signature over
the bit-packed `(seq, prevHash, payload)` triplet. Tampering anywhere in
the chain is detected at the first bad index by `verifyChain`.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Audit logs, replicated mesh state, and any "I cannot edit history,
only append to it" scenario need a chain shape: each new entry binds to
the previous via a hash, and the host signs the binding so a third party
can verify the chain wasn't forged. suaveplan has the building blocks —
`Authority`, `DomainTag`, `HashAlgorithmRegistry`, `BodyCodec` — but no
primitive that composes them into a chain. `HashChainSchema` is that
primitive.
The signing input is `(seq, prevHash, payload)`, NOT just `payload`. This
is load-bearing: a signature on a payload alone could be lifted into a
different position in the chain (different seq) or grafted onto a different
prev-link without invalidating the signature. By binding seq + prev-hash
into the signed bytes, the signature is non-portable across positions.
## Features
- `appendEntry(seq, prevHash, payload, authority)` — sign the triplet,
return a complete `HashChainEntry`
- `verifyEntry(entry, expectedPrevHash, hostPublicSlot)` — boolean
result, never throws on bad signature (callers may probe candidate
parents)
- `verifyChain(entries, hostPublicSlot)` — walks the chain in order;
returns `{valid: false, firstBadIndex}` on the first failure
- `packEntry(entry)` / `unpackEntry(bytes)` — wire-format codec for
storage / network transport
- `hashEntry(entry)` — compute the entry's digest, used as the next
entry's `prevHash`
- Pluggable hash algorithm via `HashAlgorithmRegistry` (default
`sha256` 32 bytes)
- Strict prev-hash length check at append time (must match algorithm's
`outputBytes`) — catches schema/algorithm mismatches before they
reach the wire
- AbortSignal threaded through `Authority.sign`
- OTel spans `bitpacker-crypto.hashchain-append` and
`bitpacker-crypto.hashchain-verify`
## Wire Format
```text
┌────────────┬────────────────────┬──────────────────────┬──────────────────┐
│ seq u64BE │ prevHash N bytes │ payload bit-packed │ signature 64 B │
└────────────┴────────────────────┴──────────────────────┴──────────────────┘
```
`N` is the chosen hash algorithm's `outputBytes` (32 for sha256). The
signature is 64 bytes (ed25519); other lengths are configurable via
`signatureLength`.
## Usage: Basic Example
```ts
import { defineHashChainSchema } from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
import { DOMAIN_BITPACKER_HASHCHAIN_V1 } from "@suaveplan/crypto/domain-tags";
import { verifyAuthoritySignature } from "@suaveplan/crypto/authority";
interface AuditPayload { actorId: number; eventCode: number }
const auditSchema = defineBitSchema<AuditPayload>([
{ name: "actorId", type: "uint", bits: 32 },
{ name: "eventCode", type: "uint", bits: 16 },
]);
const audit = defineHashChainSchema<AuditPayload>({
domain: DOMAIN_BITPACKER_HASHCHAIN_V1,
verifier: verifyAuthoritySignature,
payloadCodec: {
pack: (b) => auditSchema.pack(b),
unpack: (bytes) => auditSchema.unpack(bytes),
},
});
let prevHash = new Uint8Array(audit.hashLength); // suaveplan sentinel
const e0 = await audit.appendEntry(0n, prevHash, { actorId: 1, eventCode: 100 }, auth);
prevHash = await audit.hashEntry(e0);
const e1 = await audit.appendEntry(1n, prevHash, { actorId: 1, eventCode: 200 }, auth);
const result = await audit.verifyChain([e0, e1], hostPublicSlot);
// → { valid: true }
```
## Usage: Advanced Example — BLAKE3-backed chain
```ts
import {
type HashAlgorithm,
createHashRegistry,
defineHashChainSchema,
} from "@suaveplan/bitpacker-crypto";
import { blake3 as nobleBlake3 } from "@noble/hashes/blake3";
const registry = createHashRegistry();
registry.register({
name: "blake3",
outputBytes: 32,
hash: (data) => Promise.resolve(nobleBlake3(data)),
});
const chain = defineHashChainSchema({
domain: DOMAIN_BITPACKER_HASHCHAIN_V1,
payloadCodec,
hashAlgorithm: "blake3",
hashRegistry: registry,
});
```
## API Reference
### `interface HashChainEntry<TPayload>`
- `seq: bigint` — non-negative sequence number, packed as u64 BE
- `prevHash: Uint8Array` — exactly `hashLength` bytes
- `payload: TPayload` — caller-typed body
- `hostSignature: Uint8Array` — exactly `signatureLength` bytes
### `interface HashChainSchemaOptions<TPayload>`
- `domain: DomainTag` — required
- `verifier: AuthorityVerifier` — required (typically
`verifyAuthoritySignature` from `@suaveplan/crypto/authority`)
- `slot?: SlotId = "sign"` — slot used by the host authority
- `payloadCodec: BodyCodec<TPayload>` — required
- `hashAlgorithm?: string = "sha256"` — must be registered
- `hashRegistry?: HashAlgorithmRegistry` — defaults to singleton
- `signatureLength?: number = 64`
### `defineHashChainSchema<TPayload>(options): HashChainSchema<TPayload>`
Returns:
- `hashEntry(entry): Promise<Uint8Array>` — `outputBytes`-byte digest
- `appendEntry(seq, prevHash, payload, authority, signal?): Promise<HashChainEntry<TPayload>>`
- throws `VALUE_OUT_OF_RANGE` if seq is negative or > 2^64-1
- throws `CRYPTO_KEY_INVALID` if prevHash length ≠ hashLength
- throws `CRYPTO_SIGN_FAILED` if signature length ≠ signatureLength
- `verifyEntry(entry, expectedPrevHash, hostPublicSlot): Promise<boolean>`
- `verifyChain(entries, hostPublicSlot): Promise<ChainVerifyResult>`
- `packEntry(entry): Uint8Array`
- `unpackEntry(bytes): HashChainEntry<TPayload>`
- throws `CRYPTO_VERIFY_FAILED` if bytes too short
## Implementation Notes
- The suaveplan entry's `prevHash` is caller-supplied — typically all-zero
bytes, but any deterministic value works as long as both producers and
consumers agree on it. `verifyChain` treats the first entry's
`prevHash` as the trust anchor (it is not derived from anything).
- Tamper detection is positional: `firstBadIndex` is the LOWEST index at
which either the prev-hash linkage or the signature failed. A chain
with multiple tamper points reports only the first; a strict caller
who needs to know every bad index should iterate `verifyEntry`
directly.
- The hash algorithm is resolved once at construction. Swapping the
registry's algorithm later does not affect already-built schemas. This
is deliberate: the chain's wire format is fixed at construction.
- `appendEntry` does NOT compute the new `prevHash` — it only appends.
Callers are responsible for calling `hashEntry(prev)` between appends.
This separation allows callers to batch-append from a known
starting hash without forcing every append to be sequential.
- `verifyChain` rebuilds the `expectedPrev` for index `i ≥ 1` by hashing
`entries[i-1]`. The suaveplan entry's `prevHash` is taken as the trust
anchor; if it does not match what the producer supplied, every chain
built atop it will fail at `firstBadIndex: 0`.

View File

@@ -0,0 +1,8 @@
## index
**Path:** `bitpacker-crypto/src/index.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,131 @@
# MultiSignedBitSchema
Bit-packed payload co-signed by N independent authorities. Wire format is
`[bit-packed body][sig_1][sig_2]…[sig_N]`. Pinpoints which signer failed
on verify so a partial-trust caller can recover gracefully.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Some suaveplan surfaces require multiple signatures to authorise a single
payload — multi-party transaction approval, threshold-equivalent ceremony
records, and any quorum-style action. Building this on top of single-signer
`SignedBitSchema` would force every callsite to roll its own concatenation
and per-signer verify loop. `MultiSignedBitSchema` consolidates the
pattern with strict count enforcement (a 3-signer schema cannot pack with
2 authorities) and with `firstBadSignerIndex` reporting on verify so the
error can carry the actionable detail of _which_ signer failed.
## Features
- N-of-N co-signing with `signerCount` baked into the schema
- Pack rejects wrong-count authority arrays with
`CRYPTO_MULTISIGN_COUNT_MISMATCH`
- Unpack rejects wrong-count public-slot arrays with
`CRYPTO_MULTISIGN_COUNT_MISMATCH`
- `verify` returns `{ valid, firstBadSignerIndex? }` so callers can
enumerate "which signature broke?" without exception flow
- `unpack` throws `CRYPTO_VERIFY_FAILED` whose context includes
`firstBadSignerIndex`
- Each authority signs the same payload bytes under the same domain;
signature ordering follows the authority list
- AbortSignal threaded through every `Authority.sign` call so a slow
signer can be cancelled
- Pluggable body codec (same `BodyCodec<T>` as `SignedBitSchema`)
- OTel spans `bitpacker-crypto.multisigned-pack` and
`bitpacker-crypto.multisigned-verify` with signer_count + payload_bytes
## Usage: Basic Example
```ts
import { defineMultiSignedBitSchema } from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
import { DOMAIN_BITPACKER_MULTISIGN_V1 } from "@suaveplan/crypto/domain-tags";
import { verifyAuthoritySignature } from "@suaveplan/crypto/authority";
interface VoteBody { proposalId: number; vote: number }
const voteSchema = defineBitSchema<VoteBody>([
{ name: "proposalId", type: "uint", bits: 32 },
{ name: "vote", type: "uint", bits: 1 },
]);
const ms = defineMultiSignedBitSchema<VoteBody>(
{
pack: (b) => voteSchema.pack(b),
unpack: (bytes) => voteSchema.unpack(bytes),
},
{
domain: DOMAIN_BITPACKER_MULTISIGN_V1,
verifier: verifyAuthoritySignature,
signerCount: 3,
}
);
const packed = await ms.pack(
{ proposalId: 7, vote: 1 },
[authA, authB, authC]
);
const decoded = ms.unpack(packed, [pubA, pubB, pubC]);
```
## Usage: Advanced Example — graceful partial-trust recovery
```ts
const result = ms.verify(bytes, [pubA, pubB, pubC]);
if (!result.valid) {
// Tell the user EXACTLY which signer rejected the payload.
const failingSigner = signerNames[result.firstBadSignerIndex!];
throw new Error(`Quorum verify failed at signer "${failingSigner}"`);
}
```
## API Reference
### `interface MultiVerifyResult`
- `valid: boolean`
- `firstBadSignerIndex?: number` — present when `valid` is false; zero-indexed
### `interface MultiSignedBitSchemaOptions`
- `domain: DomainTag` — required
- `verifier: AuthorityVerifier` — required (typically
`verifyAuthoritySignature` from `@suaveplan/crypto/authority`)
- `slot?: SlotId = "sign"` — slot used by every authority
- `signerCount: number` — required, range 1..64
- `signatureLength?: number = 64` — wire-format signature length
### `defineMultiSignedBitSchema<T>(bodyCodec, options): MultiSignedBitSchema<T>`
Returns:
- `pack(data: T, authorities: readonly Authority[], signal?): Promise<Uint8Array>`
- `unpack(bytes: Uint8Array, signerPublicSlots: readonly KeySlotPublic<unknown>[]): T`
- `verify(bytes: Uint8Array, signerPublicSlots: readonly KeySlotPublic<unknown>[]): MultiVerifyResult`
- `domain`, `slot`, `signerCount`, `signatureLength`
## Implementation Notes
- Signatures are appended in authority list order and verified in the
same order. Re-ordering the public slot list at verify time will fail
every signature (ed25519 binds key to signature). This is intentional —
the wire format does not permute, so the schema's contract is
positional.
- Pack short-circuits early if any authority returns a wrong-length
signature, throwing `CRYPTO_SIGN_FAILED` with `signerIndex`. A
misbehaving authority cannot poison a partially-signed payload because
bytes are not assembled until every signature is ready.
- Verify does NOT short-circuit on the first failure when called via
`verify(...)` — that's a lie. It DOES short-circuit, returning
`firstBadSignerIndex`. The naming is deliberate: callers should not
rely on every signer being verified just because they asked to verify
N signatures. If you need exhaustive reporting (which signers failed,
which passed), iterate yourself with `verifyAuthoritySignature`.
- The `signerCount` upper bound (64) is conservative; raising it requires
no schema changes, but the on-wire payload grows linearly so very
large signer sets should consider threshold signing instead.
- AbortSignal is checked once per authority — a slow signer at index 5
can be cancelled, but signers 0-4 will already have signed.

View File

@@ -0,0 +1,158 @@
# SignedBitSchema
Bit-packed payload signed by an `Authority` under a fixed `DomainTag`. Wire
format is `[bit-packed body][signature]`. Verifies before decoding so a
tampered payload never even reaches the body codec. Replaces the ad-hoc
`BitPacker.pack(...) → sign(...) → concat(...)` pattern that crypto
chain bundles, public bundles, packer chunks, and game-server state-sync
encoders had been re-authoring.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Every signed wire format in the stack follows the same recipe:
bit-pack a structured body, sign the bytes with an authority slot, frame
`[body][signature]`, then on the verify side strip the signature, verify it
against a parent's public slot, and only then decode the body. Five plus
suaveplan/upstream call-sites had been manually re-authoring this dance, each
slightly differently. `SignedBitSchema` consolidates the pattern so the
domain separation, signature length, sign-slot dispatch, and verify-then-decode
order are all driven by a single primitive.
The domain `DomainTag` is required, not optional, so a signature produced
under one domain (e.g. `DOMAIN_PUBLIC_KEYCHAIN_V1`) cannot be replayed
under a different domain (e.g. `DOMAIN_AUDIT_V1`) — the tag bytes
participate in the signing input via
`@suaveplan/crypto::buildSigningInput`. Cross-protocol replay is rejected
at the bytes-don't-match level rather than at any application-layer check.
## Features
- Pluggable body codec (`BodyCodec<T>`) so the body can be a legacy
`defineBitSchema` codec, a modern `BitPacker`-backed codec, an outer
envelope, or any future encoder satisfying the two-method contract
- `pack(data, authority, signal?)` is async — delegates signing to
`Authority.sign(domain, slot, payload, signal)` so HSM/threshold backends
Just Work without the primitive having to know
- `unpack(bytes, parentPublicSlot)` enforces verify-then-decode order —
body codec is NEVER invoked on a tampered payload
- `verify(bytes, parentPublicSlot)` returns `boolean` so callers can
enumerate candidate parent keys without exception flow
- `unpackSelf(bytes, getEmbeddedSlot)` supports trust-anchor bundles whose
own embedded sign slot is the verifier
- AbortSignal threaded through `Authority.sign` per suaveplan cancellation
rules
- `signatureLength` configurable (defaults to ed25519's 64) so future
primitives with different signature shapes inherit the schema for free
- OTel spans `bitpacker-crypto.signed-pack` and
`bitpacker-crypto.signed-verify` with `domain` + `payload_bytes` attrs
## Usage: Basic Example
```ts
import { defineSignedBitSchema } from "@suaveplan/bitpacker-crypto";
import { defineBitSchema } from "@suaveplan/bitpacker";
import { DOMAIN_PUBLIC_KEYCHAIN_V1 } from "@suaveplan/crypto/domain-tags";
import { verifyAuthoritySignature } from "@suaveplan/crypto/authority";
interface LinkBody {
parentSeq: number;
purposeBits: number;
}
const linkSchema = defineBitSchema<LinkBody>([
{ name: "parentSeq", type: "uint", bits: 32 },
{ name: "purposeBits", type: "uint", bits: 16 },
]);
const link = defineSignedBitSchema<LinkBody>(
{
pack: (b) => linkSchema.pack(b),
unpack: (bytes) => linkSchema.unpack(bytes),
},
{
domain: DOMAIN_PUBLIC_KEYCHAIN_V1,
verifier: verifyAuthoritySignature,
}
);
const packed = await link.pack(
{ parentSeq: 7, purposeBits: 0xf0 },
authority
);
const decoded = link.unpack(packed, parentPublicSlot);
```
The `verifier` option is required and accepts an `AuthorityVerifier`
function (`(domain, publicSlot, payload, signature) => boolean`). The
canonical implementation is `verifyAuthoritySignature` from
`@suaveplan/crypto/authority`. Passing it via DI keeps
`@suaveplan/bitpacker-crypto` free of any runtime dependency on
`@suaveplan/crypto`, which is what allows future
`crypto::ChainBundle`-style consumers to adopt this primitive without
inducing a circular package dependency.
## Usage: Advanced Example — trust anchor via `unpackSelf`
```ts
const trustAnchor = defineSignedBitSchema<TrustAnchorBundle>(codec, {
domain: DOMAIN_BITPACKER_SIGNED_V1,
});
// Verify against the bundle's OWN embedded sign slot.
const bundle = trustAnchor.unpackSelf(bytes, (decoded) => decoded.embeddedSlot);
```
`unpackSelf` decodes the body once to extract the embedded sign slot, then
verifies the signature against that slot. Only used for the root link of a
chain (every other link verifies against its parent).
## API Reference
### `interface SignedBitSchemaOptions`
- `domain: DomainTag` (required) — the domain under which the body is signed
- `verifier: AuthorityVerifier` (required) — pure verifier function;
typically `verifyAuthoritySignature` from `@suaveplan/crypto/authority`
- `slot?: SlotId = "sign"` — the authority slot used for signing
- `signatureLength?: number = 64` — wire-format signature length
### `defineSignedBitSchema<T>(bodyCodec, options): SignedBitSchema<T>`
Returns:
- `pack(data: T, authority: Authority, signal?: AbortSignal): Promise<Uint8Array>`
- throws `CRYPTO_SIGN_FAILED` if the authority returns a wrong-length
signature
- `unpack(bytes: Uint8Array, parentPublicSlot: KeySlotPublic<unknown>): T`
- throws `CRYPTO_VERIFY_FAILED` on verify failure (BEFORE body decode)
- `verify(bytes: Uint8Array, parentPublicSlot: KeySlotPublic<unknown>): boolean`
- `unpackSelf(bytes: Uint8Array, getEmbeddedSlot: (decoded: T) => KeySlotPublic<unknown>): T`
- `domain: DomainTag`, `slot: SlotId`, `signatureLength: number`
## Implementation Notes
- The order `verify → decode` is load-bearing. A tampered byte string must
never reach the body codec because the codec may have side effects, may
log unexpected payloads, or may be slow. Verify-then-decode is asserted
by a spec-conformance test that injects a tracking codec and checks
`decodeCalls === 0` after a tampered unpack.
- `verify(bytes, slot)` deliberately swallows structural exceptions and
returns `false` so callers can probe candidate parent keys against an
arbitrary byte string without try/catch boilerplate. `unpack(...)` is
the throw-on-failure variant.
- The body codec is held by reference; mutating it after construction
changes future pack/unpack behavior. Treat the codec as immutable.
- `Authority.sign` is async because real backends (HSM, TPM, threshold
signing, network-attached signers) are async. Even the in-memory v1
authority returns a Promise to preserve the contract.
- Signature length is enforced on pack to surface a misbehaving authority
early. The check is identity-checking the `Promise<Uint8Array>.length`,
not the algorithm — a future ECDSA-backed authority signing into an
ed25519-shaped slot would be caught here.
- OTel spans cost essentially nothing under the no-op tracer; they
materialise once a real provider is wired. The spans deliberately do
NOT carry the signature itself (sensitive material) or the body bytes
(potentially sensitive); only metadata: domain, slot, payload_bytes.

View File

@@ -0,0 +1,31 @@
## xxhash32
**Path:** `src/xxhash32.ts`
**Export:** `calculateXxhash32`, `verifyXxhash32`, `packXxhash32`, `unpackXxhash32`, `appendXxhash32`, `verifyAndStripXxhash32`
### What it does
Pure TypeScript implementation of the XXHash32 non-cryptographic hash algorithm (spec v0.8.2). No external dependencies.
### Key API
| Function | Description |
|----------|-------------|
| `calculateXxhash32(data, seed?)` | Compute 32-bit hash of a `Uint8Array` |
| `verifyXxhash32(data, expected, seed?)` | Verify a hash matches |
| `packXxhash32(checksum)` | Pack hash as 4 little-endian bytes |
| `unpackXxhash32(bytes, offset?)` | Unpack 4 bytes to hash value |
| `appendXxhash32(data, seed?)` | Append 4-byte hash to data |
| `verifyAndStripXxhash32(data, seed?)` | Verify and strip appended hash |
### Constants
- `PRIME1` = `0x9e3779b1`
- `PRIME2` = `0x85ebca77`
- `PRIME3` = `0xc2b2ae3d`
- `PRIME4` = `0x27d4eb2f`
- `PRIME5` = `0x165667b1`
### Used by
- Checksum validation in downstream network protocols

View File

@@ -0,0 +1,61 @@
# @suaveplan/bitpacker-delta - Bitpacker delta plugin
## Summary
Delta encoding utilities for efficient repeated state transmission. Detects changes between successive values and encodes differences rather than full values.
<!-- begin:auto-docs -->
## Documentation tree
- [delta-manager](./src/delta-manager/delta-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [delta-manager](./src/delta-manager/delta-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [delta-state](./src/delta-state/delta-state.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [delta-state](./src/delta-state/delta-state.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [index](./src/index.md) — **Path:** `bitpacker-delta/src/index.ts` **Export:** See source for details.
- [string-diff](./src/string-diff.md) — **Path:** `src/string-diff.ts` **Export:** `calculateStringDiff`, `applyStringDiff`, `calculateDiffSavings`, `shouldUse…
- [xor-encoder](./src/xor-encoder.md) — **Path:** `src/xor-encoder.ts` **Export:** `calculateXorDelta`, `applyXorDelta`, `calculateDeltaBitSize`, `isXorEfficie…
<!-- end:auto-docs --> - [delta-manager](./src/delta-manager/delta-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta-state](./src/delta-state/delta-state.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta-state](./src/delta-state/delta-state.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-delta/src/index.ts` **Export:** See source for details.
- [string-diff](./src/string-diff.md) — **Path:** `src/string-diff.ts` **Export:** `calculateStringDiff`, `applyStringDiff`, `calculateDiffSavings`, `shouldUse…
- [xor-encoder](./src/xor-encoder.md) — **Path:** `src/xor-encoder.ts` **Export:** `calculateXorDelta`, `applyXorDelta`, `calculateDeltaBitSize`, `isXorEfficie…
- [delta-manager](./src/delta-manager/delta-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta-state](./src/delta-state/delta-state.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta-state](./src/delta-state/delta-state.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-delta/src/index.ts` **Export:** See source for details.
- [string-diff](./src/string-diff.md) — **Path:** `src/string-diff.ts` **Export:** `calculateStringDiff`, `applyStringDiff`, `calculateDiffSavings`, `shouldUse…
- [xor-encoder](./src/xor-encoder.md) — **Path:** `src/xor-encoder.ts` **Export:** `calculateXorDelta`, `applyXorDelta`, `calculateDeltaBitSize`, `isXorEfficie…
- [delta-manager](./src/delta-manager/delta-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta-state](./src/delta-state/delta-state.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta-state](./src/delta-state/delta-state.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-delta/src/index.ts` **Export:** See source for details.
- [string-diff](./src/string-diff.md) — **Path:** `src/string-diff.ts` **Export:** `calculateStringDiff`, `applyStringDiff`, `calculateDiffSavings`, `shouldUse…
- [xor-encoder](./src/xor-encoder.md) — **Path:** `src/xor-encoder.ts` **Export:** `calculateXorDelta`, `applyXorDelta`, `calculateDeltaBitSize`, `isXorEfficie…
- [delta-manager](./src/delta-manager/delta-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [delta-state](./src/delta-state/delta-state.md) — <!-- begin:auto-docs --> _(no submodules)_ - [delta-state](./src/delta-state/delta-state.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-delta/src/index.ts` **Export:** See source for details.
- [string-diff](./src/string-diff.md) — **Path:** `src/string-diff.ts` **Export:** `calculateStringDiff`, `applyStringDiff`, `calculateDiffSavings`, `shouldUse…
- [xor-encoder](./src/xor-encoder.md) — **Path:** `src/xor-encoder.ts` **Export:** `calculateXorDelta`, `applyXorDelta`, `calculateDeltaBitSize`, `isXorEfficie…
## Install
```bash
bun add @suaveplan/bitpacker-delta
```
## Usage
```typescript
import { DeltaManager, calculateStringDiff, applyStringDiff, numberXor } from "@suaveplan/bitpacker-delta";
// Change detection
const dm = new DeltaManager();
dm.detectChanges({ x: 1, y: 2 }); // returns { x: 1, y: 2 }
dm.detectChanges({ x: 1, y: 2 }); // returns undefined (no change)
dm.detectChanges({ x: 1, y: 3 }); // returns { x: 1, y: 3 }
// String diff
const diff = calculateStringDiff("hello world", "hello there");
// { prefixLen: 6, suffixLen: 0, middleLen: 5, middle: "there" }
const reconstructed = applyStringDiff("hello world", diff);
// "hello there"
// Numeric XOR delta
const delta = numberXor(3.14159, 2.71828);
```
## Modules
- `src/index.ts` — `DeltaManager`, change detection API
- `src/xor-encoder.ts` — XOR-based numeric delta encoding
- `src/string-diff.ts` — Common prefix/suffix string diff encoding

View File

@@ -0,0 +1,123 @@
# DeltaManager
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`DeltaManager` is the field-level delta detector for bit-packed objects. Given a current state and a `BitPackerSchema` describing it, it returns the ordered list of fields whose value has changed since the previous snapshot stored under the same state key. Encoders use the returned `FieldChange[]` to build a fixed-width bitmask and send only the changed fields, dramatically reducing bandwidth for high-frequency state-sync workloads (game replication, multi-peer document sync, telemetry deltas).
This is the port of suaveplan's upstream `core/delta/delta-manager.ts`. hardening replaces upstream's `Date.now()` with injectable `Clock` (CLAUDE.md determinism rule), upstream's `JSON.stringify`-based equality with a structural walker that respects `BitPackerSchema` field order (avoiding subtle false-positive diffs from key-order changes in nested objects), upstream's plain `Error` throws with `BitpackerError` codes, and validates `DeltaConfig` via Zod with strict-mode key rejection.
## Features
- Field-indexed `FieldChange[]` output matching `Object.keys(schema)` order for stable bit offsets
- Per-state-key snapshot store with LRU eviction (delegates to `DeltaStateManager`)
- Configurable `maxFieldCount` cap with `BITPACKER.DELTA_FIELD_LIMIT_EXCEEDED` enforcement
- Structural equality default (matches schema semantics) with opt-in `equalityMode: "json"` for upstream byte-compat
- Zod-validated `DeltaConfig`; unknown keys rejected with `BITPACKER.DELTA_INVALID_CONFIG`
- Threshold-driven `shouldUseDelta(originalBits, deltaBits)` for bandwidth-savings decisions
- Zero `Math.random` / `Date.now` in the implementation — fully deterministic given identical inputs
## Usage: Basic Example
```typescript
import { DeltaManager } from "@suaveplan/bitpacker-delta";
const schema = { x: "u32", y: "u32" } as const;
const dm = new DeltaManager({ mode: "field-level" });
// First call: no previous state, so no changes.
dm.detectChanges({ x: 0, y: 0 }, schema); // → []
dm.storePreviousState({ x: 0, y: 0 });
// Second call: x changed.
dm.detectChanges({ x: 5, y: 0 }, schema);
// → [{ index: 0, key: "x", previousValue: 0, currentValue: 5 }]
```
## Usage: Advanced Example
```typescript
import { DeltaManager } from "@suaveplan/bitpacker-delta";
const schema = {
pos: { x: "f32", y: "f32" },
vel: { x: "f32", y: "f32" },
health: "u8",
} as const;
// Per-peer state streams: one DeltaManager, many state keys.
const dm = new DeltaManager(
{ mode: "field-level", threshold: 25, maxFieldCount: 16 },
{ maxStates: 256 } // 256 simultaneous peers
);
for (const peer of peers) {
// Each peer is its own state stream.
const previous = dm.getPreviousState(peer.id);
const changes = dm.detectChanges(peer.state, schema, peer.id);
// Only send a delta if it saves at least 25% of bits.
const fullSize = bitWidthOf(schema);
const deltaSize = changes.length * 32 + Math.ceil(3 / 8) * 8;
if (dm.shouldUseDelta(fullSize, deltaSize)) {
sendDelta(peer.id, changes);
} else {
sendFull(peer.id, peer.state);
}
dm.storePreviousState(peer.state, peer.id);
}
```
## API Reference
### `new DeltaManager(config?: DeltaConfig, options?: DeltaManagerOptions)`
`config.mode` ∈ `"none" | "xor" | "string-diff" | "field-level"`. Default `"none"`.
`config.threshold` — bandwidth-savings %, 0–100. Default `0`.
`config.maxFieldCount` — max fields the bitmask can address. Default `64`.
`config.stateKey` — default key for `detectChanges` calls without one. Default `"default"`.
`config.equalityMode` — `"structural"` (default) or `"json"` (upstream-compat).
`options.maxStates` — capacity of the underlying state store. Default `100`.
Throws `BitpackerError(BITPACKER.DELTA_INVALID_CONFIG)` on invalid config.
### `detectChanges(current, schema, stateKey?) → FieldChange[]`
Returns the ordered list of fields whose `current` value differs from the stored snapshot under `stateKey`. First call (no snapshot) returns `[]`. Schema with more keys than `maxFieldCount` throws `BITPACKER.DELTA_FIELD_LIMIT_EXCEEDED`.
### `storePreviousState(state, stateKey?) → void`
Stores a deep-cloned snapshot of `state` for the next diff.
### `getPreviousState(stateKey?) → unknown | undefined`
### `hasPreviousState(stateKey?) → boolean`
Snapshot inspection helpers.
### `calculateSavings(originalBits, deltaBits) → number`
### `shouldUseDelta(originalBits, deltaBits) → boolean`
Bandwidth-savings calculation against the configured `threshold`.
### `clear() → void`
### `clearState(stateKey) → void`
### `getConfig() → Readonly<Required<DeltaConfig>>`
Lifecycle and inspection helpers.
## Implementation Notes
The structural-equality walker treats every `ArrayBuffer.isView` value (every typed-array variant) as a byte-by-byte comparison. `Date` instances compare via `getTime()`. Plain objects compare key-sets first (size + key membership), then recurse on values. Arrays compare length-then-elements. This matches `BitPackerSchema` semantics: two values that pack to identical bytes are equal.
The `"json"` equality mode is provided for upstream byte-compat but has known limitations: `JSON.stringify({ a: 1, b: 2 })` and `JSON.stringify({ b: 2, a: 1 })` produce different strings even though the values are semantically equal in a `BitPackerSchema` context. Consumers porting from upstream should switch to `"structural"` once they've verified no callers depend on key-order-sensitive equality.
The `maxFieldCount` cap is a defence: a hostile peer that sends a schema with thousands of fields could exhaust memory if the bitmask were unbounded. The default (64) accommodates typical state-sync workloads; consumers with larger schemas raise it explicitly.
Threshold semantics: `shouldUseDelta` returns `true` when `(originalBits - deltaBits) / originalBits * 100 >= threshold`. A `threshold` of `0` means "always prefer delta when smaller"; a `threshold` of `25` means "only prefer delta if it saves at least a quarter of the bits."

View File

@@ -0,0 +1,90 @@
# DeltaStateManager
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`DeltaStateManager` keeps deep-cloned snapshots of previous values per state key, so that a delta encoder can diff the current value against its predecessor without worrying about caller-side mutations. It is the storage layer for `DeltaManager`; consumers usually go through `DeltaManager` rather than calling this class directly.
The store is bounded: an LRU eviction policy drops the oldest key once the configured capacity is reached. Default capacity is 100 keys, which is plenty for most workloads (one key per per-peer state stream). Re-storing under an existing key updates the value in place without advancing its LRU position — this matches upstream semantics so that frequently-updated keys do not unfairly evict rarely-updated ones.
## Features
- Deep-cloning on store: `Date`, all typed-array variants (`Uint8Array`, `Int8Array`, `Uint16Array`, `Int16Array`, `Uint32Array`, `Int32Array`, `Float32Array`, `Float64Array`), arrays, and plain objects are all isolated from caller mutations.
- Insertion-order LRU eviction; `maxStates` configurable (default 100).
- `clear()`, `clearState(key)`, `getStateCount()`, `getMaxStates()` accessors for inspection and reset.
- Zero `Math.random` / `Date.now` calls — fully deterministic given identical inputs.
## Usage: Basic Example
```typescript
import { DeltaStateManager } from "@suaveplan/bitpacker-delta";
const states = new DeltaStateManager(100);
states.storePreviousState("player-1", { x: 0, y: 0 });
const previous = states.getPreviousState("player-1");
// → { x: 0, y: 0 }
```
## Usage: Advanced Example
```typescript
import { DeltaStateManager } from "@suaveplan/bitpacker-delta";
const states = new DeltaStateManager(3);
states.storePreviousState("a", { v: 1 });
states.storePreviousState("b", { v: 2 });
states.storePreviousState("c", { v: 3 });
states.storePreviousState("d", { v: 4 }); // evicts "a"
states.hasPreviousState("a"); // → false (evicted)
states.hasPreviousState("d"); // → true
// Mutating the original after storing does NOT touch the snapshot:
const original = { x: 0 };
states.storePreviousState("e", original);
original.x = 999;
states.getPreviousState("e"); // → { x: 0 }
```
## API Reference
### `new DeltaStateManager(maxStates?: number)`
- `maxStates` — positive integer; default `100`. Throws `RangeError` if non-integer or `< 1`.
### `storePreviousState(key: string, state: unknown): void`
Stores a deep-cloned copy of `state` under `key`. If `key` already exists, the value is updated in place; LRU position is unchanged. If the store is at capacity and this is a new key, the oldest key is evicted.
### `getPreviousState(key: string): unknown | undefined`
Returns the stored snapshot, or `undefined` if the key has never been stored or has been evicted.
### `hasPreviousState(key: string): boolean`
True if a snapshot exists for `key`.
### `clear(): void`
Drops every snapshot.
### `clearState(key: string): void`
Drops the snapshot for one key. No-op if absent.
### `getStateCount(): number`
Number of stored snapshots.
### `getMaxStates(): number`
Configured maximum capacity.
## Implementation Notes
The deep-clone walker handles the eight typed-array variants explicitly because the generic object-clone path (`Object.hasOwn` over keys) does not preserve `Uint8Array`-vs-plain-array distinction. Maps and Sets are intentionally NOT cloned: delta-state semantics assume keys are strings and values are plain data; if a consumer needs `Map`/`Set`, they should canonicalise to a plain object first.
Performance: insertion is O(1) amortised; eviction is O(1) (Array `.shift()` on a small bounded array). Lookup is O(1). The `stateKeys` array tracks insertion order without using a `Map`'s native iteration (some legacy environments don't preserve insertion order — though every modern runtime does — so the explicit array is defensive).

View File

@@ -0,0 +1,8 @@
## index
**Path:** `bitpacker-delta/src/index.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,26 @@
## String Diff
**Path:** `src/string-diff.ts`
**Export:** `calculateStringDiff`, `applyStringDiff`, `calculateDiffSavings`, `shouldUseDiff`, `StringDiff`
### What it does
Efficiently encodes string changes by finding common prefix and suffix, only encoding the differing middle section.
### Key API
| Function | Description |
|----------|-------------|
| `calculateStringDiff(previous, current)` | Compute `{ prefixLen, suffixLen, middleLen, middle }` |
| `applyStringDiff(previous, diff)` | Reconstruct current string from diff |
| `calculateDiffSavings(previous, current, diff)` | Percentage savings from diff encoding |
| `shouldUseDiff(previous, current, threshold?)` | Decide whether diff encoding is worthwhile |
### Format
`[prefixLen][suffixLen][middleLen][middleChars...]` — each length field is 1 byte.
### Used by
- Delta compression for string fields in game state
- Network protocols with repetitive string data

View File

@@ -0,0 +1,23 @@
## XOR Encoder
**Path:** `src/xor-encoder.ts`
**Export:** `calculateXorDelta`, `applyXorDelta`, `calculateDeltaBitSize`, `isXorEfficient`, `numberXor`
### What it does
XOR-based delta encoding for numeric values. Encodes the difference between values rather than absolute values, which can save space when consecutive values are similar.
### Key API
| Function | Description |
|----------|-------------|
| `numberXor(a, b)` | XOR two float64 values byte-by-byte |
| `calculateXorDelta(current, previous)` | Compute `current ^ previous` |
| `applyXorDelta(previous, delta)` | Reconstruct `current` from `previous ^ delta` |
| `calculateDeltaBitSize(delta)` | Count significant bits in delta |
| `isXorEfficient(current, previous, maxBits)` | Determine if XOR saves space |
### Used by
- `@suaveplan/bitpacker-delta` `DeltaManager` for change detection
- Downstream compression pipelines

View File

@@ -0,0 +1,145 @@
# @suaveplan/bitpacker-game - Bitpacker game plugin
## Summary
Game state utilities: quantization, prediction, interpolation, and client-server reconciliation.
<!-- begin:auto-docs -->
## Documentation tree
- [bitpacker](./src/bitpacker/bitpacker.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bitpacker](./src/bitpacker/bitpacker.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [integration-manager](./src/integration-manager/integration-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [integration-manager](./src/integration-manager/integration-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [interpolation](./src/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [index](./src/interpolation/index.md) — **Path:** `src/interpolation/interpolation.ts` **Export:** `GameInterpolator`
- [interpolation](./src/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [prediction](./src/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - rotation-predictor — **Path:** `bitpacker-game/src/…
- [index](./src/prediction/index.md) — **Path:** `src/prediction/` **Exports:** `PositionPredictor`, `RotationPredictor`, motion math, Kalman filter, predicti…
- [kalman-filter](./src/prediction/kalman-filter.md) — **Path:** `bitpacker-game/src/prediction/kalman-filter.ts` **Export:** See source for details.
- [motion-math](./src/prediction/motion-math.md) — **Path:** `bitpacker-game/src/prediction/motion-math.ts` **Export:** See source for details.
- [position-predictor](./src/prediction/position-predictor.md) — **Path:** `bitpacker-game/src/prediction/position-predictor.ts` **Export:** See source for details.
- [prediction-algorithms](./src/prediction/prediction-algorithms.md) — **Path:** `bitpacker-game/src/prediction/prediction-algorithms.ts` **Export:** See source for details.
- [prediction](./src/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - rotation-predictor — **Path:** `bitpacker-game/src/…
- [rotation-predictor](./src/prediction/rotation-predictor.md) — **Path:** `bitpacker-game/src/prediction/rotation-predictor.ts` **Export:** See source for details.
- [quantization](./src/quantization/quantization.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [quantization](./src/quantization/quantization.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [reconciliation](./src/reconciliation/reconciliation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [index](./src/reconciliation/index.md) — **Path:** `src/reconciliation/reconciliation.ts` **Export:** `StateReconciliation`
- [reconciliation](./src/reconciliation/reconciliation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [register-schemas](./src/register-schemas/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-game` package. Exposes `register…
- [register-schemas](./src/register-schemas/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-game` package. Exposes `register…
- [schema-factory](./src/schema-factory/schema-factory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schema-factory](./src/schema-factory/schema-factory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state-manager](./src/state-manager/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [state-manager](./src/state-manager/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types.schema](./src/types.schema/types.schema.md) — Zod schemas for the public configuration types of `@suaveplan/bitpacker-game`: the three sub-options bundles (`Predicti…
- [types.schema](./src/types.schema/types.schema.md) — Zod schemas for the public configuration types of `@suaveplan/bitpacker-game`: the three sub-options bundles (`Predicti…
- [index](./src/index.md) — **Path:** `bitpacker-game/src/index.ts` **Export:** See source for details.
- [types](./src/types.md) — **Path:** `src/types.ts` **Export:** All game-related type definitions and enums
<!-- end:auto-docs --> - [bitpacker](./src/bitpacker/bitpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [integration-manager](./src/integration-manager/integration-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [integration-manager](./src/integration-manager/integration-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/interpolation/index.md) — **Path:** `src/interpolation/interpolation.ts` **Export:** `GameInterpolator`
- [interpolation](./src/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- rotation-predictor — **Path:** `bitpacker-game/src/…
- [index](./src/prediction/index.md) — **Path:** `src/prediction/` **Exports:** `PositionPredictor`, `RotationPredictor`, motion math, Kalman filter, predicti…
- [kalman-filter](./src/prediction/kalman-filter.md) — **Path:** `bitpacker-game/src/prediction/kalman-filter.ts` **Export:** See source for details.
- [motion-math](./src/prediction/motion-math.md) — **Path:** `bitpacker-game/src/prediction/motion-math.ts` **Export:** See source for details.
- [position-predictor](./src/prediction/position-predictor.md) — **Path:** `bitpacker-game/src/prediction/position-predictor.ts` **Export:** See source for details.
- [prediction-algorithms](./src/prediction/prediction-algorithms.md) — **Path:** `bitpacker-game/src/prediction/prediction-algorithms.ts` **Export:** See source for details.
- [prediction](./src/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- rotation-predictor — **Path:** `bitpacker-game/src/…
- [rotation-predictor](./src/prediction/rotation-predictor.md) — **Path:** `bitpacker-game/src/prediction/rotation-predictor.ts` **Export:** See source for details.
- [quantization](./src/quantization/quantization.md) — <!-- begin:auto-docs --> _(no submodules)_- [quantization](./src/quantization/quantization.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation](./src/reconciliation/reconciliation.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/reconciliation/index.md) — **Path:** `src/reconciliation/reconciliation.ts` **Export:** `StateReconciliation`
- [reconciliation](./src/reconciliation/reconciliation.md) — <!-- begin:auto-docs --> _(no submodules)_- [schema-factory](./src/schema-factory/schema-factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [schema-factory](./src/schema-factory/schema-factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-manager](./src/state-manager/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-manager](./src/state-manager/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-game/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-game` package. Exposes `register…
- [types](./src/types.md) — **Path:** `src/types.ts` **Export:** All game-related type definitions and enums
- [types.schema](./src/types.schema.md) — Zod schemas for the public configuration types of `@suaveplan/bitpacker-game`: the three sub-options bundles (`Predicti…
- [bitpacker](./src/bitpacker/bitpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [integration-manager](./src/integration-manager/integration-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [integration-manager](./src/integration-manager/integration-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/interpolation/index.md) — **Path:** `src/interpolation/interpolation.ts` **Export:** `GameInterpolator`
- [interpolation](./src/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- rotation-predictor — **Path:** `bitpacker-game/src/…
- [index](./src/prediction/index.md) — **Path:** `src/prediction/` **Exports:** `PositionPredictor`, `RotationPredictor`, motion math, Kalman filter, predicti…
- [kalman-filter](./src/prediction/kalman-filter.md) — **Path:** `bitpacker-game/src/prediction/kalman-filter.ts` **Export:** See source for details.
- [motion-math](./src/prediction/motion-math.md) — **Path:** `bitpacker-game/src/prediction/motion-math.ts` **Export:** See source for details.
- [position-predictor](./src/prediction/position-predictor.md) — **Path:** `bitpacker-game/src/prediction/position-predictor.ts` **Export:** See source for details.
- [prediction-algorithms](./src/prediction/prediction-algorithms.md) — **Path:** `bitpacker-game/src/prediction/prediction-algorithms.ts` **Export:** See source for details.
- [prediction](./src/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- rotation-predictor — **Path:** `bitpacker-game/src/…
- [rotation-predictor](./src/prediction/rotation-predictor.md) — **Path:** `bitpacker-game/src/prediction/rotation-predictor.ts` **Export:** See source for details.
- [quantization](./src/quantization/quantization.md) — <!-- begin:auto-docs --> _(no submodules)_- [quantization](./src/quantization/quantization.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation](./src/reconciliation/reconciliation.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/reconciliation/index.md) — **Path:** `src/reconciliation/reconciliation.ts` **Export:** `StateReconciliation`
- [reconciliation](./src/reconciliation/reconciliation.md) — <!-- begin:auto-docs --> _(no submodules)_- [schema-factory](./src/schema-factory/schema-factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [schema-factory](./src/schema-factory/schema-factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-manager](./src/state-manager/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-manager](./src/state-manager/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-game/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-game` package. Exposes `register…
- [types](./src/types.md) — **Path:** `src/types.ts` **Export:** All game-related type definitions and enums
- [types.schema](./src/types.schema.md) — Zod schemas for the public configuration types of `@suaveplan/bitpacker-game`: the three sub-options bundles (`Predicti…
- [bitpacker](./src/bitpacker/bitpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [integration-manager](./src/integration-manager/integration-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [integration-manager](./src/integration-manager/integration-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/interpolation/index.md) — **Path:** `src/interpolation/interpolation.ts` **Export:** `GameInterpolator`
- [interpolation](./src/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- rotation-predictor — **Path:** `bitpacker-game/src/…
- [index](./src/prediction/index.md) — **Path:** `src/prediction/` **Exports:** `PositionPredictor`, `RotationPredictor`, motion math, Kalman filter, predicti…
- [kalman-filter](./src/prediction/kalman-filter.md) — **Path:** `bitpacker-game/src/prediction/kalman-filter.ts` **Export:** See source for details.
- [motion-math](./src/prediction/motion-math.md) — **Path:** `bitpacker-game/src/prediction/motion-math.ts` **Export:** See source for details.
- [position-predictor](./src/prediction/position-predictor.md) — **Path:** `bitpacker-game/src/prediction/position-predictor.ts` **Export:** See source for details.
- [prediction-algorithms](./src/prediction/prediction-algorithms.md) — **Path:** `bitpacker-game/src/prediction/prediction-algorithms.ts` **Export:** See source for details.
- [prediction](./src/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_- rotation-predictor — **Path:** `bitpacker-game/src/…
- [rotation-predictor](./src/prediction/rotation-predictor.md) — **Path:** `bitpacker-game/src/prediction/rotation-predictor.ts` **Export:** See source for details.
- [quantization](./src/quantization/quantization.md) — <!-- begin:auto-docs --> _(no submodules)_- [quantization](./src/quantization/quantization.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation](./src/reconciliation/reconciliation.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/reconciliation/index.md) — **Path:** `src/reconciliation/reconciliation.ts` **Export:** `StateReconciliation`
- [reconciliation](./src/reconciliation/reconciliation.md) — <!-- begin:auto-docs --> _(no submodules)_- [schema-factory](./src/schema-factory/schema-factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [schema-factory](./src/schema-factory/schema-factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-manager](./src/state-manager/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-manager](./src/state-manager/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-game/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-game` package. Exposes `register…
- [types](./src/types.md) — **Path:** `src/types.ts` **Export:** All game-related type definitions and enums
- [types.schema](./src/types.schema.md) — Zod schemas for the public configuration types of `@suaveplan/bitpacker-game`: the three sub-options bundles (`Predicti…
- [bitpacker](./src/bitpacker/bitpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [integration-manager](./src/integration-manager/integration-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ - [integration-manager](./src/integration-manager/integration-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [interpolation](./src/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_ - [index](./src/interpolation/index.md) — **Path:** `src/interpolation/interpolation.ts` **Export:** `GameInterpolator`
- [interpolation](./src/interpolation/interpolation.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ - [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [prediction](./src/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_ - [index](./src/prediction/index.md) — **Path:** `src/prediction/` **Exports:** `PositionPredictor`, `RotationPredictor`, motion math, Kalman filter, predicti…
- [kalman-filter](./src/prediction/kalman-filter.md) — **Path:** `bitpacker-game/src/prediction/kalman-filter.ts` **Export:** See source for details.
- [motion-math](./src/prediction/motion-math.md) — **Path:** `bitpacker-game/src/prediction/motion-math.ts` **Export:** See source for details.
- [position-predictor](./src/prediction/position-predictor.md) — **Path:** `bitpacker-game/src/prediction/position-predictor.ts` **Export:** See source for details.
- [prediction-algorithms](./src/prediction/prediction-algorithms.md) — **Path:** `bitpacker-game/src/prediction/prediction-algorithms.ts` **Export:** See source for details.
- [prediction](./src/prediction/prediction.md) — <!-- begin:auto-docs --> _(no submodules)_ - [rotation-predictor](./src/prediction/rotation-predictor.md) — **Path:** `bitpacker-game/src/prediction/rotation-predictor.ts` **Export:** See source for details.
- [quantization](./src/quantization/quantization.md) — <!-- begin:auto-docs --> _(no submodules)_ - [quantization](./src/quantization/quantization.md) — <!-- begin:auto-docs --> _(no submodules)_- [reconciliation](./src/reconciliation/reconciliation.md) — <!-- begin:auto-docs --> _(no submodules)_ - [index](./src/reconciliation/index.md) — **Path:** `src/reconciliation/reconciliation.ts` **Export:** `StateReconciliation`
- [reconciliation](./src/reconciliation/reconciliation.md) — <!-- begin:auto-docs --> _(no submodules)_- [schema-factory](./src/schema-factory/schema-factory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schema-factory](./src/schema-factory/schema-factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [state-manager](./src/state-manager/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_ - [state-manager](./src/state-manager/state-manager.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-game/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-game` package. Exposes `register…
- [types](./src/types.md) — **Path:** `src/types.ts` **Export:** All game-related type definitions and enums
- [types.schema](./src/types.schema.md) — Zod schemas for the public configuration types of `@suaveplan/bitpacker-game`: the three sub-options bundles (`Predicti…
## Install
```bash
bun add @suaveplan/bitpacker-game
```
## Usage
```typescript
import {
GameStateManager,
quantizeVector,
dequantizeVector,
PositionPredictor,
RotationPredictor,
GameInterpolator,
StateReconciliation,
PredictionAlgorithm,
InterpolationMethod,
} from "@suaveplan/bitpacker-game";
// Quantization for network compression
const quantized = quantizeVector({ x: 0.5, y: -0.25, z: 0.75 }, 16);
const restored = dequantizeVector(quantized, 16);
// Position prediction
const predictor = new PositionPredictor({ algorithm: PredictionAlgorithm.ADAPTIVE });
predictor.addPosition({ x: 0, y: 0, z: 0 }, 0);
predictor.addPosition({ x: 10, y: 0, z: 0 }, 1000);
const futurePos = predictor.predictPosition(100); // predicted position at t=100ms
// Interpolation
const interp = new GameInterpolator({ method: InterpolationMethod.HERMITE });
const midPos = interp.interpolatePosition({ x: 0, y: 0, z: 0 }, { x: 10, y: 0, z: 0 }, 0.5);
// Client-server reconciliation
const reconciliation = new StateReconciliation();
const seq = reconciliation.processInput({ moveX: 1, moveY: 0, rotateYaw: 0, rotatePitch: 0, actions: [] }, Date.now());
```
## Modules
- `src/index.ts` — `GameStateManager`, `quantizeVector`, `dequantizeVector`
- `src/types.ts` — `Vector3`, `Rotation`, `PlayerState`, `PredictionAlgorithm`, `InterpolationMethod`, etc.
- `src/prediction/` — `PositionPredictor`, `RotationPredictor`, motion math, Kalman filter, prediction algorithms
- `src/interpolation/` — `GameInterpolator` with 9 interpolation methods
- `src/reconciliation/` — `StateReconciliation` for client prediction + server reconciliation

View File

@@ -0,0 +1,82 @@
# GameBitPacker
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`GameBitPacker` is the top-level facade for the `@suaveplan/bitpacker-game` package: a `BitPacker` subclass that composes prediction, interpolation, reconciliation, network simulation, and quantization helpers into one game-ready binary serializer. Most consumers construct one `GameBitPacker` per game session and route every state pack/unpack through it; the four subsystems are wired up internally based on the constructor's `enable*` flags so a consumer doesn't have to chain managers manually.
The class is generic on the schema type (`T extends BitPackerSchema`), so a game's player schema, world schema, and input schema can each have their own typed `GameBitPacker<PlayerSchema>` / `GameBitPacker<WorldSchema>` instance. Internally the class delegates to `GameStateManager` for state ownership, `GameIntegrationManager` for the cross-subsystem orchestration, and the `GameQuantization` namespace for vector/rotation rounding.
## Features
- Inherits everything from `BitPacker` (pack, unpack, schema registry, strategy chain)
- `enablePrediction` / `enableInterpolation` / `enableReconciliation` / `enableNetworkSimulation` toggles
- Composes `PositionPredictor` + `RotationPredictor` + `GameInterpolator` + `StateReconciliation` + `NetworkSimulator`
- Quantization helpers for position, rotation, and 2-D / 3-D vectors via the `GameQuantization` namespace
- Input/state sequence tracking via `GameStateManager` (4-bit sequence wraps at `MAX_SEQUENCE = 15`)
## Usage: Basic Example
```ts
import { GameBitPacker, PLAYER_STATE_SCHEMA } from "@suaveplan/bitpacker-game";
const bp = new GameBitPacker(PLAYER_STATE_SCHEMA, {
enablePrediction: true,
enableInterpolation: true,
});
const packed = bp.pack(playerState);
const restored = bp.unpack(packed);
```
## Usage: Advanced Example
```ts
import {
GameBitPacker,
PLAYER_STATE_SCHEMA,
PredictionAlgorithm,
} from "@suaveplan/bitpacker-game";
const bp = new GameBitPacker(PLAYER_STATE_SCHEMA, {
enablePrediction: true,
enableInterpolation: true,
enableReconciliation: true,
enableNetworkSimulation: true,
predictionOptions: {
algorithm: PredictionAlgorithm.KALMAN,
historySize: 8,
},
reconciliationOptions: {
inputBufferSize: 60,
stateBufferSize: 120,
enableExtrapolation: true,
},
networkSimulationOptions: {
latencyMin: 30,
latencyMax: 80,
packetLoss: 1,
},
});
bp.pushState(serverFrame);
bp.applyPhysics((state, input, dt) => /* simulate */ state, currentInput, 1 / 60);
const interpolated = bp.interpolate(prev, next, 0.5);
```
## API Reference
### `class GameBitPacker<T extends BitPackerSchema> extends BitPacker<T>`
Constructed with `new GameBitPacker<T>(schema, options?)`. `options` defaults to `DEFAULT_GAME_BITPACKER_OPTIONS` (every subsystem disabled). Every method inherited from `BitPacker` (`pack`, `unpack`, `registerSchema`, `registerStrategy`) works without modification.
Subsystem-specific methods include `pushState`, `getLatestState`, `applyPhysics`, `predictPosition`, `interpolate`, `reconcile`, and `quantizePosition` — see the corresponding subsystem `.md` files for detailed semantics.
## Implementation Notes
- **Composition over inheritance for subsystems.** Although `GameBitPacker extends BitPacker`, the four subsystems (`Predictor`, `Interpolator`, `Reconciliation`, `NetworkSimulator`) are composed members, not inherited classes. This keeps each subsystem testable in isolation and lets a consumer construct a `GameBitPacker` with only the subsystems they need.
- **`enable*` flags gate construction, not pack/unpack.** Disabled subsystems are not constructed at all (saves memory in lightweight server pipelines that just pack/unpack); their methods throw if called when the corresponding flag is `false`.
- **No global state.** Every internal manager is instance-scoped — multiple `GameBitPacker` instances coexist without cross-talk.
- **Sequence wrapping at 4 bits.** `GameStateManager.MAX_SEQUENCE = 15` enforces the 4-bit wire-format constraint inherited from upstream. Consumers serializing sequence numbers across the wire must respect this cap.

View File

@@ -0,0 +1,8 @@
## index
**Path:** `bitpacker-game/src/index.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,77 @@
# GameIntegrationManager
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Cross-subsystem orchestration for the `GameBitPacker`. `GameIntegrationManager<T>` owns the four optional subsystems (`PositionPredictor`, `RotationPredictor`, `GameInterpolator`, `StateReconciliation`, `NetworkSimulator`) and routes calls between them so the `GameBitPacker` facade exposes a single high-level API instead of a five-way junction. Constructing a manager with `enable*: false` flags omits the corresponding subsystem entirely — the manager allocates only what's enabled.
The integration manager is the right home for cross-subsystem invariants: e.g. when a reconciliation rollback fires, the predictor's history needs to be rewound by the same number of frames, and the interpolator's smoothing buffer needs to be flushed. Centralizing those rules here keeps each subsystem free of cross-knowledge.
## Features
- Optional construction of `PositionPredictor`, `RotationPredictor`, `GameInterpolator`, `StateReconciliation`, `NetworkSimulator`
- Routes `applyPhysics`, `predictPosition`, `interpolate`, `reconcile`, `simulateSend`, `simulateReceive` to the corresponding subsystem
- Flushes / rewinds dependent subsystems atomically on reconciliation rollback
- Exposes `getNetworkStats(): NetworkSimulationStats` to surface aggregate counters
- All subsystems are accessible via their typed getters for advanced use
## Usage: Basic Example
```ts
import { GameIntegrationManager, PLAYER_STATE_SCHEMA } from "@suaveplan/bitpacker-game";
const mgr = new GameIntegrationManager(PLAYER_STATE_SCHEMA, {
enablePrediction: true,
enableInterpolation: true,
});
const predicted = mgr.predictPosition(currentPos, /*dt*/ 16);
const smooth = mgr.interpolate(prev, next, 0.5);
```
## Usage: Advanced Example
```ts
import {
GameIntegrationManager,
PLAYER_STATE_SCHEMA,
PredictionAlgorithm,
PacketStatus,
} from "@suaveplan/bitpacker-game";
const mgr = new GameIntegrationManager(PLAYER_STATE_SCHEMA, {
enablePrediction: true,
enableReconciliation: true,
enableNetworkSimulation: true,
predictionOptions: { algorithm: PredictionAlgorithm.KALMAN, historySize: 8 },
networkSimulationOptions: { latencyMin: 50, latencyMax: 100, packetLoss: 2 },
});
const send = mgr.simulateSend(payload);
if (send.status === PacketStatus.DELIVERED) {
/* …route through pipeline… */
}
const stats = mgr.getNetworkStats();
console.log(stats.packetLossRate, stats.averageLatency);
```
## API Reference
### `class GameIntegrationManager<T extends BitPackerSchema>`
Constructor takes the schema and a `GameBitPackerOptions` bundle. Subsystems are allocated based on the `enable*` flags.
#### Routing methods
- `predictPosition(current, dt)` / `predictRotation(current, dt)` — delegate to the corresponding predictor; throw if prediction is disabled
- `interpolate(a, b, t)` — delegate to the interpolator
- `reconcile(serverState, clientHistory)` — delegate to the reconciliation manager and atomically flush dependent subsystems
- `simulateSend(payload)` / `simulateReceive(payload)` — delegate to the network simulator and emit OTel events on the parent span
- `getNetworkStats()` — return the simulator's aggregate counters
## Implementation Notes
- **Atomic rollback semantics.** When `reconcile` performs a rollback, the predictor's history is truncated to the rollback frame and the interpolator's smoothing buffer is flushed in the same call. Splitting these into separate calls would create a window where the predictor and interpolator disagree on the canonical timeline.
- **Disabled subsystems throw, don't no-op.** Calling `predictPosition` with `enablePrediction: false` throws rather than returning a stub value. Silently no-op'ing would mask configuration bugs at the call site.
- **NetworkSimulator events.** `simulateSend` / `simulateReceive` are the only routing methods that produce per-packet OTel events on the parent `bitpacker_game.send` / `bitpacker_game.receive` spans. The other subsystems are too high-frequency to instrument per-call.

View File

@@ -0,0 +1,39 @@
## Interpolation
**Path:** `src/interpolation/interpolation.ts`
**Export:** `GameInterpolator`
### What it does
Advanced interpolation of game state between two points in time. Supports 9 interpolation methods for positions and rotations.
### Methods
| Method | Description |
|--------|-------------|
| `LINEAR` | Simple lerp |
| `CUBIC` | Cubic easing |
| `HERMITE` | Hermite spline with tension/bias |
| `CATMULL_ROM` | Catmull-Rom spline |
| `BEZIER` | Cubic Bezier |
| `SPRING` | Spring physics |
| `ELASTIC` | Elastic easing |
| `BOUNCE` | Bounce easing |
| `SMOOTHSTEP` | 3t² - 2t³ smoothstep |
### Key API
| Method | Description |
|--------|-------------|
| `interpolatePosition(start, end, t, method?)` | Interpolate `Vector3` |
| `interpolateRotation(start, end, t, method?)` | Interpolate `Rotation` with angle wrapping |
| `interpolatePlayerState(start, end, t)` | Full player state interpolation |
| `interpolatePath(points, t, method?)` | Interpolate along a path of points |
### Rotation Handling
Yaw is normalized to `[-180, 180]` and takes the shortest path. Pitch is clamped to `[-90, 90]`.
### Depends on
- `../types.js` — `Vector3`, `Rotation`, `PlayerState`, `InterpolationOptions`

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## interpolation
**Path:** `bitpacker-game/src/interpolation/interpolation.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,94 @@
# Bitpacker Game OTel Instruments
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
OTel meter, tracer, counter, and histogram declarations for `@suaveplan/bitpacker-game`. Ships canonical instrumentation primitives for the `GameBitPacker`'s pack/unpack boundaries plus the three sub-systems (`prediction`, `interpolation`, `reconciliation`). The instruments file is the canonical source of truth for the package's metric and span vocabulary so consumers wiring a game pipeline can attach observability without re-deriving names.
The metric and span names follow the suaveplan convention: tracer/meter named `@suaveplan/bitpacker-game`; spans named `bitpacker_game.<operation>` (`pack`, `unpack`, `predict`, `interpolate`, `reconcile`); counters named `bitpacker_game.<scope>.<metric>`. This shape matches the rest of the bitpacker family.
## Features
- `getBitPackerGameTracer()` — returns the package's tracer
- `gameBitPackerOpsTotal` — counter labelled by `op` and `success`
- `gameBitPackerOpDurationMs` — histogram of pack/unpack duration
- `gameSubsystemOpsTotal` — counter labelled by `subsystem` and `success`
- `BITPACKER_GAME_SPAN_NAMES` — five canonical span names
## Usage: Basic Example
```ts
import {
BITPACKER_GAME_SPAN_NAMES,
gameBitPackerOpsTotal,
getBitPackerGameTracer,
} from "@suaveplan/bitpacker-game";
const tracer = getBitPackerGameTracer();
tracer.startActiveSpan(BITPACKER_GAME_SPAN_NAMES.pack, (span) => {
try {
// pack work
gameBitPackerOpsTotal.add(1, { op: "pack", success: "true" });
} finally {
span.end();
}
});
```
## Usage: Advanced Example
```ts
import {
BITPACKER_GAME_SPAN_NAMES,
gameSubsystemOpsTotal,
getBitPackerGameTracer,
} from "@suaveplan/bitpacker-game";
function runPredict(state: unknown) {
const tracer = getBitPackerGameTracer();
return tracer.startActiveSpan(
BITPACKER_GAME_SPAN_NAMES.predict,
(span) => {
try {
// predict
gameSubsystemOpsTotal.add(1, {
subsystem: "prediction",
success: "true",
});
return state;
} finally {
span.end();
}
}
);
}
```
## API Reference
### `getBitPackerGameTracer(): Tracer`
Returns the OTel `Tracer` instance for the package.
### `gameBitPackerOpsTotal: Counter`
Counter labelled by `op: "pack" | "unpack"` and `success: "true" | "false"`.
### `gameBitPackerOpDurationMs: Histogram`
Histogram of elapsed milliseconds per pack/unpack call, labelled by `op`.
### `gameSubsystemOpsTotal: Counter`
Counter labelled by `subsystem: "prediction" | "interpolation" | "reconciliation"` and `success`.
### `BITPACKER_GAME_SPAN_NAMES`
Const map: `{ pack, unpack, predict, interpolate, reconcile }` → canonical span names.
## Implementation Notes
- **Five sub-spans, one parent.** The pack/unpack span is the parent; predict/interpolate/reconcile spans are children when called from inside the pack/unpack hot path. Standalone calls produce a top-level span of the corresponding name.
- **No per-frame spans.** The game subsystems run at frame rate on real-time pipelines; one span per frame would explode trace volume. The counters fire per frame; the spans are reserved for boundary entries.

View File

@@ -0,0 +1,39 @@
## Prediction
**Path:** `src/prediction/`
**Exports:** `PositionPredictor`, `RotationPredictor`, motion math, Kalman filter, prediction algorithms
### What it does
Position and rotation prediction for game state extrapolation. Supports multiple algorithms from simple linear extrapolation to Kalman filtering.
### Key Classes
| Class | Description |
|-------|-------------|
| `PositionPredictor` | Predicts future positions from historical data |
| `RotationPredictor` | Predicts future rotations from historical data |
### Algorithms
| Algorithm | Description |
|-----------|-------------|
| `linearPredict` | Constant velocity extrapolation |
| `quadraticPredict` | Velocity + acceleration |
| `cubicPredict` | Velocity + acceleration + jerk |
| `hermitePredict` | Hermite spline extrapolation |
| `catmullRomPredict` | Catmull-Rom spline extrapolation |
| `verletPredict` | Physics-based Verlet integration |
| `kalmanPredict` | Kalman filter prediction |
### Adaptive Mode
`PositionPredictor` with `algorithm: ADAPTIVE` automatically switches algorithms based on prediction error:
- Error > 2× threshold → Kalman
- Error > threshold → Verlet
- Otherwise → Cubic
### Depends on
- `../types.js` — `Vector3`, `Rotation`, `PredictionOptions`, `KalmanState`

View File

@@ -0,0 +1,8 @@
## kalman-filter
**Path:** `bitpacker-game/src/prediction/kalman-filter.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,8 @@
## motion-math
**Path:** `bitpacker-game/src/prediction/motion-math.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,8 @@
## position-predictor
**Path:** `bitpacker-game/src/prediction/position-predictor.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,8 @@
## prediction-algorithms
**Path:** `bitpacker-game/src/prediction/prediction-algorithms.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,80 @@
# Prediction Subsystem
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - rotation-predictor — **Path:** `bitpacker-game/src/…
- [rotation-predictor](./rotation-predictor.md) — **Path:** `bitpacker-game/src/prediction/rotation-predictor.ts` **Export:** See source for details.
- rotation-predictor — **Path:** `bitpacker-game/src/…
- [rotation-predictor](./rotation-predictor.md) — **Path:** `bitpacker-game/src/prediction/rotation-predictor.ts` **Export:** See source for details.
- rotation-predictor — **Path:** `bitpacker-game/src/…
- [rotation-predictor](./rotation-predictor.md) — **Path:** `bitpacker-game/src/prediction/rotation-predictor.ts` **Export:** See source for details.
- rotation-predictor — **Path:** `bitpacker-game/src/…
- [rotation-predictor](./rotation-predictor.md) — **Path:** `bitpacker-game/src/prediction/rotation-predictor.ts` **Export:** See source for details.
- [rotation-predictor](./rotation-predictor.md) — **Path:** `bitpacker-game/src/prediction/rotation-predictor.ts` **Export:** See source for details.
## Purpose
Position and rotation extrapolation primitives for the `GameBitPacker`. The module is a re-export barrel that surfaces every prediction algorithm — Kalman filter, motion-math derivatives (velocity, acceleration, jerk), per-algorithm predictor classes (`PositionPredictor`, `RotationPredictor`), and the seven supported extrapolation algorithms (linear, quadratic, cubic, hermite, catmull-rom, velocity-verlet, kalman). Consumers pick an algorithm via the `PredictionAlgorithm` enum on the `predictionOptions` they pass to `GameBitPacker`.
The Kalman family is the most accurate (model-based filter that learns process and measurement noise from observed updates). The polynomial family (linear / quadratic / cubic) is cheap and good enough for slow-moving entities. Hermite and Catmull-Rom interpolation produce smooth trajectories from sparse samples and are the right pick for cinematic camera or replay playback. Velocity-Verlet is the staple for physics-driven motion where acceleration is known.
## Features
- `initKalmanState`, `kalmanPredict`, `updateKalmanFilter` — Kalman filter primitives
- `calculateVelocity`, `calculateAcceleration`, `calculateJerk`, `calculateVelocityAt`, `calculateAccelerationAt` — discrete-difference motion math
- `linearPredict`, `quadraticPredict`, `cubicPredict`, `hermitePredict`, `catmullRomPredict`, `verletPredict` — six pure extrapolation functions
- `PositionPredictor`, `RotationPredictor` — stateful predictors that remember a rolling history and pick the algorithm at construction
## Usage: Basic Example
```ts
import { PositionPredictor, PredictionAlgorithm } from "@suaveplan/bitpacker-game";
const predictor = new PositionPredictor({
algorithm: PredictionAlgorithm.LINEAR,
historySize: 5,
});
predictor.observe({ x: 0, y: 0, z: 0 }, 0);
predictor.observe({ x: 1, y: 0, z: 0 }, 16);
predictor.observe({ x: 2, y: 0, z: 0 }, 32);
const next = predictor.predict(48); // { x: 3, y: 0, z: 0 }
```
## Usage: Advanced Example
```ts
import {
initKalmanState,
kalmanPredict,
updateKalmanFilter,
calculateVelocity,
} from "@suaveplan/bitpacker-game";
let kf = initKalmanState({ x: 0, y: 0, z: 0 });
function frame(observation: { x: number; y: number; z: number }, dt: number) {
kf = updateKalmanFilter(kf, observation, dt);
const predicted = kalmanPredict(kf, dt);
const v = calculateVelocity(predicted, kf.position, dt);
return { predicted, velocity: v };
}
```
## API Reference
This module is a barrel re-export; see the per-algorithm `.md` files (when they ship) for full per-function semantics. The headline exports are:
- `PositionPredictor` and `RotationPredictor` — stateful predictors with `observe(value, timestamp)` and `predict(timestamp)` methods
- The six pure prediction functions, each `(history: Vector3[], dt: number) => Vector3`
- `initKalmanState(initial)`, `updateKalmanFilter(state, obs, dt)`, `kalmanPredict(state, dt)` for direct Kalman usage
## Implementation Notes
- **Algorithm choice is a behaviour decision.** The `PredictionAlgorithm` enum is open via declaration merging, but each value implies different observation requirements: linear needs 2 samples, quadratic 3, cubic 4, Catmull-Rom 4, Hermite 2 + tangents, Verlet 2 + acceleration, Kalman a fitted state. Picking an algorithm without enough history degrades to a fallback (typically linear).
- **`historySize` is a soft cap, not a hard one.** The predictor stores up to `historySize` samples in a ring buffer; older samples are dropped on overflow. A larger buffer improves polynomial fit accuracy but raises latency on unstable observations.
- **No coordinate-system assumption.** All predictors operate on a generic `Vector3` (`{ x, y, z }`); rotations are wrapped to `[-π, π]` by `RotationPredictor` before extrapolation to avoid spurious 2π jumps on yaw wrap.
- **Process / measurement noise are tunable.** Kalman state exposes `processNoise` and `measurementNoise`. The defaults are tuned for 60 Hz simulation; faster sample rates need lower noise values to avoid over-smoothing.

View File

@@ -0,0 +1,8 @@
## rotation-predictor
**Path:** `bitpacker-game/src/prediction/rotation-predictor.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,84 @@
# Game Quantization Helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Coordinate-rounding helpers for game-state vectors and rotations. The functions in this module trade absolute precision for compact wire representations: `quantizePosition({ x: 1.234, y: 5.678, z: 9.012 }, 0.01)` rounds each component to two decimal places before packing, halving the bit-width versus a raw float. This is the standard tactic for bandwidth-constrained multiplayer games — a typical ±1024 m world quantized to 0.01 m precision packs into ~17 bits per axis instead of 32 bits.
The 2-D and 3-D variants exist because some games (top-down, side-scrolling) work in `Vector2` and would otherwise pay the cost of a wasted z component. `quantizeRotation` separately handles yaw and pitch with their own precisions; `interpolateAngle` handles the 360°/0° wrap so a transition from 350° to 10° interpolates through the short arc, not the long one. `validateRange` is a defensive check used at API boundaries.
## Features
- `quantizePosition(position, precision = 0.01)` — round a `Vector3` to a precision divisor
- `quantizePosition2D(position, precision = 0.01)` — round a `Vector2`
- `quantizeRotation(rotation, yawPrecision = 1, pitchPrecision = 1)` — round yaw + pitch
- `interpolateAngle(a, b, t)` — interpolate angles in degrees, taking the short arc through the wrap
- `validateRange(value, min, max, name?)` — throw if out of range, with an optional field name for the error
## Usage: Basic Example
```ts
import { quantizePosition, quantizeRotation } from "@suaveplan/bitpacker-game";
const pos = quantizePosition({ x: 1.234, y: 5.678, z: 9.012 }, 0.01);
// { x: 1.23, y: 5.68, z: 9.01 }
const rot = quantizeRotation({ yaw: 90.55, pitch: -45.4 }, 1, 1);
// { yaw: 91, pitch: -45 }
```
## Usage: Advanced Example
```ts
import {
quantizePosition,
quantizeRotation,
interpolateAngle,
validateRange,
} from "@suaveplan/bitpacker-game";
function tick(prev: { yaw: number }, target: { yaw: number }, t: number) {
validateRange(t, 0, 1, "interpolation t");
return { yaw: interpolateAngle(prev.yaw, target.yaw, t) };
}
// Quantize before pack to match the schema's fixed-point precision exactly:
function preparePacket(position: { x: number; y: number; z: number }) {
const q = quantizePosition(position, 0.01);
validateRange(q.x, -1024, 1024, "x");
validateRange(q.y, -1024, 1024, "y");
validateRange(q.z, -128, 128, "z");
return q;
}
```
## API Reference
### `quantizePosition(position: Vector3, precision = 0.01): Vector3`
Returns a new `Vector3` with each component rounded to `precision` (e.g. `0.01` for two decimal places, `1` for whole units). The result is the nearest grid point on a precision-divisor lattice.
### `quantizePosition2D(position: Vector2, precision = 0.01): Vector2`
Same as `quantizePosition` for a 2-D vector. No z component.
### `quantizeRotation(rotation: Rotation, yawPrecision = 1, pitchPrecision = 1): Rotation`
Returns a new `Rotation` with `yaw` and `pitch` rounded to their respective precisions. The two axes have separate precisions because yaw typically needs less granularity than pitch (or vice versa).
### `interpolateAngle(a: number, b: number, t: number): number`
Returns the interpolation between `a` and `b` taking the short arc through the 360°/0° wrap. `t ∈ [0, 1]`. Equivalent to slerp on the unit circle for two-axis cases.
### `validateRange(value: number, min: number, max: number, name?: string): void`
Throws if `value` is outside `[min, max]`. The optional `name` is included in the error message to identify the offending field.
## Implementation Notes
- **Quantization is round-to-nearest-grid.** `Math.round(value / precision) * precision` produces banker's rounding behaviour at the exact half. Consumers that need round-half-up should pre-add a half-precision offset before quantizing.
- **No clamp.** These helpers do not clamp out-of-range values; pair them with `validateRange` if your wire format has a hard cap. Quantizing without clamping can produce values outside the schema's bit-width and cause pack failures.
- **`interpolateAngle` is not unwrap-aware.** It takes the short arc per pair; chains of three or more interpolated angles can drift unless the consumer normalizes between calls.
- **Defensive-but-cheap.** `validateRange` is `O(1)` and emits a clear error message including the field name; it's appropriate at every API boundary, not just hot-path entries.

View File

@@ -0,0 +1,41 @@
## Reconciliation
**Path:** `src/reconciliation/reconciliation.ts`
**Export:** `StateReconciliation`
### What it does
Client-side prediction and server reconciliation for multiplayer games. Buffers inputs and server states, detects mispredictions, and smoothly reconciles client state.
### Key API
| Method | Description |
|--------|-------------|
| `processInput(input, timestamp)` | Buffer a player input, return sequence |
| `predictClientState(state, applyPhysics, deltaTime)` | Apply unprocessed inputs locally |
| `processServerUpdate(state, sequence, timestamp)` | Receive server-authoritative state |
| `reconcileState(state, applyPhysics)` | Rewind + replay after server correction |
| `extrapolateState(state, deltaTime)` | Predict forward for missing server data |
| `sendPing()` / `receivePong()` | Latency measurement |
### Buffers
- **Input buffer** — stores client inputs with sequence numbers
- **State buffer** — stores predicted and server states
- **Jitter buffer** — optional delay for out-of-order server packets
### Reconciliation Flow
1. Client predicts state from inputs
2. Server sends authoritative state
3. If prediction error > threshold, reconcile:
- Find server state in buffer
- Rewind to server state
- Replay all inputs after that point
- Smoothly interpolate to reconciled state
### Depends on
- `../types.js` — `PlayerState`, `PlayerInput`, `ReconciliationOptions`
- `../prediction/` — `PositionPredictor`
- `../interpolation/` — `GameInterpolator`

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## reconciliation
**Path:** `bitpacker-game/src/reconciliation/reconciliation.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,65 @@
# Register Schemas — `@suaveplan/bitpacker-game`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-game` package. Exposes `registerBitPackerGameSchemas(registry)` which converts the package's six public boundary schemas into JSON Schema form and registers them under `bitpacker:@suaveplan/bitpacker-game` at version 1. Re-registration on the same registry is a no-op.
The six schemas covered are: `predictionOptions`, `interpolationOptions`, `reconciliationOptions` (the three sub-options bundles), `gameStateSchema` (the `{ version, entities }` envelope), `quantizedVector` (the `quantizeVector` / `dequantizeVector` input), and `gameBitPackerOptions` (the top-level `GameBitPacker` constructor bundle). Together they describe every external configuration shape the package accepts.
## Features
- `BITPACKER_GAME_SUBJECT` constant — `"bitpacker:@suaveplan/bitpacker-game"`
- `BITPACKER_GAME_SCHEMA_VERSION` constant — `1`
- `registerBitPackerGameSchemas(registry)` — idempotent registration
## Usage: Basic Example
```ts
import { createRegistry } from "@suaveplan/schema-registry";
import {
BITPACKER_GAME_SUBJECT,
registerBitPackerGameSchemas,
} from "@suaveplan/bitpacker-game/register-schemas";
const registry = createRegistry();
registerBitPackerGameSchemas(registry);
```
## Usage: Advanced Example
```ts
import { createRegistry, SchemaRegistry } from "@suaveplan/schema-registry";
import { registerBitPackerSchemas } from "@suaveplan/bitpacker/register-schemas";
import { registerBitPackerGameSchemas } from "@suaveplan/bitpacker-game/register-schemas";
function bootstrapAll(registry: SchemaRegistry) {
registerBitPackerSchemas(registry);
registerBitPackerGameSchemas(registry);
}
const registry = createRegistry();
bootstrapAll(registry);
```
## API Reference
### `BITPACKER_GAME_SUBJECT: "bitpacker:@suaveplan/bitpacker-game"`
Const string.
### `BITPACKER_GAME_SCHEMA_VERSION: 1`
Const number.
### `registerBitPackerGameSchemas(registry: SchemaRegistry): void`
Idempotent registration. Returns without writing if the subject is already present.
## Implementation Notes
- **Six-key payload.** One key per public boundary schema.
- **`unrepresentable: "any"`.** Required because the option bundles use `z.custom` for clock and signal injection points.

View File

@@ -0,0 +1,68 @@
# Game Schema Factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Programmatic schema construction for game-specific bitpacker schemas. `createGameSchema(options)` returns a tuned `BitPackerSchema` based on the consumer's choice of position bit-width, rotation precision, health/speed caps, and other gameplay-domain knobs. The function lets a consumer derive a schema from per-game design parameters (e.g. "this game has 1024 m world bounds with 0.01 m precision") rather than hand-rolling the same schema across multiple sources.
The `GameSchemaFactory` const is a curated set of named factory presets — `small`, `medium`, `large`, `precise`, `arcade` — each producing a `BitPackerSchema` with appropriate trade-offs. New presets can be appended; the consumer's `createGameSchema(options)` always overrides preset defaults with explicit options.
## Features
- `createGameSchema(options?)` — main factory that returns a fully-tuned `BitPackerSchema`
- `GameSchemaFactory` — const map of named presets (`small`, `medium`, `large`, `precise`, `arcade`)
- `GameSchemaOptions` — `maxPosition`, `positionPrecision`, `maxRotation`, `rotationPrecision`, `maxHealth`, `maxSpeed`, plus optional fields for actions, effects, animations
- All numeric inputs are checked at construction; out-of-range values produce an error rather than a corrupted schema
## Usage: Basic Example
```ts
import { createGameSchema } from "@suaveplan/bitpacker-game";
const schema = createGameSchema({
maxPosition: 512,
positionPrecision: 1,
maxHealth: 100,
});
```
## Usage: Advanced Example
```ts
import {
createGameSchema,
GameBitPacker,
GameSchemaFactory,
} from "@suaveplan/bitpacker-game";
// Pick a preset for arcade-style games (small world, low precision):
const arcade = GameSchemaFactory.arcade;
const arcadeBP = new GameBitPacker(arcade);
// Or compose: start from a preset and override a single knob:
const customLarge = createGameSchema({
...GameSchemaFactory.large,
rotationPrecision: 0.1, // fine yaw for first-person aim
maxSpeed: 20,
});
const customBP = new GameBitPacker(customLarge);
```
## API Reference
### `createGameSchema(options?: GameSchemaOptions): BitPackerSchema`
Builds a `BitPackerSchema` for a player state with position, rotation, stats, and optional metadata. `options` defaults to `{ maxPosition: 1024, positionPrecision: 2, maxRotation: 180, rotationPrecision: 1, maxHealth: 100, maxSpeed: 8 }`. Returns a schema ready to pass to `new GameBitPacker(schema)`.
### `GameSchemaFactory: { small, medium, large, precise, arcade }`
Const map of pre-built schemas. Each preset is a fully-tuned `BitPackerSchema` produced by `createGameSchema` with appropriate options. Consumers use these as starting points or as drop-in choices for typical game shapes.
## Implementation Notes
- **Precision is a divisor, not a digit count.** `positionPrecision: 0.01` means the schema rounds positions to 2-decimal places (1/100). `positionPrecision: 1` means whole units. The fixed-point bit width is computed from `maxPosition / precision`.
- **Rotation is wrapped to `[-180, 180]`.** Values outside this range are treated as out-of-range at pack time; consumers handling continuous rotations must wrap to this range first.
- **Schema shape is stable across presets.** All factory output shares the same top-level keys (`position`, `rotation`, `stats`, `optional`); only the per-field bit-widths differ. This lets a consumer swap presets at runtime without re-deriving downstream code.
- **No defaults in the schema itself.** The output is a pure schema; default values for missing fields are the consumer's responsibility (e.g. via the optional-with-default strategy in `@suaveplan/bitpacker-strategies`).

View File

@@ -0,0 +1,97 @@
# Pre-tuned Game Schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
A curated set of `BitPackerSchema` constants tuned for common game-state shapes — player position, rotation, stats, optional metadata, world updates, player input — plus the `createDeltaSchema` factory that mirrors any of those into its delta form. Constants here are the right choice when a game's data model fits the upstream conventions (1024 m bounded world, 0.01 m precision, 180° rotation, 0–100 health). Games with unusual ranges should use the `createGameSchema` factory in the `schema-factory` module instead.
The schemas are designed to compose: `PLAYER_STATE_SCHEMA` references `PLAYER_POSITION_SCHEMA`, `PLAYER_ROTATION_SCHEMA`, `PLAYER_STATS_SCHEMA`, and the optional bundle. `WORLD_UPDATE_SCHEMA` references the player schema array. Replacing one constant in a downstream consumer cascades automatically to the consumers of the composed schemas.
## Features
- `PLAYER_POSITION_SCHEMA` — fixed-point `{ x, y, z }` with 0.01 precision and ±1024 m / ±128 m ranges
- `PLAYER_ROTATION_SCHEMA` — fixed-point `{ yaw, pitch }`, ±180° / ±90° respectively
- `PLAYER_STATS_SCHEMA` — `{ health, shield, speed, isMoving, isSprinting }` tuned for a typical FPS
- `PLAYER_OPTIONAL_SCHEMA` — animation name + effect-id array, both optional
- `PLAYER_STATE_SCHEMA` — composes all of the above into one player snapshot
- `WORLD_UPDATE_SCHEMA` — variable-length array of player states + a 16-bit tick
- `PLAYER_INPUT_SCHEMA` — analog move + rotate axes + action-id array
- `createDeltaSchema(base)` — wraps any base schema in a delta envelope (changed-mask + per-field optional)
## Usage: Basic Example
```ts
import { GameBitPacker, PLAYER_STATE_SCHEMA } from "@suaveplan/bitpacker-game";
const bp = new GameBitPacker(PLAYER_STATE_SCHEMA);
const bytes = bp.pack({
position: { x: 100, y: 0, z: 50 },
rotation: { yaw: 90, pitch: 0 },
stats: { health: 95, shield: 50, speed: 5, isMoving: true, isSprinting: false },
});
```
## Usage: Advanced Example
```ts
import {
GameBitPacker,
PLAYER_INPUT_SCHEMA,
WORLD_UPDATE_SCHEMA,
createDeltaSchema,
PLAYER_STATE_SCHEMA,
} from "@suaveplan/bitpacker-game";
// Server emits world snapshots every tick:
const worldBP = new GameBitPacker(WORLD_UPDATE_SCHEMA);
// Client emits inputs at frame rate:
const inputBP = new GameBitPacker(PLAYER_INPUT_SCHEMA);
// Server transmits deltas instead of full snapshots:
const playerDeltaSchema = createDeltaSchema(PLAYER_STATE_SCHEMA);
const deltaBP = new GameBitPacker(playerDeltaSchema);
```
## API Reference
### `PLAYER_POSITION_SCHEMA: PositionSchema`
`{ x: "-1024.2", y: "-1024.2", z: "-128.2" }` — signed fixed-point with 0.01 precision. The `-` prefix marks the field as signed.
### `PLAYER_ROTATION_SCHEMA: RotationSchema`
`{ yaw: "-180.1", pitch: "-90.1" }` — signed fixed-point with 0.1 precision.
### `PLAYER_STATS_SCHEMA: PlayerStatsSchema`
`{ health: 100, shield: 100, speed: "8.2", isMoving: true, isSprinting: true }` — uint, uint, fixed-point, bool, bool.
### `PLAYER_OPTIONAL_SCHEMA: PlayerOptionalSchema`
Optional `{ animation, effects }` bundle. Animation is a variable-length string; effects is a variable-length array of compact ids.
### `PLAYER_STATE_SCHEMA: PlayerStateSchema`
Composes `position`, `rotation`, `stats`, `optional`. The canonical shape every game subsystem reads from.
### `WORLD_UPDATE_SCHEMA: WorldUpdateSchema`
`{ tick: 65535, players: [PLAYER_STATE_SCHEMA] }` — one tick + a variable-length array of player states.
### `PLAYER_INPUT_SCHEMA: PlayerInputSchema`
`{ moveX, moveY, rotateYaw, rotatePitch, actions }` — four signed fixed-point axes plus an action-id array.
### `createDeltaSchema<T>(base: T): DeltaSchema<T>`
Returns a schema that wraps `base` with a per-field changed-mask and optional fields. The wrapped schema is appropriate for incremental updates where most fields stay the same frame-to-frame.
## Implementation Notes
- **Bit-widths are tuned for typical FPS games.** ±1024 m world with 0.01 m precision uses ~17 bits per axis after sign + integer + fractional. Smaller worlds save bits per field; the consumer should switch to `createGameSchema` if the defaults are wasteful.
- **`isMoving` + `isSprinting` are independent.** The schema models them as two booleans because a player can be sprinting while not moving (e.g. holding sprint while stuck against a wall) — collapsing to a single tri-state would lose that distinction.
- **`createDeltaSchema` is a factory, not a const.** It returns a fresh schema every call so the consumer can apply additional transformations (e.g. swapping in a custom `_optional` strategy) without mutating shared state.
- **No timestamps in the player schema.** Timing is owned by the `tick` field on `WORLD_UPDATE_SCHEMA`. Consumers that want per-player timestamps must add a `_optional` field at their own layer.

View File

@@ -0,0 +1,85 @@
# GameStateManager
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
State ownership and delta tracking for the `GameBitPacker`. `GameStateManager<T>` holds the current authoritative state, the last-sent state (for delta computation), and a 4-bit rolling sequence number used to identify out-of-order or duplicate updates on the wire. The class is generic on the `BitPackerSchema` to keep the held state type-safe; the implementation stores the value as `unknown` internally because schema-driven serialization erases the static type at the boundary.
The 4-bit sequence is the key wire-format constraint: the upstream `bitpacker-game` reserves four bits per state update to discriminate which update a delta refers to. `MAX_SEQUENCE = 15` is exposed as a `static readonly` field so callers can validate sequence wraps without redeclaring the constant.
## Features
- `updateState` — replace the current authoritative state
- `getLatestState` / `getLastSentState` — accessors for the live state and the last-transmitted state
- `markSent` — record that the current state has been transmitted (advances the rolling sequence)
- `processReceived` — handle an incoming update, gate against `lastReceivedSequence` to drop replays
- `computeDelta` — produce a `StateDelta<T>` describing the change from `lastSentState` to `currentState`
- `MAX_SEQUENCE` — the 4-bit wire-format cap (`15`)
## Usage: Basic Example
```ts
import { GameStateManager } from "@suaveplan/bitpacker-game";
const manager = new GameStateManager();
manager.updateState({ x: 10, y: 20 });
const latest = manager.getLatestState();
```
## Usage: Advanced Example
```ts
import { GameStateManager } from "@suaveplan/bitpacker-game";
const manager = new GameStateManager<{ position: { x: number; y: number } }>();
function tick(serverFrame: { position: { x: number; y: number } }, sequence: number) {
if (manager.processReceived(serverFrame, sequence)) {
// Accepted as in-order
const delta = manager.computeDelta();
if (delta.hasChanges) {
// …transmit delta only…
manager.markSent();
}
}
}
```
## API Reference
### `class GameStateManager<T extends BitPackerSchema> implements GameStateManagerInterface<T>`
Stores the latest state, the last-sent state, and a rolling 4-bit sequence. The class is generic so consumers can preserve type information through `getLatestState`.
#### `static readonly MAX_SEQUENCE = 15`
The wire-format cap on the rolling sequence. Sequence wraps from `15` back to `0`.
#### `updateState(state: unknown): void`
Replaces the current state. Does NOT advance the sequence — sequence advances only on `markSent`.
#### `getLatestState(): unknown | undefined`
Returns the current authoritative state, or `undefined` if no state has been pushed yet.
#### `markSent(): void`
Snapshots `currentState` into `lastSentState` and advances `sequence` modulo `MAX_SEQUENCE + 1`.
#### `processReceived(state: unknown, sequence: number): boolean`
Returns `true` if the incoming update is in-order (sequence ≥ `lastReceivedSequence`); `false` if it's a replay or out-of-order. Gates downstream consumers from acting on stale frames.
#### `computeDelta<T>(): StateDelta<T>`
Returns a `StateDelta<T>` with `hasChanges`, `delta` (partial of T), and `changedPaths` (string array). Compares `currentState` against `lastSentState` field-by-field.
## Implementation Notes
- **Sequence wrap is modulo 16.** The 4-bit constraint makes the sequence wrap from `15` to `0` after `markSent`. Receivers that compare sequence numbers must use modular comparison (e.g. `((received - lastReceived + 16) % 16) < 8`) rather than naive `received > lastReceived`.
- **`unknown`-typed internals.** `currentState` and `lastSentState` are typed `unknown` rather than `T` because the schema-driven pack/unpack erases the static type at the wire boundary. Generic callers preserve type-safety at their own call sites via the `T extends BitPackerSchema` parameter.
- **No deep clone on `markSent`.** The snapshot is a structural reference, not a deep clone. Consumers mutating `currentState` after `markSent` must do so via copy-on-write semantics or risk corrupting the prior delta baseline.
- **`processReceived` is a gate, not a writer.** It returns a boolean so the caller decides what to do with rejected frames (drop, log, request resync). The function does not auto-advance `lastReceivedSequence` on rejection.

View File

@@ -0,0 +1,37 @@
## Game Types
**Path:** `src/types.ts`
**Export:** All game-related type definitions and enums
### Core Types
| Type | Description |
|------|-------------|
| `Vector3` | `{ x, y, z }` 3D position |
| `Rotation` | `{ yaw, pitch }` euler rotation |
| `PlayerStats` | `{ health, shield, speed, isMoving, isSprinting }` |
| `PlayerState` | `{ position, rotation, stats, optional? }` |
| `PlayerInput` | `{ moveX, moveY, rotateYaw, rotatePitch, actions }` |
| `HistoryPoint<T>` | `{ value: T, timestamp: number }` for prediction history |
| `KalmanState` | Kalman filter position/velocity/covariance state |
### Enums
| Enum | Values |
|------|--------|
| `PredictionAlgorithm` | LINEAR, QUADRATIC, CUBIC, HERMITE, CATMULL_ROM, VELOCITY_VERLET, KALMAN, ADAPTIVE |
| `InterpolationMethod` | LINEAR, CUBIC, HERMITE, CATMULL_ROM, BEZIER, SPRING, ELASTIC, BOUNCE, SMOOTHSTEP |
### Options Interfaces
| Interface | Key Fields |
|-----------|-----------|
| `PredictionOptions` | `algorithm`, `historySize`, `maxPredictionTime`, `adaptiveThreshold`, `gravityFactor`, `velocityDamping` |
| `InterpolationOptions` | `method`, `tension`, `bias`, `springConstant`, `damping` |
| `ReconciliationOptions` | `inputBufferSize`, `stateBufferSize`, `jitterBufferSize`, `reconciliationThreshold`, `smoothingFactor`, `enableExtrapolation` |
### Defaults
- `DEFAULT_PREDICTION_OPTIONS` — historySize: 5, maxPredictionTime: 200ms
- `DEFAULT_INTERPOLATION_OPTIONS` — method: LINEAR, tension: 0.5, bias: 0.5
- `DEFAULT_RECONCILIATION_OPTIONS` — inputBufferSize: 60, stateBufferSize: 120, jitterBufferSize: 3

View File

@@ -0,0 +1,109 @@
# Game Schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas for the public configuration types of `@suaveplan/bitpacker-game`: the three sub-options bundles (`PredictionOptions`, `InterpolationOptions`, `ReconciliationOptions`), the top-level `GameBitPackerOptions`, the simple `GameStateSchema` envelope, and the `QuantizedVector` shape used by `quantizeVector`/`dequantizeVector`. Each schema is the runtime gatekeeper at the corresponding constructor or function boundary.
The schemas are strict and reject unknown keys so a typo'd flag (`enablePredictoin` for `enablePrediction`) fails at parse time rather than producing a game that silently runs without prediction enabled. Numeric ranges encode genuine invariants — buffer sizes are non-negative integers, history sizes are positive integers (a zero-history predictor is degenerate), and timing values are non-negative finite numbers.
## Features
- `predictionOptionsSchema` — `algorithm` from `PredictionAlgorithm` enum, positive integer `historySize`, non-negative timing values
- `interpolationOptionsSchema` — `method` from the union of nine published method tokens
- `reconciliationOptionsSchema` — buffer sizes, jitter window, threshold, smoothing, plus nested `predictionOptions` and `interpolationOptions`
- `gameStateSchemaSchema` — versioned `{ version, entities }` envelope, `entities` is an open record (`Record<string, unknown>`)
- `quantizedVectorSchema` — `{ x, y, z? }` for 2-D and 3-D quantization
- `gameBitPackerOptionsSchema` — top-level GameBitPacker options, composing the three sub-bundles
- `as unknown as z.ZodType<T>` casts where Zod's optional `T | undefined` collides with `exactOptionalPropertyTypes`
## Usage: Basic Example
```ts
import {
GameBitPacker,
PredictionAlgorithm,
gameBitPackerOptionsSchema,
} from "@suaveplan/bitpacker-game";
const opts = gameBitPackerOptionsSchema.parse({
enablePrediction: true,
predictionOptions: {
algorithm: PredictionAlgorithm.KALMAN,
historySize: 10,
},
});
const packer = new GameBitPacker(opts);
```
## Usage: Advanced Example
```ts
import {
GameStateManager,
quantizeVector,
quantizedVectorSchema,
gameStateSchemaSchema,
reconciliationOptionsSchema,
PredictionAlgorithm,
} from "@suaveplan/bitpacker-game";
// Validate every external state push:
const manager = new GameStateManager();
function pushState(raw: unknown): void {
manager.pushState(gameStateSchemaSchema.parse(raw));
}
// Validate vectors at the quantization boundary:
function compressedVector(raw: unknown, bits: number) {
const v = quantizedVectorSchema.parse(raw);
return quantizeVector(v, bits);
}
// Compose nested options:
const reconciliation = reconciliationOptionsSchema.parse({
inputBufferSize: 60,
stateBufferSize: 120,
enableExtrapolation: true,
predictionOptions: {
algorithm: PredictionAlgorithm.HERMITE,
historySize: 8,
},
});
```
## API Reference
### `predictionOptionsSchema: z.ZodType<PredictionOptions>`
Strict schema. `algorithm` (if present) must be a `PredictionAlgorithm` enum value. `historySize` must be a positive integer; `maxPredictionTime`, `adaptiveThreshold` must be non-negative numbers. `gravityFactor` and `velocityDamping` are unconstrained finite numbers.
### `interpolationOptionsSchema: z.ZodType<InterpolationOptions>`
Strict schema. `method` is restricted to the nine published method tokens (`linear`, `cubic`, `elastic`, `spring`, `bounce`, `smoothstep`, `hermite`, `catmull-rom`, `bezier`). Other fields are unconstrained finite numbers.
### `reconciliationOptionsSchema: z.ZodType<ReconciliationOptions>`
Strict schema with nested `predictionOptions` and `interpolationOptions`. Buffer/window sizes are non-negative integers; thresholds and timings are non-negative numbers.
### `gameStateSchemaSchema: z.ZodType<GameStateSchema>`
Validates `{ version, entities }`. `version` is a non-negative integer; `entities` is an open record so per-game shapes can be embedded. Validating each entity is the consumer's responsibility.
### `quantizedVectorSchema: z.ZodType<QuantizedVector>`
Validates `{ x, y, z? }`. `z` is optional so the same schema accepts both 2-D and 3-D vectors.
### `gameBitPackerOptionsSchema: z.ZodType<GameBitPackerOptions>`
Strict top-level options schema, composing the three sub-bundles. `networkSimulationOptions` is left as an open record (`Record<string, unknown>`) because the canonical schema lives in `@suaveplan/bitpacker-network` and importing it here would tighten the coupling unnecessarily — consumers who want to validate it can run the network package's schema themselves.
## Implementation Notes
- **Open-record `entities` and `networkSimulationOptions`.** Both are intentionally `Record<string, unknown>` because per-entity / per-network-config shapes are out of this package's scope. Validating at finer granularity would require this package to know about every consumer's data model, which it does not.
- **`PredictionAlgorithm` is a string enum.** Zod's `z.nativeEnum` works on it directly. The enum's serialized values (`"linear"`, `"kalman"`, …) are the same as the runtime values, so the schema accepts both forms.
- **`as unknown as z.ZodType<T>` cast.** Same EOPT mismatch as elsewhere in this change. Runtime semantics are identical.
- **Strict mode everywhere.** A loose schema would silently drop typo'd flags, producing a game-bitpacker configured without the consumer's intended optimizations.

View File

@@ -0,0 +1,81 @@
# @suaveplan/bitpacker-network - Bitpacker network plugin
## Summary
Network simulation utilities for testing bitpacker under realistic network conditions.
<!-- begin:auto-docs -->
## Documentation tree
- [defaults](./src/defaults/defaults.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [default-clock](./src/defaults/default-clock.md) — `defaultClock` is the **single allow-listed wall-clock source** in `@suaveplan/bitpacker-network`. The determin…
- [default-rng](./src/defaults/default-rng.md) — `defaultRng` is the canonical CSPRNG-backed `Rng` factory used by `NetworkSimulator` and any other deterministic-when-a…
- [defaults](./src/defaults/defaults.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [register-schemas](./src/register-schemas/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-network` package. Exposes `regis…
- [register-schemas](./src/register-schemas/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-network` package. Exposes `regis…
- [simulator](./src/simulator/simulator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - simulator.schema — Zod schema for `NetworkSimulator…
- [simulator](./src/simulator/simulator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - simulator.schema — Zod schema for `NetworkSimulator…
- [simulator.schema](./src/simulator/simulator.schema.md) — Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSi…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - types.schema — Zod schema for `NetworkSimulationOpt…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - types.schema — Zod schema for `NetworkSimulationOpt…
- [types.schema](./src/types/types.schema.md) — Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptio…
- [index](./src/index.md) — **Path:** `bitpacker-network/src/index.ts` **Export:** See source for details.
<!-- end:auto-docs --> - [default-clock](./src/defaults/default-clock.md) — `defaultClock` is the **single allow-listed wall-clock source** in `@suaveplan/bitpacker-network`. The determin…
- [default-rng](./src/defaults/default-rng.md) — `defaultRng` is the canonical CSPRNG-backed `Rng` factory used by `NetworkSimulator` and any other deterministic-when-a…
- [defaults](./src/defaults/defaults.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulator](./src/simulator/simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- simulator.schema — Zod schema for `NetworkSimulator…
- [simulator](./src/simulator/simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- simulator.schema — Zod schema for `NetworkSimulator…
- [simulator.schema](./src/simulator/simulator.schema.md) — Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSi…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schema for `NetworkSimulationOpt…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schema for `NetworkSimulationOpt…
- [types.schema](./src/types/types.schema.md) — Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptio…
- [index](./src/index.md) — **Path:** `bitpacker-network/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-network` package. Exposes `regis…
- [default-clock](./src/defaults/default-clock.md) — `defaultClock` is the **single allow-listed wall-clock source** in `@suaveplan/bitpacker-network`. The determin…
- [default-rng](./src/defaults/default-rng.md) — `defaultRng` is the canonical CSPRNG-backed `Rng` factory used by `NetworkSimulator` and any other deterministic-when-a…
- [defaults](./src/defaults/defaults.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulator](./src/simulator/simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- simulator.schema — Zod schema for `NetworkSimulator…
- [simulator](./src/simulator/simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- simulator.schema — Zod schema for `NetworkSimulator…
- [simulator.schema](./src/simulator/simulator.schema.md) — Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSi…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schema for `NetworkSimulationOpt…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schema for `NetworkSimulationOpt…
- [types.schema](./src/types/types.schema.md) — Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptio…
- [index](./src/index.md) — **Path:** `bitpacker-network/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-network` package. Exposes `regis…
- [default-clock](./src/defaults/default-clock.md) — `defaultClock` is the **single allow-listed wall-clock source** in `@suaveplan/bitpacker-network`. The determin…
- [default-rng](./src/defaults/default-rng.md) — `defaultRng` is the canonical CSPRNG-backed `Rng` factory used by `NetworkSimulator` and any other deterministic-when-a…
- [defaults](./src/defaults/defaults.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulator](./src/simulator/simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- simulator.schema — Zod schema for `NetworkSimulator…
- [simulator](./src/simulator/simulator.md) — <!-- begin:auto-docs --> _(no submodules)_- simulator.schema — Zod schema for `NetworkSimulator…
- [simulator.schema](./src/simulator/simulator.schema.md) — Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSi…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schema for `NetworkSimulationOpt…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- types.schema — Zod schema for `NetworkSimulationOpt…
- [types.schema](./src/types/types.schema.md) — Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptio…
- [index](./src/index.md) — **Path:** `bitpacker-network/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-network` package. Exposes `regis…
- [default-clock](./src/defaults/default-clock.md) — `defaultClock` is the **single allow-listed wall-clock source** in `@suaveplan/bitpacker-network`. The determin…
- [default-rng](./src/defaults/default-rng.md) — `defaultRng` is the canonical CSPRNG-backed `Rng` factory used by `NetworkSimulator` and any other deterministic-when-a…
- [defaults](./src/defaults/defaults.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ - [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulator](./src/simulator/simulator.md) — <!-- begin:auto-docs --> _(no submodules)_ - [simulator](./src/simulator/simulator.md) — <!-- begin:auto-docs --> _(no submodules)_ - [simulator.schema](./src/simulator/simulator.schema.md) — Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSi…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types.schema](./src/types/types.schema.md) — Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptio…
- [index](./src/index.md) — **Path:** `bitpacker-network/src/index.ts` **Export:** See source for details.
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-network` package. Exposes `regis…
## Install
```bash
bun add @suaveplan/bitpacker-network
```
## Usage
```typescript
import { NetworkSimulator } from "@suaveplan/bitpacker-network";
const sim = new NetworkSimulator({ latency: 50, jitter: 10, packetLoss: 0.02 });
sim.send(new Uint8Array([1, 2, 3]));
const received = sim.receive(); // may drop packet based on loss rate
```
## Modules
- `src/index.ts` — `NetworkSimulator`: configurable latency, jitter, and packet loss simulation

View File

@@ -0,0 +1,45 @@
# default-clock
## Purpose
`defaultClock` is the **single allow-listed wall-clock source** in `@suaveplan/bitpacker-network`. The determinism rule is that `Date.now()` may not appear at module scope in implementation files — every primitive that needs to read time must accept an injected `Clock` from `@suaveplan/types`. This file is the one exception: it provides the production-default `Clock` that callers can use when they don't need deterministic-replay behaviour, while keeping a clean grep boundary for the spec-conformance test that scans the package.
## Features
- Production-grade real-time clock backed by `Date.now()`.
- Implements the canonical `Clock` interface from `@suaveplan/types`.
- Single allow-listed `Date.now()` site in the package — every other module reads time via injection only.
- Trivially overridable in tests by injecting a `FakeClock` from `@suaveplan/testing`.
## Usage: Basic Example
```ts
import { defaultClock } from "@suaveplan/bitpacker-network";
const now = defaultClock(); // milliseconds since UNIX epoch
```
## Usage: Advanced Example
```ts
import { FakeClock } from "@suaveplan/testing";
import { NetworkSimulator } from "@suaveplan/bitpacker-network";
// Deterministic timing in tests: drive the simulator with a FakeClock
// instead of the wall clock, then advance it explicitly.
const fakeClock = new FakeClock(0);
const sim = new NetworkSimulator({ clock: fakeClock.now });
fakeClock.advance(1000);
```
## API Reference
### `defaultClock: Clock`
- **Type:** `Clock` (from `@suaveplan/types`) — i.e. `() => number`.
- **Returns:** Wall-clock milliseconds since the UNIX epoch via `Date.now()`.
## Implementation Notes
- The `Clock` interface in `@suaveplan/types` is `() => number` — no instance state, just a function call.
- This is intentionally a `const` exported value (not a factory). All callers share the same function reference; testing replaces it by passing a different `Clock` into the consumer's constructor, never by mutating this export.
- The spec-conformance test `simulator.spec-conformance.test.ts` scans for `Date.now()` outside this file and fails the build if any other implementation file calls it. That's the contract this module exists to satisfy.

View File

@@ -0,0 +1,45 @@
# default-rng
## Purpose
`defaultRng` is the canonical CSPRNG-backed `Rng` factory used by `NetworkSimulator` and any other deterministic-when-asked, random-by-default primitive in `@suaveplan/bitpacker-network`. It exists so callers don't have to reach into `@suaveplan/random` directly to obtain the standard random source — and so that swapping the simulator to a seeded RNG in tests is a single keyword argument away.
The factory delegates to `cryptoRng()` from `@suaveplan/random`, which uses `globalThis.crypto.getRandomValues` underneath. In environments without a CSPRNG (some embedded runtimes) the underlying call throws a `RANDOM.SOURCE.UNAVAILABLE` error. We surface that directly: the policy is "if you don't have a CSPRNG, you must inject a seeded `Rng` yourself" — never silently downgrade to `Math.random()`.
## Features
- Zero-config default for production — call `defaultRng()` and you get a `Rng` instance backed by Web Crypto.
- Errors surface immediately when the host lacks a CSPRNG.
- Trivially overridable in tests by passing a `Mulberry32Rng` (or any `Rng`) into the simulator constructor.
- No module-scope state; each call returns a fresh `Rng` instance.
## Usage: Basic Example
```ts
import { defaultRng } from "@suaveplan/bitpacker-network";
const rng = defaultRng();
const value = rng.nextFloat(); // [0, 1)
```
## Usage: Advanced Example
```ts
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
import { NetworkSimulator } from "@suaveplan/bitpacker-network";
// Deterministic test: seed a Mulberry32 PRNG instead of using defaultRng().
const sim = new NetworkSimulator({ rng: new Mulberry32Rng(42), clock: testClock });
```
## API Reference
### `defaultRng(): Rng`
- **Returns:** A new `Rng` instance backed by `cryptoRng()` from `@suaveplan/random`.
- **Throws:** Re-throws `RANDOM.SOURCE.UNAVAILABLE` from `cryptoRng()` if `globalThis.crypto.getRandomValues` is unavailable.
## Implementation Notes
- `Rng` interface from `@suaveplan/types` is the canonical contract; `cryptoRng()` and `Mulberry32Rng` both implement it.
- We surface `cryptoRng()` directly rather than wrapping in a try/catch because callers should know if their environment lacks a CSPRNG — silent fallback would defeat determinism guarantees elsewhere in the simulator.
- This factory is used as the default for `NetworkSimulator`'s `rng` constructor option.

View File

@@ -0,0 +1,58 @@
# Defaults — Clock and RNG factories
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`@suaveplan/bitpacker-network` enforces the determinism rule (no module-scope `Date.now()` or `Math.random()` calls in implementation files). To do that without infecting every consumer with mandatory `clock` / `rng` parameters, this package allow-lists ONE factory per source: `defaultClock` and `defaultRng()`. Every other module reads time and randomness only through the injected primitives passed at construction time, defaulting to these factories.
This is the same pattern `@suaveplan/events` uses (`events/clock/default-clock.ts`) and is the single explicit boundary between deterministic, replayable code paths and the host environment's wall clock / CSPRNG.
## Features
- `defaultClock` — `Clock = () => Date.now()`. Produces ms-since-epoch reads from the host wall clock.
- `defaultRng()` — factory that wraps `@suaveplan/random::cryptoRng()`. Returns a fresh CSPRNG-backed `Rng` per call. Throws if the host has no `crypto.getRandomValues`.
## Usage: Basic Example
```typescript
import { NetworkSimulator } from "@suaveplan/bitpacker-network";
// Without overrides: NetworkSimulator wires defaultClock and defaultRng() internally.
const sim = new NetworkSimulator();
```
## Usage: Advanced Example
```typescript
import { NetworkSimulator } from "@suaveplan/bitpacker-network";
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
// Tests: inject a seeded RNG for reproducible packet-loss patterns.
const seededRng = new Mulberry32Rng(0xDEADBEEF);
const fakeClock = (): number => 0; // every call returns t=0
const sim = new NetworkSimulator({
rng: seededRng,
clock: fakeClock,
});
// Two simulators with the same seed produce byte-identical loss sequences.
```
## API Reference
### `defaultClock: Clock`
Constant. `() => number` returning the current millisecond timestamp via `Date.now()`. Suitable for the simulator's `startTime`, `lastBandwidthCheck`, packet `timestamp`, and `uptime` calculations. NOT monotonic — consumers needing strict monotonicity should inject a different `Clock`.
### `defaultRng(): Rng`
Factory function. Returns a fresh `Rng` backed by `globalThis.crypto.getRandomValues`. Throws `RandomError` (code `RANDOM.SOURCE.UNAVAILABLE`) if the host environment lacks the API. The simulator's constructor calls this lazily — if the consumer always provides their own `rng` option, the factory is never invoked, so older / non-browser hosts that lack `crypto.getRandomValues` can still use the simulator with a seeded `Rng`.
## Implementation Notes
The package's `src/simulator/simulator.spec-conformance.test.ts` includes a grep guard that scans implementation files for `Date.now()` / `Math.random()` and fails the build if either appears outside `defaults/default-clock.ts` and `defaults/default-rng.ts`. Those two files are explicitly allow-listed in the grep — that's the integrity boundary that keeps the rest of the package deterministic given identical injected primitives.
If a future addition needs another determinism-violating API (e.g. `performance.now()`, `process.hrtime()`), the convention is: add a new file under `defaults/` and extend the spec-conformance allow-list to include it. Spreading the violations across implementation files is what the rule prevents.

View File

@@ -0,0 +1,8 @@
## index
**Path:** `bitpacker-network/src/index.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,83 @@
# Bitpacker Network OTel Instruments
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
OTel meter, tracer, counter, and histogram declarations for `@suaveplan/bitpacker-network`. Ships canonical instrumentation primitives for the `NetworkSimulator`'s send/receive boundaries plus the burst-loss event. Centralizing the metric/span vocabulary here lets a downstream consumer attach observability to the simulator without re-deriving names.
The metric and span names follow the suaveplan convention with snake-case for the multi-word package: tracer/meter named `@suaveplan/bitpacker-network`; spans named `bitpacker_network.<operation>` (`send`, `receive`, `burst`); counters/histograms named `bitpacker_network.<scope>.<metric>`.
## Features
- `getBitPackerNetworkTracer()` — returns the package's tracer
- `networkPacketsTotal` — counter labelled by `status` (one of the five `PacketStatus` values)
- `networkLatencyMs` — histogram of simulated latency for delivered packets
- `networkPacketBytes` — histogram of per-packet byte size, labelled by `status`
- `BITPACKER_NETWORK_SPAN_NAMES` — three canonical span names
## Usage: Basic Example
```ts
import {
BITPACKER_NETWORK_SPAN_NAMES,
networkPacketsTotal,
getBitPackerNetworkTracer,
} from "@suaveplan/bitpacker-network";
const tracer = getBitPackerNetworkTracer();
tracer.startActiveSpan(BITPACKER_NETWORK_SPAN_NAMES.send, (span) => {
try {
// send work
networkPacketsTotal.add(1, { status: "delivered" });
} finally {
span.end();
}
});
```
## Usage: Advanced Example
```ts
import {
networkLatencyMs,
networkPacketsTotal,
PacketStatus,
} from "@suaveplan/bitpacker-network";
function recordOutcome(status: PacketStatus, latencyMs: number, bytes: number) {
networkPacketsTotal.add(1, { status });
if (status === PacketStatus.DELIVERED) {
networkLatencyMs.record(latencyMs);
}
}
```
## API Reference
### `getBitPackerNetworkTracer(): Tracer`
Returns the OTel `Tracer` instance for the package.
### `networkPacketsTotal: Counter`
Counter labelled by `status: "delivered" | "lost" | "duplicated" | "corrupted" | "reordered"`.
### `networkLatencyMs: Histogram`
Histogram of simulated per-packet latency in milliseconds, recorded only on `delivered` packets.
### `networkPacketBytes: Histogram`
Histogram of per-packet byte size, labelled by `status`.
### `BITPACKER_NETWORK_SPAN_NAMES`
Const map: `{ send, receive, burst }` → canonical span names.
## Implementation Notes
- **Status is a label, not separate counters.** `networkPacketsTotal{status="lost"}` is more flexible than separate `dropped_total`/`reordered_total`/etc; new packet states fold in without schema churn.
- **Latency only on delivered.** Lost/dropped packets have no meaningful latency. Recording would distort the latency histogram with zeros.
- **No per-packet spans.** A simulator running 10k packets/s would produce 10k spans/s if each were instrumented. The counter-and-histogram pattern is sufficient at this rate.

View File

@@ -0,0 +1,61 @@
# Register Schemas — `@suaveplan/bitpacker-network`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-network` package. Exposes `registerBitPackerNetworkSchemas(registry)` which converts the package's two public boundary schemas into JSON Schema form and registers them under `bitpacker:@suaveplan/bitpacker-network` at version 1. Re-registration on the same registry is a no-op.
The two schemas covered are: `networkSimulationOptions` (the user-facing simulation profile) and `networkSimulatorConstructorOptions` (the same options plus the `rng`/`clock` determinism injection points).
## Features
- `BITPACKER_NETWORK_SUBJECT` constant — `"bitpacker:@suaveplan/bitpacker-network"`
- `BITPACKER_NETWORK_SCHEMA_VERSION` constant — `1`
- `registerBitPackerNetworkSchemas(registry)` — idempotent registration
## Usage: Basic Example
```ts
import { createRegistry } from "@suaveplan/schema-registry";
import {
BITPACKER_NETWORK_SUBJECT,
registerBitPackerNetworkSchemas,
} from "@suaveplan/bitpacker-network/register-schemas";
const registry = createRegistry();
registerBitPackerNetworkSchemas(registry);
```
## Usage: Advanced Example
```ts
import { createRegistry, SchemaRegistry } from "@suaveplan/schema-registry";
import { registerBitPackerSchemas } from "@suaveplan/bitpacker/register-schemas";
import { registerBitPackerNetworkSchemas } from "@suaveplan/bitpacker-network/register-schemas";
const registry = createRegistry();
registerBitPackerSchemas(registry);
registerBitPackerNetworkSchemas(registry);
```
## API Reference
### `BITPACKER_NETWORK_SUBJECT: "bitpacker:@suaveplan/bitpacker-network"`
Const string.
### `BITPACKER_NETWORK_SCHEMA_VERSION: 1`
Const number.
### `registerBitPackerNetworkSchemas(registry: SchemaRegistry): void`
Idempotent registration. Returns without writing if the subject is already present.
## Implementation Notes
- **Two-key payload.** `networkSimulationOptions` for the user-facing config; `networkSimulatorConstructorOptions` for the constructor bundle that adds `rng`/`clock`.
- **`unrepresentable: "any"`.** Required because the constructor schema uses `z.custom` for the `Rng` and `Clock` injection points.

View File

@@ -0,0 +1,156 @@
# NetworkSimulator
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - simulator.schema — Zod schema for `NetworkSimulator…
- [simulator.schema](./simulator.schema.md) — Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSi…
- simulator.schema — Zod schema for `NetworkSimulator…
- [simulator.schema](./simulator.schema.md) — Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSi…
- simulator.schema — Zod schema for `NetworkSimulator…
- [simulator.schema](./simulator.schema.md) — Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSi…
- simulator.schema — Zod schema for `NetworkSimulator…
- [simulator.schema](./simulator.schema.md) — Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSi…
- [simulator.schema](./simulator.schema.md) — Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSi…
## Purpose
`NetworkSimulator` simulates a network channel for testing real-time and multiplayer code paths without involving actual transport. It models latency, jitter, packet loss, duplication, corruption, reordering, bandwidth caps, burstiness, and disconnects, with deterministic, replayable outputs given a seeded `Rng`. Tests inject the seed; production code uses the package default (`cryptoRng()` from `@suaveplan/random`).
This is the port of upstream `bitpacker/src/network/network-simulation.ts`. The audit-driven hardening replaces every upstream `Math.random()` and `Date.now()` call with injected `Rng` and `Clock` so simulator behaviour is reproducible across runs given identical inputs. The class composes a minimal typed listener table internally rather than extending an `EventEmitter`, keeping the public surface minimal.
## Features
- Eight typed events: `packet:sent`, `packet:received`, `packet:lost`, `packet:duplicated`, `packet:corrupted`, `packet:reordered`, `bandwidth:throttled`, `stats:updated`, plus `connection:disconnected` / `connection:reconnected`.
- Seven `NetworkCondition` presets (`PERFECT`, `GOOD`, `AVERAGE`, `POOR`, `TERRIBLE`, `MOBILE`, `SATELLITE`) plus `CUSTOM` for caller-supplied options.
- Bandwidth throttling enforced over a rolling 1-second window.
- Burstiness scheduler simulates periodic high-loss windows.
- Random-disconnect scheduler samples exponentially-distributed time-to-event.
- Stat counters with overflow guard — `packetsSent` near `MAX_SAFE_INTEGER` throws `BITPACKER.NETWORK_STAT_OVERFLOW`.
- `Rng` and `Clock` injection for deterministic replay; defaults are `cryptoRng()` and `defaultClock`.
## Usage: Basic Example
```typescript
import {
NetworkCondition,
NetworkSimulator,
} from "@suaveplan/bitpacker-network";
const sim = new NetworkSimulator({ condition: NetworkCondition.MOBILE });
sim.on("packet:received", (packet) => {
console.log(`got ${packet.size} bytes after ${packet.timestamp}ms`);
});
sim.on("packet:lost", () => {
console.log("packet lost");
});
const id = sim.sendPacket(new Uint8Array([1, 2, 3, 4]));
```
## Usage: Advanced Example
```typescript
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
import { NetworkSimulator } from "@suaveplan/bitpacker-network";
// Deterministic replay: the same seed produces the same loss / duplication /
// corruption / reordering pattern on every run, so a regression test can
// pin a specific simulator outcome.
const seededRng = new Mulberry32Rng(0xCAFE_F00D);
let now = 0;
const fakeClock = (): number => now;
const sim = new NetworkSimulator({
rng: seededRng,
clock: fakeClock,
latencyMin: 50,
latencyMax: 100,
jitter: 10,
packetLoss: 5,
packetDuplication: 1,
enableThrottling: true,
bandwidthKbps: 1000,
});
let receivedCount = 0;
sim.on("packet:received", () => receivedCount++);
for (let i = 0; i < 1000; i++) {
sim.sendPacket(new Uint8Array([i & 0xff]));
now += 10;
}
const stats = sim.getStats();
// stats are reproducible across runs given the same seed + inputs
```
## API Reference
### `new NetworkSimulator(options?)`
`options` extends `NetworkSimulationOptions` with optional `rng?: Rng` and `clock?: Clock`. Defaults: `cryptoRng()` and `defaultClock`.
### `sendPacket(data, callback?) → number`
Submit a packet. Returns the assigned id, or `-1` if disconnected. The simulator decides the packet's fate (delivered / lost / duplicated / corrupted / reordered) using the configured probabilities and the injected `Rng`. Callback fires once on the packet's terminal status.
### `on(event, listener) → () => void`
### `off(event, listener) → void`
### `removeAllListeners() → void`
Typed listener registry. `on` returns an unsubscribe function.
### `getStats() → NetworkSimulationStats`
Snapshot of aggregate counters and rolling averages. Refreshes derived fields (`uptime`, `packetLossRate`, `throughputKbps`) before returning.
### `resetStats() → void`
Zero all counters and reset `startTime` (useful between independent test runs of one simulator).
### `setOptions(options) → void`
Reconfigure a running simulator. Re-arms or cancels burstiness / random-disconnect timers based on the new options.
### `getOptions() → NetworkSimulationOptions`
Snapshot of the current options.
### `disconnect() → void` / `reconnect() → void` / `isConnected() → boolean`
Manual connection control (bypasses random-disconnect scheduler).
### `getQueueSize() → number` / `clearQueues() → void`
Inspection / reset of in-flight packet and reorder queues.
### `destroy() → void`
Stops every timer, clears queues, and removes every listener. Idempotent.
### `createNetworkSimulator(condition, options?) → NetworkSimulator`
Factory shortcut for `new NetworkSimulator({ condition, ...options })`.
## Implementation Notes
The simulator deliberately does not extend `@suaveplan/events::EventBus`. The bus is overkill for the eight-event surface here (we don't need interceptors, FIFO ordering across cross-plugin topics, or backpressure), and inheriting from a typed bus would leak its full surface into `NetworkSimulator`'s public API. The minimal internal listener table covers the simulator's observability needs without that overhead.
The `STAT_OVERFLOW_GUARD = MAX_SAFE_INTEGER - 1024` margin reserves enough room for one packet to land cleanly even at the boundary; calling `sendPacket` while `packetsSent` is at or above this guard throws `BITPACKER.NETWORK_STAT_OVERFLOW`. Consumers running multi-day soak tests should `resetStats()` periodically.
Bandwidth throttling uses a 1-second rolling window: bytes sent within a window count toward the cap; once the cap is reached, further packets in that window emit `bandwidth:throttled` and the per-send callback fires with `PacketStatus.LOST`. The window resets when `now - lastBandwidthCheck > 1000` ms.
Burstiness schedules a 1-second-long burst window every `burstInterval` ms (default 5000). During a burst, `burstPacketLoss` (default 25%) replaces the normal `packetLoss` for every packet sent. The scheduler self-rearms on every burst end.
Random disconnects use an exponential-distribution sample for time-to-event: `t = -log(U) / λ` where `U ~ Uniform(0,1)`. The simulator guards against `U = 0` (which would produce `Infinity`) by clamping to `Number.EPSILON`. After disconnect, reconnection happens after a uniformly random 5–15 s delay, then the scheduler rearms.
The `rng.nextFloat()` calls for loss / duplication / corruption / reordering decisions all happen sequentially in `sendPacket` — a seeded `Rng` produces a fixed sequence of decisions across runs, which is what makes test-time replay possible.

View File

@@ -0,0 +1,70 @@
# Network Simulator Constructor Options Schema
## Purpose
Zod schema for `NetworkSimulatorConstructorOptions`, the constructor bundle for `NetworkSimulator` and `createNetworkSimulator`. Extends the `NetworkSimulationOptions` validation with two determinism injection points — `rng` (a structural `Rng` from `@suaveplan/types`) and `clock` (a structural `Clock`) — so test fakes flow through with referential identity preserved.
The schema validates the structural duck-types of `Rng` and `Clock` rather than requiring concrete classes, because tests inject `fakeRng()` and `fakeClock()` from `@suaveplan/testing` that satisfy the shape without inheriting from a base class. Reflecting that flexibility in the schema keeps the test surface clean.
## Features
- `networkSimulatorConstructorOptionsSchema` — strict, extends the simulation options bundle
- Structural `Rng` validation — must expose `nextFloat(): number`
- Structural `Clock` validation — must expose `now(): number`
- Identity preservation for `rng` and `clock` via `z.custom`
- `as unknown as z.ZodType<T>` cast for the EOPT mismatch
- `networkSimulationOptionsSchema` is re-exported for convenience
## Usage: Basic Example
```ts
import {
NetworkSimulator,
networkSimulatorConstructorOptionsSchema,
defaultRng,
defaultClock,
} from "@suaveplan/bitpacker-network";
const opts = networkSimulatorConstructorOptionsSchema.parse({
packetLoss: 1,
rng: defaultRng(),
clock: defaultClock(),
});
const sim = new NetworkSimulator(opts);
```
## Usage: Advanced Example
```ts
import {
createNetworkSimulator,
networkSimulatorConstructorOptionsSchema,
} from "@suaveplan/bitpacker-network";
import { fakeClock, fakeRng } from "@suaveplan/testing/runner";
function deterministicSim(seed: number) {
return createNetworkSimulator(
networkSimulatorConstructorOptionsSchema.parse({
packetLoss: 5,
rng: fakeRng(seed),
clock: fakeClock(),
})
);
}
```
## API Reference
### `networkSimulatorConstructorOptionsSchema: z.ZodType<NetworkSimulatorConstructorOptions>`
Strict schema extending the simulation options shape with `rng` and `clock`. Both injection points are validated structurally and preserved by identity. Unknown keys throw.
### `networkSimulationOptionsSchema` (re-exported)
The base options schema; see `../types/types.schema.md` for full details.
## Implementation Notes
- **Structural `Rng` / `Clock`.** Test fakes from `@suaveplan/testing` are duck-typed objects, not class instances. Validating via `z.custom<T>((v): v is T => …)` accepts both the class form and the duck-type, and preserves identity for both.
- **Inline option fields, not extension.** Zod doesn't have a clean `extend` story for `z.ZodType`-typed schemas, so the constructor schema re-states the option fields rather than composing the simulation options schema. The runtime behaviour is identical; the duplication is acceptable for one schema.
- **Identity matters for tests.** A test that injects `fakeRng()` then asserts on its emitted call sequence requires `===` equality between the injected and consumed instance; `z.function()`-style wrapping would break this.

View File

@@ -0,0 +1,97 @@
# Network simulation types
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - types.schema — Zod schema for `NetworkSimulationOpt…
- [types.schema](./types.schema.md) — Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptio…
- types.schema — Zod schema for `NetworkSimulationOpt…
- [types.schema](./types.schema.md) — Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptio…
- types.schema — Zod schema for `NetworkSimulationOpt…
- [types.schema](./types.schema.md) — Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptio…
- types.schema — Zod schema for `NetworkSimulationOpt…
- [types.schema](./types.schema.md) — Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptio…
- [types.schema](./types.schema.md) — Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptio…
## Purpose
Defines the type companions and constant tables used by `NetworkSimulator`. These types were previously colocated with the misplaced simulator implementation in `@suaveplan/bitpacker-game/src/types.ts`; the 2026-05-02 audit moved them here so the network-simulator concern lives in one package. The shapes and default values match upstream's `bitpacker/src/types.ts` byte-for-byte to preserve wire-compat with any code that branches on these enums.
## Features
- `NetworkCondition` enum with seven presets (`PERFECT`, `GOOD`, `AVERAGE`, `POOR`, `TERRIBLE`, `MOBILE`, `SATELLITE`) plus `CUSTOM` for caller-supplied configurations.
- `PacketStatus` enum reflecting the simulator's per-packet outcomes (`DELIVERED`, `LOST`, `DUPLICATED`, `CORRUPTED`, `REORDERED`).
- `NetworkSimulationOptions` covering latency / jitter / loss / duplication / corruption / reordering / bandwidth / disconnect / MTU / burstiness.
- `NETWORK_CONDITIONS` map binding each condition to a baseline options object — picking `condition: NetworkCondition.MOBILE` populates the seven mobile-network defaults.
- `DEFAULT_NETWORK_SIMULATION_OPTIONS` for `CUSTOM` and absent-condition cases.
- `PacketInfo` — packet metadata emitted via `packet:*` events.
- `NetworkSimulationStats` — aggregate counters reported by `getStats()`.
## Usage: Basic Example
```typescript
import {
NetworkCondition,
type NetworkSimulationOptions,
} from "@suaveplan/bitpacker-network";
const opts: NetworkSimulationOptions = {
condition: NetworkCondition.MOBILE,
};
```
## Usage: Advanced Example
```typescript
import {
NETWORK_CONDITIONS,
NetworkCondition,
type NetworkSimulationOptions,
} from "@suaveplan/bitpacker-network";
// Override one field of a preset:
const opts: NetworkSimulationOptions = {
...NETWORK_CONDITIONS[NetworkCondition.SATELLITE],
packetLoss: 0, // perfect satellite — useful for diff studies
};
```
## API Reference
### `enum NetworkCondition`
Closed list of named profiles. Adding a value here is a wire-format extension; renaming or removing one is a wire-format break.
### `enum PacketStatus`
Closed list of per-packet outcomes the simulator reports.
### `interface NetworkSimulationOptions`
Caller-side configuration. All fields optional. Numeric percentages are 0–100 (not 0–1) — match upstream convention.
### `const DEFAULT_NETWORK_SIMULATION_OPTIONS`
Baseline used when no `condition` is supplied or when `condition` is `CUSTOM`.
### `const NETWORK_CONDITIONS`
Lookup table mapping every `NetworkCondition` to the base options for that preset.
### `interface PacketInfo`
Emitted alongside `packet:*` events. `data` is a fresh `Uint8Array` copy (the simulator deep-copies on send so the caller's buffer isn't shared with the simulated network).
### `interface NetworkSimulationStats`
Counters and rolling averages. Read via `simulator.getStats()` or via the `stats:updated` event.
## Implementation Notes
`NETWORK_CONDITIONS[NetworkCondition.CUSTOM]` is `{...DEFAULT_NETWORK_SIMULATION_OPTIONS}` — choosing `CUSTOM` is equivalent to omitting the condition entirely. The simulator constructor honours either spelling.
The percentage convention (0–100, not 0–1) is the upstream convention preserved verbatim. Switching to 0–1 would be a breaking config change. If a future redesign normalises this, document it as a wire-format extension.

View File

@@ -0,0 +1,72 @@
# Network Simulation Options Schema
## Purpose
Zod schema for `NetworkSimulationOptions`, the configuration accepted by `NetworkSimulator`'s constructor and `setOptions`. Validates the configuration at the package boundary so an out-of-range percentage (`packetLoss: 150`), a negative latency, or an unknown key all fail at parse time rather than producing a simulator that silently misbehaves under load.
The schema treats the `*Loss`, `*Duplication`, `corruption`, `*Reordering`, `disconnectChance`, and `burstPacketLoss` fields as percentages bounded to `[0, 100]` because that's the simulator's documented contract. Latency and bandwidth fields are non-negative; the boolean toggles are unbounded.
## Features
- `networkSimulationOptionsSchema` — strict, every field optional with documented bounds
- Percentages enforced to `[0, 100]` (not `[0, 1]`)
- `condition` token must be a `NetworkCondition` enum value
- `as unknown as z.ZodType<T>` cast for the EOPT mismatch
## Usage: Basic Example
```ts
import {
NetworkSimulator,
networkSimulationOptionsSchema,
NetworkCondition,
} from "@suaveplan/bitpacker-network";
const opts = networkSimulationOptionsSchema.parse({
condition: NetworkCondition.AVERAGE,
packetLoss: 1.5,
});
const sim = new NetworkSimulator(opts);
```
## Usage: Advanced Example
```ts
import {
createNetworkSimulator,
networkSimulationOptionsSchema,
NetworkCondition,
} from "@suaveplan/bitpacker-network";
function safeSimulator(rawOpts: unknown) {
const result = networkSimulationOptionsSchema.safeParse(rawOpts);
if (!result.success) {
throw new Error(
`Network simulation options rejected: ${result.error.message}`
);
}
return createNetworkSimulator(result.data);
}
const sim = safeSimulator({
latencyMin: 50,
latencyMax: 200,
jitter: 25,
packetLoss: 5,
enableBurstiness: true,
condition: NetworkCondition.MOBILE,
});
```
## API Reference
### `networkSimulationOptionsSchema: z.ZodType<NetworkSimulationOptions>`
Strict schema. Every field is optional. Numeric percentages (`packetLoss`, `packetDuplication`, `corruption`, `packetReordering`, `disconnectChance`, `burstPacketLoss`) are bounded to `[0, 100]`. Latency and timing fields are non-negative finite numbers. `mtu` and `packetBatchSize` are non-negative integers (mtu strictly positive). `condition` is a `NetworkCondition` enum value.
## Implementation Notes
- **Percentages are `[0, 100]`, not `[0, 1]`.** The simulator interprets `packetLoss: 5` as 5% loss. Validating `[0, 1]` would silently allow `5` which the simulator would then floor or treat as 100%, masking the bug. Refusing it at parse time keeps the contract honest.
- **`mtu` is positive.** A zero or negative MTU produces an undefined transmission state; refusing it forces the caller to either omit the field (defaults to 1500) or supply a meaningful value.
- **`condition: CUSTOM` honours every other field.** The schema does NOT enforce mutual exclusion between `condition` and the per-field overrides, because the simulator deliberately allows callers to start from a preset and override individual fields.
- **Strict mode.** Unknown keys are rejected; misspelled flags would otherwise silently default and hide configuration bugs.

View File

@@ -0,0 +1,71 @@
# @suaveplan/bitpacker-schema - Schema registry, validation, and versioning for @suaveplan/bitpacker
## Summary
Schema registry and validation for bitpacker.
<!-- begin:auto-docs -->
## Documentation tree
- [matching](./src/matching/matching.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [matching](./src/matching/matching.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- default-clock — <!-- begin:auto-docs --> _(no submod…
- [defaults](./src/registry/defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [default-clock](./src/registry/defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [factories](./src/registry/factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [factories](./src/registry/factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [in-memory](./src/registry/storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [in-memory](./src/registry/storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [local-storage](./src/registry/storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [local-storage](./src/registry/storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- default-clock — <!-- begin:auto-docs --> _(no submod…
- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [versioning](./src/versioning/versioning.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [versioning](./src/versioning/versioning.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [visualization](./src/visualization/visualization.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [visualization](./src/visualization/visualization.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [index](./src/index.md) — **Path:** `bitpacker-schema/src/index.ts` **Export:** See source for details.
<!-- end:auto-docs --> - [matching](./src/matching/matching.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults](./src/registry/defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-clock](./src/registry/defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./src/registry/factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./src/registry/factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./src/registry/storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./src/registry/storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/registry/storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/registry/storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [versioning](./src/versioning/versioning.md) — <!-- begin:auto-docs --> _(no submodules)_- [versioning](./src/versioning/versioning.md) — <!-- begin:auto-docs --> _(no submodules)_- [visualization](./src/visualization/visualization.md) — <!-- begin:auto-docs --> _(no submodules)_- [visualization](./src/visualization/visualization.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-schema/src/index.ts` **Export:** See source for details.
- [matching](./src/matching/matching.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- default-clock — <!-- begin:a…
- [defaults](./src/registry/defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-clock](./src/registry/defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./src/registry/factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./src/registry/factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./src/registry/storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./src/registry/storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/registry/storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/registry/storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- default-clock — <!-- begin:a…
- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [versioning](./src/versioning/versioning.md) — <!-- begin:auto-docs --> _(no submodules)_- [versioning](./src/versioning/versioning.md) — <!-- begin:auto-docs --> _(no submodules)_- [visualization](./src/visualization/visualization.md) — <!-- begin:auto-docs --> _(no submodules)_- [visualization](./src/visualization/visualization.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-schema/src/index.ts` **Export:** See source for details.
- [matching](./src/matching/matching.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- default-clock — <!-- begin:auto-docs --> _(no submo…
- [defaults](./src/registry/defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-clock](./src/registry/defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./src/registry/factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./src/registry/factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./src/registry/storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./src/registry/storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/registry/storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/registry/storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- default-clock — <!-- begin:auto-docs --> _(no submo…
- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [versioning](./src/versioning/versioning.md) — <!-- begin:auto-docs --> _(no submodules)_- [versioning](./src/versioning/versioning.md) — <!-- begin:auto-docs --> _(no submodules)_- [visualization](./src/visualization/visualization.md) — <!-- begin:auto-docs --> _(no submodules)_- [visualization](./src/visualization/visualization.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-schema/src/index.ts` **Export:** See source for details.
- [matching](./src/matching/matching.md) — <!-- begin:auto-docs --> _(no submodules)_- [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_ - [migration](./src/migration/migration.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ - [defaults](./src/registry/defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_ - [default-clock](./src/registry/defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_ - [factories](./src/registry/factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_ - [factories](./src/registry/factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_ - [in-memory](./src/registry/storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [in-memory](./src/registry/storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [local-storage](./src/registry/storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ - [local-storage](./src/registry/storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ - [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_ - [validation](./src/validation/validation.md) — <!-- begin:auto-docs --> _(no submodules)_- [versioning](./src/versioning/versioning.md) — <!-- begin:auto-docs --> _(no submodules)_ - [versioning](./src/versioning/versioning.md) — <!-- begin:auto-docs --> _(no submodules)_- [visualization](./src/visualization/visualization.md) — <!-- begin:auto-docs --> _(no submodules)_ - [visualization](./src/visualization/visualization.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-schema/src/index.ts` **Export:** See source for details.
## Install
```bash
bun add @suaveplan/bitpacker-schema
```
## Usage
```typescript
import { SchemaRegistry, validateData, assertValid } from "@suaveplan/bitpacker-schema";
const registry = new SchemaRegistry();
registry.register("player", { x: 255, y: 255, health: 100 });
const schema = registry.resolve("player"); // returns the registered schema
const names = registry.listNames(); // ["player"]
// Validation
const errors = validateData(42, 255);
expect(errors).toHaveLength(0);
const badErrors = validateData(300, 255);
expect(badErrors.length).toBeGreaterThan(0);
assertValid(42, 255); // ok
assertValid(300, 255); // throws ValidationError
```
## Modules
- `src/registry.ts` — `SchemaRegistry`: name → schema mapping
- `src/validation.ts` — `validateData`, `assertValid`: runtime schema validation

View File

@@ -0,0 +1,8 @@
## index
**Path:** `bitpacker-schema/src/index.ts`
**Export:** See source for details.
### What it does
Part of the @suaveplan/ package.

View File

@@ -0,0 +1,66 @@
# Schema matching
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`matchSchemas` compares two `BitPackerSchema` values and reports their structural differences as a `SchemaMatchResult` of additions, subtractions, and type-mismatches. It is the canonical "are these two schema versions compatible?" primitive: the version-evolution layer (`VersionedSchema`, the migration engine, the schema registry) calls into it to decide whether two adjacent versions are compatible, what migrations are needed, and how to surface breaking changes to the developer. The shape mirrors a standard diff output so it can plug straight into compatibility tooling, docs generation, and CI checks.
## Features
- Recursive structural comparison across objects, arrays, primitives, and the `_optional` / `_oneOf` / `_conditional` operators.
- Three buckets (`additions`, `subtractions`, `mismatches`) so callers can apply asymmetric policies (additions allowed, mismatches blocked, etc.).
- Path-based reporting — every difference is keyed by a dotted path through the schema tree.
- Recognises optionality so adding a required field surfaces as a mismatch, but adding an optional field as a pure addition.
## Usage: Basic Example
```ts
import { matchSchemas } from "@suaveplan/bitpacker-schema";
const oldSchema = { name: "-16", age: 255 };
const newSchema = { name: "-16", age: 255, email: { _optional: "-32" } };
const result = matchSchemas(oldSchema, newSchema);
// result.additions === [".email"]
// result.subtractions === []
// result.mismatches === []
```
## Usage: Advanced Example
```ts
import { matchSchemas } from "@suaveplan/bitpacker-schema";
// CI gate: fail PRs that introduce structural mismatches but allow
// pure additions / subtractions (which the migration engine handles).
const result = matchSchemas(prevVersion.schema, currentVersion.schema);
if (result.mismatches.length > 0) {
throw new Error(
`Schema mismatch (breaking change): ${result.mismatches.join(", ")}`
);
}
```
## API Reference
### `interface SchemaMatchResult`
```ts
interface SchemaMatchResult {
additions: string[]; // paths present in B but not A
subtractions: string[]; // paths present in A but not B
mismatches: string[]; // paths present in both with different types
}
```
### `matchSchemas(a, b): SchemaMatchResult`
- `a, b: BitPackerSchema`
- Returns the structural diff. Pure-data utility, no side effects.
## Implementation Notes
- An optional-to-required transition counts as a mismatch (loosening or tightening the contract is a breaking change).
- Within `_oneOf` arms, the matcher compares the union as a set rather than positionally — adding a new arm is an `addition` whether it appears at the end or the start.
- Float-precision and bit-width differences within numeric primitives surface as mismatches even when the schema text differs only in token form (e.g. `"f32"` vs `"f64"`).

View File

@@ -0,0 +1,95 @@
# Schema Migration
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`migration.ts` ports the bit-packer schema migration toolkit from
upstream. A migration describes how to transform data shaped by an old
`BitPackerSchema` into data shaped by a newer one — adding fields,
removing fields, renaming, changing types, splitting, merging, or
running a fully custom transform. The toolkit is designed to be
composable: a single migration step is a `MigrationFunction`, a strategy
is one of `MigrationStrategy`, and a chain of strategies forms a path
between two schema versions tracked by `bitpacker-schema/versioning/`.
This module is the data-shape side of schema evolution. The companion
`versioning.ts` module owns version registration, lookup, and
chain-execution; this file owns the per-step transform vocabulary,
operation descriptors, and the helper builders that turn a strategy
into a runnable function.
## Features
- `MigrationStrategy` enum covering the eight common transforms
(`ADD_FIELD`, `REMOVE_FIELD`, `RENAME_FIELD`, `CHANGE_FIELD_TYPE`,
`MOVE_FIELD`, `SPLIT_FIELD`, `MERGE_FIELDS`, `CUSTOM`).
- `MigrationOperation` descriptor — a serialisable record of one step.
- `MigrationFunction<T, U>` — a pure data transform, no side effects.
- `SchemaTypeToDataType<T>` helper — propagates inferred runtime types.
- Pure functions; no state, no I/O.
## Usage: Basic Example
```ts
import {
MigrationStrategy,
type MigrationFunction,
} from "@suaveplan/bitpacker-schema";
// A v1 record is a plain `{ id }`; v2 adds `createdAt`.
const addCreatedAt: MigrationFunction = (data) => ({
...(data as Record<string, unknown>),
createdAt: new Date().toISOString(),
});
```
## Usage: Advanced Example
Compose multiple migrations through the versioning store. The
`migration.ts` module supplies the building blocks; `versioning.ts`
chains them.
```ts
import {
MigrationStrategy,
type MigrationFunction,
type MigrationOperation,
} from "@suaveplan/bitpacker-schema";
const renameUserId: MigrationFunction = (raw) => {
const r = raw as Record<string, unknown>;
const { user_id, ...rest } = r;
return { ...rest, userId: user_id };
};
const renameOp: MigrationOperation = {
strategy: MigrationStrategy.RENAME_FIELD,
fromField: "user_id",
toField: "userId",
};
// Combined with versioning's `addMigration(fromVersion, toVersion, fn)`,
// the v2 -> v3 hop becomes a deterministic, replayable transform.
```
## API Reference
- `enum MigrationStrategy` — eight strategy tags.
- `interface MigrationOperation` — `strategy`, plus optional
`fromField` / `toField` / `fromType` / `toType` / `metadata`.
- `type MigrationFunction<T, U>` — `(data: T) => U`.
- `type SchemaTypeToDataType<T>` — placeholder for runtime data type
inference; ported as `unknown` (upstream uses ad-hoc inference).
## Implementation Notes
- This file deliberately avoids any reference to actual
`BitPackerSchema` evaluation; it is shape-only metadata. Execution
lives in `versioning.ts` where chains are walked.
- Strategies map 1:1 with the upstream enum so that migrations
authored against the old API port without modification.
- The `CUSTOM` strategy is the escape hatch for transforms that don't
fit the other seven; the consumer supplies an arbitrary
`MigrationFunction` and a description.

View File

@@ -0,0 +1,56 @@
# defaultClock (schema-registry)
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`defaultClock` is the single point in `@suaveplan/bitpacker-schema/registry` where the wall clock is read. The `SchemaRegistry` class accepts an injectable `Clock` for stamping `createdAt`/`updatedAt` on registered entries; when no clock is provided, it falls back to this default.
Centralising the `Date.now()` call this way matches the pattern used by `@suaveplan/bitpacker-streaming` and `@suaveplan/bitpacker-network`. suaveplan enforces a "no module-scope `Date.now()`/`Math.random()`" rule outside allow-listed defaults — this file is one of those defaults.
## Features
- Single named export `defaultClock: Clock` typed via `@suaveplan/types`.
- Pure function — no side effects beyond reading the wall clock.
- Idiomatic Suaveplan convention for clock injection.
## Usage: Basic Example
```ts
import { defaultClock } from "@suaveplan/bitpacker-schema";
const now = defaultClock();
```
## Usage: Advanced Example
```ts
import type { Clock } from "@suaveplan/types";
import { SchemaRegistry } from "@suaveplan/bitpacker-schema";
// Inject a deterministic clock for reproducible test timestamps.
let t = 0;
const fakeClock: Clock = () => {
t += 1000;
return t;
};
const registry = new SchemaRegistry(undefined, { clock: fakeClock });
await registry.registerSchema(
{ id: "uuid" },
{ namespace: "events", name: "user-created" }
);
```
## API Reference
### `defaultClock: Clock`
Returns `Date.now()` — the current wall-clock time in milliseconds.
## Implementation Notes
The `Clock` type is `() => number`. suaveplan prefers function types over class instances for monotonic primitives because they are trivially serialisable, comparable, and substitutable in tests without dependency-injection frameworks.
The reason this lives in `registry/defaults/` rather than as a static class member is the lint rule that bans module-scope `Date.now()` calls in production code. By scoping the call to a single named, lint-allow-listed module, we keep the rest of the package free of static analysis exceptions.

Some files were not shown because too many files have changed in this diff Show More