seriall 1.0.1 → 1.1.0
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 +261 -10
- package/dist/index.cjs +149 -98
- package/dist/index.d.cts +21 -8
- package/dist/index.d.mts +21 -8
- package/dist/index.mjs +149 -98
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# seriall
|
|
2
2
|
|
|
3
3
|
## TL;DR
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**seriall is a data-only serialization protocol for JavaScript object graphs, preserving reference identity, circular structures, built-in types, and registered custom classes.**
|
|
6
6
|
|
|
7
|
-
The only
|
|
7
|
+
**The only JavaScript values not supported out-of-the-box are:**
|
|
8
8
|
|
|
9
|
-
- ⚠️ Custom classes instances with **
|
|
9
|
+
- ⚠️ Custom classes instances with **JavaScript private fields** (`#field`),
|
|
10
10
|
|
|
11
11
|
there are still configurable workarounds, just no out-of-the-box solution
|
|
12
12
|
|
|
@@ -16,7 +16,7 @@ The only **unsuported datatypes** out-of-the-box are:
|
|
|
16
16
|
|
|
17
17
|
even though even this is theoretically configurable
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
seriall **does not use `eval` or dynamically execute serialized JavaScript code**. Serialized functions and classes are not supported by default, which helps keep deserialization data-only.
|
|
20
20
|
|
|
21
21
|
It **conserves refenrential integrity**, so circularly referenced arrays/objects, and cross referenced arrays/objects can be serialized
|
|
22
22
|
|
|
@@ -24,6 +24,27 @@ It **can out-of-the-box register custom classes**, and recreates their instances
|
|
|
24
24
|
|
|
25
25
|
It is **highly configurable**, through the usage of `transformers`
|
|
26
26
|
|
|
27
|
+
## Table of Contents
|
|
28
|
+
|
|
29
|
+
- [TL;DR](#tldr)
|
|
30
|
+
- [Installation](#installation)
|
|
31
|
+
- [Usage](#usage)
|
|
32
|
+
- [Basic usage](#basic-usage)
|
|
33
|
+
- [Registering custom classes](#registering-custom-classes)
|
|
34
|
+
- [Supported](#supported)
|
|
35
|
+
- [Advanced Usages](#advanced-usages)
|
|
36
|
+
- [Symbols](#symbols)
|
|
37
|
+
- [Transformers](#transformers)
|
|
38
|
+
- [Custom Class Serialization](#custom-class-serialization)
|
|
39
|
+
- [Benchmark](#benchmark)
|
|
40
|
+
- [Import Notes](#import-notes)
|
|
41
|
+
- [JavaScript](#javascript)
|
|
42
|
+
- [Typescript](#typescript)
|
|
43
|
+
- [Protocol Versioning](#protocol-versioning)
|
|
44
|
+
- [Motive](#motive)
|
|
45
|
+
- [Solution](#solution)
|
|
46
|
+
- [Stable top-level structure](#stable-top-level-structure)
|
|
47
|
+
|
|
27
48
|
## Installation
|
|
28
49
|
|
|
29
50
|
Inside an npm project: `npm install seriall` or `yarn install seriall`
|
|
@@ -162,9 +183,206 @@ and boxed primitives
|
|
|
162
183
|
|
|
163
184
|
- `String`, `Number`, `Boolean`
|
|
164
185
|
|
|
186
|
+
## Advanced Usages
|
|
187
|
+
|
|
188
|
+
### Symbols
|
|
189
|
+
|
|
190
|
+
By default, seriall ignores symbol keys in objects, for performance reasons.
|
|
191
|
+
You can however enable this feature in the Serializer's options
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { Serializer } from "seriall";
|
|
195
|
+
|
|
196
|
+
const serializer = new Serializer({ enable: { objectSymbolIndexing: true } });
|
|
197
|
+
// ^^^^
|
|
198
|
+
// false by default
|
|
199
|
+
|
|
200
|
+
const secretKey = Symbol("secret");
|
|
201
|
+
|
|
202
|
+
const data = {
|
|
203
|
+
[secretKey]: "Hello from a symbol key!",
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const serialized = serializer.serialize(data);
|
|
207
|
+
const deserialized = serializer.deserialize(serialized);
|
|
208
|
+
|
|
209
|
+
const restoredKey = Object.getOwnPropertySymbols(deserialized)[0];
|
|
210
|
+
|
|
211
|
+
console.log(deserialized[restoredKey]);
|
|
212
|
+
// "Hello from a symbol key!"
|
|
213
|
+
|
|
214
|
+
console.log(restoredKey.description);
|
|
215
|
+
// "secret"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Symbol identity is also preserved across references:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const key = Symbol("key");
|
|
222
|
+
|
|
223
|
+
const data = {
|
|
224
|
+
[key]: "value",
|
|
225
|
+
key,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const restored = serializer.deserialize(serializer.serialize(data));
|
|
229
|
+
|
|
230
|
+
const restoredKey = Object.getOwnPropertySymbols(restored)[0];
|
|
231
|
+
|
|
232
|
+
console.log(restored[restoredKey]);
|
|
233
|
+
// "value"
|
|
234
|
+
|
|
235
|
+
console.log(restored.key === restoredKey);
|
|
236
|
+
// true
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
This works because seriall preserves the object graph, rather than simply converting values to JSON.
|
|
240
|
+
|
|
241
|
+
### Transformers
|
|
242
|
+
|
|
243
|
+
seriall's serialization logic is built around **transformers**.
|
|
244
|
+
|
|
245
|
+
A transformer tells seriall how to:
|
|
246
|
+
|
|
247
|
+
- identify a specific type of value
|
|
248
|
+
- encode it into serializable data
|
|
249
|
+
- decode that data back into the original type
|
|
250
|
+
|
|
251
|
+
This makes seriall highly configurable and allows it to support types that are not supported out-of-the-box.
|
|
252
|
+
|
|
253
|
+
A transformer can be registered using `registerTransformer()`:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { Serializer, Transformer } from "seriall";
|
|
257
|
+
|
|
258
|
+
const serializer = new Serializer();
|
|
259
|
+
|
|
260
|
+
const transformer = new Transformer({
|
|
261
|
+
id: "url",
|
|
262
|
+
priority: Transformer.PRIORITY.CUSTOM_CLASS,
|
|
263
|
+
match: (value) => value instanceof URL,
|
|
264
|
+
encode: (value) => value.toString(),
|
|
265
|
+
decode: (value) => new URL(value),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
serializer.registerTransformer(transformer);
|
|
269
|
+
|
|
270
|
+
const original = {
|
|
271
|
+
website: new URL("https://example.com"),
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const serialized = serializer.serialize(original);
|
|
275
|
+
const restored = serializer.deserialize(serialized);
|
|
276
|
+
|
|
277
|
+
console.log(restored.website instanceof URL);
|
|
278
|
+
// true
|
|
279
|
+
|
|
280
|
+
console.log(restored.website.href);
|
|
281
|
+
// "https://example.com/"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Custom Class Serialization
|
|
285
|
+
|
|
286
|
+
The `registerClass()` mechanism introduced earlier is actually built on top of seriall's transformer system.
|
|
287
|
+
|
|
288
|
+
In other words, **a registered class is ultimately just a transformer**.
|
|
289
|
+
|
|
290
|
+
This means that the class serialization mechanism can be reproduced and customized using `Transformer` directly when more control is needed.
|
|
291
|
+
|
|
292
|
+
By default, `SerializableClass` provides the necessary encoding and decoding behavior.
|
|
293
|
+
|
|
294
|
+
Under the hood, it looks something like:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import { SYMBOLS } from "seriall";
|
|
298
|
+
|
|
299
|
+
export abstract class SerializableClass {
|
|
300
|
+
[ENCODE]() {...};
|
|
301
|
+
static [DECODE] = function (this, registerNode) {...};
|
|
302
|
+
};
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
So you can actually overwrite a class encoding/decoding methods like this:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { SerializableClass, SYMBOLS } from "seriall";
|
|
309
|
+
|
|
310
|
+
class MyClass extends SerializableClass {
|
|
311
|
+
[SYMBOLS.ENCODE]() {
|
|
312
|
+
console.log("Calling a custom encoding function");
|
|
313
|
+
return super[SYMBOLS.ENCODE]();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Benchmark
|
|
319
|
+
|
|
320
|
+
seriall is designed for **general-purpose JavaScript object graphs**, including circular references, shared references, built-in types, and recursive custom class instances.
|
|
321
|
+
|
|
322
|
+
Benchmarks were run with **5,000 iterations**, **500 warmup iterations**, and **7 rounds** with randomized benchmark order. Results below use the median of the rounds.
|
|
323
|
+
|
|
324
|
+
### Common JavaScript graph
|
|
325
|
+
|
|
326
|
+
This benchmark contains circular references and many shared object references.
|
|
327
|
+
|
|
328
|
+
| Operation | seriall | V8 | devalue | flatted | superjson |
|
|
329
|
+
| --------------- | ---------------: | -----: | ------: | ------: | --------: |
|
|
330
|
+
| Serialization | **16,887 ops/s** | 34,903 | 9,674 | 7,086 | 2,727 |
|
|
331
|
+
| Deserialization | **16,077 ops/s** | 18,274 | 16,459 | 3,298 | 6,200 |
|
|
332
|
+
| Round-trip | **8,279 ops/s** | 11,727 | 5,974 | 2,218 | 1,875 |
|
|
333
|
+
|
|
334
|
+
seriall 's serialized size was **6,057 bytes**, compared with 6,029 bytes for devalue, 6,445 bytes for flatted, 15,233 bytes for superjson, and 4,458 bytes for V8.
|
|
335
|
+
|
|
336
|
+
For this graph, seriall 's complete round-trip was approximately **1.4× faster than devalue** and **3.7× faster than flatted**.
|
|
337
|
+
|
|
338
|
+
### Rich JavaScript graph
|
|
339
|
+
|
|
340
|
+
The rich graph includes:
|
|
341
|
+
|
|
342
|
+
- `Date`, `Map`, `Set`, and `BigInt`
|
|
343
|
+
- shared references
|
|
344
|
+
- circular references
|
|
345
|
+
- recursive custom `User` instances
|
|
346
|
+
- users referencing themselves and each other
|
|
347
|
+
- custom classes nested at multiple levels
|
|
348
|
+
|
|
349
|
+
| Operation | seriall | devalue |
|
|
350
|
+
| --------------- | --------------: | ------: |
|
|
351
|
+
| Serialization | **2,589 ops/s** | 2,502 |
|
|
352
|
+
| Deserialization | **3,505 ops/s** | 4,157 |
|
|
353
|
+
| Round-trip | **1,467 ops/s** | 1,519 |
|
|
354
|
+
|
|
355
|
+
seriall serialized this graph to **16,517 bytes**.
|
|
356
|
+
|
|
357
|
+
The most important difference in this benchmark is correctness: seriall preserves recursive custom-class identity, including self-references and cross-references between class instances.
|
|
358
|
+
|
|
359
|
+
```text
|
|
360
|
+
seriall:
|
|
361
|
+
Self referencing user: PASS
|
|
362
|
+
Cross referencing users: PASS
|
|
363
|
+
|
|
364
|
+
devalue:
|
|
365
|
+
Self referencing user: FAIL
|
|
366
|
+
Cross referencing users: FAIL
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Plots
|
|
370
|
+
|
|
371
|
+

|
|
372
|
+
|
|
373
|
+

|
|
374
|
+
|
|
375
|
+
### Conclusion
|
|
376
|
+
|
|
377
|
+
In these benchmarks, seriall was the fastest of the tested non-Node.js-only serializers on the common graph, while remaining competitive with devalue on the richer graph.
|
|
378
|
+
|
|
379
|
+
More importantly, Seriall combines this performance with full referential integrity for recursive custom class instances, configurable transformers, built-in type support, and a data-only serialization format.
|
|
380
|
+
|
|
381
|
+
That makes Seriall particularly well suited for transferring complex JavaScript object graphs, such as those encountered in network-layer protocols.
|
|
382
|
+
|
|
165
383
|
## Import Notes
|
|
166
384
|
|
|
167
|
-
|
|
385
|
+
seriall is exported in both cjs and mjs.
|
|
168
386
|
|
|
169
387
|
It therefore supports both CommonJS import (`require`) and ES Module import (`import`)
|
|
170
388
|
|
|
@@ -172,14 +390,14 @@ It also exposes both `.d.cts` and `.d.mts` declaration files, and provides there
|
|
|
172
390
|
|
|
173
391
|
### JavaScript
|
|
174
392
|
|
|
175
|
-
**
|
|
393
|
+
**ES Module**
|
|
176
394
|
|
|
177
395
|
```ts
|
|
178
396
|
// demo.mjs
|
|
179
397
|
import { Serializer } from "seriall";
|
|
180
398
|
```
|
|
181
399
|
|
|
182
|
-
**
|
|
400
|
+
**CommonJS**
|
|
183
401
|
|
|
184
402
|
```ts
|
|
185
403
|
// demo.cjs
|
|
@@ -188,7 +406,7 @@ const { Serializer } = require("seriall");
|
|
|
188
406
|
|
|
189
407
|
### Typescript
|
|
190
408
|
|
|
191
|
-
**
|
|
409
|
+
**ES Module**
|
|
192
410
|
|
|
193
411
|
```ts
|
|
194
412
|
// demo.mts
|
|
@@ -196,7 +414,7 @@ import { Serializer } from "seriall";
|
|
|
196
414
|
// Serializer is properly typed
|
|
197
415
|
```
|
|
198
416
|
|
|
199
|
-
**
|
|
417
|
+
**CommonJS**
|
|
200
418
|
|
|
201
419
|
either
|
|
202
420
|
|
|
@@ -214,3 +432,36 @@ or
|
|
|
214
432
|
const { Serializer } = require("seriall") as typeof import("seriall");
|
|
215
433
|
// Serializer is now typed properly
|
|
216
434
|
```
|
|
435
|
+
|
|
436
|
+
## Protocol Versioning
|
|
437
|
+
|
|
438
|
+
### Motive
|
|
439
|
+
|
|
440
|
+
In the advent of a **breaking protocol change**, serializers running on different platforms (e.g client vs server) could
|
|
441
|
+
go temporarily out of sync regarding their serialization protocol.
|
|
442
|
+
|
|
443
|
+
### Solution
|
|
444
|
+
|
|
445
|
+
To handle this case and prevent incompatible data from being deserialized incorrectly, **seriall checks the protocol version before deserializing data** .
|
|
446
|
+
|
|
447
|
+
⚠️ A protocol mismatch will result in a error being thrown
|
|
448
|
+
|
|
449
|
+
The protocol is however **not expected to change**, and especially not frequently.
|
|
450
|
+
|
|
451
|
+
### Stable top level structure:
|
|
452
|
+
|
|
453
|
+
every serialized data consist of a json string, structured like this:
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
{
|
|
457
|
+
"lib": "seriall", // stable
|
|
458
|
+
"v": number, // protocol version
|
|
459
|
+
"d": any // serialized data
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
- `lib` identifies the serialization format and is expected to remain stable.
|
|
464
|
+
|
|
465
|
+
- `v` identifies the protocol version and may change if a breaking protocol change is introduced.
|
|
466
|
+
|
|
467
|
+
- `d` contains the serialized object graph and may change format between protocol versions.
|
package/dist/index.cjs
CHANGED
|
@@ -27,9 +27,11 @@ __export(index_exports, {
|
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
28
|
|
|
29
29
|
// src/const/const.ts
|
|
30
|
-
var
|
|
31
|
-
var
|
|
30
|
+
var SIGNATURE_INDEX = 0;
|
|
31
|
+
var DATA_INDEX = 1;
|
|
32
32
|
var NO_TRANSFORM_DATA = void 0;
|
|
33
|
+
var PROTOCOL_VERSION = 1;
|
|
34
|
+
var LIB = "seriall";
|
|
33
35
|
var PRIORITY = {
|
|
34
36
|
CUSTOM_CLASS: 0,
|
|
35
37
|
NATIVE_CLASS: 1,
|
|
@@ -42,13 +44,13 @@ var isSpecialNumber = (value) => {
|
|
|
42
44
|
return [Infinity, -Infinity, NaN].includes(value) || Object.is(value, -0);
|
|
43
45
|
};
|
|
44
46
|
var encodeSpecialNumber = (value) => {
|
|
45
|
-
if (Number.isNaN(value)) return 0;
|
|
46
|
-
if (value === Infinity) return 1;
|
|
47
|
-
if (value === -Infinity) return 2;
|
|
48
|
-
if (Object.is(value, -0)) return 3;
|
|
47
|
+
if (Number.isNaN(value)) return [0];
|
|
48
|
+
if (value === Infinity) return [1];
|
|
49
|
+
if (value === -Infinity) return [2];
|
|
50
|
+
if (Object.is(value, -0)) return [3];
|
|
49
51
|
throw new Error(value + " is not a special number");
|
|
50
52
|
};
|
|
51
|
-
var decodeSpecialNumber = (value) => {
|
|
53
|
+
var decodeSpecialNumber = ([value]) => {
|
|
52
54
|
switch (value) {
|
|
53
55
|
case 0:
|
|
54
56
|
return NaN;
|
|
@@ -68,7 +70,7 @@ var isJsonPrimitive = (value) => {
|
|
|
68
70
|
if (typeof value === "number") {
|
|
69
71
|
return !isSpecialNumber(value);
|
|
70
72
|
}
|
|
71
|
-
return typeof value === "string" || typeof value === "boolean";
|
|
73
|
+
return typeof value === "string" || typeof value === "boolean" || value === null;
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
// src/utils/object.utils.ts
|
|
@@ -96,27 +98,30 @@ var serialize = (data, transformers) => {
|
|
|
96
98
|
const graph = [];
|
|
97
99
|
const seen = /* @__PURE__ */ new Map();
|
|
98
100
|
const addNodeToGraph = (node) => {
|
|
99
|
-
if (isJsonPrimitive(node)) {
|
|
100
|
-
const id2 = graph.length;
|
|
101
|
-
graph.push(node);
|
|
102
|
-
return id2;
|
|
103
|
-
}
|
|
104
101
|
let id = seen.get(node);
|
|
105
|
-
if (id !== void 0) {
|
|
102
|
+
if (id !== void 0 && !Object.is(node, -0)) {
|
|
106
103
|
return id;
|
|
107
104
|
}
|
|
108
105
|
id = graph.length;
|
|
109
106
|
seen.set(node, id);
|
|
107
|
+
if (isJsonPrimitive(node)) {
|
|
108
|
+
const id2 = graph.length;
|
|
109
|
+
graph.push(node);
|
|
110
|
+
return id2;
|
|
111
|
+
}
|
|
110
112
|
const transformer = transformers.find(
|
|
111
113
|
(transformer2) => transformer2.match(node)
|
|
112
114
|
);
|
|
113
115
|
if (transformer) {
|
|
114
|
-
graph.push(
|
|
116
|
+
graph.push([]);
|
|
115
117
|
const revivable = graph[id];
|
|
118
|
+
revivable[SIGNATURE_INDEX] = transformer.id;
|
|
116
119
|
const encoded = transformer.encode(node);
|
|
117
120
|
if (encoded !== NO_TRANSFORM_DATA) {
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
encoded.forEach((el, index) => {
|
|
122
|
+
const dataId = addNodeToGraph(el);
|
|
123
|
+
revivable[DATA_INDEX + index] = dataId;
|
|
124
|
+
});
|
|
120
125
|
}
|
|
121
126
|
return id;
|
|
122
127
|
}
|
|
@@ -129,21 +134,49 @@ var serialize = (data, transformers) => {
|
|
|
129
134
|
}
|
|
130
135
|
return id;
|
|
131
136
|
}
|
|
137
|
+
if (typeof node === "object") {
|
|
138
|
+
graph.push({});
|
|
139
|
+
const obj = graph[id];
|
|
140
|
+
for (const key in node) {
|
|
141
|
+
const elementId = addNodeToGraph(node[key]);
|
|
142
|
+
obj[key] = elementId;
|
|
143
|
+
}
|
|
144
|
+
return id;
|
|
145
|
+
}
|
|
132
146
|
throw new Error(
|
|
133
147
|
`No transformer found for the current value "${node?.toString()}", please add one`
|
|
134
148
|
);
|
|
135
149
|
};
|
|
136
150
|
addNodeToGraph(data);
|
|
137
|
-
|
|
151
|
+
const serialized = {
|
|
152
|
+
lib: LIB,
|
|
153
|
+
v: PROTOCOL_VERSION,
|
|
154
|
+
d: graph
|
|
155
|
+
};
|
|
156
|
+
return JSON.stringify(serialized);
|
|
138
157
|
};
|
|
139
158
|
|
|
140
159
|
// src/serialization/deserialize.ts
|
|
141
160
|
var deserialize = (data, transformers) => {
|
|
142
|
-
const
|
|
143
|
-
|
|
161
|
+
const { lib, v, d } = JSON.parse(data);
|
|
162
|
+
if (lib !== LIB) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Can't deserialize data, invalid lib metadata. Expected "${LIB}", got "${lib}"`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (v !== PROTOCOL_VERSION) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Can't deserialize data, invalid protocol version. Expected ${PROTOCOL_VERSION}, got ${v}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (!Array.isArray(d)) {
|
|
173
|
+
throw new Error("Invalid seriall payload: 'd' must be an array");
|
|
174
|
+
}
|
|
175
|
+
const graph = d;
|
|
176
|
+
const revived = [];
|
|
144
177
|
const reviveNode = (index) => {
|
|
145
|
-
if (revived
|
|
146
|
-
return revived
|
|
178
|
+
if (index in revived) {
|
|
179
|
+
return revived[index];
|
|
147
180
|
}
|
|
148
181
|
if (graph.length <= index || index < 0) {
|
|
149
182
|
throw new Error(`Invalid serialized graph reference: ${index}`);
|
|
@@ -152,37 +185,54 @@ var deserialize = (data, transformers) => {
|
|
|
152
185
|
if (isJsonPrimitive(node)) {
|
|
153
186
|
return node;
|
|
154
187
|
}
|
|
155
|
-
if (Array.isArray(node)) {
|
|
188
|
+
if (!Array.isArray(node) && typeof node === "object") {
|
|
189
|
+
const obj = {};
|
|
190
|
+
revived[index] = obj;
|
|
191
|
+
for (const key of Object.keys(node)) {
|
|
192
|
+
obj[key] = reviveNode(node[key]);
|
|
193
|
+
}
|
|
194
|
+
return obj;
|
|
195
|
+
}
|
|
196
|
+
if (typeof node[SIGNATURE_INDEX] !== "string") {
|
|
197
|
+
const typedNode2 = node;
|
|
156
198
|
const arr = [];
|
|
157
|
-
revived
|
|
158
|
-
for (const element of
|
|
199
|
+
revived[index] = arr;
|
|
200
|
+
for (const element of typedNode2) {
|
|
159
201
|
arr.push(reviveNode(element));
|
|
160
202
|
}
|
|
161
203
|
return arr;
|
|
162
204
|
}
|
|
163
|
-
const
|
|
164
|
-
const
|
|
205
|
+
const typedNode = node;
|
|
206
|
+
const transformerId = typedNode[SIGNATURE_INDEX];
|
|
207
|
+
const transformer = transformers[transformerId];
|
|
165
208
|
if (!transformerId) {
|
|
166
|
-
throw new Error(`No transformer found for node "${
|
|
209
|
+
throw new Error(`No transformer found for node "${typedNode}"`);
|
|
167
210
|
}
|
|
168
211
|
if (transformer === void 0) {
|
|
169
212
|
throw new Error(`No transformer found with id "${transformerId}"`);
|
|
170
213
|
}
|
|
171
214
|
const nonRecursiveTransformer = transformer;
|
|
172
|
-
|
|
173
|
-
if (dataId === void 0) {
|
|
215
|
+
if (!transformer.recursive && typedNode[DATA_INDEX] === void 0) {
|
|
174
216
|
const decoded = nonRecursiveTransformer.decode(NO_TRANSFORM_DATA);
|
|
175
|
-
revived
|
|
217
|
+
revived[index] = decoded;
|
|
176
218
|
return decoded;
|
|
177
219
|
}
|
|
178
220
|
if (!transformer.recursive) {
|
|
179
|
-
const
|
|
180
|
-
|
|
221
|
+
const revivedValues = new Array(typedNode.length - 1);
|
|
222
|
+
for (let i = 1; i < typedNode.length; i++) {
|
|
223
|
+
revivedValues[i - 1] = reviveNode(typedNode[i]);
|
|
224
|
+
}
|
|
225
|
+
const decoded = nonRecursiveTransformer.decode(revivedValues);
|
|
226
|
+
revived[index] = decoded;
|
|
181
227
|
return decoded;
|
|
182
228
|
}
|
|
183
229
|
const registerNode = (node2) => {
|
|
184
|
-
revived
|
|
185
|
-
|
|
230
|
+
revived[index] = node2;
|
|
231
|
+
const revivedValues = new Array(typedNode.length - 1);
|
|
232
|
+
for (let i = 1; i < typedNode.length; i++) {
|
|
233
|
+
revivedValues[i - 1] = reviveNode(typedNode[i]);
|
|
234
|
+
}
|
|
235
|
+
return revivedValues;
|
|
186
236
|
};
|
|
187
237
|
return transformer.decode(registerNode);
|
|
188
238
|
};
|
|
@@ -221,15 +271,15 @@ var primitivesTransformers = [
|
|
|
221
271
|
id: "sym",
|
|
222
272
|
priority: Transformer.PRIORITY.PRIMITIVE,
|
|
223
273
|
match: (node) => typeof node === "symbol",
|
|
224
|
-
encode: (node) =>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
274
|
+
encode: (node) => [
|
|
275
|
+
node.description,
|
|
276
|
+
Symbol.keyFor(node) === void 0 ? 0 : 1
|
|
277
|
+
],
|
|
228
278
|
decode: (encoded) => {
|
|
229
|
-
if (encoded
|
|
279
|
+
if (encoded[1] === 1) {
|
|
230
280
|
return Symbol.for(encoded.d);
|
|
231
281
|
}
|
|
232
|
-
return Symbol(encoded
|
|
282
|
+
return Symbol(encoded[0]);
|
|
233
283
|
}
|
|
234
284
|
}),
|
|
235
285
|
new Transformer({
|
|
@@ -246,52 +296,45 @@ var primitivesTransformers = [
|
|
|
246
296
|
encode: () => NO_TRANSFORM_DATA,
|
|
247
297
|
decode: () => void 0
|
|
248
298
|
}),
|
|
249
|
-
new Transformer({
|
|
250
|
-
id: "nul",
|
|
251
|
-
priority: Transformer.PRIORITY.PRIMITIVE,
|
|
252
|
-
match: (node) => node === null,
|
|
253
|
-
encode: () => NO_TRANSFORM_DATA,
|
|
254
|
-
decode: () => null
|
|
255
|
-
}),
|
|
256
299
|
new Transformer({
|
|
257
300
|
id: "big",
|
|
258
301
|
priority: Transformer.PRIORITY.PRIMITIVE,
|
|
259
302
|
match: (node) => typeof node === "bigint",
|
|
260
|
-
encode: (node) => node.toString(),
|
|
261
|
-
decode: (node) => BigInt(node)
|
|
262
|
-
}),
|
|
263
|
-
new Transformer({
|
|
264
|
-
id: "obj",
|
|
265
|
-
priority: Transformer.PRIORITY.PRIMITIVE,
|
|
266
|
-
recursive: true,
|
|
267
|
-
match: (node) => !Array.isArray(node) && typeof node === "object",
|
|
268
|
-
encode: (node) => {
|
|
269
|
-
const obj = [];
|
|
270
|
-
for (const key of Reflect.ownKeys(node)) {
|
|
271
|
-
const value = node[key];
|
|
272
|
-
if (typeof value !== "function") {
|
|
273
|
-
obj.push([key, node[key]]);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return obj;
|
|
277
|
-
},
|
|
278
|
-
decode: (registerNode) => {
|
|
279
|
-
const obj = {};
|
|
280
|
-
const encoded = registerNode(obj);
|
|
281
|
-
for (const [key, value] of encoded) {
|
|
282
|
-
obj[key] = value;
|
|
283
|
-
}
|
|
284
|
-
return obj;
|
|
285
|
-
}
|
|
303
|
+
encode: (node) => [node.toString()],
|
|
304
|
+
decode: ([node]) => BigInt(node)
|
|
286
305
|
})
|
|
287
306
|
];
|
|
307
|
+
var objectSymbolIndexingTransformer = new Transformer({
|
|
308
|
+
id: "obj",
|
|
309
|
+
priority: Transformer.PRIORITY.PRIMITIVE,
|
|
310
|
+
recursive: true,
|
|
311
|
+
match: (node) => !Array.isArray(node) && typeof node === "object",
|
|
312
|
+
encode: (node) => {
|
|
313
|
+
const obj = [];
|
|
314
|
+
for (const key of Reflect.ownKeys(node)) {
|
|
315
|
+
const value = node[key];
|
|
316
|
+
if (typeof value !== "function") {
|
|
317
|
+
obj.push([key, node[key]]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return obj;
|
|
321
|
+
},
|
|
322
|
+
decode: (registerNode) => {
|
|
323
|
+
const obj = {};
|
|
324
|
+
const encoded = registerNode(obj);
|
|
325
|
+
for (const [key, value] of encoded) {
|
|
326
|
+
obj[key] = value;
|
|
327
|
+
}
|
|
328
|
+
return obj;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
288
331
|
var nativeClassesTransformers = [
|
|
289
332
|
new Transformer({
|
|
290
333
|
id: "Dte",
|
|
291
334
|
priority: Transformer.PRIORITY.NATIVE_CLASS,
|
|
292
335
|
match: (node) => node instanceof Date,
|
|
293
|
-
encode: (node) => node.getTime(),
|
|
294
|
-
decode: (node) => new Date(node)
|
|
336
|
+
encode: (node) => [node.getTime()],
|
|
337
|
+
decode: ([node]) => new Date(node)
|
|
295
338
|
}),
|
|
296
339
|
new Transformer({
|
|
297
340
|
id: "Set",
|
|
@@ -327,29 +370,29 @@ var nativeClassesTransformers = [
|
|
|
327
370
|
id: "Rgx",
|
|
328
371
|
priority: Transformer.PRIORITY.NATIVE_CLASS,
|
|
329
372
|
match: (node) => node instanceof RegExp,
|
|
330
|
-
encode: (node) =>
|
|
331
|
-
decode: (node) => new RegExp(node
|
|
373
|
+
encode: (node) => [node.source, node.flags],
|
|
374
|
+
decode: (node) => new RegExp(node[0], node[1])
|
|
332
375
|
}),
|
|
333
376
|
new Transformer({
|
|
334
377
|
id: "Str",
|
|
335
378
|
priority: Transformer.PRIORITY.NATIVE_CLASS,
|
|
336
379
|
match: (node) => node instanceof String,
|
|
337
|
-
encode: (node) => node.valueOf(),
|
|
338
|
-
decode: (node) => new String(node)
|
|
380
|
+
encode: (node) => [node.valueOf()],
|
|
381
|
+
decode: ([node]) => new String(node)
|
|
339
382
|
}),
|
|
340
383
|
new Transformer({
|
|
341
384
|
id: "Num",
|
|
342
385
|
priority: Transformer.PRIORITY.NATIVE_CLASS,
|
|
343
386
|
match: (node) => node instanceof Number,
|
|
344
|
-
encode: (node) => node.valueOf(),
|
|
345
|
-
decode: (node) => new Number(node)
|
|
387
|
+
encode: (node) => [node.valueOf()],
|
|
388
|
+
decode: ([node]) => new Number(node)
|
|
346
389
|
}),
|
|
347
390
|
new Transformer({
|
|
348
391
|
id: "Bol",
|
|
349
392
|
priority: Transformer.PRIORITY.NATIVE_CLASS,
|
|
350
393
|
match: (node) => node instanceof Boolean,
|
|
351
|
-
encode: (node) => node.valueOf(),
|
|
352
|
-
decode: (node) => new Boolean(node)
|
|
394
|
+
encode: (node) => [node.valueOf()],
|
|
395
|
+
decode: ([node]) => new Boolean(node)
|
|
353
396
|
})
|
|
354
397
|
];
|
|
355
398
|
|
|
@@ -360,20 +403,21 @@ var SYMBOLS = { ENCODE, DECODE };
|
|
|
360
403
|
var SerializableClass = class {
|
|
361
404
|
constructor(..._) {
|
|
362
405
|
}
|
|
363
|
-
[ENCODE]
|
|
364
|
-
const encoded =
|
|
365
|
-
for (const key of
|
|
406
|
+
[ENCODE]() {
|
|
407
|
+
const encoded = [];
|
|
408
|
+
for (const key of Object.keys(this)) {
|
|
366
409
|
const value = this[key];
|
|
367
410
|
if (typeof value !== "function") {
|
|
368
|
-
encoded
|
|
411
|
+
encoded.push(this[key]);
|
|
369
412
|
}
|
|
370
413
|
}
|
|
371
414
|
return encoded;
|
|
372
|
-
}
|
|
415
|
+
}
|
|
373
416
|
static [DECODE] = function(registerNode) {
|
|
374
417
|
let revivedInstance = {};
|
|
375
418
|
const args = registerNode(revivedInstance);
|
|
376
|
-
|
|
419
|
+
const keys = Reflect.ownKeys(new this({}));
|
|
420
|
+
keys.forEach((key, index) => revivedInstance[key] = args[index]);
|
|
377
421
|
Object.setPrototypeOf(revivedInstance, this.prototype);
|
|
378
422
|
return revivedInstance;
|
|
379
423
|
};
|
|
@@ -392,23 +436,30 @@ var defaultOptions = {
|
|
|
392
436
|
enable: {
|
|
393
437
|
builtinPrimitiveTransformers: true,
|
|
394
438
|
builtinNativeClasses: true,
|
|
395
|
-
|
|
396
|
-
}
|
|
439
|
+
objectSymbolIndexing: false
|
|
440
|
+
},
|
|
441
|
+
classes: {}
|
|
397
442
|
};
|
|
398
443
|
var Serializer = class {
|
|
399
444
|
transformers = [];
|
|
400
|
-
|
|
445
|
+
transformersRecord = {};
|
|
401
446
|
constructor(options = {}) {
|
|
402
447
|
const opt = deepMerge(defaultOptions, options);
|
|
403
448
|
if (opt.enable.builtinPrimitiveTransformers)
|
|
404
449
|
primitivesTransformers.forEach(this.registerTransformer);
|
|
405
450
|
if (opt.enable.builtinNativeClasses)
|
|
406
451
|
nativeClassesTransformers.forEach(this.registerTransformer);
|
|
452
|
+
if (opt.enable.objectSymbolIndexing) {
|
|
453
|
+
this.registerTransformer(objectSymbolIndexingTransformer);
|
|
454
|
+
}
|
|
455
|
+
for (const className in opt.classes) {
|
|
456
|
+
this.registerClass(className, opt.classes[className]);
|
|
457
|
+
}
|
|
407
458
|
}
|
|
408
459
|
serialize = (data) => serialize(data, this.transformers);
|
|
409
|
-
deserialize = (data) => deserialize(data, this.
|
|
460
|
+
deserialize = (data) => deserialize(data, this.transformersRecord);
|
|
410
461
|
registerTransformer = (transformer) => {
|
|
411
|
-
if (
|
|
462
|
+
if (transformer.id in this.transformersRecord) {
|
|
412
463
|
throw new Error(
|
|
413
464
|
`A transformer with id "${transformer.id}" is already registered, chose another one`
|
|
414
465
|
);
|
|
@@ -417,11 +468,11 @@ var Serializer = class {
|
|
|
417
468
|
this.transformers = this.transformers.sort(
|
|
418
469
|
(a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity)
|
|
419
470
|
);
|
|
420
|
-
this.
|
|
471
|
+
this.transformersRecord[transformer.id] = transformer;
|
|
421
472
|
};
|
|
422
473
|
deregisterTransformer = (transformer) => {
|
|
423
474
|
const id = typeof transformer === "object" ? transformer.id : transformer;
|
|
424
|
-
if (!this.
|
|
475
|
+
if (!(id in this.transformersRecord)) {
|
|
425
476
|
throw new Error(
|
|
426
477
|
`No transformer found for id "${id}", cannot deregister it`
|
|
427
478
|
);
|
|
@@ -429,7 +480,7 @@ var Serializer = class {
|
|
|
429
480
|
this.transformers = this.transformers.filter(
|
|
430
481
|
(transformer2) => transformer2.id !== id
|
|
431
482
|
);
|
|
432
|
-
this.
|
|
483
|
+
delete this.transformersRecord[id];
|
|
433
484
|
};
|
|
434
485
|
registerClass = (name, clazz) => {
|
|
435
486
|
this.registerTransformer(createClassTransformer("$" + name, clazz));
|
package/dist/index.d.cts
CHANGED
|
@@ -8,22 +8,34 @@ declare namespace Seriall {
|
|
|
8
8
|
| object
|
|
9
9
|
| Serializable[];
|
|
10
10
|
|
|
11
|
-
export type JsonPrimitive = string | number | boolean;
|
|
11
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
12
|
+
|
|
13
|
+
export type MetaData = {
|
|
14
|
+
lib: "seriall";
|
|
15
|
+
v: 1;
|
|
16
|
+
};
|
|
12
17
|
|
|
13
18
|
export namespace Serialized {
|
|
14
|
-
export type
|
|
15
|
-
export type
|
|
19
|
+
export type NodeId = number;
|
|
20
|
+
export type TransformedNode = [Transformer.Id, NodeId];
|
|
21
|
+
export type Node =
|
|
22
|
+
| JsonPrimitive
|
|
23
|
+
| NodeId[]
|
|
24
|
+
| TransformedNode
|
|
25
|
+
| Record<string | number, NodeId>;
|
|
16
26
|
export type Graph = Node[];
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
export type Serialized = MetaData & { d: Seriall.Serialized.Graph };
|
|
30
|
+
|
|
19
31
|
namespace Transformer {
|
|
20
32
|
/** `NoData` is used to signify that no value should be passed to the Revivable node,
|
|
21
33
|
* which is then only described by its tag ($)*/
|
|
22
34
|
type NoData = undefined;
|
|
23
35
|
|
|
24
36
|
type Encoded<Recursive extends boolean> = Recursive extends false
|
|
25
|
-
? JsonPrimitive | object | Encoded<false>[] | NoData
|
|
26
|
-
: (JsonPrimitive | Encoded<true>)[]
|
|
37
|
+
? (JsonPrimitive | object | Encoded<false>)[] | NoData
|
|
38
|
+
: (JsonPrimitive | object | Encoded<true>)[];
|
|
27
39
|
|
|
28
40
|
type Id = string | number;
|
|
29
41
|
|
|
@@ -91,7 +103,7 @@ declare const SYMBOLS: {
|
|
|
91
103
|
};
|
|
92
104
|
declare abstract class SerializableClass {
|
|
93
105
|
constructor(..._: any);
|
|
94
|
-
[ENCODE]
|
|
106
|
+
[ENCODE](): any;
|
|
95
107
|
static [DECODE]: <T extends typeof SerializableClass>(this: T, registerNode: Parameters<Seriall.Transformer.Decoder<any, any, true>>[0]) => InstanceType<T>;
|
|
96
108
|
}
|
|
97
109
|
|
|
@@ -99,12 +111,13 @@ type SerializerOptions = {
|
|
|
99
111
|
enable: {
|
|
100
112
|
builtinPrimitiveTransformers: boolean;
|
|
101
113
|
builtinNativeClasses: boolean;
|
|
102
|
-
|
|
114
|
+
objectSymbolIndexing: boolean;
|
|
103
115
|
};
|
|
116
|
+
classes: Record<Seriall.Transformer.Id, typeof SerializableClass>;
|
|
104
117
|
};
|
|
105
118
|
declare class Serializer {
|
|
106
119
|
private transformers;
|
|
107
|
-
private
|
|
120
|
+
private transformersRecord;
|
|
108
121
|
constructor(options?: DeepOptional<SerializerOptions>);
|
|
109
122
|
serialize: (data: Seriall.Serializable) => string;
|
|
110
123
|
deserialize: (data: string) => any;
|
package/dist/index.d.mts
CHANGED
|
@@ -8,22 +8,34 @@ declare namespace Seriall {
|
|
|
8
8
|
| object
|
|
9
9
|
| Serializable[];
|
|
10
10
|
|
|
11
|
-
export type JsonPrimitive = string | number | boolean;
|
|
11
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
12
|
+
|
|
13
|
+
export type MetaData = {
|
|
14
|
+
lib: "seriall";
|
|
15
|
+
v: 1;
|
|
16
|
+
};
|
|
12
17
|
|
|
13
18
|
export namespace Serialized {
|
|
14
|
-
export type
|
|
15
|
-
export type
|
|
19
|
+
export type NodeId = number;
|
|
20
|
+
export type TransformedNode = [Transformer.Id, NodeId];
|
|
21
|
+
export type Node =
|
|
22
|
+
| JsonPrimitive
|
|
23
|
+
| NodeId[]
|
|
24
|
+
| TransformedNode
|
|
25
|
+
| Record<string | number, NodeId>;
|
|
16
26
|
export type Graph = Node[];
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
export type Serialized = MetaData & { d: Seriall.Serialized.Graph };
|
|
30
|
+
|
|
19
31
|
namespace Transformer {
|
|
20
32
|
/** `NoData` is used to signify that no value should be passed to the Revivable node,
|
|
21
33
|
* which is then only described by its tag ($)*/
|
|
22
34
|
type NoData = undefined;
|
|
23
35
|
|
|
24
36
|
type Encoded<Recursive extends boolean> = Recursive extends false
|
|
25
|
-
? JsonPrimitive | object | Encoded<false>[] | NoData
|
|
26
|
-
: (JsonPrimitive | Encoded<true>)[]
|
|
37
|
+
? (JsonPrimitive | object | Encoded<false>)[] | NoData
|
|
38
|
+
: (JsonPrimitive | object | Encoded<true>)[];
|
|
27
39
|
|
|
28
40
|
type Id = string | number;
|
|
29
41
|
|
|
@@ -91,7 +103,7 @@ declare const SYMBOLS: {
|
|
|
91
103
|
};
|
|
92
104
|
declare abstract class SerializableClass {
|
|
93
105
|
constructor(..._: any);
|
|
94
|
-
[ENCODE]
|
|
106
|
+
[ENCODE](): any;
|
|
95
107
|
static [DECODE]: <T extends typeof SerializableClass>(this: T, registerNode: Parameters<Seriall.Transformer.Decoder<any, any, true>>[0]) => InstanceType<T>;
|
|
96
108
|
}
|
|
97
109
|
|
|
@@ -99,12 +111,13 @@ type SerializerOptions = {
|
|
|
99
111
|
enable: {
|
|
100
112
|
builtinPrimitiveTransformers: boolean;
|
|
101
113
|
builtinNativeClasses: boolean;
|
|
102
|
-
|
|
114
|
+
objectSymbolIndexing: boolean;
|
|
103
115
|
};
|
|
116
|
+
classes: Record<Seriall.Transformer.Id, typeof SerializableClass>;
|
|
104
117
|
};
|
|
105
118
|
declare class Serializer {
|
|
106
119
|
private transformers;
|
|
107
|
-
private
|
|
120
|
+
private transformersRecord;
|
|
108
121
|
constructor(options?: DeepOptional<SerializerOptions>);
|
|
109
122
|
serialize: (data: Seriall.Serializable) => string;
|
|
110
123
|
deserialize: (data: string) => any;
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// src/const/const.ts
|
|
2
|
-
var
|
|
3
|
-
var
|
|
2
|
+
var SIGNATURE_INDEX = 0;
|
|
3
|
+
var DATA_INDEX = 1;
|
|
4
4
|
var NO_TRANSFORM_DATA = void 0;
|
|
5
|
+
var PROTOCOL_VERSION = 1;
|
|
6
|
+
var LIB = "seriall";
|
|
5
7
|
var PRIORITY = {
|
|
6
8
|
CUSTOM_CLASS: 0,
|
|
7
9
|
NATIVE_CLASS: 1,
|
|
@@ -14,13 +16,13 @@ var isSpecialNumber = (value) => {
|
|
|
14
16
|
return [Infinity, -Infinity, NaN].includes(value) || Object.is(value, -0);
|
|
15
17
|
};
|
|
16
18
|
var encodeSpecialNumber = (value) => {
|
|
17
|
-
if (Number.isNaN(value)) return 0;
|
|
18
|
-
if (value === Infinity) return 1;
|
|
19
|
-
if (value === -Infinity) return 2;
|
|
20
|
-
if (Object.is(value, -0)) return 3;
|
|
19
|
+
if (Number.isNaN(value)) return [0];
|
|
20
|
+
if (value === Infinity) return [1];
|
|
21
|
+
if (value === -Infinity) return [2];
|
|
22
|
+
if (Object.is(value, -0)) return [3];
|
|
21
23
|
throw new Error(value + " is not a special number");
|
|
22
24
|
};
|
|
23
|
-
var decodeSpecialNumber = (value) => {
|
|
25
|
+
var decodeSpecialNumber = ([value]) => {
|
|
24
26
|
switch (value) {
|
|
25
27
|
case 0:
|
|
26
28
|
return NaN;
|
|
@@ -40,7 +42,7 @@ var isJsonPrimitive = (value) => {
|
|
|
40
42
|
if (typeof value === "number") {
|
|
41
43
|
return !isSpecialNumber(value);
|
|
42
44
|
}
|
|
43
|
-
return typeof value === "string" || typeof value === "boolean";
|
|
45
|
+
return typeof value === "string" || typeof value === "boolean" || value === null;
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
// src/utils/object.utils.ts
|
|
@@ -68,27 +70,30 @@ var serialize = (data, transformers) => {
|
|
|
68
70
|
const graph = [];
|
|
69
71
|
const seen = /* @__PURE__ */ new Map();
|
|
70
72
|
const addNodeToGraph = (node) => {
|
|
71
|
-
if (isJsonPrimitive(node)) {
|
|
72
|
-
const id2 = graph.length;
|
|
73
|
-
graph.push(node);
|
|
74
|
-
return id2;
|
|
75
|
-
}
|
|
76
73
|
let id = seen.get(node);
|
|
77
|
-
if (id !== void 0) {
|
|
74
|
+
if (id !== void 0 && !Object.is(node, -0)) {
|
|
78
75
|
return id;
|
|
79
76
|
}
|
|
80
77
|
id = graph.length;
|
|
81
78
|
seen.set(node, id);
|
|
79
|
+
if (isJsonPrimitive(node)) {
|
|
80
|
+
const id2 = graph.length;
|
|
81
|
+
graph.push(node);
|
|
82
|
+
return id2;
|
|
83
|
+
}
|
|
82
84
|
const transformer = transformers.find(
|
|
83
85
|
(transformer2) => transformer2.match(node)
|
|
84
86
|
);
|
|
85
87
|
if (transformer) {
|
|
86
|
-
graph.push(
|
|
88
|
+
graph.push([]);
|
|
87
89
|
const revivable = graph[id];
|
|
90
|
+
revivable[SIGNATURE_INDEX] = transformer.id;
|
|
88
91
|
const encoded = transformer.encode(node);
|
|
89
92
|
if (encoded !== NO_TRANSFORM_DATA) {
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
encoded.forEach((el, index) => {
|
|
94
|
+
const dataId = addNodeToGraph(el);
|
|
95
|
+
revivable[DATA_INDEX + index] = dataId;
|
|
96
|
+
});
|
|
92
97
|
}
|
|
93
98
|
return id;
|
|
94
99
|
}
|
|
@@ -101,21 +106,49 @@ var serialize = (data, transformers) => {
|
|
|
101
106
|
}
|
|
102
107
|
return id;
|
|
103
108
|
}
|
|
109
|
+
if (typeof node === "object") {
|
|
110
|
+
graph.push({});
|
|
111
|
+
const obj = graph[id];
|
|
112
|
+
for (const key in node) {
|
|
113
|
+
const elementId = addNodeToGraph(node[key]);
|
|
114
|
+
obj[key] = elementId;
|
|
115
|
+
}
|
|
116
|
+
return id;
|
|
117
|
+
}
|
|
104
118
|
throw new Error(
|
|
105
119
|
`No transformer found for the current value "${node?.toString()}", please add one`
|
|
106
120
|
);
|
|
107
121
|
};
|
|
108
122
|
addNodeToGraph(data);
|
|
109
|
-
|
|
123
|
+
const serialized = {
|
|
124
|
+
lib: LIB,
|
|
125
|
+
v: PROTOCOL_VERSION,
|
|
126
|
+
d: graph
|
|
127
|
+
};
|
|
128
|
+
return JSON.stringify(serialized);
|
|
110
129
|
};
|
|
111
130
|
|
|
112
131
|
// src/serialization/deserialize.ts
|
|
113
132
|
var deserialize = (data, transformers) => {
|
|
114
|
-
const
|
|
115
|
-
|
|
133
|
+
const { lib, v, d } = JSON.parse(data);
|
|
134
|
+
if (lib !== LIB) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Can't deserialize data, invalid lib metadata. Expected "${LIB}", got "${lib}"`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (v !== PROTOCOL_VERSION) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Can't deserialize data, invalid protocol version. Expected ${PROTOCOL_VERSION}, got ${v}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (!Array.isArray(d)) {
|
|
145
|
+
throw new Error("Invalid seriall payload: 'd' must be an array");
|
|
146
|
+
}
|
|
147
|
+
const graph = d;
|
|
148
|
+
const revived = [];
|
|
116
149
|
const reviveNode = (index) => {
|
|
117
|
-
if (revived
|
|
118
|
-
return revived
|
|
150
|
+
if (index in revived) {
|
|
151
|
+
return revived[index];
|
|
119
152
|
}
|
|
120
153
|
if (graph.length <= index || index < 0) {
|
|
121
154
|
throw new Error(`Invalid serialized graph reference: ${index}`);
|
|
@@ -124,37 +157,54 @@ var deserialize = (data, transformers) => {
|
|
|
124
157
|
if (isJsonPrimitive(node)) {
|
|
125
158
|
return node;
|
|
126
159
|
}
|
|
127
|
-
if (Array.isArray(node)) {
|
|
160
|
+
if (!Array.isArray(node) && typeof node === "object") {
|
|
161
|
+
const obj = {};
|
|
162
|
+
revived[index] = obj;
|
|
163
|
+
for (const key of Object.keys(node)) {
|
|
164
|
+
obj[key] = reviveNode(node[key]);
|
|
165
|
+
}
|
|
166
|
+
return obj;
|
|
167
|
+
}
|
|
168
|
+
if (typeof node[SIGNATURE_INDEX] !== "string") {
|
|
169
|
+
const typedNode2 = node;
|
|
128
170
|
const arr = [];
|
|
129
|
-
revived
|
|
130
|
-
for (const element of
|
|
171
|
+
revived[index] = arr;
|
|
172
|
+
for (const element of typedNode2) {
|
|
131
173
|
arr.push(reviveNode(element));
|
|
132
174
|
}
|
|
133
175
|
return arr;
|
|
134
176
|
}
|
|
135
|
-
const
|
|
136
|
-
const
|
|
177
|
+
const typedNode = node;
|
|
178
|
+
const transformerId = typedNode[SIGNATURE_INDEX];
|
|
179
|
+
const transformer = transformers[transformerId];
|
|
137
180
|
if (!transformerId) {
|
|
138
|
-
throw new Error(`No transformer found for node "${
|
|
181
|
+
throw new Error(`No transformer found for node "${typedNode}"`);
|
|
139
182
|
}
|
|
140
183
|
if (transformer === void 0) {
|
|
141
184
|
throw new Error(`No transformer found with id "${transformerId}"`);
|
|
142
185
|
}
|
|
143
186
|
const nonRecursiveTransformer = transformer;
|
|
144
|
-
|
|
145
|
-
if (dataId === void 0) {
|
|
187
|
+
if (!transformer.recursive && typedNode[DATA_INDEX] === void 0) {
|
|
146
188
|
const decoded = nonRecursiveTransformer.decode(NO_TRANSFORM_DATA);
|
|
147
|
-
revived
|
|
189
|
+
revived[index] = decoded;
|
|
148
190
|
return decoded;
|
|
149
191
|
}
|
|
150
192
|
if (!transformer.recursive) {
|
|
151
|
-
const
|
|
152
|
-
|
|
193
|
+
const revivedValues = new Array(typedNode.length - 1);
|
|
194
|
+
for (let i = 1; i < typedNode.length; i++) {
|
|
195
|
+
revivedValues[i - 1] = reviveNode(typedNode[i]);
|
|
196
|
+
}
|
|
197
|
+
const decoded = nonRecursiveTransformer.decode(revivedValues);
|
|
198
|
+
revived[index] = decoded;
|
|
153
199
|
return decoded;
|
|
154
200
|
}
|
|
155
201
|
const registerNode = (node2) => {
|
|
156
|
-
revived
|
|
157
|
-
|
|
202
|
+
revived[index] = node2;
|
|
203
|
+
const revivedValues = new Array(typedNode.length - 1);
|
|
204
|
+
for (let i = 1; i < typedNode.length; i++) {
|
|
205
|
+
revivedValues[i - 1] = reviveNode(typedNode[i]);
|
|
206
|
+
}
|
|
207
|
+
return revivedValues;
|
|
158
208
|
};
|
|
159
209
|
return transformer.decode(registerNode);
|
|
160
210
|
};
|
|
@@ -193,15 +243,15 @@ var primitivesTransformers = [
|
|
|
193
243
|
id: "sym",
|
|
194
244
|
priority: Transformer.PRIORITY.PRIMITIVE,
|
|
195
245
|
match: (node) => typeof node === "symbol",
|
|
196
|
-
encode: (node) =>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
246
|
+
encode: (node) => [
|
|
247
|
+
node.description,
|
|
248
|
+
Symbol.keyFor(node) === void 0 ? 0 : 1
|
|
249
|
+
],
|
|
200
250
|
decode: (encoded) => {
|
|
201
|
-
if (encoded
|
|
251
|
+
if (encoded[1] === 1) {
|
|
202
252
|
return Symbol.for(encoded.d);
|
|
203
253
|
}
|
|
204
|
-
return Symbol(encoded
|
|
254
|
+
return Symbol(encoded[0]);
|
|
205
255
|
}
|
|
206
256
|
}),
|
|
207
257
|
new Transformer({
|
|
@@ -218,52 +268,45 @@ var primitivesTransformers = [
|
|
|
218
268
|
encode: () => NO_TRANSFORM_DATA,
|
|
219
269
|
decode: () => void 0
|
|
220
270
|
}),
|
|
221
|
-
new Transformer({
|
|
222
|
-
id: "nul",
|
|
223
|
-
priority: Transformer.PRIORITY.PRIMITIVE,
|
|
224
|
-
match: (node) => node === null,
|
|
225
|
-
encode: () => NO_TRANSFORM_DATA,
|
|
226
|
-
decode: () => null
|
|
227
|
-
}),
|
|
228
271
|
new Transformer({
|
|
229
272
|
id: "big",
|
|
230
273
|
priority: Transformer.PRIORITY.PRIMITIVE,
|
|
231
274
|
match: (node) => typeof node === "bigint",
|
|
232
|
-
encode: (node) => node.toString(),
|
|
233
|
-
decode: (node) => BigInt(node)
|
|
234
|
-
}),
|
|
235
|
-
new Transformer({
|
|
236
|
-
id: "obj",
|
|
237
|
-
priority: Transformer.PRIORITY.PRIMITIVE,
|
|
238
|
-
recursive: true,
|
|
239
|
-
match: (node) => !Array.isArray(node) && typeof node === "object",
|
|
240
|
-
encode: (node) => {
|
|
241
|
-
const obj = [];
|
|
242
|
-
for (const key of Reflect.ownKeys(node)) {
|
|
243
|
-
const value = node[key];
|
|
244
|
-
if (typeof value !== "function") {
|
|
245
|
-
obj.push([key, node[key]]);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return obj;
|
|
249
|
-
},
|
|
250
|
-
decode: (registerNode) => {
|
|
251
|
-
const obj = {};
|
|
252
|
-
const encoded = registerNode(obj);
|
|
253
|
-
for (const [key, value] of encoded) {
|
|
254
|
-
obj[key] = value;
|
|
255
|
-
}
|
|
256
|
-
return obj;
|
|
257
|
-
}
|
|
275
|
+
encode: (node) => [node.toString()],
|
|
276
|
+
decode: ([node]) => BigInt(node)
|
|
258
277
|
})
|
|
259
278
|
];
|
|
279
|
+
var objectSymbolIndexingTransformer = new Transformer({
|
|
280
|
+
id: "obj",
|
|
281
|
+
priority: Transformer.PRIORITY.PRIMITIVE,
|
|
282
|
+
recursive: true,
|
|
283
|
+
match: (node) => !Array.isArray(node) && typeof node === "object",
|
|
284
|
+
encode: (node) => {
|
|
285
|
+
const obj = [];
|
|
286
|
+
for (const key of Reflect.ownKeys(node)) {
|
|
287
|
+
const value = node[key];
|
|
288
|
+
if (typeof value !== "function") {
|
|
289
|
+
obj.push([key, node[key]]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return obj;
|
|
293
|
+
},
|
|
294
|
+
decode: (registerNode) => {
|
|
295
|
+
const obj = {};
|
|
296
|
+
const encoded = registerNode(obj);
|
|
297
|
+
for (const [key, value] of encoded) {
|
|
298
|
+
obj[key] = value;
|
|
299
|
+
}
|
|
300
|
+
return obj;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
260
303
|
var nativeClassesTransformers = [
|
|
261
304
|
new Transformer({
|
|
262
305
|
id: "Dte",
|
|
263
306
|
priority: Transformer.PRIORITY.NATIVE_CLASS,
|
|
264
307
|
match: (node) => node instanceof Date,
|
|
265
|
-
encode: (node) => node.getTime(),
|
|
266
|
-
decode: (node) => new Date(node)
|
|
308
|
+
encode: (node) => [node.getTime()],
|
|
309
|
+
decode: ([node]) => new Date(node)
|
|
267
310
|
}),
|
|
268
311
|
new Transformer({
|
|
269
312
|
id: "Set",
|
|
@@ -299,29 +342,29 @@ var nativeClassesTransformers = [
|
|
|
299
342
|
id: "Rgx",
|
|
300
343
|
priority: Transformer.PRIORITY.NATIVE_CLASS,
|
|
301
344
|
match: (node) => node instanceof RegExp,
|
|
302
|
-
encode: (node) =>
|
|
303
|
-
decode: (node) => new RegExp(node
|
|
345
|
+
encode: (node) => [node.source, node.flags],
|
|
346
|
+
decode: (node) => new RegExp(node[0], node[1])
|
|
304
347
|
}),
|
|
305
348
|
new Transformer({
|
|
306
349
|
id: "Str",
|
|
307
350
|
priority: Transformer.PRIORITY.NATIVE_CLASS,
|
|
308
351
|
match: (node) => node instanceof String,
|
|
309
|
-
encode: (node) => node.valueOf(),
|
|
310
|
-
decode: (node) => new String(node)
|
|
352
|
+
encode: (node) => [node.valueOf()],
|
|
353
|
+
decode: ([node]) => new String(node)
|
|
311
354
|
}),
|
|
312
355
|
new Transformer({
|
|
313
356
|
id: "Num",
|
|
314
357
|
priority: Transformer.PRIORITY.NATIVE_CLASS,
|
|
315
358
|
match: (node) => node instanceof Number,
|
|
316
|
-
encode: (node) => node.valueOf(),
|
|
317
|
-
decode: (node) => new Number(node)
|
|
359
|
+
encode: (node) => [node.valueOf()],
|
|
360
|
+
decode: ([node]) => new Number(node)
|
|
318
361
|
}),
|
|
319
362
|
new Transformer({
|
|
320
363
|
id: "Bol",
|
|
321
364
|
priority: Transformer.PRIORITY.NATIVE_CLASS,
|
|
322
365
|
match: (node) => node instanceof Boolean,
|
|
323
|
-
encode: (node) => node.valueOf(),
|
|
324
|
-
decode: (node) => new Boolean(node)
|
|
366
|
+
encode: (node) => [node.valueOf()],
|
|
367
|
+
decode: ([node]) => new Boolean(node)
|
|
325
368
|
})
|
|
326
369
|
];
|
|
327
370
|
|
|
@@ -332,20 +375,21 @@ var SYMBOLS = { ENCODE, DECODE };
|
|
|
332
375
|
var SerializableClass = class {
|
|
333
376
|
constructor(..._) {
|
|
334
377
|
}
|
|
335
|
-
[ENCODE]
|
|
336
|
-
const encoded =
|
|
337
|
-
for (const key of
|
|
378
|
+
[ENCODE]() {
|
|
379
|
+
const encoded = [];
|
|
380
|
+
for (const key of Object.keys(this)) {
|
|
338
381
|
const value = this[key];
|
|
339
382
|
if (typeof value !== "function") {
|
|
340
|
-
encoded
|
|
383
|
+
encoded.push(this[key]);
|
|
341
384
|
}
|
|
342
385
|
}
|
|
343
386
|
return encoded;
|
|
344
|
-
}
|
|
387
|
+
}
|
|
345
388
|
static [DECODE] = function(registerNode) {
|
|
346
389
|
let revivedInstance = {};
|
|
347
390
|
const args = registerNode(revivedInstance);
|
|
348
|
-
|
|
391
|
+
const keys = Reflect.ownKeys(new this({}));
|
|
392
|
+
keys.forEach((key, index) => revivedInstance[key] = args[index]);
|
|
349
393
|
Object.setPrototypeOf(revivedInstance, this.prototype);
|
|
350
394
|
return revivedInstance;
|
|
351
395
|
};
|
|
@@ -364,23 +408,30 @@ var defaultOptions = {
|
|
|
364
408
|
enable: {
|
|
365
409
|
builtinPrimitiveTransformers: true,
|
|
366
410
|
builtinNativeClasses: true,
|
|
367
|
-
|
|
368
|
-
}
|
|
411
|
+
objectSymbolIndexing: false
|
|
412
|
+
},
|
|
413
|
+
classes: {}
|
|
369
414
|
};
|
|
370
415
|
var Serializer = class {
|
|
371
416
|
transformers = [];
|
|
372
|
-
|
|
417
|
+
transformersRecord = {};
|
|
373
418
|
constructor(options = {}) {
|
|
374
419
|
const opt = deepMerge(defaultOptions, options);
|
|
375
420
|
if (opt.enable.builtinPrimitiveTransformers)
|
|
376
421
|
primitivesTransformers.forEach(this.registerTransformer);
|
|
377
422
|
if (opt.enable.builtinNativeClasses)
|
|
378
423
|
nativeClassesTransformers.forEach(this.registerTransformer);
|
|
424
|
+
if (opt.enable.objectSymbolIndexing) {
|
|
425
|
+
this.registerTransformer(objectSymbolIndexingTransformer);
|
|
426
|
+
}
|
|
427
|
+
for (const className in opt.classes) {
|
|
428
|
+
this.registerClass(className, opt.classes[className]);
|
|
429
|
+
}
|
|
379
430
|
}
|
|
380
431
|
serialize = (data) => serialize(data, this.transformers);
|
|
381
|
-
deserialize = (data) => deserialize(data, this.
|
|
432
|
+
deserialize = (data) => deserialize(data, this.transformersRecord);
|
|
382
433
|
registerTransformer = (transformer) => {
|
|
383
|
-
if (
|
|
434
|
+
if (transformer.id in this.transformersRecord) {
|
|
384
435
|
throw new Error(
|
|
385
436
|
`A transformer with id "${transformer.id}" is already registered, chose another one`
|
|
386
437
|
);
|
|
@@ -389,11 +440,11 @@ var Serializer = class {
|
|
|
389
440
|
this.transformers = this.transformers.sort(
|
|
390
441
|
(a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity)
|
|
391
442
|
);
|
|
392
|
-
this.
|
|
443
|
+
this.transformersRecord[transformer.id] = transformer;
|
|
393
444
|
};
|
|
394
445
|
deregisterTransformer = (transformer) => {
|
|
395
446
|
const id = typeof transformer === "object" ? transformer.id : transformer;
|
|
396
|
-
if (!this.
|
|
447
|
+
if (!(id in this.transformersRecord)) {
|
|
397
448
|
throw new Error(
|
|
398
449
|
`No transformer found for id "${id}", cannot deregister it`
|
|
399
450
|
);
|
|
@@ -401,7 +452,7 @@ var Serializer = class {
|
|
|
401
452
|
this.transformers = this.transformers.filter(
|
|
402
453
|
(transformer2) => transformer2.id !== id
|
|
403
454
|
);
|
|
404
|
-
this.
|
|
455
|
+
delete this.transformersRecord[id];
|
|
405
456
|
};
|
|
406
457
|
registerClass = (name, clazz) => {
|
|
407
458
|
this.registerTransformer(createClassTransformer("$" + name, clazz));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seriall",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A flexible js serializer, capable of serializing/deserializing any js data, while preserving referential integrity, non-json-primitive values, and even custom class instances",
|
|
5
5
|
"author": {
|
|
6
6
|
"email": "loic.duvail.pro@gmail.com",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"build:js": "node ./esbuild.js",
|
|
26
26
|
"build:types": "rollup -c",
|
|
27
27
|
"build:test": "node ./esbuild.test.js",
|
|
28
|
-
"
|
|
28
|
+
"build:benchmark": "node ./esbuild.benchmark.js",
|
|
29
|
+
"test": "npm run build:test && npx jest",
|
|
30
|
+
"bench": "npm run build:benchmark && node benchmark/benchmark.js /"
|
|
29
31
|
},
|
|
30
32
|
"keywords": [
|
|
31
33
|
"serializer",
|
|
@@ -43,10 +45,13 @@
|
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@types/jest": "^30.0.0",
|
|
45
47
|
"@types/node": "^26.5.1",
|
|
48
|
+
"devalue": "^6.0.2",
|
|
46
49
|
"esbuild": "^0.28.2",
|
|
50
|
+
"flatted": "^3.4.4",
|
|
47
51
|
"jest": "^30.5.1",
|
|
48
52
|
"rollup": "^4.63.6",
|
|
49
53
|
"rollup-plugin-dts": "^6.5.1",
|
|
54
|
+
"superjson": "^2.2.6",
|
|
50
55
|
"typescript": "^6.0.3"
|
|
51
56
|
}
|
|
52
57
|
}
|