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/index-no-eval.cjs +8 -1
- package/dist/index-no-eval.cjs.map +1 -1
- package/dist/index-no-eval.min.js +1 -1
- package/dist/index-no-eval.min.js.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/node.cjs +107 -31
- package/dist/node.cjs.map +1 -1
- package/dist/test.js +316 -31
- package/dist/test.js.map +1 -1
- package/pack.js +8 -1
- package/package.json +1 -1
- package/struct.js +99 -30
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
|
-
//
|
|
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 =
|
|
2692
|
+
transition = forceTypeTransition(nextTransition, OBJECT_DATA, 2);
|
|
2652
2693
|
size = 2;
|
|
2653
2694
|
}
|
|
2654
2695
|
} else {
|
|
2655
|
-
transition = nextTransition.object32 ||
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
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
|