@glance-apps/sync 1.3.1 → 1.3.2

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
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.2] - 2026-06-14
11
+
12
+ ### Fixed
13
+
14
+ - **`mergeRoutineDefinitions` chip collisions are now ownership-aware.** Routine
15
+ chips can be claimed by a user (stamping `ownerSyncId`), and claiming is a
16
+ one-way transition. Previously collisions were resolved purely by newer
17
+ `lastModified`, so an unclaimed copy of a chip could beat a claimed copy when
18
+ it carried an equal-or-newer timestamp — a claim made on one device would
19
+ never reach a device still holding the unclaimed copy. Resolution now prefers
20
+ the claimed side when exactly one side is claimed, regardless of
21
+ `lastModified`; when both sides share the same ownership status (both claimed
22
+ or both unclaimed), the newer `lastModified` still wins. Ordering and
23
+ tombstone behavior are unchanged.
24
+
10
25
  ## [1.3.1] - 2026-06-14
11
26
 
12
27
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glance-apps/sync",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/merge.js CHANGED
@@ -115,11 +115,14 @@ export const mergeArrayById = (localItems, remoteItems, deletedIds, syncHorizon
115
115
  *
116
116
  * Each side's bucket is an array of { id, name } chips. The merge preserves
117
117
  * local ordering and appends any remote-only chips at the end — mirroring the
118
- * mergeArrayById strategy. When the same chip id exists on both sides, the
118
+ * mergeArrayById strategy. When the same chip id exists on both sides, a
119
+ * claimed chip (one carrying an `ownerSyncId`) always beats an unclaimed copy —
120
+ * claiming is a one-way transition, so the unclaimed side is stale regardless of
121
+ * its `lastModified`. When both sides share the same ownership status, the
119
122
  * version with the newer `lastModified` wins (field-level last-write-wins), so
120
- * edits like renames, time changes, or ownership stamps propagate across
121
- * devices. Chips present in the deletedChipIds tombstone map are excluded from
122
- * the merged result.
123
+ * edits like renames and time changes still propagate across devices. Chips
124
+ * present in the deletedChipIds tombstone map are excluded from the merged
125
+ * result.
123
126
  *
124
127
  * @param {Object} localDefs - Local routine definitions (bucket → chip[])
125
128
  * @param {Object} remoteDefs - Remote routine definitions (bucket → chip[])
@@ -147,17 +150,22 @@ export const mergeRoutineDefinitions = (localDefs, remoteDefs, deletedChipIds =
147
150
  const remoteMap = new Map(remoteChips.map(c => [String(c.id), c]));
148
151
  const localMap = new Map(localChips.map(c => [String(c.id), c]));
149
152
  const lm = (c) => c?.lastModified ? new Date(c.lastModified).getTime() : 0;
153
+ const owned = (c) => !!c?.ownerSyncId;
154
+ // A claimed chip (has ownerSyncId) always overrides an unclaimed copy of the
155
+ // same id — claiming is one-way, so the unclaimed side is stale regardless of
156
+ // its lastModified. Same ownership status on both sides → newer wins.
157
+ const overrides = (x, y) => owned(x) !== owned(y) ? owned(x) : lm(x) > lm(y);
150
158
 
151
159
  const bucketMerged = [];
152
- // Local chips in order; when a chip also exists remotely, take the newer one.
160
+ // Local chips in order; when a chip also exists remotely, take the winner.
153
161
  for (const chip of localChips) {
154
162
  if (isTombstoned(chip)) { localChanged = true; continue; }
155
163
  const r = remoteMap.get(String(chip.id));
156
- if (r && !isTombstoned(r) && lm(r) > lm(chip)) {
157
- bucketMerged.push(r); localChanged = true; // remote edit wins
164
+ if (r && !isTombstoned(r) && overrides(r, chip)) {
165
+ bucketMerged.push(r); localChanged = true; // remote version wins
158
166
  } else {
159
167
  bucketMerged.push(chip);
160
- if (r && lm(chip) > lm(r)) remoteChanged = true; // local edit is newer
168
+ if (r && overrides(chip, r)) remoteChanged = true; // local version wins → push
161
169
  }
162
170
  }
163
171
  // Append remote-only chips (skip tombstoned)