cry-synced-db-client 0.1.159 → 0.1.160

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.js CHANGED
@@ -330,28 +330,67 @@ function sameIdSequence(a, b) {
330
330
  return true;
331
331
  }
332
332
  function computeArrayDiff(existingArr, updateArr, basePath, diff) {
333
- if (existingArr.length === 0) {
334
- if (updateArr.length > 0) diff[basePath] = updateArr;
335
- return;
336
- }
337
- if (!allElementsHaveId(existingArr) || !allElementsHaveId(updateArr)) {
333
+ if (existingArr.length === 0 && updateArr.length === 0) return;
334
+ if (!allElementsHaveId(updateArr) || existingArr.length > 0 && !allElementsHaveId(existingArr)) {
338
335
  if (!deepEquals(existingArr, updateArr)) {
339
336
  diff[basePath] = updateArr;
340
337
  }
341
338
  return;
342
339
  }
343
- if (!sameIdSequence(existingArr, updateArr)) {
344
- diff[basePath] = updateArr;
340
+ const existingIds = /* @__PURE__ */ new Set();
341
+ for (const e of existingArr) existingIds.add(String(e._id));
342
+ const updateIds = /* @__PURE__ */ new Set();
343
+ for (const u of updateArr) updateIds.add(String(u._id));
344
+ let sameSet = existingIds.size === updateIds.size;
345
+ if (sameSet) {
346
+ for (const id of existingIds) {
347
+ if (!updateIds.has(id)) {
348
+ sameSet = false;
349
+ break;
350
+ }
351
+ }
352
+ }
353
+ if (sameSet) {
354
+ if (sameIdSequence(existingArr, updateArr)) {
355
+ for (let i = 0; i < updateArr.length; i++) {
356
+ const elementId = String(updateArr[i]._id);
357
+ computeDiffInto(
358
+ existingArr[i],
359
+ updateArr[i],
360
+ `${basePath}[${elementId}]`,
361
+ diff
362
+ );
363
+ }
364
+ } else {
365
+ diff[basePath] = updateArr;
366
+ }
345
367
  return;
346
368
  }
347
- for (let i = 0; i < updateArr.length; i++) {
348
- const elementId = String(updateArr[i]._id);
349
- computeDiffInto(
350
- existingArr[i],
351
- updateArr[i],
352
- `${basePath}[${elementId}]`,
353
- diff
354
- );
369
+ for (const id of existingIds) {
370
+ if (!updateIds.has(id)) {
371
+ diff[`${basePath}[${id}]`] = void 0;
372
+ }
373
+ }
374
+ for (const updateEl of updateArr) {
375
+ const id = String(updateEl._id);
376
+ if (!existingIds.has(id)) {
377
+ diff[`${basePath}[${id}]`] = [updateEl];
378
+ }
379
+ }
380
+ if (existingArr.length > 0) {
381
+ const existingById = /* @__PURE__ */ new Map();
382
+ for (const e of existingArr) existingById.set(String(e._id), e);
383
+ for (const updateEl of updateArr) {
384
+ const id = String(updateEl._id);
385
+ if (existingIds.has(id)) {
386
+ computeDiffInto(
387
+ existingById.get(id),
388
+ updateEl,
389
+ `${basePath}[${id}]`,
390
+ diff
391
+ );
392
+ }
393
+ }
355
394
  }
356
395
  }
357
396
  function computeDiffInto(existing, update, basePath, diff) {
@@ -482,12 +521,62 @@ function setSegment(current, part, value) {
482
521
  }
483
522
  return false;
484
523
  }
524
+ function deleteByPath(target2, path) {
525
+ if (target2 === null || target2 === void 0) return false;
526
+ const parts = tokenizePath(path);
527
+ if (parts.length === 0) return false;
528
+ let current = target2;
529
+ for (let i = 0; i < parts.length - 1; i++) {
530
+ const next = navigateSegment(current, parts[i]);
531
+ if (next === void 0 || next === null) return false;
532
+ current = next;
533
+ }
534
+ const last = parts[parts.length - 1];
535
+ return deleteSegment(current, last);
536
+ }
537
+ function deleteSegment(current, part) {
538
+ if (current === null || current === void 0) return false;
539
+ if (part.startsWith("[") && part.endsWith("]")) {
540
+ const idStr = part.slice(1, -1);
541
+ if (!Array.isArray(current)) return false;
542
+ const idx = current.findIndex((item) => item && String(item._id) === idStr);
543
+ if (idx < 0) return false;
544
+ current.splice(idx, 1);
545
+ return true;
546
+ }
547
+ if (/^\d+$/.test(part) && Array.isArray(current)) {
548
+ const idx = Number(part);
549
+ if (idx < 0 || idx >= current.length) return false;
550
+ current.splice(idx, 1);
551
+ return true;
552
+ }
553
+ if (typeof current === "object") {
554
+ if (!Object.prototype.hasOwnProperty.call(current, part)) return false;
555
+ delete current[part];
556
+ return true;
557
+ }
558
+ return false;
559
+ }
560
+ function isTerminalBracketKey(path) {
561
+ const tokens = tokenizePath(path);
562
+ const last = tokens[tokens.length - 1];
563
+ return !!(last && last.length >= 2 && last.charCodeAt(0) === 91 && last.charCodeAt(last.length - 1) === 93);
564
+ }
485
565
  function mergeDirtyPath(accumulated, newPath, newValue) {
486
566
  for (const existingKey of Object.keys(accumulated)) {
487
567
  if (existingKey === newPath) continue;
488
568
  if (isDescendantOrEqual(newPath, existingKey)) {
569
+ const existingValue = accumulated[existingKey];
570
+ const existingIsTerminal = isTerminalBracketKey(existingKey);
571
+ if (existingIsTerminal && existingValue === void 0) {
572
+ return;
573
+ }
574
+ let mutationTarget = existingValue;
575
+ if (existingIsTerminal && Array.isArray(existingValue) && existingValue.length === 1) {
576
+ mutationTarget = existingValue[0];
577
+ }
489
578
  const relativePath = newPath.substring(existingKey.length + 1);
490
- const ok = setByPath(accumulated[existingKey], relativePath, newValue);
579
+ const ok = setByPath(mutationTarget, relativePath, newValue);
491
580
  if (ok) return;
492
581
  break;
493
582
  }
@@ -508,6 +597,51 @@ function mergeDirtyChanges(accumulated, newChanges) {
508
597
  }
509
598
  }
510
599
 
600
+ // src/utils/normalizeUndefined.ts
601
+ var SERVER_MANAGED_KEYS = /* @__PURE__ */ new Set(["_ts", "_rev", "_csq"]);
602
+ function isObjectIdLike2(v) {
603
+ return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || v._bsontype === "ObjectID"));
604
+ }
605
+ function collectUnsetPaths(value) {
606
+ const paths = [];
607
+ walk(value, "", paths);
608
+ return paths;
609
+ }
610
+ function walk(node, prefix, paths) {
611
+ if (node === null || node === void 0) return;
612
+ if (typeof node !== "object") return;
613
+ if (node instanceof Date) return;
614
+ if (isObjectIdLike2(node)) return;
615
+ if (Array.isArray(node)) {
616
+ for (let i = 0; i < node.length; i++) {
617
+ const element = node[i];
618
+ const childPath = childPathForArrayElement(prefix, element, i);
619
+ if (element === void 0) {
620
+ paths.push(childPath);
621
+ } else {
622
+ walk(element, childPath, paths);
623
+ }
624
+ }
625
+ return;
626
+ }
627
+ for (const key of Object.keys(node)) {
628
+ if (SERVER_MANAGED_KEYS.has(key)) continue;
629
+ const child = node[key];
630
+ const childPath = prefix ? `${prefix}.${key}` : key;
631
+ if (child === void 0) {
632
+ paths.push(childPath);
633
+ } else {
634
+ walk(child, childPath, paths);
635
+ }
636
+ }
637
+ }
638
+ function childPathForArrayElement(prefix, element, index) {
639
+ if (element !== null && element !== void 0 && typeof element === "object" && !Array.isArray(element) && !(element instanceof Date) && !isObjectIdLike2(element) && element._id != null) {
640
+ return `${prefix}[${String(element._id)}]`;
641
+ }
642
+ return prefix ? `${prefix}.${index}` : String(index);
643
+ }
644
+
511
645
  // src/db/managers/InMemManager.ts
512
646
  var InMemManager = class {
513
647
  constructor(config) {
@@ -2755,62 +2889,19 @@ function fixDotnetArrays(changes, serverRev, baseRev) {
2755
2889
  }
2756
2890
 
2757
2891
  // src/utils/translateBracketPaths.ts
2758
- function translateBracketPathsToIndex(changes, entity) {
2759
- const result = {};
2760
- for (const [key, value] of Object.entries(changes)) {
2761
- const translated = translateKey(key, entity);
2762
- if (translated !== null) {
2763
- result[translated] = value;
2764
- }
2765
- }
2766
- return result;
2767
- }
2768
- function translateKey(key, entity) {
2769
- const parts = tokenizePath(key);
2770
- const out = [];
2771
- let cursor = entity;
2772
- for (let i = 0; i < parts.length; i++) {
2773
- const part = parts[i];
2774
- if (part.startsWith("[") && part.endsWith("]")) {
2775
- const idStr = part.slice(1, -1);
2776
- if (!Array.isArray(cursor)) return null;
2777
- const idx = cursor.findIndex(
2778
- (item) => item && String(item._id) === idStr
2779
- );
2780
- if (idx < 0) return null;
2781
- out.push(String(idx));
2782
- cursor = cursor[idx];
2783
- continue;
2784
- }
2785
- out.push(part);
2786
- if (cursor === null || cursor === void 0) {
2787
- for (let j = i + 1; j < parts.length; j++) {
2788
- const p = parts[j];
2789
- if (p.startsWith("[") && p.endsWith("]")) return null;
2790
- out.push(p);
2791
- }
2792
- return out.join(".");
2793
- }
2794
- if (/^\d+$/.test(part) && Array.isArray(cursor)) {
2795
- cursor = cursor[Number(part)];
2796
- } else if (typeof cursor === "object") {
2797
- cursor = cursor[part];
2798
- } else {
2799
- cursor = void 0;
2800
- }
2801
- }
2802
- return out.join(".");
2892
+ function translateBracketPathsToIndex(changes, _entity) {
2893
+ return changes;
2803
2894
  }
2804
2895
 
2805
2896
  // src/utils/stripServerManaged.ts
2806
- var SERVER_MANAGED_KEYS = /* @__PURE__ */ new Set(["_ts", "_rev", "_csq"]);
2807
- function isObjectIdLike2(v) {
2897
+ var SERVER_MANAGED_KEYS2 = /* @__PURE__ */ new Set(["_ts", "_rev", "_csq"]);
2898
+ function isObjectIdLike3(v) {
2808
2899
  return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || v._bsontype === "ObjectID"));
2809
2900
  }
2810
2901
  function isServerManagedPath(key) {
2811
2902
  for (const part of tokenizePath(key)) {
2812
2903
  if (part.startsWith("[")) continue;
2813
- if (SERVER_MANAGED_KEYS.has(part)) return true;
2904
+ if (SERVER_MANAGED_KEYS2.has(part)) return true;
2814
2905
  }
2815
2906
  return false;
2816
2907
  }
@@ -2821,10 +2912,10 @@ function scrubServerManagedDeep(value) {
2821
2912
  }
2822
2913
  if (typeof value !== "object") return value;
2823
2914
  if (value instanceof Date) return value;
2824
- if (isObjectIdLike2(value)) return value;
2915
+ if (isObjectIdLike3(value)) return value;
2825
2916
  const out = {};
2826
2917
  for (const k of Object.keys(value)) {
2827
- if (SERVER_MANAGED_KEYS.has(k)) continue;
2918
+ if (SERVER_MANAGED_KEYS2.has(k)) continue;
2828
2919
  out[k] = scrubServerManagedDeep(value[k]);
2829
2920
  }
2830
2921
  return out;
@@ -4820,7 +4911,11 @@ var _SyncedDb = class _SyncedDb {
4820
4911
  this.pendingChanges.schedule(collection, id, newData, 0, "save");
4821
4912
  const isWriteOnly = (_b = this.collections.get(collection)) == null ? void 0 : _b.writeOnly;
4822
4913
  const currentMem = isWriteOnly ? null : this.inMemDb.getById(collection, id);
4823
- const merged = __spreadValues(__spreadValues({}, currentMem || existing || { _id: id }), update);
4914
+ const merged = _SyncedDb.applyDiffLocally(
4915
+ currentMem != null ? currentMem : existing,
4916
+ diff,
4917
+ id
4918
+ );
4824
4919
  if (!isWriteOnly && !(existing == null ? void 0 : existing._deleted) && !(existing == null ? void 0 : existing._archived)) {
4825
4920
  this.inMemManager.writeBatch(collection, [merged], "upsert", { source: "incremental" });
4826
4921
  }
@@ -4845,6 +4940,7 @@ var _SyncedDb = class _SyncedDb {
4845
4940
  this.ensureId(data, "insert", collection);
4846
4941
  _SyncedDb.ensureNestedIds(data);
4847
4942
  data = _SyncedDb.stringifyObjectIds(data);
4943
+ const unsetPaths = collectUnsetPaths(data);
4848
4944
  const id = String(data._id);
4849
4945
  const existing = await this.dexieDb.getById(collection, id);
4850
4946
  if (existing && !existing._deleted && !existing._archived) {
@@ -4862,6 +4958,7 @@ var _SyncedDb = class _SyncedDb {
4862
4958
  _id: id,
4863
4959
  _lastUpdaterId: this.updaterId
4864
4960
  });
4961
+ for (const path of unsetPaths) deleteByPath(newData, path);
4865
4962
  this.pendingChanges.schedule(collection, id, newData, 0, "insert");
4866
4963
  if (!((_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly)) {
4867
4964
  this.inMemManager.writeBatch(collection, [newData], "upsert", { source: "incremental" });
@@ -5945,6 +6042,64 @@ var _SyncedDb = class _SyncedDb {
5945
6042
  static isObjectIdLike(v) {
5946
6043
  return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || typeof v.toHexString === "function"));
5947
6044
  }
6045
+ /**
6046
+ * Mongo-symmetric local apply: starting from `base` (a safe deep clone of
6047
+ * `currentMem` or `existing`), walk each `(path, value)` entry of `diff`:
6048
+ *
6049
+ * - `value === undefined` → `deleteByPath` (mongo `$unset` symmetric)
6050
+ * - otherwise → `setByPath` (mongo `$set` symmetric)
6051
+ *
6052
+ * The result is what an equivalent server-side `$set` + `$unset` would
6053
+ * have produced. Replaces the previous shallow `{ ...currentMem, ...update }`
6054
+ * merge which dropped nested fields the caller's `update` didn't mention.
6055
+ *
6056
+ * Returns a new object (the cloned-and-mutated `base`); never mutates
6057
+ * the input `base` reference.
6058
+ */
6059
+ static applyDiffLocally(base, diff, fallbackId) {
6060
+ const seed = base ? _SyncedDb.safeDeepClone(base) : { _id: fallbackId };
6061
+ if (seed._id == null) seed._id = fallbackId;
6062
+ for (const path of Object.keys(diff)) {
6063
+ const value = diff[path];
6064
+ if (value === void 0) {
6065
+ deleteByPath(seed, path);
6066
+ continue;
6067
+ }
6068
+ if (!path.includes(".") && !path.includes("[")) {
6069
+ seed[path] = value;
6070
+ continue;
6071
+ }
6072
+ const ok = setByPath(seed, path, value);
6073
+ if (!ok) seed[path] = value;
6074
+ }
6075
+ return seed;
6076
+ }
6077
+ /**
6078
+ * Deep clone for `applyDiffLocally`. Recurses into plain objects and
6079
+ * arrays; preserves `Date` (cloned to avoid shared reference) and
6080
+ * `ObjectId`-like values by reference (their internal Buffer state is
6081
+ * immutable from our perspective). Other class instances pass through
6082
+ * by reference. Avoids `structuredClone` because it throws on class
6083
+ * instances like bson `ObjectId`.
6084
+ */
6085
+ static safeDeepClone(value) {
6086
+ if (value === null || value === void 0) return value;
6087
+ if (typeof value !== "object") return value;
6088
+ if (value instanceof Date) return new Date(value.getTime());
6089
+ if (_SyncedDb.isObjectIdLike(value)) return value;
6090
+ if (Array.isArray(value)) {
6091
+ const out2 = new Array(value.length);
6092
+ for (let i = 0; i < value.length; i++) out2[i] = _SyncedDb.safeDeepClone(value[i]);
6093
+ return out2;
6094
+ }
6095
+ const proto = Object.getPrototypeOf(value);
6096
+ if (proto !== Object.prototype && proto !== null) return value;
6097
+ const out = {};
6098
+ for (const key of Object.keys(value)) {
6099
+ out[key] = _SyncedDb.safeDeepClone(value[key]);
6100
+ }
6101
+ return out;
6102
+ }
5948
6103
  /**
5949
6104
  * Recursively walk `value` and ensure every plain object that appears
5950
6105
  * as an element of an array carries an `_id`. Missing `_id`s are
@@ -6345,14 +6500,6 @@ var C1 = new C1Type();
6345
6500
  C1.name = "MessagePack 0xC1";
6346
6501
  var sequentialMode = false;
6347
6502
  var inlineObjectReadThreshold = 2;
6348
- var readStruct;
6349
- var onLoadedStructures;
6350
- var onSaveState;
6351
- try {
6352
- new Function("");
6353
- } catch (error) {
6354
- inlineObjectReadThreshold = Infinity;
6355
- }
6356
6503
  var Unpackr = class _Unpackr {
6357
6504
  constructor(options) {
6358
6505
  if (options) {
@@ -6455,8 +6602,8 @@ var Unpackr = class _Unpackr {
6455
6602
  }
6456
6603
  }
6457
6604
  _mergeStructures(loadedStructures, existingStructures) {
6458
- if (onLoadedStructures)
6459
- loadedStructures = onLoadedStructures.call(this, loadedStructures);
6605
+ if (this._onLoadedStructures)
6606
+ loadedStructures = this._onLoadedStructures(loadedStructures);
6460
6607
  loadedStructures = loadedStructures || [];
6461
6608
  if (Object.isFrozen(loadedStructures))
6462
6609
  loadedStructures = loadedStructures.map((structure) => structure.slice(0));
@@ -6494,8 +6641,8 @@ function checkedRead(options) {
6494
6641
  currentStructures.length = sharedLength;
6495
6642
  }
6496
6643
  let result;
6497
- if (currentUnpackr.randomAccessStructure && src[position] < 64 && src[position] >= 32 && readStruct) {
6498
- result = readStruct(src, position, srcEnd, currentUnpackr);
6644
+ if (currentUnpackr._readStruct && src[position] < 64 && src[position] >= 32) {
6645
+ result = currentUnpackr._readStruct(src, position, srcEnd);
6499
6646
  src = null;
6500
6647
  if (!(options && options.lazy) && result)
6501
6648
  result = result.toJSON();
@@ -6784,10 +6931,16 @@ var validName = /^[a-zA-Z_$][a-zA-Z\d_$]*$/;
6784
6931
  function createStructureReader(structure, firstId) {
6785
6932
  function readObject() {
6786
6933
  if (readObject.count++ > inlineObjectReadThreshold) {
6787
- let readObject2 = structure.read = new Function("r", "return function(){return " + (currentUnpackr.freezeData ? "Object.freeze" : "") + "({" + structure.map((key) => key === "__proto__" ? "__proto_:r()" : validName.test(key) ? key + ":r()" : "[" + JSON.stringify(key) + "]:r()").join(",") + "})}")(read);
6934
+ let optimizedReadObject;
6935
+ try {
6936
+ optimizedReadObject = structure.read = new Function("r", "return function(){return " + (currentUnpackr.freezeData ? "Object.freeze" : "") + "({" + structure.map((key) => key === "__proto__" ? "__proto_:r()" : validName.test(key) ? key + ":r()" : "[" + JSON.stringify(key) + "]:r()").join(",") + "})}")(read);
6937
+ } catch (error) {
6938
+ inlineObjectReadThreshold = Infinity;
6939
+ return readObject();
6940
+ }
6788
6941
  if (structure.highByte === 0)
6789
6942
  structure.read = createSecondByteReader(firstId, structure.read);
6790
- return readObject2();
6943
+ return optimizedReadObject();
6791
6944
  }
6792
6945
  let object = {};
6793
6946
  for (let i = 0, l = structure.length; i < l; i++) {
@@ -7219,7 +7372,7 @@ currentExtensions[66] = (data) => {
7219
7372
  if (length <= 40) {
7220
7373
  let out = view.getBigUint64(start);
7221
7374
  for (let i = start + 8; i < end; i += 8) {
7222
- out <<= BigInt(/* @__PURE__ */ BigInt("64"));
7375
+ out <<= BigInt(64);
7223
7376
  out |= view.getBigUint64(i);
7224
7377
  }
7225
7378
  return out;
@@ -7334,8 +7487,8 @@ currentExtensions[255] = (data) => {
7334
7487
  return /* @__PURE__ */ new Date("invalid");
7335
7488
  };
7336
7489
  function saveState(callback) {
7337
- if (onSaveState)
7338
- onSaveState();
7490
+ if (currentUnpackr && currentUnpackr._onSaveState)
7491
+ currentUnpackr._onSaveState();
7339
7492
  let savedSrcEnd = srcEnd;
7340
7493
  let savedPosition = position;
7341
7494
  let savedStringPosition = stringPosition;
@@ -7389,6 +7542,7 @@ var FLOAT32_OPTIONS = {
7389
7542
  };
7390
7543
  var f32Array = new Float32Array(1);
7391
7544
  var u8Array = new Uint8Array(f32Array.buffer, 0, 4);
7545
+ Unpackr.SUPPORTS_STRUCT_HOOKS = true;
7392
7546
 
7393
7547
  // node_modules/msgpackr/pack.js
7394
7548
  var textEncoder;
@@ -7410,7 +7564,6 @@ var targetView;
7410
7564
  var position2 = 0;
7411
7565
  var safeEnd;
7412
7566
  var bundledStrings2 = null;
7413
- var writeStructSlots;
7414
7567
  var MAX_BUNDLE_SIZE = 21760;
7415
7568
  var hasNonLatin = /[\u0080-\uFFFF]/;
7416
7569
  var RECORD_SYMBOL = /* @__PURE__ */ Symbol("record-id");
@@ -7512,7 +7665,7 @@ var Packr = class extends Unpackr {
7512
7665
  hasSharedUpdate = false;
7513
7666
  let encodingError;
7514
7667
  try {
7515
- if (packr3.randomAccessStructure && value && typeof value === "object") {
7668
+ if (packr3._writeStruct && value && typeof value === "object") {
7516
7669
  if (value.constructor === Object) writeStruct(value);
7517
7670
  else if (value.constructor !== Map && !Array.isArray(value) && !extensionClasses.some((extClass) => value instanceof extClass)) {
7518
7671
  writeStruct(value.toJSON ? value.toJSON() : value);
@@ -7575,7 +7728,7 @@ var Packr = class extends Unpackr {
7575
7728
  if (hasSharedUpdate && packr3.saveStructures) {
7576
7729
  let sharedLength = structures.sharedLength || 0;
7577
7730
  let returnBuffer = target.subarray(start, position2);
7578
- let newSharedData = prepareStructures(structures, packr3);
7731
+ let newSharedData = (packr3._prepareStructures || prepareStructures)(structures, packr3);
7579
7732
  if (!encodingError) {
7580
7733
  if (packr3.saveStructures(newSharedData, newSharedData.isCompatible) === false) {
7581
7734
  return packr3.pack(value, encodeOptions);
@@ -7732,7 +7885,7 @@ var Packr = class extends Unpackr {
7732
7885
  position2 += length;
7733
7886
  } else if (type === "number") {
7734
7887
  if (value >>> 0 === value) {
7735
- if (value < 32 || value < 128 && this.useRecords === false || value < 64 && !this.randomAccessStructure) {
7888
+ if (value < 32 || value < 128 && this.useRecords === false || value < 64 && !this._writeStruct) {
7736
7889
  target[position2++] = value;
7737
7890
  } else if (value < 256) {
7738
7891
  target[position2++] = 204;
@@ -8091,6 +8244,23 @@ var Packr = class extends Unpackr {
8091
8244
  const writeObject = checkUseRecords ? (object) => {
8092
8245
  checkUseRecords(object) ? writeRecord(object) : writePlainObject(object);
8093
8246
  } : writeRecord;
8247
+ const writeStruct = (object) => {
8248
+ let newPosition = packr3._writeStruct(object, target, start, position2, structures, makeRoom, (value, newPosition2, notifySharedUpdate) => {
8249
+ if (notifySharedUpdate)
8250
+ return hasSharedUpdate = true;
8251
+ position2 = newPosition2;
8252
+ let startTarget = target;
8253
+ pack3(value);
8254
+ resetStructures();
8255
+ if (startTarget !== target) {
8256
+ return { position: position2, targetView, target };
8257
+ }
8258
+ return position2;
8259
+ });
8260
+ if (newPosition === 0)
8261
+ return writeObject(object);
8262
+ position2 = newPosition;
8263
+ };
8094
8264
  const makeRoom = (end) => {
8095
8265
  let newSize;
8096
8266
  if (end > 16777216) {
@@ -8191,23 +8361,6 @@ var Packr = class extends Unpackr {
8191
8361
  target[insertionOffset + start] = keysTarget[0];
8192
8362
  }
8193
8363
  };
8194
- const writeStruct = (object) => {
8195
- let newPosition = writeStructSlots(object, target, start, position2, structures, makeRoom, (value, newPosition2, notifySharedUpdate) => {
8196
- if (notifySharedUpdate)
8197
- return hasSharedUpdate = true;
8198
- position2 = newPosition2;
8199
- let startTarget = target;
8200
- pack3(value);
8201
- resetStructures();
8202
- if (startTarget !== target) {
8203
- return { position: position2, targetView, target };
8204
- }
8205
- return position2;
8206
- }, this);
8207
- if (newPosition === 0)
8208
- return writeObject(object);
8209
- position2 = newPosition;
8210
- };
8211
8364
  }
8212
8365
  useBuffer(buffer) {
8213
8366
  target = buffer;
@@ -8444,6 +8597,7 @@ function prepareStructures(structures, packr3) {
8444
8597
  };
8445
8598
  return structures;
8446
8599
  }
8600
+ Packr.SUPPORTS_STRUCT_HOOKS = true;
8447
8601
  var defaultPackr = new Packr({ useRecords: false });
8448
8602
  var pack = defaultPackr.pack;
8449
8603
  var encode = defaultPackr.pack;
@@ -403,6 +403,30 @@ export declare class SyncedDb implements I_SyncedDb {
403
403
  */
404
404
  private static stringifyObjectIds;
405
405
  private static isObjectIdLike;
406
+ /**
407
+ * Mongo-symmetric local apply: starting from `base` (a safe deep clone of
408
+ * `currentMem` or `existing`), walk each `(path, value)` entry of `diff`:
409
+ *
410
+ * - `value === undefined` → `deleteByPath` (mongo `$unset` symmetric)
411
+ * - otherwise → `setByPath` (mongo `$set` symmetric)
412
+ *
413
+ * The result is what an equivalent server-side `$set` + `$unset` would
414
+ * have produced. Replaces the previous shallow `{ ...currentMem, ...update }`
415
+ * merge which dropped nested fields the caller's `update` didn't mention.
416
+ *
417
+ * Returns a new object (the cloned-and-mutated `base`); never mutates
418
+ * the input `base` reference.
419
+ */
420
+ private static applyDiffLocally;
421
+ /**
422
+ * Deep clone for `applyDiffLocally`. Recurses into plain objects and
423
+ * arrays; preserves `Date` (cloned to avoid shared reference) and
424
+ * `ObjectId`-like values by reference (their internal Buffer state is
425
+ * immutable from our perspective). Other class instances pass through
426
+ * by reference. Avoids `structuredClone` because it throws on class
427
+ * instances like bson `ObjectId`.
428
+ */
429
+ private static safeDeepClone;
406
430
  /**
407
431
  * Recursively walk `value` and ensure every plain object that appears
408
432
  * as an element of an array carries an `_id`. Missing `_id`s are
@@ -41,6 +41,34 @@ export declare function hasDotNotationPaths(changes: Record<string, any>): boole
41
41
  * "arr[A].sub[B].field" → ["arr", "[A]", "sub", "[B]", "field"]
42
42
  */
43
43
  export declare function tokenizePath(path: string): string[];
44
+ /**
45
+ * Set a value at a path within a nested target. Supports three segment forms:
46
+ * - Numeric ("0", "1") → array index
47
+ * - Bracket ("[<_id>]") → array element matched by `_id` field
48
+ * - Plain ("field") → object key
49
+ *
50
+ * @returns true if the value was successfully set, false if path traversal failed.
51
+ */
52
+ export declare function setByPath(target: any, path: string, value: any): boolean;
53
+ /**
54
+ * Delete the value at `path` within `target`. Sibling of `setByPath` —
55
+ * navigates the same tokenized path forms (numeric, bracket-by-_id, plain),
56
+ * but the final segment removes the property/element instead of setting it.
57
+ *
58
+ * Used by `save()`/`insert()`/`upsert()` to honor the convention that an
59
+ * `undefined` value in the caller's `update` means "delete this field".
60
+ *
61
+ * Behavior on the last segment:
62
+ * - plain key: `delete obj[key]`
63
+ * - numeric `"<idx>"` on array: `arr.splice(idx, 1)` (removes element)
64
+ * - bracket `"[<_id>]"` on array of `_id`-keyed objects: splice the
65
+ * matching element
66
+ *
67
+ * Returns `true` if a delete actually occurred. `false` if traversal failed
68
+ * (path leads through a missing/non-object value) or the target segment
69
+ * doesn't exist — caller can ignore.
70
+ */
71
+ export declare function deleteByPath(target: any, path: string): boolean;
44
72
  /**
45
73
  * Smart-merge a single (path, value) entry into an accumulated dirty changes
46
74
  * object. Resolves three relationships between the new path and existing keys:
@@ -49,6 +77,13 @@ export declare function tokenizePath(path: string): string[];
49
77
  * new is "koraki.0.diag"): mutate the value inside the existing parent
50
78
  * rather than adding a conflicting child path.
51
79
  *
80
+ * SPECIAL CASES for terminal-bracket existing keys (whole-element ops):
81
+ * a) existing value is `undefined` (REMOVE marker) → drop new sub-field
82
+ * silently. Element is gone, sub-field write is moot.
83
+ * b) existing value is `[element]` (INSERT wrapper) → navigate INTO the
84
+ * wrapped element[0], not into the array itself. The pending insert
85
+ * absorbs the sub-field edit.
86
+ *
52
87
  * 2. New path is an ANCESTOR of existing keys (e.g. existing has "koraki.0.diag",
53
88
  * new is "koraki" with full array): remove the now-redundant descendants and
54
89
  * set the parent path. The new full value supersedes any field-level deltas.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Walk `value` recursively without mutation. Return the dot/bracket
3
+ * notation paths of every property whose value is `undefined`.
4
+ *
5
+ * The original `value` (and all nested objects/arrays) is left intact —
6
+ * `undefined` values stay in place so that downstream code (computeDiff,
7
+ * dirty-change storage, REST upload via msgpackr) can carry them through
8
+ * to cry-db, which natively routes `key: undefined` → `$unset` server-side.
9
+ */
10
+ export declare function collectUnsetPaths(value: any): string[];
@@ -1,7 +1,11 @@
1
1
  /**
2
- * Translate every key in `changes` from bracket notation to mongo dot
3
- * notation, resolving `[<_id>]` against the corresponding sub-array of
4
- * `entity`. Drops keys whose `_id` cannot be resolved (= element was
5
- * removed by another writer).
2
+ * Pass-through: returns changes unchanged. Server-side cry-db v2.4.33+
3
+ * `_applyBracketProcessing` handles all bracket forms atomically:
4
+ * - terminal `arr[<id>]` with array value idempotent `$concatArrays + $filter`
5
+ * - terminal `arr[<id>]` with `undefined` → `$pull`
6
+ * - terminal `arr[<id>]` with object → arrayFilter `$set`
7
+ * - sub-field `arr[<id>].field` → arrayFilter `$set` (position-independent)
8
+ *
9
+ * Signature kept (with `_entity` unused) for backward-compat with callers.
6
10
  */
7
- export declare function translateBracketPathsToIndex(changes: Record<string, any>, entity: any): Record<string, any>;
11
+ export declare function translateBracketPathsToIndex(changes: Record<string, any>, _entity: any): Record<string, any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.159",
3
+ "version": "0.1.160",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -30,19 +30,19 @@
30
30
  "bson": "^7.2.0",
31
31
  "cry-ebus-proxy": "^1.0.3",
32
32
  "dexie": "^4.4.2",
33
- "esbuild": "^0.27.4",
33
+ "esbuild": "^0.28.0",
34
34
  "fake-indexeddb": "^6.2.5",
35
35
  "typescript": "^6",
36
- "vitest": "^4.1.2"
36
+ "vitest": "^4.1.5"
37
37
  },
38
38
  "dependencies": {
39
- "cry-db": "^2.4.32",
40
- "cry-helpers": "^2.1.193",
41
- "msgpackr": "^1.11.9",
39
+ "cry-db": "^2.4.33",
40
+ "cry-helpers": "^2.1.194",
41
+ "msgpackr": "^2.0.1",
42
42
  "notepack": "^0.0.2",
43
43
  "notepack.io": "^3.0.1",
44
44
  "superjson": "^2.2.6",
45
- "undici": "^7.24.6"
45
+ "undici": "^8.2.0"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "bson": "^6.0.0 || ^7.0.0",