cry-synced-db-client 2.0.0 → 2.0.3

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,42 @@
1
1
  # Versions
2
2
 
3
+ ## 2.0.2 (2026-07-13)
4
+
5
+ ### Fix: `PendingChangesManager.schedule` — deleted fields resurrected from prior pending save
6
+
7
+ When two rapid `save()` calls arrived before the first Dexie debounce
8
+ flushed, `PendingChangesManager.schedule` merged them via
9
+ `Object.assign(existing.data, data)`. The second call's `data` was the
10
+ result of `applyObjDiff` — which **physically removes** keys set to
11
+ `undefined` (delete-by-path). `Object.assign` doesn't delete keys
12
+ absent from the source, so the first save's value silently survived in
13
+ the pending payload, resurrecting fields the caller had explicitly
14
+ cleared.
15
+
16
+ **Real-world trigger (klikvet):** `mounted()` calls
17
+ `save({ sedajObdeluje: "primoz" })` → `beforeRouteLeave` calls
18
+ `save({ sedajObdeluje: undefined })`. Both land in `PendingChanges`
19
+ before Dexie debounce fires. The merged row kept `sedajObdeluje: "primoz"`
20
+ in Dexie indefinitely — `razveljaviSamoVpogledSpremembe` never saw the
21
+ field cleared in the `objDiff` between `racunObVstopu` and
22
+ `this.racun`.
23
+
24
+ **Fix in `PendingChangesManager.schedule`:** replace shallow
25
+ `Object.assign(existing.data, data)` with key-aware merge — copy all
26
+ keys from `existing.data`, overwrite with keys in `data`, and
27
+ **delete** any key that exists in the existing row but is absent from
28
+ `data` (excluding `_id`, `_rev`, `_ts`).
29
+
30
+ **Fix in `SyncedDb.save()`:** diff against **in-mem** state (when
31
+ available) instead of stale Dexie. If PendingChanges holds writes that
32
+ haven't reached Dexie, deletions made against the live in-mem value
33
+ are invisible when diffing against stale Dexie (existing field is
34
+ missing from Dexie → no diff), and the field silently resurrects
35
+ from the in-mem base during `applyObjDiff`.
36
+
37
+ **New test:** `test/node-only/pendingChangesDeleteField.test.ts` —
38
+ 2 cases covering the two-rapid-save-delete scenario.
39
+
3
40
  ## 2.0.0 (2026-07-10)
4
41
 
5
42
  ### Dual-channel ebus support: `db/` + `dbbracketed/`
package/dist/index.js CHANGED
@@ -2723,7 +2723,17 @@ var PendingChangesManager = class {
2723
2723
  }
2724
2724
  const deltaWithId = existing ? data : __spreadProps(__spreadValues({}, data), { _id: id });
2725
2725
  savePendingWrite(this.tenant, collection, id, deltaWithId);
2726
- const fullData = existing ? Object.assign(existing.data, data, { _id: id }) : __spreadProps(__spreadValues({}, data), { _id: id });
2726
+ let fullData;
2727
+ if (existing) {
2728
+ fullData = __spreadProps(__spreadValues(__spreadValues({}, existing.data), data), { _id: id });
2729
+ for (const key2 of Object.keys(existing.data)) {
2730
+ if (key2 !== "_id" && !(key2 in data)) {
2731
+ delete fullData[key2];
2732
+ }
2733
+ }
2734
+ } else {
2735
+ fullData = __spreadProps(__spreadValues({}, data), { _id: id });
2736
+ }
2727
2737
  const timer = setTimeout(() => {
2728
2738
  this.executePendingChange(key);
2729
2739
  }, this.debounceDexieWritesMs);
@@ -4512,7 +4522,7 @@ var ServerUpdateHandler = class {
4512
4522
  return updatedIds;
4513
4523
  }
