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/test.js CHANGED
@@ -1423,7 +1423,14 @@
1423
1423
  let newSharedData = prepareStructures$1(structures, packr);
1424
1424
  if (!encodingError) { // TODO: If there is an encoding error, should make the structures as uninitialized so they get rebuilt next time
1425
1425
  if (packr.saveStructures(newSharedData, newSharedData.isCompatible) === false) {
1426
- // get updated structures and try again if the update failed
1426
+ // The save was declined (a concurrent writer updated the shared structures,
1427
+ // or the store transaction did not durably commit). Our in-memory
1428
+ // structures + transition trie may now reference record ids that were
1429
+ // never persisted; re-packing as-is would re-emit the same record pointing
1430
+ // at an unpersisted structure (-> "Record id is not defined" on decode).
1431
+ // Mark structures uninitialized so the re-pack reloads durable structures
1432
+ // via getStructures, rebuilds the transition trie, and re-mints + re-saves.
1433
+ structures.uninitialized = true;
1427
1434
  return packr.pack(value, encodeOptions)
1428
1435
  }
1429
1436
  packr.lastNamedStructuresLength = sharedLength;
@@ -2399,9 +2406,16 @@
2399
2406
  return textEncoder.encodeInto(string, target.subarray(position)).written
2400
2407
  } : false;
2401
2408
  setWriteStructSlots(writeStruct, prepareStructures);
2402
- function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr) {
2409
+ function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr, structureKnown) {
2403
2410
  let typedStructs = packr.typedStructs || (packr.typedStructs = []);
2404
2411
  // note that we rely on pack.js to load stored structures before we get to this point
2412
+ // structureKnown is set only on the internal layout-retry below: attempt 1 already minted
2413
+ // this record's structure, so the retry re-encodes a known shape and must not re-apply the
2414
+ // cap (which could otherwise bail after attempt 1 already packed refs → corrupt fallback).
2415
+ // `frozen` is a local (from this instance's typedStructs) — never a shared global — so a
2416
+ // re-entrant encode on another instance (e.g. via an enumerable getter) can't flip it.
2417
+ const cap = packr.maxOwnStructures ?? Infinity;
2418
+ const frozen = !structureKnown && typedStructs.length >= cap;
2405
2419
  let targetView = target.dataView;
2406
2420
  let refsStartPosition = (typedStructs.lastStringStart || 100) + position;
2407
2421
  let safeEnd = target.length - 10;
@@ -2432,9 +2446,12 @@
2432
2446
  let usedAscii0;
2433
2447
  let keyIndex = 0;
2434
2448
  for (let key in object) {
2435
- let value = object[key];
2436
2449
  let nextTransition = transition[key];
2450
+ // Resolve the key transition BEFORE reading the value: when frozen and the key is new we
2451
+ // bail here, so an enumerable getter isn't invoked during this (failed) struct attempt and
2452
+ // then again by the plain fallback (which would double-read a side-effecting accessor).
2437
2453
  if (!nextTransition) {
2454
+ if (frozen) return 0;
2438
2455
  transition[key] = nextTransition = {
2439
2456
  key,
2440
2457
  parent: transition,
@@ -2449,6 +2466,7 @@
2449
2466
  date64: null
2450
2467
  };
2451
2468
  }
2469
+ let value = object[key];
2452
2470
  if (position > safeEnd) {
2453
2471
  target = makeRoom(position);
2454
2472
  targetView = target.dataView;
@@ -2466,10 +2484,10 @@
2466
2484
  if (nextId < 200 || !nextTransition.num64) {
2467
2485
  if (number >> 0 === number && number < 0x20000000 && number > -0x1f000000) {
2468
2486
  if (number < 0xf6 && number >= 0 && (nextTransition.num8 && !(nextId > 200 && nextTransition.num32) || number < 0x20 && !nextTransition.num32)) {
2469
- transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1);
2487
+ transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
2470
2488
  target[position++] = number;
2471
2489
  } else {
2472
- transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
2490
+ transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
2473
2491
  targetView.setUint32(position, number, true);
2474
2492
  position += 4;
2475
2493
  }
@@ -2480,14 +2498,14 @@
2480
2498
  let xShifted;
2481
2499
  // this checks for rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
2482
2500
  if (((xShifted = number * mult10[((target[position + 3] & 0x7f) << 1) | (target[position + 2] >> 7)]) >> 0) === xShifted) {
2483
- transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
2501
+ transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
2484
2502
  position += 4;
2485
2503
  break;
2486
2504
  }
2487
2505
  }
2488
2506
  }
2489
2507
  }
2490
- transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8);
2508
+ transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8, frozen);
2491
2509
  targetView.setFloat64(position, number, true);
