cry-synced-db-client 0.1.183 → 0.1.185

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/CHANGELOG.md CHANGED
@@ -1,5 +1,123 @@
1
1
  # Versions
2
2
 
3
+ ## 0.1.185 (2026-05-14)
4
+
5
+ ### Fix: server-wins rebase resolves dirty-merge path conflicts (`sestevki`)
6
+
7
+ Production bug (klikvet, 2026-05-14): obisk records stuck dirty with
8
+ repeated upload errors —
9
+
10
+ ```
11
+ [SyncEngine] Sync upload error [obiski] _id=…:
12
+ Updating the path 'sestevki.skupaj.prc' would create a conflict at 'sestevki'
13
+ ```
14
+
15
+ Reproduction sequence (see `test/sestevkiPathConflict.test.ts`):
16
+
17
+ 1. Local record exists; `sestevki` is missing.
18
+ 2. App writes a partial `sestevki = {kritje: {…}}`. `computeDiff` emits
19
+ a single full-replace key; dirty entry stores
20
+ `changes = {sestevki: {kritje}}` at `baseRev = 1`.
21
+ 3. WS-push of a server-side update lands `sestevki.skupaj = {…}` into
22
+ the Dexie row and bumps `_rev` to `2`. The dirty entry's
23
+ accumulated `sestevki` value is NOT touched.
24
+ 4. App writes `sestevki.skupaj.prc = 50`. `computeDiff` against the
25
+ advanced Dexie row emits a deep dot-path. `mergeDirtyPath`'s
26
+ Case 1 (`setByPath` into the accumulated parent) fails on the
27
+ missing intermediate `skupaj` and falls through to Case 3, which
28
+ writes the deep path as a SIBLING key alongside the parent. The
29
+ dirty payload now carries BOTH `sestevki = {…}` AND
30
+ `sestevki.skupaj.prc = 50` — mongo `$set` refuses every retry.
31
+
32
+ **Rule** (per user directive): for any given path, when the server's
33
+ `_rev` is greater than the local `_rev` the path was emitted against,
34
+ **the server wins** — drop the locally-accumulated path. `baseMeta`
35
+ passed into `addDirtyChange*` is sampled from the live Dexie row by
36
+ `save`, so `baseMeta._rev > entry.baseRev` is the trigger.
37
+
38
+ Surgical scope: only existing paths that OVERLAP with an incoming path
39
+ (equal / ancestor / descendant) are dropped. Unrelated accumulated
40
+ paths are preserved. After the prune, `entry.baseTs` / `entry.baseRev`
41
+ advance to the new floor.
42
+
43
+ Implemented in:
44
+ - `src/db/DexieDb.ts` — new module-level `rebaseDirtyOnServerAdvance`
45
+ called from both `addDirtyChange` and `addDirtyChangesBatch` before
46
+ `mergeDirtyChanges`.
47
+ - `test/mocks/MockDexieDb.ts` — same logic so the bun-test suite
48
+ exercises the same merge semantics (the mock previously used shallow
49
+ spread `{...existing.changes, ...changes}` and masked this bug).
50
+ - `src/utils/computeDiff.ts` — new exported `pathsOverlap(a, b)` helper.
51
+
52
+ ### Fix: `applyDiffLocally` materializes plain dot-path intermediates
53
+
54
+ Companion fix: even with the dirty merge cleaned up, the second save's
55
+ deep dot-path `sestevki.skupaj.prc` still needs to land in the local
56
+ in-mem snapshot. The seed (cloned from `currentMem ?? existing`) may
57
+ be missing the `skupaj` intermediate (in-mem wasn't touched by the
58
+ external WS-push). Previously `setByPath` failed → fell to
59
+ `materializeBracketPath` → dropped with `no bracket segment found`.
60
+
61
+ New `materializePlainDotPath` private helper walks a dot-only path,
62
+ auto-creates plain-object intermediates, and lands the value:
63
+
64
+ | Shape | Result |
65
+ |---|---|
66
+ | `a.b.c = v` with seed missing `b` and `c` | `seed.a = {b: {c: v}}` |
67
+ | Existing intermediate is not a plain object (Date, primitive, array) | Refuses (false), falls through |
68
+ | Path contains numeric or bracket segment | Refuses (false), falls through to `materializeBracketPath` |
69
+
70
+ Called between `setByPath` and `materializeBracketPath` in
71
+ `applyDiffLocally`. Bracket-containing paths still take the existing
72
+ bracket-fallback path.
73
+
74
+ ## 0.1.184 (2026-05-14)
75
+
76
+ ### Fix: `applyDiffLocally` supports multi-segment plain prefix before bracket-by-id
77
+
78
+ The materialization fallback used to require the bracket-by-id to live
79
+ at token position [1] — anything deeper failed the
80
+ `secondToken.startsWith("[")` check and dropped with
81
+ `second segment is not a bracket-by-id`. Diff paths like
82
+ `outer.field[<id>]` or `a.b.c.arr[<id>].deep` fell through this gap;
83
+ the data was silently lost locally (visible drop unless the first
84
+ segment started with `_`).
85
+
86
+ Now any plain dot-notation prefix before a single bracket-by-id is
87
+ materialized:
88
+
89
+ ```
90
+ plain1.plain2…plainN[<id>][.subA.subB…] → materialize
91
+ ```
92
+
93
+ | Shape | Result |
94
+ |---|---|
95
+ | `outer.arr[<newId>] = <obj>` | `seed.outer = {arr: [<obj>]}` |
96
+ | `outer.arr[<newId>].field = v` | `seed.outer = {arr: [{_id: <newId>, field: v}]}` |
97
+ | `a.b.c.arr[<id>].seg1.seg2 = v` | `seed.a = {b: {c: {arr: [{_id, seg1: {seg2: v}}]}}}` |
98
+ | Matching `_id` exists, intermediate missing | Walk into element, create missing intermediates, set leaf. No duplicate element. |
99
+ | Existing intermediate is not a plain object (Date, primitive, array) | Drop visibly — refuse to overwrite. |
100
+
101
+ **Still drops**: multi-bracket paths (`outer[a].sub[b]`,
102
+ `outer.a.sub[i].nested[j]`) — these need shape knowledge the fallback
103
+ can't reconstruct; server applies the path and next sync hydrates the
104
+ canonical state.
105
+
106
+ **Backward-compat for `_`-mirrored roots**: paths under `_redundanca`,
107
+ `_dobavnica_saved`, etc. that extend beyond the originally-supported
108
+ single-segment-prefix shape (e.g. `_redundanca.terapije[<id>]`) still
109
+ drop silently. These fields are server-mirrored — local materialization
110
+ with partial data could diverge from the canonical state.
111
+
112
+ Tests:
113
+ - `test/applyDiffLocallyObiskShape.test.ts` — 5 new cases covering
114
+ multi-segment prefix shapes plus the `_`-prefix silent-drop contract.
115
+ - `test/applyDiffLocallyMaterialize.test.ts` — flipped the
116
+ `a.b[<id>]` case from "drops with diagnostic log" to
117
+ "materializes `{a: {b: [<el>]}}`".
118
+
119
+ Full suite: 767 bun + 18 vitest pass.
120
+
3
121
  ## 0.1.183 (2026-05-14)
