import Foundation /// Identifies a cached evaluation — FC-060 §5: "environment + subjectId + keys(sorted) + /// platform + appVersion". `sortedKeys` means caller `keys` order never affects hits. struct FeatureControlCacheKey: Hashable, Sendable { let environment: String let subjectId: String let sortedKeys: [String] let platform: String? let appVersion: String? } /// In-memory-only TTL cache (no disk persistence — see SDD §2 Non-Goals). The clock is /// injectable so tests can force expiry deterministically instead of sleeping. actor FeatureControlCache { private struct Entry { let snapshot: FeatureControlSnapshot let storedAt: Date let ttl: TimeInterval } private var entries: [FeatureControlCacheKey: Entry] = [:] private let now: @Sendable () -> Date init(now: @escaping @Sendable () -> Date = { Date() }) { self.now = now } /// `allowStale: true` returns an expired entry rather than `nil` — used by the /// safe-degrade fallback path (SDD §4.4/§4.5). func get(_ key: FeatureControlCacheKey, allowStale: Bool) -> FeatureControlSnapshot? { guard let entry = entries[key] else { return nil } let age = now().timeIntervalSince(entry.storedAt) if age <= entry.ttl { return entry.snapshot } return allowStale ? entry.snapshot : nil } /// Always overwrites — a fresh fetch's `configVersion` simply replaces whatever was there. func set(_ key: FeatureControlCacheKey, snapshot: FeatureControlSnapshot, ttl: TimeInterval) { entries[key] = Entry(snapshot: snapshot, storedAt: now(), ttl: ttl) } func invalidateAll() { entries.removeAll() } }