2492
2510
  position += 8;
2493
2511
  break;
@@ -2553,21 +2571,21 @@
2553
2571
  nextTransition.string8 = transition;
2554
2572
  pack(null, 0, true); // special call to notify that structures have been updated
2555
2573
  } else {
2556
- transition = createTypeTransition(nextTransition, UTF8, 1);
2574
+ transition = createTypeTransition(nextTransition, UTF8, 1, frozen);
2557
2575
  }
2558
2576
  }
2559
2577
  } else if (refOffset === 0 && !usedAscii0) {
2560
2578
  usedAscii0 = true;
2561
- transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0);
2579
+ transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0, frozen);
2562
2580
  break; // don't increment position
2563
2581
  }// else ascii:
2564
2582
  else if (!(transition = nextTransition.ascii8) && !(typedStructs.length > 10 && (transition = nextTransition.string8)))
2565
- transition = createTypeTransition(nextTransition, ASCII, 1);
2583
+ transition = createTypeTransition(nextTransition, ASCII, 1, frozen);
2566
2584
  target[position++] = refOffset;
2567
2585
  } else {
2568
2586
  // TODO: Enable ascii16 at some point, but get the logic right
2569
2587
  //if (isNotAscii)
2570
- transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2);
2588
+ transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2, frozen);
2571
2589
  //else
2572
2590
  //transition = nextTransition.ascii16 || createTypeTransition(nextTransition, ASCII, 2);
2573
2591
  targetView.setUint16(position, refOffset, true);
@@ -2577,7 +2595,7 @@
2577
2595
  case 'object':
2578
2596
  if (value) {
2579
2597
  if (value.constructor === Date) {
2580
- transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8);
2598
+ transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8, frozen);
2581
2599
  targetView.setFloat64(position, value.getTime(), true);
2582
2600
  position += 8;
2583
2601
  } else {
@@ -2593,7 +2611,7 @@
2593
2611
  }
2594
2612
  break;
2595
2613
  case 'boolean':
2596
- transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1);
2614
+ transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
2597
2615
  target[position++] = value ? 0xf9 : 0xf8; // match CBOR with these
2598
2616
  break;
2599
2617
  case 'undefined':
@@ -2606,9 +2624,41 @@
2606
2624
  default:
2607
2625
  queuedReferences.push(key, value, keyIndex);
2608
2626
  }
2627
+ if (transition === undefined) return 0; // frozen: structure cap reached
2609
2628
  keyIndex++;
2610
2629
  }
2611
2630
 
