cry-synced-db-client 0.1.183 → 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,52 @@
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
+
3
50
  ## 0.1.183 (2026-05-14)
4
51
 
5
52
  ### Fix: `applyDiffLocally` materializes deep sub-field paths under bracket-by-id
package/dist/index.js CHANGED
@@ -6653,25 +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`, OR fills in missing
6657
- * intermediate objects under an existing array element when the diff
6658
- * path reaches deeper than the element currently holds.
6656
+ * missing array containers AND missing intermediate plain objects so the
6657
+ * diff entry can land locally instead of being dropped.
6659
6658
  *
6660
- * Supported shapes (path tokenizes to `[firstField, [<id>], …rest]`):
6659
+ * Supported path shape (tokenizes to `[…plain prefix, [<id>], …plain suffix]`):
6661
6660
  *
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.
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>}}}]
6669
6665
  *
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.
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.
6675
6688
  *
6676
6689
  * Replaces the pre-fix blind `seed[path] = value` fallback that stamped
6677
6690
  * literal bracket-keyed top-level properties (e.g. `"postavke[<id>]": [<el>]`)
@@ -6697,28 +6710,59 @@ var _SyncedDb = class _SyncedDb {
6697
6710
  drop("first segment is not a plain field");
6698
6711
  return;
6699
6712
  }
6700
- const secondToken = tokens[1];
6701
- if (!secondToken.startsWith("[") || !secondToken.endsWith("]")) {
6702
- 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");
6703
6722
  return;
6704
6723
  }
6705
- for (let i = 2; i < tokens.length; i++) {
6724
+ for (let i = bracketIdx + 1; i < tokens.length; i++) {
6706
6725
  if (tokens[i].startsWith("[")) {
6707
6726
  drop("nested bracket path");
6708
6727
  return;
6709
6728
  }
6710
6729
  }
6711
- const bracketId = secondToken.slice(1, -1);
6730
+ if (dropSilently && bracketIdx > 1) {
6731
+ return;
6732
+ }
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);
6712
6737
  if (bracketId.length === 0) {
6713
6738
  drop("empty bracket id");
6714
6739
  return;
6715
6740
  }
6716
- const existing = seed[firstToken];
6717
- if (existing != null && !Array.isArray(existing)) {
6718
- 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`);
6719
6759
  return;
6720
6760
  }
6721
- if (tokens.length === 2) {
6761
+ if (arr == null) {
6762
+ arr = [];
6763
+ parent[lastPrefixSeg] = arr;
6764
+ }
6765
+ if (suffixTokens.length === 0) {
6722
6766
  let element = value;
6723
6767
  if (Array.isArray(value) && value.length === 1 && value[0] != null && typeof value[0] === "object") {
6724
6768
  element = value[0];
@@ -6730,14 +6774,16 @@ var _SyncedDb = class _SyncedDb {
6730
6774
  if (element._id == null) {
6731
6775
  element._id = bracketId;
6732
6776
  }
6733
- if (existing == null) {
6734
- 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;
6735
6782
  } else {
6736
- existing.push(element);
6783
+ arr.push(element);
6737
6784
  }
6738
6785
  return;
6739
6786
  }
6740
- const subFieldPath = tokens.slice(2);
6741
6787
  const buildNestedSubTree = (segs, leaf) => {
6742
6788
  let acc = leaf;
6743
6789
  for (let i = segs.length - 1; i >= 0; i--) {
@@ -6745,28 +6791,23 @@ var _SyncedDb = class _SyncedDb {
6745
6791
  }
6746
6792
  return acc;
6747
6793
  };
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(
6794
+ const existingIdx = arr.findIndex(
6754
6795
  (it) => it != null && typeof it === "object" && String(it._id) === bracketId
6755
6796
  );
6756
6797
  if (existingIdx >= 0) {
6757
- let cur = existing[existingIdx];
6758
- for (let i = 0; i < subFieldPath.length - 1; i++) {
6759
- const seg = subFieldPath[i];
6798
+ let cur = arr[existingIdx];
6799
+ for (let i = 0; i < suffixTokens.length - 1; i++) {
6800
+ const seg = suffixTokens[i];
6760
6801
  const next = cur[seg];
6761
6802
  if (next == null || typeof next !== "object" || Array.isArray(next)) {
6762
6803
  cur[seg] = {};
6763
6804
  }
6764
6805
  cur = cur[seg];
6765
6806
  }
6766
- cur[subFieldPath[subFieldPath.length - 1]] = value;
6807
+ cur[suffixTokens[suffixTokens.length - 1]] = value;
6767
6808
  } else {
6768
- const subTree = buildNestedSubTree(subFieldPath, value);
6769
- existing.push(__spreadValues({ _id: bracketId }, subTree));
6809
+ const subTree = buildNestedSubTree(suffixTokens, value);
6810
+ arr.push(__spreadValues({ _id: bracketId }, subTree));
6770
6811
  }
6771
6812
  }
6772
6813
  /**
@@ -521,25 +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`, 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.
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.
543
556
  *
544
557
  * Replaces the pre-fix blind `seed[path] = value` fallback that stamped
545
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.183",
3
+ "version": "0.1.184",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",