import Foundation /// Use both memory and disk storage. Try on memory first. public final class HybridStorage { public let memoryStorage: MemoryStorage public let diskStorage: DiskStorage private(set) var storageObservations = [UUID: (HybridStorage, StorageChange) -> Void]() private(set) var keyObservations = [String: (HybridStorage, KeyChange) -> Void]() public init(memoryStorage: MemoryStorage, diskStorage: DiskStorage) { self.memoryStorage = memoryStorage self.diskStorage = diskStorage diskStorage.onRemove = { [weak self] path in self?.handleRemovedObject(at: path) } } private func handleRemovedObject(at path: String) { notifyObserver(about: .remove) { key in let fileName = diskStorage.makeFileName(for: key) return path.contains(fileName) } } } extension HybridStorage: StorageAware { public func entry(forKey key: String) throws -> Entry { do { return try memoryStorage.entry(forKey: key) } catch { let entry = try diskStorage.entry(forKey: key) // set back to memoryStorage memoryStorage.setObject(entry.object, forKey: key, expiry: entry.expiry) return entry } } public func removeObject(forKey key: String) throws { memoryStorage.removeObject(forKey: key) try diskStorage.removeObject(forKey: key) notifyStorageObservers(about: .remove(key: key)) } public func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws { var keyChange: KeyChange? if keyObservations[key] != nil { keyChange = .edit(before: try? self.object(forKey: key), after: object) } memoryStorage.setObject(object, forKey: key, expiry: expiry) try diskStorage.setObject(object, forKey: key, expiry: expiry) if let change = keyChange { notifyObserver(forKey: key, about: change) } notifyStorageObservers(about: .add(key: key)) } public func removeAll() throws { memoryStorage.removeAll() try diskStorage.removeAll() notifyStorageObservers(about: .removeAll) notifyKeyObservers(about: .remove) } public func removeExpiredObjects() throws { memoryStorage.removeExpiredObjects() try diskStorage.removeExpiredObjects() notifyStorageObservers(about: .removeExpired) } } public extension HybridStorage { func transform(transformer: Transformer) -> HybridStorage { let storage = HybridStorage( memoryStorage: memoryStorage.transform(), diskStorage: diskStorage.transform(transformer: transformer) ) return storage } } extension HybridStorage: StorageObservationRegistry { @discardableResult public func addStorageObserver( _ observer: O, closure: @escaping (O, HybridStorage, StorageChange) -> Void ) -> ObservationToken { let id = UUID() storageObservations[id] = { [weak self, weak observer] storage, change in guard let observer = observer else { self?.storageObservations.removeValue(forKey: id) return } closure(observer, storage, change) } return ObservationToken { [weak self] in self?.storageObservations.removeValue(forKey: id) } } public func removeAllStorageObservers() { storageObservations.removeAll() } private func notifyStorageObservers(about change: StorageChange) { storageObservations.values.forEach { closure in closure(self, change) } } } extension HybridStorage: KeyObservationRegistry { @discardableResult public func addObserver( _ observer: O, forKey key: String, closure: @escaping (O, HybridStorage, KeyChange) -> Void ) -> ObservationToken { keyObservations[key] = { [weak self, weak observer] storage, change in guard let observer = observer else { self?.removeObserver(forKey: key) return } closure(observer, storage, change) } return ObservationToken { [weak self] in self?.keyObservations.removeValue(forKey: key) } } public func removeObserver(forKey key: String) { keyObservations.removeValue(forKey: key) } public func removeAllKeyObservers() { keyObservations.removeAll() } private func notifyObserver(forKey key: String, about change: KeyChange) { keyObservations[key]?(self, change) } private func notifyObserver(about change: KeyChange, whereKey closure: ((String) -> Bool)) { let observation = keyObservations.first { key, _ in closure(key) }?.value observation?(self, change) } private func notifyKeyObservers(about change: KeyChange) { keyObservations.values.forEach { closure in closure(self, change) } } }