crossmate

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

commit f1b12ffae6f3ee74572bf538ceb23149b3b333cc
parent ccf6c5adbaec793931f41ddd45b1fa748b4a51ef
Author: Michael Camilleri <[email protected]>
Date:   Wed, 13 May 2026 04:44:33 +0900

Parallelise fetchPushPingsDirect across zones

The ping fast-path looped serially over every known zone in the database scope,
issuing one CKQuery per zone and appending the results. For a user with sixteen
active games that meant sixteen sequential round-trips on every silent-push
wake — the work blocking a collaborator's 'I just joined' or 'I just solved'
toast from reaching the screen.

The per-zone Ping queries are now dispatched through a TaskGroup, mirroring the
pattern fetchKnownZoneUpdatesDirect already uses. The actor's await points
release isolation between round-trips, so the per-zone CK requests actually
overlap. Per-zone errors are caught and traced rather than propagated, so one
transient failure doesn't suppress notifications from healthy zones.

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

Diffstat:
MCrossmate/Sync/SyncEngine.swift | 56+++++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 39 insertions(+), 17 deletions(-)

diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift @@ -586,24 +586,46 @@ actor SyncEngine { let scopeCheckpoint = pingPushCheckpoints[scopeValue]? .addingTimeInterval(-pingPushCheckpointOverlap) - var collected: [CKRecord] = [] - for (zoneID, createdAt) in zones { - // Scope checkpoint (if present) wins — it's forward-moving across - // all zones. On first run for a given scope we fall back to the - // game's createdAt floor so the ping that triggered this wake is - // still in range, but pings older than the device's first - // knowledge of the game are not. - let since = scopeCheckpoint - ?? createdAt.addingTimeInterval(-pingPushCheckpointOverlap) - let recs = try await queryLiveRecords( - type: "Ping", - database: database, - zoneID: zoneID, - since: since, - desiredKeys: ["authorID", "playerName", "puzzleTitle", "kind", "scope"] - ) - collected.append(contentsOf: recs) + // Fan the per-zone Ping queries out concurrently. The actor's await + // points release isolation between round-trips, so the per-zone CK + // requests overlap; a serial N-zone scan becomes a single parallel + // batch. Per-zone errors are caught and traced so one transient + // failure doesn't suppress notifications from healthy zones. + let perZoneRecords = await withTaskGroup(of: [CKRecord].self) { group in + for (zoneID, createdAt) in zones { + // Scope checkpoint (if present) wins — it's forward-moving + // across all zones. On first run for a given scope we fall + // back to the game's createdAt floor so the ping that + // triggered this wake is still in range, but pings older + // than the device's first knowledge of the game are not. + let since = scopeCheckpoint + ?? createdAt.addingTimeInterval(-pingPushCheckpointOverlap) + group.addTask { [weak self] in + guard let self else { return [] } + do { + return try await self.queryLiveRecords( + type: "Ping", + database: database, + zoneID: zoneID, + since: since, + desiredKeys: ["authorID", "playerName", "puzzleTitle", "kind", "scope"] + ) + } catch { + await self.trace( + "\(label) ping fast-path: zone \(zoneID.zoneName) failed: " + + "\(error.localizedDescription)" + ) + return [] + } + } + } + var all: [[CKRecord]] = [] + for await batch in group { + all.append(batch) + } + return all } + let collected: [CKRecord] = perZoneRecords.flatMap { $0 } let pings = collected.compactMap(Self.parsePingRecord) if let latest = collected.compactMap(\.modificationDate).max() {