msgpackr 1.11.12 → 1.11.13

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/test.js CHANGED
@@ -2399,9 +2399,16 @@
2399
2399
  return textEncoder.encodeInto(string, target.subarray(position)).written
2400
2400
  } : false;
2401
2401
  setWriteStructSlots(writeStruct, prepareStructures);
2402
- function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr) {
2402
+ function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr, structureKnown) {
2403
2403
  let typedStructs = packr.typedStructs || (packr.typedStructs = []);
2404
2404
  // note that we rely on pack.js to load stored structures before we get to this point
2405
+ // structureKnown is set only on the internal layout-retry below: attempt 1 already minted
2406
+ // this record's structure, so the retry re-encodes a known shape and must not re-apply the
2407
+ // cap (which could otherwise bail after attempt 1 already packed refs → corrupt fallback).
2408
+ // `frozen` is a local (from this instance's typedStructs) — never a shared global — so a
2409
+ // re-entrant encode on another instance (e.g. via an enumerable getter) can't flip it.
2410
+ const cap = packr.maxOwnStructures ?? Infinity;
2411
+ const frozen = !structureKnown && typedStructs.length >= cap;
2405
2412
  let targetView = target.dataView;
2406
2413
  let refsStartPosition = (typedStructs.lastStringStart || 100) + position;
2407
2414
  let safeEnd = target.length - 10;
@@ -2432,9 +2439,12 @@
2432
2439
  let usedAscii0;
2433
2440
  let keyIndex = 0;
2434
2441
  for (let key in object) {
2435
- let value = object[key];
2436
2442
  let nextTransition = transition[key];
2443
+ // Resolve the key transition BEFORE reading the value: when frozen and the key is new we
2444
+ // bail here, so an enumerable getter isn't invoked during this (failed) struct attempt and
2445
+ // then again by the plain fallback (which would double-read a side-effecting accessor).
2437
2446
  if (!nextTransition) {
2447
+ if (frozen) return 0;
2438
2448
  transition[key] = nextTransition = {
2439
2449
  key,
2440
2450
  parent: transition,
@@ -2449,6 +2459,7 @@
2449
2459
  date64: null
2450
2460
  };
2451
2461
  }
2462
+ let value = object[key];
2452
2463
  if (position > safeEnd) {
2453
2464
  target = makeRoom(position);
2454
2465
  targetView = target.dataView;
@@ -2466,10 +2477,10 @@
2466
2477
  if (nextId < 200 || !nextTransition.num64) {
2467
2478
  if (number >> 0 === number && number < 0x20000000 && number > -0x1f000000) {
2468
2479
  if (number < 0xf6 && number >= 0 && (nextTransition.num8 && !(nextId > 200 && nextTransition.num32) || number < 0x20 && !nextTransition.num32)) {
2469
- transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1);
2480
+ transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
2470
2481
  target[position++] = number;
2471
2482
  } else {
2472
- transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
2483
+ transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
2473
2484
  targetView.setUint32(position, number, true);
2474
2485
  position += 4;
2475
2486
  }
@@ -2480,14 +2491,14 @@
2480
2491
  let xShifted;
2481
2492
  // this checks for rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
2482
2493
  if (((xShifted = number * mult10[((target[position + 3] & 0x7f) << 1) | (target[position + 2] >> 7)]) >> 0) === xShifted) {
2483
- transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
2494
+ transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
2484
2495
  position += 4;
2485
2496
  break;
2486
2497
  }
2487
2498
  }
2488
2499
  }
2489
2500
  }
2490
- transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8);
2501
+ transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8, frozen);
2491
2502
  targetView.setFloat64(position, number, true);
2492
2503
  position += 8;
2493
2504
  break;
@@ -2553,21 +2564,21 @@
2553
2564
  nextTransition.string8 = transition;
2554
2565
  pack(null, 0, true); // special call to notify that structures have been updated
2555
2566
  } else {
2556
- transition = createTypeTransition(nextTransition, UTF8, 1);
2567
+ transition = createTypeTransition(nextTransition, UTF8, 1, frozen);
2557
2568
  }
2558
2569
  }
2559
2570
  } else if (refOffset === 0 && !usedAscii0) {
2560
2571
  usedAscii0 = true;
2561
- transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0);
2572
+ transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0, frozen);
2562
2573
  break; // don't increment position
