crossmate

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

commit 4ea6f4f986c9aa61f55744bb0b0666cf75c45f27
parent 2290830d2885276eab49a8f745083f5d8310b77d
Author: Michael Camilleri <[email protected]>
Date:   Sat, 30 May 2026 23:39:07 +0900

Stop the engagement backstop from re-leasing readAt in the background

A peer who paused playing while another peer was away might never send a
session summary via the pause push notification. The reason is that the
pause push is meant to be gated by the presence of the recipient, as
determined by Player.readAt.

The problem is that the engagement reconnect backstop keeps re-leasing
readAt, and it was cancelled only on the .leave phase change, not on on
the .background phase change. Its comment assumed it would go dormant
under suspension but CKSyncEngine can wake the app (and often does so if
the app was being recently used) in which case each wake fired one
iteration and undid the collapse.

This commit cancels the backstop on .background, before the collapse, so
readAt stays at now until we return; .active re-arms it via
startEngagementIfPossible.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

Diffstat:
MCrossmate/CrossmateApp.swift | 8++++++++
MCrossmate/Services/AppServices.swift | 6++++--
2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/Crossmate/CrossmateApp.swift b/Crossmate/CrossmateApp.swift @@ -593,6 +593,14 @@ private struct PuzzleDisplayView: View { await services.startEngagementIfPossible(gameID: id) } case .background: + // Stop the reconnect backstop before collapsing the lease. + // The tick re-leases `readAt` (future-dated) on every wake, + // and CKSyncEngine wakes us constantly in the background, so + // without this the `.currentTime` collapse below is undone + // moments later — which is what kept peers from sending us + // session summaries while we were away. `.active` re-arms it + // via `startEngagementIfPossible`. + services.cancelEngagementReconnectRetry(gameID: id) Task { await services.publishReadCursor(for: id, mode: .currentTime) } case .inactive: break diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift @@ -1091,8 +1091,10 @@ final class AppServices { } } - private func cancelEngagementReconnectRetry(gameID: UUID) { - engagementReconnectTasks.removeValue(forKey: gameID)?.cancel() + func cancelEngagementReconnectRetry(gameID: UUID) { + guard let task = engagementReconnectTasks.removeValue(forKey: gameID) else { return } + task.cancel() + syncMonitor.note("engagement: reconnect backstop cancelled for \(gameID.uuidString)") } func noteLocalSelection(_ selection: PlayerSelection, gameID: UUID) async {