Skip to content

Commit

Permalink
Merge pull request #202 from hyperoslo/feature/observations
Browse files Browse the repository at this point in the history
Feature: observations
  • Loading branch information
vadymmarkov authored Aug 9, 2018
2 parents 9986637 + 468a780 commit 35d8eea
Show file tree
Hide file tree
Showing 11 changed files with 533 additions and 179 deletions.
22 changes: 8 additions & 14 deletions Cache.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,11 @@
D2D4CC251FA3426B00E4A2D5 /* JSONArrayWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4CC231FA3426B00E4A2D5 /* JSONArrayWrapper.swift */; };
D2D4CC261FA3426B00E4A2D5 /* JSONArrayWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4CC231FA3426B00E4A2D5 /* JSONArrayWrapper.swift */; };
D2D4CC281FA342CA00E4A2D5 /* JSONWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D4CC271FA342CA00E4A2D5 /* JSONWrapperTests.swift */; };
D511464B2114775100197DCE /* StorageObservationRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D511464A2114775100197DCE /* StorageObservationRegistryTests.swift */; };
D511464D2114775100197DCE /* StorageObservationRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D511464A2114775100197DCE /* StorageObservationRegistryTests.swift */; };
D511464F21147B7C00197DCE /* ObservationTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D511464E21147B7C00197DCE /* ObservationTokenTests.swift */; };
D511465121147B7C00197DCE /* ObservationTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D511464E21147B7C00197DCE /* ObservationTokenTests.swift */; };
D51146532118337500197DCE /* KeyObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51146522118337500197DCE /* KeyObservationRegistry.swift */; };
D51146542118337500197DCE /* KeyObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51146522118337500197DCE /* KeyObservationRegistry.swift */; };
D51146552118337500197DCE /* KeyObservationRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51146522118337500197DCE /* KeyObservationRegistry.swift */; };
D5291D1D1C2837DB00B702C9 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5DC59E01C20593E003BD79B /* Cache.framework */; };
D5291D6A1C283B5400B702C9 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5291D601C283B5300B702C9 /* Cache.framework */; };
D5291D851C283C7C00B702C9 /* TestHelper+OSX.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5291D811C283C7000B702C9 /* TestHelper+OSX.swift */; };
Expand All @@ -142,9 +143,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 @@ -223,8 +221,8 @@
D2D4CC1F1FA3411300E4A2D5 /* JSONDictionaryWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDictionaryWrapper.swift; sourceTree = "<group>"; };
D2D4CC231FA3426B00E4A2D5 /* JSONArrayWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONArrayWrapper.swift; sourceTree = "<group>"; };
D2D4CC271FA342CA00E4A2D5 /* JSONWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONWrapperTests.swift; sourceTree = "<group>"; };
D511464A2114775100197DCE /* StorageObservationRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageObservationRegistryTests.swift; sourceTree = "<group>"; };
D511464E21147B7C00197DCE /* ObservationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationTokenTests.swift; sourceTree = "<group>"; };
D51146522118337500197DCE /* KeyObservationRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyObservationRegistry.swift; sourceTree = "<group>"; };
D5291CDF1C28374800B702C9 /* TestHelper+iOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TestHelper+iOS.swift"; sourceTree = "<group>"; };
D5291D181C2837DB00B702C9 /* Cache-iOS-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Cache-iOS-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
D5291D231C28380100B702C9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand All @@ -238,7 +236,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 @@ -367,8 +364,8 @@
D270147F20D10982003B45C7 /* Storage.swift */,
D270148320D10E76003B45C7 /* AsyncStorage.swift */,
D270148720D11040003B45C7 /* Storage+Transform.swift */,
D5A9D1BE21134776005DBD3F /* StoreChange.swift */,
D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */,
D51146522118337500197DCE /* KeyObservationRegistry.swift */,
);
path = Storage;
sourceTree = "<group>";
Expand Down Expand Up @@ -416,7 +413,6 @@
D292DB001F6AA06B0060F614 /* SyncStorageTests.swift */,
D292DB031F6AA0730060F614 /* AsyncStorageTests.swift */,
D236F3191F6BEF73004EE01F /* StorageTests.swift */,
D511464A2114775100197DCE /* StorageObservationRegistryTests.swift */,
);
path = Storage;
sourceTree = "<group>";
Expand Down Expand Up @@ -841,12 +837,12 @@
D5A138C21EB29BFA00881A20 /* UIImage+Extensions.swift in Sources */,
D21B66851F6A723C00125DE1 /* DataSerializer.swift in Sources */,
D270147A20D1046A003B45C7 /* HybridStorage.swift in Sources */,
D51146552118337500197DCE /* KeyObservationRegistry.swift in Sources */,
D270148620D10E76003B45C7 /* AsyncStorage.swift in Sources */,
D270149620D125AC003B45C7 /* MemoryCapsule.swift in Sources */,
D28897071F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */,
D270148220D10982003B45C7 /* Storage.swift in Sources */,
D221E5C220D00DCC00BC940E /* Entry.swift in Sources */,
D5A9D1C121134776005DBD3F /* StoreChange.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -864,7 +860,6 @@
D511465121147B7C00197DCE /* ObservationTokenTests.swift in Sources */,
D28A1D241F6FFEF60030DF81 /* ObjectConverterTests.swift in Sources */,
D27014B120D12E38003B45C7 /* StorageSupportTests.swift in Sources */,
D511464D2114775100197DCE /* StorageObservationRegistryTests.swift in Sources */,
D27014AD20D12CC3003B45C7 /* SyncStorageTests.swift in Sources */,
D27014AA20D12BA4003B45C7 /* HybridStorageTests.swift in Sources */,
);
Expand All @@ -884,7 +879,6 @@
D2CF987F1F69513800CE8F68 /* ImageWrapperTests.swift in Sources */,
D2D4CC1A1FA3166900E4A2D5 /* MD5Tests.swift in Sources */,
D2D4CC281FA342CA00E4A2D5 /* JSONWrapperTests.swift in Sources */,
D511464B2114775100197DCE /* StorageObservationRegistryTests.swift in Sources */,
D27014B320D13E2C003B45C7 /* StorageTests.swift in Sources */,
D28C9BAF1F67EF8300C180C1 /* UIImage+ExtensionsTests.swift in Sources */,
D2CF987D1F69513800CE8F68 /* MemoryCapsuleTests.swift in Sources */,
Expand Down Expand Up @@ -929,12 +923,12 @@
D5A138C41EB29C2100881A20 /* NSImage+Extensions.swift in Sources */,
D21B667C1F6A723C00125DE1 /* DataSerializer.swift in Sources */,
D270147920D1046A003B45C7 /* HybridStorage.swift in Sources */,
D51146542118337500197DCE /* KeyObservationRegistry.swift in Sources */,
D270148520D10E76003B45C7 /* AsyncStorage.swift in Sources */,
D270149520D125AC003B45C7 /* MemoryCapsule.swift in Sources */,
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 @@ -982,12 +976,12 @@
D270147820D1046A003B45C7 /* HybridStorage.swift in Sources */,
D270148420D10E76003B45C7 /* AsyncStorage.swift in Sources */,
D28897051F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */,
D51146532118337500197DCE /* KeyObservationRegistry.swift in Sources */,
D270148020D10982003B45C7 /* Storage.swift in Sources */,
D270149420D125AC003B45C7 /* MemoryCapsule.swift in Sources */,
D2CF98611F694FFA00CE8F68 /* MemoryConfig.swift in Sources */,
D2CF98661F694FFA00CE8F68 /* ExpirationMode.swift in Sources */,
D221E5C020D00DCC00BC940E /* Entry.swift in Sources */,
D5A9D1BF21134776005DBD3F /* StoreChange.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
70 changes: 64 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
* [Sync APIs](#sync-apis)
* [Async APIS](#async-apis)
* [Expiry date](#expiry-date)
* [Observations](#observations)
* [Storage observations](#storage-observations)
* [Key observations](#key-observations)
* [Handling JSON response](#handling-json-response)
* [What about images?](#what-about-images)
* [Installation](#installation)
Expand Down Expand Up @@ -70,8 +73,8 @@ let diskConfig = DiskConfig(name: "Floppy")
let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)

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

### Async APIs

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`.
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`.

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.

Expand Down Expand Up @@ -290,7 +293,7 @@ storage.async.removeExpiredObjects() { result in
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`

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

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

## Observations

[Storage](#storage) allows you to observe changes in the cache layer, both on
a store and a key levels. The API lets you pass any object as an observer,
while also passing an observation closure. The observation closure will be
removed automatically when the weakly captured observer has been deallocated.

## Storage observations

```swift
// Add observer
let token = storage.addStorageObserver(self) { observer, storage, change in
switch change {
case .add(let key):
print("Added \(key)")
case .remove(let key):
print("Removed \(key)")
case .removeAll:
print("Removed all")
case .removeExpired:
print("Removed expired")
}
}

// Remove observer
token.cancel()

// Remove all observers
storage.removeAllStorageObservers()
```

## Key observations

```swift
let key = "user1"

let token = storage.addObserver(self, forKey: key) { observer, storage, change in
switch change {
case .edit(let before, let after):
print("Changed object for \(key) from \(String(describing: before)) to \(after)")
case .remove:
print("Removed \(key)")
}
}

// Remove observer by token
token.cancel()

// Remove observer for key
storage.removeObserver(forKey: key)

// Remove all observers
storage.removeAllKeyObservers()
```

## Handling JSON response

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.
Expand Down Expand Up @@ -360,7 +418,7 @@ You also need to add `SwiftHash.framework` in your [copy-frameworks](https://git
## Author

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

## Contributing

Expand Down
15 changes: 12 additions & 3 deletions Source/Shared/Storage/DiskStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +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

public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<T>) throws {
Expand Down Expand Up @@ -86,7 +88,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 +139,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 +225,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 +246,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
Loading

0 comments on commit 35d8eea

Please sign in to comment.