2631
+ // Cap enforcement for queued (nested-object / null) references. pack() advances msgpackr's
2632
+ // shared write position and we cannot cleanly bail afterward, so preflight the whole queued
2633
+ // chain through EXISTING transitions first: if the cap is reached and any field would need a
2634
+ // new structure, fall back to plain encoding now (return 0) — before touching the shared
2635
+ // position. Uses a FRESH length read (not the entry-time `frozen`): a getter invoked while
2636
+ // reading values above may have minted on this same instance since entry.
2637
+ if (!structureKnown && queuedReferences.length > 0 && typedStructs.length >= cap) {
2638
+ let t = transition;
2639
+ for (let i = 0, l = queuedReferences.length; i < l; i += 3) {
2640
+ // A non-null (object/Date) ref is pack()ed into the shared buffer, advancing
2641
+ // msgpackr's write position. Its structure variant (object16 vs object32) depends on
2642
+ // the runtime ref-section offset (inline strings + earlier refs), which we can't know
2643
+ // before packing — and we can't bail after a pack without corrupting the fallback. So
2644
+ // under the cap, any record with a packing ref falls back to plain encoding now,
2645
+ // before any pack(). null/undefined refs don't pack, so they're walked normally.
2646
+ if (queuedReferences[i + 1] != null) return 0;
2647
+ const nt = t[queuedReferences[i]];
2648
+ if (!nt) return 0;
2649
+ const next = nt.object16; // null/undefined ref → OBJECT_DATA size 2
2650
+ if (!next) return 0;
2651
+ t = next;
2652
+ }
2653
+ if (t[RECORD_SYMBOL] == null) return 0; // exact structure not yet minted
2654
+ }
2655
+
2656
+ // Past the preflight the chain is known, so no minting happens — except a rare offset
2657
+ // divergence (a known shape whose ref section now crosses 0xff00 and needs object32 where
2658
+ // the preflight matched object16). Once a ref is packed we can no longer bail, so we finish
2659
+ // via the unfrozen forceTypeTransition: a bounded, self-converging overshoot for that one
2660
+ // record. packedRef keeps the record-id mint from bailing after a pack.
2661
+ let packedRef = false;
2612
2662
  for (let i = 0, l = queuedReferences.length; i < l;) {
2613
2663
  let key = queuedReferences[i++];
2614
2664
  let value = queuedReferences[i++];
@@ -2630,15 +2680,6 @@
2630
2680
  }
2631
2681
  let newPosition;
2632
2682
  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
2683
  let size;
2643
2684
  refOffset = refPosition - refsStartPosition;
2644
2685
  if (refOffset < 0xff00) {
@@ -2648,15 +2689,15 @@
2648
2689
  else if ((transition = nextTransition.object32))
2649
2690
  size = 4;
2650
2691
  else {
2651
- transition = createTypeTransition(nextTransition, OBJECT_DATA, 2);
2692
+ transition = forceTypeTransition(nextTransition, OBJECT_DATA, 2);
2652
2693
  size = 2;
2653
2694
  }
2654
2695
  } else {
2655
- transition = nextTransition.object32 || createTypeTransition(nextTransition, OBJECT_DATA, 4);
2696
+ transition = nextTransition.object32 || forceTypeTransition(nextTransition, OBJECT_DATA, 4);
2656
2697
  size = 4;
2657
2698
  }
2658
2699
  newPosition = pack(value, refPosition);
2659
- //}
2700
+ packedRef = true;
2660
2701
  if (typeof newPosition === 'object') {
2661
2702
  // re-allocated
2662
2703
  refPosition = newPosition.position;
@@ -2676,16 +2717,19 @@
2676
2717
  position += 4;
2677
2718
  }
2678
2719
  } else { // null or undefined
2679
- transition = nextTransition.object16 || createTypeTransition(nextTransition, OBJECT_DATA, 2);
2720
+ transition = nextTransition.object16 || forceTypeTransition(nextTransition, OBJECT_DATA, 2);
2680
2721
  targetView.setInt16(position, value === null ? -10 : -9, true);
2681
2722
  position += 2;
2682
2723
  }
2683
2724
  keyIndex++;
2684
2725
  }
2685
2726
 
2686
-
2687
2727
  let recordId = transition[RECORD_SYMBOL];
