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