commit 9ab07c1abed00fc9f31880eb7cf8683bdcc5f964
parent f2bb8d4bb07f3c7d279b83a22969bafb182ef6a4
Author: Michael Camilleri <[email protected]>
Date: Wed, 29 Apr 2026 02:37:31 +0900
Add more logging for shared games
Diffstat:
3 files changed, 77 insertions(+), 14 deletions(-)
diff --git a/Crossmate/CrossmateApp.swift b/Crossmate/CrossmateApp.swift
@@ -31,7 +31,7 @@ struct CrossmateApp: App {
// MARK: - App Delegate
final class AppDelegate: NSObject, UIApplicationDelegate, @unchecked Sendable {
- var onRemoteNotification: (() async -> Void)?
+ var onRemoteNotification: ((String) async -> Void)?
func application(
_ application: UIApplication,
@@ -45,7 +45,8 @@ final class AppDelegate: NSObject, UIApplicationDelegate, @unchecked Sendable {
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) async -> UIBackgroundFetchResult {
- await onRemoteNotification?()
+ let summary = AppServices.describePush(userInfo: userInfo)
+ await onRemoteNotification?(summary)
return .newData
}
diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift
@@ -88,8 +88,8 @@ final class AppServices {
nytAuth.loadStoredSession()
driveMonitor.start()
- appDelegate.onRemoteNotification = {
- await self.handleRemoteNotification()
+ appDelegate.onRemoteNotification = { summary in
+ await self.handleRemoteNotification(summary: summary)
}
CloudShareAcceptanceBroker.shared.onAcceptShare = { metadata in
await self.enqueueShareAcceptance(metadata)
@@ -139,7 +139,7 @@ final class AppServices {
await processPendingShareAcceptances()
await syncMonitor.run("initial fetch") {
- try await syncEngine.fetchChanges()
+ try await syncEngine.fetchChanges(source: "initial")
}
await syncMonitor.run("initial push") {
try await syncEngine.pushChanges()
@@ -158,7 +158,7 @@ final class AppServices {
func syncOnForeground() async {
await moveBuffer.flush()
await syncMonitor.run("foreground fetch") {
- try await syncEngine.fetchChanges()
+ try await syncEngine.fetchChanges(source: "foreground")
}
await syncMonitor.run("foreground push") {
try await syncEngine.pushChanges()
@@ -173,7 +173,7 @@ final class AppServices {
func syncOpenSharedPuzzle() async {
await moveBuffer.flush()
await syncMonitor.run("open-puzzle fetch") {
- try await syncEngine.fetchChanges()
+ try await syncEngine.fetchChanges(source: "open-puzzle poll")
}
await refreshSnapshot()
}
@@ -189,12 +189,49 @@ final class AppServices {
)
}
- private func handleRemoteNotification() async {
+ private func handleRemoteNotification(summary: String) async {
+ syncMonitor.note("remote notification: \(summary)")
await syncMonitor.run("remote-notification fetch") {
- try await syncEngine.fetchChanges()
+ try await syncEngine.fetchChanges(source: "push")
}
}
+ /// Parses the silent-push payload into a short, human-readable summary
+ /// (database scope, notification type, subscription ID, pruned flag).
+ /// Used by the diagnostics log to confirm whether shared-DB pushes are
+ /// actually being delivered to the device.
+ static func describePush(userInfo: [AnyHashable: Any]) -> String {
+ guard let note = CKNotification(fromRemoteNotificationDictionary: userInfo) else {
+ return "unparseable userInfo=\(userInfo)"
+ }
+ let kind: String
+ let scope: CKDatabase.Scope?
+ switch note {
+ case let n as CKDatabaseNotification:
+ kind = "database"
+ scope = n.databaseScope
+ case let n as CKRecordZoneNotification:
+ kind = "recordZone"
+ scope = n.databaseScope
+ case let n as CKQueryNotification:
+ kind = "query(\(n.queryNotificationReason.rawValue))"
+ scope = n.databaseScope
+ default:
+ kind = "type(\(note.notificationType.rawValue))"
+ scope = nil
+ }
+ let scopeLabel: String
+ switch scope {
+ case .private: scopeLabel = "private"
+ case .shared: scopeLabel = "shared"
+ case .public: scopeLabel = "public"
+ case .none: scopeLabel = "n/a"
+ case .some(let other): scopeLabel = "scope(\(other.rawValue))"
+ }
+ let sub = note.subscriptionID ?? "<nil>"
+ return "scope=\(scopeLabel) kind=\(kind) sub=\(sub) pruned=\(note.isPruned)"
+ }
+
private func processPendingShareAcceptances() async {
guard isReadyForShareAcceptance, !isProcessingShareAcceptanceQueue else { return }
isProcessingShareAcceptanceQueue = true
diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift
@@ -35,6 +35,15 @@ actor SyncEngine {
private var privateEngine: CKSyncEngine?
private var sharedEngine: CKSyncEngine?
+ /// Label for the in-flight fetch — surfaced in traces so the diagnostics
+ /// log can distinguish push-driven fetches from polls / foreground / etc.
+ /// `nil` means CKSyncEngine drove the fetch itself (its internal scheduler).
+ private var currentFetchSource: String?
+ /// One-shot flag — set the first time we observe shared-DB content
+ /// arriving via a push-triggered fetch. Confirms the silent-push path is
+ /// actually wired up end-to-end.
+ private var loggedFirstSharedPushPayload = false
+
private var onRemoteMoves: (@MainActor @Sendable ([Move]) async -> Void)?
private var onAccountChange: (@MainActor @Sendable () async -> Void)?
private var onGameAccessRevoked: (@MainActor @Sendable (UUID) async -> Void)?
@@ -179,7 +188,9 @@ actor SyncEngine {
// MARK: - Explicit sync triggers (called by AppServices / diagnostics view)
- func fetchChanges() async throws {
+ func fetchChanges(source: String = "manual") async throws {
+ currentFetchSource = source
+ defer { currentFetchSource = nil }
async let p: Void = privateEngine?.fetchChanges() ?? ()
async let s: Void = sharedEngine?.fetchChanges() ?? ()
_ = try await (p, s)
@@ -649,8 +660,9 @@ actor SyncEngine {
_ event: CKSyncEngine.Event.FetchedDatabaseChanges,
isPrivate: Bool
) async {
+ let src = currentFetchSource ?? "framework"
await trace(
- "\(isPrivate ? "private" : "shared") db changes: " +
+ "\(isPrivate ? "private" : "shared") db changes [src=\(src)]: " +
"\(event.modifications.count) zone mods, \(event.deletions.count) zone deletions"
)
@@ -708,10 +720,16 @@ actor SyncEngine {
isPrivate: Bool
) async {
let scope: Int16 = isPrivate ? 0 : 1
+ let src = currentFetchSource ?? "framework"
await trace(
- "\(isPrivate ? "private" : "shared") fetch: " +
+ "\(isPrivate ? "private" : "shared") fetch [src=\(src)]: " +
"\(event.modifications.count) modifications, \(event.deletions.count) deletions"
)
+ if !isPrivate, !loggedFirstSharedPushPayload, src == "push",
+ event.modifications.count + event.deletions.count > 0 {
+ loggedFirstSharedPushPayload = true
+ await trace("✅ first shared-DB push payload received — silent-push path is live")
+ }
let ctx = persistence.container.newBackgroundContext()
let (newMoves, affectedGameIDs): ([Move], Set<UUID>) = ctx.performAndWait {
@@ -805,8 +823,15 @@ actor SyncEngine {
}
private func handleSentRecordZoneChanges(
- _ event: CKSyncEngine.Event.SentRecordZoneChanges
+ _ event: CKSyncEngine.Event.SentRecordZoneChanges,
+ isPrivate: Bool
) async {
+ await trace(
+ "\(isPrivate ? "private" : "shared") sent: " +
+ "\(event.savedRecords.count) saved, " +
+ "\(event.failedRecordSaves.count) failed, " +
+ "\(event.deletedRecordIDs.count) deleted"
+ )
let ctx = persistence.container.newBackgroundContext()
let (savedSnapshotNames, failureMessages): ([String], [String]) = ctx.performAndWait {
var snapshotNames: [String] = []
@@ -907,7 +932,7 @@ extension SyncEngine: CKSyncEngineDelegate {
break
case .sentRecordZoneChanges(let e):
- await handleSentRecordZoneChanges(e)
+ await handleSentRecordZoneChanges(e, isPrivate: isPrivate)
case .willFetchChanges, .didFetchChanges,
.willFetchRecordZoneChanges, .didFetchRecordZoneChanges,