commit 2c0f11b1dff0a9feeafecbffcd27c66a0a89ea58
parent c5b5e6280d4b6b1100679b6d015f5835b89b5b6c
Author: Michael Camilleri <[email protected]>
Date: Thu, 14 May 2026 14:40:26 +0900
Coalesce duplicate remote notification handlers
Device logs showed paired private database pushes starting the same work twice:
background notifications ran two session scans, while foreground notifications
ran duplicate zone discovery, ping fast-path scans and push fetches. The ping
dedup kept notifications from being presented twice, but the device still spent
CloudKit reads and filled diagnostics with repeated already-shown ping lines.
AppServices now tracks whether a private or shared remote-notification handler
is already in flight. A same-scope notification that arrives during that window
is coalesced into the active handler instead of starting duplicate work, with a
diagnostic breadcrumb noting the coalescing point.
Handling a scoped remote notification also cancels any pending delayed
Game/Moves catch-up for that scope. Background handlers schedule a fresh
catch-up after their session scan; foreground handlers supersede the delayed
work with their own fetch path.
Co-Authored-By: Codex GPT 5.5 <[email protected]>
Diffstat:
1 file changed, 58 insertions(+), 0 deletions(-)
diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift
@@ -41,6 +41,8 @@ final class AppServices {
private var lastRemoteNotificationAt: Date?
private var privatePushCatchUpTask: Task<Void, Never>?
private var sharedPushCatchUpTask: Task<Void, Never>?
+ private var isHandlingPrivateRemoteNotification = false
+ private var isHandlingSharedRemoteNotification = false
init() {
let preferences = PlayerPreferences()
@@ -367,6 +369,11 @@ final class AppServices {
return
}
+ guard beginRemoteNotificationHandling(scope: scope) else { return }
+ defer { endRemoteNotificationHandling(scope: scope) }
+
+ cancelBackgroundPushCatchUp(scope: scope)
+
if isBackground {
let result = await syncMonitor.run("remote-notification background session scan") {
try await syncEngine.fetchBackgroundSessionsDirect(scope: scope)
@@ -422,6 +429,57 @@ final class AppServices {
await refreshSnapshot()
}
+ private func beginRemoteNotificationHandling(scope: CKDatabase.Scope) -> Bool {
+ switch scope {
+ case .private:
+ guard !isHandlingPrivateRemoteNotification else {
+ syncMonitor.note("private remote notification coalesced into in-flight handler")
+ return false
+ }
+ isHandlingPrivateRemoteNotification = true
+ return true
+ case .shared:
+ guard !isHandlingSharedRemoteNotification else {
+ syncMonitor.note("shared remote notification coalesced into in-flight handler")
+ return false
+ }
+ isHandlingSharedRemoteNotification = true
+ return true
+ case .public:
+ return false
+ @unknown default:
+ return false
+ }
+ }
+
+ private func endRemoteNotificationHandling(scope: CKDatabase.Scope) {
+ switch scope {
+ case .private:
+ isHandlingPrivateRemoteNotification = false
+ case .shared:
+ isHandlingSharedRemoteNotification = false
+ case .public:
+ return
+ @unknown default:
+ return
+ }
+ }
+
+ private func cancelBackgroundPushCatchUp(scope: CKDatabase.Scope) {
+ switch scope {
+ case .private:
+ privatePushCatchUpTask?.cancel()
+ privatePushCatchUpTask = nil
+ case .shared:
+ sharedPushCatchUpTask?.cancel()
+ sharedPushCatchUpTask = nil
+ case .public:
+ return
+ @unknown default:
+ return
+ }
+ }
+
private func scheduleBackgroundPushCatchUp(scope: CKDatabase.Scope) {
switch scope {
case .private: