crossmate

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

commit a27fd1c70b3ed73dd4b7601e4ce033636edcb874
parent 5bbbfa443938d15600a3e7ce670fa8b773396c63
Author: Michael Camilleri <[email protected]>
Date:   Mon,  1 Jun 2026 15:29:24 +0900

Add logging to replay downloads

Diffstat:
MCrossmate/CrossmateApp.swift | 9+++++++++
MCrossmate/Services/AppServices.swift | 35+++++++++++++++++++++++++++++++----
MCrossmate/Sync/CloudQuery.swift | 6++++++
3 files changed, 46 insertions(+), 4 deletions(-)

diff --git a/Crossmate/CrossmateApp.swift b/Crossmate/CrossmateApp.swift @@ -470,9 +470,18 @@ private struct PuzzleDisplayView: View { let localDeviceID = RecordSerializer.localDeviceID let otherDevices = store.contributingDevices(for: gameID) .filter { $0.deviceID != localDeviceID } + let short = gameID.uuidString.prefix(8) if otherDevices.isEmpty { + services.syncMonitor.note( + "replay[\(short)]: local-only path " + + "(no other contributing devices), localEntries=\(entries.count)" + ) return .ready(ReplayTimeline(merging: [entries])) } + services.syncMonitor.note( + "replay[\(short)]: merged path, " + + "otherDevices=\(otherDevices.count), localEntries=\(entries.count)" + ) return await services.loadReplay(gameID: gameID) } ) diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift @@ -2587,17 +2587,44 @@ final class AppServices { /// zone can't be reached. The fetch is a plain CKQuery, so it's safe to call /// from the UI when the finish banner appears. func loadReplay(gameID: UUID) async -> JournalReplayResult { - // `try?` flattens both the thrown error and the method's own optional - // (nil = zone unknown / access revoked) into a single `.unavailable`. - guard let fetch = try? await syncEngine.fetchReplay(forGameID: gameID) else { + let short = gameID.uuidString.prefix(8) + // A nil return means zone unknown / access revoked; a throw means the + // on-demand CKQuery itself failed. Both flatten to `.unavailable`, but + // log which one (and the error) so the diagnostics stream can tell them + // apart — the replay fetch is otherwise invisible to the event log. + let fetch: JournalReplayFetch? + do { + fetch = try await syncEngine.fetchReplay(forGameID: gameID) + } catch { + let ns = error as NSError + syncMonitor.note( + "replay[\(short)]: fetch threw — domain=\(ns.domain) " + + "code=\(ns.code) \(ns.localizedDescription)" + ) + return .unavailable + } + guard let fetch else { + syncMonitor.note("replay[\(short)]: fetch unavailable (zone unknown / access revoked)") return .unavailable } let local = store.localReplaySource(gameID: gameID) - return ReplayAssembler.assemble( + let result = ReplayAssembler.assemble( fetch: fetch, localKey: local?.key ?? JournalDeviceKey(authorID: "", deviceID: ""), localEntries: local?.entries ?? [] ) + let resultDesc: String + switch result { + case .ready(let timeline): resultDesc = "ready(steps=\(timeline.count))" + case .waiting(let missing): resultDesc = "waiting(missing=\(missing))" + case .unavailable: resultDesc = "unavailable" + } + syncMonitor.note( + "replay[\(short)]: merged — expected=\(fetch.expectedDevices.count), " + + "journals=\(fetch.journals.count), localEntries=\(local?.entries.count ?? 0) " + + "→ \(resultDesc)" + ) + return result } /// Builds the `GameStore.onGameDeleted` callback. Extracted so tests can diff --git a/Crossmate/Sync/CloudQuery.swift b/Crossmate/Sync/CloudQuery.swift @@ -873,6 +873,12 @@ extension SyncEngine { ) } } + await trace( + "fetchReplay \(gameID.uuidString.prefix(8)): scope=\(info.scope) " + + "movesRecords=\(movesRecords.count) expectedDevices=\(expected.count) " + + "journalRecords=\(journalRecords.count) " + + "journalEntryCounts=[\(journals.map { String($0.entries.count) }.joined(separator: ","))]" + ) return JournalReplayFetch(journals: journals, expectedDevices: expected) } }