48 lines
2.3 KiB
Swift
48 lines
2.3 KiB
Swift
import Foundation
|
|
import NucleicProtocol
|
|
|
|
/// The on-device snapshot of the phone's mesh-cast ledger (`CastLedger`) — the activity feed,
|
|
/// mesh messages, and runner-presence cards it has received, plus (implicitly) the cursors a
|
|
/// reconnect subscribes with. Like `SessionCache`, a *projection cache*, never canonical: a
|
|
/// live host always supersedes it; it exists so a relaunch renders the feed instantly and
|
|
/// resubscribes O(gap) instead of re-pulling every channel from zero.
|
|
///
|
|
/// One JSON file under Application Support (`NucleicCastCache/casts.json`) — the ledger's
|
|
/// per-channel retention already bounds it. Reads/writes hop onto a utility `Task.detached`.
|
|
enum CastCache {
|
|
private static var root: URL? {
|
|
guard let base = try? FileManager.default.url(
|
|
for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
|
else { return nil }
|
|
return base.appendingPathComponent("NucleicCastCache", isDirectory: true)
|
|
}
|
|
private static var castsURL: URL? { root?.appendingPathComponent("casts.json") }
|
|
|
|
/// Load the persisted ledger — called once at launch, before any host connects. The ledger's
|
|
/// init re-applies retention, so a policy change between builds re-caps on load.
|
|
static func loadLedger() -> CastLedger {
|
|
guard let url = castsURL, let data = try? Data(contentsOf: url),
|
|
let casts = try? JSONDecoder().decode([WireCast].self, from: data)
|
|
else { return CastLedger() }
|
|
return CastLedger(casts: casts)
|
|
}
|
|
|
|
/// Persist the ledger's snapshot. Off the main actor; callers debounce bursts.
|
|
static func save(_ ledger: CastLedger) async {
|
|
let casts = ledger.allCasts
|
|
await Task.detached(priority: .utility) {
|
|
guard let root, let url = castsURL else { return }
|
|
try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
if let data = try? JSONEncoder().encode(casts) {
|
|
try? data.write(to: url, options: .atomic)
|
|
}
|
|
}.value
|
|
}
|
|
|
|
/// Drop the cache — on full unpair (no Mac left whose casts this device should hold).
|
|
static func clear() {
|
|
guard let root else { return }
|
|
try? FileManager.default.removeItem(at: root)
|
|
}
|
|
}
|