7.5 KiB
CompressedBitPacker
(no submodules)
- compressed-bit-packer.schema — Zod schema for `Comp…-
compressed-bit-packer.schema — Zod schema for
CompressedBitPackerOptions, the constructor options bundle accepted byCompressedBitPacker. Validate… -
compressed-bit-packer.schema — Zod schema for `Comp…
-
compressed-bit-packer.schema — Zod schema for
CompressedBitPackerOptions, the constructor options bundle accepted byCompressedBitPacker. Validate… -
compressed-bit-packer.schema — Zod schema for `Comp…
-
compressed-bit-packer.schema — Zod schema for
CompressedBitPackerOptions, the constructor options bundle accepted byCompressedBitPacker. Validate… -
compressed-bit-packer.schema — Zod schema for `Comp…
-
compressed-bit-packer.schema — Zod schema for
CompressedBitPackerOptions, the constructor options bundle accepted byCompressedBitPacker. Validate… -
compressed-bit-packer.schema — Zod schema for
CompressedBitPackerOptions, the constructor options bundle accepted byCompressedBitPacker. 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
Errorthrows →BitpackerErrorwith stable codes (UNKNOWN_COMPRESSION_ALGORITHM,COMPRESSION_FAILED,DECOMPRESSION_FAILED,COMPRESSED_BAD_HEADER). - The synchronous
pack/unpackoverrides from upstream — which threw when compression was enabled — are dropped. Consumers call the explicit asyncpackWithCompression/unpackWithDecompressionmethods. 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'sBitPackerdoes not extend an emitter; observability is OTel-driven viabitpacker-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
compressionconfig or runtime accessors. - Self-describing wire format: receiver dispatches to the named algorithm without prior knowledge.
- Synchronous
pack/unpackfromBitPackerremain for the no-compression case (no header added — bytes are exactly whatBitPacker.packproduces). - Pluggable
CompressionRegistryvia 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.