cry-synced-db-client 0.1.182 → 0.1.184

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,95 @@
1
1
  # Versions
2
2
 
3
+ ## 0.1.184 (2026-05-14)
4
+
5
+ ### Fix: `applyDiffLocally` supports multi-segment plain prefix before bracket-by-id
6
+
7
+ The materialization fallback used to require the bracket-by-id to live
8
+ at token position [1] — anything deeper failed the
9
+ `secondToken.startsWith("[")` check and dropped with
10
+ `second segment is not a bracket-by-id`. Diff paths like
11
+ `outer.field[<id>]` or `a.b.c.arr[<id>].deep` fell through this gap;
12
+ the data was silently lost locally (visible drop unless the first
13
+ segment started with `_`).
14
+
15
+ Now any plain dot-notation prefix before a single bracket-by-id is
16
+ materialized:
17
+
18
+ ```
19
+ plain1.plain2…plainN[<id>][.subA.subB…] → materialize
20
+ ```
21
+
22
+ | Shape | Result |
23
+ |---|---|
24
+ | `outer.arr[<newId>] = <obj>` | `seed.outer = {arr: [<obj>]}` |
25
+ | `outer.arr[<newId>].field = v` | `seed.outer = {arr: [{_id: <newId>, field: v}]}` |
26
+ | `a.b.c.arr[<id>].seg1.seg2 = v` | `seed.a = {b: {c: {arr: [{_id, seg1: {seg2: v}}]}}}` |
27
+ | Matching `_id` exists, intermediate missing | Walk into element, create missing intermediates, set leaf. No duplicate element. |
28
+ | Existing intermediate is not a plain object (Date, primitive, array) | Drop visibly — refuse to overwrite. |
29
+
30
+ **Still drops**: multi-bracket paths (`outer[a].sub[b]`,
31
+ `outer.a.sub[i].nested[j]`) — these need shape knowledge the fallback
32
+ can't reconstruct; server applies the path and next sync hydrates the
33
+ canonical state.
34
+
35
+ **Backward-compat for `_`-mirrored roots**: paths under `_redundanca`,
36
+ `_dobavnica_saved`, etc. that extend beyond the originally-supported
37
+ single-segment-prefix shape (e.g. `_redundanca.terapije[<id>]`) still
38
+ drop silently. These fields are server-mirrored — local materialization
39
+ with partial data could diverge from the canonical state.
40
+
41
+ Tests:
42
+ - `test/applyDiffLocallyObiskShape.test.ts` — 5 new cases covering
43
+ multi-segment prefix shapes plus the `_`-prefix silent-drop contract.
44
+ - `test/applyDiffLocallyMaterialize.test.ts` — flipped the
45
+ `a.b[<id>]` case from "drops with diagnostic log" to
46
+ "materializes `{a: {b: [<el>]}}`".
47
+
48
+ Full suite: 767 bun + 18 vitest pass.
49
+
50
+ ## 0.1.183 (2026-05-14)
51
+
52
+ ### Fix: `applyDiffLocally` materializes deep sub-field paths under bracket-by-id
53
+
54
+ `materializeBracketPath` (the fallback when `setByPath` can't reach the
55
+ target) capped at `tokens.length ≤ 3`. Any deeper path was dropped with
56
+ a `dropping bracket-path diff entry (unsupported token count N)` log
57
+ (silent when first segment started with `_`, visible otherwise).
58
+
59
+ Real-world obisk records carry nested objects on every `zaracunaj`
60
+ element — `karence.karence.meso`, `uporabljeneSerijskeNaNapravi.serijske`,
61
+ etc. Saves like
62
+
63
+ ```typescript
64
+ syncedDb.save("obiski", id, {
65
+ "zaracunaj[<newId>].karence.karence.meso": 5,
66
+ });
67
+ ```
68
+
69
+ dropped the data even though the intent is unambiguous and the data
70
+ shape is valid.
71
+
72
+ **New behavior** in `materializeBracketPath`:
73
+
74
+ | Shape | Result |
75
+ |---|---|
76
+ | `polje[<id>] = <obj>` | `seed.polje = [<obj>]` (unchanged) |
77
+ | `polje[<id>].field = v` | `seed.polje = [{_id: <id>, field: v}]` (unchanged) |
78
+ | `polje[<id>].a.b.c = v` (new) | `seed.polje = [{_id: <id>, a: {b: {c: v}}}]` |
79
+ | Matching `_id` exists, intermediate missing (new) | Walk into the element, create missing intermediates on the way down, set the leaf. No duplicate element. |
80
+
81
+ Multi-bracket paths (e.g. `polje[a].sub[b]`, `polje[a].b.sub[c]`) still
82
+ drop — they require shape knowledge the fallback can't reconstruct;
83
+ server applies them and the next sync hydrates local.
84
+
85
+ Tests: `test/applyDiffLocallyObiskShape.test.ts` (18 cases) — uses a
86
+ trimmed real-shape obisk fixture with the two _id-arrays (top-level
87
+ `zaracunaj` and `_dobavnica_saved.postavke` nested under a plain
88
+ container), all the composition-change scenarios, the deep-bracket-path
89
+ edge cases that used to drop, and a "no drop-log during typical
90
+ roundtrip" guard. 3 cases were red before the fix; all pass after.
91
+ Full suite: 762 bun + 18 vitest pass.
92
+
3
93
  ## 0.1.182 (2026-05-14)