2688
2728
  if (recordId == null) {
2729
+ // Flat records (no queued refs) reach here without packing, so the cap is enforced
2730
+ // cleanly. Records that packed nested refs already passed the preflight; either way
2731
+ // bailing now after refs were packed would corrupt the fallback.
2732
+ if (!packedRef && typedStructs.length >= cap) return 0;
2689
2733
  recordId = packr.typedStructs.length;
2690
2734
  let structure = [];
2691
2735
  let nextTransition = transition;
@@ -2739,7 +2783,11 @@
2739
2783
  if (refsStartPosition === refPosition)
2740
2784
  return position; // no refs
2741
2785
  typedStructs.lastStringStart = position - start;
2742
- return writeStruct(object, target, encodingStart, start, structures, makeRoom, pack, packr);
2786
+ // Fixed section overflowed our estimate retry with the corrected size. The structure
2787
+ // is already minted at this point, so pass structureKnown=true to skip the cap check
2788
+ // (otherwise a record that became frozen during attempt 1 would bail mid-retry, after
2789
+ // refs were already packed, and corrupt the fallback).
2790
+ return writeStruct(object, target, encodingStart, start, structures, makeRoom, pack, packr, true);
2743
2791
  }
2744
2792
  return refPosition;
2745
2793
  }
@@ -2771,9 +2819,36 @@
2771
2819
  // TODO: can we do an "any" type where we defer the decision?
2772
2820
  return;
2773
2821
  }
2774
- function createTypeTransition(transition, type, size) {
2822
+ // When the typed-structure dictionary reaches maxOwnStructures we stop minting new
2823
+ // structures/transitions. typedStructs is append-only and pinned on the long-lived
2824
+ // encoder (records reference structures by recordId), so an unbounded shape space —
2825
+ // e.g. a wide, sparsely/variably-populated schema — would otherwise grow the
2826
+ // dictionary + transition trie without limit. `frozen` is passed in (derived from the
2827
+ // encoding instance's own typedStructs.length, never a shared global) so a re-entrant
2828
+ // encode on another instance can't flip it; while frozen, a missing transition returns
2829
+ // undefined so the caller bails and the record falls back to plain encoding.
2830
+ function createTypeTransition(transition, type, size, frozen) {
2775
2831
  let typeName = TYPE_NAMES[type] + (size << 3);
2776
- let newTransition = transition[typeName] || (transition[typeName] = Object.create(null));
2832
+ let newTransition = transition[typeName];
2833
+ if (newTransition) return newTransition;
2834
+ if (frozen) return undefined;
2835
+ newTransition = transition[typeName] = Object.create(null);
2836
+ newTransition.__type = type;
2837
+ newTransition.__size = size;
2838
+ newTransition.__parent = transition;
2839
+ return newTransition;
2840
+ }
2841
+
2842
+ // Unfrozen variant: always mints. Used in the queued-ref loop once a nested value has
2843
+ // already been pack()ed — at that point pack() has advanced msgpackr's shared write
2844
+ // position, so bailing with `return 0` would corrupt the fallback. We must finish the
2845
+ // encode instead, even if that means minting a (bounded) handful of structures past the
2846
+ // cap. The cap is still enforced up front via the preflight, before the first pack().
2847
+ function forceTypeTransition(transition, type, size) {
2848
+ let typeName = TYPE_NAMES[type] + (size << 3);
2849
+ let newTransition = transition[typeName];
2850
+ if (newTransition) return newTransition;
2851
+ newTransition = transition[typeName] = Object.create(null);
2777
2852
  newTransition.__type = type;
2778
2853
  newTransition.__size = size;
2779
2854
  newTransition.__parent = transition;
@@ -2807,7 +2882,8 @@
2807
2882
  date64: null,
2808
2883
  };
2809
2884
  }
2810
- transition = createTypeTransition(nextTransition, type, size);
2885
+ // Replaying persisted structures is never subject to the cap — always mint.
2886
+ transition = createTypeTransition(nextTransition, type, size, false);
2811
2887
  }
2812
2888
  transition[RECORD_SYMBOL] = i;
2813
2889
  }
@@ -4150,6 +4226,31 @@
4150
4226
  assert.deepEqual(inputData, outputData);
4151
4227
  });
4152
4228
 
