Fix menubar tab refresh recovery

This commit is contained in:
iamtoruk 2026-05-14 11:26:15 -07:00
parent 403efd4727
commit 00c55873a4
3 changed files with 38 additions and 16 deletions

View file

@ -100,8 +100,12 @@ final class AppStore {
staleInteractivePayloadAgeSeconds != nil
}
var hasMissingInteractivePayloadWithoutAttempt: Bool {
cache[currentKey] == nil && !isCurrentKeyLoading && !hasAttemptedCurrentKeyLoad
}
var shouldResetInteractiveRefreshPipeline: Bool {
hasStaleLoading || hasStaleInteractivePayload
hasStaleLoading || hasStaleInteractivePayload || hasMissingInteractivePayloadWithoutAttempt
}
var staleInteractivePayloadAgeSeconds: Int? {
@ -149,16 +153,7 @@ final class AppStore {
/// all-provider data in parallel so tab strip costs stay in sync with the hero.
func switchTo(period: Period) {
selectedPeriod = period
switchTask?.cancel()
switchTask = Task {
if selectedProvider == .all {
await refresh(includeOptimize: false, force: true)
} else {
async let main: Void = refresh(includeOptimize: false, force: true)
async let all: Void = refreshQuietly(period: period)
_ = await (main, all)
}
}
startInteractiveSelectionRefresh()
}
/// Switch to a provider filter. Cancels any in-flight switch so rapid tab tapping only
@ -166,13 +161,21 @@ final class AppStore {
/// in parallel so the tab strip costs stay in sync with the hero.
func switchTo(provider: ProviderFilter) {
selectedProvider = provider
startInteractiveSelectionRefresh()
}
private func startInteractiveSelectionRefresh() {
switchTask?.cancel()
resetLoadingState()
let period = selectedPeriod
let provider = selectedProvider
lastErrorByKey[PayloadCacheKey(period: period, provider: provider)] = nil
switchTask = Task {
if provider == .all {
await refresh(includeOptimize: false, force: true)
await refresh(includeOptimize: false, force: true, showLoading: true)
} else {
async let main: Void = refresh(includeOptimize: false, force: true)
async let all: Void = refreshQuietly(period: selectedPeriod)
async let main: Void = refresh(includeOptimize: false, force: true, showLoading: true)
async let all: Void = refreshQuietly(period: period)
_ = await (main, all)
}
}

View file

@ -259,10 +259,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
@discardableResult
private func clearStaleForceRefreshIfNeeded(now: Date = Date()) -> Bool {
if let started = forceRefreshStartedAt, forceRefreshTask != nil {
if forceRefreshTask != nil {
guard let started = forceRefreshStartedAt else {
NSLog("CodeBurn: force refresh task had no start timestamp - clearing")
forceRefreshTask?.cancel()
forceRefreshTask = nil
forceRefreshGeneration &+= 1
store.resetLoadingState()
return true
}
let elapsed = now.timeIntervalSince(started)
guard elapsed > forceRefreshWatchdogSeconds else { return false }
NSLog("CodeBurn: force refresh stuck for %ds — cancelling and restarting", Int(elapsed))
NSLog("CodeBurn: force refresh stuck for %ds - cancelling and restarting", Int(elapsed))
forceRefreshTask?.cancel()
forceRefreshTask = nil
forceRefreshStartedAt = nil

View file

@ -60,4 +60,15 @@ struct AppStoreRefreshRecoveryTests {
#expect(!store.hasStaleInteractivePayload)
#expect(!store.shouldResetInteractiveRefreshPipeline)
}
@Test("missing unattempted payload triggers hard recovery")
func missingUnattemptedPayloadTriggersHardRecovery() {
let store = AppStore()
#expect(!store.hasCachedData)
#expect(!store.hasAttemptedCurrentKeyLoad)
#expect(store.needsInteractivePayloadRefresh)
#expect(store.hasMissingInteractivePayloadWithoutAttempt)
#expect(store.shouldResetInteractiveRefreshPipeline)
}
}