| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- // LoopFollow
- // StorageReadiness.swift
- import Combine
- import Foundation
- /// A cached value that can re-read itself from disk after a BFU launch.
- protocol BFUReloadable: AnyObject {
- func reload()
- }
- /// Before-First-Unlock (BFU) storage recovery + the app-wide readiness gate.
- ///
- /// On a launch before the device's first unlock since boot, `UserDefaults` is
- /// encrypted, so every `StorageValue`/`SecureStorageValue` initialises to its
- /// default. This distinguishes true BFU from an ordinary locked launch (where the
- /// store reads fine), gates the UI until storage is trustworthy, hydrates every
- /// registered value once readable, and suppresses writes meanwhile so poisoned
- /// defaults can't be flushed over real settings on unlock.
- ///
- /// All mutators run on the main thread (launch + the protected-data/foreground
- /// notifications); `register(_:)` runs during `Storage` init. So no locking.
- enum StorageReadiness {
- /// Published gate; `false` until storage is known-good. The root shows a
- /// storage-free loading view until `true`.
- static let ready = ObservableValue<Bool>(default: false)
- /// True for the duration of a suspected true-BFU launch, until hydration runs.
- private(set) static var suspectBFU = false
- /// While true, storage values keep writes in memory only (hydration reconciles
- /// from disk). Off-main reads see a benign eventually-consistent Bool snapshot.
- static var isSuppressingWrites: Bool { suspectBFU && !ready.value }
- /// Debug surface for any writer active during the suspect window — such a write
- /// is memory-only and lost if it was needed on disk. None exist today.
- static func noteSuppressedWrite(key: String) {
- #if DEBUG
- LogManager.shared.log(category: .general, message: "Storage write to '\(key)' suppressed during BFU window")
- #endif
- }
- // MARK: - Sentinel
- private static let markerKey = "storageReadinessMarker"
- /// Present once storage has been confirmed readable. Absent in true BFU (store
- /// encrypted) — presence, not a value, so it can't drift.
- static var markerExists: Bool {
- UserDefaults.standard.object(forKey: markerKey) != nil
- }
- // MARK: - Registry
- private struct WeakReloadable {
- weak var ref: BFUReloadable?
- }
- private static var registry: [WeakReloadable] = []
- /// Called by every storage value at init, so hydration covers all by construction.
- static func register(_ reloadable: BFUReloadable) {
- registry.append(WeakReloadable(ref: reloadable))
- }
- // MARK: - Deferred work
- private static var pending: [() -> Void] = []
- /// Run `block` now if ready, else once ready. For launch work that reads real
- /// config or mutates persisted history (telemetry, network).
- static func whenReady(_ block: @escaping () -> Void) {
- if ready.value {
- block()
- } else {
- pending.append(block)
- }
- }
- // MARK: - Lifecycle
- /// Call once at launch. A non-suspect launch is marked ready synchronously (the
- /// common path); a suspect launch stays gated until `recover()`.
- static func configure(suspectBFU: Bool) {
- self.suspectBFU = suspectBFU
- if !suspectBFU {
- markReady()
- }
- }
- /// Hydrate every registered value and open the gate. Idempotent; returns `true`
- /// only on the transition that did the work.
- @discardableResult
- static func recover() -> Bool {
- guard suspectBFU, !ready.value else { return false }
- registry.removeAll { $0.ref == nil } // drop dead weak slots
- for entry in registry {
- entry.ref?.reload()
- }
- markReady()
- return true
- }
- private static func markReady() {
- guard !ready.value else { return }
- // Clear suspect BEFORE publishing: @Published emits in willSet, so a
- // ready.$value subscriber (BLEManager reconnect) runs while ready is still
- // false — clearing first lets its persists land.
- suspectBFU = false
- // Store is readable here (proven by the probes, or by protected data
- // becoming available), and class-C UserDefaults is writable while readable.
- if !markerExists {
- UserDefaults.standard.set(true, forKey: markerKey)
- }
- ready.value = true
- let blocks = pending
- pending.removeAll()
- blocks.forEach { $0() }
- }
- }
|