2563
2574
  }// else ascii:
2564
2575
  else if (!(transition = nextTransition.ascii8) && !(typedStructs.length > 10 && (transition = nextTransition.string8)))
2565
- transition = createTypeTransition(nextTransition, ASCII, 1);
2576
+ transition = createTypeTransition(nextTransition, ASCII, 1, frozen);
2566
2577
  target[position++] = refOffset;
2567
2578
  } else {
2568
2579
  // TODO: Enable ascii16 at some point, but get the logic right
2569
2580
  //if (isNotAscii)
2570
- transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2);
2581
+ transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2, frozen);
2571
2582
  //else
2572
2583
  //transition = nextTransition.ascii16 || createTypeTransition(nextTransition, ASCII, 2);
2573
2584
  targetView.setUint16(position, refOffset, true);
@@ -2577,7 +2588,7 @@
2577
2588
  case 'object':
2578
2589
  if (value) {
2579
2590
  if (value.constructor === Date) {
2580
- transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8);
2591
+ transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8, frozen);
2581
2592
  targetView.setFloat64(position, value.getTime(), true);
2582
2593
  position += 8;
2583
2594
  } else {
@@ -2593,7 +2604,7 @@
2593
2604
  }
2594
2605
  break;
2595
2606
  case 'boolean':
2596
- transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1);
2607
+ transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
2597
2608
  target[position++] = value ? 0xf9 : 0xf8; // match CBOR with these
2598
2609
  break;
2599
2610
  case 'undefined':
@@ -2606,9 +2617,41 @@
2606
2617
  default:
2607
2618
  queuedReferences.push(key, value, keyIndex);
2608
2619
  }
2620
+ if (transition === undefined) return 0; // frozen: structure cap reached
2609
2621
  keyIndex++;
2610
2622
  }
2611
2623
 
2624
+ // Cap enforcement for queued (nested-object / null) references. pack() advances msgpackr's
2625
+ // shared write position and we cannot cleanly bail afterward, so preflight the whole queued
2626
+ // chain through EXISTING transitions first: if the cap is reached and any field would need a
2627
+ // new structure, fall back to plain encoding now (return 0) — before touching the shared
2628
+ // position. Uses a FRESH length read (not the entry-time `frozen`): a getter invoked while
2629
+ // reading values above may have minted on this same instance since entry.
2630
+ if (!structureKnown && queuedReferences.length > 0 && typedStructs.length >= cap) {
2631
+ let t = transition;
2632
+ for (let i = 0, l = queuedReferences.length; i < l; i += 3) {
2633
+ // A non-null (object/Date) ref is pack()ed into the shared buffer, advancing
2634
+ // msgpackr's write position. Its structure variant (object16 vs object32) depends on
2635
+ // the runtime ref-section offset (inline strings + earlier refs), which we can't know
2636
+ // before packing — and we can't bail after a pack without corrupting the fallback. So
2637
+ // under the cap, any record with a packing ref falls back to plain encoding now,
2638
+ // before any pack(). null/undefined refs don't pack, so they're walked normally.
2639
+ if (queuedReferences[i + 1] != null) return 0;
2640
+ const nt = t[queuedReferences[i]];
2641
+ if (!nt) return 0;
2642
+ const next = nt.object16; // null/undefined ref → OBJECT_DATA size 2
2643
+ if (!next) return 0;
2644
+ t = next;
2645
+ }
2646
+ if (t[RECORD_SYMBOL] == null) return 0; // exact structure not yet minted
2647
+ }
2648
+
2649
+ // Past the preflight the chain is known, so no minting happens — except a rare offset
2650
+ // divergence (a known shape whose ref section now crosses 0xff00 and needs object32 where
2651
+ // the preflight matched object16). Once a ref is packed we can no longer bail, so we finish
2652
+ // via the unfrozen forceTypeTransition: a bounded, self-converging overshoot for that one
2653
+ // record. packedRef keeps the record-id mint from bailing after a pack.
2654
+ let packedRef = false;
2612
2655
  for (let i = 0, l = queuedReferences.length; i < l;) {
2613
2656
  let key = queuedReferences[i++];
2614
2657
  let value = queuedReferences[i++];
@@ -2630,15 +2673,6 @@
2630
2673
  }
2631
2674
  let newPosition;
2632
2675
  if (value) {
2633
- /*if (typeof value === 'string') { // TODO: we could re-enable long strings
2634
- if (position + value.length * 3 > safeEnd) {
2635
- target = makeRoom(position + value.length * 3);
2636
- position -= start;
2637
- targetView = target.dataView;
2638
- start = 0;
2639
- }
2640
- newPosition = position + target.utf8Write(value, position, 0xffffffff);
2641
- } else { */
2642
2676
  let size;
2643
2677
  refOffset = refPosition - refsStartPosition;
2644
2678
  if (refOffset < 0xff00) {
@@ -2648,15 +2682,15 @@
2648
2682
  else if ((transition = nextTransition.object32))
2649
2683
  size = 4;
2650
2684
  else {
2651
- transition = createTypeTransition(nextTransition, OBJECT_DATA, 2);
2685
+ transition = forceTypeTransition(nextTransition, OBJECT_DATA, 2);
2652
2686
  size = 2;
2653
2687
  }
2654
2688
  } else {
2655
- transition = nextTransition.object32 || createTypeTransition(nextTransition, OBJECT_DATA, 4);
2689
+ transition = nextTransition.object32 || forceTypeTransition(nextTransition, OBJECT_DATA, 4);
2656
2690
  size = 4;
2657
2691
  }
2658
2692
  newPosition = pack(value, refPosition);
2659
- //}
2693
+ packedRef = true;
2660
2694
  if (typeof newPosition === 'object') {
2661
2695
  // re-allocated
2662
2696
  refPosition = newPosition.position;
@@ -2676,16 +2710,19 @@
2676
2710
  position += 4;
2677
2711
  }
2678
2712
  } else { // null or undefined
2679
- transition = nextTransition.object16 || createTypeTransition(nextTransition, OBJECT_DATA, 2);
2713
+ transition = nextTransition.object16 || forceTypeTransition(nextTransition, OBJECT_DATA, 2);
2680
2714
  targetView.setInt16(position, value === null ? -10 : -9, true);
2681
2715
  position += 2;
2682
2716
  }
2683
2717
  keyIndex++;
2684
2718
  }
2685
2719
 
2686
-
2687
2720
  let recordId = transition[RECORD_SYMBOL];
2688
2721
  if (recordId == null) {
2722
+ // Flat records (no queued refs) reach here without packing, so the cap is enforced
2723
+ // cleanly. Records that packed nested refs already passed the preflight; either way
2724
+ // bailing now after refs were packed would corrupt the fallback.
2725
+ if (!packedRef && typedStructs.length >= cap) return 0;
2689
2726
  recordId = packr.typedStructs.length;
2690
2727
  let structure = [];
2691
2728
  let nextTransition = transition;
@@ -2739,7 +2776,11 @@
2739
2776
  if (refsStartPosition === refPosition)
2740
2777
  return position; // no refs
2741
2778
  typedStructs.lastStringStart = position - start;
2742
- return writeStruct(object, target, encodingStart, start, structures, makeRoom, pack, packr);
2779
+ // Fixed section overflowed our estimate retry with the corrected size. The structure
2780
+ // is already minted at this point, so pass structureKnown=true to skip the cap check
2781
+ // (otherwise a record that became frozen during attempt 1 would bail mid-retry, after
2782
+ // refs were already packed, and corrupt the fallback).
2783
+ return writeStruct(object, target, encodingStart, start, structures, makeRoom, pack, packr, true);
2743
2784
  }
2744
2785
  return refPosition;
2745
2786
  }
@@ -2771,9 +2812,36 @@
2771
2812
  // TODO: can we do an "any" type where we defer the decision?
2772
2813
  return;
2773
2814
  }
