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 +118 -0
- package/dist/index.js +139 -43
- package/dist/src/db/SyncedDb.d.ts +45 -19
- package/dist/src/utils/computeDiff.d.ts +8 -0
- package/package.json +1 -1
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 (
|
|
6649
|
-
|
|
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
|
-
*
|
|
6657
|
-
*
|
|
6658
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
6663
|
-
*
|
|
6664
|
-
*
|
|
6665
|
-
*
|
|
6666
|
-
*
|
|
6667
|
-
*
|
|
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
|
-
*
|
|
6671
|
-
*
|
|
6672
|
-
*
|
|
6673
|
-
*
|
|
6674
|
-
* canonical
|
|
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
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
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 (
|
|
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
|
-
|
|
6734
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
6758
|
-
for (let i = 0; i <
|
|
6759
|
-
const seg =
|
|
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[
|
|
6844
|
+
cur[suffixTokens[suffixTokens.length - 1]] = value;
|
|
6767
6845
|
} else {
|
|
6768
|
-
const subTree = buildNestedSubTree(
|
|
6769
|
-
|
|
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
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
* intermediates
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
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
|
*
|