msgpackr 1.11.12 → 1.11.14

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/dist/node.cjs CHANGED
@@ -1428,7 +1428,14 @@ class Packr extends Unpackr {
1428
1428
  let newSharedData = prepareStructures$1(structures, packr);
1429
1429
  if (!encodingError) { // TODO: If there is an encoding error, should make the structures as uninitialized so they get rebuilt next time
1430
1430
  if (packr.saveStructures(newSharedData, newSharedData.isCompatible) === false) {
1431
- // get updated structures and try again if the update failed
1431
+ // The save was declined (a concurrent writer updated the shared structures,
1432
+ // or the store transaction did not durably commit). Our in-memory
1433
+ // structures + transition trie may now reference record ids that were
1434
+ // never persisted; re-packing as-is would re-emit the same record pointing
1435
+ // at an unpersisted structure (-> "Record id is not defined" on decode).
1436
+ // Mark structures uninitialized so the re-pack reloads durable structures
1437
+ // via getStructures, rebuilds the transition trie, and re-mints + re-saves.
1438
+ structures.uninitialized = true;
1432
1439
  return packr.pack(value, encodeOptions)
1433
1440
  }
1434
1441
  packr.lastNamedStructuresLength = sharedLength;
@@ -2406,9 +2413,16 @@ const encodeUtf8 = hasNodeBuffer ? function(target, string, position) {
2406
2413
  return textEncoder.encodeInto(string, target.subarray(position)).written
2407
2414
  } : false;
2408
2415
  setWriteStructSlots(writeStruct, prepareStructures);
2409
- function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr) {
2416
+ function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr, structureKnown) {
2410
2417
  let typedStructs = packr.typedStructs || (packr.typedStructs = []);
2411
2418
  // note that we rely on pack.js to load stored structures before we get to this point
2419
+ // structureKnown is set only on the internal layout-retry below: attempt 1 already minted
2420
+ // this record's structure, so the retry re-encodes a known shape and must not re-apply the
2421
+ // cap (which could otherwise bail after attempt 1 already packed refs → corrupt fallback).
2422
+ // `frozen` is a local (from this instance's typedStructs) — never a shared global — so a
2423
+ // re-entrant encode on another instance (e.g. via an enumerable getter) can't flip it.
2424
+ const cap = packr.maxOwnStructures ?? Infinity;
2425
+ const frozen = !structureKnown && typedStructs.length >= cap;
2412
2426
  let targetView = target.dataView;
2413
2427
  let refsStartPosition = (typedStructs.lastStringStart || 100) + position;
2414
2428
  let safeEnd = target.length - 10;
@@ -2439,9 +2453,12 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2439
2453
  let usedAscii0;
2440
2454
  let keyIndex = 0;
2441
2455
  for (let key in object) {
2442
- let value = object[key];
2443
2456
  let nextTransition = transition[key];
2457
+ // Resolve the key transition BEFORE reading the value: when frozen and the key is new we
2458
+ // bail here, so an enumerable getter isn't invoked during this (failed) struct attempt and
2459
+ // then again by the plain fallback (which would double-read a side-effecting accessor).
2444
2460
  if (!nextTransition) {
2461
+ if (frozen) return 0;
2445
2462
  transition[key] = nextTransition = {
2446
2463
  key,
2447
2464
  parent: transition,
@@ -2456,6 +2473,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2456
2473
  date64: null
2457
2474
  };
2458
2475
  }
2476
+ let value = object[key];
2459
2477
  if (position > safeEnd) {
2460
2478
  target = makeRoom(position);
2461
2479
  targetView = target.dataView;
@@ -2473,10 +2491,10 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2473
2491
  if (nextId < 200 || !nextTransition.num64) {
2474
2492
  if (number >> 0 === number && number < 0x20000000 && number > -0x1f000000) {
2475
2493
  if (number < 0xf6 && number >= 0 && (nextTransition.num8 && !(nextId > 200 && nextTransition.num32) || number < 0x20 && !nextTransition.num32)) {
2476
- transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1);
2494
+ transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
2477
2495
  target[position++] = number;
2478
2496
  } else {
2479
- transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
2497
+ transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
2480
2498
  targetView.setUint32(position, number, true);
2481
2499
  position += 4;
2482
2500
  }
@@ -2487,14 +2505,14 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2487
2505
  let xShifted;
2488
2506
  // this checks for rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
2489
2507
  if (((xShifted = number * mult10[((target[position + 3] & 0x7f) << 1) | (target[position + 2] >> 7)]) >> 0) === xShifted) {
2490
- transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
2508
+ transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
2491
2509
  position += 4;
2492
2510
  break;
2493
2511
  }
2494
2512
  }
2495
2513
  }
2496
2514
  }
2497
- transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8);
2515
+ transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8, frozen);
2498
2516
  targetView.setFloat64(position, number, true);
