Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user