4229
+ test('declined structure save re-packs against durable structures (no dangling record)', function() {
4230
+ // Regression: when saveStructures declines a save (a concurrent writer updated the shared
4231
+ // structures, or the store txn did not durably commit), the in-memory structures/transition
4232
+ // trie reference a record id that was never persisted. The re-pack must reload the durable
4233
+ // structures and re-mint/re-save — otherwise it re-emits the same record pointing at an
4234
+ // unsaved structure, and reads throw "Record id is not defined".
4235
+ const meta = new Packr();
4236
+ let store = null;
4237
+ let declinedOnce = false;
4238
+ const packr = new Packr({
4239
+ useRecords: true,
4240
+ getStructures() { return store ? meta.unpack(store) : undefined },
4241
+ saveStructures(structures) {
4242
+ if (!declinedOnce) { declinedOnce = true; return false } // decline the first save
4243
+ store = meta.pack(structures); return true
4244
+ },
4245
+ });
4246
+ const a = packr.pack({ x: 9, y: 8 }); // first mint is declined -> must re-pack + re-save
4247
+ const b = packr.pack({ x: 7, y: 6 }); // same shape -> must still reference a saved structure
4248
+ // A fresh reader sees only the durably-saved structures (another thread / post-restart):
4249
+ const reader = new Packr({ getStructures() { return store ? meta.unpack(store) : undefined } });
4250
+ assert.deepEqual(reader.unpack(a), { x: 9, y: 8 });
4251
+ assert.deepEqual(reader.unpack(b), { x: 7, y: 6 });
4252
+ });
4253
+
4153
4254
  test('big buffer', function() {
4154
4255
  var size = 100000000;
4155
4256
  var data = new Uint8Array(size).fill(1);
@@ -4564,5 +4665,189 @@
4564
4665
  });
4565
4666
  });
4566
4667
 
