cry-synced-db-client 2.0.5 → 2.0.6

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/dist/index.js CHANGED
@@ -4525,7 +4525,7 @@ var ServerUpdateHandler = class {
4525
4525
  return updatedIds;
4526
4526
  }
4527
4527
  async processBracketedItem(collection, item) {
4528
- var _a, _b, _c, _d, _e, _f, _g, _h;
4528
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
4529
4529
  const localItem = await this.dexieDb.getById(collection, item._id);
4530
4530
  if (!localItem) {
4531
4531
  if (item.operation === "insert" && item.bracketedData) {
@@ -4580,13 +4580,18 @@ var ServerUpdateHandler = class {
4580
4580
  case "apply":
4581
4581
  break;
4582
4582
  }
4583
+ const incomingUpdaterId = (_f = item._lastUpdaterId) != null ? _f : (_e = item.bracketedData) == null ? void 0 : _e._lastUpdaterId;
4583
4584
  const pendingChange = this.deps.getPendingChange(collection, item._id);
4584
4585
  if (pendingChange && item.operation === "update") {
4585
- const diff = this.stripMetaFromDiff(item.bracketedData);
4586
+ const diff = this.dropFieldsOwnedByPending(
4587
+ this.stripMetaFromDiff(item.bracketedData),
4588
+ pendingChange.data,
4589
+ incomingUpdaterId
4590
+ );
4586
4591
  const currentInMemState = Object.assign({}, localItem, pendingChange.data);
4587
4592
  const merged = applyObjDiff(currentInMemState, diff, item._id, "bracketed");
4588
- merged._rev = (_e = item._rev) != null ? _e : localItem._rev;
4589
- merged._ts = (_f = item._ts) != null ? _f : localItem._ts;
4593
+ merged._rev = (_g = item._rev) != null ? _g : localItem._rev;
4594
+ merged._ts = (_h = item._ts) != null ? _h : localItem._ts;
4590
4595
  if (!merged._deleted && !merged._archived) {
4591
4596
  this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
4592
4597
  source: "incremental"
@@ -4596,10 +4601,14 @@ var ServerUpdateHandler = class {
4596
4601
  }
4597
4602
  const dirtyChange = await this.dexieDb.getDirtyChange(collection, item._id);
4598
4603
  if (dirtyChange && item.operation === "update") {
4599
- const diff = this.stripMetaFromDiff(item.bracketedData);
4604
+ const diff = this.dropFieldsOwnedByPending(
4605
+ this.stripMetaFromDiff(item.bracketedData),
4606
+ dirtyChange,
4607
+ incomingUpdaterId
4608
+ );
4600
4609
  const merged = applyObjDiff(localItem, diff, item._id, "bracketed");
4601
- merged._rev = (_g = item._rev) != null ? _g : localItem._rev;
4602
- merged._ts = (_h = item._ts) != null ? _h : localItem._ts;
4610
+ merged._rev = (_i = item._rev) != null ? _i : localItem._rev;
4611
+ merged._ts = (_j = item._ts) != null ? _j : localItem._ts;
4603
4612
  if (!merged._deleted && !merged._archived) {
4604
4613
  await this.dexieDb.insert(collection, merged);
4605
4614
  this.deps.writeToInMemBatch(collection, [this.stripLocalFields(merged)], "upsert", {
@@ -4883,7 +4892,12 @@ var ServerUpdateHandler = class {
4883
4892
  Object.assign(pendingChange.data, newFields);
4884
4893
  }
4885
4894
  const currentInMemState = Object.assign({}, localItem, pendingChange.data);
4886
- const merged = this.mergeLocalWithDelta(currentInMemState, serverDelta);
4895
+ const filteredDelta = this.dropFieldsOwnedByPending(
4896
+ serverDelta,
4897
+ pendingChange.data,
4898
+ serverDelta._lastUpdaterId
4899
+ );
4900
+ const merged = this.mergeLocalWithDelta(currentInMemState, filteredDelta);
4887
4901
  if (!merged._deleted && !merged._archived) {
4888
4902
  this.deps.writeToInMemBatch(
4889
4903
  collection,
@@ -4897,7 +4911,12 @@ var ServerUpdateHandler = class {
4897
4911
  const dirtyChange = await this.dexieDb.getDirtyChange(collection, serverDelta._id);
4898
4912
  const metaChanged = localItem._rev !== serverDelta._rev || !this.timestampsEqual(localItem._ts, serverDelta._ts);
4899
4913
  if (dirtyChange) {
4900
- const merged = this.mergeLocalWithDelta(localItem, serverDelta);
4914
+ const filteredDelta = this.dropFieldsOwnedByPending(
4915
+ serverDelta,
4916
+ dirtyChange,
4917
+ serverDelta._lastUpdaterId
4918
+ );
4919
+ const merged = this.mergeLocalWithDelta(localItem, filteredDelta);
4901
4920
  if (metaChanged) {
4902
4921
  await this.dexieDb.save(collection, serverDelta._id, merged);
4903
4922
  }
@@ -4975,6 +4994,50 @@ var ServerUpdateHandler = class {
4975
4994
  }
4976
4995
  return local;
4977
4996
  }
4997
+ /**
4998
+ * Drop fields from an incoming server delta/diff that our OWN in-flight
4999
+ * change (pending or already-dirty) already explicitly touches — but ONLY
5000
+ * when the incoming delta was itself authored by this same client
5001
+ * (`incomingUpdaterId === this.updaterId`). That is the one case where we
5002
+ * can safely assume our own newer local pending/dirty write supersedes the
5003
+ * incoming payload for that field: a slower/older echo of our OWN prior
5004
+ * write (e.g. a lock-write's confirmation arriving after we've already
5005
+ * locally reverted it, via a rev/timing gap the entry-level self-echo
5006
+ * check in `handleServerItemUpdate`/`checkRev` didn't catch) can otherwise
5007
+ * resurrect a field our own newer pending change had deliberately set or
5008
+ * cleared — both `mergeLocalWithDelta` and `applyObjDiff` apply whatever
5009
+ * the incoming payload says with no awareness of a competing, newer local
5010
+ * intent.
5011
+ *
5012
+ * When the incoming delta comes from a genuinely different actor (a true
5013
+ * second writer, or `incomingUpdaterId` is absent/unknown), this is a
5014
+ * skip-through no-op — that delta must always apply so real-time UX stays
5015
+ * live (see `test/secondActor.test.ts`): the field-name overlap alone does
5016
+ * NOT imply staleness for another writer's edit, only for our own.
5017
+ *
5018
+ * `ownedKeys` is checked via `hasOwnProperty` (not truthiness) because a
5019
+ * deliberately-cleared field is `undefined`, not absent, on the owning
5020
+ * diff/pendingChange.data.
5021
+ *
5022
+ * Path-based (bracketed) diff keys like `koraki[id].field` are matched by
5023
+ * their top-level segment — if `koraki` is owned, every path under it is
5024
+ * dropped, which is conservative but safe (reproduced with izdajRacun.vue's
5025
+ * sedajObdeluje lock/unlock race via the dbbracketed/ channel, 2026-07-14).
5026
+ */
5027
+ dropFieldsOwnedByPending(incoming, ownedKeys, incomingUpdaterId) {
5028
+ var _a;
5029
+ if (incomingUpdaterId === void 0 || incomingUpdaterId !== this.updaterId) {
5030
+ return incoming;
5031
+ }
5032
+ const result = {};
5033
+ for (const key in incoming) {
5034
+ if (!Object.prototype.hasOwnProperty.call(incoming, key)) continue;
5035
+ const topField = (_a = key.split(/[.[]/)[0]) != null ? _a : key;
5036
+ if (Object.prototype.hasOwnProperty.call(ownedKeys, topField)) continue;
5037
+ result[key] = incoming[key];
5038
+ }
5039
+ return result;
5040
+ }
4978
5041
  /**
4979
5042
  * Fields from an incoming server delta that our OWN in-flight pending
4980
5043
  * change doesn't already touch — safe to backfill into it so a later
@@ -81,6 +81,37 @@ export declare class ServerUpdateHandler implements I_ServerUpdateHandler {
81
81
  private timestampsEqual;
82
82
  /** @mutates local — mutira vhodni objekt namesto kopiranja */
83
83
  private mergeLocalWithDelta;
84
+ /**
85
+ * Drop fields from an incoming server delta/diff that our OWN in-flight
86
+ * change (pending or already-dirty) already explicitly touches — but ONLY
87
+ * when the incoming delta was itself authored by this same client
88
+ * (`incomingUpdaterId === this.updaterId`). That is the one case where we
89
+ * can safely assume our own newer local pending/dirty write supersedes the
90
+ * incoming payload for that field: a slower/older echo of our OWN prior
91
+ * write (e.g. a lock-write's confirmation arriving after we've already
92
+ * locally reverted it, via a rev/timing gap the entry-level self-echo
93
+ * check in `handleServerItemUpdate`/`checkRev` didn't catch) can otherwise
94
+ * resurrect a field our own newer pending change had deliberately set or
95
+ * cleared — both `mergeLocalWithDelta` and `applyObjDiff` apply whatever
96
+ * the incoming payload says with no awareness of a competing, newer local
97
+ * intent.
98
+ *
99
+ * When the incoming delta comes from a genuinely different actor (a true
100
+ * second writer, or `incomingUpdaterId` is absent/unknown), this is a
101
+ * skip-through no-op — that delta must always apply so real-time UX stays
102
+ * live (see `test/secondActor.test.ts`): the field-name overlap alone does
103
+ * NOT imply staleness for another writer's edit, only for our own.
104
+ *
105
+ * `ownedKeys` is checked via `hasOwnProperty` (not truthiness) because a
106
+ * deliberately-cleared field is `undefined`, not absent, on the owning
107
+ * diff/pendingChange.data.
108
+ *
109
+ * Path-based (bracketed) diff keys like `koraki[id].field` are matched by
110
+ * their top-level segment — if `koraki` is owned, every path under it is
111
+ * dropped, which is conservative but safe (reproduced with izdajRacun.vue's
112
+ * sedajObdeluje lock/unlock race via the dbbracketed/ channel, 2026-07-14).
113
+ */
114
+ private dropFieldsOwnedByPending;
84
115
  /**
85
116
  * Fields from an incoming server delta that our OWN in-flight pending
86
117
  * change doesn't already touch — safe to backfill into it so a later
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",