first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

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.