Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

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.

View File

@@ -0,0 +1,75 @@
# SchemaRegistry Factories
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createInMemorySchemaRegistry` and `createLocalStorageSchemaRegistry` are the convenience factories for the two shipping `SchemaRegistry` configurations. They mirror the upstream factory shape so consumers porting from `bitpacker/schema/registry` only need to change the import path.
Tests and ephemeral registries call `createInMemorySchemaRegistry`; browser apps wanting persistence across page loads call `createLocalStorageSchemaRegistry`. Both forward an optional `SchemaRegistryOptions` for clock injection.
## Features
- Two factories, one per shipping storage backend.
- Forward `SchemaRegistryOptions` (currently just `clock`) so tests can stamp deterministic timestamps without manually constructing the registry.
- Forward `LocalStorageSchemaRegistryStorageOptions` for the localStorage variant — caller can inject a stub store and a custom storage key.
## Usage: Basic Example
```ts
import {
createInMemorySchemaRegistry,
createLocalStorageSchemaRegistry,
} from "@suaveplan/bitpacker-schema";
const ephemeral = createInMemorySchemaRegistry();
const persistent = createLocalStorageSchemaRegistry({ storageKey: "schemas" });
```
## Usage: Advanced Example
```ts
import type { Clock } from "@suaveplan/types";
import {
createInMemorySchemaRegistry,
createLocalStorageSchemaRegistry,
type KeyValueStringStore,
} from "@suaveplan/bitpacker-schema";
let t = 0;
const fakeClock: Clock = () => {
t += 1000;
return t;
};
const inMemory = createInMemorySchemaRegistry({ clock: fakeClock });
const m = new Map<string, string>();
const stub: KeyValueStringStore = {
getItem: (k) => m.get(k) ?? null,
setItem: (k, v) => m.set(k, v),
};
const persistent = createLocalStorageSchemaRegistry(
{ store: stub, storageKey: "test:schemas" },
{ clock: fakeClock }
);
```
## API Reference
### `createInMemorySchemaRegistry(options?) → SchemaRegistry`
`options.clock?` — injectable `Clock`. Defaults to `defaultClock`.
### `createLocalStorageSchemaRegistry(storageOptions?, options?) → SchemaRegistry`
`storageOptions.store?` — any `KeyValueStringStore`. Defaults to `globalThis.localStorage`.
`storageOptions.storageKey?` — defaults to `"bitpacker-schema-registry"`.
`options.clock?` — injectable `Clock`. Defaults to `defaultClock`.
## Implementation Notes
The factories are thin wrappers. They exist primarily for upstream-shape parity and discoverability — having `createInMemorySchemaRegistry` show up in code completion is more obvious than asking the caller to know to import both `SchemaRegistry` and `InMemorySchemaRegistryStorage` and call `new`.
Splitting the localStorage factory's argument list into two — `storageOptions` first, then `SchemaRegistryOptions` — is intentional. The storage options are the more common knob to twist (different storage keys per app), so they take the leading slot.

View File

@@ -0,0 +1,112 @@
# SchemaRegistry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [default-clock](./defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_ - [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ - [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- default-clock — <!-- begin:auto-docs --> _(no submod…
- [default-clock](./defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-clock](./defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- default-clock — <!-- begin:a…
- [default-clock](./defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- default-clock — <!-- begin:auto-docs --> _(no submo…
- [default-clock](./defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_ - [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ - [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
`SchemaRegistry` is the versioned, namespaced schema store at the heart of `@suaveplan/bitpacker-schema`. Callers register `BitPackerSchema` definitions under a `(namespace, name)` pair; each call gets an auto-incremented version stamped with `createdAt`/`updatedAt` timestamps. Lookups can target an exact version or "latest", and an entire `(namespace, name)` family can be reconstructed as a `VersionedSchema` for migration workflows.
The registry composes over a pluggable `SchemaRegistryStorage` adapter — the same class works against in-memory, browser-localStorage, or any future server-side persistence backend. Tests build the registry over the in-memory adapter; production browsers use the localStorage adapter; production servers wire to whatever storage already exists in the deployment.
## Features
- Auto-versioning: `registerSchema` reads the current latest version for `(namespace, name)` and emits the next integer version.
- Bulk versioning: `registerVersionedSchema` imports every version of a `VersionedSchema` in a single call, preserving the original `createdAt` per version.
- Latest-or-exact lookup: `getSchema(namespace, name)` returns the highest-versioned row; `getSchema(namespace, name, version)` targets that specific version.
- `getVersionedSchema` reconstructs a `VersionedSchema` from every stored version of `(namespace, name)`, sorted ascending.
- `listSchemas` and `deleteSchema` pass through to the storage adapter, with optional namespace + tag filtering.
- Injectable `Clock` for reproducible timestamps in tests.
- `BitpackerError` codes for failure paths, replacing upstream's plain `Error` throws.
## Usage: Basic Example
```ts
import {
SchemaRegistry,
InMemorySchemaRegistryStorage,
} from "@suaveplan/bitpacker-schema";
const registry = new SchemaRegistry(new InMemorySchemaRegistryStorage());
const entry = await registry.registerSchema(
{ id: "uuid", payload: "blob" },
{ namespace: "events", name: "user-created" }
);
console.log(entry.metadata.version); // → 1
const latest = await registry.getSchema("events", "user-created");
```
## Usage: Advanced Example
```ts
import {
SchemaRegistry,
LocalStorageSchemaRegistryStorage,
VersionedSchema,
} from "@suaveplan/bitpacker-schema";
// Browser-side registry persisted to localStorage.
const registry = new SchemaRegistry(
new LocalStorageSchemaRegistryStorage({ storageKey: "app:schemas" })
);
// Bulk-import an existing VersionedSchema in one call.
const versioned = new VersionedSchema(
{ id: "uuid" },
{ name: "user-created", namespace: "events" }
);
versioned.addVersion({ id: "uuid", email: "-256" }, "Add email field");
await registry.registerVersionedSchema(versioned, {
tags: ["v2", "stable"],
owner: "platform-team",
});
// Reconstruct the same VersionedSchema from storage.
const restored = await registry.getVersionedSchema("events", "user-created");
```
## API Reference
### `new SchemaRegistry(storage?, options?)`
`storage?` — any `SchemaRegistryStorage`. Defaults to `new InMemorySchemaRegistryStorage()`.
`options.clock?` — injectable `Clock` for `createdAt`/`updatedAt`. Defaults to `defaultClock`.
### `registerSchema(schema, metadata) → Promise<SchemaRegistryEntry>`
Registers a new schema. Computes `version` as `latest.version + 1` (or `1` if first). Stamps both timestamps with the configured clock. Returns the persisted entry.
### `registerVersionedSchema(versionedSchema, metadata) → Promise<SchemaRegistryEntry[]>`
Imports every version of `versionedSchema` as its own row. The `metadata` argument supplies tags/owner; namespace/name come from the `VersionedSchema` itself. Each row's `createdAt` is preserved from `version.createdAt` when present; `updatedAt` is stamped fresh.
### `getSchema(namespace, name, version?) → Promise<SchemaRegistryEntry | null>`
Exact-version or latest lookup. Returns `null` if no row matches.
### `getVersionedSchema(namespace, name) → Promise<VersionedSchema | null>`
Reconstructs a `VersionedSchema` from every stored version of `(namespace, name)`, sorted by version ascending. Returns `null` if no rows match.
### `listSchemas(namespace?, tag?) → Promise<SchemaRegistryEntry[]>`
Pass-through to `storage.listSchemas`.
### `deleteSchema(namespace, name, version?) → Promise<void>`
Pass-through to `storage.deleteSchema`. Without `version`, deletes every version of `(namespace, name)`.
## Implementation Notes
The constructor accepts `SchemaRegistryStorage` as an interface rather than a concrete class. Defaulting to `new InMemorySchemaRegistryStorage()` matches upstream behaviour but gives every consumer the option of swapping in a different backend without changing the rest of their code.
`registerVersionedSchema` preserves each version's original `createdAt` when present. This matters for migration workflows where the registry is restored from a backup — the original timestamps stay intact, and only `updatedAt` reflects "the moment this row was re-saved".
`getVersionedSchema` does its own version-name filter on top of `storage.listSchemas(namespace)` rather than calling a hypothetical `listSchemas(namespace, name)` because the `SchemaRegistryStorage` contract intentionally limits filtering to namespace + tag. Adding a name filter would require every adapter to gain a third overload, and the in-memory cost of the local filter is negligible at registry sizes.
After the empty-match early return, the index-`0` access is guarded by a type assertion rather than another runtime check — this is the pattern Suaveplan uses for unreachable post-validation accesses, and it keeps branch coverage at 100%.

View File

@@ -0,0 +1,90 @@
# InMemorySchemaRegistryStorage
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`InMemorySchemaRegistryStorage` is the in-process `SchemaRegistryStorage` adapter. It stores schema rows in a private array and is the default backend for `SchemaRegistry` instances created without an explicit storage adapter — useful for tests, ephemeral registries, and any scenario where persistence is not required.
This adapter exists primarily so the `SchemaRegistry` class can have a single storage-shape contract regardless of where the schemas actually live. Tests can build a registry over this adapter without needing browser polyfills or filesystem mocks; production servers that wire to a real persistence layer (Redis, Postgres, S3) implement the same `SchemaRegistryStorage` interface.
## Features
- Implements `SchemaRegistryStorage` exactly: `saveSchema`, `loadSchema`, `listSchemas`, `deleteSchema`.
- Defensive cloning on save/load so callers can't mutate stored entries by holding the returned reference.
- Latest-version resolution when `loadSchema` is called without a version.
- Tag and namespace filtering on `listSchemas`.
- Delete-all-versions semantics when `deleteSchema` is called without a version.
## Usage: Basic Example
```ts
import { InMemorySchemaRegistryStorage } from "@suaveplan/bitpacker-schema";
const storage = new InMemorySchemaRegistryStorage();
await storage.saveSchema({
metadata: {
namespace: "events",
name: "user-created",
version: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
schema: { id: "uuid", name: "-32" },
});
const loaded = await storage.loadSchema("events", "user-created");
```
## Usage: Advanced Example
```ts
import {
InMemorySchemaRegistryStorage,
SchemaRegistry,
} from "@suaveplan/bitpacker-schema";
const storage = new InMemorySchemaRegistryStorage();
const registry = new SchemaRegistry(storage);
await registry.registerSchema(
{ id: "uuid", payload: "blob" },
{
namespace: "events",
name: "user-updated",
tags: ["v2", "stable"],
}
);
const tagged = await registry.listSchemas(undefined, "stable");
```
## API Reference
### `new InMemorySchemaRegistryStorage()`
No arguments. Initialises an empty in-process schema array.
### `saveSchema(entry)`
Inserts `entry` if no `(namespace, name, version)` match exists; otherwise overwrites the existing row. Defensive-clones the entry.
### `loadSchema(namespace, name, version?)`
Returns the entry matching `(namespace, name, version)`. When `version` is omitted, returns the entry with the highest `metadata.version`. Returns `null` if no row matches.
### `listSchemas(namespace?, tag?)`
Returns all entries, optionally filtered by namespace and/or tag. Defensive-clones each entry so the caller can't mutate the store.
### `deleteSchema(namespace, name, version?)`
Removes the row matching `(namespace, name, version)` if `version` is supplied; removes ALL rows matching `(namespace, name)` otherwise.
## Implementation Notes
The store is a plain `SchemaRegistryEntry[]` rather than a nested map. Schemas are typically registered O(1)-O(10) per namespace, lookup is per-call rather than per-tick, and the linear-scan cost is dominated by the JS object dispatch. A nested map would add complexity without a measurable benefit at this size class.
`cloneEntry` is shallow on `metadata` (spreads the object) and identity on `schema` (BitPackerSchema is treated as opaque-by-reference because cloning it would require serialising and re-parsing the entire shape). Callers MUST NOT mutate schema objects after registering them — but if they do, the registry's stored reference points to the same object, by design.
Latest-version resolution uses a single linear pass rather than `Math.max(...versions)` because the array might be very long for high-traffic registries and `Math.max(...arr)` allocates a temporary spread for every call.

View File

@@ -0,0 +1,92 @@
# LocalStorageSchemaRegistryStorage
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`LocalStorageSchemaRegistryStorage` is the browser-`localStorage`-backed `SchemaRegistryStorage` adapter. It persists registry rows under a single key (default `"bitpacker-schema-registry"`) as a JSON array, restoring `Date` instances for `createdAt`/`updatedAt` on read.
The adapter accepts any `KeyValueStringStore`-conformant backend via dependency injection so unit tests don't need a real `localStorage` polyfill — pass an in-memory stub that implements `getItem(key) → string | null` and `setItem(key, value): void`. Production code passes `globalThis.localStorage` (the default).
## Features
- Exact `SchemaRegistryStorage` contract: `saveSchema`, `loadSchema`, `listSchemas`, `deleteSchema`.
- Dependency-injectable store — works with `globalThis.localStorage`, `sessionStorage`, an in-memory stub, or any future Web-storage-shaped backend.
- Date round-tripping: `createdAt`/`updatedAt` are restored from ISO strings to `Date` instances on read.
- Failure paths raise `BitpackerError` with codes (`SCHEMA_STORAGE_PARSE_FAILED`, `SCHEMA_STORAGE_WRITE_FAILED`) — no silent `console.error`'ing as upstream did.
- Missing-store fallback returns an empty list rather than throwing, so the same code can run in environments without any web storage at all.
## Usage: Basic Example
```ts
import { LocalStorageSchemaRegistryStorage } from "@suaveplan/bitpacker-schema";
// In a browser, defaults to globalThis.localStorage.
const storage = new LocalStorageSchemaRegistryStorage();
await storage.saveSchema({
metadata: {
namespace: "events",
name: "user-created",
version: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
schema: { id: "uuid" },
});
```
## Usage: Advanced Example
```ts
import {
LocalStorageSchemaRegistryStorage,
type KeyValueStringStore,
} from "@suaveplan/bitpacker-schema";
// Swap in a stub for tests — no real localStorage needed.
function makeStubStore(): KeyValueStringStore {
const m = new Map<string, string>();
return {
getItem: (k) => m.get(k) ?? null,
setItem: (k, v) => m.set(k, v),
removeItem: (k) => m.delete(k),
};
}
const storage = new LocalStorageSchemaRegistryStorage({
store: makeStubStore(),
storageKey: "test:registry",
});
```
## API Reference
### `new LocalStorageSchemaRegistryStorage(options?)`
`options.store?` — any `KeyValueStringStore`. Defaults to `globalThis.localStorage` (or `undefined` if no localStorage is present).
`options.storageKey?` — the key to read/write. Default `"bitpacker-schema-registry"`.
### `saveSchema(entry)`
Reads the current array, inserts or replaces the row matching `(namespace, name, version)`, writes the array back. Throws `BitpackerError(SCHEMA_STORAGE_WRITE_FAILED)` if the underlying store rejects the write (typical cause: quota exceeded).
### `loadSchema(namespace, name, version?)`
Reads the current array and returns the matching row. When `version` is omitted, returns the row with the highest `metadata.version`. Returns `null` if no row matches.
### `listSchemas(namespace?, tag?)`
Reads the current array and applies optional namespace/tag filters.
### `deleteSchema(namespace, name, version?)`
Filters out the matching row(s) and writes the array back. Removes ALL versions of `(namespace, name)` if `version` is omitted.
## Implementation Notes
The adapter writes the full array on every save. For registries with hundreds of schemas this becomes O(n) per write, but at typical sizes (10-50 schemas per app) the cost is dominated by the JSON encode/decode rather than the array copy. If a deployment grows past that, switch to a chunk-keyed adapter (one localStorage key per `namespace + name`) — the `SchemaRegistryStorage` contract is the only thing that needs to stay the same.
`reviveEntry` casts the parsed metadata through an intermediate shape and rebuilds `Date` instances from the ISO strings. We do not run a Zod validator on the parsed rows — the adapter trusts its own write format (the only writer in production is `writeAll` from this same class). If a deployment needs hardened deserialisation against tampering, wrap the adapter with a validating decorator.
The default-store resolution looks at `globalThis.localStorage` rather than `window.localStorage` so the adapter compiles unchanged in Node.js, Bun, Web Workers, and Service Workers — anywhere with a global `localStorage` shape.

View File

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

View File

@@ -0,0 +1,69 @@
# VersionedSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`VersionedSchema` is the working unit for evolving a `BitPackerSchema` over time without breaking previously-packed data. A single `VersionedSchema` instance keeps an ordered list of `SchemaVersion` rows (each pairing a version number with a `BitPackerSchema` and an optional description), plus a list of `SchemaMigration` rows that describe how to transform data between adjacent versions. Production code constructs versioned schemas in two ways: directly via the constructor when authoring a new schema lineage, or via `SchemaRegistry.getVersionedSchema(namespace, name)` when reconstructing one from storage. It is the contract the migration engine, the registry, and the runtime pack/unpack path all consult.
## Features
- Ordered version list with `addVersion` for new entries and `latestSchema` / `latestVersion` accessors.
- Per-version migration registration via `addMigration<U extends BitPackerSchema>` with a `MigrationFunction<T, U>`.
- Bit-packer-aware: pack/unpack methods that select the schema by version number and route through an injectable `BitPacker`.
- Bulk reconstruction from `SchemaRegistry` storage rows.
## Usage: Basic Example
```ts
import { VersionedSchema } from "@suaveplan/bitpacker-schema";
const player = new VersionedSchema({ name: "-16", health: 255 }, { name: "player" });
player.addVersion({ name: "-16", health: 255, mana: 255 }, "added mana");
const latest = player.latestSchema;
```
## Usage: Advanced Example
```ts
import { VersionedSchema } from "@suaveplan/bitpacker-schema";
const player = new VersionedSchema(
{ name: "-16", health: 255 },
{ name: "player", namespace: "game" }
);
player.addVersion({ name: "-16", health: 255, mana: 255 });
player.addMigration(1, 2, (data) => ({ ...data, mana: 100 }));
// Pack v1 data, then transparently migrate up.
const packed = player.pack({ name: "Hero", health: 200 }, 1);
const v2Data = player.unpackAndMigrate(packed, 1, 2);
```
## API Reference
### `class VersionedSchema<T extends BitPackerSchema = BitPackerSchema>`
- `constructor(schema: T, options: VersionedSchemaOptions)`
- `addVersion(schema: BitPackerSchema, description?: string): number` — appends; returns new version number.
- `addMigration<U extends BitPackerSchema>(from: number, to: number, fn: MigrationFunction): void`
- `getSchema(version: number): BitPackerSchema` — throws if version not found.
- `pack(data, version): Uint8Array` / `unpack(data, version): unknown` — pack/unpack at a specific version.
- `get versions: readonly SchemaVersion[]`
- `get latestSchema: BitPackerSchema`
- `get latestVersion: number`
### `interface SchemaVersion`
`{ version: number; schema: BitPackerSchema; description?: string; createdAt?: Date }`
### `interface VersionedSchemaOptions`
`{ name: string; namespace?: string; description?: string; autoMigrate?: boolean; includeVersion?: boolean; bitPacker?: BitPacker }`
## Implementation Notes
- The internal `_bitPacker` defaults to a fresh `BitPacker()` instance; tests inject a stub to verify pack/unpack are routed through the injectable, not module-scope state.
- `MigrationFunction` signatures are intentionally `any → any` because migration functions transform between heterogeneous data shapes; the `T → U` generic threading is encouraged but not enforced at runtime.
- `addVersion` appends in monotonic order. There is no insertion or rewriting — historical versions are immutable so packed data on disk stays decodable.

View File

@@ -0,0 +1,67 @@
# Schema visualization
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The visualization module renders a `BitPackerSchema` into one of five inspection-friendly formats — JSON, HTML, Markdown, plain text, or Graphviz DOT — for tooling, documentation, and debugging. Schemas are JSON-serialisable but their nested structure (objects of `_optional`, `_oneOf`, `_conditional`, primitive tokens, arrays) is fiddly to read at scale; this module produces a renderable tree view that surfaces the structure at a glance. It is the layer that downstream developer tools (schema explorers, doc generators, registry UIs) consume to display the contents of the registry without parsing the schema themselves.
## Features
- Five output formats (`SchemaVisualizationFormat`): `JSON`, `HTML`, `MARKDOWN`, `TEXT`, `DOT`.
- Configurable via `SchemaVisualizationOptions` (format, depth limit, include/exclude metadata).
- Recursive descent through nested objects, arrays, and the `_optional` / `_oneOf` / `_conditional` operators.
- Recognises the canonical primitive tokens (UUID, float, integer, string).
## Usage: Basic Example
```ts
import {
visualizeSchema,
SchemaVisualizationFormat,
} from "@suaveplan/bitpacker-schema";
const html = visualizeSchema(
{ name: "-16", age: 255, tags: [{ _optional: "-32" }] },
{ format: SchemaVisualizationFormat.HTML }
);
```
## Usage: Advanced Example
```ts
import {
visualizeSchema,
SchemaVisualizationFormat,
} from "@suaveplan/bitpacker-schema";
// Render a Graphviz DOT graph for a docs site.
const dot = visualizeSchema(playerSchema, {
format: SchemaVisualizationFormat.DOT,
maxDepth: 5,
});
// Pipe through `dot -Tsvg` to produce an SVG of the schema tree.
```
## API Reference
### `enum SchemaVisualizationFormat`
`JSON | HTML | MARKDOWN | TEXT | DOT` — pick the renderer to apply.
### `interface SchemaVisualizationOptions`
- `format?: SchemaVisualizationFormat` — default `JSON`.
- `maxDepth?: number` — recursion depth limit; deeper nodes render as `…`.
- `includeMetadata?: boolean` — include `description`, `tags`, etc. when present.
### `visualizeSchema(schema, options?): string`
Renders the schema as a string in the requested format. Returns valid syntax for the chosen format (parseable JSON, render-ready HTML, etc.).
## Implementation Notes
- HTML output is intentionally semantic (`<details>` / `<summary>` for nesting) so the result is keyboard-navigable and accessible without extra CSS.
- DOT output names every node deterministically (path-based IDs) so two renders of the same schema produce identical graphs — useful for regression-diffing schema docs.
- The renderer handles cycles via a max-depth limit, not via cycle detection. `BitPackerSchema` is a tree by construction, but `_ref` lookups can produce structural cycles; in practice the `maxDepth` ceiling keeps the renderer linear.

View File

@@ -0,0 +1,202 @@
# @suaveplan/bitpacker-strategies - Built-in packing strategies for @suaveplan/bitpacker
## Summary
Built-in packing strategies for `@suaveplan/bitpacker`. Provides strategies for numbers, strings, booleans, objects, arrays, enums, optional fields, oneOf unions, varints, dates, maps, sets, bit arrays, blobs, bitsets, conditionals, versions, definitions, and custom types.
<!-- begin:auto-docs -->
## Documentation tree
- [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-default-strategies](./src/register-default-strategies/register-default-strategies.md) — **Path:** `bitpacker-strategies/src/register-default-strategies.ts` **Export:** See source for details.
- [register-default-strategies](./src/register-default-strategies/register-default-strategies.md) — **Path:** `bitpacker-strategies/src/register-default-strategies.ts` **Export:** See source for details.
- [register-default-strategies.schema](./src/register-default-strategies.schema/register-default-strategies.schema.md) — Zod schema for the `manager` argument of `registerDefaultStrategies`. Validates that an external caller hands the funct…
- [register-default-strategies.schema](./src/register-default-strategies.schema/register-default-strategies.schema.md) — Zod schema for the `manager` argument of `registerDefaultStrategies`. Validates that an external caller hands the funct…
- [register-schemas](./src/register-schemas/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-strategies` package. Exposes a s…
- [register-schemas](./src/register-schemas/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-strategies` package. Exposes a s…
- [array](./src/strategies/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [array](./src/strategies/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [array-config](./src/strategies/array-config/array-config.md) — Packs/unpacks arrays described by an explicit configuration object.
- [array-config](./src/strategies/array-config/array-config.md) — Packs/unpacks arrays described by an explicit configuration object.
- [array-notation](./src/strategies/array-notation/array-notation.md) — Packs/unpacks arrays described by a string notation.
- [array-notation](./src/strategies/array-notation/array-notation.md) — Packs/unpacks arrays described by a string notation.
- [array-schema](./src/strategies/array-schema/array-schema.md) — Packs/unpacks fixed-length tuple schemas.
- [array-schema](./src/strategies/array-schema/array-schema.md) — Packs/unpacks fixed-length tuple schemas.
- [bigint](./src/strategies/bigint/bigint.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bigint](./src/strategies/bigint/bigint.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bigint-array](./src/strategies/bigint-array/bigint-array.md) — Packs/unpacks arrays of 64-bit BigInt values.
- [bigint-array](./src/strategies/bigint-array/bigint-array.md) — Packs/unpacks arrays of 64-bit BigInt values.
- [bitarray](./src/strategies/bitarray/bitarray.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bitarray](./src/strategies/bitarray/bitarray.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bitset](./src/strategies/bitset/bitset.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bitset](./src/strategies/bitset/bitset.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [blob](./src/strategies/blob/blob.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [blob](./src/strategies/blob/blob.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [boolean](./src/strategies/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [boolean](./src/strategies/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [conditional](./src/strategies/conditional/conditional.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [conditional](./src/strategies/conditional/conditional.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [customType](./src/strategies/customType/customType.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [customType](./src/strategies/customType/customType.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [datetime](./src/strategies/datetime/datetime.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [datetime](./src/strategies/datetime/datetime.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [defs](./src/strategies/defs/defs.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [defs](./src/strategies/defs/defs.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [enum](./src/strategies/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [enum](./src/strategies/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [map](./src/strategies/map/map.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [map](./src/strategies/map/map.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [null](./src/strategies/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [null](./src/strategies/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [number](./src/strategies/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [number](./src/strategies/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [object](./src/strategies/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [object](./src/strategies/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [oneOf](./src/strategies/oneOf/oneOf.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [oneOf](./src/strategies/oneOf/oneOf.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [optional](./src/strategies/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [optional](./src/strategies/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [optional-marker](./src/strategies/optional-marker/optional-marker.md) — Packs/unpacks optional values using an explicit marker object.
- [optional-marker](./src/strategies/optional-marker/optional-marker.md) — Packs/unpacks optional values using an explicit marker object.
- [optional-suffix](./src/strategies/optional-suffix/optional-suffix.md) — Packs/unpacks optional primitive schemas using a `?` suffix.
- [optional-suffix](./src/strategies/optional-suffix/optional-suffix.md) — Packs/unpacks optional primitive schemas using a `?` suffix.
- [set](./src/strategies/set/set.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [set](./src/strategies/set/set.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [string](./src/strategies/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [string](./src/strategies/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [union](./src/strategies/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [union](./src/strategies/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [varint](./src/strategies/varint/varint.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [varint](./src/strategies/varint/varint.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [version](./src/strategies/version/version.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [version](./src/strategies/version/version.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [index](./src/index.md) — **Path:** `bitpacker-strategies/src/index.ts` **Export:** See source for details.
<!-- end:auto-docs --> - [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/strategies/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/strategies/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [array-config](./src/strategies/array-config/array-config.md) — Packs/unpacks arrays described by an explicit configuration object.
- [array-config](./src/strategies/array-config/array-config.md) — Packs/unpacks arrays described by an explicit configuration object.
- [array-notation](./src/strategies/array-notation/array-notation.md) — Packs/unpacks arrays described by a string notation.
- [array-notation](./src/strategies/array-notation/array-notation.md) — Packs/unpacks arrays described by a string notation.
- [array-schema](./src/strategies/array-schema/array-schema.md) — Packs/unpacks fixed-length tuple schemas.
- [array-schema](./src/strategies/array-schema/array-schema.md) — Packs/unpacks fixed-length tuple schemas.
- [bigint](./src/strategies/bigint/bigint.md) — <!-- begin:auto-docs --> _(no submodules)_- [bigint](./src/strategies/bigint/bigint.md) — <!-- begin:auto-docs --> _(no submodules)_- [bigint-array](./src/strategies/bigint-array/bigint-array.md) — Packs/unpacks arrays of 64-bit BigInt values.
- [bigint-array](./src/strategies/bigint-array/bigint-array.md) — Packs/unpacks arrays of 64-bit BigInt values.
- [bitarray](./src/strategies/bitarray/bitarray.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitarray](./src/strategies/bitarray/bitarray.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitset](./src/strategies/bitset/bitset.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitset](./src/strategies/bitset/bitset.md) — <!-- begin:auto-docs --> _(no submodules)_- [blob](./src/strategies/blob/blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [blob](./src/strategies/blob/blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/strategies/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/strategies/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditional](./src/strategies/conditional/conditional.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditional](./src/strategies/conditional/conditional.md) — <!-- begin:auto-docs --> _(no submodules)_- [customType](./src/strategies/customType/customType.md) — <!-- begin:auto-docs --> _(no submodules)_- [customType](./src/strategies/customType/customType.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime](./src/strategies/datetime/datetime.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime](./src/strategies/datetime/datetime.md) — <!-- begin:auto-docs --> _(no submodules)_- [defs](./src/strategies/defs/defs.md) — <!-- begin:auto-docs --> _(no submodules)_- [defs](./src/strategies/defs/defs.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/strategies/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/strategies/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [map](./src/strategies/map/map.md) — <!-- begin:auto-docs --> _(no submodules)_- [map](./src/strategies/map/map.md) — <!-- begin:auto-docs --> _(no submodules)_- [null](./src/strategies/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_- [null](./src/strategies/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/strategies/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/strategies/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/strategies/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/strategies/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [oneOf](./src/strategies/oneOf/oneOf.md) — <!-- begin:auto-docs --> _(no submodules)_- [oneOf](./src/strategies/oneOf/oneOf.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/strategies/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/strategies/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional-marker](./src/strategies/optional-marker/optional-marker.md) — Packs/unpacks optional values using an explicit marker object.
- [optional-marker](./src/strategies/optional-marker/optional-marker.md) — Packs/unpacks optional values using an explicit marker object.
- [optional-suffix](./src/strategies/optional-suffix/optional-suffix.md) — Packs/unpacks optional primitive schemas using a `?` suffix.
- [optional-suffix](./src/strategies/optional-suffix/optional-suffix.md) — Packs/unpacks optional primitive schemas using a `?` suffix.
- [set](./src/strategies/set/set.md) — <!-- begin:auto-docs --> _(no submodules)_- [set](./src/strategies/set/set.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/strategies/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/strategies/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/strategies/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/strategies/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [varint](./src/strategies/varint/varint.md) — <!-- begin:auto-docs --> _(no submodules)_- [varint](./src/strategies/varint/varint.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/strategies/version/version.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/strategies/version/version.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-strategies/src/index.ts` **Export:** See source for details.
- [register-default-strategies](./src/register-default-strategies.md) — **Path:** `bitpacker-strategies/src/register-default-strategies.ts` **Export:** See source for details.
- [register-default-strategies.schema](./src/register-default-strategies.schema.md) — Zod schema for the `manager` argument of `registerDefaultStrategies`. Validates that an external caller hands the funct…
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-strategies` package. Exposes a s…
- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/strategies/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/strategies/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [array-config](./src/strategies/array-config/array-config.md) — Packs/unpacks arrays described by an explicit configuration object.
- [array-config](./src/strategies/array-config/array-config.md) — Packs/unpacks arrays described by an explicit configuration object.
- [array-notation](./src/strategies/array-notation/array-notation.md) — Packs/unpacks arrays described by a string notation.
- [array-notation](./src/strategies/array-notation/array-notation.md) — Packs/unpacks arrays described by a string notation.
- [array-schema](./src/strategies/array-schema/array-schema.md) — Packs/unpacks fixed-length tuple schemas.
- [array-schema](./src/strategies/array-schema/array-schema.md) — Packs/unpacks fixed-length tuple schemas.
- [bigint](./src/strategies/bigint/bigint.md) — <!-- begin:auto-docs --> _(no submodules)_- [bigint](./src/strategies/bigint/bigint.md) — <!-- begin:auto-docs --> _(no submodules)_- [bigint-array](./src/strategies/bigint-array/bigint-array.md) — Packs/unpacks arrays of 64-bit BigInt values.
- [bigint-array](./src/strategies/bigint-array/bigint-array.md) — Packs/unpacks arrays of 64-bit BigInt values.
- [bitarray](./src/strategies/bitarray/bitarray.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitarray](./src/strategies/bitarray/bitarray.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitset](./src/strategies/bitset/bitset.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitset](./src/strategies/bitset/bitset.md) — <!-- begin:auto-docs --> _(no submodules)_- [blob](./src/strategies/blob/blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [blob](./src/strategies/blob/blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/strategies/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/strategies/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditional](./src/strategies/conditional/conditional.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditional](./src/strategies/conditional/conditional.md) — <!-- begin:auto-docs --> _(no submodules)_- [customType](./src/strategies/customType/customType.md) — <!-- begin:auto-docs --> _(no submodules)_- [customType](./src/strategies/customType/customType.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime](./src/strategies/datetime/datetime.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime](./src/strategies/datetime/datetime.md) — <!-- begin:auto-docs --> _(no submodules)_- [defs](./src/strategies/defs/defs.md) — <!-- begin:auto-docs --> _(no submodules)_- [defs](./src/strategies/defs/defs.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/strategies/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/strategies/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [map](./src/strategies/map/map.md) — <!-- begin:auto-docs --> _(no submodules)_- [map](./src/strategies/map/map.md) — <!-- begin:auto-docs --> _(no submodules)_- [null](./src/strategies/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_- [null](./src/strategies/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/strategies/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/strategies/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/strategies/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/strategies/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [oneOf](./src/strategies/oneOf/oneOf.md) — <!-- begin:auto-docs --> _(no submodules)_- [oneOf](./src/strategies/oneOf/oneOf.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/strategies/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/strategies/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional-marker](./src/strategies/optional-marker/optional-marker.md) — Packs/unpacks optional values using an explicit marker object.
- [optional-marker](./src/strategies/optional-marker/optional-marker.md) — Packs/unpacks optional values using an explicit marker object.
- [optional-suffix](./src/strategies/optional-suffix/optional-suffix.md) — Packs/unpacks optional primitive schemas using a `?` suffix.
- [optional-suffix](./src/strategies/optional-suffix/optional-suffix.md) — Packs/unpacks optional primitive schemas using a `?` suffix.
- [set](./src/strategies/set/set.md) — <!-- begin:auto-docs --> _(no submodules)_- [set](./src/strategies/set/set.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/strategies/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/strategies/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/strategies/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/strategies/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [varint](./src/strategies/varint/varint.md) — <!-- begin:auto-docs --> _(no submodules)_- [varint](./src/strategies/varint/varint.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/strategies/version/version.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/strategies/version/version.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-strategies/src/index.ts` **Export:** See source for details.
- [register-default-strategies](./src/register-default-strategies.md) — **Path:** `bitpacker-strategies/src/register-default-strategies.ts` **Export:** See source for details.
- [register-default-strategies.schema](./src/register-default-strategies.schema.md) — Zod schema for the `manager` argument of `registerDefaultStrategies`. Validates that an external caller hands the funct…
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-strategies` package. Exposes a s…
- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/strategies/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/strategies/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [array-config](./src/strategies/array-config/array-config.md) — Packs/unpacks arrays described by an explicit configuration object.
- [array-config](./src/strategies/array-config/array-config.md) — Packs/unpacks arrays described by an explicit configuration object.
- [array-notation](./src/strategies/array-notation/array-notation.md) — Packs/unpacks arrays described by a string notation.
- [array-notation](./src/strategies/array-notation/array-notation.md) — Packs/unpacks arrays described by a string notation.
- [array-schema](./src/strategies/array-schema/array-schema.md) — Packs/unpacks fixed-length tuple schemas.
- [array-schema](./src/strategies/array-schema/array-schema.md) — Packs/unpacks fixed-length tuple schemas.
- [bigint](./src/strategies/bigint/bigint.md) — <!-- begin:auto-docs --> _(no submodules)_- [bigint](./src/strategies/bigint/bigint.md) — <!-- begin:auto-docs --> _(no submodules)_- [bigint-array](./src/strategies/bigint-array/bigint-array.md) — Packs/unpacks arrays of 64-bit BigInt values.
- [bigint-array](./src/strategies/bigint-array/bigint-array.md) — Packs/unpacks arrays of 64-bit BigInt values.
- [bitarray](./src/strategies/bitarray/bitarray.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitarray](./src/strategies/bitarray/bitarray.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitset](./src/strategies/bitset/bitset.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitset](./src/strategies/bitset/bitset.md) — <!-- begin:auto-docs --> _(no submodules)_- [blob](./src/strategies/blob/blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [blob](./src/strategies/blob/blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/strategies/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/strategies/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditional](./src/strategies/conditional/conditional.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditional](./src/strategies/conditional/conditional.md) — <!-- begin:auto-docs --> _(no submodules)_- [customType](./src/strategies/customType/customType.md) — <!-- begin:auto-docs --> _(no submodules)_- [customType](./src/strategies/customType/customType.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime](./src/strategies/datetime/datetime.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime](./src/strategies/datetime/datetime.md) — <!-- begin:auto-docs --> _(no submodules)_- [defs](./src/strategies/defs/defs.md) — <!-- begin:auto-docs --> _(no submodules)_- [defs](./src/strategies/defs/defs.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/strategies/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/strategies/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [map](./src/strategies/map/map.md) — <!-- begin:auto-docs --> _(no submodules)_- [map](./src/strategies/map/map.md) — <!-- begin:auto-docs --> _(no submodules)_- [null](./src/strategies/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_- [null](./src/strategies/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/strategies/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/strategies/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/strategies/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/strategies/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [oneOf](./src/strategies/oneOf/oneOf.md) — <!-- begin:auto-docs --> _(no submodules)_- [oneOf](./src/strategies/oneOf/oneOf.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/strategies/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/strategies/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional-marker](./src/strategies/optional-marker/optional-marker.md) — Packs/unpacks optional values using an explicit marker object.
- [optional-marker](./src/strategies/optional-marker/optional-marker.md) — Packs/unpacks optional values using an explicit marker object.
- [optional-suffix](./src/strategies/optional-suffix/optional-suffix.md) — Packs/unpacks optional primitive schemas using a `?` suffix.
- [optional-suffix](./src/strategies/optional-suffix/optional-suffix.md) — Packs/unpacks optional primitive schemas using a `?` suffix.
- [set](./src/strategies/set/set.md) — <!-- begin:auto-docs --> _(no submodules)_- [set](./src/strategies/set/set.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/strategies/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/strategies/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/strategies/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/strategies/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [varint](./src/strategies/varint/varint.md) — <!-- begin:auto-docs --> _(no submodules)_- [varint](./src/strategies/varint/varint.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/strategies/version/version.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/strategies/version/version.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-strategies/src/index.ts` **Export:** See source for details.
- [register-default-strategies](./src/register-default-strategies.md) — **Path:** `bitpacker-strategies/src/register-default-strategies.ts` **Export:** See source for details.
- [register-default-strategies.schema](./src/register-default-strategies.schema.md) — Zod schema for the `manager` argument of `registerDefaultStrategies`. Validates that an external caller hands the funct…
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-strategies` package. Exposes a s…
- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/strategies/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_ - [array](./src/strategies/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [array-config](./src/strategies/array-config/array-config.md) — Packs/unpacks arrays described by an explicit configuration object.
- [array-config](./src/strategies/array-config/array-config.md) — Packs/unpacks arrays described by an explicit configuration object.
- [array-notation](./src/strategies/array-notation/array-notation.md) — Packs/unpacks arrays described by a string notation.
- [array-notation](./src/strategies/array-notation/array-notation.md) — Packs/unpacks arrays described by a string notation.
- [array-schema](./src/strategies/array-schema/array-schema.md) — Packs/unpacks fixed-length tuple schemas.
- [array-schema](./src/strategies/array-schema/array-schema.md) — Packs/unpacks fixed-length tuple schemas.
- [bigint](./src/strategies/bigint/bigint.md) — <!-- begin:auto-docs --> _(no submodules)_ - [bigint](./src/strategies/bigint/bigint.md) — <!-- begin:auto-docs --> _(no submodules)_- [bigint-array](./src/strategies/bigint-array/bigint-array.md) — Packs/unpacks arrays of 64-bit BigInt values.
- [bigint-array](./src/strategies/bigint-array/bigint-array.md) — Packs/unpacks arrays of 64-bit BigInt values.
- [bitarray](./src/strategies/bitarray/bitarray.md) — <!-- begin:auto-docs --> _(no submodules)_ - [bitarray](./src/strategies/bitarray/bitarray.md) — <!-- begin:auto-docs --> _(no submodules)_- [bitset](./src/strategies/bitset/bitset.md) — <!-- begin:auto-docs --> _(no submodules)_ - [bitset](./src/strategies/bitset/bitset.md) — <!-- begin:auto-docs --> _(no submodules)_- [blob](./src/strategies/blob/blob.md) — <!-- begin:auto-docs --> _(no submodules)_ - [blob](./src/strategies/blob/blob.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/strategies/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_ - [boolean](./src/strategies/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [conditional](./src/strategies/conditional/conditional.md) — <!-- begin:auto-docs --> _(no submodules)_ - [conditional](./src/strategies/conditional/conditional.md) — <!-- begin:auto-docs --> _(no submodules)_- [customType](./src/strategies/customType/customType.md) — <!-- begin:auto-docs --> _(no submodules)_ - [customType](./src/strategies/customType/customType.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime](./src/strategies/datetime/datetime.md) — <!-- begin:auto-docs --> _(no submodules)_ - [datetime](./src/strategies/datetime/datetime.md) — <!-- begin:auto-docs --> _(no submodules)_- [defs](./src/strategies/defs/defs.md) — <!-- begin:auto-docs --> _(no submodules)_ - [defs](./src/strategies/defs/defs.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/strategies/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_ - [enum](./src/strategies/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [map](./src/strategies/map/map.md) — <!-- begin:auto-docs --> _(no submodules)_ - [map](./src/strategies/map/map.md) — <!-- begin:auto-docs --> _(no submodules)_- [null](./src/strategies/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_ - [null](./src/strategies/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/strategies/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_ - [number](./src/strategies/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/strategies/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_ - [object](./src/strategies/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [oneOf](./src/strategies/oneOf/oneOf.md) — <!-- begin:auto-docs --> _(no submodules)_ - [oneOf](./src/strategies/oneOf/oneOf.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/strategies/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_ - [optional](./src/strategies/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional-marker](./src/strategies/optional-marker/optional-marker.md) — Packs/unpacks optional values using an explicit marker object.
- [optional-marker](./src/strategies/optional-marker/optional-marker.md) — Packs/unpacks optional values using an explicit marker object.
- [optional-suffix](./src/strategies/optional-suffix/optional-suffix.md) — Packs/unpacks optional primitive schemas using a `?` suffix.
- [optional-suffix](./src/strategies/optional-suffix/optional-suffix.md) — Packs/unpacks optional primitive schemas using a `?` suffix.
- [set](./src/strategies/set/set.md) — <!-- begin:auto-docs --> _(no submodules)_ - [set](./src/strategies/set/set.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/strategies/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_ - [string](./src/strategies/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/strategies/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_ - [union](./src/strategies/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [varint](./src/strategies/varint/varint.md) — <!-- begin:auto-docs --> _(no submodules)_ - [varint](./src/strategies/varint/varint.md) — <!-- begin:auto-docs --> _(no submodules)_- [version](./src/strategies/version/version.md) — <!-- begin:auto-docs --> _(no submodules)_ - [version](./src/strategies/version/version.md) — <!-- begin:auto-docs --> _(no submodules)_- [index](./src/index.md) — **Path:** `bitpacker-strategies/src/index.ts` **Export:** See source for details.
- [register-default-strategies](./src/register-default-strategies.md) — **Path:** `bitpacker-strategies/src/register-default-strategies.ts` **Export:** See source for details.
- [register-default-strategies.schema](./src/register-default-strategies.schema.md) — Zod schema for the `manager` argument of `registerDefaultStrategies`. Validates that an external caller hands the funct…
- [register-schemas](./src/register-schemas.md) — Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-strategies` package. Exposes a s…
## Install
```bash
bun add @suaveplan/bitpacker-strategies
```
## Usage
```typescript
import { BitPacker } from "@suaveplan/bitpacker";
import { registerDefaultStrategies } from "@suaveplan/bitpacker-strategies";
const bp = new BitPacker();
registerDefaultStrategies(bp.strategyManager);
// Pack a number (unsigned 8-bit)
const packed = bp.pack(42, 255);
const unpacked = bp.unpack(packed, 255);
```
## Strategies
| Strategy | Schema | Example |
|----------|--------|---------|
| `NumberPackingStrategy` | `number` | `255` (uint8), `-127` (signed), `4.2` (fixed-point) |
| `StringPackingStrategy` | `string` | `"8"` (fixed 8 chars), `"-16"` (variable length) |
| `BooleanPackingStrategy` | `boolean` | `true` / `false` |
| `NullPackingStrategy` | `null` | `null` |
| `BigIntPackingStrategy` | `bigint` | `"bigint-i64"`, `"bigint-u64"` |
| `ObjectPackingStrategy` | `object` | `{ x: 255, y: "-16" }` |
| `ArrayPackingStrategy` | `array` | `[255]` |
| `EnumPackingStrategy` | `enum` | `"enum:a\|b\|c"` |
| `OptionalPackingStrategy` | `optional` | `"8?"` |
| `OneOfPackingStrategy` | `oneOf` | `{ _bp: "oneOf", schemas: [...] }` |
| `VarIntPackingStrategy` | `varint` | `"varint"`, `"varint-signed"` |
| `DateTimePackingStrategy` | `datetime` | `"timestamp"`, `"timespan-ms"` |
| `MapPackingStrategy` | `map` | `"map<string>"` |
| `SetPackingStrategy` | `set` | `"set<number>"` |
| `BitArrayPackingStrategy` | `bitarray` | `{ _bp: "bitarray", length: 8 }` |
| `BlobPackingStrategy` | `blob` | `{ _bp: "blob", maxLength: 1024 }` |
| `BitsetPackingStrategy` | `bitset` | `"bitset-8"` |
| `ConditionalPackingStrategy` | `conditional` | `{ _bp: "conditional", condition, then, else }` |
| `VersionPackingStrategy` | `version` | `{ _bp: "version", versions: [...] }` |
| `DefsPackingStrategy` | `defs` | `{ _bp: "defs", definitions: {...} }` |
| `CustomTypePackingStrategy` | `custom` | `{ _bp: { schema: ... } }` |
## Custom Strategy
```typescript
class MyStrategy implements PackingStrategy {
canHandle(schema: BitPackerSchema): boolean {
return schema === "my-type";
}
pack(data: unknown, schema: BitPackerSchema, context: PackingContext): void {
// write to context.writer
}
unpack(schema: BitPackerSchema, context: PackingContext): unknown {
// read from context.reader
}
}
bp.strategyManager.register(new MyStrategy());
```

View File

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

View File

@@ -0,0 +1,76 @@
# Bitpacker Strategies OTel Instruments
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
OTel meter, tracer, counter, and histogram declarations for `@suaveplan/bitpacker-strategies`. The package's only public boundary is `registerDefaultStrategies(manager)` — strategies are stateless chain handlers invoked from inside the parent `bitpacker.pack` / `bitpacker.unpack` span owned by `@suaveplan/bitpacker`. Per the proposal "one span per top-level call; per-chunk events nested", strategy invocations do NOT get their own spans; only the bootstrap call `registerDefaultStrategies` is instrumented.
The metric and span names follow the suaveplan convention with snake-case for the multi-word package: tracer/meter named `@suaveplan/bitpacker-strategies`; span named `bitpacker_strategies.register`; counter/histogram named `bitpacker_strategies.register.<metric>`.
## Features
- `getBitPackerStrategiesTracer()` — returns the package's tracer
- `registerStrategiesTotal` — counter labelled by `success`
- `registerStrategiesDurationMs` — histogram of bootstrap duration
- `BITPACKER_STRATEGIES_SPAN_NAMES` — `{ register: "bitpacker_strategies.register" }`
## Usage: Basic Example
```ts
import { PackingStrategyManager } from "@suaveplan/bitpacker";
import { registerDefaultStrategies } from "@suaveplan/bitpacker-strategies";
const manager = new PackingStrategyManager();
registerDefaultStrategies(manager);
// One span emitted: "bitpacker_strategies.register"
// Counter incremented: bitpacker_strategies.register.total{success="true"}
// Histogram recorded: bitpacker_strategies.register.duration_ms
```
## Usage: Advanced Example
```ts
import { trace } from "@opentelemetry/api";
import { PackingStrategyManager } from "@suaveplan/bitpacker";
import {
BITPACKER_STRATEGIES_SPAN_NAMES,
registerDefaultStrategies,
} from "@suaveplan/bitpacker-strategies";
trace
.getTracer("my-app")
.startActiveSpan(
BITPACKER_STRATEGIES_SPAN_NAMES.register + ".wrapper",
(span) => {
const manager = new PackingStrategyManager();
registerDefaultStrategies(manager);
// bitpacker_strategies.register is a child span here.
span.end();
}
);
```
## API Reference
### `getBitPackerStrategiesTracer(): Tracer`
Returns the OTel `Tracer` instance for the package.
### `registerStrategiesTotal: Counter`
Counter incremented once per `registerDefaultStrategies` call. Labels: `success: "true" | "false"`.
### `registerStrategiesDurationMs: Histogram`
Histogram of elapsed wall-clock milliseconds per bootstrap call. No labels.
### `BITPACKER_STRATEGIES_SPAN_NAMES`
Const map: `{ register: "bitpacker_strategies.register" }`.
## Implementation Notes
- **No per-strategy spans.** The 30 shipped strategies are invoked thousands of times per pack/unpack on large schemas; one span each would explode trace volume. They run inside the parent `bitpacker.pack` / `bitpacker.unpack` span without instrumentation.
- **Bootstrap-only counter.** `registerStrategiesTotal` is incremented once per `registerDefaultStrategies` call. A consumer can reason about chain-bootstrap frequency from this counter without correlating across packages.

View File

@@ -0,0 +1,72 @@
# Register Default Strategies — Schema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schema for the `manager` argument of `registerDefaultStrategies`. Validates that an external caller hands the function a real `PackingStrategyManager` instance (not a plain object that "quacks like" one) so the chain ordering invariants the function depends on are guaranteed at the boundary. Identity is preserved — the parsed value is the same instance the caller passed in, so `registerDefaultStrategies(manager)` mutates the caller's manager rather than a clone.
This package's strategy classes are themselves parameterless and therefore expose no per-strategy options to validate. The single boundary that needs a schema is `registerDefaultStrategies(manager)`. Strategy *shape* (`canHandle`/`pack`/`unpack` are functions) is already validated upstream by `packingStrategySchema` in `@suaveplan/bitpacker`.
## Features
- `packingStrategyManagerSchema` — `z.custom<PackingStrategyManager>` `instanceof` check
- Identity preservation (no wrapping)
- Rejects null, undefined, and plain objects that share the manager API surface
## Usage: Basic Example
```ts
import {
PackingStrategyManager,
BitPackerError,
BITPACKER_CODES,
} from "@suaveplan/bitpacker";
import {
packingStrategyManagerSchema,
registerDefaultStrategies,
} from "@suaveplan/bitpacker-strategies";
const manager = packingStrategyManagerSchema.parse(new PackingStrategyManager());
registerDefaultStrategies(manager);
```
## Usage: Advanced Example
```ts
import {
PackingStrategyManager,
BitPackerError,
BITPACKER_CODES,
} from "@suaveplan/bitpacker";
import {
packingStrategyManagerSchema,
registerDefaultStrategies,
} from "@suaveplan/bitpacker-strategies";
function bootstrap(rawManager: unknown): PackingStrategyManager {
const result = packingStrategyManagerSchema.safeParse(rawManager);
if (!result.success) {
throw new BitPackerError(
BITPACKER_CODES.INVALID_SCHEMA,
"registerDefaultStrategies received a non-manager value",
{ cause: result.error }
);
}
registerDefaultStrategies(result.data);
return result.data;
}
```
## API Reference
### `packingStrategyManagerSchema: z.ZodType<PackingStrategyManager>`
Strict `instanceof` validator. Returns the input value unchanged on success; throws `ZodError` with the message `"manager must be a PackingStrategyManager instance"` on failure.
## Implementation Notes
- **No per-strategy schemas.** Every shipped strategy in this package is parameterless — `new ArrayPackingStrategy()`, `new ObjectPackingStrategy()`, etc. There is nothing per-strategy to validate at construction time. Strategy shape (`canHandle`/`pack`/`unpack`) is enforced by `packingStrategySchema` in `@suaveplan/bitpacker` at the registration boundary, so this package does not duplicate that validation.
- **`instanceof` over duck-typing.** A plain object with `add`/`register` methods would compile, but the order-sensitive insertion logic in `PackingStrategyManager.register` (insert before catch-all) depends on the class internals. Refusing duck-types prevents subtle ordering bugs that would only surface deep into a pack/unpack chain.

View File

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

View File

@@ -0,0 +1,68 @@
# Register Schemas — `@suaveplan/bitpacker-strategies`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Idempotent integration with `@suaveplan/schema-registry` for the `@suaveplan/bitpacker-strategies` package. Exposes a single `registerBitPackerStrategiesSchemas(registry)` function that converts the package's only public boundary schema (`packingStrategyManagerSchema`) into JSON Schema form and registers it under the canonical subject `bitpacker:@suaveplan/bitpacker-strategies` at version 1.
This package's strategies are parameterless, so the only registered surface is the boundary check on `registerDefaultStrategies(manager)` — the strategy classes themselves are validated by `@suaveplan/bitpacker`'s `packingStrategySchema`. Mirroring the cross-package shape avoids duplicating those schemas here.
## Features
- `BITPACKER_STRATEGIES_SUBJECT` constant — `"bitpacker:@suaveplan/bitpacker-strategies"`
- `BITPACKER_STRATEGIES_SCHEMA_VERSION` constant — `1`
- `registerBitPackerStrategiesSchemas(registry)` — idempotent registration
## Usage: Basic Example
```ts
import { createRegistry } from "@suaveplan/schema-registry";
import {
BITPACKER_STRATEGIES_SUBJECT,
registerBitPackerStrategiesSchemas,
} from "@suaveplan/bitpacker-strategies/register-schemas";
const registry = createRegistry();
registerBitPackerStrategiesSchemas(registry);
console.log(registry.getLatest(BITPACKER_STRATEGIES_SUBJECT).version); // 1
```
## Usage: Advanced Example
```ts
import { createRegistry, SchemaRegistry } from "@suaveplan/schema-registry";
import { registerBitPackerSchemas } from "@suaveplan/bitpacker/register-schemas";
import { registerBitPackerStrategiesSchemas } from "@suaveplan/bitpacker-strategies/register-schemas";
function bootstrapAll(registry: SchemaRegistry) {
registerBitPackerSchemas(registry);
registerBitPackerStrategiesSchemas(registry);
}
const registry = createRegistry();
bootstrapAll(registry);
bootstrapAll(registry); // safe: idempotent
```
## API Reference
### `BITPACKER_STRATEGIES_SUBJECT: "bitpacker:@suaveplan/bitpacker-strategies"`
Const string. The exact subject under which the package's schemas are registered.
### `BITPACKER_STRATEGIES_SCHEMA_VERSION: 1`
Const number. The version under which the package's schemas are registered.
### `registerBitPackerStrategiesSchemas(registry: SchemaRegistry): void`
Idempotent registration. If the subject is already present in `registry.listSubjects()`, returns without writing. Otherwise registers the `packingStrategyManager` payload with `{ namespace, package }` metadata.
## Implementation Notes
- **Single-key payload.** This package's only boundary schema is `packingStrategyManagerSchema`. The payload object keeps a single key (`packingStrategyManager`) for parity with the larger families' payloads, so a downstream consumer can pick the schema by name.
- **Idempotency via `listSubjects()` check.** Same pattern as the core package — a membership test is sufficient to keep `version: 1` stable across re-imports.
- **`unrepresentable: "any"`.** The strategy-manager schema uses `z.custom` for the `instanceof` check; without this option, `z.toJSONSchema` throws. Coercing to `{}` is fine because consumers reading the JSON-Schema payload are typically asking "what does this package register?" not "validate me against this schema at runtime."

View File

@@ -0,0 +1,27 @@
# ArrayConfigPackingStrategy
Packs/unpacks arrays described by an explicit configuration object.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Schema shape
```ts
{
type: "array",
maxLength: number,
elementType: "integer" | "float" | "string" | "boolean" | "schema",
elementMax?: number, // for integer / float / string
elementSchema?: BitPackerSchema // for elementType "schema"
}
```
## Behaviour
- Writes the array length using the minimum bits required for `maxLength`.
- Each element is packed according to its `elementType`.
- `float` elements are encoded as unsigned integers up to `elementMax`.
- `string` elements are encoded as fixed-length byte sequences (padded with null bytes).
- `schema` elements are recursively packed via the strategy chain.

View File

@@ -0,0 +1,22 @@
# ArrayNotationPackingStrategy
Packs/unpacks arrays described by a string notation.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Schema shape
String tokens of the form `-length[elementSchema]`, e.g.:
- `-5[10]` → array of up to 5 integers (max 10)
- `-5[true]` → array of up to 5 booleans
- `-3[{"x":255}]` → array of up to 3 objects
## Behaviour
Parses the notation into an `ArrayConfig` and packs:
- Length prefix using the minimum bits for `length`.
- Elements encoded according to the inner schema.

View File

@@ -0,0 +1,22 @@
# ArraySchemaPackingStrategy
Packs/unpacks fixed-length tuple schemas.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Schema shape
An array where each element is a distinct schema:
```ts
[255, "-16", true] // tuple of [uint8, variable-string, boolean]
```
## Behaviour
- No length prefix is written (the schema itself defines the arity).
- Each data element is packed/unpacked against the schema at the same index.
- Arrays matching suaveplan homogeneous formats (`[schema]` or `[number, schema]`)
are delegated to `ArrayPackingStrategy`.

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## array
**Path:** `bitpacker-strategies/src/strategies/array/array.ts`
**Export:** See source for details.
### What it does
Packing strategy for @suaveplan/bitpacker.

View File

@@ -0,0 +1,18 @@
# BigIntArrayPackingStrategy
Packs/unpacks arrays of 64-bit BigInt values.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Schema tokens
- `"bigint-i64[]"` — signed 64-bit BigInt array
- `"bigint-u64[]"` — unsigned 64-bit BigInt array
## Behaviour
- Accepts `bigint[]`, `BigInt64Array`, or `BigUint64Array`.
- Writes a 16-bit unsigned length prefix.
- Each element is written as 8 bytes (big-endian).

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## bigint
**Path:** `bitpacker-strategies/src/strategies/bigint/bigint.ts`
**Export:** See source for details.
### What it does
Packing strategy for @suaveplan/bitpacker.

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## bitarray
**Path:** `bitpacker-strategies/src/strategies/bitarray/bitarray.ts`
**Export:** See source for details.
### What it does
Packing strategy for @suaveplan/bitpacker.

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## bitset
**Path:** `bitpacker-strategies/src/strategies/bitset/bitset.ts`
**Export:** See source for details.
### What it does
Packing strategy for @suaveplan/bitpacker.

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## blob
**Path:** `bitpacker-strategies/src/strategies/blob/blob.ts`
**Export:** See source for details.
### What it does
Packing strategy for @suaveplan/bitpacker.

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## boolean
**Path:** `bitpacker-strategies/src/strategies/boolean/boolean.ts`
**Export:** See source for details.
### What it does
Packing strategy for @suaveplan/bitpacker.

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## conditional
**Path:** `bitpacker-strategies/src/strategies/conditional/conditional.ts`
**Export:** See source for details.
### What it does
Packing strategy for @suaveplan/bitpacker.

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## customType
**Path:** `bitpacker-strategies/src/strategies/customType/customType.ts`
**Export:** See source for details.
### What it does
Packing strategy for @suaveplan/bitpacker.

View File

@@ -0,0 +1,12 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## datetime
**Path:** `bitpacker-strategies/src/strategies/datetime/datetime.ts`
**Export:** See source for details.
### What it does
Packing strategy for @suaveplan/bitpacker.

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