81 lines · 4.1 KB · Swift
RawDownload
import Foundation /// Wiring around `AdaptiveRefreshPolicy` for `UsageStore.startTimer()`: gathering live signals,/// logging the resulting decision, and applying the DEBUG-only sleep-duration override used by/// tests. Split out of UsageStore.swift to keep that file's class body under the lint line limit.extension UsageStore {    func effectiveTimerSleepDuration(_ computed: Duration) -> Duration {        #if DEBUG        self.refreshTimerSleepOverrideForTesting ?? computed        #else        computed        #endif    }     /// Pure wiring helper: builds the `AdaptiveRefreshPolicy.Input` from explicit values and    /// returns the resulting decision. `startTimer()` supplies live `ProcessInfo` state and    /// `lastMenuOpenAt` at call time; this stays a plain, testable function of its arguments.    nonisolated 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))    }     nonisolated static func shouldAdvanceAdaptiveTimer(scheduledAt: Date?, candidate: Date) -> Bool {        guard let scheduledAt else { return true }        return candidate < scheduledAt    }     func logAdaptiveRefreshDecision(_ decision: AdaptiveRefreshPolicy.Decision) {        // Reason and delay only; never provider/account/email/path/credential/response data.        // No "adaptive refresh: " prefix — the adaptiveRefresh log category already identifies the source.        self.adaptiveRefreshLogger.debug(            "reason=\(decision.reason.rawValue) delay=\(decision.delay.components.seconds)s")    }     /// Computes this tick's adaptive sleep duration (and logs the decision) while briefly holding a    /// strong reference to `store`; returns nil once the store has deallocated, ending the loop.    /// Kept as a separate call so the strong reference doesn't extend into the caller's `Task.sleep`.    static func nextAdaptiveTimerSleepDuration(for store: UsageStore?) async -> Duration? {        guard let store else { return nil }        let now = Date()        let decision = Self.adaptiveRefreshDecision(            now: now,            lastMenuOpenAt: store.lastMenuOpenAt,            lowPowerModeEnabled: ProcessInfo.processInfo.isLowPowerModeEnabled,            thermalState: ProcessInfo.processInfo.thermalState)        store.adaptiveRefreshScheduledAt = now.addingTimeInterval(TimeInterval(decision.delay.components.seconds))        store.logAdaptiveRefreshDecision(decision)        return store.effectiveTimerSleepDuration(decision.delay)    }     /// The refresh interval scheduling *heuristics* (reset-boundary refresh, OpenAI web staleness,    /// persistent-CLI-session idle windows) should use as "how often does a normal refresh happen".    /// This is deliberately distinct from `RefreshFrequency.seconds`, which is nil for both `.manual`    /// (no timer at all — heuristics correctly get nil here too) and `.adaptive` (no *fixed*    /// interval, but ticks are still happening on a real, computable cadence). For `.adaptive`, this    /// resolves to what `AdaptiveRefreshPolicy` would decide right now from live signals, so those    /// heuristics stay active and roughly proportionate instead of silently behaving like manual.    func normalRefreshIntervalForHeuristics() -> TimeInterval? {        switch self.settings.refreshFrequency {        case .manual:            nil        case .adaptive:            TimeInterval(Self.adaptiveRefreshDecision(                now: Date(),                lastMenuOpenAt: self.lastMenuOpenAt,                lowPowerModeEnabled: ProcessInfo.processInfo.isLowPowerModeEnabled,                thermalState: ProcessInfo.processInfo.thermalState).delay.components.seconds)        default:            self.settings.refreshFrequency.seconds        }    }}