// Author: Simon-Pierre Boucher — contact@spboucher.ai // // Run registry with atomic persistence: every mutation writes // runs.json.tmp then renames — the app survives kill -9 at any moment with // either the old or the new registry, never a torn one. import Foundation @Observable final class RunStore { private(set) var runs: [Run] = [] let workspaceURL: URL private var registryURL: URL { workspaceURL.appendingPathComponent("runs.json") } init(workspace: URL) { workspaceURL = workspace load() } func load() { guard let data = try? Data(contentsOf: registryURL), let decoded = try? JSONDecoder().decode([Run].self, from: data) else { return } runs = decoded } func upsert(_ run: Run) { if let i = runs.firstIndex(where: { $0.id == run.id }) { runs[i] = run } else { runs.append(run) } persist() } func remove(_ run: Run) { runs.removeAll { $0.id == run.id } persist() } private func persist() { do { let enc = JSONEncoder() enc.outputFormatting = [.prettyPrinted, .sortedKeys] enc.dateEncodingStrategy = .iso8601 let data = try enc.encode(runs) let tmp = registryURL.appendingPathExtension("tmp") try FileManager.default .createDirectory(at: workspaceURL, withIntermediateDirectories: true) try data.write(to: tmp, options: .atomic) _ = try FileManager.default.replaceItemAt(registryURL, withItemAt: tmp) } catch { // Persistence failure must be visible, never silent: kept on the // store for the UI to surface. lastPersistError = error.localizedDescription } } private(set) var lastPersistError: String? }