Skip to content

Commit

Permalink
Notify key observers when needed
Browse files Browse the repository at this point in the history
  • Loading branch information
vadymmarkov committed Aug 6, 2018
1 parent 5e53be9 commit a7e40e7
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 33 deletions.
8 changes: 0 additions & 8 deletions Cache.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,6 @@
D5A9D1B721134547005DBD3F /* ObservationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1B621134547005DBD3F /* ObservationToken.swift */; };
D5A9D1B821134547005DBD3F /* ObservationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1B621134547005DBD3F /* ObservationToken.swift */; };
D5A9D1B921134547005DBD3F /* ObservationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1B621134547005DBD3F /* ObservationToken.swift */; };
D5A9D1BF21134776005DBD3F /* StoreChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1BE21134776005DBD3F /* StoreChange.swift */; };
D5A9D1C021134776005DBD3F /* StoreChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1BE21134776005DBD3F /* StoreChange.swift */; };
D5A9D1C121134776005DBD3F /* StoreChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1BE21134776005DBD3F /* StoreChange.swift */; };
D5A9D1C321144B65005DBD3F /* StorageObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */; };
D5A9D1C421144B65005DBD3F /* StorageObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */; };
D5A9D1C521144B65005DBD3F /* StorageObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */; };
Expand Down Expand Up @@ -242,7 +239,6 @@
D5A138C01EB29BFA00881A20 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = "<group>"; };
D5A138C31EB29C2100881A20 /* NSImage+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSImage+Extensions.swift"; sourceTree = "<group>"; };
D5A9D1B621134547005DBD3F /* ObservationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationToken.swift; sourceTree = "<group>"; };
D5A9D1BE21134776005DBD3F /* StoreChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreChange.swift; sourceTree = "<group>"; };
D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageObservationRegistry.swift; sourceTree = "<group>"; };
D5DC59E01C20593E003BD79B /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EBAACA991FBC369300FA206E /* SimpleStorage.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = SimpleStorage.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
Expand Down Expand Up @@ -371,7 +367,6 @@
D270147F20D10982003B45C7 /* Storage.swift */,
D270148320D10E76003B45C7 /* AsyncStorage.swift */,
D270148720D11040003B45C7 /* Storage+Transform.swift */,
D5A9D1BE21134776005DBD3F /* StoreChange.swift */,
D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */,
D51146522118337500197DCE /* KeyObservationRegistry.swift */,
);
Expand Down Expand Up @@ -852,7 +847,6 @@
D28897071F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */,
D270148220D10982003B45C7 /* Storage.swift in Sources */,
D221E5C220D00DCC00BC940E /* Entry.swift in Sources */,
D5A9D1C121134776005DBD3F /* StoreChange.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -941,7 +935,6 @@
D28897061F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */,
D270148120D10982003B45C7 /* Storage.swift in Sources */,
D221E5C120D00DCC00BC940E /* Entry.swift in Sources */,
D5A9D1C021134776005DBD3F /* StoreChange.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -995,7 +988,6 @@
D2CF98611F694FFA00CE8F68 /* MemoryConfig.swift in Sources */,
D2CF98661F694FFA00CE8F68 /* ExpirationMode.swift in Sources */,
D221E5C020D00DCC00BC940E /* Entry.swift in Sources */,
D5A9D1BF21134776005DBD3F /* StoreChange.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
14 changes: 12 additions & 2 deletions Source/Shared/Storage/DiskStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ final public class DiskStorage<T> {
/// File manager to read/write to the disk
public let fileManager: FileManager
/// Configuration
fileprivate let config: DiskConfig
private let config: DiskConfig
/// The computed path `directory+name`
public let path: String
/// The closure to be called when single file has been removed
var onRemove: ((String) -> Void)?

private let transformer: Transformer<T>


// MARK: - Initialization

Expand Down Expand Up @@ -86,7 +89,9 @@ extension DiskStorage: StorageAware {
}

public func removeObject(forKey key: String) throws {
try fileManager.removeItem(atPath: makeFilePath(for: key))
let filePath = makeFilePath(for: key)
try fileManager.removeItem(atPath: filePath)
onRemove?(filePath)
}

public func removeAll() throws {
Expand Down Expand Up @@ -135,6 +140,7 @@ extension DiskStorage: StorageAware {
// Remove expired objects
for url in filesToDelete {
try fileManager.removeItem(at: url)
onRemove?(url.path)
}

// Remove objects if storage size exceeds max size
Expand Down Expand Up @@ -220,9 +226,12 @@ extension DiskStorage {

for file in sortedFiles {
try fileManager.removeItem(at: file.url)
onRemove?(file.url.path)

if let fileSize = file.resourceValues.totalFileAllocatedSize {
totalSize -= UInt(fileSize)
}

if totalSize < targetSize {
break
}
Expand All @@ -238,6 +247,7 @@ extension DiskStorage {
let attributes = try fileManager.attributesOfItem(atPath: filePath)
if let expiryDate = attributes[.modificationDate] as? Date, expiryDate.inThePast {
try fileManager.removeItem(atPath: filePath)
onRemove?(filePath)
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions Source/Shared/Storage/HybridStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@ public final class HybridStorage<T> {
public let memoryStorage: MemoryStorage<T>
public let diskStorage: DiskStorage<T>
public let storageObservationRegistry = StorageObservationRegistry<HybridStorage>()
public let keyObservationRegistry = KeyObservationRegistry<HybridStorage>()

public init(memoryStorage: MemoryStorage<T>, diskStorage: DiskStorage<T>) {
self.memoryStorage = memoryStorage
self.diskStorage = diskStorage

diskStorage.onRemove = { [weak self] path in
self?.handleRemovedObject(at: path)
}
}

private func handleRemovedObject(at path: String) {
keyObservationRegistry.notifyObserver(about: .remove, in: self) { key in
let fileName = diskStorage.makeFileName(for: key)
return path.contains(fileName)
}
}
}

Expand All @@ -27,24 +39,40 @@ extension HybridStorage: StorageAware {
public func removeObject(forKey key: String) throws {
memoryStorage.removeObject(forKey: key)
try diskStorage.removeObject(forKey: key)

storageObservationRegistry.notifyObservers(about: .remove(key: key), in: self)
}

public func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws {
var keyChange: KeyChange<T>?

if !keyObservationRegistry.isEmpty {
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 {
keyObservationRegistry.notifyObserver(forKey: key, about: change, in: self)
}

storageObservationRegistry.notifyObservers(about: .add(key: key), in: self)
}

public func removeAll() throws {
memoryStorage.removeAll()
try diskStorage.removeAll()

storageObservationRegistry.notifyObservers(about: .removeAll, in: self)
keyObservationRegistry.notifyAllObservers(about: .remove, in: self)
}

public func removeExpiredObjects() throws {
memoryStorage.removeExpiredObjects()
try diskStorage.removeExpiredObjects()

storageObservationRegistry.notifyObservers(about: .removeExpired, in: self)
}
}
Expand Down
42 changes: 36 additions & 6 deletions Source/Shared/Storage/KeyObservationRegistry.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import Foundation

public enum KeyChange<T> {
case edit(before: T?, after: T?)
case remove
}

public final class KeyObservationRegistry<Storage: StorageAware> {
public typealias Observation = (Storage, KeyChange<Storage.T>) -> Void
private(set) var observations = [String: Observation]()

public var isEmpty: Bool {
return observations.isEmpty
}

@discardableResult
public func addObservation(_ observation: @escaping Observation, forKey key: String) -> ObservationToken {
observations[key] = observation
Expand All @@ -30,9 +29,40 @@ public final class KeyObservationRegistry<Storage: StorageAware> {
observations.removeAll()
}

func notifyObservers(about change: KeyChange<Storage.T>, in storage: Storage) {
func notifyObserver(forKey key: String, about change: KeyChange<Storage.T>, in storage: Storage) {
observations[key]?(storage, change)
}

func notifyObserver(about change: KeyChange<Storage.T>,
in storage: Storage,
where closure: ((String) -> Bool)) {
let observation = observations.first { key, value in closure(key) }?.value
observation?(storage, change)
}

func notifyAllObservers(about change: KeyChange<Storage.T>, in storage: Storage) {
observations.values.forEach { closure in
closure(storage, change)
}
}
}

// MARK: - KeyChange

public enum KeyChange<T> {
case edit(before: T?, after: T)
case remove
}

extension KeyChange: Equatable where T: Equatable {
public static func == (lhs: KeyChange<T>, rhs: KeyChange<T>) -> Bool {
switch (lhs, rhs) {
case (.edit(let before1, let after1), .edit(let before2, let after2)):
return before1 == before2 && after1 == after2
case (.remove, .remove):
return true
default:
return false
}
}
}
1 change: 1 addition & 0 deletions Source/Shared/Storage/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public final class Storage<T> {
let asyncStorage: AsyncStorage<T>

public let storageObservationRegistry = StorageObservationRegistry<Storage>()
public let keyObservationRegistry = KeyObservationRegistry<Storage>()

/// Initialize storage with configuration options.
///
Expand Down
24 changes: 24 additions & 0 deletions Source/Shared/Storage/StorageObservationRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ public final class StorageObservationRegistry<Storage: StorageAware> {
public typealias Observation = (Storage, StorageChange) -> Void
private(set) var observations = [UUID: Observation]()

public var isEmpty: Bool {
return observations.isEmpty
}

@discardableResult
public func addObservation(_ observation: @escaping Observation) -> ObservationToken {
let id = UUID()
Expand All @@ -28,3 +32,23 @@ public final class StorageObservationRegistry<Storage: StorageAware> {
}
}
}

// MARK: - StorageChange

public enum StorageChange: Equatable {
case add(key: String)
case remove(key: String)
case removeAll
case removeExpired
}

public func == (lhs: StorageChange, rhs: StorageChange) -> Bool {
switch (lhs, rhs) {
case (.add(let key1), .add(let key2)), (.remove(let key1), .remove(let key2)):
return key1 == key2
case (.removeAll, .removeAll), (.removeExpired, .removeExpired):
return true
default:
return false
}
}
17 changes: 0 additions & 17 deletions Source/Shared/Storage/StoreChange.swift

This file was deleted.

0 comments on commit a7e40e7

Please sign in to comment.