commit 65d6d5da27b95af2201de210b5780385c24a6e2f
parent c6073ccdf2e5365010503edf6095c641e6ca2344
Author: Michael Camilleri <[email protected]>
Date: Sun, 17 May 2026 18:51:00 +0900
Keep the ping fast-path from matching its own newest record
Prior to this commit, the background ping fast path floored each per-zone Ping
query at the stored checkpoint minus a 30s overlap, then set that checkpoint to
the maximum modificationDate of the records the scan had just returned. Because
the floor was always derived from — and 30s behind — the newest record it had
just matched, `modificationDate > floor` re-matched that record on every
subsequent push. The checkpoint could never climb past it; it was pinned 30s
behind itself. So the newest ping in a scope (e.g. a freshly written .win) was
re-fetched and re-delivered to onPings on every database push for the whole
session — a full N-zone CKQuery fan-out every couple of seconds accomplishing
nothing — and the traced `pings=` count could never fall back to zero once any
ping had been seen. State stayed correct only because the presentation layer
dedupes notifications by record name and the .opened/.friend handlers are
idempotent; the cost was pure wasted work and a metric that no longer signalled
anything.
The checkpoint now advances monotonically — max of the prior value and the
batch's latest, never assigned the batch max outright — so a slow zone's older
records can't drag every zone's window backward. Emission is deduped at the
detection layer via a per-scope record-name to modificationDate map: a ping
reaches onPings only on first sighting, mirroring the presentation- layer
dedupe already in NotificationState. The overlap window keeps doing its real
job — a skew-tolerant re-fetch so a record whose server modificationDate lands
behind the checkpoint isn't missed — without driving an unbounded re-emit. The
map is pruned to the overlap window each scan so it stays bounded to ~30s of
pings rather than the session, and is cleared alongside pingPushCheckpoints on
engine reinit. The trace gains a `dup=` field for suppressed re-fetches, so
`pings=` is now an honest count of genuinely new pings that returns to zero at
rest.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
1 file changed, 38 insertions(+), 3 deletions(-)
diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift
@@ -139,6 +139,13 @@ actor SyncEngine {
/// (0 = private, 1 = shared).
private var pingPushCheckpoints: [Int16: Date] = [:]
private let pingPushCheckpointOverlap: TimeInterval = 30
+ /// Per-scope record-name → modificationDate of Ping records already
+ /// surfaced by the fast path. The time-window query deliberately re-fetches
+ /// anything within `pingPushCheckpointOverlap` of the floor (skew safety),
+ /// so without this a record — unboundedly, the newest one — would re-emit
+ /// on every push. Pruned to the overlap window each scan, so it stays
+ /// small. Mirrors the presentation-layer dedupe in `NotificationState`.
+ private var seenPingRecords: [Int16: [String: Date]] = [:]
private let backgroundSessionLookback: TimeInterval = 10 * 60
func setTracer(_ t: @MainActor @Sendable @escaping (String) -> Void) {
@@ -869,10 +876,36 @@ actor SyncEngine {
}
let collected: [CKRecord] = perZoneRecords.flatMap(\.records)
- let pings = collected.compactMap(Self.parsePingRecord)
+ // Dedupe by record name: the overlap window re-fetches recent pings on
+ // every push, so emit each only on first sighting. Without this the
+ // newest ping re-fires forever — the floor is the stored checkpoint
+ // minus the overlap, so `modificationDate > floor` always re-matches it.
+ var seen = seenPingRecords[scopeValue] ?? [:]
+ var pings: [Ping] = []
+ var fetchedCount = 0
+ for record in collected {
+ guard let ping = Self.parsePingRecord(record) else { continue }
+ fetchedCount += 1
+ let modDate = record.modificationDate ?? Date()
+ if seen.updateValue(modDate, forKey: record.recordID.recordName) == nil {
+ pings.append(ping)
+ }
+ }
+
+ // Advance the checkpoint monotonically — `max(prior, latest)`, never
+ // `= latest` — so a slow zone's older batch can't drag every zone's
+ // window backward.
if let latest = collected.compactMap(\.modificationDate).max() {
- pingPushCheckpoints[scopeValue] = latest
+ let prior = pingPushCheckpoints[scopeValue] ?? .distantPast
+ pingPushCheckpoints[scopeValue] = max(prior, latest)
+ }
+ // Forget names the next query's floor can no longer return, keeping
+ // the seen set bounded to the overlap window rather than the session.
+ if let checkpoint = pingPushCheckpoints[scopeValue] {
+ let floor = checkpoint.addingTimeInterval(-pingPushCheckpointOverlap)
+ seen = seen.filter { $0.value >= floor }
}
+ seenPingRecords[scopeValue] = seen
let orphans = Set(perZoneRecords.compactMap(\.orphanedZone))
if !orphans.isEmpty {
@@ -880,7 +913,8 @@ actor SyncEngine {
}
await trace(
- "\(label) ping fast-path: zones=\(zones.count), pings=\(pings.count)"
+ "\(label) ping fast-path: zones=\(zones.count), " +
+ "pings=\(pings.count), dup=\(fetchedCount - pings.count)"
)
if !pings.isEmpty, let onPings {
@@ -1711,6 +1745,7 @@ actor SyncEngine {
))
pendingPings = [:]
pingPushCheckpoints = [:]
+ seenPingRecords = [:]
liveQueryCheckpoints = [:]
loggedFirstSharedPushPayload = false
_ = enqueueUnconfirmedMoves()