2774
- function createTypeTransition(transition, type, size) {
2815
+ // When the typed-structure dictionary reaches maxOwnStructures we stop minting new
2816
+ // structures/transitions. typedStructs is append-only and pinned on the long-lived
2817
+ // encoder (records reference structures by recordId), so an unbounded shape space —
2818
+ // e.g. a wide, sparsely/variably-populated schema — would otherwise grow the
2819
+ // dictionary + transition trie without limit. `frozen` is passed in (derived from the
2820
+ // encoding instance's own typedStructs.length, never a shared global) so a re-entrant
2821
+ // encode on another instance can't flip it; while frozen, a missing transition returns
2822
+ // undefined so the caller bails and the record falls back to plain encoding.
2823
+ function createTypeTransition(transition, type, size, frozen) {
2824
+ let typeName = TYPE_NAMES[type] + (size << 3);
2825
+ let newTransition = transition[typeName];
2826
+ if (newTransition) return newTransition;
2827
+ if (frozen) return undefined;
2828
+ newTransition = transition[typeName] = Object.create(null);
2829
+ newTransition.__type = type;
2830
+ newTransition.__size = size;
2831
+ newTransition.__parent = transition;
2832
+ return newTransition;
2833
+ }
2834
+
2835
+ // Unfrozen variant: always mints. Used in the queued-ref loop once a nested value has
2836
+ // already been pack()ed — at that point pack() has advanced msgpackr's shared write
2837
+ // position, so bailing with `return 0` would corrupt the fallback. We must finish the
2838
+ // encode instead, even if that means minting a (bounded) handful of structures past the
2839
+ // cap. The cap is still enforced up front via the preflight, before the first pack().
2840
+ function forceTypeTransition(transition, type, size) {
2775
2841
  let typeName = TYPE_NAMES[type] + (size << 3);
2776
- let newTransition = transition[typeName] || (transition[typeName] = Object.create(null));
2842
+ let newTransition = transition[typeName];
2843
+ if (newTransition) return newTransition;
2844
+ newTransition = transition[typeName] = Object.create(null);
2777
2845
  newTransition.__type = type;
2778
2846
  newTransition.__size = size;
2779
2847
  newTransition.__parent = transition;
@@ -2807,7 +2875,8 @@
2807
2875
  date64: null,
2808
2876
  };
2809
2877
  }
2810
- transition = createTypeTransition(nextTransition, type, size);
2878
+ // Replaying persisted structures is never subject to the cap — always mint.
2879
+ transition = createTypeTransition(nextTransition, type, size, false);
2811
2880
  }
2812
2881
  transition[RECORD_SYMBOL] = i;
2813
2882
  }
@@ -4564,5 +4633,189 @@
4564
4633
  });
4565
4634
  });
4566
4635
 