4
94
 
5
95
  ### `networkError` utility — quiet log noise during sustained offline
package/dist/index.js CHANGED
@@ -6653,19 +6653,38 @@ var _SyncedDb = class _SyncedDb {
6653
6653
  }
6654
6654
  /**
6655
6655
  * Fallback for `setByPath` failures inside `applyDiffLocally`. Materializes
6656
- * the parent array when it's missing from `seed`. Two patterns covered, per
6657
- * production-spec (mozirje 2026-05-10 literal bracket-keyed sibling
6658
- * properties polluting Dexie/in-mem):
6656
+ * missing array containers AND missing intermediate plain objects so the
6657
+ * diff entry can land locally instead of being dropped.
6659
6658
  *
6660
- * 1. `polje[<id>] = <obj>` → seed.polje = [<obj>]
6661
- * 2. `polje[<id>].<field> = <v>` → seed.polje = [{_id: <id>, <field>: <v>}]
6659
+ * Supported path shape (tokenizes to `[…plain prefix, [<id>], …plain suffix]`):
6662
6660
  *
6663
- * Everything else (nested-via-dots before bracket, multi-bracket paths
6664
- * like `_redundanca.terapije[<id>].postavke[<id2>]`, deeper sub-fields)
6665
- * is dropped silently materializing those locally would risk corrupting
6666
- * unrelated invariants on the nested objects. Dirty-change still carries
6667
- * the path, so server applies it; next sync brings the canonical state
6668
- * back to local.
6661
+ * Single-segment prefix:
6662
+ * - `polje[<id>] = <obj>` → seed.polje = [<obj>]
6663
+ * - `polje[<id>].field = <v>` → seed.polje = [{_id, field: <v>}]
6664
+ * - `polje[<id>].a.b.c = <v>` → seed.polje = [{_id, a: {b: {c: <v>}}}]
6665
+ *
6666
+ * Multi-segment plain prefix:
6667
+ * - `outer.polje[<id>] = <obj>` → seed.outer = {polje: [<obj>]}
6668
+ * - `outer.inner.polje[<id>].field = <v>` → seed.outer = {inner: {polje: [{_id, field: <v>}]}}
6669
+ *
6670
+ * • Existing matching `_id` on the target array: walk into the element
6671
+ * and create missing intermediates on the way down; set the leaf
6672
+ * without pushing a duplicate element.
6673
+ *
6674
+ * Dropped:
6675
+ * • Multi-bracket paths (e.g. `polje[a].sub[b]`, `outer[a].sub.inner[b]`)
6676
+ * — require shape knowledge the fallback can't reconstruct. Server
6677
+ * applies the path; next sync hydrates the canonical state locally.
6678
+ * • Existing intermediate that is a non-plain value (Date, primitive,
6679
+ * array where an object is expected).
6680
+ *
6681
+ * `_`-prefixed first segment (e.g. `_redundanca…`): for shapes that
6682
+ * extend BEYOND the originally-supported single-segment prefix, the
6683
+ * drop is **silent** and unconditional — these fields are server-
6684
+ * mirrored and local materialization could create state that diverges
6685
+ * from the canonical server form. Simple single-segment shapes
6686
+ * (`_field[<id>]`, `_field[<id>].sub`, `_field[<id>].sub.deep…`) still
6687
+ * materialize as before.
6669
6688
  *
6670
6689
  * Replaces the pre-fix blind `seed[path] = value` fallback that stamped
6671
6690
  * literal bracket-keyed top-level properties (e.g. `"postavke[<id>]": [<el>]`)
@@ -6683,7 +6702,7 @@ var _SyncedDb = class _SyncedDb {
6683
6702
  { collection, _id: String(id), path, value }
6684
6703
  );
6685
6704
  };
6686
- if (tokens.length < 2 || tokens.length > 3) {
6705
+ if (tokens.length < 2) {
6687
6706
  drop(`unsupported token count ${tokens.length}`);
6688
6707
  return;
6689
6708
  }
@@ -6691,26 +6710,59 @@ var _SyncedDb = class _SyncedDb {
6691
6710
  drop("first segment is not a plain field");
6692
6711
  return;
6693
6712
  }
6694
- const secondToken = tokens[1];
6695
- if (!secondToken.startsWith("[") || !secondToken.endsWith("]")) {
6696
- drop("second segment is not a bracket-by-id");
6713
+ let bracketIdx = -1;
6714
+ for (let i = 1; i < tokens.length; i++) {
6715
+ if (tokens[i].startsWith("[")) {
6716
+ bracketIdx = i;
6717
+ break;
6718
+ }
6719
+ }
6720
+ if (bracketIdx < 0) {
6721
+ drop("no bracket segment found");
6697
6722
  return;
6698
6723
  }
6699
- if (tokens.length === 3 && tokens[2].startsWith("[")) {
6700
- drop("nested bracket path");
6724
+ for (let i = bracketIdx + 1; i < tokens.length; i++) {
6725
+ if (tokens[i].startsWith("[")) {
6726
+ drop("nested bracket path");
6727
+ return;
6728
+ }
6729
+ }
6730
+ if (dropSilently && bracketIdx > 1) {
6701
6731
  return;
6702
6732
  }
6703
- const bracketId = secondToken.slice(1, -1);
6733
+ const prefixTokens = tokens.slice(0, bracketIdx);
6734
+ const bracketToken = tokens[bracketIdx];
6735
+ const suffixTokens = tokens.slice(bracketIdx + 1);
6736
+ const bracketId = bracketToken.slice(1, -1);
6704
6737
  if (bracketId.length === 0) {
6705
6738
  drop("empty bracket id");
6706
6739
  return;
6707
6740
  }
6708
- const existing = seed[firstToken];
6709
- if (existing != null && !Array.isArray(existing)) {
6710
- drop(`existing "${firstToken}" is not an array`);
6741
+ let parent = seed;
6742
+ for (let i = 0; i < prefixTokens.length - 1; i++) {
6743
+ const seg = prefixTokens[i];
6744
+ const cur = parent[seg];
6745
+ if (cur === void 0 || cur === null) {
6746
+ parent[seg] = {};
6747
+ } else if (typeof cur !== "object" || Array.isArray(cur) || cur instanceof Date) {
6748
+ drop(
6749
+ `existing intermediate "${prefixTokens.slice(0, i + 1).join(".")}" is not a plain object`
6750
+ );
6751
+ return;
6752
+ }
6753
+ parent = parent[seg];
6754
+ }
6755
+ const lastPrefixSeg = prefixTokens[prefixTokens.length - 1];
6756
+ let arr = parent[lastPrefixSeg];
6757
+ if (arr != null && !Array.isArray(arr)) {
6758
+ drop(`existing "${prefixTokens.join(".")}" is not an array`);
6711
6759
  return;
6712
6760
  }
6713
- if (tokens.length === 2) {
6761
+ if (arr == null) {
6762
+ arr = [];
6763
+ parent[lastPrefixSeg] = arr;
6764
+ }
6765
+ if (suffixTokens.length === 0) {
6714
6766
  let element = value;
6715
6767
  if (Array.isArray(value) && value.length === 1 && value[0] != null && typeof value[0] === "object") {
6716
6768
  element = value[0];
@@ -6722,19 +6774,40 @@ var _SyncedDb = class _SyncedDb {
6722
6774
  if (element._id == null) {
6723
6775
  element._id = bracketId;
6724
6776
  }
6725
- if (existing == null) {
6726
- seed[firstToken] = [element];
6777
+ const replaceIdx = arr.findIndex(
6778
+ (it) => it != null && typeof it === "object" && String(it._id) === bracketId
6779
+ );
6780
+ if (replaceIdx >= 0) {
6781
+ arr[replaceIdx] = element;
6727
6782
  } else {
6728
- existing.push(element);
6783
+ arr.push(element);
6729
6784
  }
6730
6785
  return;
6731
6786
  }
6732
- const fieldName = tokens[2];
6733
- const newElement = { _id: bracketId, [fieldName]: value };
6734
- if (existing == null) {
6735
- seed[firstToken] = [newElement];
6787
+ const buildNestedSubTree = (segs, leaf) => {
6788
+ let acc = leaf;
6789
+ for (let i = segs.length - 1; i >= 0; i--) {
6790
+ acc = { [segs[i]]: acc };
6791
+ }
6792
+ return acc;
6793
+ };
6794
+ const existingIdx = arr.findIndex(
6795
+ (it) => it != null && typeof it === "object" && String(it._id) === bracketId
6796
+ );
6797
+ if (existingIdx >= 0) {
6798
+ let cur = arr[existingIdx];
6799
+ for (let i = 0; i < suffixTokens.length - 1; i++) {
6800
+ const seg = suffixTokens[i];
6801
+ const next = cur[seg];
6802
+ if (next == null || typeof next !== "object" || Array.isArray(next)) {
6803
+ cur[seg] = {};
6804
+ }
6805
+ cur = cur[seg];
6806
+ }
6807
+ cur[suffixTokens[suffixTokens.length - 1]] = value;
6736
6808
  } else {
6737
- existing.push(newElement);
6809
+ const subTree = buildNestedSubTree(suffixTokens, value);
6810
+ arr.push(__spreadValues({ _id: bracketId }, subTree));
6738
6811
  }
6739
6812
  }
6740
6813
  /**
@@ -521,19 +521,38 @@ export declare class SyncedDb implements I_SyncedDb {
521
521
  private static applyDiffLocally;
522
522
  /**
523
523
  * Fallback for `setByPath` failures inside `applyDiffLocally`. Materializes
524
- * the parent array when it's missing from `seed`. Two patterns covered, per
525
- * production-spec (mozirje 2026-05-10 literal bracket-keyed sibling
526
- * properties polluting Dexie/in-mem):
527
- *
528
- * 1. `polje[<id>] = <obj>` → seed.polje = [<obj>]
529
- * 2. `polje[<id>].<field> = <v>` → seed.polje = [{_id: <id>, <field>: <v>}]
530
- *
531
- * Everything else (nested-via-dots before bracket, multi-bracket paths
532
- * like `_redundanca.terapije[<id>].postavke[<id2>]`, deeper sub-fields)
533
- * is dropped silently — materializing those locally would risk corrupting
534
- * unrelated invariants on the nested objects. Dirty-change still carries
535
- * the path, so server applies it; next sync brings the canonical state
536
- * back to local.
524
+ * missing array containers AND missing intermediate plain objects so the
525
+ * diff entry can land locally instead of being dropped.
526
+ *
527
+ * Supported path shape (tokenizes to `[…plain prefix, [<id>], …plain suffix]`):
528
+ *
529
+ * Single-segment prefix:
530
+ * - `polje[<id>] = <obj>` → seed.polje = [<obj>]
531
+ * - `polje[<id>].field = <v>` → seed.polje = [{_id, field: <v>}]
532
+ * - `polje[<id>].a.b.c = <v>` → seed.polje = [{_id, a: {b: {c: <v>}}}]
533
+ *
534
+ * Multi-segment plain prefix:
535
+ * - `outer.polje[<id>] = <obj>` → seed.outer = {polje: [<obj>]}
536
+ * - `outer.inner.polje[<id>].field = <v>` → seed.outer = {inner: {polje: [{_id, field: <v>}]}}
537
+ *
538
+ * • Existing matching `_id` on the target array: walk into the element
539
+ * and create missing intermediates on the way down; set the leaf
540
+ * without pushing a duplicate element.
541
+ *
542
+ * Dropped:
543
+ * • Multi-bracket paths (e.g. `polje[a].sub[b]`, `outer[a].sub.inner[b]`)
544
+ * — require shape knowledge the fallback can't reconstruct. Server
545
+ * applies the path; next sync hydrates the canonical state locally.
546
+ * • Existing intermediate that is a non-plain value (Date, primitive,
547
+ * array where an object is expected).
548
+ *
549
+ * `_`-prefixed first segment (e.g. `_redundanca…`): for shapes that
550
+ * extend BEYOND the originally-supported single-segment prefix, the
551
+ * drop is **silent** and unconditional — these fields are server-
552
+ * mirrored and local materialization could create state that diverges
553
+ * from the canonical server form. Simple single-segment shapes
554
+ * (`_field[<id>]`, `_field[<id>].sub`, `_field[<id>].sub.deep…`) still
555
+ * materialize as before.
537
556
  *
538
557
  * Replaces the pre-fix blind `seed[path] = value` fallback that stamped
539
558
  * literal bracket-keyed top-level properties (e.g. `"postavke[<id>]": [<el>]`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.182",
3
+ "version": "0.1.184",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",