4
122
 
5
123
  ### Fix: `applyDiffLocally` materializes deep sub-field paths under bracket-by-id
package/dist/index.js CHANGED
@@ -488,6 +488,9 @@ function isDescendantOrEqual(path, candidate) {
488
488
  const next = path[candidate.length];
489
489
  return next === "." || next === "[";
490
490
  }
491
+ function pathsOverlap(a, b) {
492
+ return isDescendantOrEqual(a, b) || isDescendantOrEqual(b, a);
493
+ }
491
494
  function tokenizePath(path) {
492
495
  const out = [];
493
496
  let buf = "";
@@ -6645,33 +6648,80 @@ var _SyncedDb = class _SyncedDb {
6645
6648
  continue;
6646
6649
  }
6647
6650
  const ok = setByPath(seed, path, value);
6648
- if (!ok) {
6649
- _SyncedDb.materializeBracketPath(seed, path, value, collection, fallbackId);
6651
+ if (ok) continue;
6652
+ if (!path.includes("[") && _SyncedDb.materializePlainDotPath(seed, path, value)) {
6653
+ continue;
6650
6654
  }
6655
+ _SyncedDb.materializeBracketPath(seed, path, value, collection, fallbackId);
6651
6656
  }
6652
6657
  return seed;
6653
6658
  }