2499
2517
  position += 8;
2500
2518
  break;
@@ -2560,21 +2578,21 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2560
2578
  nextTransition.string8 = transition;
2561
2579
  pack(null, 0, true); // special call to notify that structures have been updated
2562
2580
  } else {
2563
- transition = createTypeTransition(nextTransition, UTF8, 1);
2581
+ transition = createTypeTransition(nextTransition, UTF8, 1, frozen);
2564
2582
  }
2565
2583
  }
2566
2584
  } else if (refOffset === 0 && !usedAscii0) {
2567
2585
  usedAscii0 = true;
2568
- transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0);
2586
+ transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0, frozen);
2569
2587
  break; // don't increment position
2570
2588
  }// else ascii:
2571
2589
  else if (!(transition = nextTransition.ascii8) && !(typedStructs.length > 10 && (transition = nextTransition.string8)))
2572
- transition = createTypeTransition(nextTransition, ASCII, 1);
2590
+ transition = createTypeTransition(nextTransition, ASCII, 1, frozen);
2573
2591
  target[position++] = refOffset;
2574
2592
  } else {
2575
2593
  // TODO: Enable ascii16 at some point, but get the logic right
2576
2594
  //if (isNotAscii)
2577
- transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2);
2595
+ transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2, frozen);
2578
2596
  //else
2579
2597
  //transition = nextTransition.ascii16 || createTypeTransition(nextTransition, ASCII, 2);
2580
2598
  targetView.setUint16(position, refOffset, true);
@@ -2584,7 +2602,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2584
2602
  case 'object':
2585
2603
  if (value) {
2586
2604
  if (value.constructor === Date) {
2587
- transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8);
2605
+ transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8, frozen);
2588
2606
  targetView.setFloat64(position, value.getTime(), true);
2589
2607
  position += 8;
2590
2608
  } else {
@@ -2600,7 +2618,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2600
2618
  }
2601
2619
  break;
2602
2620
  case 'boolean':
2603
- transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1);
2621
+ transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
2604
2622
  target[position++] = value ? 0xf9 : 0xf8; // match CBOR with these
2605
2623
  break;
2606
2624
  case 'undefined':
@@ -2613,9 +2631,41 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2613
2631
  default:
2614
2632
  queuedReferences.push(key, value, keyIndex);
2615
2633
  }
2634
+ if (transition === undefined) return 0; // frozen: structure cap reached
2616
2635
  keyIndex++;
2617
2636
  }
2618
2637
 
