msgpackr 1.11.11 → 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 +114 -40
- 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/stream.js +15 -10
- package/struct.js +99 -30
package/dist/node.cjs
CHANGED
|
@@ -2406,9 +2406,16 @@ const encodeUtf8 = hasNodeBuffer ? function(target, string, position) {
|
|
|
2406
2406
|
return textEncoder.encodeInto(string, target.subarray(position)).written
|
|
2407
2407
|
} : false;
|
|
2408
2408
|
setWriteStructSlots(writeStruct, prepareStructures);
|
|
2409
|
-
function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr) {
|
|
2409
|
+
function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr, structureKnown) {
|
|
2410
2410
|
let typedStructs = packr.typedStructs || (packr.typedStructs = []);
|
|
2411
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;
|
|
2412
2419
|
let targetView = target.dataView;
|
|
2413
2420
|
let refsStartPosition = (typedStructs.lastStringStart || 100) + position;
|
|
2414
2421
|
let safeEnd = target.length - 10;
|
|
@@ -2439,9 +2446,12 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2439
2446
|
let usedAscii0;
|
|
2440
2447
|
let keyIndex = 0;
|
|
2441
2448
|
for (let key in object) {
|
|
2442
|
-
let value = object[key];
|
|
2443
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).
|
|
2444
2453
|
if (!nextTransition) {
|
|
2454
|
+
if (frozen) return 0;
|
|
2445
2455
|
transition[key] = nextTransition = {
|
|
2446
2456
|
key,
|
|
2447
2457
|
parent: transition,
|
|
@@ -2456,6 +2466,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2456
2466
|
date64: null
|
|
2457
2467
|
};
|
|
2458
2468
|
}
|
|
2469
|
+
let value = object[key];
|
|
2459
2470
|
if (position > safeEnd) {
|
|
2460
2471
|
target = makeRoom(position);
|
|
2461
2472
|
targetView = target.dataView;
|
|
@@ -2473,10 +2484,10 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2473
2484
|
if (nextId < 200 || !nextTransition.num64) {
|
|
2474
2485
|
if (number >> 0 === number && number < 0x20000000 && number > -0x1f000000) {
|
|
2475
2486
|
if (number < 0xf6 && number >= 0 && (nextTransition.num8 && !(nextId > 200 && nextTransition.num32) || number < 0x20 && !nextTransition.num32)) {
|
|
2476
|
-
transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1);
|
|
2487
|
+
transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
|
|
2477
2488
|
target[position++] = number;
|
|
2478
2489
|
} else {
|
|
2479
|
-
transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
|
|
2490
|
+
transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
|
|
2480
2491
|
targetView.setUint32(position, number, true);
|
|
2481
2492
|
position += 4;
|
|
2482
2493
|
}
|
|
@@ -2487,14 +2498,14 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2487
2498
|
let xShifted;
|
|
2488
2499
|
// this checks for rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
|
|
2489
2500
|
if (((xShifted = number * mult10[((target[position + 3] & 0x7f) << 1) | (target[position + 2] >> 7)]) >> 0) === xShifted) {
|
|
2490
|
-
transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
|
|
2501
|
+
transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
|
|
2491
2502
|
position += 4;
|
|
2492
2503
|
break;
|
|
2493
2504
|
}
|
|
2494
2505
|
}
|
|
2495
2506
|
}
|
|
2496
2507
|
}
|
|
2497
|
-
transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8);
|
|
2508
|
+
transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8, frozen);
|
|
2498
2509
|
targetView.setFloat64(position, number, true);
|
|
2499
2510
|
position += 8;
|
|
2500
2511
|
break;
|
|
@@ -2560,21 +2571,21 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2560
2571
|
nextTransition.string8 = transition;
|
|
2561
2572
|
pack(null, 0, true); // special call to notify that structures have been updated
|
|
2562
2573
|
} else {
|
|
2563
|
-
transition = createTypeTransition(nextTransition, UTF8, 1);
|
|
2574
|
+
transition = createTypeTransition(nextTransition, UTF8, 1, frozen);
|
|
2564
2575
|
}
|
|
2565
2576
|
}
|
|
2566
2577
|
} else if (refOffset === 0 && !usedAscii0) {
|
|
2567
2578
|
usedAscii0 = true;
|
|
2568
|
-
transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0);
|
|
2579
|
+
transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0, frozen);
|
|
2569
2580
|
break; // don't increment position
|
|
2570
2581
|
}// else ascii:
|
|
2571
2582
|
else if (!(transition = nextTransition.ascii8) && !(typedStructs.length > 10 && (transition = nextTransition.string8)))
|
|
2572
|
-
transition = createTypeTransition(nextTransition, ASCII, 1);
|
|
2583
|
+
transition = createTypeTransition(nextTransition, ASCII, 1, frozen);
|
|
2573
2584
|
target[position++] = refOffset;
|
|
2574
2585
|
} else {
|
|
2575
2586
|
// TODO: Enable ascii16 at some point, but get the logic right
|
|
2576
2587
|
//if (isNotAscii)
|
|
2577
|
-
transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2);
|
|
2588
|
+
transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2, frozen);
|
|
2578
2589
|
//else
|
|
2579
2590
|
//transition = nextTransition.ascii16 || createTypeTransition(nextTransition, ASCII, 2);
|
|
2580
2591
|
targetView.setUint16(position, refOffset, true);
|
|
@@ -2584,7 +2595,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2584
2595
|
case 'object':
|
|
2585
2596
|
if (value) {
|
|
2586
2597
|
if (value.constructor === Date) {
|
|
2587
|
-
transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8);
|
|
2598
|
+
transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8, frozen);
|
|
2588
2599
|
targetView.setFloat64(position, value.getTime(), true);
|
|
2589
2600
|
position += 8;
|
|
2590
2601
|
} else {
|
|
@@ -2600,7 +2611,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2600
2611
|
}
|
|
2601
2612
|
break;
|
|
2602
2613
|
case 'boolean':
|
|
2603
|
-
transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1);
|
|
2614
|
+
transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
|
|
2604
2615
|
target[position++] = value ? 0xf9 : 0xf8; // match CBOR with these
|
|
2605
2616
|
break;
|
|
2606
2617
|
case 'undefined':
|
|
@@ -2613,9 +2624,41 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2613
2624
|
default:
|
|
2614
2625
|
queuedReferences.push(key, value, keyIndex);
|
|
2615
2626
|
}
|
|
2627
|
+
if (transition === undefined) return 0; // frozen: structure cap reached
|
|
2616
2628
|
keyIndex++;
|
|
2617
2629
|
}
|
|
2618
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;
|
|
2619
2662
|
for (let i = 0, l = queuedReferences.length; i < l;) {
|
|
2620
2663
|
let key = queuedReferences[i++];
|
|
2621
2664
|
let value = queuedReferences[i++];
|
|
@@ -2637,15 +2680,6 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2637
2680
|
}
|
|
2638
2681
|
let newPosition;
|
|
2639
2682
|
if (value) {
|
|
2640
|
-
/*if (typeof value === 'string') { // TODO: we could re-enable long strings
|
|
2641
|
-
if (position + value.length * 3 > safeEnd) {
|
|
2642
|
-
target = makeRoom(position + value.length * 3);
|
|
2643
|
-
position -= start;
|
|
2644
|
-
targetView = target.dataView;
|
|
2645
|
-
start = 0;
|
|
2646
|
-
}
|
|
2647
|
-
newPosition = position + target.utf8Write(value, position, 0xffffffff);
|
|
2648
|
-
} else { */
|
|
2649
2683
|
let size;
|
|
2650
2684
|
refOffset = refPosition - refsStartPosition;
|
|
2651
2685
|
if (refOffset < 0xff00) {
|
|
@@ -2655,15 +2689,15 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2655
2689
|
else if ((transition = nextTransition.object32))
|
|
2656
2690
|
size = 4;
|
|
2657
2691
|
else {
|
|
2658
|
-
transition =
|
|
2692
|
+
transition = forceTypeTransition(nextTransition, OBJECT_DATA, 2);
|
|
2659
2693
|
size = 2;
|
|
2660
2694
|
}
|
|
2661
2695
|
} else {
|
|
2662
|
-
transition = nextTransition.object32 ||
|
|
2696
|
+
transition = nextTransition.object32 || forceTypeTransition(nextTransition, OBJECT_DATA, 4);
|
|
2663
2697
|
size = 4;
|
|
2664
2698
|
}
|
|
2665
2699
|
newPosition = pack(value, refPosition);
|
|
2666
|
-
|
|
2700
|
+
packedRef = true;
|
|
2667
2701
|
if (typeof newPosition === 'object') {
|
|
2668
2702
|
// re-allocated
|
|
2669
2703
|
refPosition = newPosition.position;
|
|
@@ -2683,16 +2717,19 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2683
2717
|
position += 4;
|
|
2684
2718
|
}
|
|
2685
2719
|
} else { // null or undefined
|
|
2686
|
-
transition = nextTransition.object16 ||
|
|
2720
|
+
transition = nextTransition.object16 || forceTypeTransition(nextTransition, OBJECT_DATA, 2);
|
|
2687
2721
|
targetView.setInt16(position, value === null ? -10 : -9, true);
|
|
2688
2722
|
position += 2;
|
|
2689
2723
|
}
|
|
2690
2724
|
keyIndex++;
|
|
2691
2725
|
}
|
|
2692
2726
|
|
|
2693
|
-
|
|
2694
2727
|
let recordId = transition[RECORD_SYMBOL];
|
|
2695
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;
|
|
2696
2733
|
recordId = packr.typedStructs.length;
|
|
2697
2734
|
let structure = [];
|
|
2698
2735
|
let nextTransition = transition;
|
|
@@ -2746,7 +2783,11 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
2746
2783
|
if (refsStartPosition === refPosition)
|
|
2747
2784
|
return position; // no refs
|
|
2748
2785
|
typedStructs.lastStringStart = position - start;
|
|
2749
|
-
|
|
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);
|
|
2750
2791
|
}
|
|
2751
2792
|
return refPosition;
|
|
2752
2793
|
}
|
|
@@ -2778,9 +2819,36 @@ function anyType(transition, position, targetView, value) {
|
|
|
2778
2819
|
// TODO: can we do an "any" type where we defer the decision?
|
|
2779
2820
|
return;
|
|
2780
2821
|
}
|
|
2781
|
-
|
|
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) {
|
|
2782
2831
|
let typeName = TYPE_NAMES[type] + (size << 3);
|
|
2783
|
-
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);
|
|
2784
2852
|
newTransition.__type = type;
|
|
2785
2853
|
newTransition.__size = size;
|
|
2786
2854
|
newTransition.__parent = transition;
|
|
@@ -2814,7 +2882,8 @@ function onLoadedStructures(sharedData) {
|
|
|
2814
2882
|
date64: null,
|
|
2815
2883
|
};
|
|
2816
2884
|
}
|
|
2817
|
-
|
|
2885
|
+
// Replaying persisted structures is never subject to the cap — always mint.
|
|
2886
|
+
transition = createTypeTransition(nextTransition, type, size, false);
|
|
2818
2887
|
}
|
|
2819
2888
|
transition[RECORD_SYMBOL] = i;
|
|
2820
2889
|
}
|
|
@@ -3172,6 +3241,7 @@ class UnpackrStream extends stream.Transform {
|
|
|
3172
3241
|
options.objectMode = true;
|
|
3173
3242
|
super(options);
|
|
3174
3243
|
options.structures = [];
|
|
3244
|
+
this.maxIncompleteBufferSize = options.maxIncompleteBufferSize !== undefined ? options.maxIncompleteBufferSize : 0x4000000;
|
|
3175
3245
|
this.unpackr = options.unpackr || new Unpackr(options);
|
|
3176
3246
|
}
|
|
3177
3247
|
_transform(chunk, encoding, callback) {
|
|
@@ -3184,19 +3254,23 @@ class UnpackrStream extends stream.Transform {
|
|
|
3184
3254
|
values = this.unpackr.unpackMultiple(chunk);
|
|
3185
3255
|
} catch(error) {
|
|
3186
3256
|
if (error.incomplete) {
|
|
3187
|
-
|
|
3257
|
+
let incompleteBuffer = chunk.slice(error.lastPosition);
|
|
3258
|
+
if (incompleteBuffer.length > this.maxIncompleteBufferSize) {
|
|
3259
|
+
this.incompleteBuffer = null;
|
|
3260
|
+
return callback(new Error('Maximum incomplete buffer size exceeded'))
|
|
3261
|
+
}
|
|
3262
|
+
this.incompleteBuffer = incompleteBuffer;
|
|
3188
3263
|
values = error.values;
|
|
3189
|
-
}
|
|
3190
|
-
|
|
3191
|
-
throw error
|
|
3192
|
-
} finally {
|
|
3193
|
-
for (let value of values || []) {
|
|
3194
|
-
if (value === null)
|
|
3195
|
-
value = this.getNullValue();
|
|
3196
|
-
this.push(value);
|
|
3264
|
+
} else {
|
|
3265
|
+
return callback(error)
|
|
3197
3266
|
}
|
|
3198
3267
|
}
|
|
3199
|
-
|
|
3268
|
+
for (let value of values || []) {
|
|
3269
|
+
if (value === null)
|
|
3270
|
+
value = this.getNullValue();
|
|
3271
|
+
this.push(value);
|
|
3272
|
+
}
|
|
3273
|
+
callback();
|
|
3200
3274
|
}
|
|
3201
3275
|
getNullValue() {
|
|
3202
3276
|
return Symbol.for(null)
|