6659
+ /**
6660
+ * Bracket-free dot-path fallback for `applyDiffLocally`. Walks the path,
6661
+ * creating missing plain-object intermediates as needed, and sets the
6662
+ * leaf. Refuses (returns `false`) if any segment is numeric (array
6663
+ * index) or `[<id>]` (bracket selector) — those need shape knowledge
6664
+ * outside the scope of plain-object materialization. Also refuses if
6665
+ * an existing intermediate is non-plain (array, Date, primitive) —
6666
+ * overwriting would risk silent data loss.
6667
+ *
6668
+ * Used after `setByPath` fails on a dot-only path (no `[` in `path`).
6669
+ * Counterpart to `materializeBracketPath` for the bracket case.
6670
+ */
6671
+ static materializePlainDotPath(seed, path, value) {
6672
+ const parts = tokenizePath(path);
6673
+ if (parts.length < 2) return false;
6674
+ for (const seg of parts) {
6675
+ if (seg.startsWith("[") || /^\d+$/.test(seg)) return false;
6676
+ }
6677
+ let cur = seed;
6678
+ for (let i = 0; i < parts.length - 1; i++) {
6679
+ const seg = parts[i];
6680
+ const next = cur[seg];
6681
+ if (next == null) {
6682
+ cur[seg] = {};
6683
+ } else if (typeof next !== "object" || Array.isArray(next) || next instanceof Date) {
6684
+ return false;
6685
+ }
6686
+ cur = cur[seg];
6687
+ }
6688
+ cur[parts[parts.length - 1]] = value;
6689
+ return true;
6690
+ }
6654
6691
  /**
6655
6692
  * Fallback for `setByPath` failures inside `applyDiffLocally`. Materializes
6656
- * the parent array when it's missing from `seed`, OR fills in missing
6657
- * intermediate objects under an existing array element when the diff
6658
- * path reaches deeper than the element currently holds.
6693
+ * missing array containers AND missing intermediate plain objects so the
6694
+ * diff entry can land locally instead of being dropped.
6695
+ *
6696
+ * Supported path shape (tokenizes to `[…plain prefix, [<id>], …plain suffix]`):
6697
+ *
6698
+ * • Single-segment prefix:
6699
+ * - `polje[<id>] = <obj>` → seed.polje = [<obj>]
6700
+ * - `polje[<id>].field = <v>` → seed.polje = [{_id, field: <v>}]
6701
+ * - `polje[<id>].a.b.c = <v>` → seed.polje = [{_id, a: {b: {c: <v>}}}]
6702
+ *
6703
+ * • Multi-segment plain prefix:
6704
+ * - `outer.polje[<id>] = <obj>` → seed.outer = {polje: [<obj>]}
6705
+ * - `outer.inner.polje[<id>].field = <v>` → seed.outer = {inner: {polje: [{_id, field: <v>}]}}
6659
6706
  *
6660
- * Supported shapes (path tokenizes to `[firstField, [<id>], …rest]`):
6707
+ * Existing matching `_id` on the target array: walk into the element
6708
+ * and create missing intermediates on the way down; set the leaf
6709
+ * without pushing a duplicate element.
6661
6710
  *
6662
- * 1. `polje[<id>] = <obj>` → seed.polje = [<obj>]
6663
- * 2. `polje[<id>].field = <v>` → seed.polje = [{_id: <id>, field: <v>}]
6664
- * 3. `polje[<id>].a.b = <v>` → seed.polje = [{_id: <id>, a: {b: <v>}}]
6665
- * 4. `polje[<id>].a.b.c.d = <v>` → seed.polje = [{_id: <id>, a: {b: {c: {d: <v>}}}}]
6666
- * 5. Existing element with `_id` matches `<id>` but missing intermediate
6667
- * walk down the element, create missing intermediate plain objects,
6668
- * set the leaf. No duplicate element pushed.
6711
+ * Dropped:
6712
+ * • Multi-bracket paths (e.g. `polje[a].sub[b]`, `outer[a].sub.inner[b]`)
6713
+ * require shape knowledge the fallback can't reconstruct. Server
6714
+ * applies the path; next sync hydrates the canonical state locally.
6715
+ * Existing intermediate that is a non-plain value (Date, primitive,
6716
+ * array where an object is expected).
6669
6717
  *
6670
- * Dropped (multi-bracket / nested-via-dots-before-bracket / non-plain
6671
- * intermediates) materializing those locally would risk corrupting
6672
- * unrelated invariants on the nested objects. Dirty-change still
6673
- * carries the path, so the server applies it; next sync brings the
6674
- * canonical state back to local.
6718
+ * `_`-prefixed first segment (e.g. `_redundanca…`): for shapes that
6719
+ * extend BEYOND the originally-supported single-segment prefix, the
6720
+ * drop is **silent** and unconditional these fields are server-
6721
+ * mirrored and local materialization could create state that diverges
6722
+ * from the canonical server form. Simple single-segment shapes
6723
+ * (`_field[<id>]`, `_field[<id>].sub`, `_field[<id>].sub.deep…`) still
6724
+ * materialize as before.
6675
6725
  *
6676
6726
  * Replaces the pre-fix blind `seed[path] = value` fallback that stamped
6677
6727
  * literal bracket-keyed top-level properties (e.g. `"postavke[<id>]": [<el>]`)
@@ -6697,28 +6747,59 @@ var _SyncedDb = class _SyncedDb {
6697
6747
  drop("first segment is not a plain field");
6698
6748
  return;
6699
6749
  }
6700
- const secondToken = tokens[1];
6701
- if (!secondToken.startsWith("[") || !secondToken.endsWith("]")) {
6702
- drop("second segment is not a bracket-by-id");
6750
+ let bracketIdx = -1;
6751
+ for (let i = 1; i < tokens.length; i++) {
6752
+ if (tokens[i].startsWith("[")) {
6753
+ bracketIdx = i;
6754
+ break;
6755
+ }
6756
+ }
6757
+ if (bracketIdx < 0) {
6758
+ drop("no bracket segment found");
6703
6759
  return;
6704
6760
  }
6705
- for (let i = 2; i < tokens.length; i++) {
6761
+ for (let i = bracketIdx + 1; i < tokens.length; i++) {
6706
6762
  if (tokens[i].startsWith("[")) {
6707
6763
  drop("nested bracket path");
6708
6764
  return;
6709
6765
  }
6710
6766
  }
6711
- const bracketId = secondToken.slice(1, -1);
6767
+ if (dropSilently && bracketIdx > 1) {
6768
+ return;
6769
+ }
6770
+ const prefixTokens = tokens.slice(0, bracketIdx);
6771
+ const bracketToken = tokens[bracketIdx];
6772
+ const suffixTokens = tokens.slice(bracketIdx + 1);
6773
+ const bracketId = bracketToken.slice(1, -1);
6712
6774
  if (bracketId.length === 0) {
6713
6775
  drop("empty bracket id");
6714
6776
  return;
6715
6777
  }
6716
- const existing = seed[firstToken];
6717
- if (existing != null && !Array.isArray(existing)) {
6718
- drop(`existing "${firstToken}" is not an array`);
6778
+ let parent = seed;
6779
+ for (let i = 0; i < prefixTokens.length - 1; i++) {
6780
+ const seg = prefixTokens[i];
6781
+ const cur = parent[seg];
6782
+ if (cur === void 0 || cur === null) {
6783
+ parent[seg] = {};
6784
+ } else if (typeof cur !== "object" || Array.isArray(cur) || cur instanceof Date) {
6785
+ drop(
6786
+ `existing intermediate "${prefixTokens.slice(0, i + 1).join(".")}" is not a plain object`
6787
+ );
6788
+ return;
6789
+ }
6790
+ parent = parent[seg];
6791
+ }
6792
+ const lastPrefixSeg = prefixTokens[prefixTokens.length - 1];
6793
+ let arr = parent[lastPrefixSeg];
6794
+ if (arr != null && !Array.isArray(arr)) {
6795
+ drop(`existing "${prefixTokens.join(".")}" is not an array`);
6719
6796
  return;
6720
6797
  }
6721
- if (tokens.length === 2) {
6798
+ if (arr == null) {
6799
+ arr = [];
6800
+ parent[lastPrefixSeg] = arr;
6801
+ }
6802
+ if (suffixTokens.length === 0) {
6722
6803
  let element = value;
6723
6804
  if (Array.isArray(value) && value.length === 1 && value[0] != null && typeof value[0] === "object") {
6724
6805
  element = value[0];
@@ -6730,14 +6811,16 @@ var _SyncedDb = class _SyncedDb {
6730
6811
  if (element._id == null) {
6731
6812
  element._id = bracketId;
6732
6813
  }
6733
- if (existing == null) {
6734
- seed[firstToken] = [element];
6814
+ const replaceIdx = arr.findIndex(
6815
+ (it) => it != null && typeof it === "object" && String(it._id) === bracketId
6816
+ );
6817
+ if (replaceIdx >= 0) {
6818
+ arr[replaceIdx] = element;
6735
6819
  } else {
6736
- existing.push(element);
6820
+ arr.push(element);
6737
6821
  }
6738
6822
  return;
6739
6823
  }
6740
- const subFieldPath = tokens.slice(2);
6741
6824
  const buildNestedSubTree = (segs, leaf) => {
6742
6825
  let acc = leaf;
6743
6826
  for (let i = segs.length - 1; i >= 0; i--) {
@@ -6745,28 +6828,23 @@ var _SyncedDb = class _SyncedDb {
6745
6828
  }
6746
6829
  return acc;
6747
6830
  };
6748
- if (existing == null) {
6749
- const subTree = buildNestedSubTree(subFieldPath, value);
6750
- seed[firstToken] = [__spreadValues({ _id: bracketId }, subTree)];
6751
- return;
6752
- }
6753
- const existingIdx = existing.findIndex(
6831
+ const existingIdx = arr.findIndex(
6754
6832
  (it) => it != null && typeof it === "object" && String(it._id) === bracketId
6755
6833
  );
6756
6834
  if (existingIdx >= 0) {
6757
- let cur = existing[existingIdx];
6758
- for (let i = 0; i < subFieldPath.length - 1; i++) {
6759
- const seg = subFieldPath[i];
6835
+ let cur = arr[existingIdx];
6836
+ for (let i = 0; i < suffixTokens.length - 1; i++) {
6837
+ const seg = suffixTokens[i];
6760
6838
  const next = cur[seg];
6761
6839
  if (next == null || typeof next !== "object" || Array.isArray(next)) {
6762
6840
  cur[seg] = {};
6763
6841
  }
6764
6842
  cur = cur[seg];
6765
6843
  }
6766
- cur[subFieldPath[subFieldPath.length - 1]] = value;
6844
+ cur[suffixTokens[suffixTokens.length - 1]] = value;
6767
6845
  } else {
6768
- const subTree = buildNestedSubTree(subFieldPath, value);
6769
- existing.push(__spreadValues({ _id: bracketId }, subTree));
6846
+ const subTree = buildNestedSubTree(suffixTokens, value);
6847
+ arr.push(__spreadValues({ _id: bracketId }, subTree));
6770
6848
  }
6771
6849
  }
6772
6850
  /**
@@ -6864,6 +6942,22 @@ function isMetaOnlyChanges(changes) {
6864
6942
  }
6865
6943
  return true;
6866
6944
  }
6945
+ function rebaseDirtyOnServerAdvance(entry, newChanges, baseMeta) {
6946
+ const newRev = baseMeta == null ? void 0 : baseMeta._rev;
6947
+ if (typeof newRev !== "number" || typeof entry.baseRev !== "number" || newRev <= entry.baseRev) {
6948
+ return;
6949
+ }
6950
+ const existingPaths = Object.keys(entry.changes);
6951
+ for (const newPath of Object.keys(newChanges)) {
6952
+ for (const existingPath of existingPaths) {
6953
+ if (pathsOverlap(newPath, existingPath)) {
6954
+ delete entry.changes[existingPath];
6955
+ }
6956
+ }
6957
+ }
6958
+ entry.baseTs = baseMeta._ts;
6959
+ entry.baseRev = newRev;
6960
+ }
6867
6961
  var DexieDb = class extends Dexie {
6868
6962
  constructor(tenant, collectionConfigs) {
6869
6963
  super(`synced-db-${tenant}`);
@@ -7033,6 +7127,7 @@ var DexieDb = class extends Dexie {
7033
7127
  const existing = await this.dirtyChanges.get([collection, stringId]);
7034
7128
  const now = Date.now();
7035
7129
  if (existing) {
7130
+ rebaseDirtyOnServerAdvance(existing, changes, baseMeta);
7036
7131
  mergeDirtyChanges(existing.changes, changes);
7037
7132
  existing.updatedAt = now;
7038
7133
  await this.dirtyChanges.put(existing);
@@ -7063,6 +7158,7 @@ var DexieDb = class extends Dexie {
7063
7158
  const stringId = this.idToString(changeItem.id);
7064
7159
  const existing = existingEntries[i];
7065
7160
  if (existing) {
7161
+ rebaseDirtyOnServerAdvance(existing, changeItem.changes, changeItem.baseMeta);
7066
7162
  mergeDirtyChanges(existing.changes, changeItem.changes);
7067
7163
  existing.updatedAt = now;
7068
7164
  toWrite.push(existing);
@@ -519,27 +519,53 @@ export declare class SyncedDb implements I_SyncedDb {
519
519
  * the input `base` reference.
520
520
  */