4636
+ suite('msgpackr – maxOwnStructures cap (randomAccessStructure)', function () {
4637
+ // Helper: create a width-heterogeneous record generator using a deterministic PRNG.
4638
+ // Objects have up to 6 fields drawn from a sparse set, each value is an integer whose
4639
+ // width (num8 / num32 / num64) varies per value, producing many distinct typed structures.
4640
+ // useRecords: false keeps the classic named-record encoder out of the way so all structure
4641
+ // creation goes through the typed-struct path and the cap is exercised in isolation.
4642
+ function makeCapRunner(cap) {
4643
+ const fields = ['a','b','c','d','e','f','g','h'];
4644
+ let seed = 42;
4645
+ const rnd = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 0xffffffff; };
4646
+ const packr = new Packr({
4647
+ structures: [],
4648
+ useRecords: false,
4649
+ randomAccessStructure: true,
4650
+ maxOwnStructures: cap,
4651
+ });
4652
+ const norm = r => packr.unpack(packr.pack(r));
4653
+ for (let i = 0; i < 4000; i++) {
4654
+ const r = {};
4655
+ for (let j = 0; j < Math.ceil(rnd() * 6); j++) {
4656
+ // values cycle across num8 / num32 / float64 ranges to force distinct structures
4657
+ const mag = rnd() < 0.33 ? 10 : rnd() < 0.5 ? 400000 : 1e13;
4658
+ r[fields[Math.floor(rnd() * 8)]] = Math.floor(rnd() * mag);
4659
+ }
4660
+ assert.deepEqual(norm(r), r);
4661
+ }
4662
+ return packr.typedStructs ? packr.typedStructs.length : 0;
4663
+ }
4664
+
4665
+ test('uncapped (default) grows well past 256 for width-heterogeneous records', function () {
4666
+ assert.ok(makeCapRunner(undefined) > 256, 'expected uncapped typedStructs to exceed 256');
4667
+ });
4668
+
4669
+ test('cap=64 bounds typedStructs.length and preserves round-trips', function () {
4670
+ assert.ok(makeCapRunner(64) <= 64, 'typedStructs should not exceed cap of 64');
4671
+ });
4672
+
4673
+ test('cap=256 bounds typedStructs.length and preserves round-trips', function () {
4674
+ assert.ok(makeCapRunner(256) <= 256, 'typedStructs should not exceed cap of 256');
4675
+ });
4676
+
4677
+ test('flat-record streams stay a strict hard bound', function () {
4678
+ // With maxOwnStructures=16, pack 2000 flat records; typedStructs must never exceed 16.
4679
+ const packr = new Packr({
4680
+ structures: [],
4681
+ useRecords: false,
4682
+ randomAccessStructure: true,
4683
+ maxOwnStructures: 16,
4684
+ });
4685
+ let seed = 99;
4686
+ const rnd = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 0xffffffff; };
4687
+ for (let i = 0; i < 2000; i++) {
4688
+ const r = { x: Math.floor(rnd() * 1e6), y: Math.floor(rnd() * 200), z: Math.floor(rnd() * 1e12) };
4689
+ assert.deepEqual(packr.unpack(packr.pack(r)), r);
4690
+ }
4691
+ assert.ok(packr.typedStructs.length <= 16, 'flat records must stay within cap, got ' + packr.typedStructs.length);
4692
+ });
4693
+
4694
+ test('capped-out records fall back to plain encoding and still round-trip', function () {
4695
+ // Once the cap is hit, novel shapes must decode correctly via plain msgpack fallback.
4696
+ const packr = new Packr({
4697
+ structures: [],
4698
+ useRecords: false,
4699
+ randomAccessStructure: true,
4700
+ maxOwnStructures: 2,
4701
+ });
4702
+ const norm = r => packr.unpack(packr.pack(r));
4703
+ assert.deepEqual(norm({ a: 1 }), { a: 1 }); // mints structure 0
4704
+ assert.deepEqual(norm({ b: 'hello' }), { b: 'hello' }); // mints structure 1, cap hit
4705
+ // These novel shapes fall back to plain msgpack — must still decode correctly:
4706
+ assert.deepEqual(norm({ c: 42 }), { c: 42 });
4707
+ assert.deepEqual(norm({ a: 1, b: 'hi', c: 99 }), { a: 1, b: 'hi', c: 99 });
4708
+ assert.strictEqual(packr.typedStructs.length, 2, 'cap must be exact');
4709
+ });
4710
+
4711
+ test('a known key later seen as a nested object falls back cleanly', function () {
4712
+ // Once frozen, a previously-learned scalar key carrying an object must bail BEFORE pack()
4713
+ // advances the shared encoder position — otherwise the plain fallback gets corrupt bytes.
4714
+ const packr = new Packr({
4715
+ structures: [],
4716
+ useRecords: false,
4717
+ randomAccessStructure: true,
4718
+ maxOwnStructures: 1,
4719
+ });
4720
+ const norm = r => packr.unpack(packr.pack(r));
4721
+ assert.deepEqual(norm({ a: 1 }), { a: 1 }); // mints structure 0, cap hit
4722
+ const r2 = { a: { x: 1 } };
4723
+ assert.deepEqual(norm(r2), r2); // 'a' known but now carries object — must fall back cleanly
4724
+ });
4725
+
4726
+ test('nested records do not overshoot the cap and still round-trip', function () {
4727
+ // A nested object mints its own structure before the outer record, so a stale frozen flag
4728
+ // could push the outer record past the cap. The record-id mint guard re-checks the live
4729
+ // length, keeping typedStructs.length a strict bound.
4730
+ const packr = new Packr({
4731
+ structures: [],
4732
+ useRecords: false,
4733
+ randomAccessStructure: true,
4734
+ maxOwnStructures: 4,
4735
+ });
4736
+ let seed = 7;
4737
+ const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
4738
+ for (let i = 0; i < 1000; i++) {
4739
+ const r = { outer: { inner: (rnd() * 1e7 | 0) * 1000 }, tag: 't' + (i % 20), n: (rnd() * 300 | 0) };
4740
+ assert.deepEqual(packr.unpack(packr.pack(r)), r);
4741
+ }
4742
+ assert.ok(packr.typedStructs.length <= 4, 'nested encodes must not push typedStructs past the cap, got ' + packr.typedStructs.length);
4743
+ });
4744
+
4745
+ test('persisted typed structures still load after a capped encoder froze the dictionary', function () {
4746
+ // Replaying persisted structures in onLoadedStructures must always succeed, regardless of
4747
+ // maxOwnStructures — the cap only limits minting NEW structures during encode.
4748
+ let saved = null;
4749
+ const writer = new Packr({
4750
+ structures: [],
4751
+ useRecords: false,
4752
+ randomAccessStructure: true,
4753
+ saveStructures(s) { saved = s; return true; },
4754
+ getStructures() { return saved; },
4755
+ });
4756
+ const buf = writer.pack({ name: 'Alice', age: 30 });
4757
+
4758
+ // Warm up a capped encoder so the module-global-if-any freeze state is set.
4759
+ const capped = new Packr({
4760
+ structures: [],
4761
+ useRecords: false,
4762
+ randomAccessStructure: true,
4763
+ maxOwnStructures: 1,
4764
+ });
4765
+ capped.pack({ x: 1 });
4766
+ capped.pack({ y: 2, z: 3 }); // cap reached
4767
+
4768
+ // A fresh reader must still rebuild the transition trie from saved structures.
4769
+ const reader = new Packr({
4770
+ structures: [],
4771
+ useRecords: false,
4772
+ randomAccessStructure: true,
4773
+ getStructures() { return saved; },
4774
+ });
4775
+ const result = reader.unpack(buf);
4776
+ assert.equal(result.name, 'Alice');
4777
+ assert.equal(result.age, 30);
4778
+ });
4779
+
4780
+ test('the cap is per-instance: an uncapped sibling cannot lift this instance\'s cap', function () {
4781
+ // frozen is derived from each encoder's own typedStructs.length — not a shared global —
4782
+ // so an uncapped sibling churning out structures cannot lift the cap on the bounded one.
4783
+ const uncapped = new Packr({
4784
+ structures: [],
4785
+ useRecords: false,
4786
+ randomAccessStructure: true,
4787
+ });
4788
+ const capped = new Packr({
4789
+ structures: [],
4790
+ useRecords: false,
4791
+ randomAccessStructure: true,
4792
+ maxOwnStructures: 2,
4793
+ });
4794
+ let seed = 1;
4795
+ const rnd = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 0xffffffff; };
4796
+ const mk = () => { const o = {}; for (let f = 0; f < 20; f++) if (rnd() < 0.5) o['f' + f] = Math.floor(rnd() * 1e7); return o; };
4797
+ for (let i = 0; i < 500; i++) {
4798
+ uncapped.pack(mk()); // grows the sibling's dictionary freely
4799
+ const r = mk();
4800
+ assert.deepEqual(capped.unpack(capped.pack(r)), r);
4801
+ }
4802
+ assert.ok(capped.typedStructs.length <= 2, 'capped must stay bounded, got ' + capped.typedStructs.length);
4803
+ assert.ok(uncapped.typedStructs.length > 2, 'uncapped sibling should grow freely');
4804
+ });
4805
+
4806
+ test('a layout-retry record (large fixed section + nested refs) does not corrupt', function () {
4807
+ // A large fixed section overflows the ref-start estimate and triggers the internal retry
4808
+ // (which re-invokes writeStruct after refs were packed). The retry passes structureKnown=true
4809
+ // to re-encode the already-minted structure rather than bailing under the now-reached cap —
4810
+ // bailing there would write the fallback at an advanced position and corrupt the bytes.
4811
+ const packr = new Packr({ structures: [], useRecords: false, randomAccessStructure: true, maxOwnStructures: 1 });
4812
+ const norm = r => packr.unpack(packr.pack(r));
4813
+ const mk = base => { const r = {}; for (let i = 0; i < 40; i++) r['n' + i] = base + i; r.a = { x: base }; r.b = { y: base + 1 }; return r; };
4814
+ assert.deepEqual(norm(mk(1000000)), mk(1000000));
4815
+ assert.deepEqual(norm(mk(2000000)), mk(2000000));
4816
+ assert.ok(packr.typedStructs.length <= 1, 'retry-path records must not exceed the cap, got ' + packr.typedStructs.length);
4817
+ });
4818
+ });
4819
+
4567
4820
  })(chai, null, module, fs);
4568
4821
  //# sourceMappingURL=test.js.map