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/node.cjs +99 -30
- package/dist/node.cjs.map +1 -1
- package/dist/test.js +283 -30
- package/dist/test.js.map +1 -1
- package/package.json +1 -1
- package/struct.js +99 -30
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 =
|
|
2685
|
+
transition = forceTypeTransition(nextTransition, OBJECT_DATA, 2);
|
|
2652
2686
|
size = 2;
|
|
2653
2687
|
}
|
|
2654
2688
|
} else {
|
|
2655
|
-
transition = nextTransition.object32 ||
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
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
|