seroval 0.3.0 → 0.4.0-alpha.1

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 CHANGED
@@ -16,7 +16,8 @@ import {
16
16
  TypedArrayValue,
17
17
  } from './types';
18
18
  import getIdentifier from './get-identifier';
19
- import serializePrimitive from './serialize-primitive';
19
+ import { Feature, parseTargets } from './compat';
20
+ import assert from './assert';
20
21
 
21
22
  interface IndexAssignment {
22
23
  type: 'index';
@@ -55,9 +56,21 @@ export interface SerializationContext {
55
56
  vars: string[];
56
57
  // Array of assignments to be done (used for recursion)
57
58
  assignments: Assignment[];
59
+ // Supported features
60
+ features: Set<Feature>;
58
61
  }
59
62
 
60
- export function createSerializationContext(): SerializationContext {
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 || {});
61
74
  return {
62
75
  markedRefs: [],
63
76
  stack: [],
@@ -65,6 +78,7 @@ export function createSerializationContext(): SerializationContext {
65
78
  vars: [],
66
79
  assignments: [],
67
80
  validRefs: new Map(),
81
+ features: parseTargets(result.target),
68
82
  };
69
83
  }
70
84
 
@@ -81,12 +95,12 @@ function getAssignmentExpression(assignment: Assignment): string {
81
95
  }
82
96
  }
83
97
 
