Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update front storage with back storage when cache only found in back storage #84

Merged
merged 4 commits into from
May 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need to be public?

Copy link
Contributor Author

@kirmanie kirmanie May 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vadymmarkov I made it public because I wanted a method that would return, not just the object, but object and metadata; I have code that needs to read the expiry time of a cached object (that hasn't expired) and make decisions about how it can be used based on how old it is. Does this sound like a use case you guys want to support?

I wasn't sure if we should add a new method to the Cache public interface or have an overload for the object method that takes a completion lambda with a CacheEntry parameter. The side effect of the later is we lose a bit of the swift inference when passing a lambda to the object method; users will have to be explicit with their parameter types which means a breaking change. Hence the new method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense @kirmanie, I just find cacheEntry vs object a bit confusing, but don't really have a better name 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vadymmarkov yeah agreed! I wrestled with the name a bit. First I thought objectEntry but that means the same as object. And objectMetadata seemed too specific; felt like leaving a little wiggle room since it was an addition to the public interface. Maybe a better name might come to one of us in a dream, lol. Thanks for getting those changes in though; I'll be sure to open PRs/issues for anything I find as I continue to use this great library.

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