chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import Foundation
|
||||
|
||||
/// Canonical adaptive-refresh decision table shared by the app and offline replay tooling.
|
||||
/// Platform adapters normalize their thermal signals before calling this type; thresholds and
|
||||
/// delays live here only.
|
||||
package struct AdaptiveRefreshPolicyCore: Sendable {
|
||||
package struct Input: Sendable, Equatable {
|
||||
package let now: Date
|
||||
package let lastMenuOpenAt: Date?
|
||||
package let lowPowerModeEnabled: Bool
|
||||
package let thermalPressure: ThermalPressure
|
||||
|
||||
package init(
|
||||
now: Date,
|
||||
lastMenuOpenAt: Date?,
|
||||
lowPowerModeEnabled: Bool,
|
||||
thermalPressure: ThermalPressure)
|
||||
{
|
||||
self.now = now
|
||||
self.lastMenuOpenAt = lastMenuOpenAt
|
||||
self.lowPowerModeEnabled = lowPowerModeEnabled
|
||||
self.thermalPressure = thermalPressure
|
||||
}
|
||||
}
|
||||
|
||||
package enum ThermalPressure: Sendable, Equatable {
|
||||
case nominal
|
||||
case constrained
|
||||
}
|
||||
|
||||
package enum Reason: String, Sendable, Equatable {
|
||||
case recentInteraction
|
||||
case warm
|
||||
case idle
|
||||
case longIdle
|
||||
case constrained
|
||||
}
|
||||
|
||||
package struct Decision: Sendable, Equatable {
|
||||
package let delay: Duration
|
||||
package let reason: Reason
|
||||
|
||||
fileprivate init(delay: Duration, reason: Reason) {
|
||||
self.delay = delay
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
|
||||
private static let recentInteractionThreshold: TimeInterval = 5 * 60
|
||||
private static let warmThreshold: TimeInterval = 60 * 60
|
||||
private static let idleThreshold: TimeInterval = 4 * 60 * 60
|
||||
|
||||
private static let recentInteractionDelay: Duration = .seconds(2 * 60)
|
||||
private static let warmDelay: Duration = .seconds(5 * 60)
|
||||
private static let idleDelay: Duration = .seconds(15 * 60)
|
||||
private static let longIdleDelay: Duration = .seconds(30 * 60)
|
||||
private static let constrainedDelay: Duration = .seconds(30 * 60)
|
||||
|
||||
/// Representative cadence for consumers that need one interval but cannot access live state.
|
||||
package static let nominalIntervalForHeuristics: TimeInterval = 5 * 60
|
||||
|
||||
package init() {}
|
||||
|
||||
package func nextDelay(for input: Input) -> Decision {
|
||||
if input.lowPowerModeEnabled || input.thermalPressure == .constrained {
|
||||
return Decision(delay: Self.constrainedDelay, reason: .constrained)
|
||||
}
|
||||
|
||||
guard let lastMenuOpenAt = input.lastMenuOpenAt else {
|
||||
return Decision(delay: Self.longIdleDelay, reason: .longIdle)
|
||||
}
|
||||
|
||||
// A future or clock-adjusted timestamp yields a negative age, which reads as recent.
|
||||
let age = input.now.timeIntervalSince(lastMenuOpenAt)
|
||||
|
||||
if age <= Self.recentInteractionThreshold {
|
||||
return Decision(delay: Self.recentInteractionDelay, reason: .recentInteraction)
|
||||
}
|
||||
if age <= Self.warmThreshold {
|
||||
return Decision(delay: Self.warmDelay, reason: .warm)
|
||||
}
|
||||
if age < Self.idleThreshold {
|
||||
return Decision(delay: Self.idleDelay, reason: .idle)
|
||||
}
|
||||
return Decision(delay: Self.longIdleDelay, reason: .longIdle)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user