import Foundation import PromiseKit import SwiftUI public protocol DiskCache { func value(for key: String) -> Promise func set(_ value: T, for key: String) -> Promise } private struct DiskCacheKey: EnvironmentKey { static let defaultValue = Current.diskCache } // also in AppEnvironment public extension EnvironmentValues { var diskCache: DiskCache { get { self[DiskCacheKey.self] } set { self[DiskCacheKey.self] = newValue } } } public final class DiskCacheImpl: DiskCache { public func value(for key: String) -> Promise { let (promise, seal) = Promise.pending() DispatchQueue.global().async { [coordinator, container] in var coordinatorError: NSError? coordinator.coordinate( readingItemAt: Self.URL(in: container, for: key), error: &coordinatorError ) { url in do { let data = try Data(contentsOf: url) let value = try JSONDecoder().decode(T.self, from: data) seal.fulfill(value) } catch { seal.reject(error) } } if let error = coordinatorError { seal.reject(error) } } return promise } public func set(_ value: some Codable, for key: String) -> Promise { let data: Data do { // the contents of the value may be unsafe off the thread this is called on // we can at least move the write operation itself off the thread data = try JSONEncoder().encode(value) } catch { return .init(error: error) } let (promise, seal) = Promise.pending() DispatchQueue.global().async { [coordinator, container] in var coordinatorError: NSError? coordinator.coordinate( writingItemAt: Self.URL(in: container, for: key), error: &coordinatorError ) { url in do { try data.write(to: url) seal.fulfill(()) } catch { seal.reject(error) } } if let error = coordinatorError { seal.reject(error) } } return promise } private class func URL(containerName: String) -> URL { let fileManager = FileManager.default let url = AppConstants.AppGroupContainer .appendingPathComponent("DiskCache", isDirectory: true) .appendingPathComponent(containerName, isDirectory: false) try? fileManager.createDirectory( at: url, withIntermediateDirectories: true, attributes: nil ) return url } var container: URL var coordinator: NSFileCoordinator init(containerName: String = "Default") { self.coordinator = NSFileCoordinator() self.container = Self.URL(containerName: containerName) } static func URL(in container: URL, for key: String) -> URL { let escapedKey = key.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? key return container.appendingPathComponent("\(escapedKey).json", isDirectory: false) } }