4668
+ suite('msgpackr – maxOwnStructures cap (randomAccessStructure)', function () {
4669
+ // Helper: create a width-heterogeneous record generator using a deterministic PRNG.
4670
+ // Objects have up to 6 fields drawn from a sparse set, each value is an integer whose
4671
+ // width (num8 / num32 / num64) varies per value, producing many distinct typed structures.
4672
+ // useRecords: false keeps the classic named-record encoder out of the way so all structure
4673
+ // creation goes through the typed-struct path and the cap is exercised in isolation.
4674
+ function makeCapRunner(cap) {
4675
+ const fields = ['a','b','c','d','e','f','g','h'];
4676
+ let seed = 42;
4677
+ const rnd = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 0xffffffff; };
4678
+ const packr = new Packr({
4679
+ structures: [],
4680
+ useRecords: false,
4681
+ randomAccessStructure: true,
4682
+ maxOwnStructures: cap,
4683
+ });
4684
+ const norm = r => packr.unpack(packr.pack(r));
4685
+ for (let i = 0; i < 4000; i++) {
4686
+ const r = {};
4687
+ for (let j = 0; j < Math.ceil(rnd() * 6); j++) {
4688
+ // values cycle across num8 / num32 / float64 ranges to force distinct structures
4689
+ const mag = rnd() < 0.33 ? 10 : rnd() < 0.5 ? 400000 : 1e13;
4690
+ r[fields[Math.floor(rnd() * 8)]] = Math.floor(rnd() * mag);
4691
+ }
4692
+ assert.deepEqual(norm(r), r);
4693
+ }
4694
+ return packr.typedStructs ? packr.typedStructs.length : 0;
4695
+ }
4696
+
4697
+ test('uncapped (default) grows well past 256 for width-heterogeneous records', function () {
4698
+ assert.ok(makeCapRunner(undefined) > 256, 'expected uncapped typedStructs to exceed 256');
4699
+ });
4700
+
4701
+ test('cap=64 bounds typedStructs.length and preserves round-trips', function () {
4702
+ assert.ok(makeCapRunner(64) <= 64, 'typedStructs should not exceed cap of 64');
4703
+ });
4704
+
4705
+ test('cap=256 bounds typedStructs.length and preserves round-trips', function () {
4706
+ assert.ok(makeCapRunner(256) <= 256, 'typedStructs should not exceed cap of 256');
4707
+ });
4708
+
4709
+ test('flat-record streams stay a strict hard bound', function () {
4710
+ // With maxOwnStructures=16, pack 2000 flat records; typedStructs must never exceed 16.
4711
+ const packr = new Packr({
4712
+ structures: [],
4713
+ useRecords: false,
4714
+ randomAccessStructure: true,
4715
+ maxOwnStructures: 16,
4716
+ });
4717
+ let seed = 99;
4718
+ const rnd = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 0xffffffff; };
4719
+ for (let i = 0; i < 2000; i++) {
4720
+ const r = { x: Math.floor(rnd() * 1e6), y: Math.floor(rnd() * 200), z: Math.floor(rnd() * 1e12) };
4721
+ assert.deepEqual(packr.unpack(packr.pack(r)), r);
4722
+ }
4723
+ assert.ok(packr.typedStructs.length <= 16, 'flat records must stay within cap, got ' + packr.typedStructs.length);
4724
+ });
4725
+
4726
+ test('capped-out records fall back to plain encoding and still round-trip', function () {
4727
+ // Once the cap is hit, novel shapes must decode correctly via plain msgpack fallback.
4728
+ const packr = new Packr({
4729
+ structures: [],
4730
+ useRecords: false,
4731
+ randomAccessStructure: true,
4732
+ maxOwnStructures: 2,
4733
+ });
4734
+ const norm = r => packr.unpack(packr.pack(r));
4735
+ assert.deepEqual(norm({ a: 1 }), { a: 1 }); // mints structure 0
4736
+ assert.deepEqual(norm({ b: 'hello' }), { b: 'hello' }); // mints structure 1, cap hit
4737
+ // These novel shapes fall back to plain msgpack — must still decode correctly:
4738
+ assert.deepEqual(norm({ c: 42 }), { c: 42 });
4739
+ assert.deepEqual(norm({ a: 1, b: 'hi', c: 99 }), { a: 1, b: 'hi', c: 99 });
4740
+ assert.strictEqual(packr.typedStructs.length, 2, 'cap must be exact');
4741
+ });
4742
+
4743
+ test('a known key later seen as a nested object falls back cleanly', function () {
4744
+ // Once frozen, a previously-learned scalar key carrying an object must bail BEFORE pack()
4745
+ // advances the shared encoder position — otherwise the plain fallback gets corrupt bytes.
4746
+ const packr = new Packr({
4747
+ structures: [],
4748
+ useRecords: false,
4749
+ randomAccessStructure: true,
4750
+ maxOwnStructures: 1,
4751
+ });
4752
+ const norm = r => packr.unpack(packr.pack(r));
4753
+ assert.deepEqual(norm({ a: 1 }), { a: 1 }); // mints structure 0, cap hit
4754
+ const r2 = { a: { x: 1 } };
4755
+ assert.deepEqual(norm(r2), r2); // 'a' known but now carries object — must fall back cleanly
4756
+ });
4757
+
4758
+ test('nested records do not overshoot the cap and still round-trip', function () {
4759
+ // A nested object mints its own structure before the outer record, so a stale frozen flag
4760
+ // could push the outer record past the cap. The record-id mint guard re-checks the live
4761
+ // length, keeping typedStructs.length a strict bound.
4762
+ const packr = new Packr({
4763
+ structures: [],
4764
+ useRecords: false,
4765
+ randomAccessStructure: true,
4766
+ maxOwnStructures: 4,
4767
+ });
4768
+ let seed = 7;
4769
+ const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
4770
+ for (let i = 0; i < 1000; i++) {
4771
+ const r = { outer: { inner: (rnd() * 1e7 | 0) * 1000 }, tag: 't' + (i % 20), n: (rnd() * 300 | 0) };
4772
+ assert.deepEqual(packr.unpack(packr.pack(r)), r);
4773
+ }
4774
+ assert.ok(packr.typedStructs.length <= 4, 'nested encodes must not push typedStructs past the cap, got ' + packr.typedStructs.length);
4775
+ });
4776
+
4777
+ test('persisted typed structures still load after a capped encoder froze the dictionary', function () {
4778
+ // Replaying persisted structures in onLoadedStructures must always succeed, regardless of
4779
+ // maxOwnStructures — the cap only limits minting NEW structures during encode.
4780
+ let saved = null;
4781
+ const writer = new Packr({
4782
+ structures: [],
4783
+ useRecords: false,
4784
+ randomAccessStructure: true,
4785
+ saveStructures(s) { saved = s; return true; },
4786
+ getStructures() { return saved; },
4787
+ });
4788
+ const buf = writer.pack({ name: 'Alice', age: 30 });
4789
+
4790
+ // Warm up a capped encoder so the module-global-if-any freeze state is set.
4791
+ const capped = new Packr({
4792
+ structures: [],
4793
+ useRecords: false,
4794
+ randomAccessStructure: true,
4795
+ maxOwnStructures: 1,
4796
+ });
4797
+ capped.pack({ x: 1 });
4798
+ capped.pack({ y: 2, z: 3 }); // cap reached
4799
+
4800
+ // A fresh reader must still rebuild the transition trie from saved structures.
4801
+ const reader = new Packr({
4802
+ structures: [],
4803
+ useRecords: false,
4804
+ randomAccessStructure: true,
4805
+ getStructures() { return saved; },
4806
+ });
4807
+ const result = reader.unpack(buf);
4808
+ assert.equal(result.name, 'Alice');
4809
+ assert.equal(result.age, 30);
4810
+ });
4811
+
4812
+ test('the cap is per-instance: an uncapped sibling cannot lift this instance\'s cap', function () {
4813
+ // frozen is derived from each encoder's own typedStructs.length — not a shared global —
4814
+ // so an uncapped sibling churning out structures cannot lift the cap on the bounded one.
4815
+ const uncapped = new Packr({
4816
+ structures: [],
4817
+ useRecords: false,
4818
+ randomAccessStructure: true,
4819
+ });
4820
+ const capped = new Packr({
4821
+ structures: [],
4822
+ useRecords: false,
4823
+ randomAccessStructure: true,
4824
+ maxOwnStructures: 2,
4825
+ });
4826
+ let seed = 1;
4827
+ const rnd = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 0xffffffff; };
4828
+ const mk = () => { const o = {}; for (let f = 0; f < 20; f++) if (rnd() < 0.5) o['f' + f] = Math.floor(rnd() * 1e7); return o; };
4829
+ for (let i = 0; i < 500; i++) {
4830
+ uncapped.pack(mk()); // grows the sibling's dictionary freely
4831
+ const r = mk();
4832
+ assert.deepEqual(capped.unpack(capped.pack(r)), r);
4833
+ }
4834
+ assert.ok(capped.typedStructs.length <= 2, 'capped must stay bounded, got ' + capped.typedStructs.length);
4835
+ assert.ok(uncapped.typedStructs.length > 2, 'uncapped sibling should grow freely');
4836
+ });
4837
+
4838
+ test('a layout-retry record (large fixed section + nested refs) does not corrupt', function () {
4839
+ // A large fixed section overflows the ref-start estimate and triggers the internal retry
4840
+ // (which re-invokes writeStruct after refs were packed). The retry passes structureKnown=true
4841
+ // to re-encode the already-minted structure rather than bailing under the now-reached cap —
4842
+ // bailing there would write the fallback at an advanced position and corrupt the bytes.
4843
+ const packr = new Packr({ structures: [], useRecords: false, randomAccessStructure: true, maxOwnStructures: 1 });
4844
+ const norm = r => packr.unpack(packr.pack(r));
4845
+ 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; };
4846
+ assert.deepEqual(norm(mk(1000000)), mk(1000000));
4847
+ assert.deepEqual(norm(mk(2000000)), mk(2000000));
4848
+ assert.ok(packr.typedStructs.length <= 1, 'retry-path records must not exceed the cap, got ' + packr.typedStructs.length);
4849
+ });
4850
+ });
4851
+
4567
4852
  })(chai, null, module, fs);
4568
4853
  //# sourceMappingURL=test.js.map