# VersionPackingStrategy _(no submodules)_ ## Purpose Packing strategy for the version-pinned schema shape `{ _version: number; _schema: BitPackerSchema; _tolerance?: "strict" | "forward"; _allowedVersions?: number[] }`. Writes a 16-bit unsigned big-endian wire-version followed by the schema-encoded payload. On unpack, the strategy reads the wire-version and either matches it strictly against `_version` (the default) or accepts a configurable set of alternate versions when the schema opts into forward tolerance. Forward tolerance is the right choice for clients that read newer-version payloads from a server they don't control: setting `_tolerance: "forward"` lets the client decode a future-versioned wire format using its current schema, as long as the consumer guarantees structural compatibility (e.g. fields added in the newer version are wrapped in `_optional` so they decode as `undefined` against the older schema). Strict mode remains the default because most consumers benefit from the early failure of a version mismatch. ## Features - 16-bit big-endian wire-version prefix (range `[0, 65535]`) - Strict mode (default) — throws `BitpackerError(INVALID_SCHEMA)` on any wire-version mismatch - Forward-tolerant mode (`_tolerance: "forward"`) — accepts alternate versions - Optional `_allowedVersions: number[]` — restricts forward-tolerant acceptance to a curated allowlist - Wire format unchanged across all modes — only the unpack-time validation changes ## Usage: Basic Example ```ts import { BitPacker } from "@suaveplan/bitpacker"; import { registerDefaultStrategies } from "@suaveplan/bitpacker-strategies"; const bp = new BitPacker(); registerDefaultStrategies(bp.strategyManager); const schema = { _version: 1, _schema: 255 }; const bytes = bp.pack(42, schema); console.log(bp.unpack(bytes, schema)); // 42 ``` ## Usage: Advanced Example ```ts import { BitPacker, BitpackerError, BITPACKER_CODES } from "@suaveplan/bitpacker"; import { registerDefaultStrategies } from "@suaveplan/bitpacker-strategies"; const bp = new BitPacker(); registerDefaultStrategies(bp.strategyManager); // Write at version 1: const writeSchema = { _version: 1, _schema: 255 }; const wireBytes = bp.pack(42, writeSchema); // Read with forward tolerance, accepting versions 1-3: const readSchema = { _version: 1, _schema: 255, _tolerance: "forward" as const, _allowedVersions: [1, 2, 3], }; console.log(bp.unpack(wireBytes, readSchema)); // 42 // Reading a payload tagged version 9 with the same readSchema fails: const futureBytes = bp.pack(42, { _version: 9, _schema: 255 }); try { bp.unpack(futureBytes, readSchema); } catch (err) { if (err instanceof BitpackerError && err.code === BITPACKER_CODES.INVALID_SCHEMA) { // Error context: { expected: 1, received: 9, allowedVersions: [1, 2, 3] } } } ``` ## API Reference ### `class VersionPackingStrategy implements PackingStrategy` #### `canHandle(schema)` Returns `true` if `isVersionSchema(schema)` is `true`. #### `pack(data, schema, ctx)` Writes 16 bits of `_version` masked to 16 bits, then delegates to `ctx.packValue(data, schema._schema)`. #### `unpack(schema, ctx)` Reads 16 bits of wire-version. Behaviour depends on `_tolerance`: - `"strict"` (default, or omitted): throw `BitpackerError(INVALID_SCHEMA)` on mismatch. - `"forward"` with `_allowedVersions`: throw if the wire-version is NOT in the allowlist. - `"forward"` without `_allowedVersions`: accept any wire-version in `[0, 65535]`. On acceptance, delegates to `ctx.unpackValue(schema._schema)`. ### `function isVersionSchema(value): value is VersionSchemaShape` Type guard. Returns `true` for any object with `_version: number` (in `[0, 65535]`), `_schema: BitPackerSchema`, optional `_tolerance: "strict" | "forward"`, and optional `_allowedVersions: number[]` (each in `[0, 65535]`). ### `type VersionTolerance = "strict" | "forward"` The two supported tolerance modes. ## Implementation Notes - **Wire format unchanged.** Forward-tolerance is purely an unpack-time decision. Bytes produced by a strict-mode pack are byte-identical to those produced by a forward-mode pack at the same `_version`. Bytes produced by either mode are unpacked equivalently when wire-versions match. - **Schema identity on success.** When forward-tolerance accepts an alternate wire-version, the strategy still uses `_schema` for decoding. This places the burden on the consumer to ensure the schema can decode the alternate wire format. Typical pattern: wrap fields added in newer versions in `_optional` so they decode as `undefined` against the older schema. - **`_allowedVersions: undefined` is permissive, `[]` is restrictive.** An empty allowlist accepts no alternate versions (forward-tolerant mode collapses to strict). An undefined allowlist with `_tolerance: "forward"` accepts any wire-version. - **No silent migration.** The strategy does NOT auto-migrate fields between versions. Migrations are the consumer's responsibility — typically encoded as `_optional` wrappers with default values, or as the richer `MigrationStrategy` framework in `@suaveplan/bitpacker`'s `schema-versioning-types`. - **Stable wire-version cap at 16 bits.** A schema with `_version > 65535` is rejected by `isVersionSchema` and would not be dispatched to this strategy in the first place.