fix: 用 FetchGate actor 修复并发触发丢失问题
This commit is contained in:
parent
b5ad9c1ec3
commit
55d0f86392
|
|
@ -11,8 +11,8 @@ final class PollerService {
|
||||||
private let configService = AppStateService.shared
|
private let configService = AppStateService.shared
|
||||||
|
|
||||||
private var timer: Timer?
|
private var timer: Timer?
|
||||||
private var isFetching = false
|
|
||||||
private var lastFetchTime: Date?
|
private var lastFetchTime: Date?
|
||||||
|
private let fetchGate = FetchGate()
|
||||||
|
|
||||||
private(set) var isRunning = false
|
private(set) var isRunning = false
|
||||||
|
|
||||||
|
|
@ -85,11 +85,53 @@ final class PollerService {
|
||||||
|
|
||||||
// MARK: - Fetch
|
// MARK: - Fetch
|
||||||
|
|
||||||
private func performFetch() async {
|
/// Serializes fetches and coalesces concurrent triggers.
|
||||||
guard !isFetching else { return }
|
///
|
||||||
isFetching = true
|
/// Previously a trigger arriving mid-fetch was silently dropped
|
||||||
defer { isFetching = false }
|
/// (`guard !isFetching else { return }`). Rapid successive changes —
|
||||||
|
/// e.g. an A→B→A 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()
|
lastFetchTime = Date()
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue