@glance-apps/sync 1.3.0 → 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,34 @@ 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
+
25
+ ## [1.3.1] - 2026-06-14
26
+
27
+ ### Fixed
28
+
29
+ - **`mergeRoutineDefinitions` now applies last-write-wins on chip id collisions.**
30
+ Previously, when the same chip id existed on both sides, the local chip was
31
+ kept verbatim and the remote one skipped — `lastModified` was only consulted
32
+ for tombstone resurrection. As a result, any edit to an existing routine chip
33
+ (rename, reorder, time change, or stamping `ownerSyncId`) failed to propagate
34
+ across devices. Colliding chips are now resolved by the newer `lastModified`,
35
+ matching the behavior of `mergeArrayById` / `mergeHabits`. Ordering and
36
+ tombstone behavior are preserved.
37
+
10
38
  ## [1.3.0] - 2026-06-14
11
39
 
12
40
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glance-apps/sync",
3
- "version": "1.3.0",
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,8 +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. Chips present in the deletedChipIds tombstone map
119
- * are excluded from the merged result.
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
122
+ * version with the newer `lastModified` wins (field-level last-write-wins), so
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.
120
126
  *
121
127
  * @param {Object} localDefs - Local routine definitions (bucket → chip[])
122
128
  * @param {Object} remoteDefs - Remote routine definitions (bucket → chip[])
@@ -141,40 +147,38 @@ export const mergeRoutineDefinitions = (localDefs, remoteDefs, deletedChipIds =
141
147
  for (const bucket of allBuckets) {
142
148
  const localChips = localDefs[bucket] || [];
143
149
  const remoteChips = remoteDefs[bucket] || [];
144
- const localIds = new Set(localChips.map(c => String(c.id)));
145
150
  const remoteMap = new Map(remoteChips.map(c => [String(c.id), c]));
151
+ const localMap = new Map(localChips.map(c => [String(c.id), c]));
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);
146
158
 
147
- // Start with local chips in order, filtering out tombstoned chips
148
159
  const bucketMerged = [];
160
+ // Local chips in order; when a chip also exists remotely, take the winner.
149
161
  for (const chip of localChips) {
150
- if (isTombstoned(chip)) {
151
- localChanged = true; // chip removed locally
152
- continue;
162
+ if (isTombstoned(chip)) { localChanged = true; continue; }
163
+ const r = remoteMap.get(String(chip.id));
164
+ if (r && !isTombstoned(r) && overrides(r, chip)) {
165
+ bucketMerged.push(r); localChanged = true; // remote version wins
166
+ } else {
167
+ bucketMerged.push(chip);
168
+ if (r && overrides(chip, r)) remoteChanged = true; // local version wins → push
153
169
  }
154
- bucketMerged.push(chip);
155
170
  }
156
-
157
171
  // Append remote-only chips (skip tombstoned)
158
- for (const remoteChip of remoteChips) {
159
- const id = String(remoteChip.id);
160
- if (localIds.has(id)) continue;
161
- if (isTombstoned(remoteChip)) {
162
- remoteChanged = true; // tell remote this was deleted
163
- continue;
164
- }
165
- bucketMerged.push(remoteChip);
166
- localChanged = true;
172
+ for (const r of remoteChips) {
173
+ if (localMap.has(String(r.id))) continue;
174
+ if (isTombstoned(r)) { remoteChanged = true; continue; }
175
+ bucketMerged.push(r); localChanged = true;
167
176
  }
168
-
169
- // Check for local-only chips (remote needs them)
170
- for (const localChip of localChips) {
171
- const id = String(localChip.id);
172
- if (isTombstoned(localChip)) continue; // don't flag deleted chips as needing push
173
- if (!remoteMap.has(id)) {
174
- remoteChanged = true;
175
- }
177
+ // Local-only chips → remote needs them
178
+ for (const c of localChips) {
179
+ if (isTombstoned(c)) continue;
180
+ if (!remoteMap.has(String(c.id))) remoteChanged = true;
176
181
  }
177
-
178
182
  merged[bucket] = bucketMerged;
179
183
  }
180
184