import Foundation /// T-iOS-23 · Persistence seam for `SessionCore.UnreadLedger` watermarks. /// The ledger itself is persistence-agnostic (plan §7) — the App layer wires /// UserDefaults here. NON-SECRET UI state only (plan §5.3 split: Keychain = /// secrets, UserDefaults = prefs), same tier as `LastSessionStore`. protocol UnreadWatermarkStore: Sendable { func load() -> [UUID: Int] func save(_ watermarks: [UUID: Int]) } /// UserDefaults-backed implementation with an injectable suite for tests /// (mirrors `UserDefaultsLastSessionStore`). `@unchecked Sendable`: /// `UserDefaults` is documented thread-safe and this wrapper adds no mutable /// state of its own. struct UserDefaultsUnreadWatermarkStore: UnreadWatermarkStore, @unchecked Sendable { private static let key = "unreadWatermarks" private let defaults: UserDefaults init(defaults: UserDefaults = .standard) { self.defaults = defaults } func load() -> [UUID: Int] { guard let raw = defaults.dictionary(forKey: Self.key) as? [String: Int] else { return [:] } // Stored data crosses a storage boundary — validate every key; garbage // is dropped, never trusted (two case-variant spellings of one UUID // collapse via max, so this can never crash on duplicates). let pairs = raw.compactMap { key, value in UUID(uuidString: key).map { ($0, value) } } return Dictionary(pairs, uniquingKeysWith: max) } func save(_ watermarks: [UUID: Int]) { let plist = Dictionary( uniqueKeysWithValues: watermarks.map { ($0.key.uuidString, $0.value) } ) defaults.set(plist, forKey: Self.key) } }