Sources/Confidence/EventStorage.swift (124 lines of code) (raw):
import Foundation
import os
struct ConfidenceEvent: Codable {
let name: String
let payload: [String: ConfidenceValue]
let eventTime: Date
}
internal protocol EventStorage {
func startNewBatch() throws
func writeEvent(event: ConfidenceEvent) throws
func batchReadyIds() throws -> [String]
func eventsFrom(id: String) throws -> [ConfidenceEvent]
func remove(id: String) throws
}
internal class EventStorageImpl: EventStorage {
private let READYTOSENDEXTENSION = "READY"
private let storageQueue = DispatchQueue(label: "com.confidence.events.storage")
private var folderURL: URL
private var currentFileUrl: URL?
private var currentFileHandle: FileHandle?
init() throws {
self.folderURL = try EventStorageImpl.getFolderURL()
if !FileManager.default.fileExists(atPath: folderURL.backport.path) {
try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true)
}
try resetCurrentFile()
}
func startNewBatch() throws {
try storageQueue.sync {
guard let currentFileName = self.currentFileUrl else {
return
}
try currentFileHandle?.close()
try FileManager.default.moveItem(
at: currentFileName,
to: currentFileName.appendingPathExtension(READYTOSENDEXTENSION))
try resetCurrentFile()
}
}
func writeEvent(event: ConfidenceEvent) throws {
try storageQueue.sync {
guard let currentFileHandle = currentFileHandle else {
return
}
let encoder = JSONEncoder()
let serialied = try encoder.encode(event)
let delimiter = Data("\n".utf8)
currentFileHandle.seekToEndOfFile()
try currentFileHandle.write(contentsOf: delimiter)
try currentFileHandle.write(contentsOf: serialied)
}
}
func batchReadyIds() throws -> [String] {
try storageQueue.sync {
let fileUrls = try FileManager.default.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil)
return fileUrls.filter { url in
url.pathExtension == READYTOSENDEXTENSION
}
.map { url in
url.lastPathComponent
}
}
}
func eventsFrom(id: String) throws -> [ConfidenceEvent] {
try storageQueue.sync {
let decoder = JSONDecoder()
let fileUrl = folderURL.appendingPathComponent(id)
let data = try Data(contentsOf: fileUrl)
if let dataString = String(data: data, encoding: .utf8) {
return try dataString.components(separatedBy: "\n")
.filter { events in
!events.isEmpty
}
.compactMap { eventString in
guard let stringData = eventString.data(using: .utf8) else {
return nil
}
return try decoder.decode(ConfidenceEvent.self, from: stringData)
}
} else {
return []
}
}
}
func remove(id: String) throws {
try storageQueue.sync {
let fileUrl = folderURL.appendingPathComponent(id)
if FileManager.default.fileExists(atPath: fileUrl.path) {
try FileManager.default.removeItem(at: fileUrl)
}
}
}
private func getLastWritingFile() throws -> URL? {
let files = try FileManager.default.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil)
for fileUrl in files where fileUrl.pathExtension != READYTOSENDEXTENSION {
return fileUrl
}
return nil
}
private func resetCurrentFile() throws {
// Handling already existing file from previous session
if let currentFile = try getLastWritingFile() {
self.currentFileUrl = currentFile
self.currentFileHandle = try FileHandle(forWritingTo: currentFile)
} else {
// Create a brand new file
let fileUrl = folderURL.appendingPathComponent(String(UUID().uuidString))
FileManager.default.createFile(atPath: fileUrl.path, contents: nil)
self.currentFileUrl = fileUrl
self.currentFileHandle = try FileHandle(forWritingTo: fileUrl)
}
}
internal static func getFolderURL() throws -> URL {
guard
let applicationSupportUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
.last
else {
throw ConfidenceError.cacheError(message: "Could not get URL for application directory")
}
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
throw ConfidenceError.cacheError(message: "Unable to get bundle identifier")
}
return applicationSupportUrl.backport.appending(
components: "com.confidence.events.storage", "\(bundleIdentifier)", "events")
}
}