84
- function mergeAssignments(ctx: SerializationContext) {
98
+ function mergeAssignments(assignments: Assignment[]) {
85
99
  const newAssignments = [];
86
- let current = ctx.assignments[0];
100
+ let current = assignments[0];
87
101
  let prev = current;
88
- for (let i = 1, len = ctx.assignments.length; i < len; i += 1) {
89
- const item = ctx.assignments[i];
102
+ for (let i = 1, len = assignments.length; i < len; i++) {
103
+ const item = assignments[i];
90
104
  if (item.type === prev.type) {
91
105
  if (item.type === 'index' && item.value === prev.value) {
92
106
  // Merge if the right-hand value is the same
@@ -128,15 +142,20 @@ function mergeAssignments(ctx: SerializationContext) {
128
142
  return newAssignments;
129
143
  }
130
144
 
131
- export function resolvePatches(ctx: SerializationContext) {
132
- if (ctx.assignments.length) {
145
+ function resolveAssignments(assignments: Assignment[]) {
146
+ if (assignments.length) {
133
147
  let result = '';
134
- for (const assignment of mergeAssignments(ctx)) {
135
- result += `${getAssignmentExpression(assignment)},`;
148
+ const merged = mergeAssignments(assignments);
149
+ for (let i = 0, len = merged.length; i < len; i++) {
150
+ result += `${getAssignmentExpression(merged[i])},`;
136
151
  }
137
152
  return result;
138
153
  }
139
- return '';
154
+ return undefined;
155
+ }
156
+
157
+ export function resolvePatches(ctx: SerializationContext) {
158
+ return resolveAssignments(ctx.assignments);
140
159
  }
141
160
 
142
161
  /**
@@ -303,14 +322,9 @@ function getErrorConstructor(error: ErrorValue) {
303
322
  return 'Error';
304
323
  }
305
324
 
306
- const EXCLUDED_ERROR_KEYS = {
307
- name: true,
308
- cause: true,
309
- stack: true,
310
- message: true,
311
- };
312
-
313
- function getErrorOptions(error: Error) {
325
+ function getErrorOptions(
326
+ error: Error,
327
+ ) {
314
328
  let options: Record<string, any> | undefined;
315
329
  const constructor = getErrorConstructor(error);
316
330
  // Name has been modified
@@ -323,7 +337,7 @@ function getErrorOptions(error: Error) {
323
337
  }
324
338
  const names = Object.getOwnPropertyNames(error);
325
339
  for (const name of names) {
326
- if (!(name in EXCLUDED_ERROR_KEYS)) {
340
+ if (name !== 'name' && name !== 'message') {
327
341
  options = options || {};
328
342
  options[name] = error[name as keyof Error];
329
343
  }
@@ -367,48 +381,66 @@ type SerovalSemiPrimitiveNode =
367
381
  | [type: SerovalNodeType.RegExp, value: RegExp, id: number]
368
382
  | [
369
383
  type: SerovalNodeType.TypedArray,
370
- value: { constructor: string, array: TypedArrayValue },
384
+ value: [constructor: string, array: TypedArrayValue],
371
385
  id: number
372
386
  ];
373
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
+
374
431
  type SerovalNode =
375
432
  | SerovalPrimitiveNode
376
433
  | SerovalReferenceNode
377
434
  | 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
- ];
435
+ | SerovalSetNode
436
+ | SerovalMapNode
437
+ | SerovalArrayNode
438
+ | SerovalObjectNode
439
+ | SerovalNullConstructorNode
440
+ | SerovalPromiseNode
441
+ | SerovalErrorNode
442
+ | SerovalAggregateErrorNode
443
+ | SerovalIterableNode;
412
444
 
413
445
  function isReferenceInStack(
414
446
  ctx: SerializationContext,
@@ -438,6 +470,7 @@ function generateRef(
438
470
  }
439
471
 
440
472
  function generateSemiPrimitiveValue(
473
+ ctx: SerializationContext,
441
474
  current: unknown,
442
475
  id: number,
443
476
  ): SerovalNode | undefined {
@@ -448,37 +481,56 @@ function generateSemiPrimitiveValue(
448
481
  return [SerovalNodeType.RegExp, current, id];
449
482
  }
450
483
  if (constructorCheck<Int8Array>(current, Int8Array)) {
451
- return [SerovalNodeType.TypedArray, { constructor: 'Int8Array', array: current }, id];
484
+ assert(ctx.features.has(Feature.TypedArray), 'Unsupported value type "Int8Array"');
485
+ return [SerovalNodeType.TypedArray, ['Int8Array', current], id];
452
486
  }
453
487
  if (constructorCheck<Int16Array>(current, Int16Array)) {
454
- return [SerovalNodeType.TypedArray, { constructor: 'Int16Array', array: current }, id];
488
+ assert(ctx.features.has(Feature.TypedArray), 'Unsupported value type "Int16Array"');
489
+ return [SerovalNodeType.TypedArray, ['Int16Array', current], id];
455
490
  }
456
491
  if (constructorCheck<Int32Array>(current, Int32Array)) {
457
- return [SerovalNodeType.TypedArray, { constructor: 'Int32Array', array: current }, id];
492
+ assert(ctx.features.has(Feature.TypedArray), 'Unsupported value type "Int32Array"');
493
+ return [SerovalNodeType.TypedArray, ['Int32Array', current], id];
458
494
  }
459
495
  if (constructorCheck<Uint8Array>(current, Uint8Array)) {
460
- return [SerovalNodeType.TypedArray, { constructor: 'Uint8Array', array: current }, id];
496
+ assert(ctx.features.has(Feature.TypedArray), 'Unsupported value type "Uint8Array"');
497
+ return [SerovalNodeType.TypedArray, ['Uint8Array', current], id];
461
498
  }
462
499
  if (constructorCheck<Uint16Array>(current, Uint16Array)) {
463
- return [SerovalNodeType.TypedArray, { constructor: 'Uint16Array', array: current }, id];
500
+ assert(ctx.features.has(Feature.TypedArray), 'Unsupported value type "Uint16Array"');
501
+ return [SerovalNodeType.TypedArray, ['Uint16Array', current], id];
464
502
  }
465
503
  if (constructorCheck<Uint32Array>(current, Uint32Array)) {
466
- return [SerovalNodeType.TypedArray, { constructor: 'Uint32Array', array: current }, id];
504
+ assert(ctx.features.has(Feature.TypedArray), 'Unsupported value type "Uint32Array"');
505
+ return [SerovalNodeType.TypedArray, ['Uint32Array', current], id];
467
506
  }
468
507
  if (constructorCheck<Uint8ClampedArray>(current, Uint8ClampedArray)) {
469
- return [SerovalNodeType.TypedArray, { constructor: 'Uint8ClampedArray', array: current }, id];
508
+ assert(ctx.features.has(Feature.TypedArray), 'Unsupported value type "Uint8ClampedArray"');
509
+ return [SerovalNodeType.TypedArray, ['Uint8ClampedArray', current], id];
470
510
  }
471
511
  if (constructorCheck<Float32Array>(current, Float32Array)) {
472
- return [SerovalNodeType.TypedArray, { constructor: 'Float32Array', array: current }, id];
512
+ assert(ctx.features.has(Feature.TypedArray), 'Unsupported value type "Float32Array"');
513
+ return [SerovalNodeType.TypedArray, ['Float32Array', current], id];
473
514
  }
474
515
  if (constructorCheck<Float64Array>(current, Float64Array)) {
475
- return [SerovalNodeType.TypedArray, { constructor: 'Float64Array', array: current }, id];
516
+ assert(ctx.features.has(Feature.TypedArray), 'Unsupported value type "Float64Array"');
517
+ return [SerovalNodeType.TypedArray, ['Float64Array', current], id];
476
518
  }
477
519
  if (constructorCheck<BigInt64Array>(current, BigInt64Array)) {
478
- return [SerovalNodeType.TypedArray, { constructor: 'BigInt64Array', array: current }, id];
520
+ assert(
521
+ ctx.features.has(Feature.TypedArray)
522
+ && ctx.features.has(Feature.BigInt),
523
+ 'Unsupported value type "BigInt64Array"',
524
+ );
525
+ return [SerovalNodeType.TypedArray, ['BigInt64Array', current], id];
479
526
  }
480
527
  if (constructorCheck<BigUint64Array>(current, BigUint64Array)) {
481
- return [SerovalNodeType.TypedArray, { constructor: 'BigUint64Array', array: current }, id];
528
+ assert(
529
+ ctx.features.has(Feature.TypedArray)
530
+ && ctx.features.has(Feature.BigInt),
531
+ 'Unsupported value type "BigUint64Array"',
532
+ );
533
+ return [SerovalNodeType.TypedArray, ['BigUint64Array', current], id];
482
534
  }
483
535
  return undefined;
484
536
  }
@@ -487,6 +539,31 @@ export function generateTreeSync(
487
539
  ctx: SerializationContext,
488
540
  current: ServerValue,
489
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
+ }
490
567
  if (isPrimitive(current)) {
491
568
  return [SerovalNodeType.Primitive, current];
492
569
  }
@@ -496,11 +573,12 @@ export function generateTreeSync(
496
573
  if (Array.isArray(id)) {
497
574
  return id;
498
575
  }
499
- const semiPrimitive = generateSemiPrimitiveValue(current, id);
576
+ const semiPrimitive = generateSemiPrimitiveValue(ctx, current, id);
500
577
  if (semiPrimitive) {
501
578
  return semiPrimitive;
502
579
  }
503
580
  if (constructorCheck<Set<ServerValue>>(current, Set)) {
581
+ assert(ctx.features.has(Feature.Set), 'Unsupported type "Set"');
504
582
  const nodes: SerovalNode[] = [];
505
583
  const deferred: ServerValue[] = [];
506
584
  for (const item of current.keys()) {
@@ -512,98 +590,88 @@ export function generateTreeSync(
512
590
  }
513
591
  }
514
592
  // Parse deferred items
515
- for (const item of deferred) {
516
- nodes.push(generateTreeSync(ctx, item));
593
+ for (let i = 0, len = deferred.length; i < len; i++) {
594
+ nodes.push(generateTreeSync(ctx, deferred[i]));
517
595
  }
518
596
  return [SerovalNodeType.Set, nodes, id];
519
597
  }
520
598
  if (constructorCheck<Map<ServerValue, ServerValue>>(current, Map)) {
521
- const nodes: [key: SerovalNode, value: SerovalNode][] = [];
522
- const deferred: [ServerValue, ServerValue][] = [];
599
+ assert(ctx.features.has(Feature.Map), 'Unsupported type "Map"');
600
+ const keyNodes: SerovalNode[] = [];
601
+ const valueNodes: SerovalNode[] = [];
602
+ const deferredKey: ServerValue[] = [];
603
+ const deferredValue: ServerValue[] = [];
604
+ let deferredSize = 0;
523
605
  for (const [key, value] of current.entries()) {
524
606
  // Either key or value might be an iterable
525
607
  if (isIterable(key) || isIterable(value)) {
526
- deferred.push([key, value]);
608
+ deferredKey.push(key);
609
+ deferredValue.push(value);
610
+ deferredSize++;
527
611
  } else {
528
- const keyNode = generateTreeSync(ctx, key);
529
- const valueNode = generateTreeSync(ctx, value);
530
- nodes.push([keyNode, valueNode]);
612
+ keyNodes.push(generateTreeSync(ctx, key));
613
+ valueNodes.push(generateTreeSync(ctx, value));
531
614
  }
532
615
  }
533
- for (const [key, value] of deferred) {
534
- const keyNode = generateTreeSync(ctx, key);
535
- const valueNode = generateTreeSync(ctx, value);
536
- nodes.push([keyNode, valueNode]);
616
+ for (let i = 0; i < deferredSize; i++) {
617
+ keyNodes.push(generateTreeSync(ctx, deferredKey[i]));
618
+ valueNodes.push(generateTreeSync(ctx, deferredValue[i]));
537
619
  }
538
- return [SerovalNodeType.Map, nodes, id];
620
+ return [SerovalNodeType.Map, [keyNodes, valueNodes, current.size], id];
539
621
  }
540
622
  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);
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
+ }
549
633
  }
550
634
  }
551
- for (const key in deferred) {
552
- nodes[key] = generateTreeSync(ctx, deferred[key]);
635
+ for (let i = 0; i < size; i++) {
636
+ if (i in deferred) {
637
+ nodes[i] = generateTreeSync(ctx, deferred[i]);
638
+ }
553
639
  }
554
640
  return [SerovalNodeType.Array, nodes, id];
555
641
  }
556
- if (current instanceof AggregateError) {
642
+ if (current instanceof AggregateError && ctx.features.has(Feature.AggregateError)) {
557
643
  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];
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];
568
649
  }
569
650
  if (current instanceof Error) {
570
651
  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];
652
+ const optionsNode = options
653
+ ? serializeProperties(options)
654
+ : undefined;
655
+ return [
656
+ SerovalNodeType.Error,
657
+ [getErrorConstructor(current), current.message, optionsNode],
658
+ id,
659
+ ];
579
660
  }
580
661
  if (isIterable(current)) {
662
+ assert(ctx.features.has(Feature.SymbolIterator), 'Unsupported type "Iterable"');
581
663
  const options = getIterableOptions(current);
582
- return [SerovalNodeType.Iterable, {
664
+ return [SerovalNodeType.Iterable, [
583
665
  // 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];
666
+ options ? serializeProperties(options) : undefined,
667
+ generateTreeSync(ctx, Array.from(current)),
668
+ ], id];
589
669
  }
590
670
  const empty = current.constructor == null;
591
671
  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
672
  return [
605
673
  empty ? SerovalNodeType.NullConstructor : SerovalNodeType.Object,
606
- nodes,
674
+ serializeProperties(current as Record<string, unknown>),
607
675
  id,
608
676
  ];
609
677
  }
@@ -614,6 +682,31 @@ export async function generateTreeAsync(
614
682
  ctx: SerializationContext,
615
683
  current: AsyncServerValue,
616
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
+ }
617
710
  if (isPrimitive(current)) {
618
711
  return [SerovalNodeType.Primitive, current];
619
712
  }
@@ -621,20 +714,22 @@ export async function generateTreeAsync(
621
714
  if (Array.isArray(id)) {
622
715
  return id;
623
716
  }
624
- const semiPrimitive = generateSemiPrimitiveValue(current, id);
717
+ const semiPrimitive = generateSemiPrimitiveValue(ctx, current, id);
625
718
  if (semiPrimitive) {
626
719
  return semiPrimitive;
627
720
  }
628
721
  if (isPromise(current)) {
722
+ assert(ctx.features.has(Feature.Promise), 'Unsupported type "Promise"');
629
723
  return current.then(async (value) => [
630
724
  SerovalNodeType.Promise,
631
725
  await generateTreeAsync(ctx, value),
632
726
  id,
633
727
  ]);
634
728
  }
635
- if (constructorCheck<Set<ServerValue>>(current, Set)) {
729
+ if (constructorCheck<Set<AsyncServerValue>>(current, Set)) {
730
+ assert(ctx.features.has(Feature.Set), 'Unsupported type "Set"');
636
731
  const nodes: SerovalNode[] = [];
637
- const deferred: ServerValue[] = [];
732
+ const deferred: AsyncServerValue[] = [];
638
733
  for (const item of current.keys()) {
639
734
  if (isIterable(item)) {
640
735
  deferred.push(item);
@@ -642,109 +737,209 @@ export async function generateTreeAsync(
642
737
  nodes.push(await generateTreeAsync(ctx, item));
643
738
  }
644
739
  }
645
- for (const item of deferred) {
646
- nodes.push(await generateTreeAsync(ctx, item));
740
+ for (let i = 0, len = deferred.length; i < len; i++) {
741
+ nodes.push(await generateTreeAsync(ctx, deferred[i]));
647
742
  }
648
743
  return [SerovalNodeType.Set, nodes, id];
649
744
  }
650
- if (constructorCheck<Map<ServerValue, ServerValue>>(current, Map)) {
651
- const nodes: [key: SerovalNode, value: SerovalNode][] = [];
652
- const deferred: [ServerValue, ServerValue][] = [];
745
+ if (constructorCheck<Map<AsyncServerValue, AsyncServerValue>>(current, Map)) {
746
+ assert(ctx.features.has(Feature.Map), 'Unsupported type "Map"');
747
+ const keyNodes: SerovalNode[] = [];
748
+ const valueNodes: SerovalNode[] = [];
749
+ const deferredKey: AsyncServerValue[] = [];
750
+ const deferredValue: AsyncServerValue[] = [];
751
+ let deferredSize = 0;
653
752
  for (const [key, value] of current.entries()) {
654
753
  if (isIterable(key) || isIterable(value)) {
655
- deferred.push([key, value]);
754
+ deferredKey.push(key);
755
+ deferredValue.push(value);
756
+ deferredSize++;
656
757
  } else {
657
- const keyNode = await generateTreeAsync(ctx, key);
658
- const valueNode = await generateTreeAsync(ctx, value);
659
- nodes.push([keyNode, valueNode]);
758
+ keyNodes.push(await generateTreeAsync(ctx, key));
759
+ valueNodes.push(await generateTreeAsync(ctx, value));
660
760
  }
661
761
  }
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]);
762
+ for (let i = 0; i < deferredSize; i++) {
763
+ keyNodes.push(await generateTreeAsync(ctx, deferredKey[i]));
764
+ valueNodes.push(await generateTreeAsync(ctx, deferredValue[i]));
666
765
  }
667
- return [SerovalNodeType.Map, nodes, id];
766
+ return [SerovalNodeType.Map, [keyNodes, valueNodes, current.size], id];
668
767
  }
669
768
  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);
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
+ }
678
780
  }
679
781
  }
680
- for (const key in deferred) {
681
- nodes[key] = await generateTreeAsync(ctx, deferred[key]);
782
+ for (let i = 0; i < size; i++) {
783
+ if (i in deferred) {
784
+ nodes[i] = await generateTreeAsync(ctx, deferred[i]);
785
+ }
682
786
  }
683
787
  return [SerovalNodeType.Array, nodes, id];
684
788
  }
685
789
  if (current instanceof AggregateError) {
686
790
  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];
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];
697
796
  }
698
797
  if (current instanceof Error) {
699
798
  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];
799
+ const optionsNode = options
800
+ ? await serializeProperties(options)
801
+ : undefined;
802
+ return [
803
+ SerovalNodeType.Error,
804
+ [getErrorConstructor(current), current.message, optionsNode],
805
+ id,
806
+ ];
708
807
  }
709
808
  if (isIterable(current)) {
809
+ assert(ctx.features.has(Feature.SymbolIterator), 'Unsupported type "Iterable"');
710
810
  const options = getIterableOptions(current);
711
- return [SerovalNodeType.Iterable, {
712
- options: options
713
- ? await generateTreeAsync(ctx, options as ServerValue)
811
+ return [SerovalNodeType.Iterable, [
812
+ options
813
+ ? await serializeProperties(options)
714
814
  : undefined,
715
- items: await generateTreeAsync(ctx, Array.from(current)),
716
- }, id];
815
+ await generateTreeAsync(ctx, Array.from(current)),
816
+ ], id];
717
817
  }
718
818
  const empty = current.constructor == null;
719
819
  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
820
  return [
733
821
  empty ? SerovalNodeType.NullConstructor : SerovalNodeType.Object,
734
- nodes,
822
+ await serializeProperties(current as Record<string, unknown>),
735
823
  id,
736
824
  ];
737
825
  }
738
826
  throw new Error('Unsupported value');
739
827
  }
740
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(Feature.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
+
741
866
  export function serializeTree(
742
867
  ctx: SerializationContext,
743
868
  [type, value, id]: SerovalNode,
744
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
+ }
745
940
  switch (type) {
746
941
  case SerovalNodeType.Primitive:
747
- return serializePrimitive(value);
942
+ return serializePrimitive(ctx, value);
748
943
  case SerovalNodeType.Reference:
749
944
  return getRefParam(ctx, value);
750
945
  case SerovalNodeType.Promise: {
@@ -771,40 +966,48 @@ export function serializeTree(
771
966
  return assignRef(ctx, id, String(value));
772
967
  case SerovalNodeType.TypedArray: {
773
968
  // 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}`;
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}`;
778
976
  }
779
- return assignRef(ctx, id, `new ${value.constructor}(${args})`);
977
+ return assignRef(ctx, id, `new ${value[0]}(${args})`);
780
978
  }
781
979
  case SerovalNodeType.Set: {
782
980
  let serialized = 'new Set';
783
- if (value.length) {
784
- const values: string[] = [];
981
+ const size = value.length;
982
+ if (size) {
983
+ let result = '';
785
984
  ctx.stack.push(id);
786
- for (const item of value) {
985
+ for (let i = 0; i < size; i++) {
986
+ const item = value[i];
787
987
  if (isReferenceInStack(ctx, item)) {
788
988
  createSetAdd(ctx, id, getRefParam(ctx, item[1]));
789
989
  } else {
790
990
  // Push directly
791
- values.push(serializeTree(ctx, item));
991
+ result += `${serializeTree(ctx, item)},`;
792
992
  }
793
993
  }
794
994
  ctx.stack.pop();
795
- if (values.length) {
796
- serialized += `([${values.join(',')}])`;
995
+ if (result) {
996
+ serialized += `([${result.substring(0, result.length - 1)}])`;
797
997
  }
798
998
  }
799
999
  return assignRef(ctx, id, serialized);
800
1000
  }
801
1001
  case SerovalNodeType.Map: {
802
1002
  let serialized = 'new Map';
803
- if (value.length) {
804
- const values: string[] = [];
1003
+ const size = value[2];
1004
+ if (size) {
1005
+ let result = '';
805
1006
  ctx.stack.push(id);
806
- for (const [key, val] of value) {
1007
+ for (let i = 0; i < size; i++) {
807
1008
  // Check if key is a parent
1009
+ const key = value[0][i];
1010
+ const val = value[1][i];
808
1011
  if (isReferenceInStack(ctx, key)) {
809
1012
  // Create reference for the map instance
810
1013
  const keyRef = getRefParam(ctx, key[1]);
@@ -835,15 +1038,15 @@ export function serializeTree(
835
1038
  createMapSet(ctx, id, serializeTree(ctx, key), valueRef);
836
1039
  ctx.stack = parent;
837
1040
  } else {
838
- values.push(`[${serializeTree(ctx, key)},${serializeTree(ctx, val)}]`);
1041
+ result += `[${serializeTree(ctx, key)},${serializeTree(ctx, val)}],`;
839
1042
  }
840
1043
  }
841
1044
  ctx.stack.pop();
842
1045
  // Check if there are any values
843
1046
  // so that the empty Map constructor
844
1047
  // can be used instead
845
- if (values.length) {
846
- serialized += `([${values.join(',')}])`;
1048
+ if (result) {
1049
+ serialized += `([${result.substring(0, result.length - 1)}])`;
847
1050
  }
848
1051
  }
849
1052
  return assignRef(ctx, id, serialized);
@@ -852,10 +1055,10 @@ export function serializeTree(
852
1055
  // This is different than Map and Set
853
1056
  // because we also need to serialize
854
1057
  // the holes of the Array
1058
+ const size = value.length;
855
1059
  let values = '';
856
-
857
1060
  ctx.stack.push(id);
858
- for (let i = 0, len = value.length; i < len; i++) {
1061
+ for (let i = 0; i < size; i++) {
859
1062
  const item = value[i];
860
1063
  // Check if index is a hole
861
1064
  if (i in value) {
@@ -865,7 +1068,7 @@ export function serializeTree(
865
1068
  values += ',';
866
1069
  } else {
867
1070
  values += serializeTree(ctx, item);
868
- if (i < value.length - 1) {
1071
+ if (i < size - 1) {
869
1072
  values += ',';
870
1073
  }
871
1074
  }
@@ -879,84 +1082,96 @@ export function serializeTree(
879
1082
  }
880
1083
  case SerovalNodeType.AggregateError: {
881
1084
  // 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(',')})`;
1085
+ ctx.stack.push(id);
1086
+ let serialized = `new AggregateError(${serializeTree(ctx, value[2])},${quote(value[0])})`;
1087
+ ctx.stack.pop();
888
1088
  // `AggregateError` might've been extended
889
1089
  // either through class or custom properties
890
1090
  // Make sure to assign extra properties
891
- if (value.options) {
892
- const options = serializeTree(ctx, value.options);
893
- serialized = `Object.assign(${serialized},${options})`;
1091
+ if (value[1]) {
1092
+ if (ctx.features.has(Feature.ObjectAssign)) {
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
+ }
894
1103
  }
895
1104
  return assignRef(ctx, id, serialized);
896
1105
  }
897
1106
  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})`;
1107
+ let serialized = `new ${value[0]}(${quote(value[1])})`;
1108
+ if (value[2]) {
1109
+ if (ctx.features.has(Feature.ObjectAssign)) {
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
+ }
906
1120
  }
907
1121
  return assignRef(ctx, id, serialized);
908
1122
  }
909
1123
  case SerovalNodeType.Iterable: {
910
1124
  const parent = ctx.stack;
911
1125
  ctx.stack = [];
912
- const values = serializeTree(ctx, value.items);
1126
+ const values = serializeTree(ctx, value[1]);
913
1127
  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})`;
1128
+ let serialized: string;
1129
+ if (ctx.features.has(Feature.ArrayPrototypeValues)) {
1130
+ serialized = `${values}.values()`;
1131
+ } else {
1132
+ serialized = `${values}[Symbol.iterator]()`;
920
1133
  }
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);
1134
+ if (ctx.features.has(Feature.ArrowFunction)) {
1135
+ serialized = `{[Symbol.iterator]:()=>${serialized}}`;
1136
+ } else if (ctx.features.has(Feature.MethodShorthand)) {
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(Feature.ObjectAssign)) {
1143
+ const options = serializeObject(id, value[0]);
1144
+ serialized = `Object.assign(${serialized},${options})`;
949
1145
  } else {
950
- values.push(`${quote(key)}:${serializeTree(ctx, val)}`);
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
+ }
951
1152
  }
952
1153
  }
953
- ctx.stack.pop();
954
- let serialized = `{${values.join(',')}}`;
955
- if (type === SerovalNodeType.NullConstructor) {
956
- serialized = `Object.assign(Object.create(null),${serialized})`;
1154
+ return assignRef(ctx, id, serialized);
1155
+ }
1156
+ case SerovalNodeType.NullConstructor: {
1157
+ let serialized = 'Object.create(null)';
1158
+ if (ctx.features.has(Feature.ObjectAssign)) {
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
+ }
957
1170
  }
958
1171
  return assignRef(ctx, id, serialized);
959
1172
  }
1173
+ case SerovalNodeType.Object:
1174
+ return assignRef(ctx, id, serializeObject(id, value));
960
1175
  default:
961
1176
  throw new Error('Unsupported type');
962
1177
  }