4514
4524
  async processBracketedItem(collection, item) {
4515
- var _a, _b, _c, _d, _e, _f;
4525
+ var _a, _b, _c, _d, _e, _f, _g, _h;
4516
4526
  const localItem = await this.dexieDb.getById(collection, item._id);
4517
4527
  if (!localItem) {
4518
4528
  if (item.operation === "insert" && item.bracketedData) {
@@ -4529,8 +4539,7 @@ var ServerUpdateHandler = class {
4529
4539
  const revAction = this.checkRev(
4530
4540
  item._rev,
4531
4541
  item._ts,
4532
- void 0,
4533
- // bracketedData doesn't carry _lastUpdaterId
4542
+ (_b = item._lastUpdaterId) != null ? _b : (_a = item.bracketedData) == null ? void 0 : _a._lastUpdaterId,
4534
4543
  localItem
4535
4544
  );
4536
4545
  switch (revAction) {
@@ -4559,8 +4568,8 @@ var ServerUpdateHandler = class {
4559
4568
  this.callOnEbusUpdateSkipped({
4560
4569
  collection,
4561
4570
  _id: item._id,
4562
- incomingRev: (_a = item._rev) != null ? _a : 0,
4563
- localRev: (_b = localItem._rev) != null ? _b : 0,
4571
+ incomingRev: (_c = item._rev) != null ? _c : 0,
4572
+ localRev: (_d = localItem._rev) != null ? _d : 0,
4564
4573
  reason: "fetch-failed"
4565
4574
  });
4566
4575
  return false;
@@ -4573,8 +4582,8 @@ var ServerUpdateHandler = class {
4573
4582
  const diff = this.stripMetaFromDiff(item.bracketedData);
4574
4583
  const currentInMemState = Object.assign({}, localItem, pendingChange.data);
4575
4584
  const merged = applyObjDiff(currentInMemState, diff, item._id, "bracketed");
4576
- merged._rev = (_c = item._rev) != null ? _c : localItem._rev;
4577
- merged._ts = (_d = item._ts) != null ? _d : localItem._ts;
4585
+ merged._rev = (_e = item._rev) != null ? _e : localItem._rev;
4586
+ merged._ts = (_f = item._ts) != null ? _f : localItem._ts;
4578
4587
  if (!merged._deleted && !merged._archived) {
4579
4588
  this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
4580
4589
  source: "incremental"
@@ -4586,8 +4595,8 @@ var ServerUpdateHandler = class {
4586
4595
  if (dirtyChange && item.operation === "update") {
4587
4596
  const diff = this.stripMetaFromDiff(item.bracketedData);
4588
4597
  const merged = applyObjDiff(localItem, diff, item._id, "bracketed");
4589
- merged._rev = (_e = item._rev) != null ? _e : localItem._rev;
4590
- merged._ts = (_f = item._ts) != null ? _f : localItem._ts;
4598
+ merged._rev = (_g = item._rev) != null ? _g : localItem._rev;
4599
+ merged._ts = (_h = item._ts) != null ? _h : localItem._ts;
4591
4600
  if (!merged._deleted && !merged._archived) {
4592
4601
  await this.dexieDb.insert(collection, merged);
4593
4602
  this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
@@ -6516,26 +6525,22 @@ var _SyncedDb = class _SyncedDb {
6516
6525
  delete update._id;
6517
6526
  }
6518
6527
  update = _SyncedDb.stringifyObjectIds(update);
6528
+ const isWriteOnly = (_b = (_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly) != null ? _b : false;
6519
6529
  const existing = await this.dexieDb.getById(collection, id);
6520
- if (!existing && !((_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly)) {
6530
+ if (!existing && !isWriteOnly) {
6521
6531
  console.warn(
6522
6532
  `[SyncedDb] SyncedDb.save: Object ${String(id)} not found in ${collection}, creating new`
6523
6533
  );
6524
6534
  }
6525
6535
  const fullChanges = __spreadProps(__spreadValues({}, update), { _lastUpdaterId: this.updaterId });
6526
- const diff = computeObjDiff(existing, fullChanges);
6536
+ const currentMem = isWriteOnly ? null : this.inMemDb.getById(collection, id);
6537
+ const base = currentMem != null ? currentMem : existing;
6538
+ const diff = computeObjDiff(base, fullChanges);
6527
6539
  await this.dexieDb.addDirtyChange(collection, id, diff, {
6528
6540
  _ts: existing == null ? void 0 : existing._ts,
6529
6541
  _rev: existing == null ? void 0 : existing._rev
6530
6542
  });
6531
- const isWriteOnly = (_b = this.collections.get(collection)) == null ? void 0 : _b.writeOnly;
6532
- const currentMem = isWriteOnly ? null : this.inMemDb.getById(collection, id);
6533
- const merged = applyObjDiff(
6534
- currentMem != null ? currentMem : existing,
6535
- diff,
6536
- id,
6537
- collection
6538
- );
6543
+ const merged = applyObjDiff(base, diff, id, collection);
6539
6544
  this.pendingChanges.schedule(
6540
6545
  collection,
6541
6546
  id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "2.0.0",
3
+ "version": "2.0.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",