// The last good feed, and the last detail of the 30 threads saved most recently, saved as JSON in // the app's caches folder. Launch shows them at once while the first refresh runs. Signing out clears // them, and demo mode never reads or writes them. import Foundation struct FeedCache: Sendable { let directory: URL /// How many thread details are kept. Saving one more deletes the oldest. var threadLimit = 30 static let standard = FeedCache(directory: .cachesDirectory.appending(path: "WristCode", directoryHint: .isDirectory)) func feed() -> Feed? { read(Feed.self, "feed.json") } func save(_ feed: Feed) { write(feed, "feed.json") } func thread(hostId: String, threadId: String) -> ThreadDetail? { read(ThreadDetail.self, Self.threadFile(hostId, threadId)) } func save(_ detail: ThreadDetail) { write(detail, Self.threadFile(detail.item.hostId, detail.item.threadId)) pruneThreads() } /// Deletes the thread details saved longest ago, past `threadLimit`. private func pruneThreads() { let fileManager = FileManager.default let key = URLResourceKey.contentModificationDateKey guard let files = try? fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [key]) else { return } let threads = files.filter { $0.lastPathComponent.hasPrefix("thread-") } guard threads.count > threadLimit else { return } let modified = { (url: URL) in (try? url.resourceValues(forKeys: [key]).contentModificationDate) ?? .distantPast } for url in threads.sorted(by: { modified($0) > modified($1) }).dropFirst(threadLimit) { try? fileManager.removeItem(at: url) } } /// Deletes everything saved, as when the watch unpairs. func clear() { try? FileManager.default.removeItem(at: directory) } /// `thread--.json`, with the ids percent-encoded so any id is a safe file name. private static func threadFile(_ hostId: String, _ threadId: String) -> String { let safe = { (id: String) in id.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? id } return "thread-\(safe(hostId))-\(safe(threadId)).json" } private func read(_ type: Value.Type, _ name: String) -> Value? { guard let data = try? Data(contentsOf: directory.appending(path: name)) else { return nil } return try? JSON.decoder().decode(type, from: data) } private func write(_ value: some Encodable, _ name: String) { guard let data = try? JSON.encoder().encode(value) else { return } try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) try? data.write(to: directory.appending(path: name), options: .atomic) } }