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/package.json
CHANGED
package/stream.js
CHANGED
|
@@ -25,6 +25,7 @@ export class UnpackrStream extends Transform {
|
|
|
25
25
|
options.objectMode = true
|
|
26
26
|
super(options)
|
|
27
27
|
options.structures = []
|
|
28
|
+
this.maxIncompleteBufferSize = options.maxIncompleteBufferSize !== undefined ? options.maxIncompleteBufferSize : 0x4000000
|
|
28
29
|
this.unpackr = options.unpackr || new Unpackr(options)
|
|
29
30
|
}
|
|
30
31
|
_transform(chunk, encoding, callback) {
|
|
@@ -37,19 +38,23 @@ export class UnpackrStream extends Transform {
|
|
|
37
38
|
values = this.unpackr.unpackMultiple(chunk)
|
|
38
39
|
} catch(error) {
|
|
39
40
|
if (error.incomplete) {
|
|
40
|
-
|
|
41
|
+
let incompleteBuffer = chunk.slice(error.lastPosition)
|
|
42
|
+
if (incompleteBuffer.length > this.maxIncompleteBufferSize) {
|
|
43
|
+
this.incompleteBuffer = null
|
|
44
|
+
return callback(new Error('Maximum incomplete buffer size exceeded'))
|
|
45
|
+
}
|
|
46
|
+
this.incompleteBuffer = incompleteBuffer
|
|
41
47
|
values = error.values
|
|
48
|
+
} else {
|
|
49
|
+
return callback(error)
|
|
42
50
|
}
|
|
43
|
-
else
|
|
44
|
-
throw error
|
|
45
|
-
} finally {
|
|
46
|
-
for (let value of values || []) {
|
|
47
|
-
if (value === null)
|
|
48
|
-
value = this.getNullValue()
|
|
49
|
-
this.push(value)
|
|
50
|
-
}
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
for (let value of values || []) {
|
|
53
|
+
if (value === null)
|
|
54
|
+
value = this.getNullValue()
|
|
55
|
+
this.push(value)
|
|
56
|
+
}
|
|
57
|
+
callback()
|
|
53
58
|
}
|
|
54
59
|
getNullValue() {
|
|
55
60
|
return Symbol.for(null)
|
package/struct.js
CHANGED
|
@@ -69,9 +69,16 @@ const encodeUtf8 = hasNodeBuffer ? function(target, string, position) {
|
|
|
69
69
|
const TYPE = Symbol('type');
|
|
70
70
|
const PARENT = Symbol('parent');
|
|
71
71
|
setWriteStructSlots(writeStruct, prepareStructures);
|
|
72
|
-
function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr) {
|
|
72
|
+
function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr, structureKnown) {
|
|
73
73
|
let typedStructs = packr.typedStructs || (packr.typedStructs = []);
|
|
74
74
|
// note that we rely on pack.js to load stored structures before we get to this point
|
|
75
|
+
// structureKnown is set only on the internal layout-retry below: attempt 1 already minted
|
|
76
|
+
// this record's structure, so the retry re-encodes a known shape and must not re-apply the
|
|
77
|
+
// cap (which could otherwise bail after attempt 1 already packed refs → corrupt fallback).
|
|
78
|
+
// `frozen` is a local (from this instance's typedStructs) — never a shared global — so a
|
|
79
|
+
// re-entrant encode on another instance (e.g. via an enumerable getter) can't flip it.
|
|
80
|
+
const cap = packr.maxOwnStructures ?? Infinity;
|
|
81
|
+
const frozen = !structureKnown && typedStructs.length >= cap;
|
|
75
82
|
let targetView = target.dataView;
|
|
76
83
|
let refsStartPosition = (typedStructs.lastStringStart || 100) + position;
|
|
77
84
|
let safeEnd = target.length - 10;
|
|
@@ -102,9 +109,12 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
102
109
|
let usedAscii0;
|
|
103
110
|
let keyIndex = 0;
|
|
104
111
|
for (let key in object) {
|
|
105
|
-
let value = object[key];
|
|
106
112
|
let nextTransition = transition[key];
|
|
113
|
+
// Resolve the key transition BEFORE reading the value: when frozen and the key is new we
|
|
114
|
+
// bail here, so an enumerable getter isn't invoked during this (failed) struct attempt and
|
|
115
|
+
// then again by the plain fallback (which would double-read a side-effecting accessor).
|
|
107
116
|
if (!nextTransition) {
|
|
117
|
+
if (frozen) return 0;
|
|
108
118
|
transition[key] = nextTransition = {
|
|
109
119
|
key,
|
|
110
120
|
parent: transition,
|
|
@@ -119,6 +129,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
119
129
|
date64: null
|
|
120
130
|
};
|
|
121
131
|
}
|
|
132
|
+
let value = object[key];
|
|
122
133
|
if (position > safeEnd) {
|
|
123
134
|
target = makeRoom(position);
|
|
124
135
|
targetView = target.dataView;
|
|
@@ -136,10 +147,10 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
136
147
|
if (nextId < 200 || !nextTransition.num64) {
|
|
137
148
|
if (number >> 0 === number && number < 0x20000000 && number > -0x1f000000) {
|
|
138
149
|
if (number < 0xf6 && number >= 0 && (nextTransition.num8 && !(nextId > 200 && nextTransition.num32) || number < 0x20 && !nextTransition.num32)) {
|
|
139
|
-
transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1);
|
|
150
|
+
transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
|
|
140
151
|
target[position++] = number;
|
|
141
152
|
} else {
|
|
142
|
-
transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
|
|
153
|
+
transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
|
|
143
154
|
targetView.setUint32(position, number, true);
|
|
144
155
|
position += 4;
|
|
145
156
|
}
|
|
@@ -150,14 +161,14 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
150
161
|
let xShifted
|
|
151
162
|
// this checks for rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
|
|
152
163
|
if (((xShifted = number * mult10[((target[position + 3] & 0x7f) << 1) | (target[position + 2] >> 7)]) >> 0) === xShifted) {
|
|
153
|
-
transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
|
|
164
|
+
transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
|
|
154
165
|
position += 4;
|
|
155
166
|
break;
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
169
|
}
|
|
159
170
|
}
|
|
160
|
-
transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8);
|
|
171
|
+
transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8, frozen);
|
|
161
172
|
targetView.setFloat64(position, number, true);
|
|
162
173
|
position += 8;
|
|
163
174
|
break;
|
|
@@ -223,21 +234,21 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
223
234
|
nextTransition.string8 = transition;
|
|
224
235
|
pack(null, 0, true); // special call to notify that structures have been updated
|
|
225
236
|
} else {
|
|
226
|
-
transition = createTypeTransition(nextTransition, UTF8, 1);
|
|
237
|
+
transition = createTypeTransition(nextTransition, UTF8, 1, frozen);
|
|
227
238
|
}
|
|
228
239
|
}
|
|
229
240
|
} else if (refOffset === 0 && !usedAscii0) {
|
|
230
241
|
usedAscii0 = true;
|
|
231
|
-
transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0);
|
|
242
|
+
transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0, frozen);
|
|
232
243
|
break; // don't increment position
|
|
233
244
|
}// else ascii:
|
|
234
245
|
else if (!(transition = nextTransition.ascii8) && !(typedStructs.length > 10 && (transition = nextTransition.string8)))
|
|
235
|
-
transition = createTypeTransition(nextTransition, ASCII, 1);
|
|
246
|
+
transition = createTypeTransition(nextTransition, ASCII, 1, frozen);
|
|
236
247
|
target[position++] = refOffset;
|
|
237
248
|
} else {
|
|
238
249
|
// TODO: Enable ascii16 at some point, but get the logic right
|
|
239
250
|
//if (isNotAscii)
|
|
240
|
-
transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2);
|
|
251
|
+
transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2, frozen);
|
|
241
252
|
//else
|
|
242
253
|
//transition = nextTransition.ascii16 || createTypeTransition(nextTransition, ASCII, 2);
|
|
243
254
|
targetView.setUint16(position, refOffset, true);
|
|
@@ -247,7 +258,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
247
258
|
case 'object':
|
|
248
259
|
if (value) {
|
|
249
260
|
if (value.constructor === Date) {
|
|
250
|
-
transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8);
|
|
261
|
+
transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8, frozen);
|
|
251
262
|
targetView.setFloat64(position, value.getTime(), true);
|
|
252
263
|
position += 8;
|
|
253
264
|
} else {
|
|
@@ -263,7 +274,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
263
274
|
}
|
|
264
275
|
break;
|
|
265
276
|
case 'boolean':
|
|
266
|
-
transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1);
|
|
277
|
+
transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
|
|
267
278
|
target[position++] = value ? 0xf9 : 0xf8; // match CBOR with these
|
|
268
279
|
break;
|
|
269
280
|
case 'undefined':
|
|
@@ -276,9 +287,41 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
276
287
|
default:
|
|
277
288
|
queuedReferences.push(key, value, keyIndex);
|
|
278
289
|
}
|
|
290
|
+
if (transition === undefined) return 0; // frozen: structure cap reached
|
|
279
291
|
keyIndex++;
|
|
280
292
|
}
|
|
281
293
|
|
|
294
|
+
// Cap enforcement for queued (nested-object / null) references. pack() advances msgpackr's
|
|
295
|
+
// shared write position and we cannot cleanly bail afterward, so preflight the whole queued
|
|
296
|
+
// chain through EXISTING transitions first: if the cap is reached and any field would need a
|
|
297
|
+
// new structure, fall back to plain encoding now (return 0) — before touching the shared
|
|
298
|
+
// position. Uses a FRESH length read (not the entry-time `frozen`): a getter invoked while
|
|
299
|
+
// reading values above may have minted on this same instance since entry.
|
|
300
|
+
if (!structureKnown && queuedReferences.length > 0 && typedStructs.length >= cap) {
|
|
301
|
+
let t = transition;
|
|
302
|
+
for (let i = 0, l = queuedReferences.length; i < l; i += 3) {
|
|
303
|
+
// A non-null (object/Date) ref is pack()ed into the shared buffer, advancing
|
|
304
|
+
// msgpackr's write position. Its structure variant (object16 vs object32) depends on
|
|
305
|
+
// the runtime ref-section offset (inline strings + earlier refs), which we can't know
|
|
306
|
+
// before packing — and we can't bail after a pack without corrupting the fallback. So
|
|
307
|
+
// under the cap, any record with a packing ref falls back to plain encoding now,
|
|
308
|
+
// before any pack(). null/undefined refs don't pack, so they're walked normally.
|
|
309
|
+
if (queuedReferences[i + 1] != null) return 0;
|
|
310
|
+
const nt = t[queuedReferences[i]];
|
|
311
|
+
if (!nt) return 0;
|
|
312
|
+
const next = nt.object16; // null/undefined ref → OBJECT_DATA size 2
|
|
313
|
+
if (!next) return 0;
|
|
314
|
+
t = next;
|
|
315
|
+
}
|
|
316
|
+
if (t[RECORD_SYMBOL] == null) return 0; // exact structure not yet minted
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Past the preflight the chain is known, so no minting happens — except a rare offset
|
|
320
|
+
// divergence (a known shape whose ref section now crosses 0xff00 and needs object32 where
|
|
321
|
+
// the preflight matched object16). Once a ref is packed we can no longer bail, so we finish
|
|
322
|
+
// via the unfrozen forceTypeTransition: a bounded, self-converging overshoot for that one
|
|
323
|
+
// record. packedRef keeps the record-id mint from bailing after a pack.
|
|
324
|
+
let packedRef = false;
|
|
282
325
|
for (let i = 0, l = queuedReferences.length; i < l;) {
|
|
283
326
|
let key = queuedReferences[i++];
|
|
284
327
|
let value = queuedReferences[i++];
|
|
@@ -300,15 +343,6 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
300
343
|
}
|
|
301
344
|
let newPosition;
|
|
302
345
|
if (value) {
|
|
303
|
-
/*if (typeof value === 'string') { // TODO: we could re-enable long strings
|
|
304
|
-
if (position + value.length * 3 > safeEnd) {
|
|
305
|
-
target = makeRoom(position + value.length * 3);
|
|
306
|
-
position -= start;
|
|
307
|
-
targetView = target.dataView;
|
|
308
|
-
start = 0;
|
|
309
|
-
}
|
|
310
|
-
newPosition = position + target.utf8Write(value, position, 0xffffffff);
|
|
311
|
-
} else { */
|
|
312
346
|
let size;
|
|
313
347
|
refOffset = refPosition - refsStartPosition;
|
|
314
348
|
if (refOffset < 0xff00) {
|
|
@@ -318,15 +352,15 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
318
352
|
else if ((transition = nextTransition.object32))
|
|
319
353
|
size = 4;
|
|
320
354
|
else {
|
|
321
|
-
transition =
|
|
355
|
+
transition = forceTypeTransition(nextTransition, OBJECT_DATA, 2);
|
|
322
356
|
size = 2;
|
|
323
357
|
}
|
|
324
358
|
} else {
|
|
325
|
-
transition = nextTransition.object32 ||
|
|
359
|
+
transition = nextTransition.object32 || forceTypeTransition(nextTransition, OBJECT_DATA, 4);
|
|
326
360
|
size = 4;
|
|
327
361
|
}
|
|
328
362
|
newPosition = pack(value, refPosition);
|
|
329
|
-
|
|
363
|
+
packedRef = true;
|
|
330
364
|
if (typeof newPosition === 'object') {
|
|
331
365
|
// re-allocated
|
|
332
366
|
refPosition = newPosition.position;
|
|
@@ -346,16 +380,19 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
346
380
|
position += 4;
|
|
347
381
|
}
|
|
348
382
|
} else { // null or undefined
|
|
349
|
-
transition = nextTransition.object16 ||
|
|
383
|
+
transition = nextTransition.object16 || forceTypeTransition(nextTransition, OBJECT_DATA, 2);
|
|
350
384
|
targetView.setInt16(position, value === null ? -10 : -9, true);
|
|
351
385
|
position += 2;
|
|
352
386
|
}
|
|
353
387
|
keyIndex++;
|
|
354
388
|
}
|
|
355
389
|
|
|
356
|
-
|
|
357
390
|
let recordId = transition[RECORD_SYMBOL];
|
|
358
391
|
if (recordId == null) {
|
|
392
|
+
// Flat records (no queued refs) reach here without packing, so the cap is enforced
|
|
393
|
+
// cleanly. Records that packed nested refs already passed the preflight; either way
|
|
394
|
+
// bailing now after refs were packed would corrupt the fallback.
|
|
395
|
+
if (!packedRef && typedStructs.length >= cap) return 0;
|
|
359
396
|
recordId = packr.typedStructs.length;
|
|
360
397
|
let structure = [];
|
|
361
398
|
let nextTransition = transition;
|
|
@@ -409,7 +446,11 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
|
|
|
409
446
|
if (refsStartPosition === refPosition)
|
|
410
447
|
return position; // no refs
|
|
411
448
|
typedStructs.lastStringStart = position - start;
|
|
412
|
-
|
|
449
|
+
// Fixed section overflowed our estimate — retry with the corrected size. The structure
|
|
450
|
+
// is already minted at this point, so pass structureKnown=true to skip the cap check
|
|
451
|
+
// (otherwise a record that became frozen during attempt 1 would bail mid-retry, after
|
|
452
|
+
// refs were already packed, and corrupt the fallback).
|
|
453
|
+
return writeStruct(object, target, encodingStart, start, structures, makeRoom, pack, packr, true);
|
|
413
454
|
}
|
|
414
455
|
return refPosition;
|
|
415
456
|
}
|
|
@@ -441,9 +482,36 @@ function anyType(transition, position, targetView, value) {
|
|
|
441
482
|
// TODO: can we do an "any" type where we defer the decision?
|
|
442
483
|
return;
|
|
443
484
|
}
|
|
444
|
-
|
|
485
|
+
// When the typed-structure dictionary reaches maxOwnStructures we stop minting new
|
|
486
|
+
// structures/transitions. typedStructs is append-only and pinned on the long-lived
|
|
487
|
+
// encoder (records reference structures by recordId), so an unbounded shape space —
|
|
488
|
+
// e.g. a wide, sparsely/variably-populated schema — would otherwise grow the
|
|
489
|
+
// dictionary + transition trie without limit. `frozen` is passed in (derived from the
|
|
490
|
+
// encoding instance's own typedStructs.length, never a shared global) so a re-entrant
|
|
491
|
+
// encode on another instance can't flip it; while frozen, a missing transition returns
|
|
492
|
+
// undefined so the caller bails and the record falls back to plain encoding.
|
|
493
|
+
function createTypeTransition(transition, type, size, frozen) {
|
|
494
|
+
let typeName = TYPE_NAMES[type] + (size << 3);
|
|
495
|
+
let newTransition = transition[typeName];
|
|
496
|
+
if (newTransition) return newTransition;
|
|
497
|
+
if (frozen) return undefined;
|
|
498
|
+
newTransition = transition[typeName] = Object.create(null);
|
|
499
|
+
newTransition.__type = type;
|
|
500
|
+
newTransition.__size = size;
|
|
501
|
+
newTransition.__parent = transition;
|
|
502
|
+
return newTransition;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Unfrozen variant: always mints. Used in the queued-ref loop once a nested value has
|
|
506
|
+
// already been pack()ed — at that point pack() has advanced msgpackr's shared write
|
|
507
|
+
// position, so bailing with `return 0` would corrupt the fallback. We must finish the
|
|
508
|
+
// encode instead, even if that means minting a (bounded) handful of structures past the
|
|
509
|
+
// cap. The cap is still enforced up front via the preflight, before the first pack().
|
|
510
|
+
function forceTypeTransition(transition, type, size) {
|
|
445
511
|
let typeName = TYPE_NAMES[type] + (size << 3);
|
|
446
|
-
let newTransition = transition[typeName]
|
|
512
|
+
let newTransition = transition[typeName];
|
|
513
|
+
if (newTransition) return newTransition;
|
|
514
|
+
newTransition = transition[typeName] = Object.create(null);
|
|
447
515
|
newTransition.__type = type;
|
|
448
516
|
newTransition.__size = size;
|
|
449
517
|
newTransition.__parent = transition;
|
|
@@ -477,7 +545,8 @@ function onLoadedStructures(sharedData) {
|
|
|
477
545
|
date64: null,
|
|
478
546
|
};
|
|
479
547
|
}
|
|
480
|
-
|
|
548
|
+
// Replaying persisted structures is never subject to the cap — always mint.
|
|
549
|
+
transition = createTypeTransition(nextTransition, type, size, false);
|
|
481
550
|
}
|
|
482
551
|
transition[RECORD_SYMBOL] = i;
|
|
483
552
|
}
|