seriall 1.0.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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+
2
+
3
+ Copyright 2026 Loïc Duvail
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,2 @@
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.
package/dist/index.cjs ADDED
@@ -0,0 +1,437 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.ts
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ SYMBOLS: () => SYMBOLS,
23
+ SerializableClass: () => SerializableClass,
24
+ Serializer: () => Serializer,
25
+ Transformer: () => Transformer
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/const/const.ts
30
+ var SIGNATURE_KEY = "$";
31
+ var DATA_KEY = "d";
32
+ var NO_TRANSFORM_DATA = void 0;
33
+ var PRIORITY = {
34
+ CUSTOM_CLASS: 0,
35
+ NATIVE_CLASS: 1,
36
+ PRIMITIVE: 2,
37
+ custom: (priority) => (priority <= 0 ? 1 : priority) + PRIORITY.CUSTOM_CLASS
38
+ };
39
+
40
+ // src/utils/number.utils.ts
41
+ var isSpecialNumber = (value) => {
42
+ return [Infinity, -Infinity, NaN].includes(value) || Object.is(value, -0);
43
+ };
44
+ 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;
49
+ throw new Error(value + " is not a special number");
50
+ };
51
+ var decodeSpecialNumber = (value) => {
52
+ switch (value) {
53
+ case 0:
54
+ return NaN;
55
+ case 1:
56
+ return Infinity;
57
+ case 2:
58
+ return -Infinity;
59
+ case 3:
60
+ return -0;
61
+ default:
62
+ throw new Error(value + " is not a special number");
63
+ }
64
+ };
65
+
66
+ // src/utils/json.utils.ts
67
+ var isJsonPrimitive = (value) => {
68
+ if (typeof value === "number") {
69
+ return !isSpecialNumber(value);
70
+ }
71
+ return typeof value === "string" || typeof value === "boolean";
72
+ };
73
+
74
+ // src/utils/object.utils.ts
75
+ var isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
76
+ var deepMerge = (...objects) => {
77
+ const result = {};
78
+ for (const obj of objects) {
79
+ if (!isPlainObject(obj)) continue;
80
+ for (const [key, value] of Object.entries(obj)) {
81
+ if (Array.isArray(value)) {
82
+ result[key] = structuredClone(value);
83
+ } else if (isPlainObject(value)) {
84
+ const existing = result[key];
85
+ result[key] = deepMerge(isPlainObject(existing) ? existing : {}, value);
86
+ } else {
87
+ result[key] = value;
88
+ }
89
+ }
90
+ }
91
+ return result;
92
+ };
93
+
94
+ // src/serialization/serialize.ts
95
+ var serialize = (data, transformers) => {
96
+ const graph = [];
97
+ const seen = /* @__PURE__ */ new Map();
98
+ const addNodeToGraph = (node) => {
99
+ if (isJsonPrimitive(node)) {
100
+ const id2 = graph.length;
101
+ graph.push(node);
102
+ return id2;
103
+ }
104
+ let id = seen.get(node);
105
+ if (id !== void 0) {
106
+ return id;
107
+ }
108
+ id = graph.length;
109
+ seen.set(node, id);
110
+ const transformer = transformers.find(
111
+ (transformer2) => transformer2.match(node)
112
+ );
113
+ if (transformer) {
114
+ graph.push({ $: transformer.id });
115
+ const revivable = graph[id];
116
+ const encoded = transformer.encode(node);
117
+ if (encoded !== NO_TRANSFORM_DATA) {
118
+ const dataId = addNodeToGraph(encoded);
119
+ revivable.d = dataId;
120
+ }
121
+ return id;
122
+ }
123
+ if (Array.isArray(node)) {
124
+ graph.push([]);
125
+ const arr = graph[id];
126
+ for (const element of node) {
127
+ const elementId = addNodeToGraph(element);
128
+ arr.push(elementId);
129
+ }
130
+ return id;
131
+ }
132
+ throw new Error(
133
+ `No transformer found for the current value "${node?.toString()}", please add one`
134
+ );
135
+ };
136
+ addNodeToGraph(data);
137
+ return JSON.stringify(graph);
138
+ };
139
+
140
+ // src/serialization/deserialize.ts
141
+ var deserialize = (data, transformers) => {
142
+ const graph = JSON.parse(data);
143
+ const revived = /* @__PURE__ */ new Map();
144
+ const reviveNode = (index) => {
145
+ if (revived.has(index)) {
146
+ return revived.get(index);
147
+ }
148
+ if (graph.length <= index || index < 0) {
149
+ throw new Error(`Invalid serialized graph reference: ${index}`);
150
+ }
151
+ const node = graph[index];
152
+ if (isJsonPrimitive(node)) {
153
+ return node;
154
+ }
155
+ if (Array.isArray(node)) {
156
+ const arr = [];
157
+ revived.set(index, arr);
158
+ for (const element of node) {
159
+ arr.push(reviveNode(element));
160
+ }
161
+ return arr;
162
+ }
163
+ const transformerId = node[SIGNATURE_KEY];
164
+ const transformer = transformers.get(transformerId);
165
+ if (!transformerId) {
166
+ throw new Error(`No transformer found for node "${node}"`);
167
+ }
168
+ if (transformer === void 0) {
169
+ throw new Error(`No transformer found with id "${transformerId}"`);
170
+ }
171
+ const nonRecursiveTransformer = transformer;
172
+ const dataId = node[DATA_KEY];
173
+ if (dataId === void 0) {
174
+ const decoded = nonRecursiveTransformer.decode(NO_TRANSFORM_DATA);
175
+ revived.set(index, decoded);
176
+ return decoded;
177
+ }
178
+ if (!transformer.recursive) {
179
+ const decoded = nonRecursiveTransformer.decode(reviveNode(dataId));
180
+ revived.set(index, decoded);
181
+ return decoded;
182
+ }
183
+ const registerNode = (node2) => {
184
+ revived.set(index, node2);
185
+ return reviveNode(dataId);
186
+ };
187
+ return transformer.decode(registerNode);
188
+ };
189
+ return reviveNode(0);
190
+ };
191
+
192
+ // src/transformer/Transformer.ts
193
+ var Transformer = class {
194
+ id;
195
+ recursive;
196
+ match;
197
+ encode;
198
+ decode;
199
+ priority;
200
+ static PRIORITY = PRIORITY;
201
+ constructor({
202
+ id,
203
+ match,
204
+ encode,
205
+ decode,
206
+ priority,
207
+ recursive
208
+ }) {
209
+ this.id = id;
210
+ this.recursive = recursive;
211
+ this.match = match;
212
+ this.encode = encode;
213
+ this.decode = decode;
214
+ this.priority = priority || PRIORITY.CUSTOM_CLASS;
215
+ }
216
+ };
217
+
218
+ // src/transformer/builtin.transformers.ts
219
+ var primitivesTransformers = [
220
+ new Transformer({
221
+ id: "sym",
222
+ priority: Transformer.PRIORITY.PRIMITIVE,
223
+ match: (node) => typeof node === "symbol",
224
+ encode: (node) => ({
225
+ d: node.description,
226
+ f: Symbol.keyFor(node) === void 0 ? 0 : 1
227
+ }),
228
+ decode: (encoded) => {
229
+ if (encoded.f === 1 && encoded.d) {
230
+ return Symbol.for(encoded.d);
231
+ }
232
+ return Symbol(encoded.d);
233
+ }
234
+ }),
235
+ new Transformer({
236
+ id: "spe",
237
+ priority: Transformer.PRIORITY.PRIMITIVE,
238
+ match: isSpecialNumber,
239
+ encode: encodeSpecialNumber,
240
+ decode: decodeSpecialNumber
241
+ }),
242
+ new Transformer({
243
+ id: "udf",
244
+ priority: Transformer.PRIORITY.PRIMITIVE,
245
+ match: (node) => node === void 0,
246
+ encode: () => NO_TRANSFORM_DATA,
247
+ decode: () => void 0
248
+ }),
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
+ new Transformer({
257
+ id: "big",
258
+ priority: Transformer.PRIORITY.PRIMITIVE,
259
+ 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
+ obj.push([key, node[key]]);
272
+ }
273
+ return obj;
274
+ },
275
+ decode: (registerNode) => {
276
+ const obj = {};
277
+ const encoded = registerNode(obj);
278
+ for (const [key, value] of encoded) {
279
+ obj[key] = value;
280
+ }
281
+ return obj;
282
+ }
283
+ })
284
+ ];
285
+ var nativeClassesTransformers = [
286
+ new Transformer({
287
+ id: "Dte",
288
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
289
+ match: (node) => node instanceof Date,
290
+ encode: (node) => node.getTime(),
291
+ decode: (node) => new Date(node)
292
+ }),
293
+ new Transformer({
294
+ id: "Set",
295
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
296
+ recursive: true,
297
+ match: (node) => node instanceof Set,
298
+ encode: (node) => Array.from(node),
299
+ decode: (registerNode) => {
300
+ const set = /* @__PURE__ */ new Set();
301
+ const encoded = registerNode(set);
302
+ for (const element of encoded) {
303
+ set.add(element);
304
+ }
305
+ return set;
306
+ }
307
+ }),
308
+ new Transformer({
309
+ id: "Map",
310
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
311
+ recursive: true,
312
+ match: (node) => node instanceof Map,
313
+ encode: (node) => Array.from(node),
314
+ decode: (registerNode) => {
315
+ const map = /* @__PURE__ */ new Map();
316
+ const encoded = registerNode(map);
317
+ for (const [key, value] of encoded) {
318
+ map.set(key, value);
319
+ }
320
+ return map;
321
+ }
322
+ }),
323
+ new Transformer({
324
+ id: "Rgx",
325
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
326
+ match: (node) => node instanceof RegExp,
327
+ encode: (node) => ({ s: node.source, f: node.flags }),
328
+ decode: (node) => new RegExp(node.s, node.f)
329
+ }),
330
+ new Transformer({
331
+ id: "Str",
332
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
333
+ match: (node) => node instanceof String,
334
+ encode: (node) => node.valueOf(),
335
+ decode: (node) => new String(node)
336
+ }),
337
+ new Transformer({
338
+ id: "Num",
339
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
340
+ match: (node) => node instanceof Number,
341
+ encode: (node) => node.valueOf(),
342
+ decode: (node) => new Number(node)
343
+ }),
344
+ new Transformer({
345
+ id: "Bol",
346
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
347
+ match: (node) => node instanceof Boolean,
348
+ encode: (node) => node.valueOf(),
349
+ decode: (node) => new Boolean(node)
350
+ })
351
+ ];
352
+
353
+ // src/transformer/builtin.classTransformer.ts
354
+ var ENCODE = /* @__PURE__ */ Symbol("encode");
355
+ var DECODE = /* @__PURE__ */ Symbol("decode");
356
+ var SYMBOLS = { ENCODE, DECODE };
357
+ var SerializableClass = class {
358
+ constructor(..._) {
359
+ }
360
+ [ENCODE] = () => {
361
+ const encoded = {};
362
+ for (const key of Reflect.ownKeys(this)) {
363
+ const value = this[key];
364
+ if (typeof value !== "function") {
365
+ encoded[key] = this[key];
366
+ }
367
+ }
368
+ return encoded;
369
+ };
370
+ static [DECODE] = function(registerNode) {
371
+ let revivedInstance = {};
372
+ const args = registerNode(revivedInstance);
373
+ Object.assign(revivedInstance, args);
374
+ Object.setPrototypeOf(revivedInstance, this.prototype);
375
+ return revivedInstance;
376
+ };
377
+ };
378
+ var createClassTransformer = (id, clazz) => new Transformer({
379
+ id,
380
+ priority: Transformer.PRIORITY.CUSTOM_CLASS,
381
+ recursive: true,
382
+ match: (node) => node instanceof clazz,
383
+ encode: (node) => node[ENCODE](),
384
+ decode: (registerNode) => clazz[DECODE](registerNode)
385
+ });
386
+
387
+ // src/serialization/Serializer.ts
388
+ var defaultOptions = {
389
+ enable: {
390
+ builtinPrimitiveTransformers: true,
391
+ builtinNativeClasses: true,
392
+ builtinClassTransformer: true
393
+ }
394
+ };
395
+ var Serializer = class {
396
+ transformers = [];
397
+ transformersMap = /* @__PURE__ */ new Map();
398
+ constructor(options = {}) {
399
+ const opt = deepMerge(defaultOptions, options);
400
+ if (opt.enable.builtinPrimitiveTransformers)
401
+ primitivesTransformers.forEach(this.registerTransformer);
402
+ if (opt.enable.builtinNativeClasses)
403
+ nativeClassesTransformers.forEach(this.registerTransformer);
404
+ }
405
+ serialize = (data) => serialize(data, this.transformers);
406
+ deserialize = (data) => deserialize(data, this.transformersMap);
407
+ registerTransformer = (transformer) => {
408
+ if (this.transformersMap.has(transformer.id)) {
409
+ throw new Error(
410
+ `A transformer with id "${transformer.id}" is already registered, chose another one`
411
+ );
412
+ }
413
+ this.transformers.push(transformer);
414
+ this.transformers = this.transformers.sort(
415
+ (a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity)
416
+ );
417
+ this.transformersMap.set(transformer.id, transformer);
418
+ };
419
+ deregisterTransformer = (transformer) => {
420
+ const id = typeof transformer === "object" ? transformer.id : transformer;
421
+ if (!this.transformersMap.has(id)) {
422
+ throw new Error(
423
+ `No transformer found for id "${id}", cannot deregister it`
424
+ );
425
+ }
426
+ this.transformers = this.transformers.filter(
427
+ (transformer2) => transformer2.id !== id
428
+ );
429
+ this.transformersMap.delete(id);
430
+ };
431
+ registerClass = (name, clazz) => {
432
+ this.registerTransformer(createClassTransformer("$" + name, clazz));
433
+ };
434
+ deregisterClass = (name) => {
435
+ this.deregisterTransformer("$" + name);
436
+ };
437
+ };
@@ -0,0 +1,117 @@
1
+ declare namespace Seriall {
2
+ export type Serializable =
3
+ | JsonPrimitive
4
+ | symbol
5
+ | bigint
6
+ | undefined
7
+ | null
8
+ | object
9
+ | Serializable[];
10
+
11
+ export type JsonPrimitive = string | number | boolean;
12
+
13
+ export namespace Serialized {
14
+ export type RevivableNode = { $: Transformer.Id; d?: number };
15
+ export type Node = JsonPrimitive | number[] | RevivableNode;
16
+ export type Graph = Node[];
17
+ }
18
+
19
+ namespace Transformer {
20
+ /** `NoData` is used to signify that no value should be passed to the Revivable node,
21
+ * which is then only described by its tag ($)*/
22
+ type NoData = undefined;
23
+
24
+ type Encoded<Recursive extends boolean> = Recursive extends false
25
+ ? JsonPrimitive | object | Encoded<false>[] | NoData
26
+ : (JsonPrimitive | Encoded<true>)[] | object;
27
+
28
+ type Id = string | number;
29
+
30
+ type Matcher<T extends Serializable> = (node: T) => boolean;
31
+
32
+ type Encoder<
33
+ T extends Serializable,
34
+ O extends Encoded<Recursive>,
35
+ Recursive extends boolean,
36
+ > = (node: T) => O;
37
+
38
+ type Decoder<
39
+ T extends Encoded<Recursive>,
40
+ O extends Serializable,
41
+ Recursive extends boolean,
42
+ > = Recursive extends false
43
+ ? (node: T) => O
44
+ : (registerNode: (emptyNode: O) => T) => O;
45
+ }
46
+
47
+ export type Transformer<
48
+ Recursive extends boolean = boolean,
49
+ Decoded extends Serializable = Serializable,
50
+ Encoded extends Transformer.Encoded<Recursive> =
51
+ Transformer.Encoded<Recursive>,
52
+ > = {
53
+ id: Transformer.Id;
54
+ priority?: number;
55
+ recursive?: Recursive | undefined;
56
+ match: Transformer.Matcher<Decoded>;
57
+ encode: Transformer.Encoder<Decoded, Encoded, Recursive>;
58
+ decode: Transformer.Decoder<Encoded, Decoded, Recursive>;
59
+ };
60
+ }
61
+
62
+ type DeepOptional<T> = {
63
+ [K in keyof T]?: T[K] extends object ? DeepOptional<T[K]> : T[K];
64
+ };
65
+
66
+ declare class Transformer<Decoded extends Seriall.Serializable, Encoded extends Seriall.Transformer.Encoded<Options["recursive"]>, Options extends {
67
+ recursive: boolean;
68
+ } = {
69
+ recursive: false;
70
+ }> implements Seriall.Transformer<Options["recursive"], Decoded, Encoded> {
71
+ id: Seriall.Transformer.Id;
72
+ recursive?: Options["recursive"] | undefined;
73
+ match: Seriall.Transformer.Matcher<Seriall.Serializable>;
74
+ encode: Seriall.Transformer.Encoder<Decoded, Encoded, Options["recursive"]>;
75
+ decode: Seriall.Transformer.Decoder<Encoded, Decoded, Options["recursive"]>;
76
+ priority: number;
77
+ static PRIORITY: {
78
+ readonly CUSTOM_CLASS: 0;
79
+ readonly NATIVE_CLASS: 1;
80
+ readonly PRIMITIVE: 2;
81
+ readonly custom: (priority: number) => number;
82
+ };
83
+ constructor({ id, match, encode, decode, priority, recursive, }: Seriall.Transformer<Options["recursive"], Decoded, Encoded>);
84
+ }
85
+
86
+ declare const ENCODE: unique symbol;
87
+ declare const DECODE: unique symbol;
88
+ declare const SYMBOLS: {
89
+ ENCODE: symbol;
90
+ DECODE: symbol;
91
+ };
92
+ declare abstract class SerializableClass {
93
+ constructor(..._: any);
94
+ [ENCODE]: () => {};
95
+ static [DECODE]: <T extends typeof SerializableClass>(this: T, registerNode: Parameters<Seriall.Transformer.Decoder<any, any, true>>[0]) => InstanceType<T>;
96
+ }
97
+
98
+ type SerializerOptions = {
99
+ enable: {
100
+ builtinPrimitiveTransformers: boolean;
101
+ builtinNativeClasses: boolean;
102
+ builtinClassTransformer: boolean;
103
+ };
104
+ };
105
+ declare class Serializer {
106
+ private transformers;
107
+ private transformersMap;
108
+ constructor(options?: DeepOptional<SerializerOptions>);
109
+ serialize: (data: Seriall.Serializable) => string;
110
+ deserialize: (data: string) => any;
111
+ registerTransformer: (transformer: Seriall.Transformer) => void;
112
+ deregisterTransformer: (transformer: Seriall.Transformer | Seriall.Transformer.Id) => void;
113
+ registerClass: (name: string, clazz: typeof SerializableClass) => void;
114
+ deregisterClass: (name: string) => void;
115
+ }
116
+
117
+ export { SYMBOLS, SerializableClass, Serializer, Seriall, Transformer };
@@ -0,0 +1,117 @@
1
+ declare namespace Seriall {
2
+ export type Serializable =
3
+ | JsonPrimitive
4
+ | symbol
5
+ | bigint
6
+ | undefined
7
+ | null
8
+ | object
9
+ | Serializable[];
10
+
11
+ export type JsonPrimitive = string | number | boolean;
12
+
13
+ export namespace Serialized {
14
+ export type RevivableNode = { $: Transformer.Id; d?: number };
15
+ export type Node = JsonPrimitive | number[] | RevivableNode;
16
+ export type Graph = Node[];
17
+ }
18
+
19
+ namespace Transformer {
20
+ /** `NoData` is used to signify that no value should be passed to the Revivable node,
21
+ * which is then only described by its tag ($)*/
22
+ type NoData = undefined;
23
+
24
+ type Encoded<Recursive extends boolean> = Recursive extends false
25
+ ? JsonPrimitive | object | Encoded<false>[] | NoData
26
+ : (JsonPrimitive | Encoded<true>)[] | object;
27
+
28
+ type Id = string | number;
29
+
30
+ type Matcher<T extends Serializable> = (node: T) => boolean;
31
+
32
+ type Encoder<
33
+ T extends Serializable,
34
+ O extends Encoded<Recursive>,
35
+ Recursive extends boolean,
36
+ > = (node: T) => O;
37
+
38
+ type Decoder<
39
+ T extends Encoded<Recursive>,
40
+ O extends Serializable,
41
+ Recursive extends boolean,
42
+ > = Recursive extends false
43
+ ? (node: T) => O
44
+ : (registerNode: (emptyNode: O) => T) => O;
45
+ }
46
+
47
+ export type Transformer<
48
+ Recursive extends boolean = boolean,
49
+ Decoded extends Serializable = Serializable,
50
+ Encoded extends Transformer.Encoded<Recursive> =
51
+ Transformer.Encoded<Recursive>,
52
+ > = {
53
+ id: Transformer.Id;
54
+ priority?: number;
55
+ recursive?: Recursive | undefined;
56
+ match: Transformer.Matcher<Decoded>;
57
+ encode: Transformer.Encoder<Decoded, Encoded, Recursive>;
58
+ decode: Transformer.Decoder<Encoded, Decoded, Recursive>;
59
+ };
60
+ }
61
+
62
+ type DeepOptional<T> = {
63
+ [K in keyof T]?: T[K] extends object ? DeepOptional<T[K]> : T[K];
64
+ };
65
+
66
+ declare class Transformer<Decoded extends Seriall.Serializable, Encoded extends Seriall.Transformer.Encoded<Options["recursive"]>, Options extends {
67
+ recursive: boolean;
68
+ } = {
69
+ recursive: false;
70
+ }> implements Seriall.Transformer<Options["recursive"], Decoded, Encoded> {
71
+ id: Seriall.Transformer.Id;
72
+ recursive?: Options["recursive"] | undefined;
73
+ match: Seriall.Transformer.Matcher<Seriall.Serializable>;
74
+ encode: Seriall.Transformer.Encoder<Decoded, Encoded, Options["recursive"]>;
75
+ decode: Seriall.Transformer.Decoder<Encoded, Decoded, Options["recursive"]>;
76
+ priority: number;
77
+ static PRIORITY: {
78
+ readonly CUSTOM_CLASS: 0;
79
+ readonly NATIVE_CLASS: 1;
80
+ readonly PRIMITIVE: 2;
81
+ readonly custom: (priority: number) => number;
82
+ };
83
+ constructor({ id, match, encode, decode, priority, recursive, }: Seriall.Transformer<Options["recursive"], Decoded, Encoded>);
84
+ }
85
+
86
+ declare const ENCODE: unique symbol;
87
+ declare const DECODE: unique symbol;
88
+ declare const SYMBOLS: {
89
+ ENCODE: symbol;
90
+ DECODE: symbol;
91
+ };
92
+ declare abstract class SerializableClass {
93
+ constructor(..._: any);
94
+ [ENCODE]: () => {};
95
+ static [DECODE]: <T extends typeof SerializableClass>(this: T, registerNode: Parameters<Seriall.Transformer.Decoder<any, any, true>>[0]) => InstanceType<T>;
96
+ }
97
+
98
+ type SerializerOptions = {
99
+ enable: {
100
+ builtinPrimitiveTransformers: boolean;
101
+ builtinNativeClasses: boolean;
102
+ builtinClassTransformer: boolean;
103
+ };
104
+ };
105
+ declare class Serializer {
106
+ private transformers;
107
+ private transformersMap;
108
+ constructor(options?: DeepOptional<SerializerOptions>);
109
+ serialize: (data: Seriall.Serializable) => string;
110
+ deserialize: (data: string) => any;
111
+ registerTransformer: (transformer: Seriall.Transformer) => void;
112
+ deregisterTransformer: (transformer: Seriall.Transformer | Seriall.Transformer.Id) => void;
113
+ registerClass: (name: string, clazz: typeof SerializableClass) => void;
114
+ deregisterClass: (name: string) => void;
115
+ }
116
+
117
+ export { SYMBOLS, SerializableClass, Serializer, Seriall, Transformer };
package/dist/index.mjs ADDED
@@ -0,0 +1,415 @@
1
+ // src/const/const.ts
2
+ var SIGNATURE_KEY = "$";
3
+ var DATA_KEY = "d";
4
+ var NO_TRANSFORM_DATA = void 0;
5
+ var PRIORITY = {
6
+ CUSTOM_CLASS: 0,
7
+ NATIVE_CLASS: 1,
8
+ PRIMITIVE: 2,
9
+ custom: (priority) => (priority <= 0 ? 1 : priority) + PRIORITY.CUSTOM_CLASS
10
+ };
11
+
12
+ // src/utils/number.utils.ts
13
+ var isSpecialNumber = (value) => {
14
+ return [Infinity, -Infinity, NaN].includes(value) || Object.is(value, -0);
15
+ };
16
+ 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;
21
+ throw new Error(value + " is not a special number");
22
+ };
23
+ var decodeSpecialNumber = (value) => {
24
+ switch (value) {
25
+ case 0:
26
+ return NaN;
27
+ case 1:
28
+ return Infinity;
29
+ case 2:
30
+ return -Infinity;
31
+ case 3:
32
+ return -0;
33
+ default:
34
+ throw new Error(value + " is not a special number");
35
+ }
36
+ };
37
+
38
+ // src/utils/json.utils.ts
39
+ var isJsonPrimitive = (value) => {
40
+ if (typeof value === "number") {
41
+ return !isSpecialNumber(value);
42
+ }
43
+ return typeof value === "string" || typeof value === "boolean";
44
+ };
45
+
46
+ // src/utils/object.utils.ts
47
+ var isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
48
+ var deepMerge = (...objects) => {
49
+ const result = {};
50
+ for (const obj of objects) {
51
+ if (!isPlainObject(obj)) continue;
52
+ for (const [key, value] of Object.entries(obj)) {
53
+ if (Array.isArray(value)) {
54
+ result[key] = structuredClone(value);
55
+ } else if (isPlainObject(value)) {
56
+ const existing = result[key];
57
+ result[key] = deepMerge(isPlainObject(existing) ? existing : {}, value);
58
+ } else {
59
+ result[key] = value;
60
+ }
61
+ }
62
+ }
63
+ return result;
64
+ };
65
+
66
+ // src/serialization/serialize.ts
67
+ var serialize = (data, transformers) => {
68
+ const graph = [];
69
+ const seen = /* @__PURE__ */ new Map();
70
+ const addNodeToGraph = (node) => {
71
+ if (isJsonPrimitive(node)) {
72
+ const id2 = graph.length;
73
+ graph.push(node);
74
+ return id2;
75
+ }
76
+ let id = seen.get(node);
77
+ if (id !== void 0) {
78
+ return id;
79
+ }
80
+ id = graph.length;
81
+ seen.set(node, id);
82
+ const transformer = transformers.find(
83
+ (transformer2) => transformer2.match(node)
84
+ );
85
+ if (transformer) {
86
+ graph.push({ $: transformer.id });
87
+ const revivable = graph[id];
88
+ const encoded = transformer.encode(node);
89
+ if (encoded !== NO_TRANSFORM_DATA) {
90
+ const dataId = addNodeToGraph(encoded);
91
+ revivable.d = dataId;
92
+ }
93
+ return id;
94
+ }
95
+ if (Array.isArray(node)) {
96
+ graph.push([]);
97
+ const arr = graph[id];
98
+ for (const element of node) {
99
+ const elementId = addNodeToGraph(element);
100
+ arr.push(elementId);
101
+ }
102
+ return id;
103
+ }
104
+ throw new Error(
105
+ `No transformer found for the current value "${node?.toString()}", please add one`
106
+ );
107
+ };
108
+ addNodeToGraph(data);
109
+ return JSON.stringify(graph);
110
+ };
111
+
112
+ // src/serialization/deserialize.ts
113
+ var deserialize = (data, transformers) => {
114
+ const graph = JSON.parse(data);
115
+ const revived = /* @__PURE__ */ new Map();
116
+ const reviveNode = (index) => {
117
+ if (revived.has(index)) {
118
+ return revived.get(index);
119
+ }
120
+ if (graph.length <= index || index < 0) {
121
+ throw new Error(`Invalid serialized graph reference: ${index}`);
122
+ }
123
+ const node = graph[index];
124
+ if (isJsonPrimitive(node)) {
125
+ return node;
126
+ }
127
+ if (Array.isArray(node)) {
128
+ const arr = [];
129
+ revived.set(index, arr);
130
+ for (const element of node) {
131
+ arr.push(reviveNode(element));
132
+ }
133
+ return arr;
134
+ }
135
+ const transformerId = node[SIGNATURE_KEY];
136
+ const transformer = transformers.get(transformerId);
137
+ if (!transformerId) {
138
+ throw new Error(`No transformer found for node "${node}"`);
139
+ }
140
+ if (transformer === void 0) {
141
+ throw new Error(`No transformer found with id "${transformerId}"`);
142
+ }
143
+ const nonRecursiveTransformer = transformer;
144
+ const dataId = node[DATA_KEY];
145
+ if (dataId === void 0) {
146
+ const decoded = nonRecursiveTransformer.decode(NO_TRANSFORM_DATA);
147
+ revived.set(index, decoded);
148
+ return decoded;
149
+ }
150
+ if (!transformer.recursive) {
151
+ const decoded = nonRecursiveTransformer.decode(reviveNode(dataId));
152
+ revived.set(index, decoded);
153
+ return decoded;
154
+ }
155
+ const registerNode = (node2) => {
156
+ revived.set(index, node2);
157
+ return reviveNode(dataId);
158
+ };
159
+ return transformer.decode(registerNode);
160
+ };
161
+ return reviveNode(0);
162
+ };
163
+
164
+ // src/transformer/Transformer.ts
165
+ var Transformer = class {
166
+ id;
167
+ recursive;
168
+ match;
169
+ encode;
170
+ decode;
171
+ priority;
172
+ static PRIORITY = PRIORITY;
173
+ constructor({
174
+ id,
175
+ match,
176
+ encode,
177
+ decode,
178
+ priority,
179
+ recursive
180
+ }) {
181
+ this.id = id;
182
+ this.recursive = recursive;
183
+ this.match = match;
184
+ this.encode = encode;
185
+ this.decode = decode;
186
+ this.priority = priority || PRIORITY.CUSTOM_CLASS;
187
+ }
188
+ };
189
+
190
+ // src/transformer/builtin.transformers.ts
191
+ var primitivesTransformers = [
192
+ new Transformer({
193
+ id: "sym",
194
+ priority: Transformer.PRIORITY.PRIMITIVE,
195
+ match: (node) => typeof node === "symbol",
196
+ encode: (node) => ({
197
+ d: node.description,
198
+ f: Symbol.keyFor(node) === void 0 ? 0 : 1
199
+ }),
200
+ decode: (encoded) => {
201
+ if (encoded.f === 1 && encoded.d) {
202
+ return Symbol.for(encoded.d);
203
+ }
204
+ return Symbol(encoded.d);
205
+ }
206
+ }),
207
+ new Transformer({
208
+ id: "spe",
209
+ priority: Transformer.PRIORITY.PRIMITIVE,
210
+ match: isSpecialNumber,
211
+ encode: encodeSpecialNumber,
212
+ decode: decodeSpecialNumber
213
+ }),
214
+ new Transformer({
215
+ id: "udf",
216
+ priority: Transformer.PRIORITY.PRIMITIVE,
217
+ match: (node) => node === void 0,
218
+ encode: () => NO_TRANSFORM_DATA,
219
+ decode: () => void 0
220
+ }),
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
+ new Transformer({
229
+ id: "big",
230
+ priority: Transformer.PRIORITY.PRIMITIVE,
231
+ 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
+ obj.push([key, node[key]]);
244
+ }
245
+ return obj;
246
+ },
247
+ decode: (registerNode) => {
248
+ const obj = {};
249
+ const encoded = registerNode(obj);
250
+ for (const [key, value] of encoded) {
251
+ obj[key] = value;
252
+ }
253
+ return obj;
254
+ }
255
+ })
256
+ ];
257
+ var nativeClassesTransformers = [
258
+ new Transformer({
259
+ id: "Dte",
260
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
261
+ match: (node) => node instanceof Date,
262
+ encode: (node) => node.getTime(),
263
+ decode: (node) => new Date(node)
264
+ }),
265
+ new Transformer({
266
+ id: "Set",
267
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
268
+ recursive: true,
269
+ match: (node) => node instanceof Set,
270
+ encode: (node) => Array.from(node),
271
+ decode: (registerNode) => {
272
+ const set = /* @__PURE__ */ new Set();
273
+ const encoded = registerNode(set);
274
+ for (const element of encoded) {
275
+ set.add(element);
276
+ }
277
+ return set;
278
+ }
279
+ }),
280
+ new Transformer({
281
+ id: "Map",
282
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
283
+ recursive: true,
284
+ match: (node) => node instanceof Map,
285
+ encode: (node) => Array.from(node),
286
+ decode: (registerNode) => {
287
+ const map = /* @__PURE__ */ new Map();
288
+ const encoded = registerNode(map);
289
+ for (const [key, value] of encoded) {
290
+ map.set(key, value);
291
+ }
292
+ return map;
293
+ }
294
+ }),
295
+ new Transformer({
296
+ id: "Rgx",
297
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
298
+ match: (node) => node instanceof RegExp,
299
+ encode: (node) => ({ s: node.source, f: node.flags }),
300
+ decode: (node) => new RegExp(node.s, node.f)
301
+ }),
302
+ new Transformer({
303
+ id: "Str",
304
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
305
+ match: (node) => node instanceof String,
306
+ encode: (node) => node.valueOf(),
307
+ decode: (node) => new String(node)
308
+ }),
309
+ new Transformer({
310
+ id: "Num",
311
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
312
+ match: (node) => node instanceof Number,
313
+ encode: (node) => node.valueOf(),
314
+ decode: (node) => new Number(node)
315
+ }),
316
+ new Transformer({
317
+ id: "Bol",
318
+ priority: Transformer.PRIORITY.NATIVE_CLASS,
319
+ match: (node) => node instanceof Boolean,
320
+ encode: (node) => node.valueOf(),
321
+ decode: (node) => new Boolean(node)
322
+ })
323
+ ];
324
+
325
+ // src/transformer/builtin.classTransformer.ts
326
+ var ENCODE = /* @__PURE__ */ Symbol("encode");
327
+ var DECODE = /* @__PURE__ */ Symbol("decode");
328
+ var SYMBOLS = { ENCODE, DECODE };
329
+ var SerializableClass = class {
330
+ constructor(..._) {
331
+ }
332
+ [ENCODE] = () => {
333
+ const encoded = {};
334
+ for (const key of Reflect.ownKeys(this)) {
335
+ const value = this[key];
336
+ if (typeof value !== "function") {
337
+ encoded[key] = this[key];
338
+ }
339
+ }
340
+ return encoded;
341
+ };
342
+ static [DECODE] = function(registerNode) {
343
+ let revivedInstance = {};
344
+ const args = registerNode(revivedInstance);
345
+ Object.assign(revivedInstance, args);
346
+ Object.setPrototypeOf(revivedInstance, this.prototype);
347
+ return revivedInstance;
348
+ };
349
+ };
350
+ var createClassTransformer = (id, clazz) => new Transformer({
351
+ id,
352
+ priority: Transformer.PRIORITY.CUSTOM_CLASS,
353
+ recursive: true,
354
+ match: (node) => node instanceof clazz,
355
+ encode: (node) => node[ENCODE](),
356
+ decode: (registerNode) => clazz[DECODE](registerNode)
357
+ });
358
+
359
+ // src/serialization/Serializer.ts
360
+ var defaultOptions = {
361
+ enable: {
362
+ builtinPrimitiveTransformers: true,
363
+ builtinNativeClasses: true,
364
+ builtinClassTransformer: true
365
+ }
366
+ };
367
+ var Serializer = class {
368
+ transformers = [];
369
+ transformersMap = /* @__PURE__ */ new Map();
370
+ constructor(options = {}) {
371
+ const opt = deepMerge(defaultOptions, options);
372
+ if (opt.enable.builtinPrimitiveTransformers)
373
+ primitivesTransformers.forEach(this.registerTransformer);
374
+ if (opt.enable.builtinNativeClasses)
375
+ nativeClassesTransformers.forEach(this.registerTransformer);
376
+ }
377
+ serialize = (data) => serialize(data, this.transformers);
378
+ deserialize = (data) => deserialize(data, this.transformersMap);
379
+ registerTransformer = (transformer) => {
380
+ if (this.transformersMap.has(transformer.id)) {
381
+ throw new Error(
382
+ `A transformer with id "${transformer.id}" is already registered, chose another one`
383
+ );
384
+ }
385
+ this.transformers.push(transformer);
386
+ this.transformers = this.transformers.sort(
387
+ (a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity)
388
+ );
389
+ this.transformersMap.set(transformer.id, transformer);
390
+ };
391
+ deregisterTransformer = (transformer) => {
392
+ const id = typeof transformer === "object" ? transformer.id : transformer;
393
+ if (!this.transformersMap.has(id)) {
394
+ throw new Error(
395
+ `No transformer found for id "${id}", cannot deregister it`
396
+ );
397
+ }
398
+ this.transformers = this.transformers.filter(
399
+ (transformer2) => transformer2.id !== id
400
+ );
401
+ this.transformersMap.delete(id);
402
+ };
403
+ registerClass = (name, clazz) => {
404
+ this.registerTransformer(createClassTransformer("$" + name, clazz));
405
+ };
406
+ deregisterClass = (name) => {
407
+ this.deregisterTransformer("$" + name);
408
+ };
409
+ };
410
+ export {
411
+ SYMBOLS,
412
+ SerializableClass,
413
+ Serializer,
414
+ Transformer
415
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "seriall",
3
+ "version": "1.0.0",
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
+ "author": {
6
+ "email": "loic.duvail.pro@gmail.com",
7
+ "name": "loicd"
8
+ },
9
+ "type": "module",
10
+ "exports": {
11
+ "import": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "rm -rf ./dist && npm run build:js && npm run build:types",
25
+ "build:js": "node ./esbuild.js",
26
+ "build:types": "rollup -c",
27
+ "build:test": "node ./esbuild.test.js",
28
+ "test": "npm run build:test && npx jest"
29
+ },
30
+ "keywords": [
31
+ "serializer",
32
+ "serialization",
33
+ "circular",
34
+ "json",
35
+ "referential",
36
+ "integrity"
37
+ ],
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/loicDuvail/seriall.git"
42
+ },
43
+ "devDependencies": {
44
+ "@types/jest": "^30.0.0",
45
+ "@types/node": "^26.5.1",
46
+ "esbuild": "^0.28.2",
47
+ "jest": "^30.5.1",
48
+ "rollup": "^4.63.6",
49
+ "rollup-plugin-dts": "^6.5.1",
50
+ "typescript": "^6.0.3"
51
+ }
52
+ }