Files
skills/docs/foundation/bitpacker/bitpacker-compressed/src/compressed-bit-packer/compressed-bit-packer.md
2026-05-29 08:20:56 +01:00

7.5 KiB
Raw Blame History

CompressedBitPacker

(no submodules)

- compressed-bit-packer.schema — Zod schema for `Comp…
  • compressed-bit-packer.schema — Zod schema for CompressedBitPackerOptions, the constructor options bundle accepted by CompressedBitPacker. Validate…

  • compressed-bit-packer.schema — Zod schema for `Comp…

  • compressed-bit-packer.schema — Zod schema for CompressedBitPackerOptions, the constructor options bundle accepted by CompressedBitPacker. Validate…

  • compressed-bit-packer.schema — Zod schema for `Comp…

  • compressed-bit-packer.schema — Zod schema for CompressedBitPackerOptions, the constructor options bundle accepted by CompressedBitPacker. Validate…

  • compressed-bit-packer.schema — Zod schema for `Comp…

  • compressed-bit-packer.schema — Zod schema for CompressedBitPackerOptions, the constructor options bundle accepted by CompressedBitPacker. Validate…

  • compressed-bit-packer.schema — 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

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

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.