123 lines
7.5 KiB
Markdown
123 lines
7.5 KiB
Markdown
# 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.
|