seriall 1.0.0 → 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 +467 -2
- package/dist/index.cjs +148 -94
- package/dist/index.d.cts +21 -8
- package/dist/index.d.mts +21 -8
- package/dist/index.mjs +148 -94
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,2 +1,467 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# seriall
|
|
2
|
+
|
|
3
|
+
## TL;DR
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
**The only JavaScript values not supported out-of-the-box are:**
|
|
8
|
+
|
|
9
|
+
- ⚠️ Custom classes instances with **JavaScript private fields** (`#field`),
|
|
10
|
+
|
|
11
|
+
there are still configurable workarounds, just no out-of-the-box solution
|
|
12
|
+
|
|
13
|
+
typescript `private` visibility is generally supported ✅
|
|
14
|
+
|
|
15
|
+
- 🚫 Any **function / whole class**, for security reasons (e.g: `serialize(MyClass)` or `serialize(myFunction)`)
|
|
16
|
+
|
|
17
|
+
even though even this is theoretically configurable
|
|
18
|
+
|
|
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
|
+
|
|
21
|
+
It **conserves refenrential integrity**, so circularly referenced arrays/objects, and cross referenced arrays/objects can be serialized
|
|
22
|
+
|
|
23
|
+
It **can out-of-the-box register custom classes**, and recreates their instances at deserialization time
|
|
24
|
+
|
|
25
|
+
It is **highly configurable**, through the usage of `transformers`
|
|
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
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
Inside an npm project: `npm install seriall` or `yarn install seriall`
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Basic usage
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// example.ts
|
|
58
|
+
|
|
59
|
+
import { Serializer } from "seriall";
|
|
60
|
+
|
|
61
|
+
const { serialize, deserialize } = new Serializer();
|
|
62
|
+
|
|
63
|
+
// ------- create test data -------
|
|
64
|
+
|
|
65
|
+
const nested = { name: "Gömböc" };
|
|
66
|
+
|
|
67
|
+
const myData: any = {
|
|
68
|
+
nestedObjects: [nested, nested],
|
|
69
|
+
name: "It works!",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
myData.self = myData;
|
|
73
|
+
|
|
74
|
+
// ------- serialize data -------
|
|
75
|
+
|
|
76
|
+
const str = serialize(myData);
|
|
77
|
+
|
|
78
|
+
// ------- ------- -------
|
|
79
|
+
// `str` can now go through a network for instance, and be deserialized at the other end like so:
|
|
80
|
+
// ------- ------- -------
|
|
81
|
+
|
|
82
|
+
const revivedData = deserialize(str);
|
|
83
|
+
|
|
84
|
+
console.log(revivedData.self === revivedData);
|
|
85
|
+
// true
|
|
86
|
+
console.log(revivedData.nestedObjects[0] === revivedData.nestedObjects[1]);
|
|
87
|
+
// true
|
|
88
|
+
console.log(revivedData.nestedObjects[0]);
|
|
89
|
+
// {name: "Gömböc"}
|
|
90
|
+
console.log(revivedData.name);
|
|
91
|
+
// It works!
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Registering custom classes
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// example.ts
|
|
98
|
+
|
|
99
|
+
import { Serializer, SerializableClass } from "seriall";
|
|
100
|
+
|
|
101
|
+
// ------- create a new serializer -------
|
|
102
|
+
|
|
103
|
+
const serializer = new Serializer();
|
|
104
|
+
|
|
105
|
+
// ------- create and register custom classes -------
|
|
106
|
+
|
|
107
|
+
class Address extends SerializableClass {
|
|
108
|
+
country: string;
|
|
109
|
+
city: string;
|
|
110
|
+
constructor(country: string, city: string) {
|
|
111
|
+
super();
|
|
112
|
+
this.country = country;
|
|
113
|
+
this.city = city;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getFullAddress() {
|
|
117
|
+
return `${this.city}, ${this.country}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
serializer.registerClass("adr", Address);
|
|
122
|
+
|
|
123
|
+
class User extends SerializableClass {
|
|
124
|
+
name: string;
|
|
125
|
+
friends: User[] = [];
|
|
126
|
+
address: Address | undefined;
|
|
127
|
+
constructor(name: string) {
|
|
128
|
+
super();
|
|
129
|
+
this.name = name;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
serializer.registerClass("usr", User);
|
|
134
|
+
|
|
135
|
+
// ------- create test data -------
|
|
136
|
+
|
|
137
|
+
const mary = new User("Mary");
|
|
138
|
+
const john = new User("John");
|
|
139
|
+
const paris = new Address("France", "Paris");
|
|
140
|
+
|
|
141
|
+
mary.address = paris;
|
|
142
|
+
john.address = paris;
|
|
143
|
+
mary.friends.push(john);
|
|
144
|
+
john.friends.push(mary);
|
|
145
|
+
|
|
146
|
+
const str = serializer.serialize(mary);
|
|
147
|
+
|
|
148
|
+
// ------- ------- -------
|
|
149
|
+
// `str` can now go through a network for instance, and be deserialized at the other end like so:
|
|
150
|
+
// ------- ------- -------
|
|
151
|
+
|
|
152
|
+
const revived = serializer.deserialize(str);
|
|
153
|
+
|
|
154
|
+
console.log(revived.name);
|
|
155
|
+
// mary
|
|
156
|
+
console.log(revived.address.getFullAddress());
|
|
157
|
+
// Paris, France
|
|
158
|
+
console.log(revived.friends[0].name);
|
|
159
|
+
// john
|
|
160
|
+
console.log(revived.friends[0].friends[0] === revived);
|
|
161
|
+
// true
|
|
162
|
+
console.log(revived.address === revived.friends[0].address);
|
|
163
|
+
// true
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Supported
|
|
167
|
+
|
|
168
|
+
As mentionned earlyer, seriall supports any graph data (objects or arrays), while preserving referential integrity
|
|
169
|
+
|
|
170
|
+
It also natively support any non-json-primitives:
|
|
171
|
+
|
|
172
|
+
- `undefined`, `null`, `Infinity`, `-Infinity`, `-0`, `NaN`
|
|
173
|
+
|
|
174
|
+
as well as JS-specific data-types:
|
|
175
|
+
|
|
176
|
+
- `Symbols`, `BigInts`
|
|
177
|
+
|
|
178
|
+
native classes instances
|
|
179
|
+
|
|
180
|
+
- `Date`, `RegExp`, `Set`, `Map`
|
|
181
|
+
|
|
182
|
+
and boxed primitives
|
|
183
|
+
|
|
184
|
+
- `String`, `Number`, `Boolean`
|
|
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
|
+
|
|
383
|
+
## Import Notes
|
|
384
|
+
|
|
385
|
+
seriall is exported in both cjs and mjs.
|
|
386
|
+
|
|
387
|
+
It therefore supports both CommonJS import (`require`) and ES Module import (`import`)
|
|
388
|
+
|
|
389
|
+
It also exposes both `.d.cts` and `.d.mts` declaration files, and provides therefore type-safety in both environment.
|
|
390
|
+
|
|
391
|
+
### JavaScript
|
|
392
|
+
|
|
393
|
+
**ES Module**
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
// demo.mjs
|
|
397
|
+
import { Serializer } from "seriall";
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**CommonJS**
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
// demo.cjs
|
|
404
|
+
const { Serializer } = require("seriall");
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Typescript
|
|
408
|
+
|
|
409
|
+
**ES Module**
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
// demo.mts
|
|
413
|
+
import { Serializer } from "seriall";
|
|
414
|
+
// Serializer is properly typed
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**CommonJS**
|
|
418
|
+
|
|
419
|
+
either
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
// demo.cts
|
|
423
|
+
import seriall = require("seriall");
|
|
424
|
+
const { Serializer } = seriall;
|
|
425
|
+
// Serializer is now typed properly
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
or
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
// demo.cts
|
|
432
|
+
const { Serializer } = require("seriall") as typeof import("seriall");
|
|
433
|
+
// Serializer is now typed properly
|
|
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.
|