Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -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.