Skip to content

Commit

Permalink
Merge pull request hyperoslo#201 from hyperoslo/feature/storage-obser…
Browse files Browse the repository at this point in the history
…vations

Feature: storage observations
  • Loading branch information
vadymmarkov authored Aug 7, 2018
2 parents 34db394 + 364d2ec commit 9986637
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 4 deletions.
36 changes: 36 additions & 0 deletions Cache.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,26 @@
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 */; };
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 */; };
D5291DA31C2841D200B702C9 /* NSImage+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5291DA21C2841D200B702C9 /* NSImage+ExtensionsTests.swift */; };
D5A138C11EB29BFA00881A20 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A138C01EB29BFA00881A20 /* UIImage+Extensions.swift */; };
D5A138C21EB29BFA00881A20 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A138C01EB29BFA00881A20 /* UIImage+Extensions.swift */; };
D5A138C41EB29C2100881A20 /* NSImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A138C31EB29C2100881A20 /* NSImage+Extensions.swift */; };
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 */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -210,6 +223,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>"; };
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 @@ -222,6 +237,9 @@
D5643E361C43F2CC00582E17 /* Storage.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Storage.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
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; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -333,6 +351,7 @@
D270148F20D1251E003B45C7 /* TypeWrapper.swift */,
D270149320D125AC003B45C7 /* MemoryCapsule.swift */,
D27014A420D129EB003B45C7 /* TransformerFactory.swift */,
D5A9D1B621134547005DBD3F /* ObservationToken.swift */,
);
path = Library;
sourceTree = "<group>";
Expand All @@ -348,6 +367,8 @@
D270147F20D10982003B45C7 /* Storage.swift */,
D270148320D10E76003B45C7 /* AsyncStorage.swift */,
D270148720D11040003B45C7 /* Storage+Transform.swift */,
D5A9D1BE21134776005DBD3F /* StoreChange.swift */,
D5A9D1C221144B65005DBD3F /* StorageObservationRegistry.swift */,
);
path = Storage;
sourceTree = "<group>";
Expand Down Expand Up @@ -380,6 +401,7 @@
D285143E1F6FFE1F00C674D1 /* ObjectConverterTests.swift */,
D2D4CC191FA3166900E4A2D5 /* MD5Tests.swift */,
D2D4CC271FA342CA00E4A2D5 /* JSONWrapperTests.swift */,
D511464E21147B7C00197DCE /* ObservationTokenTests.swift */,
);
path = Library;
sourceTree = "<group>";
Expand All @@ -394,6 +416,7 @@
D292DB001F6AA06B0060F614 /* SyncStorageTests.swift */,
D292DB031F6AA0730060F614 /* AsyncStorageTests.swift */,
D236F3191F6BEF73004EE01F /* StorageTests.swift */,
D511464A2114775100197DCE /* StorageObservationRegistryTests.swift */,
);
path = Storage;
sourceTree = "<group>";
Expand Down Expand Up @@ -794,6 +817,7 @@
files = (
D221E5C620D00DDB00BC940E /* DiskStorage.swift in Sources */,
D21B669D1F6A724600125DE1 /* DiskConfig.swift in Sources */,
D5A9D1C521144B65005DBD3F /* StorageObservationRegistry.swift in Sources */,
D21B66871F6A723C00125DE1 /* ExpirationMode.swift in Sources */,
D21B66881F6A723C00125DE1 /* Expiry.swift in Sources */,
D270147620D101F3003B45C7 /* StorageAware.swift in Sources */,
Expand All @@ -812,6 +836,7 @@
D21B669A1F6A724300125DE1 /* Date+Extensions.swift in Sources */,
D21B66891F6A723C00125DE1 /* ImageWrapper.swift in Sources */,
D21B668B1F6A723C00125DE1 /* StorageError.swift in Sources */,
D5A9D1B921134547005DBD3F /* ObservationToken.swift in Sources */,
D270148A20D11040003B45C7 /* Storage+Transform.swift in Sources */,
D5A138C21EB29BFA00881A20 /* UIImage+Extensions.swift in Sources */,
D21B66851F6A723C00125DE1 /* DataSerializer.swift in Sources */,
Expand All @@ -821,6 +846,7 @@
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 @@ -835,8 +861,10 @@
D27014A020D12870003B45C7 /* MemoryStorageTests.swift in Sources */,
D2CF98261F69427C00CE8F68 /* User.swift in Sources */,
D27014AE20D12D83003B45C7 /* AsyncStorageTests.swift in Sources */,
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 @@ -852,9 +880,11 @@
D2CF987C1F69513800CE8F68 /* Date+ExtensionsTests.swift in Sources */,
D28C9BAC1F67ECD400C180C1 /* TestHelper+iOS.swift in Sources */,
D2CF98211F69427C00CE8F68 /* TestHelper.swift in Sources */,
D511464F21147B7C00197DCE /* ObservationTokenTests.swift in Sources */,
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 All @@ -875,6 +905,7 @@
files = (
D221E5C520D00DDB00BC940E /* DiskStorage.swift in Sources */,
D21B669B1F6A724600125DE1 /* DiskConfig.swift in Sources */,
D5A9D1C421144B65005DBD3F /* StorageObservationRegistry.swift in Sources */,
D21B667E1F6A723C00125DE1 /* ExpirationMode.swift in Sources */,
D21B667F1F6A723C00125DE1 /* Expiry.swift in Sources */,
D270147520D101F3003B45C7 /* StorageAware.swift in Sources */,
Expand All @@ -893,6 +924,7 @@
D21B66991F6A724200125DE1 /* Date+Extensions.swift in Sources */,
D21B66801F6A723C00125DE1 /* ImageWrapper.swift in Sources */,
D21B66821F6A723C00125DE1 /* StorageError.swift in Sources */,
D5A9D1B821134547005DBD3F /* ObservationToken.swift in Sources */,
D270148920D11040003B45C7 /* Storage+Transform.swift in Sources */,
D5A138C41EB29C2100881A20 /* NSImage+Extensions.swift in Sources */,
D21B667C1F6A723C00125DE1 /* DataSerializer.swift in Sources */,
Expand All @@ -902,6 +934,7 @@
D28897061F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */,
D270148120D10982003B45C7 /* Storage.swift in Sources */,
D221E5C120D00DCC00BC940E /* Entry.swift in Sources */,
D5A9D1C021134776005DBD3F /* StoreChange.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -925,6 +958,7 @@
files = (
D221E5C420D00DDB00BC940E /* DiskStorage.swift in Sources */,
D2CF98681F694FFA00CE8F68 /* ImageWrapper.swift in Sources */,
D5A9D1C321144B65005DBD3F /* StorageObservationRegistry.swift in Sources */,
D2CF98871F695B8F00CE8F68 /* Types.swift in Sources */,
D2CF98621F694FFA00CE8F68 /* Date+Extensions.swift in Sources */,
D2CF98641F694FFA00CE8F68 /* DataSerializer.swift in Sources */,
Expand All @@ -943,6 +977,7 @@
D2CF98671F694FFA00CE8F68 /* Expiry.swift in Sources */,
D270148820D11040003B45C7 /* Storage+Transform.swift in Sources */,
D2CF986A1F694FFA00CE8F68 /* StorageError.swift in Sources */,
D5A9D1B721134547005DBD3F /* ObservationToken.swift in Sources */,
D5A138C11EB29BFA00881A20 /* UIImage+Extensions.swift in Sources */,
D270147820D1046A003B45C7 /* HybridStorage.swift in Sources */,
D270148420D10E76003B45C7 /* AsyncStorage.swift in Sources */,
Expand All @@ -952,6 +987,7 @@
D2CF98611F694FFA00CE8F68 /* MemoryConfig.swift in Sources */,
D2CF98661F694FFA00CE8F68 /* ExpirationMode.swift in Sources */,
D221E5C020D00DCC00BC940E /* Entry.swift in Sources */,
D5A9D1BF21134776005DBD3F /* StoreChange.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
11 changes: 11 additions & 0 deletions Source/Shared/Library/ObservationToken.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
public final class ObservationToken {
private let cancellationClosure: () -> Void

init(cancellationClosure: @escaping () -> Void) {
self.cancellationClosure = cancellationClosure
}

public func cancel() {
cancellationClosure()
}
}
7 changes: 6 additions & 1 deletion Source/Shared/Storage/HybridStorage.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Foundation

/// Use both memory and disk storage. Try on memory first.
public class HybridStorage<T> {
public final class HybridStorage<T> {
public let memoryStorage: MemoryStorage<T>
public let diskStorage: DiskStorage<T>
public let storageObservationRegistry = StorageObservationRegistry<HybridStorage>()

public init(memoryStorage: MemoryStorage<T>, diskStorage: DiskStorage<T>) {
self.memoryStorage = memoryStorage
Expand All @@ -26,21 +27,25 @@ 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 {
memoryStorage.setObject(object, forKey: key, expiry: expiry)
try diskStorage.setObject(object, forKey: key, expiry: expiry)
storageObservationRegistry.notifyObservers(about: .add(key: key), in: self)
}

public func removeAll() throws {
memoryStorage.removeAll()
try diskStorage.removeAll()
storageObservationRegistry.notifyObservers(about: .removeAll, in: self)
}

public func removeExpiredObjects() throws {
memoryStorage.removeExpiredObjects()
try diskStorage.removeExpiredObjects()
storageObservationRegistry.notifyObservers(about: .removeExpired, in: self)
}
}

Expand Down
20 changes: 18 additions & 2 deletions Source/Shared/Storage/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import Dispatch

/// Manage storage. Use memory storage if specified.
/// Synchronous by default. Use `async` for asynchronous operations.
public class Storage<T> {
public final class Storage<T> {
/// Used for sync operations
let syncStorage: SyncStorage<T>
let asyncStorage: AsyncStorage<T>

public let storageObservationRegistry = StorageObservationRegistry<Storage>()

/// Initialize storage with configuration options.
///
/// - Parameters:
Expand All @@ -23,7 +25,6 @@ public class Storage<T> {
storage: hybridStorage,
serialQueue: DispatchQueue(label: "Cache.SyncStorage.SerialQueue")
)

let asyncStorage = AsyncStorage(
storage: hybridStorage,
serialQueue: DispatchQueue(label: "Cache.AsyncStorage.SerialQueue")
Expand All @@ -39,10 +40,25 @@ public class Storage<T> {
public required init(syncStorage: SyncStorage<T>, asyncStorage: AsyncStorage<T>) {
self.syncStorage = syncStorage
self.asyncStorage = asyncStorage
subscribeToChanges()
}

/// Used for async operations
public lazy var async = self.asyncStorage

private func subscribeToChanges() {
subscribeToChanges(in: syncStorage.innerStorage)
if syncStorage.innerStorage !== asyncStorage.innerStorage {
subscribeToChanges(in: asyncStorage.innerStorage)
}
}

private func subscribeToChanges(in storage: HybridStorage<T>) {
storage.storageObservationRegistry.addObservation { [weak self] _, change in
guard let strongSelf = self else { return }
strongSelf.storageObservationRegistry.notifyObservers(about: change, in: strongSelf)
}
}
}

extension Storage: StorageAware {
Expand Down
30 changes: 30 additions & 0 deletions Source/Shared/Storage/StorageObservationRegistry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

public final class StorageObservationRegistry<T: StorageAware> {
public typealias Observation = (T, StorageChange) -> Void
private(set) var observations = [UUID: Observation]()

@discardableResult
public func addObservation(_ observation: @escaping Observation) -> ObservationToken {
let id = UUID()
observations[id] = observation

return ObservationToken { [weak self] in
self?.observations.removeValue(forKey: id)
}
}

public func removeObservation(token: ObservationToken) {
token.cancel()
}

public func removeAllObservations() {
observations.removeAll()
}

func notifyObservers(about change: StorageChange, in storage: T) {
observations.values.forEach { closure in
closure(storage, change)
}
}
}
17 changes: 17 additions & 0 deletions Source/Shared/Storage/StoreChange.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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
}
}
15 changes: 15 additions & 0 deletions Tests/iOS/Tests/Library/ObservationTokenTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import XCTest
@testable import Cache

final class ObservationTokenTests: XCTestCase {
func testCancel() {
var cancelled = false

let token = ObservationToken {
cancelled = true
}

token.cancel()
XCTAssertTrue(cancelled)
}
}
24 changes: 24 additions & 0 deletions Tests/iOS/Tests/Storage/HybridStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,28 @@ final class HybridStorageTests: XCTestCase {
XCTAssertNotNil(try? storage.object(forKey: key2))
}
}

func testAddObservations() throws {
var changes = [StorageChange]()

storage.storageObservationRegistry.addObservation { storage, change in
changes.append(change)
}

try storage.setObject(testObject, forKey: "user1")
try storage.setObject(testObject, forKey: "user2")
try storage.removeObject(forKey: "user1")
try storage.removeExpiredObjects()
try storage.removeAll()

let expectedChanges: [StorageChange] = [
.add(key: "user1"),
.add(key: "user2"),
.remove(key: "user1"),
.removeExpired,
.removeAll
]

XCTAssertEqual(changes, expectedChanges)
}
}
Loading

0 comments on commit 9986637

Please sign in to comment.