crossmate

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

commit 7c16492130c5d80d56b2341a432e1f72a26b9797
parent 85511b54f005889749233a9592860403fe00f2e5
Author: Michael Camilleri <[email protected]>
Date:   Sat, 27 Jun 2026 06:38:25 +0900

Coalesce duplicate accountSeen pushes

Opening a shared puzzle could publish two near-identical accountSeen
pushes: the delivered-notification dismissal stamped the game as seen,
and the active read lease then published the same forward-dated horizon.
That showed up as duplicate worker traffic even though both paths were
reporting the same user-visible state.

This commit coalesces per-game accountSeen publishes inside
AccountPushCoordinator when the new read horizon is within 30 seconds of
the last successful publish attempt. The first signal still reaches
sibling devices promptly, while the repeated open-time publish is
skipped with an explicit diagnostic line.

Co-Authored-By: Codex GPT 5.5 <[email protected]>

Diffstat:
MCrossmate/Services/AccountPushCoordinator.swift | 26++++++++++++++++++++++----
1 file changed, 22 insertions(+), 4 deletions(-)

diff --git a/Crossmate/Services/AccountPushCoordinator.swift b/Crossmate/Services/AccountPushCoordinator.swift @@ -17,6 +17,7 @@ final class AccountPushCoordinator { private static let accountPushSecretVersionDefaultsPrefix = "push.accountSecretVersion." static let accountJoinedPushKind = "accountJoined" static let accountSeenPushKind = "accountSeen" + private static let accountSeenCoalesceWindow: TimeInterval = 30 private let identity: AuthorIdentity private let preferences: PlayerPreferences @@ -27,6 +28,7 @@ final class AccountPushCoordinator { private var preferenceObservationTask: Task<Void, Never>? private var preferenceDebounceTask: Task<Void, Never>? + private var lastAccountSeenReadAt: [UUID: Date] = [:] init( identity: AuthorIdentity, @@ -323,17 +325,32 @@ final class AccountPushCoordinator { } func publishAccountSeenPush(gameID: UUID, readAt: Date) async { - await publishAccountEvent(kind: Self.accountSeenPushKind, gameID: gameID, readAt: readAt) + if shouldCoalesceAccountSeen(gameID: gameID, readAt: readAt) { + syncMonitor.note( + "push(accountSeen): coalesced \(gameID.uuidString.prefix(8)) " + + "readAt=\(readAt.ISO8601Format())" + ) + return + } + if await publishAccountEvent(kind: Self.accountSeenPushKind, gameID: gameID, readAt: readAt) { + lastAccountSeenReadAt[gameID] = readAt + } } - private func publishAccountEvent(kind: String, gameID: UUID, readAt: Date? = nil) async { + private func shouldCoalesceAccountSeen(gameID: UUID, readAt: Date) -> Bool { + guard let previous = lastAccountSeenReadAt[gameID] else { return false } + return abs(readAt.timeIntervalSince(previous)) <= Self.accountSeenCoalesceWindow + } + + @discardableResult + private func publishAccountEvent(kind: String, gameID: UUID, readAt: Date? = nil) async -> Bool { guard let pushClient else { syncMonitor.note("push(\(kind)): skipped (no pushClient)") - return + return false } guard let authorID = identity.currentID, !authorID.isEmpty else { syncMonitor.note("push(\(kind)): skipped (no authorID)") - return + return false } let address = ensureAccountPushAddress(authorID: authorID) await pushClient.publishAccountEvent( @@ -342,5 +359,6 @@ final class AccountPushCoordinator { address: address, readAt: readAt ) + return true } }