Files
multi-simadmin/apps/ios/Sources/MultiSimAdmin/Persistence/FileLogStore.swift
T

113 lines
3.7 KiB
Swift

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<T: Sendable>(
_ 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
}
}
}