cry-synced-db-client 0.1.158 → 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;
@@ -3671,6 +3762,11 @@ var ServerUpdateHandler = class {
3671
3762
  async handleServerItemInsert(collection, serverItem) {
3672
3763
  const localItem = await this.dexieDb.getById(collection, serverItem._id);
3673
3764
  if (localItem) {
3765
+ const isStaleSelfEcho = serverItem._lastUpdaterId === this.updaterId && typeof serverItem._rev === "number" && typeof localItem._rev === "number" && serverItem._rev <= localItem._rev;
3766
+ if (isStaleSelfEcho) {
3767
+ await this.dexieDb.clearDirtyChange(collection, serverItem._id);
3768
+ return;
3769
+ }
3674
3770
  const dirtyChange = await this.dexieDb.getDirtyChange(collection, serverItem._id);
3675
3771
  const metaChanged = localItem._rev !== serverItem._rev || !this.timestampsEqual(localItem._ts, serverItem._ts);
3676
3772
  if (metaChanged) {
@@ -3696,12 +3792,13 @@ var ServerUpdateHandler = class {
3696
3792
  * Handle server item update (delta).
3697
3793
  */
3698
3794
  async handleServerItemUpdate(collection, localItem, serverDelta) {
3699
- const isLoopback = serverDelta._lastUpdaterId === this.updaterId && serverDelta._rev !== void 0 && localItem._rev !== void 0 && serverDelta._rev === localItem._rev + 1;
3700
- if (isLoopback) {
3701
- const metaChanged2 = localItem._rev !== serverDelta._rev || !this.timestampsEqual(localItem._ts, serverDelta._ts);
3702
- if (metaChanged2) {
3795
+ const serverRev = serverDelta._rev;
3796
+ const localRev = localItem._rev;
3797
+ const isSelfEcho = serverDelta._lastUpdaterId === this.updaterId && typeof serverRev === "number" && typeof localRev === "number" && serverRev <= localRev + 1;
3798
+ if (isSelfEcho) {
3799
+ if (serverRev > localRev) {
3703
3800
  await this.dexieDb.save(collection, serverDelta._id, {
3704
- _rev: serverDelta._rev,
3801
+ _rev: serverRev,
3705
3802
  _ts: serverDelta._ts
3706
3803
  });
3707
3804
  }
@@ -4814,7 +4911,11 @@ var _SyncedDb = class _SyncedDb {
4814
4911
  this.pendingChanges.schedule(collection, id, newData, 0, "save");
4815
4912
  const isWriteOnly = (_b = this.collections.get(collection)) == null ? void 0 : _b.writeOnly;
4816
4913
  const currentMem = isWriteOnly ? null : this.inMemDb.getById(collection, id);
4817
- const merged = __spreadValues(__spreadValues({}, currentMem || existing || { _id: id }), update);
4914
+ const merged = _SyncedDb.applyDiffLocally(
4915
+ currentMem != null ? currentMem : existing,
4916
+ diff,
4917
+ id
4918
+ );
4818
4919
  if (!isWriteOnly && !(existing == null ? void 0 : existing._deleted) && !(existing == null ? void 0 : existing._archived)) {
4819
4920
  this.inMemManager.writeBatch(collection, [merged], "upsert", { source: "incremental" });
4820
4921
  }
@@ -4839,6 +4940,7 @@ var _SyncedDb = class _SyncedDb {
4839
4940
  this.ensureId(data, "insert", collection);
4840
4941
  _SyncedDb.ensureNestedIds(data);
4841
4942
  data = _SyncedDb.stringifyObjectIds(data);
4943
+ const unsetPaths = collectUnsetPaths(data);
4842
4944
  const id = String(data._id);
4843
4945
  const existing = await this.dexieDb.getById(collection, id);
4844
4946
  if (existing && !existing._deleted && !existing._archived) {
@@ -4856,6 +4958,7 @@ var _SyncedDb = class _SyncedDb {
4856
4958
  _id: id,
4857
4959
  _lastUpdaterId: this.updaterId
4858
4960
  });
4961
+ for (const path of unsetPaths) deleteByPath(newData, path);
4859
4962
  this.pendingChanges.schedule(collection, id, newData, 0, "insert");
4860
4963
  if (!((_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly)) {
4861
4964
  this.inMemManager.writeBatch(collection, [newData], "upsert", { source: "incremental" });
@@ -5939,6 +6042,64 @@ var _SyncedDb = class _SyncedDb {
5939
6042
  static isObjectIdLike(v) {
5940
6043
  return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || typeof v.toHexString === "function"));
5941
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
+ }
5942
6103
  /**
5943
6104
  * Recursively walk `value` and ensure every plain object that appears
5944
6105
  * as an element of an array carries an `_id`. Missing `_id`s are
@@ -6339,14 +6500,6 @@ var C1 = new C1Type();
6339
6500
  C1.name = "MessagePack 0xC1";
6340
6501
  var sequentialMode = false;
6341
6502
  var inlineObjectReadThreshold = 2;
6342
- var readStruct;
6343
- var onLoadedStructures;
6344
- var onSaveState;
6345
- try {
6346
- new Function("");
6347
- } catch (error) {
6348
- inlineObjectReadThreshold = Infinity;
6349
- }
6350
6503
  var Unpackr = class _Unpackr {
6351
6504
  constructor(options) {
6352
6505
  if (options) {
@@ -6449,8 +6602,8 @@ var Unpackr = class _Unpackr {
6449
6602
  }
6450
6603
  }
6451
6604
  _mergeStructures(loadedStructures, existingStructures) {
6452
- if (onLoadedStructures)
6453
- loadedStructures = onLoadedStructures.call(this, loadedStructures);
6605
+ if (this._onLoadedStructures)
6606
+ loadedStructures = this._onLoadedStructures(loadedStructures);
6454
6607
  loadedStructures = loadedStructures || [];
6455
6608
  if (Object.isFrozen(loadedStructures))
6456
6609
  loadedStructures = loadedStructures.map((structure) => structure.slice(0));
@@ -6488,8 +6641,8 @@ function checkedRead(options) {
6488
6641
  currentStructures.length = sharedLength;
6489
6642
  }
6490
6643
  let result;
6491
- if (currentUnpackr.randomAccessStructure && src[position] < 64 && src[position] >= 32 && readStruct) {
6492
- result = readStruct(src, position, srcEnd, currentUnpackr);
6644
+ if (currentUnpackr._readStruct && src[position] < 64 && src[position] >= 32) {
6645
+ result = currentUnpackr._readStruct(src, position, srcEnd);
6493
6646
  src = null;
6494
6647
  if (!(options && options.lazy) && result)
6495
6648
  result = result.toJSON();
@@ -6778,10 +6931,16 @@ var validName = /^[a-zA-Z_$][a-zA-Z\d_$]*$/;
6778
6931
  function createStructureReader(structure, firstId) {
6779
6932
  function readObject() {
6780
6933
  if (readObject.count++ > inlineObjectReadThreshold) {
6781
- 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
+ }
6782
6941
  if (structure.highByte === 0)
6783
6942
  structure.read = createSecondByteReader(firstId, structure.read);
6784
- return readObject2();
6943
+ return optimizedReadObject();
6785
6944
  }
6786
6945
  let object = {};
6787
6946
  for (let i = 0, l = structure.length; i < l; i++) {
@@ -7213,7 +7372,7 @@ currentExtensions[66] = (data) => {
7213
7372
  if (length <= 40) {
7214
7373
  let out = view.getBigUint64(start);
7215
7374
  for (let i = start + 8; i < end; i += 8) {
7216
- out <<= BigInt(/* @__PURE__ */ BigInt("64"));
7375
+ out <<= BigInt(64);
7217
7376
  out |= view.getBigUint64(i);
7218
7377
  }
7219
7378
  return out;
@@ -7328,8 +7487,8 @@ currentExtensions[255] = (data) => {
7328
7487
  return /* @__PURE__ */ new Date("invalid");
7329
7488
  };
7330
7489
  function saveState(callback) {
7331
- if (onSaveState)
7332
- onSaveState();
7490
+ if (currentUnpackr && currentUnpackr._onSaveState)
7491
+ currentUnpackr._onSaveState();
7333
7492
  let savedSrcEnd = srcEnd;
7334
7493
  let savedPosition = position;
7335
7494
  let savedStringPosition = stringPosition;
@@ -7383,6 +7542,7 @@ var FLOAT32_OPTIONS = {
7383
7542
  };
7384
7543
  var f32Array = new Float32Array(1);
7385
7544
  var u8Array = new Uint8Array(f32Array.buffer, 0, 4);
7545
+ Unpackr.SUPPORTS_STRUCT_HOOKS = true;
7386
7546
 
7387
7547
  // node_modules/msgpackr/pack.js
7388
7548
  var textEncoder;
@@ -7404,7 +7564,6 @@ var targetView;
7404
7564
  var position2 = 0;
7405
7565
  var safeEnd;
7406
7566
  var bundledStrings2 = null;
7407
- var writeStructSlots;
7408
7567
  var MAX_BUNDLE_SIZE = 21760;
7409
7568
  var hasNonLatin = /[\u0080-\uFFFF]/;
7410
7569
  var RECORD_SYMBOL = /* @__PURE__ */ Symbol("record-id");
@@ -7506,7 +7665,7 @@ var Packr = class extends Unpackr {
7506
7665
  hasSharedUpdate = false;
7507
7666
  let encodingError;
7508
7667
  try {
7509
- if (packr3.randomAccessStructure && value && typeof value === "object") {
7668
+ if (packr3._writeStruct && value && typeof value === "object") {
7510
7669
  if (value.constructor === Object) writeStruct(value);
7511
7670
  else if (value.constructor !== Map && !Array.isArray(value) && !extensionClasses.some((extClass) => value instanceof extClass)) {
7512
7671
  writeStruct(value.toJSON ? value.toJSON() : value);
@@ -7569,7 +7728,7 @@ var Packr = class extends Unpackr {
7569
7728
  if (hasSharedUpdate && packr3.saveStructures) {
7570
7729
  let sharedLength = structures.sharedLength || 0;
7571
7730
  let returnBuffer = target.subarray(start, position2);
7572
- let newSharedData = prepareStructures(structures, packr3);
7731
+ let newSharedData = (packr3._prepareStructures || prepareStructures)(structures, packr3);
7573
7732
  if (!encodingError) {
7574
7733
  if (packr3.saveStructures(newSharedData, newSharedData.isCompatible) === false) {
7575
7734
  return packr3.pack(value, encodeOptions);
@@ -7726,7 +7885,7 @@ var Packr = class extends Unpackr {
7726
7885
  position2 += length;
7727
7886
  } else if (type === "number") {
7728
7887
  if (value >>> 0 === value) {
7729
- 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) {
7730
7889
  target[position2++] = value;
7731
7890
  } else if (value < 256) {
7732
7891
  target[position2++] = 204;
@@ -8085,6 +8244,23 @@ var Packr = class extends Unpackr {
8085
8244
  const writeObject = checkUseRecords ? (object) => {
8086
8245
  checkUseRecords(object) ? writeRecord(object) : writePlainObject(object);
8087
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
+ };
8088
8264
  const makeRoom = (end) => {
8089
8265
  let newSize;
8090
8266
  if (end > 16777216) {
@@ -8185,23 +8361,6 @@ var Packr = class extends Unpackr {
8185
8361
  target[insertionOffset + start] = keysTarget[0];
8186
8362
  }
8187
8363
  };
8188
- const writeStruct = (object) => {
8189
- let newPosition = writeStructSlots(object, target, start, position2, structures, makeRoom, (value, newPosition2, notifySharedUpdate) => {
8190
- if (notifySharedUpdate)
8191
- return hasSharedUpdate = true;
8192
- position2 = newPosition2;
8193
- let startTarget = target;
8194
- pack3(value);
8195
- resetStructures();
8196
- if (startTarget !== target) {
8197
- return { position: position2, targetView, target };
8198
- }
8199
- return position2;
8200
- }, this);
8201
- if (newPosition === 0)
8202
- return writeObject(object);
8203
- position2 = newPosition;
8204
- };
8205
8364
  }
8206
8365
  useBuffer(buffer) {
8207
8366
  target = buffer;
@@ -8438,6 +8597,7 @@ function prepareStructures(structures, packr3) {
8438
8597
  };
8439
8598
  return structures;
8440
8599
  }
8600
+ Packr.SUPPORTS_STRUCT_HOOKS = true;
8441
8601
  var defaultPackr = new Packr({ useRecords: false });
8442
8602
  var pack = defaultPackr.pack;
8443
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.158",
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",