mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-10 01:29:41 +00:00
fix(menubar): recover from stuck loading state
Commit 5800179 (multi-day calendar) accidentally removed stuck-loading
recovery. Re-add an 8-second watchdog loop in MenuBarContent that calls
recoverFromStuckLoading(). Change inFlightKeys from Set to timestamped
Dict so the watchdog can detect orphaned quiet-refresh entries. Stop
nuking payloadRefreshGeneration in the watchdog (it was killing healthy
in-flight fetches). Bump Package.swift platform to macOS 15 to match
NSAttributedString(attachment:) usage.
This commit is contained in:
parent
ce043631ed
commit
ac35746209
3 changed files with 38 additions and 13 deletions
|
|
@ -4,7 +4,7 @@ import PackageDescription
|
|||
let package = Package(
|
||||
name: "CodeBurnMenubar",
|
||||
platforms: [
|
||||
.macOS(.v14)
|
||||
.macOS(.v15)
|
||||
],
|
||||
products: [
|
||||
.executable(name: "CodeBurnMenubar", targets: ["CodeBurnMenubar"])
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ final class AppStore {
|
|||
}
|
||||
}
|
||||
|
||||
private var inFlightKeys: Set<PayloadCacheKey> = []
|
||||
private var inFlightKeys: [PayloadCacheKey: Date] = [:]
|
||||
|
||||
func resetLoadingState() {
|
||||
payloadRefreshGeneration &+= 1
|
||||
|
|
@ -333,22 +333,30 @@ final class AppStore {
|
|||
@discardableResult
|
||||
func clearStaleLoadingIfNeeded() -> Bool {
|
||||
let now = Date()
|
||||
let staleEntries = loadingStartedAtByKey.filter {
|
||||
let staleLoading = loadingStartedAtByKey.filter {
|
||||
now.timeIntervalSince($0.value) > loadingWatchdogSeconds
|
||||
}
|
||||
guard !staleEntries.isEmpty else { return false }
|
||||
let staleInFlight = inFlightKeys.filter { (key, insertedAt) in
|
||||
now.timeIntervalSince(insertedAt) > loadingWatchdogSeconds &&
|
||||
loadingStartedAtByKey[key] == nil
|
||||
}
|
||||
guard !staleLoading.isEmpty || !staleInFlight.isEmpty else { return false }
|
||||
|
||||
payloadRefreshGeneration &+= 1
|
||||
for (key, started) in staleEntries {
|
||||
for (key, started) in staleLoading {
|
||||
NSLog("CodeBurn: loading stuck for %ds on %@/%@ — auto-clearing",
|
||||
Int(now.timeIntervalSince(started)), key.label, key.provider.rawValue)
|
||||
loadingCountsByKey[key] = nil
|
||||
loadingStartedAtByKey[key] = nil
|
||||
inFlightKeys.remove(key)
|
||||
inFlightKeys[key] = nil
|
||||
if cache[key] == nil {
|
||||
lastErrorByKey[key] = "Refresh took longer than expected. CodeBurn will keep retrying in the background."
|
||||
}
|
||||
}
|
||||
for (key, insertedAt) in staleInFlight {
|
||||
NSLog("CodeBurn: orphaned in-flight key stuck for %ds on %@/%@ — clearing",
|
||||
Int(now.timeIntervalSince(insertedAt)), key.label, key.provider.rawValue)
|
||||
inFlightKeys[key] = nil
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -394,6 +402,11 @@ final class AppStore {
|
|||
cache.removeAll()
|
||||
}
|
||||
|
||||
func recoverFromStuckLoading() async {
|
||||
resetLoadingState()
|
||||
await refresh(key: currentKey, includeOptimize: false, force: true, showLoading: true)
|
||||
}
|
||||
|
||||
func refresh(includeOptimize: Bool, force: Bool = false, showLoading: Bool = false) async {
|
||||
await refresh(key: currentKey, includeOptimize: includeOptimize, force: force, showLoading: showLoading)
|
||||
}
|
||||
|
|
@ -404,8 +417,8 @@ final class AppStore {
|
|||
let generationAtStart = payloadRefreshGeneration
|
||||
if Task.isCancelled { return }
|
||||
if !force, cache[key]?.isFresh == true { return }
|
||||
if inFlightKeys.contains(key) { return }
|
||||
inFlightKeys.insert(key)
|
||||
if inFlightKeys[key] != nil { return }
|
||||
inFlightKeys[key] = Date()
|
||||
attemptedKeys.insert(key)
|
||||
lastErrorByKey[key] = nil
|
||||
let didShowLoading = showLoading || cache[key] == nil
|
||||
|
|
@ -425,7 +438,7 @@ final class AppStore {
|
|||
}
|
||||
defer {
|
||||
let abandonedAttempt = Task.isCancelled || generationAtStart != payloadRefreshGeneration
|
||||
inFlightKeys.remove(key)
|
||||
inFlightKeys[key] = nil
|
||||
if didShowLoading {
|
||||
finishLoading(for: key)
|
||||
}
|
||||
|
|
@ -496,8 +509,8 @@ final class AppStore {
|
|||
invalidateStaleDayCache()
|
||||
let key = PayloadCacheKey(period: period, provider: .all, day: day)
|
||||
if !force, cache[key]?.isFresh == true { return }
|
||||
if inFlightKeys.contains(key) { return }
|
||||
inFlightKeys.insert(key)
|
||||
if inFlightKeys[key] != nil { return }
|
||||
inFlightKeys[key] = Date()
|
||||
attemptedKeys.insert(key)
|
||||
let cacheDateAtStart = cacheDate
|
||||
let generationAtStart = payloadRefreshGeneration
|
||||
|
|
@ -505,7 +518,7 @@ final class AppStore {
|
|||
NSLog("CodeBurn: refreshing stale today status payload after %ds", age)
|
||||
}
|
||||
defer {
|
||||
inFlightKeys.remove(key)
|
||||
inFlightKeys[key] = nil
|
||||
}
|
||||
do {
|
||||
let fresh = try await DataClient.fetch(period: period, day: day, provider: .all, includeOptimize: false)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,18 @@ struct MenuBarContent: View {
|
|||
} else {
|
||||
BurnLoadingOverlay(periodLabel: store.selectionLabel)
|
||||
.transition(.opacity)
|
||||
.task {
|
||||
// Keep retrying until data loads or the view is removed.
|
||||
// The original one-shot recovery silently gave up if the
|
||||
// first attempt also stalled (generation mismatch, CLI
|
||||
// timeout, day rollover race). Looping guarantees the
|
||||
// user never sees a permanent spinner.
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(8))
|
||||
guard !Task.isCancelled, !store.hasCachedData else { return }
|
||||
await store.recoverFromStuckLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue