@zudojs/serialization 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/serializerJson/index.d.ts +1 -1
- package/dist/serializerJson/index.js +1 -1
- package/dist/serializerJson/jsonSerializer.core.d.ts +21 -5
- package/dist/serializerJson/jsonSerializer.core.js +12 -15
- package/dist/serializerJson/jsonSerializer.restore.d.ts +2 -2
- package/dist/serializerJson/jsonSerializer.transform.d.ts +2 -2
- package/dist/serializerJson/jsonSerializer.transform.js +7 -6
- package/dist/serializerRegistry/serializerRegistry.factory.d.ts +6 -1
- package/dist/serializerRegistry/serializerRegistry.factory.js +6 -2
- package/dist/serializerTransformPolicy/index.d.ts +10 -0
- package/dist/serializerTransformPolicy/index.js +10 -0
- package/dist/serializerTransformPolicy/transformerLookup.core.d.ts +31 -0
- package/dist/serializerTransformPolicy/transformerLookup.core.js +37 -0
- package/dist/serializerTransformPolicy/transformerOutput.helper.d.ts +27 -0
- package/dist/serializerTransformPolicy/transformerOutput.helper.js +78 -0
- package/dist/serializerTransformsExt/error.transformer.js +31 -5
- package/dist/serializerTypes/serializer.type.d.ts +22 -3
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -70,13 +70,40 @@ const registry = new TransformerRegistry();
|
|
|
70
70
|
registry.register({
|
|
71
71
|
type: "Money",
|
|
72
72
|
canSerialize: (v): v is Money => v instanceof Money,
|
|
73
|
-
serialize: (v) => ({ $type: "Money", $value:
|
|
73
|
+
serialize: (v) => (v as Money).toString(), // wrapped as { $type: "Money", $value: "..." }
|
|
74
74
|
deserialize: (v) => Money.parse((v as { $value: string }).$value),
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
const serializer = new JSONSerializer({ transformers: registry });
|
|
78
|
+
// Money uses your transformer; Date, BigInt, Map, Set, Buffer and Error still
|
|
79
|
+
// use the built-ins.
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
Your registry is consulted first, so registering a tag the built-ins use (say
|
|
83
|
+
`"Date"`) replaces that built-in. Transformers you add to the registry later are
|
|
84
|
+
picked up.
|
|
85
|
+
|
|
86
|
+
**The transformer contract.** On the wire a transformed value is always
|
|
87
|
+
`{ "$type": <type>, "$value": ... }`.
|
|
88
|
+
|
|
89
|
+
- `serialize` returns either just the value to store, which the serializer
|
|
90
|
+
wraps as `{ $type: type, $value: <returned> }`, or that full tagged object
|
|
91
|
+
itself (as the built-ins do). A plain object with its own string `$type` is
|
|
92
|
+
taken as the full tagged object; anything else is wrapped. Special values
|
|
93
|
+
nested inside what you return (a `Date`, a `BigInt`) are transformed too.
|
|
94
|
+
- `deserialize` always receives the full tagged object, with nested values
|
|
95
|
+
already restored, whichever form `serialize` returned. Read `$value` from it.
|
|
96
|
+
|
|
97
|
+
**Opting out of the built-ins.** Pass `builtins: false` (to `JSONSerializer`
|
|
98
|
+
or `createSerializer`) to use only your registry. A value that no transformer
|
|
99
|
+
handles and that JSON would write as `{}` (a `Map`, `Set`, `WeakMap`, `Error`,
|
|
100
|
+
`RegExp`, `ArrayBuffer`, ...) then throws `SerializeError` under
|
|
101
|
+
`preserveTypes` instead of being silently emptied. A type no built-in covers
|
|
102
|
+
(`RegExp`, `WeakMap`, `ArrayBuffer`, `DataView`, ...) throws the same way with
|
|
103
|
+
the built-ins on. The message names the fix: register a transformer, or, when
|
|
104
|
+
a built-in handles the type and you turned them off, keep them enabled. To
|
|
105
|
+
compose a registry by hand, start from `createBuiltinTransformers()`.
|
|
106
|
+
|
|
80
107
|
## Untrusted input
|
|
81
108
|
|
|
82
109
|
`deserialize` is the boundary that matters — the string arrives from a queue, an
|
|
@@ -126,8 +153,19 @@ serializer.serialize({ error }, { preserveTypes: true }); // name, message, code
|
|
|
126
153
|
serializer.serialize({ error }, { preserveTypes: true, includeStack: true });
|
|
127
154
|
```
|
|
128
155
|
|
|
129
|
-
On the way back
|
|
130
|
-
|
|
156
|
+
On the way back:
|
|
157
|
+
|
|
158
|
+
- A built-in subclass (`TypeError`, `RangeError`, `SyntaxError`,
|
|
159
|
+
`ReferenceError`, `EvalError`, `URIError`, `AggregateError`) is rebuilt with
|
|
160
|
+
its own constructor, so `instanceof TypeError` still holds. Any other name
|
|
161
|
+
is rebuilt as a plain `Error` with `name` set. An `AggregateError` comes back
|
|
162
|
+
with an empty `errors` array, because the inner errors are not serialized.
|
|
163
|
+
- `.stack` is only the header line (`"TypeError: bad input"`) with no frames.
|
|
164
|
+
The reader's own frames would point at the deserializer, not at where the
|
|
165
|
+
error was thrown.
|
|
166
|
+
- A wire-supplied stack (sent with `includeStack`) is attached as a
|
|
167
|
+
non-enumerable `originalStack`. It never overwrites `.stack`, because it is
|
|
168
|
+
text the sender controls.
|
|
131
169
|
|
|
132
170
|
## Envelopes
|
|
133
171
|
|
package/dist/index.d.ts
CHANGED
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
*/
|
|
27
27
|
export type { SerializationFormat, SerializedValue, SerializeOptions, DeserializeOptions, SerializationMetadata, SerializedEnvelope, Serializer, TypeTransformer, } from "./serializerTypes/index.js";
|
|
28
28
|
export { JSONSerializer, ESCAPED_OBJECT_TAG } from "./serializerJson/index.js";
|
|
29
|
+
export type { JSONSerializerOptions } from "./serializerJson/index.js";
|
|
29
30
|
export { TransformerRegistry } from "./serializerTransforms/index.js";
|
|
30
31
|
export { DateTransformer } from "./serializerTransforms/index.js";
|
|
31
32
|
export { BigIntTransformer } from "./serializerTransforms/index.js";
|
|
32
33
|
export { MapTransformer } from "./serializerTransforms/index.js";
|
|
33
34
|
export { SetTransformer } from "./serializerTransforms/index.js";
|
|
35
|
+
export { createBuiltinTransformers } from "./serializerTransformPolicy/index.js";
|
|
34
36
|
export { BufferTransformer } from "./serializerTransformsExt/index.js";
|
|
35
37
|
export { ErrorTransformer } from "./serializerTransformsExt/index.js";
|
|
36
38
|
export { toBase64, fromBase64, encodeUtf8, decodeUtf8, } from "./serializerTransformsExt/index.js";
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ export { DateTransformer } from "./serializerTransforms/index.js";
|
|
|
32
32
|
export { BigIntTransformer } from "./serializerTransforms/index.js";
|
|
33
33
|
export { MapTransformer } from "./serializerTransforms/index.js";
|
|
34
34
|
export { SetTransformer } from "./serializerTransforms/index.js";
|
|
35
|
+
export { createBuiltinTransformers } from "./serializerTransformPolicy/index.js";
|
|
35
36
|
// ─── Extended Transformers ────────────────────────────────────
|
|
36
37
|
export { BufferTransformer } from "./serializerTransformsExt/index.js";
|
|
37
38
|
export { ErrorTransformer } from "./serializerTransformsExt/index.js";
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* JSON serializer with fast path (native JSON) and advanced path
|
|
5
5
|
* with type preservation via transformer registry.
|
|
6
6
|
*/
|
|
7
|
-
export { JSONSerializer } from "./jsonSerializer.core.js";
|
|
7
|
+
export { JSONSerializer, type JSONSerializerOptions, } from "./jsonSerializer.core.js";
|
|
8
8
|
export { ESCAPED_OBJECT_TAG } from "./jsonSerializer.escape.js";
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* JSON serializer with fast path (native JSON) and advanced path
|
|
5
5
|
* with type preservation via transformer registry.
|
|
6
6
|
*/
|
|
7
|
-
export { JSONSerializer } from "./jsonSerializer.core.js";
|
|
7
|
+
export { JSONSerializer, } from "./jsonSerializer.core.js";
|
|
8
8
|
export { ESCAPED_OBJECT_TAG } from "./jsonSerializer.escape.js";
|
|
9
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Serializer, SerializeOptions, DeserializeOptions } from "../serializerTypes/index.js";
|
|
9
9
|
import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
10
|
+
/** Options accepted by the {@link JSONSerializer} constructor. */
|
|
11
|
+
export interface JSONSerializerOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Your transformers. They are consulted first; the built-in transformers
|
|
14
|
+
* (`Date`, `BigInt`, `Map`, `Set`, `Uint8Array`/`Buffer`, `Error`) still
|
|
15
|
+
* handle every type your registry does not, unless `builtins` is `false`.
|
|
16
|
+
* Transformers registered on it later are picked up.
|
|
17
|
+
*/
|
|
18
|
+
readonly transformers?: TransformerRegistry;
|
|
19
|
+
/**
|
|
20
|
+
* Include the built-in transformers behind `transformers` (default
|
|
21
|
+
* `true`). With `false` only your registry is used, and a value no
|
|
22
|
+
* transformer handles that JSON would write as `{}` (a `Map`, `Set`,
|
|
23
|
+
* `Error`, ...) throws `SerializeError` under `preserveTypes`.
|
|
24
|
+
*/
|
|
25
|
+
readonly builtins?: boolean;
|
|
26
|
+
/** Default options merged into every call. */
|
|
27
|
+
readonly defaults?: SerializeOptions & DeserializeOptions;
|
|
28
|
+
}
|
|
10
29
|
/**
|
|
11
30
|
* JSON serializer. With `preserveTypes`, plain objects that carry their own
|
|
12
31
|
* string `$type` key are escaped as `{"$type":"Object","$value":{...}}` on
|
|
@@ -16,13 +35,10 @@ import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
|
16
35
|
export declare class JSONSerializer implements Serializer<unknown, string> {
|
|
17
36
|
readonly name = "json";
|
|
18
37
|
readonly contentType = "application/json";
|
|
38
|
+
private readonly registry;
|
|
19
39
|
private readonly transformers;
|
|
20
40
|
private readonly defaults;
|
|
21
|
-
constructor(options?:
|
|
22
|
-
readonly transformers?: TransformerRegistry;
|
|
23
|
-
/** Default options merged into every call. */
|
|
24
|
-
readonly defaults?: SerializeOptions & DeserializeOptions;
|
|
25
|
-
});
|
|
41
|
+
constructor(options?: JSONSerializerOptions);
|
|
26
42
|
/**
|
|
27
43
|
* Register a custom type transformer.
|
|
28
44
|
*
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Advanced path (preserveTypes: true): recursive traversal with
|
|
6
6
|
* transformer-based type preservation.
|
|
7
7
|
*/
|
|
8
|
-
import { TransformerRegistry
|
|
9
|
-
import {
|
|
8
|
+
import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
9
|
+
import { createBuiltinTransformers, layerTransformers, } from "../serializerTransformPolicy/index.js";
|
|
10
10
|
import { SerializationLimits } from "@zudojs/constants";
|
|
11
11
|
import { InvalidSerializedDataError, TransformerError } from "@zudojs/errors";
|
|
12
12
|
import { assertNoCircularReference, assertDepthWithinLimit, } from "@zudojs/validation";
|
|
@@ -14,17 +14,6 @@ import { ESCAPED_OBJECT_TAG } from "./jsonSerializer.escape.js";
|
|
|
14
14
|
import { assertByteSize } from "./jsonSerializer.keys.js";
|
|
15
15
|
import { transformValue } from "./jsonSerializer.transform.js";
|
|
16
16
|
import { restoreValue } from "./jsonSerializer.restore.js";
|
|
17
|
-
/** Default transformer registry with all built-in transformers. */
|
|
18
|
-
function createDefaultTransformers() {
|
|
19
|
-
const registry = new TransformerRegistry();
|
|
20
|
-
registry.register(DateTransformer);
|
|
21
|
-
registry.register(BigIntTransformer);
|
|
22
|
-
registry.register(MapTransformer);
|
|
23
|
-
registry.register(SetTransformer);
|
|
24
|
-
registry.register(BufferTransformer);
|
|
25
|
-
registry.register(ErrorTransformer);
|
|
26
|
-
return registry;
|
|
27
|
-
}
|
|
28
17
|
/**
|
|
29
18
|
* JSON serializer. With `preserveTypes`, plain objects that carry their own
|
|
30
19
|
* string `$type` key are escaped as `{"$type":"Object","$value":{...}}` on
|
|
@@ -34,10 +23,18 @@ function createDefaultTransformers() {
|
|
|
34
23
|
export class JSONSerializer {
|
|
35
24
|
name = "json";
|
|
36
25
|
contentType = "application/json";
|
|
26
|
+
registry;
|
|
37
27
|
transformers;
|
|
38
28
|
defaults;
|
|
39
29
|
constructor(options) {
|
|
40
|
-
|
|
30
|
+
const builtins = options?.builtins !== false;
|
|
31
|
+
const custom = options?.transformers;
|
|
32
|
+
this.registry =
|
|
33
|
+
custom ?? (builtins ? createBuiltinTransformers() : new TransformerRegistry());
|
|
34
|
+
this.transformers =
|
|
35
|
+
custom && builtins
|
|
36
|
+
? layerTransformers(custom, createBuiltinTransformers())
|
|
37
|
+
: this.registry;
|
|
41
38
|
this.defaults = options?.defaults ?? {};
|
|
42
39
|
}
|
|
43
40
|
/**
|
|
@@ -49,7 +46,7 @@ export class JSONSerializer {
|
|
|
49
46
|
if (transformer.type === ESCAPED_OBJECT_TAG) {
|
|
50
47
|
throw new TransformerError(transformer.type, `"${ESCAPED_OBJECT_TAG}" is reserved for escaped plain objects.`);
|
|
51
48
|
}
|
|
52
|
-
this.
|
|
49
|
+
this.registry.register(transformer);
|
|
53
50
|
}
|
|
54
51
|
serialize(value, options) {
|
|
55
52
|
const opts = { ...this.defaults, ...options };
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* plain objects (see `jsonSerializer.escape.ts`).
|
|
6
6
|
*/
|
|
7
7
|
import type { DeserializeOptions } from "../serializerTypes/index.js";
|
|
8
|
-
import type {
|
|
8
|
+
import type { TransformerLookup } from "../serializerTransformPolicy/index.js";
|
|
9
9
|
/** What the walker needs besides the value. */
|
|
10
10
|
export interface RestoreWalk {
|
|
11
|
-
readonly transformers:
|
|
11
|
+
readonly transformers: TransformerLookup;
|
|
12
12
|
readonly maxDepth: number;
|
|
13
13
|
readonly options: DeserializeOptions;
|
|
14
14
|
}
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* and escapes plain objects that would otherwise read back as a tag.
|
|
6
6
|
*/
|
|
7
7
|
import type { SerializeOptions } from "../serializerTypes/index.js";
|
|
8
|
-
import type
|
|
8
|
+
import { type TransformerLookup } from "../serializerTransformPolicy/index.js";
|
|
9
9
|
/** What the walker needs besides the value. */
|
|
10
10
|
export interface TransformWalk {
|
|
11
|
-
readonly transformers:
|
|
11
|
+
readonly transformers: TransformerLookup;
|
|
12
12
|
readonly maxDepth: number;
|
|
13
13
|
readonly options: SerializeOptions;
|
|
14
14
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { SerializationDepthError } from "@zudojs/errors";
|
|
8
8
|
import { isPlainObject } from "@zudojs/types";
|
|
9
|
+
import { assertNotLossy, toTaggedOutput, } from "../serializerTransformPolicy/index.js";
|
|
9
10
|
import { escapeObject, needsEscape } from "./jsonSerializer.escape.js";
|
|
10
11
|
import { defineKey } from "./jsonSerializer.keys.js";
|
|
11
12
|
/** Converts a value into its JSON-safe, tagged representation. */
|
|
@@ -15,7 +16,7 @@ export function transformValue(walk, value, depth) {
|
|
|
15
16
|
if (typeof value === "bigint") {
|
|
16
17
|
const transformer = walk.transformers.findForValue(value);
|
|
17
18
|
return transformer
|
|
18
|
-
? transformer.serialize(value, walk.options)
|
|
19
|
+
? transformEntries(walk, toTaggedOutput(transformer, transformer.serialize(value, walk.options)), depth + 1)
|
|
19
20
|
: value.toString();
|
|
20
21
|
}
|
|
21
22
|
if (typeof value !== "object")
|
|
@@ -35,17 +36,17 @@ export function transformValue(walk, value, depth) {
|
|
|
35
36
|
}
|
|
36
37
|
const transformer = walk.transformers.findForValue(value);
|
|
37
38
|
if (transformer) {
|
|
38
|
-
const raw = transformer.serialize(value, walk.options);
|
|
39
39
|
// The transformer's own tag object is not user data: its children are
|
|
40
|
-
// walked, but it is never escaped.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// walked, but it is never escaped. A bare return value is wrapped in
|
|
41
|
+
// `{ $type, $value }` for the transformer author.
|
|
42
|
+
const raw = transformer.serialize(value, walk.options);
|
|
43
|
+
return transformEntries(walk, toTaggedOutput(transformer, raw), depth + 1);
|
|
44
44
|
}
|
|
45
45
|
if (isPlainObject(value)) {
|
|
46
46
|
const body = transformEntries(walk, value, depth);
|
|
47
47
|
return needsEscape(value) ? escapeObject(body) : body;
|
|
48
48
|
}
|
|
49
|
+
assertNotLossy(value);
|
|
49
50
|
return value;
|
|
50
51
|
}
|
|
51
52
|
/** Transforms each own enumerable key of a plain object. */
|
|
@@ -17,8 +17,13 @@ import { SerializerRegistry } from "./serializerRegistry.core.js";
|
|
|
17
17
|
* default that each call can still override.
|
|
18
18
|
*/
|
|
19
19
|
export interface CreateSerializerOptions extends SerializeOptions, DeserializeOptions {
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Your transformers, consulted before the built-in ones (which still
|
|
22
|
+
* handle every other type unless `builtins` is `false`).
|
|
23
|
+
*/
|
|
21
24
|
readonly transformers?: TransformerRegistry;
|
|
25
|
+
/** Include the built-in transformers (default `true`). */
|
|
26
|
+
readonly builtins?: boolean;
|
|
22
27
|
/** Pretty-print by default. Overridable per call. */
|
|
23
28
|
readonly pretty?: boolean;
|
|
24
29
|
/** Preserve special JS types by default. Overridable per call. */
|
|
@@ -15,9 +15,13 @@ export function createSerializer(format, options) {
|
|
|
15
15
|
case Format.JSON: {
|
|
16
16
|
// Only `pretty` and `preserveTypes` used to be forwarded, so instance
|
|
17
17
|
// limits such as `maxDepth`/`maxSize`/`strict` were silently dropped.
|
|
18
|
-
const { transformers, ...rest } = options ?? {};
|
|
18
|
+
const { transformers, builtins, ...rest } = options ?? {};
|
|
19
19
|
const defaults = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== undefined));
|
|
20
|
-
return new JSONSerializer({
|
|
20
|
+
return new JSONSerializer({
|
|
21
|
+
...(transformers !== undefined ? { transformers } : {}),
|
|
22
|
+
...(builtins !== undefined ? { builtins } : {}),
|
|
23
|
+
defaults,
|
|
24
|
+
});
|
|
21
25
|
}
|
|
22
26
|
default:
|
|
23
27
|
throw new UnsupportedSerializationFormatError(format);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer policy.
|
|
3
|
+
*
|
|
4
|
+
* How the serializer resolves transformers (a caller registry layered over
|
|
5
|
+
* the built-ins), normalises what a transformer's `serialize` returns, and
|
|
6
|
+
* refuses values it would otherwise silently write as `{}`.
|
|
7
|
+
*/
|
|
8
|
+
export { createBuiltinTransformers, layerTransformers, type TransformerLookup, } from "./transformerLookup.core.js";
|
|
9
|
+
export { assertNotLossy, toTaggedOutput } from "./transformerOutput.helper.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer policy.
|
|
3
|
+
*
|
|
4
|
+
* How the serializer resolves transformers (a caller registry layered over
|
|
5
|
+
* the built-ins), normalises what a transformer's `serialize` returns, and
|
|
6
|
+
* refuses values it would otherwise silently write as `{}`.
|
|
7
|
+
*/
|
|
8
|
+
export { createBuiltinTransformers, layerTransformers, } from "./transformerLookup.core.js";
|
|
9
|
+
export { assertNotLossy, toTaggedOutput } from "./transformerOutput.helper.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer lookup.
|
|
3
|
+
*
|
|
4
|
+
* The serializer resolves transformers through a lookup rather than a single
|
|
5
|
+
* registry, so a caller-supplied registry can sit in front of the built-ins
|
|
6
|
+
* instead of replacing them.
|
|
7
|
+
*/
|
|
8
|
+
import type { TypeTransformer } from "../serializerTypes/index.js";
|
|
9
|
+
import { TransformerRegistry } from "../serializerTransforms/index.js";
|
|
10
|
+
/** The read side of a transformer registry, as the walkers use it. */
|
|
11
|
+
export interface TransformerLookup {
|
|
12
|
+
/** Find a transformer that can serialize the given value. */
|
|
13
|
+
findForValue(value: unknown): TypeTransformer | undefined;
|
|
14
|
+
/** Returns true when a transformer is registered for the type tag. */
|
|
15
|
+
has(type: string): boolean;
|
|
16
|
+
/** Retrieve a transformer by type tag (throws when absent). */
|
|
17
|
+
get(type: string): TypeTransformer;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a registry holding every built-in transformer: `Date`, `BigInt`,
|
|
21
|
+
* `Map`, `Set`, `Uint8Array`/`Buffer` and `Error`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createBuiltinTransformers(): TransformerRegistry;
|
|
24
|
+
/**
|
|
25
|
+
* Layers `primary` over `fallback`: every lookup consults `primary` first,
|
|
26
|
+
* so a caller's transformer for a tag or a value wins over the built-in
|
|
27
|
+
* one. `primary` is read live, so transformers registered on it later are
|
|
28
|
+
* seen too.
|
|
29
|
+
*/
|
|
30
|
+
export declare function layerTransformers(primary: TransformerLookup, fallback: TransformerLookup): TransformerLookup;
|
|
31
|
+
//# sourceMappingURL=transformerLookup.core.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer lookup.
|
|
3
|
+
*
|
|
4
|
+
* The serializer resolves transformers through a lookup rather than a single
|
|
5
|
+
* registry, so a caller-supplied registry can sit in front of the built-ins
|
|
6
|
+
* instead of replacing them.
|
|
7
|
+
*/
|
|
8
|
+
import { BigIntTransformer, DateTransformer, MapTransformer, SetTransformer, TransformerRegistry, } from "../serializerTransforms/index.js";
|
|
9
|
+
import { BufferTransformer, ErrorTransformer, } from "../serializerTransformsExt/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Creates a registry holding every built-in transformer: `Date`, `BigInt`,
|
|
12
|
+
* `Map`, `Set`, `Uint8Array`/`Buffer` and `Error`.
|
|
13
|
+
*/
|
|
14
|
+
export function createBuiltinTransformers() {
|
|
15
|
+
const registry = new TransformerRegistry();
|
|
16
|
+
registry.register(DateTransformer);
|
|
17
|
+
registry.register(BigIntTransformer);
|
|
18
|
+
registry.register(MapTransformer);
|
|
19
|
+
registry.register(SetTransformer);
|
|
20
|
+
registry.register(BufferTransformer);
|
|
21
|
+
registry.register(ErrorTransformer);
|
|
22
|
+
return registry;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Layers `primary` over `fallback`: every lookup consults `primary` first,
|
|
26
|
+
* so a caller's transformer for a tag or a value wins over the built-in
|
|
27
|
+
* one. `primary` is read live, so transformers registered on it later are
|
|
28
|
+
* seen too.
|
|
29
|
+
*/
|
|
30
|
+
export function layerTransformers(primary, fallback) {
|
|
31
|
+
return {
|
|
32
|
+
findForValue: (value) => primary.findForValue(value) ?? fallback.findForValue(value),
|
|
33
|
+
has: (type) => primary.has(type) || fallback.has(type),
|
|
34
|
+
get: (type) => (primary.has(type) ? primary.get(type) : fallback.get(type)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=transformerLookup.core.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer output and lossy-value checks.
|
|
3
|
+
*/
|
|
4
|
+
import type { TypeTransformer } from "../serializerTypes/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Normalises what a transformer's `serialize` returned into the tagged
|
|
7
|
+
* `{ $type, $value }` form that is written to the wire.
|
|
8
|
+
*
|
|
9
|
+
* A transformer may return the full tagged object (the built-ins do; any
|
|
10
|
+
* plain object with its own string `$type` is taken as-is), or just the
|
|
11
|
+
* value to store, which is wrapped as `{ $type: transformer.type, $value }`.
|
|
12
|
+
* Before, a bare return was written untagged and could never be revived.
|
|
13
|
+
*/
|
|
14
|
+
export declare function toTaggedOutput(transformer: TypeTransformer, raw: unknown): Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Throws when a value that no transformer handles would be written as `{}`
|
|
17
|
+
* (a `Map`, `Set`, `Error`, `RegExp`, ...), instead of silently losing its
|
|
18
|
+
* contents. Only reachable when the built-in transformers are disabled or
|
|
19
|
+
* a type has no transformer.
|
|
20
|
+
*
|
|
21
|
+
* Suggests re-enabling the built-ins only when one of them handles the
|
|
22
|
+
* type; otherwise the fix is registering a transformer.
|
|
23
|
+
*
|
|
24
|
+
* @throws {SerializeError} naming the type and how to fix it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function assertNotLossy(value: object): void;
|
|
27
|
+
//# sourceMappingURL=transformerOutput.helper.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/serialization — Transformer output and lossy-value checks.
|
|
3
|
+
*/
|
|
4
|
+
import { SerializationTags } from "@zudojs/constants";
|
|
5
|
+
import { SerializeError } from "@zudojs/errors";
|
|
6
|
+
import { isPlainObject } from "@zudojs/types";
|
|
7
|
+
import { createBuiltinTransformers, } from "./transformerLookup.core.js";
|
|
8
|
+
/**
|
|
9
|
+
* Normalises what a transformer's `serialize` returned into the tagged
|
|
10
|
+
* `{ $type, $value }` form that is written to the wire.
|
|
11
|
+
*
|
|
12
|
+
* A transformer may return the full tagged object (the built-ins do; any
|
|
13
|
+
* plain object with its own string `$type` is taken as-is), or just the
|
|
14
|
+
* value to store, which is wrapped as `{ $type: transformer.type, $value }`.
|
|
15
|
+
* Before, a bare return was written untagged and could never be revived.
|
|
16
|
+
*/
|
|
17
|
+
export function toTaggedOutput(transformer, raw) {
|
|
18
|
+
if (isPlainObject(raw) &&
|
|
19
|
+
Object.hasOwn(raw, SerializationTags.TYPE) &&
|
|
20
|
+
typeof raw[SerializationTags.TYPE] === "string") {
|
|
21
|
+
return raw;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
[SerializationTags.TYPE]: transformer.type,
|
|
25
|
+
[SerializationTags.VALUE]: raw,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Built-in objects whose JSON form silently drops their contents. */
|
|
29
|
+
const LOSSY_TYPES = [
|
|
30
|
+
["Map", (value) => value instanceof Map],
|
|
31
|
+
["Set", (value) => value instanceof Set],
|
|
32
|
+
["WeakMap", (value) => value instanceof WeakMap],
|
|
33
|
+
["WeakSet", (value) => value instanceof WeakSet],
|
|
34
|
+
["WeakRef", (value) => value instanceof WeakRef],
|
|
35
|
+
["Promise", (value) => value instanceof Promise],
|
|
36
|
+
["RegExp", (value) => value instanceof RegExp],
|
|
37
|
+
["Error", (value) => value instanceof Error],
|
|
38
|
+
["ArrayBuffer", (value) => value instanceof ArrayBuffer],
|
|
39
|
+
["DataView", (value) => value instanceof DataView],
|
|
40
|
+
];
|
|
41
|
+
let builtins;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a built-in transformer handles `value`. When one does and the
|
|
44
|
+
* value still reached {@link assertNotLossy}, the built-ins were disabled.
|
|
45
|
+
*/
|
|
46
|
+
function hasBuiltinTransformer(value) {
|
|
47
|
+
builtins ??= createBuiltinTransformers();
|
|
48
|
+
return builtins.findForValue(value) !== undefined;
|
|
49
|
+
}
|
|
50
|
+
/** `"a Map"`, `"an Error"`. */
|
|
51
|
+
function withArticle(name) {
|
|
52
|
+
return `${/^[AEIOU]/.test(name) ? "an" : "a"} ${name}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Throws when a value that no transformer handles would be written as `{}`
|
|
56
|
+
* (a `Map`, `Set`, `Error`, `RegExp`, ...), instead of silently losing its
|
|
57
|
+
* contents. Only reachable when the built-in transformers are disabled or
|
|
58
|
+
* a type has no transformer.
|
|
59
|
+
*
|
|
60
|
+
* Suggests re-enabling the built-ins only when one of them handles the
|
|
61
|
+
* type; otherwise the fix is registering a transformer.
|
|
62
|
+
*
|
|
63
|
+
* @throws {SerializeError} naming the type and how to fix it.
|
|
64
|
+
*/
|
|
65
|
+
export function assertNotLossy(value) {
|
|
66
|
+
for (const [name, matches] of LOSSY_TYPES) {
|
|
67
|
+
if (matches(value)) {
|
|
68
|
+
const fix = hasBuiltinTransformer(value)
|
|
69
|
+
? `Register a transformer for ${name}, or keep the built-in ` +
|
|
70
|
+
`transformers enabled (do not pass builtins: false).`
|
|
71
|
+
: `Register a transformer for ${name}.`;
|
|
72
|
+
throw new SerializeError(`Cannot serialize ${withArticle(name)} with preserveTypes: no ` +
|
|
73
|
+
`transformer is registered for it, and JSON would write it as {} ` +
|
|
74
|
+
`and lose its contents. ${fix}`, { format: "json" });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=transformerOutput.helper.js.map
|
|
@@ -9,6 +9,33 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { SerializationTags } from "@zudojs/constants";
|
|
11
11
|
const ERROR_TYPE = "Error";
|
|
12
|
+
/**
|
|
13
|
+
* Built-in Error subclasses rebuilt with their own constructor, so a
|
|
14
|
+
* deserialized `TypeError` is still `instanceof TypeError`. Any other name
|
|
15
|
+
* falls back to `Error` with `name` set.
|
|
16
|
+
*/
|
|
17
|
+
const BUILT_IN_ERRORS = Object.freeze({
|
|
18
|
+
TypeError: (message) => new TypeError(message),
|
|
19
|
+
RangeError: (message) => new RangeError(message),
|
|
20
|
+
SyntaxError: (message) => new SyntaxError(message),
|
|
21
|
+
ReferenceError: (message) => new ReferenceError(message),
|
|
22
|
+
EvalError: (message) => new EvalError(message),
|
|
23
|
+
URIError: (message) => new URIError(message),
|
|
24
|
+
AggregateError: (message) => new AggregateError([], message),
|
|
25
|
+
});
|
|
26
|
+
/** Creates the error for a serialized name, preferring a built-in subclass. */
|
|
27
|
+
function createError(name, message) {
|
|
28
|
+
const build = typeof name === "string" && Object.hasOwn(BUILT_IN_ERRORS, name)
|
|
29
|
+
? BUILT_IN_ERRORS[name]
|
|
30
|
+
: undefined;
|
|
31
|
+
if (build !== undefined)
|
|
32
|
+
return build(message);
|
|
33
|
+
const error = new Error(message);
|
|
34
|
+
if (typeof name === "string" && name !== "Error") {
|
|
35
|
+
error.name = name;
|
|
36
|
+
}
|
|
37
|
+
return error;
|
|
38
|
+
}
|
|
12
39
|
/** Transformer that handles Error round-trips. */
|
|
13
40
|
export const ErrorTransformer = {
|
|
14
41
|
type: ERROR_TYPE,
|
|
@@ -33,11 +60,10 @@ export const ErrorTransformer = {
|
|
|
33
60
|
deserialize(value) {
|
|
34
61
|
const data = value;
|
|
35
62
|
const message = typeof data.message === "string" ? data.message : "";
|
|
36
|
-
const error =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
63
|
+
const error = createError(data.name, message);
|
|
64
|
+
// The reader's own frames point at this deserializer, not at where the
|
|
65
|
+
// error was thrown, so the rebuilt stack is the header line only.
|
|
66
|
+
error.stack = message === "" ? error.name : `${error.name}: ${message}`;
|
|
41
67
|
// A stack from the wire is attacker-controlled text. Restoring it over the
|
|
42
68
|
// real one would make the reconstructed error lie about where it came
|
|
43
69
|
// from, so it is carried as a separate, clearly-named field instead.
|
|
@@ -86,15 +86,34 @@ export interface Serializer<TValue = unknown, TSerialized = SerializedValue> {
|
|
|
86
86
|
/** Deserialize a value from the target format. */
|
|
87
87
|
deserialize<T = TValue>(value: TSerialized, options?: DeserializeOptions): T;
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Transforms a specific JS type during serialization.
|
|
91
|
+
*
|
|
92
|
+
* On the wire a transformed value is always the tagged object
|
|
93
|
+
* `{ "$type": type, "$value": ... }`.
|
|
94
|
+
*/
|
|
90
95
|
export interface TypeTransformer<TValue = unknown> {
|
|
91
96
|
/** Tag name used in tagged representations (e.g., "Date"). */
|
|
92
97
|
readonly type: string;
|
|
93
98
|
/** Returns true when this transformer handles the given value. */
|
|
94
99
|
canSerialize(value: unknown): value is TValue;
|
|
95
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Convert the value into a JSON-safe representation.
|
|
102
|
+
*
|
|
103
|
+
* Return either just the value to store (for example
|
|
104
|
+
* `v.toString()`), which the serializer wraps as
|
|
105
|
+
* `{ $type: type, $value: <returned> }`, or the full tagged object
|
|
106
|
+
* `{ $type: type, $value: ... }` yourself (as the built-ins do). A plain
|
|
107
|
+
* object with its own string `$type` is taken to be the full tagged
|
|
108
|
+
* object; anything else is wrapped. Nested special values inside the
|
|
109
|
+
* returned value are transformed too.
|
|
110
|
+
*/
|
|
96
111
|
serialize(value: TValue, options?: SerializeOptions): unknown;
|
|
97
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Reconstruct the original value. Always receives the full tagged object
|
|
114
|
+
* `{ $type, $value }` (with nested values already restored), whichever
|
|
115
|
+
* form `serialize` returned, so read `$value` from it.
|
|
116
|
+
*/
|
|
98
117
|
deserialize(value: unknown, options?: DeserializeOptions): TValue;
|
|
99
118
|
}
|
|
100
119
|
//# sourceMappingURL=serializer.type.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/serialization",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Data translation layer with JSON serializer, type transformers, envelopes, and registry for Zudojs applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zudojs/constants": "1.1.
|
|
23
|
-
"@zudojs/errors": "1.
|
|
24
|
-
"@zudojs/types": "1.
|
|
25
|
-
"@zudojs/validation": "1.0
|
|
22
|
+
"@zudojs/constants": "1.1.2",
|
|
23
|
+
"@zudojs/errors": "1.3.0",
|
|
24
|
+
"@zudojs/types": "1.2.0",
|
|
25
|
+
"@zudojs/validation": "1.1.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"typescript": "7.0.2",
|
|
29
|
-
"vitest": "^
|
|
29
|
+
"vitest": "^5.0.1"
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"author": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"node": ">=24.0.0"
|
|
47
47
|
},
|
|
48
48
|
"module": "./dist/index.js",
|
|
49
|
-
"homepage": "https://
|
|
49
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-serialization",
|
|
50
50
|
"bugs": {
|
|
51
51
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
52
52
|
},
|