Skip to content

Commit 35d8eea

Browse files
authored
Merge pull request #202 from hyperoslo/feature/observations
Feature: observations
2 parents 9986637 + 468a780 commit 35d8eea

File tree

11 files changed

+533
-179
lines changed

11 files changed

+533
-179
lines changed

Cache.xcodeproj/project.pbxproj

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,11 @@
128128
D2D4CC251FA3426B00E4A2D5 /* JSONArrayWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4CC231FA3426B00E4A2D5 /* JSONArrayWrapper.swift */; };
129129
D2D4CC261FA3426B00E4A2D5 /* JSONArrayWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4CC231FA3426B00E4A2D5 /* JSONArrayWrapper.swift */; };
130130
D2D4CC281FA342CA00E4A2D5 /* JSONWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4CC271FA342CA00E4A2D5 /* JSONWrapperTests.swift */; };
131-
D511464B2114775100197DCE /* StorageObservationRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D511464A2114775100197DCE /* StorageObservationRegistryTests.swift */; };
132-
D511464D2114775100197DCE /* StorageObservationRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D511464A2114775100197DCE /* StorageObservationRegistryTests.swift */; };
133131
D511464F21147B7C00197DCE /* ObservationTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D511464E21147B7C00197DCE /* ObservationTokenTests.swift */; };
134132
D511465121147B7C00197DCE /* ObservationTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D511464E21147B7C00197DCE /* ObservationTokenTests.swift */; };
133+
D51146532118337500197DCE /* KeyObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51146522118337500197DCE /* KeyObservationRegistry.swift */; };
134+
D51146542118337500197DCE /* KeyObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51146522118337500197DCE /* KeyObservationRegistry.swift */; };
135+
D51146552118337500197DCE /* KeyObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51146522118337500197DCE /* KeyObservationRegistry.swift */; };
135136
D5291D1D1C2837DB00B702C9 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5DC59E01C20593E003BD79B /* Cache.framework */; };
136137
D5291D6A1C283B5400B702C9 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5291D601C283B5300B702C9 /* Cache.framework */; };
137138
D5291D851C283C7C00B702C9 /* TestHelper+OSX.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5291D811C283C7000B702C9 /* TestHelper+OSX.swift */; };
@@ -142,9 +143,6 @@
142143
D5A9D1B721134547005DBD3F /* ObservationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1B621134547005DBD3F /* ObservationToken.swift */; };
143144
D5A9D1B821134547005DBD3F /* ObservationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1B621134547005DBD3F /* ObservationToken.swift */; };
144145
D5A9D1B921134547005DBD3F /* ObservationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1B621134547005DBD3F /* ObservationToken.swift */; };
145-
D5A9D1BF21134776005DBD3F /* StoreChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1BE21134776005DBD3F /* StoreChange.swift */; };
146-
D5A9D1C021134776005DBD3F /* StoreChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1BE21134776005DBD3F /* StoreChange.swift */; };
147-
D5A9D1C121134776005DBD3F /* StoreChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1BE21134776005DBD3F /* StoreChange.swift */; };
148146
D5A9D1C321144B65005DBD3F /* StorageObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */; };
149147
D5A9D1C421144B65005DBD3F /* StorageObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */; };
150148
D5A9D1C521144B65005DBD3F /* StorageObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */; };
@@ -223,8 +221,8 @@
223221
D2D4CC1F1FA3411300E4A2D5 /* JSONDictionaryWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDictionaryWrapper.swift; sourceTree = "<group>"; };
224222
D2D4CC231FA3426B00E4A2D5 /* JSONArrayWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONArrayWrapper.swift; sourceTree = "<group>"; };
225223
D2D4CC271FA342CA00E4A2D5 /* JSONWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONWrapperTests.swift; sourceTree = "<group>"; };
226-
D511464A2114775100197DCE /* StorageObservationRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageObservationRegistryTests.swift; sourceTree = "<group>"; };
227224
D511464E21147B7C00197DCE /* ObservationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationTokenTests.swift; sourceTree = "<group>"; };
225+
D51146522118337500197DCE /* KeyObservationRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyObservationRegistry.swift; sourceTree = "<group>"; };
228226
D5291CDF1C28374800B702C9 /* TestHelper+iOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TestHelper+iOS.swift"; sourceTree = "<group>"; };
229227
D5291D181C2837DB00B702C9 /* Cache-iOS-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Cache-iOS-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
230228
D5291D231C28380100B702C9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -238,7 +236,6 @@
238236
D5A138C01EB29BFA00881A20 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = "<group>"; };
239237
D5A138C31EB29C2100881A20 /* NSImage+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSImage+Extensions.swift"; sourceTree = "<group>"; };
240238
D5A9D1B621134547005DBD3F /* ObservationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationToken.swift; sourceTree = "<group>"; };
241-
D5A9D1BE21134776005DBD3F /* StoreChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreChange.swift; sourceTree = "<group>"; };
242239
D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageObservationRegistry.swift; sourceTree = "<group>"; };
243240
D5DC59E01C20593E003BD79B /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
244241
EBAACA991FBC369300FA206E /* SimpleStorage.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = SimpleStorage.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
@@ -367,8 +364,8 @@
367364
D270147F20D10982003B45C7 /* Storage.swift */,
368365
D270148320D10E76003B45C7 /* AsyncStorage.swift */,
369366
D270148720D11040003B45C7 /* Storage+Transform.swift */,
370-
D5A9D1BE21134776005DBD3F /* StoreChange.swift */,
371367
D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */,
368+
D51146522118337500197DCE /* KeyObservationRegistry.swift */,
372369
);
373370
path = Storage;
374371
sourceTree = "<group>";
@@ -416,7 +413,6 @@
416413
D292DB001F6AA06B0060F614 /* SyncStorageTests.swift */,
417414
D292DB031F6AA0730060F614 /* AsyncStorageTests.swift */,
418415
D236F3191F6BEF73004EE01F /* StorageTests.swift */,
419-
D511464A2114775100197DCE /* StorageObservationRegistryTests.swift */,
420416
);
421417
path = Storage;
422418
sourceTree = "<group>";
@@ -841,12 +837,12 @@
841837
D5A138C21EB29BFA00881A20 /* UIImage+Extensions.swift in Sources */,
842838
D21B66851F6A723C00125DE1 /* DataSerializer.swift in Sources */,
843839
D270147A20D1046A003B45C7 /* HybridStorage.swift in Sources */,
840+
D51146552118337500197DCE /* KeyObservationRegistry.swift in Sources */,
844841
D270148620D10E76003B45C7 /* AsyncStorage.swift in Sources */,
845842
D270149620D125AC003B45C7 /* MemoryCapsule.swift in Sources */,
846843
D28897071F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */,
847844
D270148220D10982003B45C7 /* Storage.swift in Sources */,
848845
D221E5C220D00DCC00BC940E /* Entry.swift in Sources */,
849-
D5A9D1C121134776005DBD3F /* StoreChange.swift in Sources */,
850846
);
851847
runOnlyForDeploymentPostprocessing = 0;
852848
};
@@ -864,7 +860,6 @@
864860
D511465121147B7C00197DCE /* ObservationTokenTests.swift in Sources */,
865861
D28A1D241F6FFEF60030DF81 /* ObjectConverterTests.swift in Sources */,
866862
D27014B120D12E38003B45C7 /* StorageSupportTests.swift in Sources */,
867-
D511464D2114775100197DCE /* StorageObservationRegistryTests.swift in Sources */,
868863
D27014AD20D12CC3003B45C7 /* SyncStorageTests.swift in Sources */,
869864
D27014AA20D12BA4003B45C7 /* HybridStorageTests.swift in Sources */,
870865
);
@@ -884,7 +879,6 @@
884879
D2CF987F1F69513800CE8F68 /* ImageWrapperTests.swift in Sources */,
885880
D2D4CC1A1FA3166900E4A2D5 /* MD5Tests.swift in Sources */,
886881
D2D4CC281FA342CA00E4A2D5 /* JSONWrapperTests.swift in Sources */,
887-
D511464B2114775100197DCE /* StorageObservationRegistryTests.swift in Sources */,
888882
D27014B320D13E2C003B45C7 /* StorageTests.swift in Sources */,
889883
D28C9BAF1F67EF8300C180C1 /* UIImage+ExtensionsTests.swift in Sources */,
890884
D2CF987D1F69513800CE8F68 /* MemoryCapsuleTests.swift in Sources */,
@@ -929,12 +923,12 @@
929923
D5A138C41EB29C2100881A20 /* NSImage+Extensions.swift in Sources */,
930924
D21B667C1F6A723C00125DE1 /* DataSerializer.swift in Sources */,
931925
D270147920D1046A003B45C7 /* HybridStorage.swift in Sources */,
926+
D51146542118337500197DCE /* KeyObservationRegistry.swift in Sources */,
932927
D270148520D10E76003B45C7 /* AsyncStorage.swift in Sources */,
933928
D270149520D125AC003B45C7 /* MemoryCapsule.swift in Sources */,
934929
D28897061F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */,
935930
D270148120D10982003B45C7 /* Storage.swift in Sources */,
936931
D221E5C120D00DCC00BC940E /* Entry.swift in Sources */,
937-
D5A9D1C021134776005DBD3F /* StoreChange.swift in Sources */,
938932
);
939933
runOnlyForDeploymentPostprocessing = 0;
940934
};
@@ -982,12 +976,12 @@
982976
D270147820D1046A003B45C7 /* HybridStorage.swift in Sources */,
983977
D270148420D10E76003B45C7 /* AsyncStorage.swift in Sources */,
984978
D28897051F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */,
979+
D51146532118337500197DCE /* KeyObservationRegistry.swift in Sources */,
985980
D270148020D10982003B45C7 /* Storage.swift in Sources */,
986981
D270149420D125AC003B45C7 /* MemoryCapsule.swift in Sources */,
987982
D2CF98611F694FFA00CE8F68 /* MemoryConfig.swift in Sources */,
988983
D2CF98661F694FFA00CE8F68 /* ExpirationMode.swift in Sources */,
989984
D221E5C020D00DCC00BC940E /* Entry.swift in Sources */,
990-
D5A9D1BF21134776005DBD3F /* StoreChange.swift in Sources */,
991985
);
992986
runOnlyForDeploymentPostprocessing = 0;
993987
};