521
521
  private static applyDiffLocally;
522
+ /**
523
+ * Bracket-free dot-path fallback for `applyDiffLocally`. Walks the path,
524
+ * creating missing plain-object intermediates as needed, and sets the
525
+ * leaf. Refuses (returns `false`) if any segment is numeric (array
526
+ * index) or `[<id>]` (bracket selector) — those need shape knowledge
527
+ * outside the scope of plain-object materialization. Also refuses if
528
+ * an existing intermediate is non-plain (array, Date, primitive) —
529
+ * overwriting would risk silent data loss.
530
+ *
531
+ * Used after `setByPath` fails on a dot-only path (no `[` in `path`).
532
+ * Counterpart to `materializeBracketPath` for the bracket case.
533
+ */
534
+ private static materializePlainDotPath;
522
535
  /**
523
536
  * Fallback for `setByPath` failures inside `applyDiffLocally`. Materializes
524
- * the parent array when it's missing from `seed`, OR fills in missing
525
- * intermediate objects under an existing array element when the diff
526
- * path reaches deeper than the element currently holds.
527
- *
528
- * Supported shapes (path tokenizes to `[firstField, [<id>], …rest]`):
529
- *
530
- * 1. `polje[<id>] = <obj>` → seed.polje = [<obj>]
531
- * 2. `polje[<id>].field = <v>` → seed.polje = [{_id: <id>, field: <v>}]
532
- * 3. `polje[<id>].a.b = <v>` → seed.polje = [{_id: <id>, a: {b: <v>}}]
533
- * 4. `polje[<id>].a.b.c.d = <v>` → seed.polje = [{_id: <id>, a: {b: {c: {d: <v>}}}}]
534
- * 5. Existing element with `_id` matches `<id>` but missing intermediate
535
- * walk down the element, create missing intermediate plain objects,
536
- * set the leaf. No duplicate element pushed.
537
- *
538
- * Dropped (multi-bracket / nested-via-dots-before-bracket / non-plain
539
- * intermediates) materializing those locally would risk corrupting
540
- * unrelated invariants on the nested objects. Dirty-change still
541
- * carries the path, so the server applies it; next sync brings the
542
- * canonical state back to local.
537
+ * missing array containers AND missing intermediate plain objects so the
538
+ * diff entry can land locally instead of being dropped.
539
+ *
540
+ * Supported path shape (tokenizes to `[…plain prefix, [<id>], …plain suffix]`):
541
+ *
542
+ * • Single-segment prefix:
543
+ * - `polje[<id>] = <obj>` → seed.polje = [<obj>]
544
+ * - `polje[<id>].field = <v>` → seed.polje = [{_id, field: <v>}]
545
+ * - `polje[<id>].a.b.c = <v>` → seed.polje = [{_id, a: {b: {c: <v>}}}]
546
+ *
547
+ * Multi-segment plain prefix:
548
+ * - `outer.polje[<id>] = <obj>` → seed.outer = {polje: [<obj>]}
549
+ * - `outer.inner.polje[<id>].field = <v>` → seed.outer = {inner: {polje: [{_id, field: <v>}]}}
550
+ *
551
+ * Existing matching `_id` on the target array: walk into the element
552
+ * and create missing intermediates on the way down; set the leaf
553
+ * without pushing a duplicate element.
554
+ *
555
+ * Dropped:
556
+ * • Multi-bracket paths (e.g. `polje[a].sub[b]`, `outer[a].sub.inner[b]`)
557
+ * — require shape knowledge the fallback can't reconstruct. Server
558
+ * applies the path; next sync hydrates the canonical state locally.
559
+ * • Existing intermediate that is a non-plain value (Date, primitive,
560
+ * array where an object is expected).
561
+ *
562
+ * `_`-prefixed first segment (e.g. `_redundanca…`): for shapes that
563
+ * extend BEYOND the originally-supported single-segment prefix, the
564
+ * drop is **silent** and unconditional — these fields are server-
565
+ * mirrored and local materialization could create state that diverges
566
+ * from the canonical server form. Simple single-segment shapes
567
+ * (`_field[<id>]`, `_field[<id>].sub`, `_field[<id>].sub.deep…`) still
568
+ * materialize as before.
543
569
  *
544
570
  * Replaces the pre-fix blind `seed[path] = value` fallback that stamped
545
571
  * literal bracket-keyed top-level properties (e.g. `"postavke[<id>]": [<el>]`)
@@ -32,6 +32,14 @@ export declare function computeDiff(existing: Record<string, any> | null | undef
32
32
  * to legacy Object.assign accumulation.
33
33
  */
34
34
  export declare function hasDotNotationPaths(changes: Record<string, any>): boolean;
35
+ /**
36
+ * Symmetric overlap check used by dirty-merge rebase: paths overlap when
37
+ * they are equal OR one is an ancestor of the other. Returning `true`
38
+ * means a single mongo `$set` containing both would be rejected with a
39
+ * "Updating the path X would create a conflict at Y" — or at minimum the
40
+ * two writes target the same subtree.
41
+ */
42
+ export declare function pathsOverlap(a: string, b: string): boolean;
35
43
  /**
36
44
  * Tokenize a dot-notation path that may contain bracket-id segments.
37
45
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.183",
3
+ "version": "0.1.185",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",