mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Keep a fenced feed refresh from stranding the loading flag
The refresh captured its account generation before hopping onto the detached task it runs in. That hop is a suspension point: a context handler running in the gap - the wake-time status flap bumps the generation there - left the request carrying a stale generation. Its completion was then fenced out, and the fenced return deliberately leaves isLoading alone, so nothing current owned the flag: it stayed true, every later refresh skipped itself as already loading, and the retry paths only watch for errors - the feed sat on a spinner nothing would ever finish. The generation and context are now read at dispatch time, so the request that claims isLoading always carries the generation current at that moment and its completion is the one that releases it. The stateless-server poller's terminal writes get the same fence: a stale poller must not hand a newer request's spinner an early ready.
This commit is contained in:
@@ -389,21 +389,33 @@ final class SubscriptionFeedCache {
|
||||
return
|
||||
}
|
||||
|
||||
let generation = beginRequest()
|
||||
|
||||
// The *effective* context, so an account still waiting on its instance
|
||||
// or credential falls back to local rather than reporting
|
||||
// notAuthenticated while the subscriptions list already shows local data.
|
||||
let context = appEnvironment.dataContextValidator.effectiveContext
|
||||
let backend = appEnvironment.feedBackend(for: context)
|
||||
LoggingService.shared.debug("Starting feed refresh for \(backend)", category: .general)
|
||||
|
||||
// Use a detached task to prevent SwiftUI from cancelling the request
|
||||
// when @State properties change during the refresh operation.
|
||||
// This is necessary because loadSubscriptionsAsync() updates @State
|
||||
// before feedCache.refresh() completes, which can cause SwiftUI to
|
||||
// cancel the parent task.
|
||||
//
|
||||
// The generation and context are read *inside* the detached body, not
|
||||
// before it: the hop onto it is a suspension point, and a context
|
||||
// handler running in that gap bumps the generation and resets
|
||||
// `isLoading`. A request dispatched with the pre-hop generation is
|
||||
// fenced out on arrival, and the fenced return deliberately leaves
|
||||
// `isLoading` alone — with nothing current owning it, the flag stayed
|
||||
// true and every later refresh was skipped as "already loading",
|
||||
// leaving a spinner nothing would ever finish. Reading at dispatch
|
||||
// time means the request that sets `isLoading` always carries the
|
||||
// generation that is current at that moment, so its completion is the
|
||||
// one that resets it.
|
||||
await Task.detached { @MainActor in
|
||||
let generation = self.beginRequest()
|
||||
|
||||
// The *effective* context, so an account still waiting on its instance
|
||||
// or credential falls back to local rather than reporting
|
||||
// notAuthenticated while the subscriptions list already shows local data.
|
||||
let context = appEnvironment.dataContextValidator.effectiveContext
|
||||
let backend = appEnvironment.feedBackend(for: context)
|
||||
LoggingService.shared.debug("Starting feed refresh for \(backend)", category: .general)
|
||||
|
||||
switch backend {
|
||||
case .local:
|
||||
await self.refreshLocalAccount(using: appEnvironment, generation: generation)
|
||||
@@ -783,6 +795,7 @@ final class SubscriptionFeedCache {
|
||||
appEnvironment.dataManager.updateLastVideoPublishedDates(from: serverVideos)
|
||||
prefetchDeArrow(for: serverVideos, using: appEnvironment)
|
||||
}
|
||||
guard generation == accountGeneration else { return }
|
||||
feedLoadState = .ready
|
||||
isLoading = false
|
||||
return
|
||||
@@ -818,7 +831,11 @@ final class SubscriptionFeedCache {
|
||||
)
|
||||
}
|
||||
|
||||
// Show whatever we have
|
||||
// Show whatever we have — unless the login this poller was working for
|
||||
// has been fenced out meanwhile: then the state belongs to whatever
|
||||
// request the switch started, and a stale ".ready" here would end its
|
||||
// spinner early or let a duplicate refresh through.
|
||||
guard generation == accountGeneration else { return }
|
||||
feedLoadState = .ready
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user