import Foundation public struct FileLogStore: LogStore { private struct Store: Codable { var runtime: [RuntimeLogEntry] = [] var connections: [ConnectionLogEntry] = [] } private let location: URL private let encoder: JSONEncoder private let decoder: JSONDecoder private let queue = DispatchQueue(label: "fun.chick.multi-simadmin.log-store") public init(location: URL) { self.location = location let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = [.sortedKeys] self.encoder = encoder let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 self.decoder = decoder } public func runtimeLogs() async throws -> [RuntimeLogEntry] { try await run { $0.runtime.sorted { $0.occurredAt > $1.occurredAt } } } public func connectionLogs() async throws -> [ConnectionLogEntry] { try await run { $0.connections.sorted { $0.observedAt > $1.observedAt } } } public func append( runtime: [RuntimeLogEntry], connections: [ConnectionLogEntry] ) async throws { try await run { store in var mergedRuntime = store.runtime for entry in runtime where !mergedRuntime.contains(where: { $0.id == entry.id }) { mergedRuntime.append(entry) } mergedRuntime.sort { $0.occurredAt > $1.occurredAt } store.runtime = Array(mergedRuntime.prefix(2_000)) var mergedConnections = store.connections for entry in connections where !mergedConnections.contains(where: { $0.id == entry.id }) { mergedConnections.append(entry) } mergedConnections.sort { $0.observedAt > $1.observedAt } store.connections = Array(mergedConnections.prefix(20_000)) } } public func pruneConnections(before cutoff: Date) async throws -> Int { try await run { store in let retained = store.connections.filter { $0.observedAt >= cutoff } let removed = store.connections.count - retained.count store.connections = retained return removed } } public func clearRuntimeLogs() async throws -> Int { try await run { store in let removed = store.runtime.count store.runtime = [] return removed } } private func run( _ transform: @escaping @Sendable (inout Store) throws -> T ) async throws -> T { try await withCheckedThrowingContinuation { continuation in queue.async { do { var store = try self.readStore() let result = try transform(&store) try self.writeStore(store) continuation.resume(returning: result) } catch { continuation.resume(throwing: error) } } } } private func readStore() throws -> Store { guard FileManager.default.fileExists(atPath: location.path) else { return Store() } do { return try decoder.decode(Store.self, from: Data(contentsOf: location)) } catch { return Store() } } private func writeStore(_ store: Store) throws { do { try FileManager.default.createDirectory( at: location.deletingLastPathComponent(), withIntermediateDirectories: true ) try encoder.encode(store).write(to: location, options: [.atomic]) } catch { throw RepositoryError.databaseFailed } } }