2638
+ // Cap enforcement for queued (nested-object / null) references. pack() advances msgpackr's
2639
+ // shared write position and we cannot cleanly bail afterward, so preflight the whole queued
2640
+ // chain through EXISTING transitions first: if the cap is reached and any field would need a
2641
+ // new structure, fall back to plain encoding now (return 0) — before touching the shared
2642
+ // position. Uses a FRESH length read (not the entry-time `frozen`): a getter invoked while
2643
+ // reading values above may have minted on this same instance since entry.
2644
+ if (!structureKnown && queuedReferences.length > 0 && typedStructs.length >= cap) {
2645
+ let t = transition;
2646
+ for (let i = 0, l = queuedReferences.length; i < l; i += 3) {
2647
+ // A non-null (object/Date) ref is pack()ed into the shared buffer, advancing
2648
+ // msgpackr's write position. Its structure variant (object16 vs object32) depends on
2649
+ // the runtime ref-section offset (inline strings + earlier refs), which we can't know
2650
+ // before packing — and we can't bail after a pack without corrupting the fallback. So
2651
+ // under the cap, any record with a packing ref falls back to plain encoding now,
2652
+ // before any pack(). null/undefined refs don't pack, so they're walked normally.
2653
+ if (queuedReferences[i + 1] != null) return 0;
2654
+ const nt = t[queuedReferences[i]];
2655
+ if (!nt) return 0;
2656
+ const next = nt.object16; // null/undefined ref → OBJECT_DATA size 2
2657
+ if (!next) return 0;
2658
+ t = next;
2659
+ }
2660
+ if (t[RECORD_SYMBOL] == null) return 0; // exact structure not yet minted
2661
+ }
2662
+
2663
+ // Past the preflight the chain is known, so no minting happens — except a rare offset
2664
+ // divergence (a known shape whose ref section now crosses 0xff00 and needs object32 where
2665
+ // the preflight matched object16). Once a ref is packed we can no longer bail, so we finish
2666
+ // via the unfrozen forceTypeTransition: a bounded, self-converging overshoot for that one
2667
+ // record. packedRef keeps the record-id mint from bailing after a pack.
2668
+ let packedRef = false;
2619
2669
  for (let i = 0, l = queuedReferences.length; i < l;) {
2620
2670
  let key = queuedReferences[i++];
2621
2671
  let value = queuedReferences[i++];
@@ -2637,15 +2687,6 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2637
2687
  }
2638
2688
  let newPosition;
2639
2689
  if (value) {
2640
- /*if (typeof value === 'string') { // TODO: we could re-enable long strings
2641
- if (position + value.length * 3 > safeEnd) {
2642
- target = makeRoom(position + value.length * 3);
2643
- position -= start;
2644
- targetView = target.dataView;
2645
- start = 0;
2646
- }
2647
- newPosition = position + target.utf8Write(value, position, 0xffffffff);
2648
- } else { */
2649
2690
  let size;
2650
2691
  refOffset = refPosition - refsStartPosition;
2651
2692
  if (refOffset < 0xff00) {
@@ -2655,15 +2696,15 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2655
2696
  else if ((transition = nextTransition.object32))
2656
2697
  size = 4;
2657
2698
  else {
2658
- transition = createTypeTransition(nextTransition, OBJECT_DATA, 2);
2699
+ transition = forceTypeTransition(nextTransition, OBJECT_DATA, 2);
2659
2700
  size = 2;
2660
2701
  }
2661
2702
  } else {
2662
- transition = nextTransition.object32 || createTypeTransition(nextTransition, OBJECT_DATA, 4);
2703
+ transition = nextTransition.object32 || forceTypeTransition(nextTransition, OBJECT_DATA, 4);
2663
2704
  size = 4;
2664
2705
  }
2665
2706
  newPosition = pack(value, refPosition);
2666
- //}
2707
+ packedRef = true;
2667
2708
  if (typeof newPosition === 'object') {
2668
2709
  // re-allocated
2669
2710
  refPosition = newPosition.position;
@@ -2683,16 +2724,19 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2683
2724
  position += 4;
2684
2725
  }
2685
2726
  } else { // null or undefined
2686
- transition = nextTransition.object16 || createTypeTransition(nextTransition, OBJECT_DATA, 2);
2727
+ transition = nextTransition.object16 || forceTypeTransition(nextTransition, OBJECT_DATA, 2);
2687
2728
  targetView.setInt16(position, value === null ? -10 : -9, true);
2688
2729
  position += 2;
2689
2730
  }
2690
2731
  keyIndex++;
2691
2732
  }
2692
2733
 
2693
-
2694
2734
  let recordId = transition[RECORD_SYMBOL];
