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 CHANGED
@@ -1,12 +1,12 @@
1
- # Seriall
1
+ # seriall
2
2
 
3
3
  ## TL;DR
4
4
 
5
- **Seriall serializes JavaScript object graphs while preserving references, circular references, special primitive values, built-in types, and registered custom classes.**
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 **unsuported datatypes** out-of-the-box are:
7
+ **The only JavaScript values not supported out-of-the-box are:**
8
8
 
9
- - ⚠️ Custom classes instances with **js-private fields** (prefixed 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
- 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.
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
+ ![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
+
165
383
  ## Import Notes
166
384
 
167
- Seriall is exported in both cjs and mjs.
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
- **CommonJS**
393
+ **ES Module**
176
394
 
177
395
  ```ts
178
396
  // demo.mjs
179
397
  import { Serializer } from "seriall";
180
398
  ```
181
399
 
182
- **ES Module**
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
- **CommonJS**
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
- **ES Module**
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 SIGNATURE_KEY = "$";
31
- var DATA_KEY = "d";
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({ $: transformer.id });
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
- const dataId = addNodeToGraph(encoded);
119
- revivable.d = dataId;
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
- return JSON.stringify(graph);
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 graph = JSON.parse(data);
143
- const revived = /* @__PURE__ */ new Map();
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.has(index)) {
146
- return revived.get(index);
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.set(index, arr);
158
- for (const element of node) {
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 transformerId = node[SIGNATURE_KEY];
164
- const transformer = transformers.get(transformerId);
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 "${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
- const dataId = node[DATA_KEY];
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.set(index, decoded);
217
+ revived[index] = decoded;
176
218
  return decoded;
177
219
  }
178
220
  if (!transformer.recursive) {
179
- const decoded = nonRecursiveTransformer.decode(reviveNode(dataId));
180
- revived.set(index, decoded);
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.set(index, node2);
185
- return reviveNode(dataId);
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
- d: node.description,
226
- f: Symbol.keyFor(node) === void 0 ? 0 : 1
227
- }),
274
+ encode: (node) => [
275
+ node.description,
276
+ Symbol.keyFor(node) === void 0 ? 0 : 1
277
+ ],
228
278
  decode: (encoded) => {
229
- if (encoded.f === 1 && encoded.d) {
279
+ if (encoded[1] === 1) {
230
280
  return Symbol.for(encoded.d);
231
281
  }
232
- return Symbol(encoded.d);
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) => ({ s: node.source, f: node.flags }),
331
- decode: (node) => new RegExp(node.s, node.f)
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 Reflect.ownKeys(this)) {
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[key] = this[key];
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
- Object.assign(revivedInstance, args);
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
- builtinClassTransformer: true
396
- }
439
+ objectSymbolIndexing: false
440
+ },
441
+ classes: {}
397
442
  };
398
443
  var Serializer = class {
399
444
  transformers = [];
400
- transformersMap = /* @__PURE__ */ new Map();
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.transformersMap);
460
+ deserialize = (data) => deserialize(data, this.transformersRecord);
410
461
  registerTransformer = (transformer) => {
411
- if (this.transformersMap.has(transformer.id)) {
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.transformersMap.set(transformer.id, transformer);
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.transformersMap.has(id)) {
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.transformersMap.delete(id);
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 RevivableNode = { $: Transformer.Id; d?: number };
15
- export type Node = JsonPrimitive | number[] | RevivableNode;
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>)[] | object;
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
- builtinClassTransformer: boolean;
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 transformersMap;
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 RevivableNode = { $: Transformer.Id; d?: number };
15
- export type Node = JsonPrimitive | number[] | RevivableNode;
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>)[] | object;
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
- builtinClassTransformer: boolean;
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 transformersMap;
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 SIGNATURE_KEY = "$";
3
- var DATA_KEY = "d";
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({ $: transformer.id });
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
- const dataId = addNodeToGraph(encoded);
91
- revivable.d = dataId;
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
- return JSON.stringify(graph);
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 graph = JSON.parse(data);
115
- const revived = /* @__PURE__ */ new Map();
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.has(index)) {
118
- return revived.get(index);
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.set(index, arr);
130
- for (const element of node) {
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 transformerId = node[SIGNATURE_KEY];
136
- const transformer = transformers.get(transformerId);
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 "${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
- const dataId = node[DATA_KEY];
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.set(index, decoded);
189
+ revived[index] = decoded;
148
190
  return decoded;
149
191
  }
150
192
  if (!transformer.recursive) {
151
- const decoded = nonRecursiveTransformer.decode(reviveNode(dataId));
152
- revived.set(index, decoded);
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.set(index, node2);
157
- return reviveNode(dataId);
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
- d: node.description,
198
- f: Symbol.keyFor(node) === void 0 ? 0 : 1
199
- }),
246
+ encode: (node) => [
247
+ node.description,
248
+ Symbol.keyFor(node) === void 0 ? 0 : 1
249
+ ],
200
250
  decode: (encoded) => {
201
- if (encoded.f === 1 && encoded.d) {
251
+ if (encoded[1] === 1) {
202
252
  return Symbol.for(encoded.d);
203
253
  }
204
- return Symbol(encoded.d);
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) => ({ s: node.source, f: node.flags }),
303
- decode: (node) => new RegExp(node.s, node.f)
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 Reflect.ownKeys(this)) {
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[key] = this[key];
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
- Object.assign(revivedInstance, args);
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
- builtinClassTransformer: true
368
- }
411
+ objectSymbolIndexing: false
412
+ },
413
+ classes: {}
369
414
  };
370
415
  var Serializer = class {
371
416
  transformers = [];
372
- transformersMap = /* @__PURE__ */ new Map();
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.transformersMap);
432
+ deserialize = (data) => deserialize(data, this.transformersRecord);
382
433
  registerTransformer = (transformer) => {
383
- if (this.transformersMap.has(transformer.id)) {
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.transformersMap.set(transformer.id, transformer);
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.transformersMap.has(id)) {
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.transformersMap.delete(id);
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.1",
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
- "test": "npm run build:test && npx jest"
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
  }