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 CHANGED
@@ -1,2 +1,467 @@
1
- # Seriall
2
- A general purpose js serializer, which preserves referential integrity (circular objects...), special values (Infintiy, NaN, undefined...), as well as native and custom classes instances.
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
+ ![Common JavaScript Graph](./assets/benchmark-common.svg)
372
+
373
+ ![Rich JavaScript Graph](./assets/benchmark-rich.svg)
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.