2695
2735
  if (recordId == null) {
2736
+ // Flat records (no queued refs) reach here without packing, so the cap is enforced
2737
+ // cleanly. Records that packed nested refs already passed the preflight; either way
2738
+ // bailing now after refs were packed would corrupt the fallback.
2739
+ if (!packedRef && typedStructs.length >= cap) return 0;
2696
2740
  recordId = packr.typedStructs.length;
2697
2741
  let structure = [];
2698
2742
  let nextTransition = transition;
@@ -2746,7 +2790,11 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
2746
2790
  if (refsStartPosition === refPosition)
2747
2791
  return position; // no refs
2748
2792
  typedStructs.lastStringStart = position - start;
2749
- return writeStruct(object, target, encodingStart, start, structures, makeRoom, pack, packr);
2793
+ // Fixed section overflowed our estimate retry with the corrected size. The structure
2794
+ // is already minted at this point, so pass structureKnown=true to skip the cap check
2795
+ // (otherwise a record that became frozen during attempt 1 would bail mid-retry, after
2796
+ // refs were already packed, and corrupt the fallback).
2797
+ return writeStruct(object, target, encodingStart, start, structures, makeRoom, pack, packr, true);
2750
2798
  }
2751
2799
  return refPosition;
2752
2800
  }
@@ -2778,9 +2826,36 @@ function anyType(transition, position, targetView, value) {
2778
2826
  // TODO: can we do an "any" type where we defer the decision?
2779
2827
  return;
2780
2828
  }
2781
- function createTypeTransition(transition, type, size) {
2829
+ // When the typed-structure dictionary reaches maxOwnStructures we stop minting new
2830
+ // structures/transitions. typedStructs is append-only and pinned on the long-lived
2831
+ // encoder (records reference structures by recordId), so an unbounded shape space —
2832
+ // e.g. a wide, sparsely/variably-populated schema — would otherwise grow the
2833
+ // dictionary + transition trie without limit. `frozen` is passed in (derived from the
2834
+ // encoding instance's own typedStructs.length, never a shared global) so a re-entrant
2835
+ // encode on another instance can't flip it; while frozen, a missing transition returns
2836
+ // undefined so the caller bails and the record falls back to plain encoding.
2837
+ function createTypeTransition(transition, type, size, frozen) {
2838
+ let typeName = TYPE_NAMES[type] + (size << 3);
2839
+ let newTransition = transition[typeName];
2840
+ if (newTransition) return newTransition;
2841
+ if (frozen) return undefined;
2842
+ newTransition = transition[typeName] = Object.create(null);
2843
+ newTransition.__type = type;
2844
+ newTransition.__size = size;
2845
+ newTransition.__parent = transition;
2846
+ return newTransition;
2847
+ }
2848
+
2849
+ // Unfrozen variant: always mints. Used in the queued-ref loop once a nested value has
2850
+ // already been pack()ed — at that point pack() has advanced msgpackr's shared write
2851
+ // position, so bailing with `return 0` would corrupt the fallback. We must finish the
2852
+ // encode instead, even if that means minting a (bounded) handful of structures past the
2853
+ // cap. The cap is still enforced up front via the preflight, before the first pack().
2854
+ function forceTypeTransition(transition, type, size) {
2782
2855
  let typeName = TYPE_NAMES[type] + (size << 3);
2783
- let newTransition = transition[typeName] || (transition[typeName] = Object.create(null));
2856
+ let newTransition = transition[typeName];
2857
+ if (newTransition) return newTransition;
2858
+ newTransition = transition[typeName] = Object.create(null);
2784
2859
  newTransition.__type = type;
2785
2860
  newTransition.__size = size;
2786
2861
  newTransition.__parent = transition;
@@ -2814,7 +2889,8 @@ function onLoadedStructures(sharedData) {
2814
2889
  date64: null,
2815
2890
  };
2816
2891
  }
2817
- transition = createTypeTransition(nextTransition, type, size);
2892
+ // Replaying persisted structures is never subject to the cap — always mint.
2893
+ transition = createTypeTransition(nextTransition, type, size, false);
2818
2894
  }
2819
2895
  transition[RECORD_SYMBOL] = i;
2820
2896
  }