diff --git a/Account/Sources/Account/OPMLFile.swift b/Account/Sources/Account/OPMLFile.swift index e33fd0bb2..f84f1c179 100644 --- a/Account/Sources/Account/OPMLFile.swift +++ b/Account/Sources/Account/OPMLFile.swift @@ -18,21 +18,19 @@ import Core private let fileURL: URL private let account: Account - - private var isDirty = false { - didSet { - queueSaveToDiskIfNeeded() - } - } - private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5) + private let dataFile: DataFile init(filename: String, account: Account) { - self.fileURL = URL(fileURLWithPath: filename) + self.account = account + self.fileURL = URL(fileURLWithPath: filename) + self.dataFile = DataFile(fileURL: self.fileURL) + + self.dataFile.delegate = self } func markAsDirty() { - isDirty = true + dataFile.markAsDirty() } func load() { @@ -44,33 +42,15 @@ import Core account.loadOPMLItems(opmlItems) } } - + func save() { - guard !account.isDeleted else { return } - let opmlDocumentString = opmlDocument() - - do { - try opmlDocumentString.write(to: fileURL, atomically: true, encoding: .utf8) - } catch let error as NSError { - os_log(.error, log: log, "OPML save to disk failed: %@.", error.localizedDescription) - } + + dataFile.save() } - } private extension OPMLFile { - func queueSaveToDiskIfNeeded() { - saveQueue.add(self, #selector(saveToDiskIfNeeded)) - } - - @objc func saveToDiskIfNeeded() { - if isDirty { - isDirty = false - save() - } - } - func opmlFileData() -> Data? { var fileData: Data? = nil @@ -122,5 +102,29 @@ private extension OPMLFile { let opml = openingText + middleText + closingText return opml } - +} + +extension OPMLFile: DataFileDelegate { + + func data(for dataFile: DataFile) -> Data? { + + guard !account.isDeleted else { + return nil + } + + let opmlDocumentString = opmlDocument() + guard let data = opmlDocumentString.data(using: .utf8, allowLossyConversion: true) else { + + assertionFailure("OPML String conversion to Data failed.") + os_log(.error, log: log, "OPML String conversion to Data failed.") + return nil + } + + return data + } + + func dataFileWriteToDiskDidFail(for dataFile: DataFile, error: Error) { + + os_log(.error, log: log, "OPML save to disk failed: %@.", error.localizedDescription) + } } diff --git a/Core/Sources/Core/DataFile.swift b/Core/Sources/Core/DataFile.swift new file mode 100644 index 000000000..b9bf1c342 --- /dev/null +++ b/Core/Sources/Core/DataFile.swift @@ -0,0 +1,96 @@ +// +// DataFile.swift +// +// +// Created by Brent Simmons on 6/9/24. +// + +import Foundation +import os + +public protocol DataFileDelegate: AnyObject { + + @MainActor func data(for dataFile: DataFile) -> Data? + @MainActor func dataFileWriteToDiskDidFail(for dataFile: DataFile, error: Error) +} + +@MainActor public final class DataFile { + + public weak var delegate: DataFileDelegate? = nil + + private var isDirty = false { + didSet { + if isDirty { + restartTimer() + } + else { + invalidateTimer() + } + } + } + + private let fileURL: URL + private let saveInterval: TimeInterval = 1.0 + private var timer: Timer? + + public init(fileURL: URL) { + + self.fileURL = fileURL + } + + public func markAsDirty() { + + isDirty = true + } + + public func save() { + + assert(Thread.isMainThread) + isDirty = false + + guard let data = delegate?.data(for: self) else { + return + } + + do { + try data.write(to: fileURL) + } catch { + delegate?.dataFileWriteToDiskDidFail(for: self, error: error) + } + } +} + +private extension DataFile { + + func saveToDiskIfNeeded() { + + assert(Thread.isMainThread) + + if isDirty { + save() + } + } + + func restartTimer() { + + assert(Thread.isMainThread) + + invalidateTimer() + + timer = Timer.scheduledTimer(withTimeInterval: saveInterval, repeats: false) { timer in + MainActor.assumeIsolated { + self.saveToDiskIfNeeded() + } + } + } + + func invalidateTimer() { + + assert(Thread.isMainThread) + + if let timer, timer.isValid { + timer.invalidate() + } + timer = nil + } +} diff --git a/Web/Package.swift b/Web/Package.swift index 5c5fe5d02..7dfd2c16b 100644 --- a/Web/Package.swift +++ b/Web/Package.swift @@ -15,7 +15,6 @@ let package = Package( .target( name: "Web", dependencies: [], - resources: [.copy("UTS46/uts46")], swiftSettings: [ .define("SWIFT_PACKAGE"), .enableExperimentalFeature("StrictConcurrency")