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 automaticallyStartsBackgroundWork is true.
  • Triggers post-startup refreshes including refreshHistoricalDatasetIfNeeded() and refresh().

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:

  1. Prepare refresh state by persisting codex source corrections via prepareRefreshState().
  2. Determine refresh phases distinguishing startup vs. regular refreshes.
  3. Collect provider sets using enabledProvidersForBackgroundWork() and enabledProviders.
  4. 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: UsageStore aggregates all usage data, token costs, and provider status in one @Observable class, exposing only UI-relevant properties while caching heavy data with @ObservationIgnored.
  • Settings-driven reactivity: Automatic observation of SettingsStore changes 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: ProviderRefreshCoordinator prevents overlapping network requests while TaskGroup parallelism 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →