Skip to content

Commit 517cbf5

Browse files
Generalize cache storage to allow using any hashable key parameter
1 parent 98f7a3b commit 517cbf5

File tree

10 files changed

+139
-115
lines changed

10 files changed

+139
-115
lines changed

Source/Shared/Storage/AsyncStorage.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import Dispatch
33

44
/// Manipulate storage in a "all async" manner.
55
/// The completion closure will be called when operation completes.
6-
public class AsyncStorage<T> {
7-
public let innerStorage: HybridStorage<T>
6+
public class AsyncStorage<Key: Hashable, Value> {
7+
public let innerStorage: HybridStorage<Key, Value>
88
public let serialQueue: DispatchQueue
99

10-
public init(storage: HybridStorage<T>, serialQueue: DispatchQueue) {
10+
public init(storage: HybridStorage<Key, Value>, serialQueue: DispatchQueue) {
1111
self.innerStorage = storage
1212
self.serialQueue = serialQueue
1313
}
1414
}
1515

1616
extension AsyncStorage {
17-
public func entry(forKey key: String, completion: @escaping (Result<Entry<T>>) -> Void) {
17+
public func entry(forKey key: Key, completion: @escaping (Result<Entry<Value>>) -> Void) {
1818
serialQueue.async { [weak self] in
1919
guard let `self` = self else {
2020
completion(Result.error(StorageError.deallocated))
@@ -30,7 +30,7 @@ extension AsyncStorage {
3030
}
3131
}
3232

33-
public func removeObject(forKey key: String, completion: @escaping (Result<()>) -> Void) {
33+
public func removeObject(forKey key: Key, completion: @escaping (Result<()>) -> Void) {
3434
serialQueue.async { [weak self] in
3535
guard let `self` = self else {
3636
completion(Result.error(StorageError.deallocated))
@@ -47,8 +47,8 @@ extension AsyncStorage {
4747
}
4848

4949
public func setObject(
50-
_ object: T,
51-
forKey key: String,
50+
_ object: Value,
51+
forKey key: Key,
5252
expiry: Expiry? = nil,
5353
completion: @escaping (Result<()>) -> Void) {
5454
serialQueue.async { [weak self] in
@@ -98,18 +98,18 @@ extension AsyncStorage {
9898
}
9999
}
100100

101-
public func object(forKey key: String, completion: @escaping (Result<T>) -> Void) {
102-
entry(forKey: key, completion: { (result: Result<Entry<T>>) in
101+
public func object(forKey key: Key, completion: @escaping (Result<Value>) -> Void) {
102+
entry(forKey: key, completion: { (result: Result<Entry<Value>>) in
103103
completion(result.map({ entry in
104104
return entry.object
105105
}))
106106
})
107107
}
108108

109109
public func existsObject(
110-
forKey key: String,
110+
forKey key: Key,
111111
completion: @escaping (Result<Bool>) -> Void) {
112-
object(forKey: key, completion: { (result: Result<T>) in
112+
object(forKey: key, completion: { (result: Result<Value>) in
113113
completion(result.map({ _ in
114114
return true
115115
}))
@@ -118,8 +118,8 @@ extension AsyncStorage {
118118
}
119119

120120
public extension AsyncStorage {
121-
func transform<U>(transformer: Transformer<U>) -> AsyncStorage<U> {
122-
let storage = AsyncStorage<U>(
121+
func transform<U>(transformer: Transformer<U>) -> AsyncStorage<Key, U> {
122+
let storage = AsyncStorage<Key, U>(
123123
storage: innerStorage.transform(transformer: transformer),
124124
serialQueue: serialQueue
125125
)

Source/Shared/Storage/DiskStorage.swift

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// Save objects to file on disk
4-
final public class DiskStorage<T> {
4+
final public class DiskStorage<Key: Hashable, Value> {
55
enum Error: Swift.Error {
66
case fileEnumeratorFailed
77
}
@@ -15,10 +15,11 @@ final public class DiskStorage<T> {
1515
/// The closure to be called when single file has been removed
1616
var onRemove: ((String) -> Void)?
1717

18-
private let transformer: Transformer<T>
18+
private let transformer: Transformer<Value>
19+
private let hasher = Hasher.constantAccrossExecutions()
1920

2021
// MARK: - Initialization
21-
public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<T>) throws {
22+
public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<Value>) throws {
2223
let url: URL
2324
if let directory = config.directory {
2425
url = directory
@@ -48,7 +49,7 @@ final public class DiskStorage<T> {
4849
#endif
4950
}
5051

51-
public required init(config: DiskConfig, fileManager: FileManager = FileManager.default, path: String, transformer: Transformer<T>) {
52+
public required init(config: DiskConfig, fileManager: FileManager = FileManager.default, path: String, transformer: Transformer<Value>) {
5253
self.config = config
5354
self.fileManager = fileManager
5455
self.path = path
@@ -57,7 +58,7 @@ final public class DiskStorage<T> {
5758
}
5859

5960
extension DiskStorage: StorageAware {
60-
public func entry(forKey key: String) throws -> Entry<T> {
61+
public func entry(forKey key: Key) throws -> Entry<Value> {
6162
let filePath = makeFilePath(for: key)
6263
let data = try Data(contentsOf: URL(fileURLWithPath: filePath))
6364
let attributes = try fileManager.attributesOfItem(atPath: filePath)
@@ -74,15 +75,15 @@ extension DiskStorage: StorageAware {
7475
)
7576
}
7677

77-
public func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws {
78+
public func setObject(_ object: Value, forKey key: Key, expiry: Expiry? = nil) throws {
7879
let expiry = expiry ?? config.expiry
7980
let data = try transformer.toData(object)
8081
let filePath = makeFilePath(for: key)
8182
_ = fileManager.createFile(atPath: filePath, contents: data, attributes: nil)
8283
try fileManager.setAttributes([.modificationDate: expiry.date], ofItemAtPath: filePath)
8384
}
8485

85-
public func removeObject(forKey key: String) throws {
86+
public func removeObject(forKey key: Key) throws {
8687
let filePath = makeFilePath(for: key)
8788
try fileManager.removeItem(atPath: filePath)
8889
onRemove?(filePath)
@@ -160,24 +161,30 @@ extension DiskStorage {
160161
- Parameter key: Unique key to identify the object in the cache
161162
- Returns: A md5 string
162163
*/
163-
func makeFileName(for key: String) -> String {
164-
let fileExtension = URL(fileURLWithPath: key).pathExtension
165-
let fileName = MD5(key)
166-
167-
switch fileExtension.isEmpty {
168-
case true:
169-
return fileName
170-
case false:
171-
return "\(fileName).\(fileExtension)"
164+
func makeFileName(for key: Key) -> String {
165+
if let key = key as? String {
166+
let fileExtension = URL(fileURLWithPath: key).pathExtension
167+
let fileName = MD5(key)
168+
169+
switch fileExtension.isEmpty {
170+
case true:
171+
return fileName
172+
case false:
173+
return "\(fileName).\(fileExtension)"
174+
}
172175
}
176+
177+
var hasher = self.hasher
178+
key.hash(into: &hasher)
179+
return String(hasher.finalize())
173180
}
174181

175182
/**
176183
Builds file path from the key.
177184
- Parameter key: Unique key to identify the object in the cache
178185
- Returns: A string path based on key
179186
*/
180-
func makeFilePath(for key: String) -> String {
187+
func makeFilePath(for key: Key) -> String {
181188
return "\(path)/\(makeFileName(for: key))"
182189
}
183190

@@ -244,7 +251,7 @@ extension DiskStorage {
244251
Removes the object from the cache if it's expired.
245252
- Parameter key: Unique key to identify the object in the cache
246253
*/
247-
func removeObjectIfExpired(forKey key: String) throws {
254+
func removeObjectIfExpired(forKey key: Key) throws {
248255
let filePath = makeFilePath(for: key)
249256
let attributes = try fileManager.attributesOfItem(atPath: filePath)
250257
if let expiryDate = attributes[.modificationDate] as? Date, expiryDate.inThePast {
@@ -255,8 +262,8 @@ extension DiskStorage {
255262
}
256263

257264
public extension DiskStorage {
258-
func transform<U>(transformer: Transformer<U>) -> DiskStorage<U> {
259-
let storage = DiskStorage<U>(
265+
func transform<U>(transformer: Transformer<U>) -> DiskStorage<Key, U> {
266+
let storage = DiskStorage<Key, U>(
260267
config: config,
261268
fileManager: fileManager,
262269
path: path,

Source/Shared/Storage/HybridStorage.swift

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import Foundation
22

33
/// Use both memory and disk storage. Try on memory first.
4-
public final class HybridStorage<T> {
5-
public let memoryStorage: MemoryStorage<T>
6-
public let diskStorage: DiskStorage<T>
4+
public final class HybridStorage<Key: Hashable, Value> {
5+
public let memoryStorage: MemoryStorage<Key, Value>
6+
public let diskStorage: DiskStorage<Key, Value>
77

8-
private(set) var storageObservations = [UUID: (HybridStorage, StorageChange) -> Void]()
9-
private(set) var keyObservations = [String: (HybridStorage, KeyChange<T>) -> Void]()
8+
private(set) var storageObservations = [UUID: (HybridStorage, StorageChange<Key>) -> Void]()
9+
private(set) var keyObservations = [Key: (HybridStorage, KeyChange<Value>) -> Void]()
1010

11-
public init(memoryStorage: MemoryStorage<T>, diskStorage: DiskStorage<T>) {
11+
public init(memoryStorage: MemoryStorage<Key, Value>, diskStorage: DiskStorage<Key, Value>) {
1212
self.memoryStorage = memoryStorage
1313
self.diskStorage = diskStorage
1414

@@ -26,7 +26,7 @@ public final class HybridStorage<T> {
2626
}
2727

2828
extension HybridStorage: StorageAware {
29-
public func entry(forKey key: String) throws -> Entry<T> {
29+
public func entry(forKey key: Key) throws -> Entry<Value> {
3030
do {
3131
return try memoryStorage.entry(forKey: key)
3232
} catch {
@@ -37,15 +37,15 @@ extension HybridStorage: StorageAware {
3737
}
3838
}
3939

40-
public func removeObject(forKey key: String) throws {
40+
public func removeObject(forKey key: Key) throws {
4141
memoryStorage.removeObject(forKey: key)
4242
try diskStorage.removeObject(forKey: key)
4343

4444
notifyStorageObservers(about: .remove(key: key))
4545
}
4646

47-
public func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws {
48-
var keyChange: KeyChange<T>?
47+
public func setObject(_ object: Value, forKey key: Key, expiry: Expiry? = nil) throws {
48+
var keyChange: KeyChange<Value>?
4949

5050
if keyObservations[key] != nil {
5151
keyChange = .edit(before: try? self.object(forKey: key), after: object)
@@ -78,8 +78,8 @@ extension HybridStorage: StorageAware {
7878
}
7979

8080
public extension HybridStorage {
81-
func transform<U>(transformer: Transformer<U>) -> HybridStorage<U> {
82-
let storage = HybridStorage<U>(
81+
func transform<U>(transformer: Transformer<U>) -> HybridStorage<Key, U> {
82+
let storage = HybridStorage<Key, U>(
8383
memoryStorage: memoryStorage.transform(),
8484
diskStorage: diskStorage.transform(transformer: transformer)
8585
)
@@ -92,7 +92,7 @@ extension HybridStorage: StorageObservationRegistry {
9292
@discardableResult
9393
public func addStorageObserver<O: AnyObject>(
9494
_ observer: O,
95-
closure: @escaping (O, HybridStorage, StorageChange) -> Void
95+
closure: @escaping (O, HybridStorage, StorageChange<Key>) -> Void
9696
) -> ObservationToken {
9797
let id = UUID()
9898

@@ -114,7 +114,7 @@ extension HybridStorage: StorageObservationRegistry {
114114
storageObservations.removeAll()
115115
}
116116

117-
private func notifyStorageObservers(about change: StorageChange) {
117+
private func notifyStorageObservers(about change: StorageChange<Key>) {
118118
storageObservations.values.forEach { closure in
119119
closure(self, change)
120120
}
@@ -125,8 +125,8 @@ extension HybridStorage: KeyObservationRegistry {
125125
@discardableResult
126126
public func addObserver<O: AnyObject>(
127127
_ observer: O,
128-
forKey key: String,
129-
closure: @escaping (O, HybridStorage, KeyChange<T>) -> Void
128+
forKey key: Key,
129+
closure: @escaping (O, HybridStorage, KeyChange<Value>) -> Void
130130
) -> ObservationToken {
131131
keyObservations[key] = { [weak self, weak observer] storage, change in
132132
guard let observer = observer else {
@@ -142,24 +142,24 @@ extension HybridStorage: KeyObservationRegistry {
142142
}
143143
}
144144

145-
public func removeObserver(forKey key: String) {
145+
public func removeObserver(forKey key: Key) {
146146
keyObservations.removeValue(forKey: key)
147147
}
148148

149149
public func removeAllKeyObservers() {
150150
keyObservations.removeAll()
151151
}
152152

153-
private func notifyObserver(forKey key: String, about change: KeyChange<T>) {
153+
private func notifyObserver(forKey key: Key, about change: KeyChange<Value>) {
154154
keyObservations[key]?(self, change)
155155
}
156156

157-
private func notifyObserver(about change: KeyChange<T>, whereKey closure: ((String) -> Bool)) {
157+
private func notifyObserver(about change: KeyChange<Value>, whereKey closure: ((Key) -> Bool)) {
158158
let observation = keyObservations.first { key, _ in closure(key) }?.value
159159
observation?(self, change)
160160
}
161161

162-
private func notifyKeyObservers(about change: KeyChange<T>) {
162+
private func notifyKeyObservers(about change: KeyChange<Value>) {
163163
keyObservations.values.forEach { closure in
164164
closure(self, change)
165165
}

Source/Shared/Storage/KeyObservationRegistry.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ public protocol KeyObservationRegistry {
1515
@discardableResult
1616
func addObserver<O: AnyObject>(
1717
_ observer: O,
18-
forKey key: String,
19-
closure: @escaping (O, S, KeyChange<S.T>) -> Void
18+
forKey key: S.Key,
19+
closure: @escaping (O, S, KeyChange<S.Value>) -> Void
2020
) -> ObservationToken
2121

2222
/**
2323
Removes observer by the given key.
2424
- Parameter key: Unique key to identify the object in the cache
2525
*/
26-
func removeObserver(forKey key: String)
26+
func removeObserver(forKey key: S.Key)
2727

2828
/// Removes all registered key observers
2929
func removeAllKeyObservers()

0 commit comments

Comments
 (0)