Skip to content

Commit

Permalink
Merge pull request #84 from kirmanie/update-cache-logic
Browse files Browse the repository at this point in the history
Update front storage with back storage when cache only found in back storage
  • Loading branch information
zenangst authored May 15, 2017
2 parents 26ea6f6 + c8f0026 commit 4bbe8e1
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 36 deletions.
10 changes: 9 additions & 1 deletion Cache.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
objects = {

/* Begin PBXBuildFile section */
57506FAE1EC29437009B71E9 /* CacheEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* CacheEntry.swift */; };
57506FBF1EC2E1B7009B71E9 /* CacheEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* CacheEntry.swift */; };
57506FC01EC2E1BA009B71E9 /* CacheEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* CacheEntry.swift */; };
BDEDD3601DBCE5CE007416A6 /* BasicHybridCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5291C6A1C2827FB00B702C9 /* BasicHybridCache.swift */; };
BDEDD3611DBCE5CE007416A6 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACACDD1CD0272600567809 /* Cache.swift */; };
BDEDD3621DBCE5CE007416A6 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5291C151C28220B00B702C9 /* Config.swift */; };
Expand Down Expand Up @@ -143,6 +146,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
57506FAD1EC29437009B71E9 /* CacheEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheEntry.swift; sourceTree = "<group>"; };
BDEDD3561DBCE5B1007416A6 /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BDEDD3781DBCEB8A007416A6 /* Cache-tvOS-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Cache-tvOS-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
D5291C151C28220B00B702C9 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -196,7 +200,7 @@
D5ACACCB1CD0207300567809 /* SyncHybridCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncHybridCache.swift; sourceTree = "<group>"; };
D5ACACCE1CD0227200567809 /* SyncCacheSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCacheSpec.swift; sourceTree = "<group>"; };
D5ACACD21CD0254300567809 /* SyncHybridCacheSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncHybridCacheSpec.swift; sourceTree = "<group>"; };
D5ACACDD1CD0272600567809 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
D5ACACDD1CD0272600567809 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; tabWidth = 2; };
D5DC59E01C20593E003BD79B /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -322,6 +326,7 @@
D5291C251C28220B00B702C9 /* MemoryStorage.swift */,
D5291C261C28220B00B702C9 /* StorageAware.swift */,
D5291C271C28220B00B702C9 /* StorageFactory.swift */,
57506FAD1EC29437009B71E9 /* CacheEntry.swift */,
);
path = Storage;
sourceTree = "<group>";
Expand Down Expand Up @@ -840,6 +845,7 @@
BDEDD36D1DBCE5D8007416A6 /* SyncCache.swift in Sources */,
BDEDD36A1DBCE5D8007416A6 /* NSDate+Cache.swift in Sources */,
BDEDD36F1DBCE5D8007416A6 /* DiskStorage.swift in Sources */,
57506FC01EC2E1BA009B71E9 /* CacheEntry.swift in Sources */,
D5A138C21EB29BFA00881A20 /* UIImage+Cache.swift in Sources */,
BDEDD3601DBCE5CE007416A6 /* BasicHybridCache.swift in Sources */,
BDEDD3701DBCE5D8007416A6 /* MemoryStorage.swift in Sources */,
Expand Down Expand Up @@ -922,6 +928,7 @@
D5291D911C283CFB00B702C9 /* String+Cache.swift in Sources */,
D5291D901C283CFB00B702C9 /* NSDate+Cache.swift in Sources */,
D5A138C41EB29C2100881A20 /* NSImage+Cache.swift in Sources */,
57506FBF1EC2E1B7009B71E9 /* CacheEntry.swift in Sources */,
D5291D931C283CFB00B702C9 /* DiskStorage.swift in Sources */,
D5291D861C283CFB00B702C9 /* BasicHybridCache.swift in Sources */,
D5ACACCD1CD0207300567809 /* SyncHybridCache.swift in Sources */,
Expand Down Expand Up @@ -960,6 +967,7 @@
D5291C321C28220B00B702C9 /* StorageKind.swift in Sources */,
D5291C301C28220B00B702C9 /* Expiry.swift in Sources */,
D5291C331C28220B00B702C9 /* JSON+Cache.swift in Sources */,
57506FAE1EC29437009B71E9 /* CacheEntry.swift in Sources */,
D5A138C11EB29BFA00881A20 /* UIImage+Cache.swift in Sources */,
D5291C2F1C28220B00B702C9 /* Capsule.swift in Sources */,
D5291C2D1C28220B00B702C9 /* Config.swift in Sources */,
Expand Down
55 changes: 40 additions & 15 deletions Source/Shared/BasicHybridCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,22 @@ public class BasicHybridCache: NSObject {
- Parameter name: A name of the cache
- Parameter config: Cache configuration
*/
public init(name: String, config: Config = Config.defaultConfig) {
public convenience init(name: String, config: Config = Config.defaultConfig) {
let frontStorage = StorageFactory.resolve(name, kind: config.frontKind, maxSize: UInt(config.maxObjects))
let backStorage = StorageFactory.resolve(name, kind: config.backKind, maxSize: config.maxSize)
self.init(name: name, frontStorage: frontStorage, backStorage: backStorage, config: config)

}
internal init(name: String, frontStorage: StorageAware, backStorage: StorageAware, config: Config) {
self.name = name
self.frontStorage = frontStorage
self.backStorage = backStorage
self.config = config

frontStorage = StorageFactory.resolve(name, kind: config.frontKind, maxSize: UInt(config.maxObjects))
backStorage = StorageFactory.resolve(name, kind: config.backKind, maxSize: config.maxSize)
super.init()

let notificationCenter = NotificationCenter.default

#if os(macOS)
notificationCenter.addObserver(self, selector: #selector(clearExpiredDataInBackStorage),
name: NSNotification.Name.NSApplicationWillTerminate, object: nil)
Expand All @@ -56,7 +62,7 @@ public class BasicHybridCache: NSObject {
name: .UIApplicationDidEnterBackground, object: nil)
#endif
}

/**
Removes notification center observer.
*/
Expand Down Expand Up @@ -96,20 +102,39 @@ public class BasicHybridCache: NSObject {
- Parameter key: Unique key to identify the object in the cache
- Parameter completion: Completion closure returns object or nil
*/
func object<T: Cachable>(forKey key: String, completion: @escaping (_ object: T?) -> Void) {
frontStorage.object(key) { [weak self] (object: T?) in
if let object = object {
completion(object)
func object<T: Cachable>(forKey key: String, completion: @escaping (_ object: T?) -> Void){
cacheEntry(forKey: key) { (entry: CacheEntry<T>?) in
completion(entry?.object)
}
}

/**
Tries to retrieve the cache entry from to the front and back cache storages.
- Parameter key: Unique key to identify the cache entry in the cache
- Parameter completion: Completion closure returns cache entry or nil
*/
func cacheEntry<T: Cachable>(forKey key: String, completion: @escaping (_ object: CacheEntry<T>?) -> Void) {
frontStorage.cacheEntry(key) { [weak self] (entry: CacheEntry<T>?) in
if let entry = entry {
completion(entry)
return
}

guard let weakSelf = self else {
completion(object)
completion(entry)
return
}

weakSelf.backStorage.object(key) { (object: T?) in
completion(object)

weakSelf.backStorage.cacheEntry(key) { (entry: CacheEntry<T>?) in
guard let entry = entry else {
completion(nil)
return
}

weakSelf.frontStorage.add(key, object: entry.object, expiry: entry.expiry) { _ in
completion(entry)
}
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion Source/Shared/Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,17 @@ public final class Cache<T: Cachable>: BasicHybridCache {
- Parameter key: Unique key to identify the object in the cache
- Parameter completion: Completion closure returns object or nil
*/

public func object(_ key: String, completion: @escaping (_ object: T?) -> Void) {
super.object(forKey: key, completion: completion)
}

/**
Tries to retrieve the cache entry from to the front and back cache storages.
- Parameter key: Unique key to identify the cache entry in the cache
- Parameter completion: Completion closure returns cache entry or nil
*/
public func cacheEntry(_ key: String, completion: @escaping (_ object: CacheEntry<T>?) -> Void) {
super.cacheEntry(forKey: key, completion: completion)
}
}
10 changes: 10 additions & 0 deletions Source/Shared/HybridCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,14 @@ public class HybridCache: BasicHybridCache {
public func object<T: Cachable>(_ key: String, completion: @escaping (_ object: T?) -> Void) {
super.object(forKey: key, completion: completion)
}

/**
Tries to retrieve the cache entry from to the front and back cache storages.
- Parameter key: Unique key to identify the cache entry in the cache
- Parameter completion: Completion closure returns cache entry or nil
*/
public func cacheEntry<T: Cachable>(_ key: String, completion: @escaping (_ object: CacheEntry<T>?) -> Void) {
super.cacheEntry(forKey: key, completion: completion)
}
}
4 changes: 4 additions & 0 deletions Source/Shared/Storage/CacheEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public struct CacheEntry<T> {
public let object: T
public let expiry: Expiry
}
46 changes: 39 additions & 7 deletions Source/Shared/Storage/DiskStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,30 +81,62 @@ public final class DiskStorage: StorageAware {
completion?()
}
}

/**
Tries to retrieve the object from the disk storage.
Gets information about the cached object.
- Parameter key: Unique key to identify the object in the cache
- Parameter completion: Completion closure returns object or nil
*/
public func object<T: Cachable>(_ key: String, completion: @escaping (_ object: T?) -> Void) {
cacheEntry(key) { (entry: CacheEntry<T>?) in
completion(entry?.object)
}
}

/**
Get cache entry which includes object with metadata.
- Parameter key: Unique key to identify the object in the cache
- Parameter completion: Completion closure returns object wrapper with metadata or nil
*/
public func cacheEntry<T: Cachable>(_ key: String, completion: @escaping (_ object: CacheEntry<T>?) -> Void) {
readQueue.async { [weak self] in
guard let weakSelf = self else {
completion(nil)
return
}

let filePath = weakSelf.filePath(key)
var cachedObject: T?

if let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) {
cachedObject = T.decode(data) as? T
}

completion(cachedObject)

if let cachedObject = cachedObject,
let expiry = weakSelf.cachedObjectExpiry(path: filePath) {

completion(CacheEntry(object: cachedObject, expiry: expiry))
return
}

completion(nil)
}
}

private func cachedObjectExpiry(path: String) -> Expiry? {
do {
let attributes = try fileManager.attributesOfItem(atPath: path)

guard let modificationDate = attributes[FileAttributeKey.modificationDate] as? Date else {
return nil
}
return Expiry.date(modificationDate)
} catch {}

return nil
}

/**
Removes the object from the cache by the given key.
Expand Down
37 changes: 29 additions & 8 deletions Source/Shared/Storage/MemoryStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,26 +64,47 @@ public final class MemoryStorage: StorageAware {
completion?()
}
}

/**
Tries to retrieve the object from the memory storage.
- Parameter key: Unique key to identify the object in the cache
- Parameter completion: Completion closure returns object or nil
*/

public func object<T: Cachable>(_ key: String, completion: @escaping (_ object: T?) -> Void) {
cacheEntry(key) { (entry: CacheEntry<T>?) in
completion(entry?.object)
}
}

/**
Get cache entry which includes object with metadata.
- Parameter key: Unique key to identify the object in the cache
- Parameter completion: Completion closure returns object wrapper with metadata or nil
*/
public func cacheEntry<T: Cachable>(_ key: String, completion: @escaping (_ object: CacheEntry<T>?) -> Void) {
readQueue.async { [weak self] in
guard let weakSelf = self else {
completion(nil)
return
}

let capsule = weakSelf.cache.object(forKey: key as AnyObject) as? Capsule
completion(capsule?.object as? T)

if let capsule = capsule {
weakSelf.removeIfExpired(key, capsule: capsule)

guard let capsule = weakSelf.cache.object(forKey: key as AnyObject) as? Capsule else {
completion(nil)
return
}

var entry: CacheEntry<T>?

if let object = capsule.object as? T {
entry = CacheEntry(object: object, expiry: Expiry.date(capsule.expiryDate))
}

completion(entry)

weakSelf.removeIfExpired(key, capsule: capsule)
}
}

Expand Down
8 changes: 8 additions & 0 deletions Source/Shared/Storage/StorageAware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ public protocol CacheAware {
*/
func add<T: Cachable>(_ key: String, object: T, expiry: Expiry, completion: (() -> Void)?)

/**
Get cache entry which includes object with metadata.
- Parameter key: Unique key to identify the object in the cache
- Parameter completion: Completion closure returns object wrapper with metadata or nil
*/
func cacheEntry<T: Cachable>(_ key: String, completion: @escaping (_ object: CacheEntry<T>?) -> Void)

/**
Tries to retrieve the object from the cache.
Expand Down
52 changes: 49 additions & 3 deletions Tests/iOS/Specs/CacheSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,67 @@ class CacheSpec: QuickSpec {
self.waitForExpectations(timeout: 8.0, handler:nil)
}
}


describe("#cacheEntry") {
it("resolves cache entry") {
waitUntil(timeout: 4.0) { done in
let expiryDate = Date()

cache.add(key, object: object, expiry: .date(expiryDate)) {
cache.cacheEntry(key) { (entry: CacheEntry<User>?) in
expect(entry?.object.firstName).to(equal(object.firstName))
expect(entry?.object.lastName).to(equal(object.lastName))
expect(entry?.expiry.date).to(equal(expiryDate))

done()
}
}
}
}
}

describe("#object") {
it("resolves cached object") {
let expectation = self.expectation(description: "Object Expectation")

cache.add(key, object: object) {
cache.object(key) { (receivedObject: User?) in
expect(receivedObject?.firstName).to(equal(object.firstName))
expect(receivedObject?.lastName).to(equal(object.lastName))
expectation.fulfill()
}
}

self.waitForExpectations(timeout: 4.0, handler:nil)
}

it("should resolve from disk and set in-memory cache if object not in-memory") {
let frontStorage = MemoryStorage(name: "MemoryStorage")
let backStorage = DiskStorage(name: "DiskStorage")
let config = Config.defaultConfig
let key = "myusernamedjohn"
let object = SpecHelper.user

let cache = Cache<User>(name: "MyCache", frontStorage: frontStorage, backStorage: backStorage, config: config)

waitUntil(timeout: 4.0) { done in

backStorage.add(key, object: object) {

cache.object(key) { (receivedObject: User?) in

expect(receivedObject?.firstName).to(equal(object.firstName))
expect(receivedObject?.lastName).to(equal(object.lastName))

frontStorage.object(key) { (inmemoryCachedUser: User?) in
expect(inmemoryCachedUser?.firstName).to(equal(object.firstName))
expect(inmemoryCachedUser?.lastName).to(equal(object.lastName))
done()
}
}
}
}
}
}

describe("#remove") {
Expand Down
Loading

0 comments on commit 4bbe8e1

Please sign in to comment.