README.md

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
* [Sync APIs](#sync-apis)
1919
* [Async APIS](#async-apis)
2020
* [Expiry date](#expiry-date)
21+
* [Observations](#observations)
22+
* [Storage observations](#storage-observations)
23+
* [Key observations](#key-observations)
2124
* [Handling JSON response](#handling-json-response)
2225
* [What about images?](#what-about-images)
2326
* [Installation](#installation)
@@ -70,8 +73,8 @@ let diskConfig = DiskConfig(name: "Floppy")
7073
let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)
7174

7275
let storage = try? Storage(
73-
diskConfig: diskConfig,
74-
memoryConfig: memoryConfig,
76+
diskConfig: diskConfig,
77+
memoryConfig: memoryConfig,
7578
transformer: TransformerFactory.forCodable(ofType: User.self) // Storage<User>
7679
)
7780
```
@@ -158,7 +161,7 @@ let diskConfig = DiskConfig(
158161
// Maximum size of the disk cache storage (in bytes)
159162
maxSize: 10000,
160163
// Where to store the disk cache. If nil, it is placed in `cachesDirectory` directory.
161-
directory: try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask,
164+
directory: try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask,
162165
appropriateFor: nil, create: true).appendingPathComponent("MyPreferences"),
163166
// Data protection is used to store files in an encrypted format on disk and to decrypt them on demand
164167
protectionType: .complete
@@ -237,7 +240,7 @@ try? storage.setObject(user, forKey: "character")
237240

238241
### Async APIs
239242

240-
In `async` fashion, you deal with `Result` instead of `try catch` because the result is delivered at a later time, in order to not block the current calling queue. In the completion block, you either have `value` or `error`.
243+
In `async` fashion, you deal with `Result` instead of `try catch` because the result is delivered at a later time, in order to not block the current calling queue. In the completion block, you either have `value` or `error`.
241244

242245
You access Async APIs via `storage.async`, it is also thread safe, and you can use Sync and Async APIs in any order you want. All Async functions are constrained by `AsyncStorageAware` protocol.
243246

@@ -290,7 +293,7 @@ storage.async.removeExpiredObjects() { result in
290293
By default, all saved objects have the same expiry as the expiry you specify in `DiskConfig` or `MemoryConfig`. You can overwrite this for a specific object by specifying `expiry` for `setObject`
291294

292295
```swift
293-
// Default cexpiry date from configuration will be applied to the item
296+
// Default expiry date from configuration will be applied to the item
294297
try? storage.setObject("This is a string", forKey: "string")
295298

296299
// A given expiry date will be applied to the item
@@ -304,6 +307,61 @@ try? storage.setObject(
304307
storage.removeExpiredObjects()
305308
```
306309

310+
## Observations
311+
312+
[Storage](#storage) allows you to observe changes in the cache layer, both on
313+
a store and a key levels. The API lets you pass any object as an observer,
314+
while also passing an observation closure. The observation closure will be
315+
removed automatically when the weakly captured observer has been deallocated.
316+
317+
## Storage observations
318+
319+
```swift
320+
// Add observer
321+
let token = storage.addStorageObserver(self) { observer, storage, change in
322+
switch change {
323+
case .add(let key):
324+
print("Added \(key)")
325+
case .remove(let key):
326+
print("Removed \(key)")
327+
case .removeAll:
328+
print("Removed all")
329+
case .removeExpired:
330+
print("Removed expired")
331+
}
332+
}
333+
334+
// Remove observer
335+
token.cancel()
336+
337+
// Remove all observers
338+
storage.removeAllStorageObservers()
339+
```
340+
341+
## Key observations
342+
343+
```swift
344+
let key = "user1"
345+
346+
let token = storage.addObserver(self, forKey: key) { observer, storage, change in
347+
switch change {
348+
case .edit(let before, let after):
349+
print("Changed object for \(key) from \(String(describing: before)) to \(after)")
350+
case .remove:
351+
print("Removed \(key)")
352+
}
353+
}
354+
355+
// Remove observer by token
356+
token.cancel()
357+
358+
// Remove observer for key
359+
storage.removeObserver(forKey: key)
360+
361+
// Remove all observers
362+
storage.removeAllKeyObservers()
363+
```
364+
307365
## Handling JSON response
308366

309367
Most of the time, our use case is to fetch some json from backend, display it while saving the json to storage for future uses. If you're using libraries like [Alamofire](https://github.com/Alamofire/Alamofire) or [Malibu](https://github.com/hyperoslo/Malibu), you mostly get json in the form of dictionary, string, or data.
@@ -360,7 +418,7 @@ You also need to add `SwiftHash.framework` in your [copy-frameworks](https://git
360418
## Author
361419

362420
- [Hyper](http://hyper.no) made this with ❤️
363-
- Inline MD5 implementation from [SwiftHash](https://github.com/onmyway133/SwiftHash)
421+
- Inline MD5 implementation from [SwiftHash](https://github.com/onmyway133/SwiftHash)
364422

365423
## Contributing
366424

Source/Shared/Storage/DiskStorage.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ final public class DiskStorage<T> {
99
/// File manager to read/write to the disk
1010
public let fileManager: FileManager
1111
/// Configuration
12-
fileprivate let config: DiskConfig
12+
private let config: DiskConfig
1313
/// The computed path `directory+name`
1414
public let path: String
15+
/// The closure to be called when single file has been removed
16+
var onRemove: ((String) -> Void)?
1517

1618
private let transformer: Transformer<T>
17-
19+
1820
// MARK: - Initialization
1921

2022
public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<T>) throws {
@@ -86,7 +88,9 @@ extension DiskStorage: StorageAware {
8688
}
8789

8890
public func removeObject(forKey key: String) throws {
89-
try fileManager.removeItem(atPath: makeFilePath(for: key))
91+
let filePath = makeFilePath(for: key)
92+
try fileManager.removeItem(atPath: filePath)
93+
onRemove?(filePath)
9094
}
9195

9296
public func removeAll() throws {
@@ -135,6 +139,7 @@ extension DiskStorage: StorageAware {
135139
// Remove expired objects
136140
for url in filesToDelete {
137141
try fileManager.removeItem(at: url)
142+
onRemove?(url.path)
138143
}
139144

140145
// Remove objects if storage size exceeds max size
@@ -220,9 +225,12 @@ extension DiskStorage {
220225

221226
for file in sortedFiles {
222227
try fileManager.removeItem(at: file.url)
228+
onRemove?(file.url.path)
229+
223230
if let fileSize = file.resourceValues.totalFileAllocatedSize {
224231
totalSize -= UInt(fileSize)
225232
}
233+
226234
if totalSize < targetSize {
227235
break
228236
}
@@ -238,6 +246,7 @@ extension DiskStorage {
238246
let attributes = try fileManager.attributesOfItem(atPath: filePath)
239247
if let expiryDate = attributes[.modificationDate] as? Date, expiryDate.inThePast {
240248
try fileManager.removeItem(atPath: filePath)
249+
onRemove?(filePath)
241250
}
242251
}
243252
}

0 commit comments

Comments
 (0)