commit ca02d00b7763f647648ee75a100cd79d1e6fe5d5
parent 93b8097d8ec51d9b38bac1cb8b3998285fe6e7f3
Author: Michael Camilleri <[email protected]>
Date: Tue, 14 Apr 2026 23:38:37 +0900
Change gating logic for iCloud fetches
Diffstat:
1 file changed, 13 insertions(+), 9 deletions(-)
diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift
@@ -363,15 +363,19 @@ actor SyncEngine {
.joined(separator: ", ")
await trace("fetch: changedZoneIDs count=\(changedZoneIDs.count) [\(zoneDescriptions)]")
- // Only proceed if our zone has changes. Equality works here because
- // `resolveOwnerIfNeeded()` has rebuilt `zoneID` with the real owner
- // record ID, matching what the server returns.
- guard changedZoneIDs.contains(zoneID) else {
- await trace("fetch: our zone \(zoneID.zoneName)/\(shortOwner(zoneID.ownerName)) not in changed set, skipping zone fetch")
- return
- }
-
- // Step 2: Fetch zone-level changes
+ // Step 2: Fetch zone-level changes.
+ //
+ // We deliberately do NOT gate this on `changedZoneIDs.contains(zoneID)`.
+ // `fetchDatabaseChanges` persists its returned token at the end of
+ // every successful call, so if the zone fetch is ever skipped while
+ // the database token advances (as happened under the previous
+ // owner-mismatch bug), the zone's records become permanently
+ // orphaned — the database token is past the change, yet the zone
+ // token never advanced because we never ran the zone fetch.
+ //
+ // Instead, always call `fetchZoneChanges` for our known custom zone
+ // and let the zone token be the source of truth for what we've
+ // applied. The call is cheap when there are no changes.
let zoneToken: CKServerChangeToken? = context.performAndWait {
SyncStateEntity.current(in: context).decodedPrivateZoneToken
}