@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 +28 -0
- package/package.json +1 -1
- package/src/merge.js +31 -27
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
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.
|
|
119
|
-
*
|
|
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
|
-
|
|
152
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|