crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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:
MCrossmate/Sync/SyncEngine.swift | 22+++++++++++++---------
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 }