seroval 0.2.0 → 0.3.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/src/tree.ts ADDED
@@ -0,0 +1,963 @@
1
+ /* eslint-disable guard-for-in */
2
+ /* eslint-disable @typescript-eslint/no-for-in-array */
3
+ /* eslint-disable no-await-in-loop */
4
+ import {
5
+ isIterable,
6
+ isPrimitive,
7
+ isPromise,
8
+ constructorCheck,
9
+ } from './checks';
10
+ import quote from './quote';
11
+ import {
12
+ AsyncServerValue,
13
+ ErrorValue,
14
+ PrimitiveValue,
15
+ ServerValue,
16
+ TypedArrayValue,
17
+ } from './types';
18
+ import getIdentifier from './get-identifier';
19
+ import serializePrimitive from './serialize-primitive';
20
+
21
+ interface IndexAssignment {
22
+ type: 'index';
23
+ source: string;
24
+ value: string;
25
+ }
26
+
27
+ interface MapAssignment {
28
+ type: 'map';
29
+ source: string;
30
+ key: string;
31
+ value: string;
32
+ }
33
+
34
+ interface SetAssignment {
35
+ type: 'set';
36
+ source: string;
37
+ value: string;
38
+ }
39
+
40
+ // Array of assignments to be done (used for recursion)
41
+ export type Assignment =
42
+ | IndexAssignment
43
+ | MapAssignment
44
+ | SetAssignment;
45
+
46
+ export interface SerializationContext {
47
+ stack: number[];
48
+ // Value-to-ref map
49
+ refs: Map<unknown, number>;
50
+ // Map tree refs to actual refs
51
+ validRefs: Map<number, number>;
52
+ // Refs that are...referenced
53
+ markedRefs: boolean[];
54
+ // Variables
55
+ vars: string[];
56
+ // Array of assignments to be done (used for recursion)
57
+ assignments: Assignment[];
58
+ }
59
+
60
+ export function createSerializationContext(): SerializationContext {
61
+ return {
62
+ markedRefs: [],
63
+ stack: [],
64
+ refs: new Map(),
65
+ vars: [],
66
+ assignments: [],
67
+ validRefs: new Map(),
68
+ };
69
+ }
70
+
71
+ function getAssignmentExpression(assignment: Assignment): string {
72
+ switch (assignment.type) {
73
+ case 'index':
74
+ return `${assignment.source}=${assignment.value}`;
75
+ case 'map':
76
+ return `${assignment.source}.set(${assignment.key},${assignment.value})`;
77
+ case 'set':
78
+ return `${assignment.source}.add(${assignment.value})`;
79
+ default:
80
+ return '';
81
+ }
82
+ }
83
+
84
+ function mergeAssignments(ctx: SerializationContext) {
85
+ const newAssignments = [];
86
+ let current = ctx.assignments[0];
87
+ let prev = current;
88
+ for (let i = 1, len = ctx.assignments.length; i < len; i += 1) {
89
+ const item = ctx.assignments[i];
90
+ if (item.type === prev.type) {
91
+ if (item.type === 'index' && item.value === prev.value) {
92
+ // Merge if the right-hand value is the same
93
+ // saves at least 2 chars
94
+ current = {
95
+ type: 'index',
96
+ source: item.source,
97
+ value: getAssignmentExpression(current),
98
+ };
99
+ } else if (item.type === 'map' && item.source === prev.source) {
100
+ // Maps has chaining methods, merge if source is the same
101
+ current = {
102
+ type: 'map',
103
+ source: getAssignmentExpression(current),
104
+ key: item.key,
105
+ value: item.value,
106
+ };
107
+ } else if (item.type === 'set' && item.source === prev.source) {
108
+ // Sets has chaining methods too
109
+ current = {
110
+ type: 'set',
111
+ source: getAssignmentExpression(current),
112
+ value: item.value,
113
+ };
114
+ } else {
115
+ // Different assignment, push current
116
+ newAssignments.push(current);
117
+ current = item;
118
+ }
119
+ } else {
120
+ newAssignments.push(current);
121
+ current = item;
122
+ }
123
+ prev = item;
124
+ }
125
+
126
+ newAssignments.push(current);
127
+
128
+ return newAssignments;
129
+ }
130
+
131
+ export function resolvePatches(ctx: SerializationContext) {
132
+ if (ctx.assignments.length) {
133
+ let result = '';
134
+ for (const assignment of mergeAssignments(ctx)) {
135
+ result += `${getAssignmentExpression(assignment)},`;
136
+ }
137
+ return result;
138
+ }
139
+ return '';
140
+ }
141
+
142
+ /**
143
+ * Increments the number of references the referenced value has
144
+ */
145
+ function markRef(ctx: SerializationContext, current: number) {
146
+ ctx.markedRefs[current] = true;
147
+ }
148
+
149
+ /**
150
+ * Creates a reference ID from the given values
151
+ */
152
+ export function createRef(
153
+ ctx: SerializationContext,
154
+ index: unknown,
155
+ ) {
156
+ const current = ctx.refs.get(index);
157
+ if (current != null) {
158
+ return current;
159
+ }
160
+ const id = ctx.refs.size;
161
+ ctx.refs.set(index, id);
162
+ return id;
163
+ }
164
+
165
+ /**
166
+ * Creates a new reference ID from a given reference ID
167
+ * This new reference ID means that the reference itself
168
+ * has been referenced at least once, and is used to generate
169
+ * the variables
170
+ */
171
+ function createValidRef(
172
+ ctx: SerializationContext,
173
+ index: number,
174
+ ) {
175
+ const current = ctx.validRefs.get(index);
176
+ if (current != null) {
177
+ return current;
178
+ }
179
+ const id = ctx.validRefs.size;
180
+ ctx.validRefs.set(index, id);
181
+ return id;
182
+ }
183
+
184
+ /**
185
+ * Creates the reference param (identifier) from the given reference ID
186
+ * Calling this function means the value has been referenced somewhere
187
+ */
188
+ export function getRefParam(ctx: SerializationContext, index: number) {
189
+ const actualIndex = createValidRef(ctx, index);
190
+ if (ctx.vars[actualIndex]) {
191
+ return ctx.vars[actualIndex];
192
+ }
193
+ const result = getIdentifier(actualIndex);
194
+ ctx.vars[actualIndex] = result;
195
+ return result;
196
+ }
197
+
198
+ /**
199
+ * Generates the inlined assignment for the reference
200
+ * This is different from the assignments array as this one
201
+ * signifies creation rather than mutation
202
+ */
203
+ function assignRef(
204
+ ctx: SerializationContext,
205
+ index: number,
206
+ value: string,
207
+ ) {
208
+ if (ctx.markedRefs[index]) {
209
+ return `${getRefParam(ctx, index)}=${value}`;
210
+ }
211
+ return value;
212
+ }
213
+
214
+ function createAssignment(
215
+ ctx: SerializationContext,
216
+ source: string,
217
+ value: string,
218
+ ) {
219
+ ctx.assignments.push({
220
+ type: 'index',
221
+ source,
222
+ value,
223
+ });
224
+ }
225
+
226
+ function createSetAdd(
227
+ ctx: SerializationContext,
228
+ ref: number,
229
+ value: string,
230
+ ) {
231
+ markRef(ctx, ref);
232
+ ctx.assignments.push({
233
+ type: 'set',
234
+ source: getRefParam(ctx, ref),
235
+ value,
236
+ });
237
+ }
238
+
239
+ function createMapSet(
240
+ ctx: SerializationContext,
241
+ ref: number,
242
+ key: string,
243
+ value: string,
244
+ ) {
245
+ markRef(ctx, ref);
246
+ ctx.assignments.push({
247
+ type: 'map',
248
+ source: getRefParam(ctx, ref),
249
+ key,
250
+ value,
251
+ });
252
+ }
253
+
254
+ function createArrayAssign(
255
+ ctx: SerializationContext,
256
+ ref: number,
257
+ index: number,
258
+ value: string,
259
+ ) {
260
+ markRef(ctx, ref);
261
+ createAssignment(ctx, `${getRefParam(ctx, ref)}[${index}]`, value);
262
+ }
263
+
264
+ function createObjectIdentifierAssign(
265
+ ctx: SerializationContext,
266
+ ref: number,
267
+ key: string,
268
+ value: string,
269
+ ) {
270
+ markRef(ctx, ref);
271
+ createAssignment(ctx, `${getRefParam(ctx, ref)}.${key}`, value);
272
+ }
273
+
274
+ function createObjectComputedAssign(
275
+ ctx: SerializationContext,
276
+ ref: number,
277
+ key: string,
278
+ value: string,
279
+ ) {
280
+ markRef(ctx, ref);
281
+ createAssignment(ctx, `${getRefParam(ctx, ref)}[${key}]`, value);
282
+ }
283
+
284
+ function getErrorConstructor(error: ErrorValue) {
285
+ if (error instanceof EvalError) {
286
+ return 'EvalError';
287
+ }
288
+ if (error instanceof RangeError) {
289
+ return 'RangeError';
290
+ }
291
+ if (error instanceof ReferenceError) {
292
+ return 'ReferenceError';
293
+ }
294
+ if (error instanceof SyntaxError) {
295
+ return 'SyntaxError';
296
+ }
297
+ if (error instanceof TypeError) {
298
+ return 'TypeError';
299
+ }
300
+ if (error instanceof URIError) {
301
+ return 'URIError';
302
+ }
303
+ return 'Error';
304
+ }
305
+
306
+ const EXCLUDED_ERROR_KEYS = {
307
+ name: true,
308
+ cause: true,
309
+ stack: true,
310
+ message: true,
311
+ };
312
+
313
+ function getErrorOptions(error: Error) {
314
+ let options: Record<string, any> | undefined;
315
+ const constructor = getErrorConstructor(error);
316
+ // Name has been modified
317
+ if (error.name !== constructor) {
318
+ options = { name: error.name };
319
+ } else if (error.constructor.name !== constructor) {
320
+ // Otherwise, name is overriden because
321
+ // the Error class is extended
322
+ options = { name: error.constructor.name };
323
+ }
324
+ const names = Object.getOwnPropertyNames(error);
325
+ for (const name of names) {
326
+ if (!(name in EXCLUDED_ERROR_KEYS)) {
327
+ options = options || {};
328
+ options[name] = error[name as keyof Error];
329
+ }
330
+ }
331
+ return options;
332
+ }
333
+
334
+ function getIterableOptions(obj: Iterable<any>) {
335
+ const names = Object.getOwnPropertyNames(obj);
336
+ if (names.length) {
337
+ const options: Record<string, unknown> = {};
338
+ for (const name of names) {
339
+ options[name] = obj[name as unknown as keyof typeof obj];
340
+ }
341
+ return options;
342
+ }
343
+ return undefined;
344
+ }
345
+
346
+ const enum SerovalNodeType {
347
+ Primitive,
348
+ Reference,
349
+ Date,
350
+ RegExp,
351
+ Set,
352
+ Map,
353
+ Array,
354
+ Object,
355
+ NullConstructor,
356
+ Promise,
357
+ Error,
358
+ AggregateError,
359
+ Iterable,
360
+ TypedArray,
361
+ }
362
+
363
+ type SerovalPrimitiveNode = [type: SerovalNodeType.Primitive, value: PrimitiveValue];
364
+ type SerovalReferenceNode = [type: SerovalNodeType.Reference, value: number];
365
+ type SerovalSemiPrimitiveNode =
366
+ | [type: SerovalNodeType.Date, value: Date, id: number]
367
+ | [type: SerovalNodeType.RegExp, value: RegExp, id: number]
368
+ | [
369
+ type: SerovalNodeType.TypedArray,
370
+ value: { constructor: string, array: TypedArrayValue },
371
+ id: number
372
+ ];
373
+
374
+ type SerovalNode =
375
+ | SerovalPrimitiveNode
376
+ | SerovalReferenceNode
377
+ | SerovalSemiPrimitiveNode
378
+ | [type: SerovalNodeType.Set, value: SerovalNode[], id: number]
379
+ | [type: SerovalNodeType.Map, value: [key: SerovalNode, value: SerovalNode][], id: number]
380
+ | [type: SerovalNodeType.Array, value: SerovalNode[], id: number]
381
+ | [type: SerovalNodeType.Object, value: Record<string, SerovalNode>, id: number]
382
+ | [type: SerovalNodeType.NullConstructor, value: Record<string, SerovalNode>, id: number]
383
+ | [type: SerovalNodeType.Promise, value: SerovalNode, id: number]
384
+ | [
385
+ type: SerovalNodeType.Error,
386
+ value: {
387
+ constructor: string;
388
+ message: string;
389
+ options?: SerovalNode;
390
+ cause?: SerovalNode;
391
+ },
392
+ id: number
393
+ ]
394
+ | [
395
+ type: SerovalNodeType.AggregateError,
396
+ value: {
397
+ message: string;
398
+ options?: SerovalNode;
399
+ cause?: SerovalNode;
400
+ errors: SerovalNode;
401
+ },
402
+ id: number
403
+ ]
404
+ | [
405
+ type: SerovalNodeType.Iterable,
406
+ value: {
407
+ items: SerovalNode,
408
+ options?: SerovalNode,
409
+ },
410
+ id: number,
411
+ ];
412
+
413
+ function isReferenceInStack(
414
+ ctx: SerializationContext,
415
+ node: SerovalNode,
416
+ ): node is SerovalReferenceNode {
417
+ return node[0] === SerovalNodeType.Reference && ctx.stack.includes(node[1]);
418
+ }
419
+
420
+ function generateRef(
421
+ ctx: SerializationContext,
422
+ current: unknown,
423
+ ): number | SerovalReferenceNode {
424
+ // Check if reference number already exists
425
+ const ref = ctx.refs.has(current);
426
+ if (ref) {
427
+ // Exists, means this value is currently
428
+ // being referenced
429
+ const refID = ctx.refs.get(current) || 0;
430
+ // Mark reference
431
+ markRef(ctx, refID);
432
+ return [SerovalNodeType.Reference, refID];
433
+ }
434
+ // Create a new reference ID
435
+ const id = ctx.refs.size;
436
+ ctx.refs.set(current, id);
437
+ return id;
438
+ }
439
+
440
+ function generateSemiPrimitiveValue(
441
+ current: unknown,
442
+ id: number,
443
+ ): SerovalNode | undefined {
444
+ if (constructorCheck<Date>(current, Date)) {
445
+ return [SerovalNodeType.Date, current, id];
446
+ }
447
+ if (constructorCheck<RegExp>(current, RegExp)) {
448
+ return [SerovalNodeType.RegExp, current, id];
449
+ }
450
+ if (constructorCheck<Int8Array>(current, Int8Array)) {
451
+ return [SerovalNodeType.TypedArray, { constructor: 'Int8Array', array: current }, id];
452
+ }
453
+ if (constructorCheck<Int16Array>(current, Int16Array)) {
454
+ return [SerovalNodeType.TypedArray, { constructor: 'Int16Array', array: current }, id];
455
+ }
456
+ if (constructorCheck<Int32Array>(current, Int32Array)) {
457
+ return [SerovalNodeType.TypedArray, { constructor: 'Int32Array', array: current }, id];
458
+ }
459
+ if (constructorCheck<Uint8Array>(current, Uint8Array)) {
460
+ return [SerovalNodeType.TypedArray, { constructor: 'Uint8Array', array: current }, id];
461
+ }
462
+ if (constructorCheck<Uint16Array>(current, Uint16Array)) {
463
+ return [SerovalNodeType.TypedArray, { constructor: 'Uint16Array', array: current }, id];
464
+ }
465
+ if (constructorCheck<Uint32Array>(current, Uint32Array)) {
466
+ return [SerovalNodeType.TypedArray, { constructor: 'Uint32Array', array: current }, id];
467
+ }
468
+ if (constructorCheck<Uint8ClampedArray>(current, Uint8ClampedArray)) {
469
+ return [SerovalNodeType.TypedArray, { constructor: 'Uint8ClampedArray', array: current }, id];
470
+ }
471
+ if (constructorCheck<Float32Array>(current, Float32Array)) {
472
+ return [SerovalNodeType.TypedArray, { constructor: 'Float32Array', array: current }, id];
473
+ }
474
+ if (constructorCheck<Float64Array>(current, Float64Array)) {
475
+ return [SerovalNodeType.TypedArray, { constructor: 'Float64Array', array: current }, id];
476
+ }
477
+ if (constructorCheck<BigInt64Array>(current, BigInt64Array)) {
478
+ return [SerovalNodeType.TypedArray, { constructor: 'BigInt64Array', array: current }, id];
479
+ }
480
+ if (constructorCheck<BigUint64Array>(current, BigUint64Array)) {
481
+ return [SerovalNodeType.TypedArray, { constructor: 'BigUint64Array', array: current }, id];
482
+ }
483
+ return undefined;
484
+ }
485
+
486
+ export function generateTreeSync(
487
+ ctx: SerializationContext,
488
+ current: ServerValue,
489
+ ): SerovalNode {
490
+ if (isPrimitive(current)) {
491
+ return [SerovalNodeType.Primitive, current];
492
+ }
493
+ // Non-primitive values needs a reference ID
494
+ // mostly because the values themselves are stateful
495
+ const id = generateRef(ctx, current);
496
+ if (Array.isArray(id)) {
497
+ return id;
498
+ }
499
+ const semiPrimitive = generateSemiPrimitiveValue(current, id);
500
+ if (semiPrimitive) {
501
+ return semiPrimitive;
502
+ }
503
+ if (constructorCheck<Set<ServerValue>>(current, Set)) {
504
+ const nodes: SerovalNode[] = [];
505
+ const deferred: ServerValue[] = [];
506
+ for (const item of current.keys()) {
507
+ // Iterables are lazy, so the evaluation must be deferred
508
+ if (isIterable(item)) {
509
+ deferred.push(item);
510
+ } else {
511
+ nodes.push(generateTreeSync(ctx, item));
512
+ }
513
+ }
514
+ // Parse deferred items
515
+ for (const item of deferred) {
516
+ nodes.push(generateTreeSync(ctx, item));
517
+ }
518
+ return [SerovalNodeType.Set, nodes, id];
519
+ }
520
+ if (constructorCheck<Map<ServerValue, ServerValue>>(current, Map)) {
521
+ const nodes: [key: SerovalNode, value: SerovalNode][] = [];
522
+ const deferred: [ServerValue, ServerValue][] = [];
523
+ for (const [key, value] of current.entries()) {
524
+ // Either key or value might be an iterable
525
+ if (isIterable(key) || isIterable(value)) {
526
+ deferred.push([key, value]);
527
+ } else {
528
+ const keyNode = generateTreeSync(ctx, key);
529
+ const valueNode = generateTreeSync(ctx, value);
530
+ nodes.push([keyNode, valueNode]);
531
+ }
532
+ }
533
+ for (const [key, value] of deferred) {
534
+ const keyNode = generateTreeSync(ctx, key);
535
+ const valueNode = generateTreeSync(ctx, value);
536
+ nodes.push([keyNode, valueNode]);
537
+ }
538
+ return [SerovalNodeType.Map, nodes, id];
539
+ }
540
+ if (Array.isArray(current)) {
541
+ const nodes = new Array<SerovalNode>(current.length);
542
+ const deferred = new Array<ServerValue>(current.length);
543
+ for (const key in current) {
544
+ const item = current[key];
545
+ if (isIterable(item)) {
546
+ deferred[key] = item;
547
+ } else {
548
+ nodes[key] = generateTreeSync(ctx, item);
549
+ }
550
+ }
551
+ for (const key in deferred) {
552
+ nodes[key] = generateTreeSync(ctx, deferred[key]);
553
+ }
554
+ return [SerovalNodeType.Array, nodes, id];
555
+ }
556
+ if (current instanceof AggregateError) {
557
+ const options = getErrorOptions(current);
558
+ return [SerovalNodeType.AggregateError, {
559
+ message: current.message,
560
+ options: options
561
+ ? generateTreeSync(ctx, options)
562
+ : undefined,
563
+ cause: 'cause' in current
564
+ ? generateTreeSync(ctx, { cause: current.cause as ServerValue })
565
+ : undefined,
566
+ errors: generateTreeSync(ctx, current.errors as ServerValue),
567
+ }, id];
568
+ }
569
+ if (current instanceof Error) {
570
+ const options = getErrorOptions(current);
571
+ return [SerovalNodeType.Error, {
572
+ constructor: getErrorConstructor(current),
573
+ message: current.message,
574
+ options: options ? generateTreeSync(ctx, options) : undefined,
575
+ cause: 'cause' in current
576
+ ? generateTreeSync(ctx, { cause: current.cause as ServerValue })
577
+ : undefined,
578
+ }, id];
579
+ }
580
+ if (isIterable(current)) {
581
+ const options = getIterableOptions(current);
582
+ return [SerovalNodeType.Iterable, {
583
+ // Parse options first before the items
584
+ options: options
585
+ ? generateTreeSync(ctx, options as ServerValue)
586
+ : undefined,
587
+ items: generateTreeSync(ctx, Array.from(current)),
588
+ }, id];
589
+ }
590
+ const empty = current.constructor == null;
591
+ if (current.constructor === Object || empty) {
592
+ const nodes: Record<string, SerovalNode> = {};
593
+ const deferred: Record<string, ServerValue> = {};
594
+ for (const [key, item] of Object.entries(current)) {
595
+ if (isIterable(item)) {
596
+ deferred[key] = item as ServerValue;
597
+ } else {
598
+ nodes[key] = generateTreeSync(ctx, item as ServerValue);
599
+ }
600
+ }
601
+ for (const [key, item] of Object.entries(deferred)) {
602
+ nodes[key] = generateTreeSync(ctx, item);
603
+ }
604
+ return [
605
+ empty ? SerovalNodeType.NullConstructor : SerovalNodeType.Object,
606
+ nodes,
607
+ id,
608
+ ];
609
+ }
610
+ throw new Error('Unsupported value');
611
+ }
612
+
613
+ export async function generateTreeAsync(
614
+ ctx: SerializationContext,
615
+ current: AsyncServerValue,
616
+ ): Promise<SerovalNode> {
617
+ if (isPrimitive(current)) {
618
+ return [SerovalNodeType.Primitive, current];
619
+ }
620
+ const id = generateRef(ctx, current);
621
+ if (Array.isArray(id)) {
622
+ return id;
623
+ }
624
+ const semiPrimitive = generateSemiPrimitiveValue(current, id);
625
+ if (semiPrimitive) {
626
+ return semiPrimitive;
627
+ }
628
+ if (isPromise(current)) {
629
+ return current.then(async (value) => [
630
+ SerovalNodeType.Promise,
631
+ await generateTreeAsync(ctx, value),
632
+ id,
633
+ ]);
634
+ }
635
+ if (constructorCheck<Set<ServerValue>>(current, Set)) {
636
+ const nodes: SerovalNode[] = [];
637
+ const deferred: ServerValue[] = [];
638
+ for (const item of current.keys()) {
639
+ if (isIterable(item)) {
640
+ deferred.push(item);
641
+ } else {
642
+ nodes.push(await generateTreeAsync(ctx, item));
643
+ }
644
+ }
645
+ for (const item of deferred) {
646
+ nodes.push(await generateTreeAsync(ctx, item));
647
+ }
648
+ return [SerovalNodeType.Set, nodes, id];
649
+ }
650
+ if (constructorCheck<Map<ServerValue, ServerValue>>(current, Map)) {
651
+ const nodes: [key: SerovalNode, value: SerovalNode][] = [];
652
+ const deferred: [ServerValue, ServerValue][] = [];
653
+ for (const [key, value] of current.entries()) {
654
+ if (isIterable(key) || isIterable(value)) {
655
+ deferred.push([key, value]);
656
+ } else {
657
+ const keyNode = await generateTreeAsync(ctx, key);
658
+ const valueNode = await generateTreeAsync(ctx, value);
659
+ nodes.push([keyNode, valueNode]);
660
+ }
661
+ }
662
+ for (const [key, value] of deferred) {
663
+ const keyNode = await generateTreeAsync(ctx, key);
664
+ const valueNode = await generateTreeAsync(ctx, value);
665
+ nodes.push([keyNode, valueNode]);
666
+ }
667
+ return [SerovalNodeType.Map, nodes, id];
668
+ }
669
+ if (Array.isArray(current)) {
670
+ const nodes = new Array<SerovalNode>(current.length);
671
+ const deferred = new Array<AsyncServerValue>(current.length);
672
+ for (const key in current) {
673
+ const item = current[key];
674
+ if (isIterable(item)) {
675
+ deferred[key] = item;
676
+ } else {
677
+ nodes[key] = await generateTreeAsync(ctx, item);
678
+ }
679
+ }
680
+ for (const key in deferred) {
681
+ nodes[key] = await generateTreeAsync(ctx, deferred[key]);
682
+ }
683
+ return [SerovalNodeType.Array, nodes, id];
684
+ }
685
+ if (current instanceof AggregateError) {
686
+ const options = getErrorOptions(current);
687
+ return [SerovalNodeType.AggregateError, {
688
+ message: current.message,
689
+ options: options
690
+ ? generateTreeSync(ctx, options)
691
+ : undefined,
692
+ cause: 'cause' in current
693
+ ? await generateTreeAsync(ctx, { cause: current.cause as ServerValue })
694
+ : undefined,
695
+ errors: await generateTreeAsync(ctx, current.errors as ServerValue),
696
+ }, id];
697
+ }
698
+ if (current instanceof Error) {
699
+ const options = getErrorOptions(current);
700
+ return [SerovalNodeType.Error, {
701
+ constructor: getErrorConstructor(current),
702
+ message: current.message,
703
+ options: options ? await generateTreeAsync(ctx, options) : undefined,
704
+ cause: 'cause' in current
705
+ ? await generateTreeAsync(ctx, { cause: current.cause as ServerValue })
706
+ : undefined,
707
+ }, id];
708
+ }
709
+ if (isIterable(current)) {
710
+ const options = getIterableOptions(current);
711
+ return [SerovalNodeType.Iterable, {
712
+ options: options
713
+ ? await generateTreeAsync(ctx, options as ServerValue)
714
+ : undefined,
715
+ items: await generateTreeAsync(ctx, Array.from(current)),
716
+ }, id];
717
+ }
718
+ const empty = current.constructor == null;
719
+ if (current.constructor === Object || empty) {
720
+ const nodes: Record<string, SerovalNode> = {};
721
+ const deferred: Record<string, AsyncServerValue> = {};
722
+ for (const [key, item] of Object.entries(current)) {
723
+ if (isIterable(item)) {
724
+ deferred[key] = item as ServerValue;
725
+ } else {
726
+ nodes[key] = await generateTreeAsync(ctx, item as ServerValue);
727
+ }
728
+ }
729
+ for (const [key, item] of Object.entries(deferred)) {
730
+ nodes[key] = await generateTreeAsync(ctx, item);
731
+ }
732
+ return [
733
+ empty ? SerovalNodeType.NullConstructor : SerovalNodeType.Object,
734
+ nodes,
735
+ id,
736
+ ];
737
+ }
738
+ throw new Error('Unsupported value');
739
+ }
740
+
741
+ export function serializeTree(
742
+ ctx: SerializationContext,
743
+ [type, value, id]: SerovalNode,
744
+ ): string {
745
+ switch (type) {
746
+ case SerovalNodeType.Primitive:
747
+ return serializePrimitive(value);
748
+ case SerovalNodeType.Reference:
749
+ return getRefParam(ctx, value);
750
+ case SerovalNodeType.Promise: {
751
+ let serialized: string;
752
+ // Check if resolved value is a parent expression
753
+ if (isReferenceInStack(ctx, value)) {
754
+ // A Promise trick, reference the value
755
+ // inside the `then` expression so that
756
+ // the Promise evaluates after the parent
757
+ // has initialized
758
+ serialized = `Promise.resolve().then(()=>${getRefParam(ctx, value[1])})`;
759
+ } else {
760
+ ctx.stack.push(id);
761
+ const result = serializeTree(ctx, value);
762
+ ctx.stack.pop();
763
+ // just inline the value/reference here
764
+ serialized = `Promise.resolve(${result})`;
765
+ }
766
+ return assignRef(ctx, id, serialized);
767
+ }
768
+ case SerovalNodeType.Date:
769
+ return assignRef(ctx, id, `new Date("${value.toISOString()}")`);
770
+ case SerovalNodeType.RegExp:
771
+ return assignRef(ctx, id, String(value));
772
+ case SerovalNodeType.TypedArray: {
773
+ // BigInt typed arrays are broken for toString()
774
+ const values = [...value.array].map(serializePrimitive).join(',');
775
+ let args = `[${values}]`;
776
+ if (value.array.byteOffset !== 0) {
777
+ args += `,${value.array.byteOffset}`;
778
+ }
779
+ return assignRef(ctx, id, `new ${value.constructor}(${args})`);
780
+ }
781
+ case SerovalNodeType.Set: {
782
+ let serialized = 'new Set';
783
+ if (value.length) {
784
+ const values: string[] = [];
785
+ ctx.stack.push(id);
786
+ for (const item of value) {
787
+ if (isReferenceInStack(ctx, item)) {
788
+ createSetAdd(ctx, id, getRefParam(ctx, item[1]));
789
+ } else {
790
+ // Push directly
791
+ values.push(serializeTree(ctx, item));
792
+ }
793
+ }
794
+ ctx.stack.pop();
795
+ if (values.length) {
796
+ serialized += `([${values.join(',')}])`;
797
+ }
798
+ }
799
+ return assignRef(ctx, id, serialized);
800
+ }
801
+ case SerovalNodeType.Map: {
802
+ let serialized = 'new Map';
803
+ if (value.length) {
804
+ const values: string[] = [];
805
+ ctx.stack.push(id);
806
+ for (const [key, val] of value) {
807
+ // Check if key is a parent
808
+ if (isReferenceInStack(ctx, key)) {
809
+ // Create reference for the map instance
810
+ const keyRef = getRefParam(ctx, key[1]);
811
+ // Check if value is a parent
812
+ if (isReferenceInStack(ctx, val)) {
813
+ const valueRef = getRefParam(ctx, val[1]);
814
+ // Register an assignment since
815
+ // both key and value are a parent of this
816
+ // Map instance
817
+ createMapSet(ctx, id, keyRef, valueRef);
818
+ } else {
819
+ // Reset the stack
820
+ // This is required because the serialized
821
+ // value is no longer part of the expression
822
+ // tree and has been moved to the deferred
823
+ // assignment
824
+ const parent = ctx.stack;
825
+ ctx.stack = [];
826
+ createMapSet(ctx, id, keyRef, serializeTree(ctx, val));
827
+ ctx.stack = parent;
828
+ }
829
+ } else if (isReferenceInStack(ctx, val)) {
830
+ // Create ref for the Map instance
831
+ const valueRef = getRefParam(ctx, val[1]);
832
+ // Reset stack for the key serialization
833
+ const parent = ctx.stack;
834
+ ctx.stack = [];
835
+ createMapSet(ctx, id, serializeTree(ctx, key), valueRef);
836
+ ctx.stack = parent;
837
+ } else {
838
+ values.push(`[${serializeTree(ctx, key)},${serializeTree(ctx, val)}]`);
839
+ }
840
+ }
841
+ ctx.stack.pop();
842
+ // Check if there are any values
843
+ // so that the empty Map constructor
844
+ // can be used instead
845
+ if (values.length) {
846
+ serialized += `([${values.join(',')}])`;
847
+ }
848
+ }
849
+ return assignRef(ctx, id, serialized);
850
+ }
851
+ case SerovalNodeType.Array: {
852
+ // This is different than Map and Set
853
+ // because we also need to serialize
854
+ // the holes of the Array
855
+ let values = '';
856
+
857
+ ctx.stack.push(id);
858
+ for (let i = 0, len = value.length; i < len; i++) {
859
+ const item = value[i];
860
+ // Check if index is a hole
861
+ if (i in value) {
862
+ // Check if item is a parent
863
+ if (isReferenceInStack(ctx, item)) {
864
+ createArrayAssign(ctx, id, i, getRefParam(ctx, item[1]));
865
+ values += ',';
866
+ } else {
867
+ values += serializeTree(ctx, item);
868
+ if (i < value.length - 1) {
869
+ values += ',';
870
+ }
871
+ }
872
+ } else {
873
+ // Add an empty item
874
+ values += ',';
875
+ }
876
+ }
877
+ ctx.stack.pop();
878
+ return assignRef(ctx, id, `[${values}]`);
879
+ }
880
+ case SerovalNodeType.AggregateError: {
881
+ // Serialize the required arguments
882
+ const args = [serializeTree(ctx, value.errors), quote(value.message)];
883
+ // cause is an optional argument
884
+ if (value.cause) {
885
+ args.push(serializeTree(ctx, value.cause));
886
+ }
887
+ let serialized = `new AggregateError(${args.join(',')})`;
888
+ // `AggregateError` might've been extended
889
+ // either through class or custom properties
890
+ // Make sure to assign extra properties
891
+ if (value.options) {
892
+ const options = serializeTree(ctx, value.options);
893
+ serialized = `Object.assign(${serialized},${options})`;
894
+ }
895
+ return assignRef(ctx, id, serialized);
896
+ }
897
+ case SerovalNodeType.Error: {
898
+ const args = [quote(value.message)];
899
+ if (value.cause) {
900
+ args.push(serializeTree(ctx, value.cause));
901
+ }
902
+ let serialized = `new ${value.constructor}(${args.join(',')})`;
903
+ if (value.options) {
904
+ const options = serializeTree(ctx, value.options);
905
+ serialized = `Object.assign(${serialized},${options})`;
906
+ }
907
+ return assignRef(ctx, id, serialized);
908
+ }
909
+ case SerovalNodeType.Iterable: {
910
+ const parent = ctx.stack;
911
+ ctx.stack = [];
912
+ const values = serializeTree(ctx, value.items);
913
+ ctx.stack = parent;
914
+ let serialized = `{[Symbol.iterator]:()=>${values}.values()}`;
915
+ if (value.options) {
916
+ ctx.stack.push(id);
917
+ const options = serializeTree(ctx, value.options);
918
+ ctx.stack.pop();
919
+ serialized = `Object.assign(${serialized},${options})`;
920
+ }
921
+ return assignRef(ctx, id, serialized);
922
+ }
923
+ case SerovalNodeType.NullConstructor:
924
+ case SerovalNodeType.Object: {
925
+ const values: string[] = [];
926
+
927
+ ctx.stack.push(id);
928
+ for (const [key, val] of Object.entries(value)) {
929
+ const check = Number(key);
930
+ // Test if key is a valid number or JS identifier
931
+ // so that we don't have to serialize the key and wrap with brackets
932
+ if (
933
+ check >= 0
934
+ || (Number.isNaN(check) && /^([$A-Z_][0-9A-Z_$]*)$/i.test(key))
935
+ ) {
936
+ if (isReferenceInStack(ctx, val)) {
937
+ const refParam = getRefParam(ctx, val[1]);
938
+ if (!Number.isNaN(check)) {
939
+ createObjectComputedAssign(ctx, id, key, refParam);
940
+ } else {
941
+ createObjectIdentifierAssign(ctx, id, key, refParam);
942
+ }
943
+ } else {
944
+ values.push(`${key}:${serializeTree(ctx, val)}`);
945
+ }
946
+ } else if (isReferenceInStack(ctx, val)) {
947
+ const refParam = getRefParam(ctx, val[1]);
948
+ createObjectComputedAssign(ctx, id, quote(key), refParam);
949
+ } else {
950
+ values.push(`${quote(key)}:${serializeTree(ctx, val)}`);
951
+ }
952
+ }
953
+ ctx.stack.pop();
954
+ let serialized = `{${values.join(',')}}`;
955
+ if (type === SerovalNodeType.NullConstructor) {
956
+ serialized = `Object.assign(Object.create(null),${serialized})`;
957
+ }
958
+ return assignRef(ctx, id, serialized);
959
+ }
960
+ default:
961
+ throw new Error('Unsupported type');
962
+ }
963
+ }