cry-synced-db-client 0.1.182 → 0.1.183

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,48 @@
1
1
  # Versions
2
2
 
3
+ ## 0.1.183 (2026-05-14)
4
+
5
+ ### Fix: `applyDiffLocally` materializes deep sub-field paths under bracket-by-id
6
+
7
+ `materializeBracketPath` (the fallback when `setByPath` can't reach the
8
+ target) capped at `tokens.length ≤ 3`. Any deeper path was dropped with
9
+ a `dropping bracket-path diff entry (unsupported token count N)` log
10
+ (silent when first segment started with `_`, visible otherwise).
11
+
12
+ Real-world obisk records carry nested objects on every `zaracunaj`
13
+ element — `karence.karence.meso`, `uporabljeneSerijskeNaNapravi.serijske`,
14
+ etc. Saves like
15
+
16
+ ```typescript
17
+ syncedDb.save("obiski", id, {
18
+ "zaracunaj[<newId>].karence.karence.meso": 5,
19
+ });
20
+ ```
21
+
22
+ dropped the data even though the intent is unambiguous and the data
23
+ shape is valid.
24
+
25
+ **New behavior** in `materializeBracketPath`:
26
+
27
+ | Shape | Result |
28
+ |---|---|
29
+ | `polje[<id>] = <obj>` | `seed.polje = [<obj>]` (unchanged) |
30
+ | `polje[<id>].field = v` | `seed.polje = [{_id: <id>, field: v}]` (unchanged) |
31
+ | `polje[<id>].a.b.c = v` (new) | `seed.polje = [{_id: <id>, a: {b: {c: v}}}]` |
32
+ | Matching `_id` exists, intermediate missing (new) | Walk into the element, create missing intermediates on the way down, set the leaf. No duplicate element. |
33
+
34
+ Multi-bracket paths (e.g. `polje[a].sub[b]`, `polje[a].b.sub[c]`) still
35
+ drop — they require shape knowledge the fallback can't reconstruct;
36
+ server applies them and the next sync hydrates local.
37
+
38
+ Tests: `test/applyDiffLocallyObiskShape.test.ts` (18 cases) — uses a
39
+ trimmed real-shape obisk fixture with the two _id-arrays (top-level
40
+ `zaracunaj` and `_dobavnica_saved.postavke` nested under a plain
41
+ container), all the composition-change scenarios, the deep-bracket-path
42
+ edge cases that used to drop, and a "no drop-log during typical
43
+ roundtrip" guard. 3 cases were red before the fix; all pass after.
44
+ Full suite: 762 bun + 18 vitest pass.
45
+
3
46
  ## 0.1.182 (2026-05-14)
4
47
 
5
48
  ### `networkError` utility — quiet log noise during sustained offline
package/dist/index.js CHANGED
@@ -6653,19 +6653,25 @@ 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
+ * 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.
6659
6659
  *
6660
- * 1. `polje[<id>] = <obj>` → seed.polje = [<obj>]
6661
- * 2. `polje[<id>].<field> = <v>` → seed.polje = [{_id: <id>, <field>: <v>}]
6660
+ * Supported shapes (path tokenizes to `[firstField, [<id>], …rest]`):
6662
6661
  *
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.
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.
6669
+ *
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.
6669
6675
  *
6670
6676
  * Replaces the pre-fix blind `seed[path] = value` fallback that stamped
6671
6677
  * literal bracket-keyed top-level properties (e.g. `"postavke[<id>]": [<el>]`)
@@ -6683,7 +6689,7 @@ var _SyncedDb = class _SyncedDb {
6683
6689
  { collection, _id: String(id), path, value }
6684
6690
  );
6685
6691
  };
6686
- if (tokens.length < 2 || tokens.length > 3) {
6692
+ if (tokens.length < 2) {
6687
6693
  drop(`unsupported token count ${tokens.length}`);
6688
6694
  return;
6689
6695
  }
@@ -6696,9 +6702,11 @@ var _SyncedDb = class _SyncedDb {
6696
6702
  drop("second segment is not a bracket-by-id");
6697
6703
  return;
6698
6704
  }
6699
- if (tokens.length === 3 && tokens[2].startsWith("[")) {
6700
- drop("nested bracket path");
6701
- return;
6705
+ for (let i = 2; i < tokens.length; i++) {
6706
+ if (tokens[i].startsWith("[")) {
6707
+ drop("nested bracket path");
6708
+ return;
6709
+ }
6702
6710
  }
6703
6711
  const bracketId = secondToken.slice(1, -1);
6704
6712
  if (bracketId.length === 0) {
@@ -6729,12 +6737,36 @@ var _SyncedDb = class _SyncedDb {
6729
6737
  }
6730
6738
  return;
6731
6739
  }
6732
- const fieldName = tokens[2];
6733
- const newElement = { _id: bracketId, [fieldName]: value };
6740
+ const subFieldPath = tokens.slice(2);
6741
+ const buildNestedSubTree = (segs, leaf) => {
6742
+ let acc = leaf;
6743
+ for (let i = segs.length - 1; i >= 0; i--) {
6744
+ acc = { [segs[i]]: acc };
6745
+ }
6746
+ return acc;
6747
+ };
6734
6748
  if (existing == null) {
6735
- seed[firstToken] = [newElement];
6749
+ const subTree = buildNestedSubTree(subFieldPath, value);
6750
+ seed[firstToken] = [__spreadValues({ _id: bracketId }, subTree)];
6751
+ return;
6752
+ }
6753
+ const existingIdx = existing.findIndex(
6754
+ (it) => it != null && typeof it === "object" && String(it._id) === bracketId
6755
+ );
6756
+ if (existingIdx >= 0) {
6757
+ let cur = existing[existingIdx];
6758
+ for (let i = 0; i < subFieldPath.length - 1; i++) {
6759
+ const seg = subFieldPath[i];
6760
+ const next = cur[seg];
6761
+ if (next == null || typeof next !== "object" || Array.isArray(next)) {
6762
+ cur[seg] = {};
6763
+ }
6764
+ cur = cur[seg];
6765
+ }
6766
+ cur[subFieldPath[subFieldPath.length - 1]] = value;
6736
6767
  } else {
6737
- existing.push(newElement);
6768
+ const subTree = buildNestedSubTree(subFieldPath, value);
6769
+ existing.push(__spreadValues({ _id: bracketId }, subTree));
6738
6770
  }
6739
6771
  }
6740
6772
  /**
@@ -521,19 +521,25 @@ 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
+ * 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
543
  *
538
544
  * Replaces the pre-fix blind `seed[path] = value` fallback that stamped
539
545
  * 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.183",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",