fix: 用 FetchGate actor 修复并发触发丢失问题

This commit is contained in:
tigerenwork 2026-07-19 15:10:46 +08:00
parent b5ad9c1ec3
commit 55d0f86392
1 changed files with 47 additions and 5 deletions

View File

@ -11,8 +11,8 @@ final class PollerService {
private let configService = AppStateService.shared
private var timer: Timer?
private var isFetching = false
private var lastFetchTime: Date?
private let fetchGate = FetchGate()
private(set) var isRunning = false
@ -85,11 +85,53 @@ final class PollerService {
// MARK: - Fetch
private func performFetch() async {
guard !isFetching else { return }
isFetching = true
defer { isFetching = false }
/// Serializes fetches and coalesces concurrent triggers.
///
/// Previously a trigger arriving mid-fetch was silently dropped
/// (`guard !isFetching else { return }`). Rapid successive changes
/// e.g. an ABA reassignment producing two SSE wake-ups then
/// collapsed into a single fetch, and if that fetch sampled the final
/// state the transition was never observed and no notification fired.
/// Now a trigger arriving mid-fetch queues exactly one follow-up round,
/// so the final state is always observed. The actor also makes the
/// check-then-set race-safe (triggers arrive from the timer, the SSE
/// handler and the UI on different tasks).
private actor FetchGate {
private var fetching = false
private var pending = false
/// Returns true if the caller may start fetching; otherwise queues
/// a follow-up round and returns false.
func tryBegin() -> Bool {
if fetching {
pending = true
return false
}
fetching = true
return true
}
/// Ends one round. Returns true when another round was requested
/// while fetching; the gate stays held between rounds so no
/// parallel fetch can slip in.
func endRound() -> Bool {
if pending {
pending = false
return true
}
fetching = false
return false
}
}
private func performFetch() async {
guard await fetchGate.tryBegin() else { return }
repeat {
await fetchOnce()
} while await fetchGate.endRound()
}
private func fetchOnce() async {
lastFetchTime = Date()
await MainActor.run {