How UsageStore Manages State and Refresh Cycles in CodexBar
The UsageStore class serves as the central @Observable hub that aggregates usage data, token costs, and provider status while orchestrating multi-stage refresh cycles through adaptive timers and coalesced network requests.
CodexBar's menu-bar UI, CLI commands, and background workers rely on a single source of truth for all provider data. The UsageStore, implemented in Sources/CodexBar/UsageStore.swift, leverages Swift's Observation framework to manage state while coordinating complex background refresh cycles. Understanding how this store handles state aggregation, adapts to system conditions, and prevents overlapping requests reveals the architecture behind the app's real-time usage tracking.
Core State Architecture with @Observable
The UsageStore class is marked with @Observable to enable SwiftUI reactivity, storing plain values for UI-relevant properties while isolating heavy caches with @ObservationIgnored. At initialization, the store creates dictionaries to hold snapshots for every supported provider.
According to the source in [UsageStore.swift](https://github.com/steipete/CodexBar/blob/main/Sources/CodexBar/UsageStore.swift#L42-L78), the core state includes:
@Observable
final class UsageStore {
var snapshots: [UsageProvider: UsageSnapshot] = [:] // Latest usage bar data
var errors: [UsageProvider: String] = [:] // Fetch errors
var knownLimitsAvailabilityByProvider: [UsageProvider: UsageLimitsAvailability] = [:]
var lastSourceLabels: [UsageProvider: String] = [:]
var lastFetchAttempts: [UsageProvider: [ProviderFetchAttempt]] = [:]
var tokenSnapshots: [UsageProvider: CostUsageTokenSnapshot] = [:]
var tokenErrors: [UsageProvider: String] = [:]
var credits: CreditsSnapshot? // Codex credits
var statuses: [UsageProvider: ProviderStatus] = [:] // Provider status pages
var isRefreshing = false
var refreshingProviders: Set<UsageProvider> = []
// Additional derived caches marked with @ObservationIgnored
}
Properties that should not trigger UI updates—such as background tasks and in-memory caches—are wrapped with @ObservationIgnored (lines 80-120 in the same file). This design ensures that large dataset updates do not cause unnecessary SwiftUI view recomputations.
Observing Settings Changes
The store automatically binds to SettingsStore changes during initialization. The bindSettings() method (lines 115-124) invokes observeSettingsChanges(), which uses withObservationTracking to detect any modifications to user preferences.
When settings change, the store:
- Rebinds the observation listener recursively.
- Invalidates the provider-availability cache via
invalidateProviderAvailabilityCache(). - Clears probe logs.
- Restarts background timers if
automaticallyStartsBackgroundWorkis true. - Triggers post-startup refreshes including
refreshHistoricalDatasetIfNeeded()andrefresh().
This reactive binding ensures that modifying the refresh frequency or enabling new providers immediately updates the store's behavior without requiring manual restarts.
Timer Creation: Fixed vs Adaptive Refresh
The startTimer(preservingResetBoundaryRefresh:) method creates a background Task that sleeps and calls refresh() on a schedule. As implemented in [UsageStore.swift](https://github.com/steipete/CodexBar/blob/main/Sources/CodexBar/UsageStore.swift#L63-L95), the store supports both fixed intervals and an adaptive mode:
private func startTimer(preservingResetBoundaryRefresh: Bool = false) {
self.timerTask?.cancel()
self.adaptiveRefreshScheduledAt = nil
let frequency = self.settings.refreshFrequency
guard frequency != .manual else { return }
if frequency == .adaptive {
// Adaptive loop computes fresh delay each iteration
self.timerTask = Task.detached(priority: .utility) { [weak self] in
while !Task.isCancelled {
guard let sleepDuration = await Self.nextAdaptiveTimerSleepDuration(for: self) else { return }
try? await Task.sleep(for: sleepDuration)
await self?.refresh()
}
}
return
}
// Fixed-interval path
guard let wait = frequency.seconds else { return }
self.timerTask = Task.detached(priority: .utility) { [weak self] in
while !Task.isCancelled {
let sleepDuration = await self?.effectiveTimerSleepDuration(.seconds(wait)) ?? .seconds(wait)
try? await Task.sleep(for: sleepDuration)
await self?.refresh()
}
}
}
Adaptive Refresh Policy Logic
When the user selects adaptive refresh frequency, the store consults AdaptiveRefreshPolicy to determine the next sleep interval based on power state, thermal state, and recent menu activity.
The decision logic resides in UsageStore+AdaptiveRefresh.swift:
static func adaptiveRefreshDecision(
now: Date,
lastMenuOpenAt: Date?,
lowPowerModeEnabled: Bool,
thermalState: ProcessInfo.ThermalState,
policy: AdaptiveRefreshPolicy = AdaptiveRefreshPolicy()
) -> AdaptiveRefreshPolicy.Decision {
policy.nextDelay(for: AdaptiveRefreshPolicy.Input(
now: now,
lastMenuOpenAt: lastMenuOpenAt,
lowPowerModeEnabled: lowPowerModeEnabled,
thermalState: thermalState))
}
The computed delay is logged via logAdaptiveRefreshDecision and can be overridden in tests via refreshTimerSleepOverrideForTesting (lines 44-58).
The Multi-Stage Refresh Cycle
The public entry point refresh(forceTokenUsage:) forwards to runRefresh, which orchestrates several sub-steps. As defined in [UsageStore.swift](https://github.com/steipete/CodexBar/blob/main/Sources/CodexBar/UsageStore.swift#L88-L105), the method guards against concurrent refreshes using isRefreshing:
func refresh(forceTokenUsage: Bool = false) async {
await self.runRefresh(forceTokenUsage: forceTokenUsage, startupConnectivityRetryAttempt: nil)
}
The runRefresh method (lines 115-178) performs the following sequence:
- Prepare refresh state by persisting codex source corrections via
prepareRefreshState(). - Determine refresh phases distinguishing startup vs. regular refreshes.
- Collect provider sets using
enabledProvidersForBackgroundWork()andenabledProviders. - Schedule ancillary work including storage-footprint checks, token refreshes, and OpenAI web scraping.
Provider-Level Refresh Coordination
Each provider refresh executes inside a TaskGroup to enable parallel fetching. According to [UsageStore.swift](https://github.com/steipete/CodexBar/blob/main/Sources/CodexBar/UsageStore.swift#L180-L210):
await withTaskGroup(of: Void.self) { group in
for provider in refreshProviders {
group.addTask { await self.refreshProvider(provider, ...) }
if availableRefreshProviders.contains(provider) {
group.addTask { await self.refreshProviderStatus(provider) }
}
}
}
The refreshProvider(_:coalesceIfRefreshing:) method in UsageStore+Refresh.swift utilizes ProviderRefreshCoordinator to coalesce overlapping requests, ensuring that if a background timer and manual refresh trigger simultaneously, only one network request executes per provider.
Applying Refresh Outcomes
After fetching, applyProviderRefreshOutcome (lines 242-306) updates state dictionaries and handles edge cases:
switch outcome.result {
case .success(let result):
// Store new UsageSnapshot with reset-backfill and quota handling
// Update token-cost snapshot if applicable
// Record source label and clear errors
// Notify provider runtime of success
case .failure(let error):
// Decide whether to surface error or preserve old snapshot
// Record error string and notify runtime of failure
}
This method also performs Claude-specific credential housekeeping, invalidating caches and resetting derived state after credential changes to prevent stale data.
Specialized Refresh Timers
Beyond the main refresh cycle, UsageStore maintains separate timers for token-cost data and reset-boundary detection.
Token-Cost Refresh Cycle
Token-cost data refreshes on an independent schedule (defaulting to 1 hour) via startTokenTimer() in [UsageStore.swift](https://github.com/steipete/CodexBar/blob/main/Sources/CodexBar/UsageStore.swift#L200-L213):
private func startTokenTimer() {
self.tokenTimerTask?.cancel()
let wait = self.tokenFetchTTL // 1 hour (configurable)
self.tokenTimerTask = Task.detached(priority: .utility) { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(wait))
await self?.scheduleTokenRefresh(force: false)
}
}
}
The scheduleTokenRefresh method creates a sequenced task that walks through enabledProvidersForBackgroundWork(), invoking refreshTokenUsage(provider:force:) for each provider in order (lines 215-260).
Reset-Boundary Refresh
To capture usage resets that occur at minute boundaries, the store implements a reset-boundary refresh that triggers a short-lived extra refresh shortly after the regular cycle. The constants are defined at the top of UsageStore.swift (lines 13-15):
static let resetBoundaryRefreshGraceSeconds: TimeInterval = 30
static let resetBoundaryRefreshMinimumDelaySeconds: TimeInterval = 5
The scheduleResetBoundaryRefreshIfNeeded method schedules a task that sleeps for 30 seconds (or a minimum of 5 seconds) before triggering a lightweight refresh(forceTokenUsage: false) limited to providers likely to have crossed a quota reset. This logic resides in UsageStore+ResetBoundaryRefresh.swift.
Concurrency Safety and Error Handling
All public entry points to UsageStore are marked @MainActor, ensuring thread-safe state updates. Background work runs off-main using Task.detached(priority: .utility) and the ProviderRefreshCoordinator prevents overlapping provider refreshes.
The store distinguishes between transient network failures, credential-change events, and permanent quota unavailability. When a fetch fails, applyProviderRefreshOutcome intelligently decides whether to preserve the prior snapshot or surface the error to the UI, maintaining stability during intermittent connectivity issues.
Integrating UsageStore in SwiftUI
To use the store in a SwiftUI view, initialize it with dependencies and trigger refreshes manually when needed:
import SwiftUI
import CodexBar
struct ContentView: View {
@StateObject private var usageStore = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(),
settings: SettingsStore.shared
)
var body: some View {
VStack {
if let snap = usageStore.preferredSnapshot {
Text("Used: \(snap.primary?.usedPercent ?? 0)%")
} else {
Text("Loading…")
}
}
.onAppear {
Task { await usageStore.refresh() }
}
}
}
To manually trigger an adaptive refresh or notify the store of user activity:
Task {
// Force full refresh including token-cost tables
await usageStore.refresh(forceTokenUsage: true)
}
// Notify store of menu interaction to adjust adaptive timing
usageStore.noteMenuOpened()
Summary
- Single observable source:
UsageStoreaggregates all usage data, token costs, and provider status in one@Observableclass, exposing only UI-relevant properties while caching heavy data with@ObservationIgnored. - Settings-driven reactivity: Automatic observation of
SettingsStorechanges invalidates caches and restarts timers without manual intervention. - Adaptive timing: The store adjusts refresh intervals based on power state, thermal conditions, and user interaction patterns via
AdaptiveRefreshPolicy. - Coalesced refresh pipeline:
ProviderRefreshCoordinatorprevents overlapping network requests whileTaskGroupparallelism maximizes efficiency. - Robust error handling: The store preserves snapshots during transient failures and performs provider-specific cleanup (such as Claude credential invalidation) when authentication changes.
Frequently Asked Questions
How does UsageStore prevent multiple simultaneous refresh requests?
The store uses a boolean isRefreshing flag checked at the start of runRefresh() to prevent concurrent full cycles. Additionally, the ProviderRefreshCoordinator coalesces overlapping requests for individual providers, ensuring that if a background timer and manual refresh trigger simultaneously, only one network request executes per provider.
What triggers an adaptive refresh cycle adjustment?
The AdaptiveRefreshPolicy recalculates the next sleep duration based on four signals: the current time, the last menu open timestamp (lastMenuOpenAt), Low Power Mode status, and the current thermal state. Calling noteMenuOpened() records user interaction and may shorten the next refresh interval to ensure fresh data is available when the user is actively checking the menu.
How does the store handle token-cost data differently from usage snapshots?
Token-cost data refreshes on a separate timer (tokenTimerTask) with a default 1-hour TTL, managed by startTokenTimer(). Unlike regular usage snapshots that refresh frequently (every few minutes), token costs are fetched sequentially via scheduleTokenRefresh() to avoid overwhelming API endpoints, while usage snapshots utilize parallel TaskGroup processing.
Where does UsageStore handle errors without breaking the UI?
Error handling occurs primarily in applyProviderRefreshOutcome() within UsageStore+Refresh.swift. The method distinguishes between transient failures (preserving the previous snapshot) and permanent errors (surfacing messages to the UI). It also handles provider-specific logic such as clearing Claude OAuth caches when credential changes are detected, ensuring the UI never displays stale authentication errors.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →