// // DigestCache.swift // Prisme // // Author: Simon-Pierre Boucher // import CryptoKit import Foundation /// Content-addressed digest cache (CLAUDE.md ยง4: a page is never distilled /// twice). Keys are hashes of the extracted text, so a reloaded page with /// identical content is a hit even if the URL differs. Digests are durable /// artefacts: kept in memory and on disk, ready to feed history and diffs. actor DigestCache { private struct Entry: Codable { let digest: PageDigest let blocks: [ContentBlock] } private var memory: [String: Entry] = [:] private let directory: URL init() { let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] directory = base.appendingPathComponent("Digests", isDirectory: true) try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) } static func key(for content: ExtractedContent) -> String { var hasher = SHA256() hasher.update(data: Data(content.title.utf8)) for block in content.blocks { hasher.update(data: Data(block.text.utf8)) } return hasher.finalize().map { String(format: "%02x", $0) }.joined() } func lookup(_ key: String) -> (PageDigest, [ContentBlock])? { if let entry = memory[key] { return (entry.digest, entry.blocks) } let file = directory.appendingPathComponent("\(key).json") guard let data = try? Data(contentsOf: file), let entry = try? JSONDecoder().decode(Entry.self, from: data) else { return nil } memory[key] = entry return (entry.digest, entry.blocks) } func store(_ digest: PageDigest, blocks: [ContentBlock], key: String) { let entry = Entry(digest: digest, blocks: blocks) memory[key] = entry if memory.count > 64 { // Cheap pressure valve; disk keeps the long tail. memory.removeValue(forKey: memory.keys.first!) } let file = directory.appendingPathComponent("\(key).json") if let data = try? JSONEncoder().encode(entry) { try? data.write(to: file, options: .atomic) } } }