msgpackr 1.11.13 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/test.js CHANGED
@@ -1357,7 +1357,13 @@
1357
1357
  hasSharedUpdate = false;
1358
1358
  let encodingError;
1359
1359
  try {
1360
- if (packr.randomAccessStructure && value && typeof value === 'object') {
1360
+ // readOnlyStructures: skip the random-access struct write path so NO new struct is
1361
+ // minted. randomAccessStructure stays true (the struct READ path and the struct-safe
1362
+ // integer boundary are preserved, so existing struct data still decodes), but objects
1363
+ // fall through to the normal pack()->writeObject->writeRecord path and are written as
1364
+ // classic shared-structure records (byte range 0x40-0x7f, disjoint from struct headers
1365
+ // at 0x20-0x3f) — the bounded, width-agnostic encoding used before struct mode.
1366
+ if (packr.randomAccessStructure && !packr.readOnlyStructures && value && typeof value === 'object') {
1361
1367
  if (value.constructor === Object) writeStruct(value); // simple object
1362
1368
  else if (value.constructor !== Map && !Array.isArray(value) && !extensionClasses.some(extClass => value instanceof extClass)) {
1363
1369
  // allow user classes, if they don't need special handling (but do use toJSON if available)
@@ -1423,7 +1429,14 @@
1423
1429
  let newSharedData = prepareStructures$1(structures, packr);
1424
1430
  if (!encodingError) { // TODO: If there is an encoding error, should make the structures as uninitialized so they get rebuilt next time
1425
1431
  if (packr.saveStructures(newSharedData, newSharedData.isCompatible) === false) {
1426
- // get updated structures and try again if the update failed
1432
+ // The save was declined (a concurrent writer updated the shared structures,
1433
+ // or the store transaction did not durably commit). Our in-memory
1434
+ // structures + transition trie may now reference record ids that were
1435
+ // never persisted; re-packing as-is would re-emit the same record pointing
1436
+ // at an unpersisted structure (-> "Record id is not defined" on decode).
1437
+ // Mark structures uninitialized so the re-pack reloads durable structures
1438
+ // via getStructures, rebuilds the transition trie, and re-mints + re-saves.
1439
+ structures.uninitialized = true;
1427
1440
  return packr.pack(value, encodeOptions)
1428
1441
  }
1429
1442
  packr.lastNamedStructuresLength = sharedLength;
@@ -4219,6 +4232,31 @@
4219
4232
  assert.deepEqual(inputData, outputData);
4220
4233
  });
4221
4234
 
4235
+ test('declined structure save re-packs against durable structures (no dangling record)', function() {
4236
+ // Regression: when saveStructures declines a save (a concurrent writer updated the shared
4237
+ // structures, or the store txn did not durably commit), the in-memory structures/transition
4238
+ // trie reference a record id that was never persisted. The re-pack must reload the durable
4239
+ // structures and re-mint/re-save — otherwise it re-emits the same record pointing at an
4240
+ // unsaved structure, and reads throw "Record id is not defined".
4241
+ const meta = new Packr();
4242
+ let store = null;
4243
+ let declinedOnce = false;
4244
+ const packr = new Packr({
4245
+ useRecords: true,
4246
+ getStructures() { return store ? meta.unpack(store) : undefined },
4247
+ saveStructures(structures) {
4248
+ if (!declinedOnce) { declinedOnce = true; return false } // decline the first save
4249
+ store = meta.pack(structures); return true
4250
+ },
4251
+ });
4252
+ const a = packr.pack({ x: 9, y: 8 }); // first mint is declined -> must re-pack + re-save
4253
+ const b = packr.pack({ x: 7, y: 6 }); // same shape -> must still reference a saved structure
4254
+ // A fresh reader sees only the durably-saved structures (another thread / post-restart):
4255
+ const reader = new Packr({ getStructures() { return store ? meta.unpack(store) : undefined } });
4256
+ assert.deepEqual(reader.unpack(a), { x: 9, y: 8 });
4257
+ assert.deepEqual(reader.unpack(b), { x: 7, y: 6 });
4258
+ });
4259
+
4222
4260
  test('big buffer', function() {
4223
4261
  var size = 100000000;
4224
4262
  var data = new Uint8Array(size).fill(1);
@@ -4817,5 +4855,74 @@
4817
4855
  });
4818
4856
  });
4819
4857
 
4858
+ suite('msgpackr – readOnlyStructures (write-disable, read-compatible)', function () {
4859
+ // readOnlyStructures keeps randomAccessStructure decode semantics — existing random-access
4860
+ // struct data still decodes and the struct-safe integer boundary is preserved — but never mints
4861
+ // a new random-access structure on write. Objects fall through to the classic shared-structure
4862
+ // record path (bytes 0x40-0x7f, disjoint from struct headers at 0x20-0x3f), the bounded,
4863
+ // width-agnostic encoding used before struct mode. This is the mechanism for disabling typed
4864
+ // structures on an existing store without breaking reads.
4865
+ const sharedStore = () => {
4866
+ let saved;
4867
+ return { getStructures: () => saved, saveStructures: (s) => { saved = s; return true; } };
4868
+ };
4869
+
4870
+ test('decodes pre-existing random-access structs (read stays enabled)', function () {
4871
+ const full = new Packr({ randomAccessStructure: true, useRecords: true });
4872
+ const rec = { a: 1, b: 'hello', c: true, n: 42 };
4873
+ const buf = full.pack(rec);
4874
+ assert.ok(buf[0] >= 0x20 && buf[0] < 0x40, 'control: full encoder emits a random-access struct');
4875
+
4876
+ const ro = new Packr({ randomAccessStructure: true, useRecords: true, readOnlyStructures: true });
4877
+ ro.typedStructs = full.typedStructs; // share the existing random-access dictionary
4878
+ assert.deepEqual(ro.unpack(buf), rec);
4879
+ });
4880
+
4881
+ test('writes classic shared-structure records (0x40-0x7f), never a random-access struct', function () {
4882
+ const ro = new Packr({ randomAccessStructure: true, useRecords: true, readOnlyStructures: true, ...sharedStore() });
4883
+ const rec = { x: 9, y: 'world', z: [1, 2, 3], nested: { deep: 50, k: 42 } };
4884
+ const b1 = ro.pack(rec);
4885
+ assert.ok(b1[0] >= 0x40 && b1[0] < 0x80, 'expected a classic record (0x40-0x7f), got 0x' + b1[0].toString(16));
4886
+ assert.strictEqual(ro.typedStructs ? ro.typedStructs.length : 0, 0, 'no random-access struct minted');
4887
+ assert.deepEqual(ro.unpack(b1), rec);
4888
+ // a second record of the same shape reuses the shared classic structure (still classic)
4889
+ const b2 = ro.pack({ x: 1, y: 'a', z: [], nested: { deep: 0, k: 0 } });
4890
+ assert.ok(b2[0] >= 0x40 && b2[0] < 0x80, 'second record also classic');
4891
+ });
4892
+
4893
+ test('preserves the struct-safe integer boundary (nested 0x20-0x3f ints do not collide)', function () {
4894
+ const ro = new Packr({ randomAccessStructure: true, useRecords: true, readOnlyStructures: true, ...sharedStore() });
4895
+ for (const v of [0x1f, 0x20, 0x2a, 0x3f, 0x40, 0x7f]) {
4896
+ assert.deepEqual(ro.unpack(ro.pack({ v, nested: { w: v } })), { v, nested: { w: v } });
4897
+ }
4898
+ });
4899
+
4900
+ test('a normal randomAccessStructure reader decodes readOnly classic records (replication compat)', function () {
4901
+ let saved;
4902
+ const store = { getStructures: () => saved, saveStructures: (s) => { saved = s; return true; } };
4903
+ const ro = new Packr({ randomAccessStructure: true, useRecords: true, readOnlyStructures: true, ...store });
4904
+ const roBuf = ro.pack({ p: 'q', n: 42 });
4905
+ assert.ok(roBuf[0] >= 0x40 && roBuf[0] < 0x80, 'readOnly writes a classic record');
4906
+
4907
+ // A peer that is NOT readOnly (a normal struct-writing node) shares the classic-structure
4908
+ // store and decodes the classic record correctly — no mapsAsObjects needed, since classic
4909
+ // records decode to objects natively.
4910
+ const peer = new Packr({ randomAccessStructure: true, useRecords: true, ...store });
4911
+ assert.deepEqual(peer.unpack(roBuf), { p: 'q', n: 42 });
4912
+ });
4913
+
4914
+ test('every object shape uses the classic path (null-proto, shadowed ctor) — no random-access struct', function () {
4915
+ const ro = new Packr({ randomAccessStructure: true, useRecords: true, readOnlyStructures: true, ...sharedStore() });
4916
+
4917
+ const np = Object.create(null); np.a = 1; np.b = 'x';
4918
+ assert.deepEqual(ro.unpack(ro.pack(np)), { a: 1, b: 'x' });
4919
+
4920
+ const cs = { constructor: 'shadow', a: 1 };
4921
+ assert.deepEqual(ro.unpack(ro.pack(cs)), { constructor: 'shadow', a: 1 });
4922
+
4923
+ assert.strictEqual(ro.typedStructs ? ro.typedStructs.length : 0, 0, 'no random-access struct minted for any shape');
4924
+ });
4925
+ });
4926
+
4820
4927
  })(chai, null, module, fs);
4821
4928
  //# sourceMappingURL=test.js.map