Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
# @suaveplan/serialize - Round-trip-preserving structured serialization — tagged JSON, MessagePack binary, streaming, eval-safe by default
|
||||
|
||||
## Summary
|
||||
|
||||
Round-trip-preserving structured serialization for JavaScript's rich type system. The standard `JSON.stringify`/`JSON.parse` pair silently drops `Date` objects, `Map`, `Set`, `BigInt`, `Uint8Array`, `URL`, `RegExp`, `undefined`, `NaN`, and `±Infinity`. This package preserves all of them via a tagged-JSON codec (`serialize`/`deserialize`) and an efficient MessagePack binary codec (`encodeBinary`/`decodeBinary`). It also provides 4-byte-framed streaming codecs for `ReadableStream` pipelines, cycle detection with path context, a pluggable type registry for custom classes, and an opt-in `/unsafe` subpath that handles `Function`-bearing payloads. The default entry point is eval-safe. Use `@suaveplan/serialize` whenever you need to serialise application state across process boundaries, persist structured data, or stream binary payloads — and want to get back exactly what you put in.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [eval-safe](./src/adapters/eval-safe/eval-safe.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [eval-safe](./src/adapters/eval-safe/eval-safe.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [json-decode](./src/codec/json/json-decode.md) — Implements `deserialize`, the inverse of `serialize`. Given a tagged JSON string produced by the encoder, it reconstruc…
|
||||
- [json-encode](./src/codec/json/json-encode.md) — Implements the `serialize` function that converts any JavaScript value to a tagged JSON string preserving type informat…
|
||||
- [msgpack-decode](./src/codec/msgpack/msgpack-decode.md) — Implements `decodeBinary`, the inverse of `encodeBinary`. Given a MessagePack binary buffer produced by the encoder, it…
|
||||
- [msgpack-encode](./src/codec/msgpack/msgpack-encode.md) — Implements `encodeBinary`, the MessagePack counterpart to `serialize`. It converts any JavaScript value to a compact bi…
|
||||
- [stream](./src/codec/stream/stream.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [stream](./src/codec/stream/stream.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [wire](./src/codec/wire/wire.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [wire.schemas](./src/codec/wire/wire.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `error-codes` is the public re-export surface for the serialization error code registry. It imports `serialize-error` a…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `error-codes` is the public re-export surface for the serialization error code registry. It imports `serialize-error` a…
|
||||
- [codes](./src/errors/codes.md) — `codes` defines the canonical set of error codes for the `@suaveplan/serialize` package using `defineCodes` from `@suav…
|
||||
- [serialize-error](./src/errors/serialize-error.md) — Defines the error hierarchy for `@suaveplan/serialize`. All errors thrown by this package extend `SerializationError`,…
|
||||
- [legacy](./src/legacy/legacy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [legacy](./src/legacy/legacy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [eval-safe](./src/adapters/eval-safe/eval-safe.md) — <!-- begin:auto-docs --> _(no submodules)_- [json-decode](./src/codec/json/json-decode.md) — Implements `deserialize`, the inverse of `serialize`. Given a tagged JSON string produced by the encoder, it reconstruc…
|
||||
- [json-encode](./src/codec/json/json-encode.md) — Implements the `serialize` function that converts any JavaScript value to a tagged JSON string preserving type informat…
|
||||
- [msgpack-decode](./src/codec/msgpack/msgpack-decode.md) — Implements `decodeBinary`, the inverse of `encodeBinary`. Given a MessagePack binary buffer produced by the encoder, it…
|
||||
- [msgpack-encode](./src/codec/msgpack/msgpack-encode.md) — Implements `encodeBinary`, the MessagePack counterpart to `serialize`. It converts any JavaScript value to a compact bi…
|
||||
- [stream](./src/codec/stream/stream.md) — <!-- begin:auto-docs --> _(no submodules)_- [stream](./src/codec/stream/stream.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire](./src/codec/wire/wire.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire.schemas](./src/codec/wire/wire.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — `codes` defines the canonical set of error codes for the `@suaveplan/serialize` package using `defineCodes` from `@suav…
|
||||
- [serialize-error](./src/errors/serialize-error.md) — Defines the error hierarchy for `@suaveplan/serialize`. All errors thrown by this package extend `SerializationError`,…
|
||||
- [legacy](./src/legacy/legacy.md) — <!-- begin:auto-docs --> _(no submodules)_- [legacy](./src/legacy/legacy.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — `error-codes` is the public re-export surface for the serialization error code registry. It imports `serialize-error` a…
|
||||
- [eval-safe](./src/adapters/eval-safe/eval-safe.md) — <!-- begin:auto-docs --> _(no submodules)_- [json-decode](./src/codec/json/json-decode.md) — Implements `deserialize`, the inverse of `serialize`. Given a tagged JSON string produced by the encoder, it reconstruc…
|
||||
- [json-encode](./src/codec/json/json-encode.md) — Implements the `serialize` function that converts any JavaScript value to a tagged JSON string preserving type informat…
|
||||
- [msgpack-decode](./src/codec/msgpack/msgpack-decode.md) — Implements `decodeBinary`, the inverse of `encodeBinary`. Given a MessagePack binary buffer produced by the encoder, it…
|
||||
- [msgpack-encode](./src/codec/msgpack/msgpack-encode.md) — Implements `encodeBinary`, the MessagePack counterpart to `serialize`. It converts any JavaScript value to a compact bi…
|
||||
- [stream](./src/codec/stream/stream.md) — <!-- begin:auto-docs --> _(no submodules)_- [stream](./src/codec/stream/stream.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire](./src/codec/wire/wire.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire.schemas](./src/codec/wire/wire.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — `codes` defines the canonical set of error codes for the `@suaveplan/serialize` package using `defineCodes` from `@suav…
|
||||
- [serialize-error](./src/errors/serialize-error.md) — Defines the error hierarchy for `@suaveplan/serialize`. All errors thrown by this package extend `SerializationError`,…
|
||||
- [legacy](./src/legacy/legacy.md) — <!-- begin:auto-docs --> _(no submodules)_- [legacy](./src/legacy/legacy.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — `error-codes` is the public re-export surface for the serialization error code registry. It imports `serialize-error` a…
|
||||
- [eval-safe](./src/adapters/eval-safe/eval-safe.md) — <!-- begin:auto-docs --> _(no submodules)_- [json-decode](./src/codec/json/json-decode.md) — Implements `deserialize`, the inverse of `serialize`. Given a tagged JSON string produced by the encoder, it reconstruc…
|
||||
- [json-encode](./src/codec/json/json-encode.md) — Implements the `serialize` function that converts any JavaScript value to a tagged JSON string preserving type informat…
|
||||
- [msgpack-decode](./src/codec/msgpack/msgpack-decode.md) — Implements `decodeBinary`, the inverse of `encodeBinary`. Given a MessagePack binary buffer produced by the encoder, it…
|
||||
- [msgpack-encode](./src/codec/msgpack/msgpack-encode.md) — Implements `encodeBinary`, the MessagePack counterpart to `serialize`. It converts any JavaScript value to a compact bi…
|
||||
- [stream](./src/codec/stream/stream.md) — <!-- begin:auto-docs --> _(no submodules)_- [stream](./src/codec/stream/stream.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire](./src/codec/wire/wire.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire.schemas](./src/codec/wire/wire.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — `codes` defines the canonical set of error codes for the `@suaveplan/serialize` package using `defineCodes` from `@suav…
|
||||
- [serialize-error](./src/errors/serialize-error.md) — Defines the error hierarchy for `@suaveplan/serialize`. All errors thrown by this package extend `SerializationError`,…
|
||||
- [legacy](./src/legacy/legacy.md) — <!-- begin:auto-docs --> _(no submodules)_- [legacy](./src/legacy/legacy.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — `error-codes` is the public re-export surface for the serialization error code registry. It imports `serialize-error` a…
|
||||
- [eval-safe](./src/adapters/eval-safe/eval-safe.md) — <!-- begin:auto-docs --> _(no submodules)_- [json-decode](./src/codec/json/json-decode.md) — Implements `deserialize`, the inverse of `serialize`. Given a tagged JSON string produced by the encoder, it reconstruc…
|
||||
- [json-encode](./src/codec/json/json-encode.md) — Implements the `serialize` function that converts any JavaScript value to a tagged JSON string preserving type informat…
|
||||
- [msgpack-decode](./src/codec/msgpack/msgpack-decode.md) — Implements `decodeBinary`, the inverse of `encodeBinary`. Given a MessagePack binary buffer produced by the encoder, it…
|
||||
- [msgpack-encode](./src/codec/msgpack/msgpack-encode.md) — Implements `encodeBinary`, the MessagePack counterpart to `serialize`. It converts any JavaScript value to a compact bi…
|
||||
- [stream](./src/codec/stream/stream.md) — <!-- begin:auto-docs --> _(no submodules)_ - [stream](./src/codec/stream/stream.md) — <!-- begin:auto-docs --> _(no submodules)_- [wire](./src/codec/wire/wire.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wire.schemas](./src/codec/wire/wire.schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — `codes` defines the canonical set of error codes for the `@suaveplan/serialize` package using `defineCodes` from `@suav…
|
||||
- [serialize-error](./src/errors/serialize-error.md) — Defines the error hierarchy for `@suaveplan/serialize`. All errors thrown by this package extend `SerializationError`,…
|
||||
- [legacy](./src/legacy/legacy.md) — <!-- begin:auto-docs --> _(no submodules)_ - [legacy](./src/legacy/legacy.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ - [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_ - [registry](./src/registry/registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — `error-codes` is the public re-export surface for the serialization error code registry. It imports `serialize-error` a…
|
||||
|
||||
## Features
|
||||
|
||||
- **Tagged JSON** — `serialize`/`deserialize` preserve `Date`, `Map`, `Set`, `BigInt`, all TypedArrays, `URL`, `RegExp`, `undefined`, `NaN`, `±Infinity`
|
||||
- **MessagePack binary** — `encodeBinary`/`decodeBinary` produce compact binary with canonical key ordering
|
||||
- **Streaming** — `encodeStream`/`decodeStream` via 4-byte-framed `ReadableStream<Uint8Array>`
|
||||
- **Custom type registry** — `registerType` adds arbitrary classes with encode/decode hooks
|
||||
- **Eval-safe adapter** — `@suaveplan/serialize/unsafe` for Function-including payloads (opt-in only)
|
||||
- **AbortSignal support** on all codec paths
|
||||
- **Cycle detection** — `CircularReferenceError` with path context
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { deserialize, serialize } from "@suaveplan/serialize";
|
||||
|
||||
const obj = {
|
||||
created: new Date(),
|
||||
tags: new Set(["a", "b"]),
|
||||
count: BigInt(9007199254740993),
|
||||
};
|
||||
|
||||
const json = serialize(obj);
|
||||
const restored = deserialize(json);
|
||||
// restored.created instanceof Date → true
|
||||
// restored.tags instanceof Set → true
|
||||
// restored.count === 9007199254740993n → true
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| [`src/codec/json/json-encode.md`](src/codec/json/json-encode.md) | Tagged JSON encoder |
|
||||
| [`src/codec/json/json-decode.md`](src/codec/json/json-decode.md) | Tagged JSON decoder |
|
||||
| [`src/codec/msgpack/msgpack-encode.md`](src/codec/msgpack/msgpack-encode.md) | MessagePack encoder |
|
||||
| [`src/codec/msgpack/msgpack-decode.md`](src/codec/msgpack/msgpack-decode.md) | MessagePack decoder |
|
||||
| [`src/codec/stream/stream.md`](src/codec/stream/stream.md) | Streaming framed codec |
|
||||
| [`src/registry/registry.md`](src/registry/registry.md) | Type registry |
|
||||
| [`src/errors/serialize-error.md`](src/errors/serialize-error.md) | Error hierarchy |
|
||||
| [`src/legacy/legacy.md`](src/legacy/legacy.md) | Legacy Date-only adapter |
|
||||
| [`src/adapters/eval-safe/eval-safe.md`](src/adapters/eval-safe/eval-safe.md) | Eval-safe adapter (unsafe subpath) |
|
||||
|
||||
## API
|
||||
|
||||
See co-located `.md` files for each module's full API reference.
|
||||
|
||||
## Subpath Exports
|
||||
|
||||
```typescript
|
||||
import { serialize, deserialize } from "@suaveplan/serialize"; // main
|
||||
import { serializeEvalSafe } from "@suaveplan/serialize/unsafe"; // eval-capable (opt-in)
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
# eval-safe
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `serializeEvalSafe` and `deserializeEvalSafe` as an explicit opt-in adapter for scenarios where the standard tagged JSON or MessagePack codecs cannot be used — for example, when the payload must include JavaScript `Function` values, `RegExp` literals with complex syntax, or other types that cannot be expressed in a JSON-safe intermediate. The adapter wraps the `serialize-javascript` library, which is purpose-built for this use case (e.g. server-side Redux initial state injection into HTML).
|
||||
|
||||
This module lives under the `./unsafe` subpath export and must **never** appear in the main `@suaveplan/serialize` bundle. Importing it is an explicit acknowledgment of the security trade-off.
|
||||
|
||||
## Features
|
||||
|
||||
- `serializeEvalSafe` — produces a JavaScript expression string via `serialize-javascript` that, when eval'd, reconstructs the original value including Functions and RegExps
|
||||
- `deserializeEvalSafe` — evaluates the produced string via `(0, eval)(...)` (indirect eval) to reconstruct the value
|
||||
- Security warning emitted on every call unless `{ ignoreSecurityWarning: true }` is passed
|
||||
- `EvalSafeOptions` interface for suppressing warnings in controlled environments
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { serializeEvalSafe, deserializeEvalSafe } from "@suaveplan/serialize/unsafe";
|
||||
|
||||
// Suppress warnings in a trusted server-side context
|
||||
const str = serializeEvalSafe({ fn: () => 42 }, { ignoreSecurityWarning: true });
|
||||
const restored = deserializeEvalSafe(str, { ignoreSecurityWarning: true });
|
||||
// (restored as any).fn() === 42
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { serializeEvalSafe } from "@suaveplan/serialize/unsafe";
|
||||
|
||||
// Inject Redux initial state into an HTML page server-side.
|
||||
// This is the canonical use case for serialize-javascript.
|
||||
const html = `
|
||||
<script>
|
||||
window.__INITIAL_STATE__ = ${serializeEvalSafe(reduxState, { ignoreSecurityWarning: true })};
|
||||
</script>
|
||||
`;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `serializeEvalSafe(value: unknown, opts?: EvalSafeOptions): string`
|
||||
|
||||
Serialize `value` using `serialize-javascript`. The output is a JavaScript expression string, not JSON.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `value` — any JavaScript value, including Functions and RegExps
|
||||
- `opts.ignoreSecurityWarning` — pass `true` to suppress the `console.warn`
|
||||
|
||||
**Returns:** a JavaScript expression string.
|
||||
|
||||
### `deserializeEvalSafe(json: string, opts?: EvalSafeOptions): unknown`
|
||||
|
||||
Evaluate `json` as a JavaScript expression and return the result.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `json` — a string produced by `serializeEvalSafe`; MUST be from a trusted source
|
||||
- `opts.ignoreSecurityWarning` — pass `true` to suppress the `console.warn`
|
||||
|
||||
**Returns:** the reconstructed value.
|
||||
|
||||
**Security:** Uses indirect `eval` (`(0, eval)(...)`), which runs in the global scope rather than the local closure scope, slightly reducing the attack surface compared to direct `eval`.
|
||||
|
||||
### `EvalSafeOptions`
|
||||
|
||||
```typescript
|
||||
interface EvalSafeOptions {
|
||||
ignoreSecurityWarning?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `(0, eval)(...)` pattern (indirect eval) is used intentionally: it evaluates in the global scope rather than capturing local variables, which slightly reduces the blast radius if a malicious string is accidentally evaluated.
|
||||
- `serialize-javascript` is listed as a regular dependency (not a dev dependency) because it is shipped as part of the `/unsafe` subpath. The main bundle's Rollup config marks it as `external`, so it is never bundled into `dist/index.js`.
|
||||
- The biome `lint/security/noEval` rule is suppressed with a comment at the call site to document that the eval usage is intentional, reviewed, and isolated to this specific adapter.
|
||||
@@ -0,0 +1,85 @@
|
||||
# json-decode
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `deserialize`, the inverse of `serialize`. Given a tagged JSON string produced by the encoder, it reconstructs the original JavaScript value graph including all registered types (built-ins and custom), `undefined`, `NaN`, and `±Infinity`. Any `__t` tag that has no registered handler causes an `UnsupportedTypeError` to be thrown immediately rather than silently discarding data.
|
||||
|
||||
The decoder is intentionally strict: it is safer to fail loudly on an unrecognised tag than to silently drop values, which could cause subtle data corruption downstream.
|
||||
|
||||
## Features
|
||||
|
||||
- Full inverse of `serialize`: all built-in type tags are decoded back to their original JavaScript types
|
||||
- `AbortSignal` support for cancelling mid-traversal on large payloads, checked every 256 nodes
|
||||
- `UnsupportedTypeError` thrown (with the tag name in `context.tag`) for unrecognised `__t` values
|
||||
- Recursive array and plain-object decoding
|
||||
- Zero allocations for pass-through primitives (string, number, boolean, null)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { deserialize, serialize } from "@suaveplan/serialize";
|
||||
|
||||
const original = { created: new Date(), tags: new Set(["x", "y"]) };
|
||||
const json = serialize(original);
|
||||
const restored = deserialize(json) as typeof original;
|
||||
|
||||
restored.created instanceof Date; // true
|
||||
restored.tags instanceof Set; // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { deserialize, UnsupportedTypeError } from "@suaveplan/serialize";
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
// Cancel deserialization partway through:
|
||||
setTimeout(() => controller.abort(new Error("timeout")), 10);
|
||||
|
||||
try {
|
||||
const value = deserialize(largeJson, { signal: controller.signal });
|
||||
processValue(value);
|
||||
} catch (err) {
|
||||
if (err instanceof UnsupportedTypeError) {
|
||||
console.error("Unknown type tag:", err.context?.tag);
|
||||
} else {
|
||||
// likely the abort reason
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `deserialize(json: string, opts?: DeserializeOptions): unknown`
|
||||
|
||||
Parse `json` and recursively reconstruct the JavaScript value it encodes.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `json` — a tagged JSON string produced by `serialize`
|
||||
- `opts.signal` — an `AbortSignal`; checked every 256 nodes
|
||||
|
||||
**Returns:** the reconstructed value.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `UnsupportedTypeError` when a `__t` tag has no registered decoder
|
||||
- `SyntaxError` from `JSON.parse` if the input is not valid JSON
|
||||
- The `AbortSignal`'s reason when `signal` is aborted
|
||||
|
||||
### `DeserializeOptions`
|
||||
|
||||
```typescript
|
||||
interface DeserializeOptions {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The decoder calls `JSON.parse` once at the top level and then recursively traverses the parsed intermediate tree. This avoids parsing partial JSON strings and keeps the implementation simple.
|
||||
- For tagged objects, the inner `v` payload is decoded recursively *before* being handed to `def.decode`. This means registered type decoders always receive already-decoded JavaScript values, not raw JSON intermediates.
|
||||
- Signal checking is shared with a module-level counter that resets per `deserialize` call; it fires every 256 traversed nodes.
|
||||
- The decoder does not validate the structure of `v` beyond calling `def.decode`; responsibility for validating the payload shape belongs to each registered type definition.
|
||||
@@ -0,0 +1,99 @@
|
||||
# json-encode
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements the `serialize` function that converts any JavaScript value to a tagged JSON string preserving type information beyond what `JSON.stringify` supports. The encoder traverses the value graph recursively, wraps non-primitive types in `{ "__t": tag, "v": ... }` envelopes handled by the type registry, and detects both cycles and excessive nesting depth before they cause runtime errors.
|
||||
|
||||
The tagged JSON format is self-describing: a receiver that imports `deserialize` from the same package can reconstruct the original value graph without out-of-band schema information.
|
||||
|
||||
## Features
|
||||
|
||||
- Handles all types registered in the global registry (Date, Map, Set, BigInt, TypedArrays, URL, RegExp, custom classes)
|
||||
- Preserves `undefined` as `{"__t":"__undefined","v":null}` (normally silently dropped by `JSON.stringify`)
|
||||
- Preserves `NaN` as `{"__t":"__NaN","v":null}` and `±Infinity` as `{"__t":"__Infinity","v":"+"/"−"}`
|
||||
- Detects circular references via `WeakSet` and throws `CircularReferenceError` with the structural path
|
||||
- Configurable maximum depth (default 100) via `maxDepth` option; throws `MaxDepthExceededError`
|
||||
- Configurable maximum byte length via `maxSize` option; throws `MaxSizeExceededError`
|
||||
- Honour `AbortSignal` cancellation; checked every 256 nodes for minimal overhead
|
||||
- `seen` set cleared per-branch (re-entrant DAG nodes are allowed, only true cycles throw)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { serialize } from "@suaveplan/serialize";
|
||||
|
||||
const obj = {
|
||||
created: new Date("2024-01-01"),
|
||||
tags: new Set(["a", "b"]),
|
||||
count: BigInt(9007199254740993),
|
||||
};
|
||||
|
||||
const json = serialize(obj);
|
||||
// '{"created":{"__t":"__Date","v":"2024-01-01T00:00:00.000Z"},...}'
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { serialize } from "@suaveplan/serialize";
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
// Enforce max 50KB output and max 20 levels of nesting
|
||||
const json = serialize(hugeObject, {
|
||||
maxDepth: 20,
|
||||
maxSize: 50_000,
|
||||
signal: controller.signal,
|
||||
});
|
||||
sendToNetwork(json);
|
||||
} catch (err) {
|
||||
if (err instanceof MaxSizeExceededError) {
|
||||
console.error("Payload too large:", err.context?.size, "bytes");
|
||||
} else if (err instanceof MaxDepthExceededError) {
|
||||
console.error("Too deeply nested at depth:", err.context?.depth);
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel mid-way through a large serialization:
|
||||
controller.abort();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `serialize(value: unknown, opts?: SerializeOptions): string`
|
||||
|
||||
Converts `value` to a tagged JSON string.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `value` — any JavaScript value; functions and symbols are silently dropped (same as `JSON.stringify`)
|
||||
- `opts.maxDepth` — maximum recursion depth before `MaxDepthExceededError` is thrown (default: `100`)
|
||||
- `opts.maxSize` — maximum byte length of the resulting JSON string; checked after encoding (default: unlimited)
|
||||
- `opts.signal` — an `AbortSignal`; checked every 256 nodes; throws the signal's reason if aborted
|
||||
|
||||
**Returns:** a JSON string in the tagged wire format.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `CircularReferenceError` when a cycle is detected
|
||||
- `MaxDepthExceededError` when nesting exceeds `maxDepth`
|
||||
- `MaxSizeExceededError` when output exceeds `maxSize` bytes
|
||||
- The `AbortSignal`'s reason when `signal` is aborted
|
||||
|
||||
### `SerializeOptions`
|
||||
|
||||
```typescript
|
||||
interface SerializeOptions {
|
||||
maxDepth?: number; // default: 100
|
||||
maxSize?: number; // default: unlimited
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The cycle-detection `WeakSet` (`seen`) is managed with add-before/delete-after so that DAG structures (same object reachable via multiple paths) do not falsely trigger the cycle check. Only true back-edges to an ancestor in the current call stack throw.
|
||||
- Signal checking happens every 256 nodes rather than every node to keep the hot path free of extra branch overhead.
|
||||
- The `maxSize` check occurs **after** `JSON.stringify` on the fully encoded intermediate object, not during encoding. This trades one additional pass for implementation simplicity and avoids double-counting tag overhead.
|
||||
- `undefined` at object property positions is omitted by `JSON.stringify` but handled explicitly here by converting it to a tagged sentinel before the `JSON.stringify` call.
|
||||
@@ -0,0 +1,75 @@
|
||||
# msgpack-decode
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `decodeBinary`, the inverse of `encodeBinary`. Given a MessagePack binary buffer produced by the encoder, it reconstructs the original JavaScript value by first decoding the msgpack wire bytes to an intermediate plain-JS-object representation and then applying the same tag-based reconstruction logic used by `deserialize`.
|
||||
|
||||
The two-phase approach (msgpack decode → tag reconstruct) keeps the decoder code minimal and reuses the same registry lookup path as the JSON decoder.
|
||||
|
||||
## Features
|
||||
|
||||
- Full inverse of `encodeBinary`: all built-in and custom registered types are reconstructed
|
||||
- `UnsupportedTypeError` thrown on unrecognised `__t` tags (same strict policy as JSON decoder)
|
||||
- Reuses a single shared `Decoder` instance for zero allocation overhead across calls
|
||||
- `DeserializeOptions` parameter accepted for API consistency with the JSON codec (signal not yet applied internally, as msgpack decode is synchronous and very fast)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { decodeBinary, encodeBinary } from "@suaveplan/serialize";
|
||||
|
||||
const original = new Map([["key", new Date()]]);
|
||||
const bytes = encodeBinary(original);
|
||||
const restored = decodeBinary(bytes) as Map<string, Date>;
|
||||
|
||||
restored instanceof Map; // true
|
||||
restored.get("key") instanceof Date; // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { decodeBinary, UnsupportedTypeError } from "@suaveplan/serialize";
|
||||
|
||||
async function receiveMessage(bytes: Uint8Array) {
|
||||
try {
|
||||
const value = decodeBinary(bytes);
|
||||
return value;
|
||||
} catch (err) {
|
||||
if (err instanceof UnsupportedTypeError) {
|
||||
// The sender used a type tag unknown to this receiver.
|
||||
// Either register the type or reject the message.
|
||||
throw new Error(`Unknown type: ${err.context?.tag}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `decodeBinary(bytes: Uint8Array, opts?: DeserializeOptions): unknown`
|
||||
|
||||
Decode a MessagePack buffer to the original JavaScript value.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `bytes` — a `Uint8Array` produced by `encodeBinary`
|
||||
- `opts` — `DeserializeOptions` (accepted for API parity; signal not currently applied within this synchronous path)
|
||||
|
||||
**Returns:** the reconstructed JavaScript value.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `UnsupportedTypeError` when a `__t` tag has no registered decoder
|
||||
- `RangeError` or `TypeError` from `@msgpack/msgpack` if the input bytes are malformed
|
||||
|
||||
### `DeserializeOptions`
|
||||
|
||||
See `json-decode.md`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- A single shared `Decoder` instance is reused to avoid per-call setup overhead; the decoder is stateless between calls.
|
||||
- The `postDecode` function is structurally identical to the inner `decodeValue` used by `json-decode.ts`. Future refactoring could extract it to a shared utility; currently it is kept local to avoid premature abstraction.
|
||||
- The `opts` parameter is accepted but currently unused because the msgpack decode step is synchronous and does not lend itself to mid-stream cancellation without a streaming API.
|
||||
@@ -0,0 +1,78 @@
|
||||
# msgpack-encode
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `encodeBinary`, the MessagePack counterpart to `serialize`. It converts any JavaScript value to a compact binary `Uint8Array` using the same type-registry tag scheme as the JSON encoder. By pre-processing the input into plain msgpack-compatible intermediate values before handing off to `@msgpack/msgpack`, the codec avoids the complexity of a custom extension codec while still preserving all built-in and custom registered types.
|
||||
|
||||
Object keys are sorted canonically before encoding, so two structurally identical objects always produce bit-identical output regardless of property insertion order.
|
||||
|
||||
## Features
|
||||
|
||||
- Produces a compact binary wire format via `@msgpack/msgpack`
|
||||
- Same type-registry integration as the JSON codec: all built-in and custom types round-trip faithfully
|
||||
- Canonical key ordering for deterministic binary output
|
||||
- `maxDepth`, `maxSize`, and `AbortSignal` options parallel to the JSON encoder
|
||||
- Cycle detection via `WeakSet` — throws `CircularReferenceError` on cycles
|
||||
- Throws `MaxDepthExceededError` and `MaxSizeExceededError` on limit violations
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { decodeBinary, encodeBinary } from "@suaveplan/serialize";
|
||||
|
||||
const payload = { created: new Date(), ids: new Set([1, 2, 3]) };
|
||||
const bytes = encodeBinary(payload);
|
||||
// bytes is a compact Uint8Array — smaller than JSON for numeric-heavy data
|
||||
|
||||
const restored = decodeBinary(bytes);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { encodeBinary } from "@suaveplan/serialize";
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
const bytes = encodeBinary(largeDataset, {
|
||||
maxDepth: 30,
|
||||
maxSize: 1_000_000, // 1 MB
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
// Send over WebSocket, IndexedDB, etc.
|
||||
socket.send(bytes);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `encodeBinary(value: unknown, opts?: SerializeOptions): Uint8Array`
|
||||
|
||||
Encodes `value` to a MessagePack binary buffer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `value` — any JavaScript value; functions and symbols are silently dropped
|
||||
- `opts.maxDepth` — maximum recursion depth (default: `100`)
|
||||
- `opts.maxSize` — maximum byte length of the binary output; checked after encoding
|
||||
- `opts.signal` — `AbortSignal`; checked once before encoding begins
|
||||
|
||||
**Returns:** a `Uint8Array` containing the MessagePack payload.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `CircularReferenceError` on cycles
|
||||
- `MaxDepthExceededError` when nesting exceeds `maxDepth`
|
||||
- `MaxSizeExceededError` when output exceeds `maxSize` bytes
|
||||
- The `AbortSignal`'s reason if aborted
|
||||
|
||||
### `SerializeOptions`
|
||||
|
||||
See `json-encode.md` — the same options interface is shared.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- A single shared `Encoder` instance is reused across calls to amortise initialisation overhead; the encoder is stateless between calls.
|
||||
- The `preEncode` helper mirrors the logic in `json-encode.ts` but emits plain objects/arrays instead of a JSON string, avoiding a second parse step.
|
||||
- `AbortSignal` is checked once at the call boundary rather than every 256 nodes because msgpack encoding is typically fast and does not support mid-stream interruption without a custom streaming approach.
|
||||
- Canonical key sorting uses the default `Array.prototype.sort` comparator (lexicographic UTF-16 code units), matching the sort used by many canonical JSON implementations.
|
||||
@@ -0,0 +1,92 @@
|
||||
# stream
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `encodeStream` and `decodeStream` for streaming serialization over environments that produce or consume `ReadableStream<Uint8Array>`. Rather than requiring the entire payload to be buffered before transmission, `encodeStream` chunks the MessagePack binary output into 4-byte-length-prefixed frames of up to 4 KB each, enabling progressive delivery over WebSockets, HTTP streaming responses, or other byte-stream transports.
|
||||
|
||||
`decodeStream` is the inverse: it reads framed chunks from a `ReadableStream<Uint8Array>`, reassembles the payload, and emits a single decoded value via a `ReadableStream<unknown>`.
|
||||
|
||||
## Features
|
||||
|
||||
- Frame format: 4-byte big-endian payload length + up to 4 KB payload
|
||||
- Streaming encode over `ReadableStream<Uint8Array>` using `encodeBinary` internally
|
||||
- Streaming decode that handles partial frames across multiple read chunks
|
||||
- `AbortSignal` support in both encode and decode paths
|
||||
- Errors propagated via `controller.error()` so consumers can catch via stream pipelines
|
||||
- Zero external deps beyond the msgpack codec and Web Streams API
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { decodeStream, encodeStream } from "@suaveplan/serialize";
|
||||
|
||||
const payload = { data: new Array(1000).fill("item") };
|
||||
const stream = encodeStream(payload);
|
||||
|
||||
// In the same process or after transmission:
|
||||
const decoded = decodeStream(stream);
|
||||
const reader = decoded.getReader();
|
||||
const { value } = await reader.read();
|
||||
// value deep-equals payload
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { decodeStream, encodeStream } from "@suaveplan/serialize";
|
||||
|
||||
// Abort if encoding takes too long
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(new Error("encode timeout")), 5000);
|
||||
|
||||
const stream = encodeStream(hugeObject, { signal: controller.signal });
|
||||
|
||||
// Pipe to a fetch body or WebSocket
|
||||
const response = new Response(stream, {
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
|
||||
// On the receiving side:
|
||||
const incoming: ReadableStream<Uint8Array> = response.body!;
|
||||
const valueStream = decodeStream(incoming);
|
||||
const reader = valueStream.getReader();
|
||||
const { value } = await reader.read();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `encodeStream(value: unknown, opts?: SerializeOptions & { signal?: AbortSignal }): ReadableStream<Uint8Array>`
|
||||
|
||||
Serialize `value` using `encodeBinary` and emit the result as framed 4 KB chunks.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `value` — any serializable JavaScript value
|
||||
- `opts` — same as `SerializeOptions` plus optional `signal`
|
||||
|
||||
**Returns:** a `ReadableStream<Uint8Array>` where each chunk is a complete length-prefixed frame.
|
||||
|
||||
**Throws (via stream error):** all errors from `encodeBinary` plus `AbortSignal` reason.
|
||||
|
||||
### `decodeStream(stream: ReadableStream<Uint8Array>, opts?: DeserializeOptions): ReadableStream<unknown>`
|
||||
|
||||
Reassemble frames from `stream` and emit the decoded value.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `stream` — a framed stream produced by `encodeStream`
|
||||
- `opts` — `DeserializeOptions` including optional `signal`
|
||||
|
||||
**Returns:** a `ReadableStream<unknown>` that emits exactly one decoded value then closes.
|
||||
|
||||
**Throws (via stream error):** all errors from `decodeBinary` plus `AbortSignal` reason.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Each frame is self-delimiting (4-byte length prefix), so the receiver never needs to know the total payload size in advance.
|
||||
- `decodeStream` handles partial frames across multiple read iterations via an internal accumulator; frames that span chunk boundaries are correctly reassembled.
|
||||
- If the encoded payload is empty (e.g. for `null` or `undefined`), a single zero-payload frame is emitted so the stream does not close silently without producing any data.
|
||||
- The Web Streams API (`ReadableStream`) is used throughout, making the codec compatible with browser, Node.js (≥18), Bun, and Deno environments without polyfills.
|
||||
@@ -0,0 +1,57 @@
|
||||
# wire.schemas
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the Zod schemas that pin the **wire-format invariants** of the serialize codecs. Both the JSON codec and the MessagePack codec produce structurally identical intermediate values: a recursive tree of JSON-compatible primitives, arrays, plain records, and tagged envelopes (`{ "__t": "<tag>", "v": <encoded> }`). This module defends the public `deserialize` and `decodeBinary` boundaries against accidental or malicious inputs that would otherwise reach the type registry's `decode` functions in an unexpected shape — for example a number where an envelope is expected, or an envelope with `__t` set to a non-string value.
|
||||
|
||||
The schemas are exported as both reusable values (`WireNodeSchema`, `TaggedEnvelopeSchema`) and an ergonomic helper (`validateWireNode`) that wraps `safeParse` and exposes the parse outcome as a discriminated union so call sites can wrap rejection in a `MalformedWireError` with stable code metadata.
|
||||
|
||||
## Features
|
||||
|
||||
- Recursive `WireNodeSchema` covering primitives, arrays, tagged envelopes, and plain records
|
||||
- Strict `TaggedEnvelopeSchema` that rejects extra keys
|
||||
- `validateWireNode` helper returning `{ ok: true; value } | { ok: false; error }`
|
||||
- Used by `deserialize` (JSON) and `decodeBinary` (MessagePack) at the public boundary
|
||||
- Pure, side-effect free; tree-shakes when unused
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { validateWireNode } from "@suaveplan/serialize";
|
||||
|
||||
const result = validateWireNode({ __t: "__Date", v: "2024-01-01T00:00:00.000Z" });
|
||||
if (result.ok) {
|
||||
// safe to feed to the type-registry decode pipeline
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MalformedWireError, validateWireNode } from "@suaveplan/serialize";
|
||||
|
||||
function strictDeserialize(input: unknown): unknown {
|
||||
const wire = validateWireNode(input);
|
||||
if (!wire.ok) {
|
||||
throw new MalformedWireError(
|
||||
"input does not match the tagged-wire schema",
|
||||
"root",
|
||||
wire.error
|
||||
);
|
||||
}
|
||||
return wire.value;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `WireNodeSchema: z.ZodType<unknown>` — recursive schema for any wire-format node.
|
||||
- `TaggedEnvelopeSchema: z.ZodObject<{ __t: ZodString, v: ZodUnknown }>` — strict envelope schema.
|
||||
- `validateWireNode(value: unknown): { ok: true; value: unknown } | { ok: false; error: ZodError }` — non-throwing validator.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`WireNodeSchema` is defined via `z.lazy()` to support recursion through arrays and records. The envelope schema uses `.strict()` so an envelope with a smuggled-in third key is rejected — this matches the `serialize`/`encodeBinary` output, which only ever emits `__t` + `v`. We intentionally exclude `BigInt`, `Map`, `Set`, and other JS-only constructs from the schema: the wire format never produces them at this layer (BigInts arrive as a `__BigInt` envelope with a string `v`), so admitting them would weaken the invariant.
|
||||
@@ -0,0 +1,65 @@
|
||||
# error-codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`error-codes` is the public re-export surface for the serialization error code registry. It imports `serialize-error` as a side effect — which ensures all `SERIALIZE_CODES` are registered with `@suaveplan/error`'s global code table at the moment any consumer first imports this module — and then re-exports the codes and the `SerializationErrorCode` union type under the stable public alias `SERIALIZE_ERROR_METADATA`.
|
||||
|
||||
The module also exports a `registerSerializeErrorCodes()` function for callers that need an explicit, named registration step (e.g., test setup files or dependency injection bootstraps) rather than relying on side-effect imports.
|
||||
|
||||
## Features
|
||||
|
||||
- Triggers side-effect registration of all `SERIALIZE_CODES` via a single import
|
||||
- Re-exports `SERIALIZE_CODES` as `SERIALIZE_ERROR_METADATA` to match the `@suaveplan/error` naming convention for externally-published code registries
|
||||
- Re-exports the `SerializationErrorCode` union type for use in typed error handling
|
||||
- Provides an explicit `registerSerializeErrorCodes()` no-op function for consumers that prefer named-import bootstrapping over implicit side effects
|
||||
- Lightweight: no runtime logic beyond the re-export and the single side-effect import
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SERIALIZE_ERROR_METADATA } from "@suaveplan/serialize/error-codes";
|
||||
|
||||
// SERIALIZE_ERROR_METADATA is now populated with all SERIALIZE_* codes
|
||||
console.log(SERIALIZE_ERROR_METADATA["CODEC.CIRCULAR_REFERENCE"]);
|
||||
// => "SERIALIZE.CODEC.CIRCULAR_REFERENCE"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { registerSerializeErrorCodes, SERIALIZE_ERROR_METADATA, type SerializationErrorCode } from "@suaveplan/serialize/error-codes";
|
||||
import { SuaveplanError } from "@suaveplan/error";
|
||||
|
||||
// Explicit bootstrap for DI containers that prefer named calls
|
||||
registerSerializeErrorCodes(); // no-op, side effect already happened at import
|
||||
|
||||
function isSerializeError(err: unknown): err is SuaveplanError {
|
||||
if (!(err instanceof SuaveplanError)) return false;
|
||||
const codes = Object.values(SERIALIZE_ERROR_METADATA) as SerializationErrorCode[];
|
||||
return codes.includes(err.code as SerializationErrorCode);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SERIALIZE_ERROR_METADATA`
|
||||
|
||||
Re-export of `SERIALIZE_CODES` from `./errors/codes`. A frozen record mapping each short code key (e.g., `"CODEC.CIRCULAR_REFERENCE"`) to its fully-qualified `"SERIALIZE.CODEC.CIRCULAR_REFERENCE"` string.
|
||||
|
||||
### `type SerializationErrorCode`
|
||||
|
||||
Union of all values in `SERIALIZE_ERROR_METADATA`. Use this type to narrow `SuaveplanError.code` in catch blocks.
|
||||
|
||||
### `registerSerializeErrorCodes(): void`
|
||||
|
||||
Intentional no-op. The actual registration occurs as a side effect of importing this module. The function exists for callers that want a named, explicit registration call for documentation or DI-bootstrap clarity.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The side-effect import pattern (`import "./errors/serialize-error.js"`) is the standard approach used across all `@suaveplan/*` packages to ensure error codes are registered before any error can be constructed or inspected. Consumers that import this module at application startup (or in a test setup file) are guaranteed that all `SERIALIZE_*` codes are present in the global registry even if no serialization operation has been attempted yet.
|
||||
|
||||
The `registerSerializeErrorCodes()` stub is provided as a forward-compatible hook: if registration ever needs to become asynchronous or accept configuration parameters, the function signature can evolve without breaking existing callers.
|
||||
@@ -0,0 +1,92 @@
|
||||
# codes
|
||||
|
||||
## Purpose
|
||||
|
||||
`codes` defines the canonical set of error codes for the `@suaveplan/serialize` package using `defineCodes` from `@suaveplan/error`. Each code encodes both the package origin (`SERIALIZE`) and the subsystem-plus-reason (`CODEC.CIRCULAR_REFERENCE`, `REGISTRY.DUPLICATE_TAG`, etc.), producing fully-qualified strings that are globally unique across the project's error registry.
|
||||
|
||||
By centralising all error codes in this single file, the entire `serialize` package — codecs, registry, legacy adapter, and streaming layer — can import a single frozen object rather than scattering magic strings across modules. The exported `SerializationErrorCode` union provides complete type coverage for `SuaveplanError.code` narrowing in catch blocks.
|
||||
|
||||
## Features
|
||||
|
||||
- Registers six error codes via `defineCodes("SERIALIZE", [...])`:
|
||||
- `CODEC.CIRCULAR_REFERENCE` — JSON/MessagePack encode detected a circular object reference
|
||||
- `CODEC.MAX_DEPTH` — object nesting exceeded the configured depth limit
|
||||
- `CODEC.MAX_SIZE` — encoded output exceeded the configured size limit
|
||||
- `CODEC.UNKNOWN_TAG` — decode encountered a tagged token whose type is not in the registry
|
||||
- `REGISTRY.DUPLICATE_TAG` — a type was registered under a tag already claimed by another handler
|
||||
- `LEGACY.REFUSED` — a legacy deserialisation was attempted on a value the adapter cannot handle
|
||||
- Exports `SERIALIZE_CODES` as a frozen record for use throughout the package
|
||||
- Exports `SerializationErrorCode` union type for typed error narrowing
|
||||
- Zero runtime side effects beyond the `defineCodes` call — safe to import anywhere
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SERIALIZE_CODES } from "@suaveplan/serialize/errors/codes";
|
||||
import { SerializeError } from "@suaveplan/serialize/errors";
|
||||
|
||||
throw new SerializeError(
|
||||
SERIALIZE_CODES["CODEC.CIRCULAR_REFERENCE"],
|
||||
"Cannot serialise: circular reference detected in ctx.payload"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { SERIALIZE_CODES, type SerializationErrorCode } from "@suaveplan/serialize/errors/codes";
|
||||
import { SuaveplanError } from "@suaveplan/error";
|
||||
|
||||
const CODEC_CODES = new Set<string>([
|
||||
SERIALIZE_CODES["CODEC.CIRCULAR_REFERENCE"],
|
||||
SERIALIZE_CODES["CODEC.MAX_DEPTH"],
|
||||
SERIALIZE_CODES["CODEC.MAX_SIZE"],
|
||||
SERIALIZE_CODES["CODEC.UNKNOWN_TAG"],
|
||||
]);
|
||||
|
||||
function isCodecError(err: unknown): boolean {
|
||||
return err instanceof SuaveplanError && CODEC_CODES.has(err.code);
|
||||
}
|
||||
|
||||
// In a catch block:
|
||||
try {
|
||||
const encoded = jsonEncode(value);
|
||||
} catch (err) {
|
||||
if (isCodecError(err)) {
|
||||
logger.warn("Encoding failed — falling back to string coercion", { err });
|
||||
return String(value);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SERIALIZE_CODES`
|
||||
|
||||
```ts
|
||||
const SERIALIZE_CODES: {
|
||||
"CODEC.CIRCULAR_REFERENCE": "SERIALIZE.CODEC.CIRCULAR_REFERENCE";
|
||||
"CODEC.MAX_DEPTH": "SERIALIZE.CODEC.MAX_DEPTH";
|
||||
"CODEC.MAX_SIZE": "SERIALIZE.CODEC.MAX_SIZE";
|
||||
"CODEC.UNKNOWN_TAG": "SERIALIZE.CODEC.UNKNOWN_TAG";
|
||||
"REGISTRY.DUPLICATE_TAG": "SERIALIZE.REGISTRY.DUPLICATE_TAG";
|
||||
"LEGACY.REFUSED": "SERIALIZE.LEGACY.REFUSED";
|
||||
}
|
||||
```
|
||||
|
||||
The object is frozen by `defineCodes` and safe to use as a constant map. Each key is the short, human-readable code; each value is the fully-qualified string registered in the global error table.
|
||||
|
||||
### `type SerializationErrorCode`
|
||||
|
||||
```ts
|
||||
type SerializationErrorCode = (typeof SERIALIZE_CODES)[keyof typeof SERIALIZE_CODES];
|
||||
```
|
||||
|
||||
Union of all six qualified code strings. Use with `SuaveplanError.code` to narrow errors originating from this package.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` is idempotent for the same namespace+key pair — re-importing this module from multiple paths (or from both `error-codes.ts` and directly) does not register duplicates or throw. The frozen return value ensures the constants are not accidentally mutated at runtime.
|
||||
|
||||
The code taxonomy uses two-segment keys (`SUBSYSTEM.REASON`) rather than flat keys to make the error origin immediately scannable in logs and to allow future wildcard matching on the subsystem prefix without regex.
|
||||
@@ -0,0 +1,151 @@
|
||||
# serialize-error
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the error hierarchy for `@suaveplan/serialize`. All errors thrown by this package extend `SerializationError`, which in turn extends `SuaveplanError` from `@suaveplan/error`. This design ensures every thrown error carries a stable, machine-readable code string (`SERIALIZE.*`) so callers can programmatically distinguish between circular references, depth overflows, size overflows, and unknown type tags without parsing human-readable messages.
|
||||
|
||||
The contextual payload (`SerializationErrorContext`) transports structural metadata — tag names, recursion depths, byte sizes, and dot-path strings — that would otherwise require expensive re-traversal to reconstruct after the fact.
|
||||
|
||||
## Features
|
||||
|
||||
- `SerializationError` — base class; extends `SuaveplanError<SerializationErrorCode>` with an optional `SerializationErrorContext`
|
||||
- `CircularReferenceError` — emitted when a cycle is detected in the input object graph; includes the dot-path where the cycle was encountered
|
||||
- `MaxDepthExceededError` — emitted when recursion depth exceeds the caller-configured `maxDepth`; includes the depth value
|
||||
- `MaxSizeExceededError` — emitted when the serialized byte count exceeds `maxSize`; includes the approximate size in bytes
|
||||
- `UnsupportedTypeError` — emitted during deserialization when a `__t` tag has no registered decoder; includes the unknown tag string
|
||||
- Exported `SerializationErrorCode` union type for exhaustive `switch` narrowing
|
||||
- Re-exported `SERIALIZE_CODES` constant map for use in tests and error construction
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
CircularReferenceError,
|
||||
MaxDepthExceededError,
|
||||
SerializationError,
|
||||
} from "@suaveplan/serialize";
|
||||
|
||||
try {
|
||||
// serialization call here
|
||||
} catch (err) {
|
||||
if (err instanceof CircularReferenceError) {
|
||||
console.error("Cycle at", err.context?.path);
|
||||
} else if (err instanceof SerializationError) {
|
||||
console.error("Serialization failed:", err.code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
SERIALIZE_CODES,
|
||||
SerializationError,
|
||||
UnsupportedTypeError,
|
||||
type SerializationErrorCode,
|
||||
} from "@suaveplan/serialize";
|
||||
|
||||
function handleSerializeError(err: unknown): never {
|
||||
if (!(err instanceof SerializationError)) throw err;
|
||||
|
||||
const code: SerializationErrorCode = err.code;
|
||||
switch (code) {
|
||||
case SERIALIZE_CODES["CODEC.CIRCULAR_REFERENCE"]:
|
||||
throw new Error(`Cycle at path: ${err.context?.path ?? "unknown"}`);
|
||||
case SERIALIZE_CODES["CODEC.MAX_DEPTH"]:
|
||||
throw new Error(`Too deep: ${err.context?.depth}`);
|
||||
case SERIALIZE_CODES["CODEC.MAX_SIZE"]:
|
||||
throw new Error(`Too large: ${err.context?.size} bytes`);
|
||||
case SERIALIZE_CODES["CODEC.UNKNOWN_TAG"]:
|
||||
throw new Error(`No decoder for tag: ${err.context?.tag}`);
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// UnsupportedTypeError carries the unknown tag
|
||||
const e = new UnsupportedTypeError("__MyCustomClass");
|
||||
console.log(e.context?.tag); // "__MyCustomClass"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SerializationError`
|
||||
|
||||
```typescript
|
||||
class SerializationError extends SuaveplanError<SerializationErrorCode> {
|
||||
constructor(
|
||||
code: SerializationErrorCode,
|
||||
messageKey: string,
|
||||
options?: SerializationErrorOptions,
|
||||
): void
|
||||
}
|
||||
```
|
||||
|
||||
- `code` — one of the `SERIALIZE.*` codes
|
||||
- `messageKey` — i18n key resolved via `@suaveplan/error` resolver
|
||||
- `options.cause` — underlying error, chained via `Error.cause`
|
||||
- `options.context` — `SerializationErrorContext` metadata
|
||||
|
||||
### `CircularReferenceError`
|
||||
|
||||
```typescript
|
||||
new CircularReferenceError(path?: string, cause?: unknown)
|
||||
```
|
||||
|
||||
Code: `SERIALIZE.CODEC.CIRCULAR_REFERENCE`. `context.path` is the dot-path string where the cycle was detected.
|
||||
|
||||
### `MaxDepthExceededError`
|
||||
|
||||
```typescript
|
||||
new MaxDepthExceededError(depth: number, cause?: unknown)
|
||||
```
|
||||
|
||||
Code: `SERIALIZE.CODEC.MAX_DEPTH`. `context.depth` is the depth value at which encoding was aborted.
|
||||
|
||||
### `MaxSizeExceededError`
|
||||
|
||||
```typescript
|
||||
new MaxSizeExceededError(size: number, cause?: unknown)
|
||||
```
|
||||
|
||||
Code: `SERIALIZE.CODEC.MAX_SIZE`. `context.size` is the approximate byte count.
|
||||
|
||||
### `UnsupportedTypeError`
|
||||
|
||||
```typescript
|
||||
new UnsupportedTypeError(tag: string, cause?: unknown)
|
||||
```
|
||||
|
||||
Code: `SERIALIZE.CODEC.UNKNOWN_TAG`. `context.tag` is the unrecognised tag string.
|
||||
|
||||
### `SerializationErrorContext`
|
||||
|
||||
```typescript
|
||||
interface SerializationErrorContext {
|
||||
tag?: string;
|
||||
depth?: number;
|
||||
size?: number;
|
||||
path?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### `SerializationErrorOptions`
|
||||
|
||||
```typescript
|
||||
interface SerializationErrorOptions {
|
||||
cause?: unknown;
|
||||
context?: SerializationErrorContext;
|
||||
}
|
||||
```
|
||||
|
||||
### `SerializationErrorCode`
|
||||
|
||||
Union type of all `SERIALIZE.*` code strings; use with exhaustive `switch` statements.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `SerializationError` maps `SerializationErrorOptions.context` to the generic `Record<string, unknown>` shape expected by `SuaveplanErrorOptions` without widening the exported interface — callers always get the narrow `SerializationErrorContext` type.
|
||||
- Each subclass uses a specific constructor signature (only the meaningful fields, not the full code/key/options triple) to make throw sites readable and to prevent code/message mismatches.
|
||||
- The `SERIALIZE_CODES` constant is defined in a separate `codes.ts` module and re-exported from here so that the error file is self-contained for consumers who only import errors.
|
||||
@@ -0,0 +1,80 @@
|
||||
# legacy
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `legacySerialize` and `legacyDeserialize` as a migration bridge for code that previously used plain `JSON.stringify`/`JSON.parse` with a hand-rolled Date replacer/reviver pattern. This module intentionally supports only `Date` — not the full type registry — because its purpose is to decode existing persisted data, not to replace the full codec.
|
||||
|
||||
Both functions emit a `console.warn` on every invocation to actively surface the migration need to developers during testing and development.
|
||||
|
||||
## Features
|
||||
|
||||
- `legacySerialize` — JSON stringify with a `__legacyDate__` envelope for Date values
|
||||
- `legacyDeserialize` — JSON parse with a reviver that reconstructs `__legacyDate__` envelopes back to `Date` objects
|
||||
- Unconditional `console.warn` on every call to encourage migration
|
||||
- Zero registry dependency — intentionally frozen at Date-only support
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { legacyDeserialize, legacySerialize } from "@suaveplan/serialize";
|
||||
|
||||
// Migrating old persisted data:
|
||||
const json = legacySerialize({ created: new Date("2024-01-01") });
|
||||
// Warns: [serialize/legacy] legacySerialize is deprecated. ...
|
||||
|
||||
const restored = legacyDeserialize(json) as { created: Date };
|
||||
restored.created instanceof Date; // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { deserialize, serialize } from "@suaveplan/serialize";
|
||||
import { legacyDeserialize } from "@suaveplan/serialize";
|
||||
|
||||
// One-time migration of old stored JSON blobs:
|
||||
async function migrateRecord(oldJson: string): Promise<string> {
|
||||
// Decode with legacy reviver (handles old Date envelopes)
|
||||
const value = legacyDeserialize(oldJson);
|
||||
// Re-encode with the modern tagged codec
|
||||
return serialize(value);
|
||||
}
|
||||
|
||||
// After migration, all reads use the modern codec:
|
||||
const value = deserialize(await migrateRecord(storedJson));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `legacySerialize(value: unknown): string`
|
||||
|
||||
Serialize `value` to JSON, wrapping any `Date` instances as `{ "__legacyDate__": "<ISO string>" }`.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `value` — any JavaScript value; behaves like `JSON.stringify` for non-Date types
|
||||
|
||||
**Returns:** a JSON string with Date values encoded in the legacy envelope format.
|
||||
|
||||
**Side effects:** emits `console.warn` on every call.
|
||||
|
||||
### `legacyDeserialize(json: string): unknown`
|
||||
|
||||
Parse `json` and reconstruct `Date` objects from any `{ "__legacyDate__": "..." }` envelopes found anywhere in the value graph.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `json` — a JSON string, typically produced by `legacySerialize` or by old code using the same Date envelope convention
|
||||
|
||||
**Returns:** the reconstructed value with `Date` objects restored.
|
||||
|
||||
**Side effects:** emits `console.warn` on every call.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `__legacyDate__` envelope key was chosen to be obviously non-standard and unlikely to collide with real application data, but it is not the same as the modern `__t`/`__Date` tagged format — the two formats are intentionally incompatible so migration is explicit.
|
||||
- The `dateReplacer` is implemented as a JSON replacer function (receives key/value pairs during stringify traversal); the `dateReviver` is a JSON reviver (receives key/value pairs during parse traversal). Both are standard `JSON.stringify`/`JSON.parse` hooks.
|
||||
- No `process.env` is consulted for the dev-only warning — the warning fires in all environments. This is intentional: legacy paths should be visible in test suites, CI, and production logs equally, so that the migration is never silently deferred to later.
|
||||
@@ -0,0 +1,61 @@
|
||||
# observability/metrics
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides the OpenTelemetry instruments used by `@suaveplan/serialize` to emit metrics on every encode/decode call. Built lazily from `metrics.getMeter('suaveplan.serialize')` so the cost is paid once per process. All instruments are no-ops until a `MeterProvider` is registered globally — consumers pay zero overhead unless they wire up an SDK.
|
||||
|
||||
The module exposes a single `getSerializeMetrics()` accessor returning a cached `SerializeMetrics` object, plus a `resetSerializeMetrics()` test hook for swapping meter providers between test runs.
|
||||
|
||||
## Features
|
||||
|
||||
- Six instruments: `encodeBytes`, `decodeBytes`, `payloadBytes` (histograms), `encodeDuration`, `decodeDuration` (histograms), `errorTotal` (counter)
|
||||
- Stable instrument names (`serialize.encode.bytes`, `serialize.decode.bytes`, `serialize.payload_bytes`, `serialize.encode.duration_ms`, `serialize.decode.duration_ms`, `serialize.error.total`) pinned by spec SZ-11
|
||||
- Stable label set: `format` (`json` | `msgpack`) on every instrument; `code` additionally on `errorTotal`
|
||||
- Lazy singleton — first call constructs instruments, subsequent calls return the cached object
|
||||
- Test-only `resetSerializeMetrics()` for swapping `MeterProvider`s
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { getSerializeMetrics, serialize } from "@suaveplan/serialize";
|
||||
|
||||
// First serialize call records duration + bytes histograms
|
||||
serialize({ hello: "world" });
|
||||
|
||||
const metrics = getSerializeMetrics();
|
||||
// metrics.encodeBytes / metrics.encodeDuration are now backed by
|
||||
// the global MeterProvider (no-op by default).
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
getSerializeMetrics,
|
||||
resetSerializeMetrics,
|
||||
serialize,
|
||||
} from "@suaveplan/serialize";
|
||||
import { metrics, type Meter } from "@opentelemetry/api";
|
||||
|
||||
// Wire a custom MeterProvider + reset the singleton so the new provider is used.
|
||||
metrics.setGlobalMeterProvider(myMeterProvider);
|
||||
resetSerializeMetrics();
|
||||
|
||||
serialize({ x: 1 });
|
||||
const m = getSerializeMetrics();
|
||||
// myMeterProvider receives the encode bytes / duration / payload_bytes
|
||||
// histograms with `{ format: "json" }` labels.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `getSerializeMetrics(): SerializeMetrics` — lazy accessor; first call constructs the meter + instruments.
|
||||
- `resetSerializeMetrics(): void` — test-only; clears the cache so the next `getSerializeMetrics()` re-resolves the meter.
|
||||
- `SerializeMetrics` — typed interface exposing `encodeBytes`, `decodeBytes`, `payloadBytes`, `encodeDuration`, `decodeDuration`, `errorTotal`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The module mirrors the lazy-singleton pattern used by `@suaveplan/i18n` and `@suaveplan/logger`. Names are pinned: `serialize.encode.duration_ms` (per spec SZ-11), `serialize.encode.bytes` / `serialize.decode.bytes` (encode/decode payload sizes by format), `serialize.payload_bytes` (alias retained for backwards-compatibility with the historical spec wording), and `serialize.error.total` (incremented on every thrown error with `{ code, format }` labels). `errorTotal` is the only counter; everything else is a histogram so percentile aggregation is preserved by OTel exporters.
|
||||
@@ -0,0 +1,115 @@
|
||||
# registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Maintains a global singleton map of type definitions that the JSON and MessagePack codecs consult to encode and decode non-primitive JavaScript values in a round-trip-safe way. Each entry maps a string tag to an encoder/decoder pair and a constructor reference, enabling both forward lookup (value → tag) and reverse lookup (tag → value reconstruction).
|
||||
|
||||
The registry ships with 17 built-in entries covering the types most commonly lost by `JSON.stringify`: `Date`, `Map`, `Set`, `BigInt`, the six TypedArray variants, `URL`, `RegExp`, and three special sentinel tags for `undefined`, `NaN`, and `±Infinity`.
|
||||
|
||||
## Features
|
||||
|
||||
- `registerType(def)` — add a custom type; throws `SERIALIZE.REGISTRY.DUPLICATE_TAG` on tag collision with a different constructor; idempotent for identical re-registrations
|
||||
- `unregisterType(tag)` — remove by tag; silent if absent (useful for test cleanup)
|
||||
- `listTypes()` — enumerate all current entries as an array of `TypeDefinition`
|
||||
- `getType(tag)` — look up by tag string; returns `undefined` if absent
|
||||
- `getTypeByCtor(ctor)` — look up by constructor; returns `undefined` if absent
|
||||
- 14 built-in type definitions registered at module load time
|
||||
- Three special sentinel definitions for `undefined`, `NaN`, and `±Infinity`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { registerType, unregisterType } from "@suaveplan/serialize";
|
||||
|
||||
class Color {
|
||||
constructor(public hex: string) {}
|
||||
}
|
||||
|
||||
registerType({
|
||||
tag: "__Color",
|
||||
ctor: Color,
|
||||
encode: (v) => (v as Color).hex,
|
||||
decode: (enc) => new Color(enc as string),
|
||||
});
|
||||
|
||||
// After registration, serialize/deserialize will handle Color instances.
|
||||
// Clean up in tests:
|
||||
unregisterType("__Color");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
getType,
|
||||
getTypeByCtor,
|
||||
listTypes,
|
||||
registerType,
|
||||
} from "@suaveplan/serialize";
|
||||
|
||||
class Vector2 {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
registerType({
|
||||
tag: "__Vector2",
|
||||
ctor: Vector2,
|
||||
encode: (v) => { const vec = v as Vector2; return [vec.x, vec.y]; },
|
||||
decode: (enc) => { const [x, y] = enc as [number, number]; return new Vector2(x, y); },
|
||||
});
|
||||
|
||||
// Look up by tag
|
||||
const def = getType("__Vector2");
|
||||
console.log(def?.tag); // "__Vector2"
|
||||
|
||||
// Look up by ctor
|
||||
const def2 = getTypeByCtor(Vector2);
|
||||
console.log(def2?.tag); // "__Vector2"
|
||||
|
||||
// Enumerate all registered types
|
||||
const all = listTypes();
|
||||
console.log(all.map((d) => d.tag));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerType(def: TypeDefinition): void`
|
||||
|
||||
Registers a custom type. Throws `SerializationError` with code `SERIALIZE.REGISTRY.DUPLICATE_TAG` if the same `tag` string is already registered for a _different_ constructor. Registering the same tag+ctor pair again is a silent no-op (idempotent).
|
||||
|
||||
### `unregisterType(tag: string): void`
|
||||
|
||||
Removes the registration for `tag` and its associated constructor from both internal maps. Silent if the tag was never registered.
|
||||
|
||||
### `listTypes(): TypeDefinition[]`
|
||||
|
||||
Returns a snapshot array of all registered `TypeDefinition` objects at the time of the call.
|
||||
|
||||
### `getType(tag: string): TypeDefinition | undefined`
|
||||
|
||||
Returns the `TypeDefinition` for the given tag, or `undefined` if no entry exists.
|
||||
|
||||
### `getTypeByCtor(ctor: Function): TypeDefinition | undefined`
|
||||
|
||||
Returns the `TypeDefinition` whose `ctor` reference matches, or `undefined` if none is registered for that constructor.
|
||||
|
||||
### `TypeDefinition`
|
||||
|
||||
```typescript
|
||||
interface TypeDefinition {
|
||||
tag: string;
|
||||
ctor: Function;
|
||||
encode: (value: unknown) => unknown;
|
||||
decode: (encoded: unknown) => unknown;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Two parallel `Map` instances (`_byTag` and `_byCtor`) are kept in sync to give O(1) lookup in both directions without requiring full list iteration.
|
||||
- Built-in registrations use an internal `registerBuiltin` helper that force-sets both maps, making them immune to the duplicate-tag guard that external callers hit. This allows the module to be loaded multiple times in test environments without throwing.
|
||||
- The sentinel entries for `undefined`, `NaN`, and `±Infinity` use placeholder constructors (`Object`, `Number`, `Boolean`) that will never match a real value via `getTypeByCtor`; they are only reached via the `getType(tag)` path during deserialization.
|
||||
- TypedArray `Float32Array` and `Float64Array` encode to plain number arrays; precision loss for `Float32Array` is intentional and matches the original in-memory representation.
|
||||
Reference in New Issue
Block a user