Wrap cache object in CacheEntry which will hold both object and metadata

This commit is contained in:
Kirmanie L Ravariere
2017-05-11 14:49:18 -04:00
parent a2361a4b17
commit c8f002657e
12 changed files with 213 additions and 143 deletions
+9 -9
View File
@@ -7,9 +7,9 @@
objects = {
/* Begin PBXBuildFile section */
57506FAE1EC29437009B71E9 /* ObjectMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* ObjectMetadata.swift */; };
57506FBF1EC2E1B7009B71E9 /* ObjectMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* ObjectMetadata.swift */; };
57506FC01EC2E1BA009B71E9 /* ObjectMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* ObjectMetadata.swift */; };
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 */; };
@@ -146,7 +146,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
57506FAD1EC29437009B71E9 /* ObjectMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectMetadata.swift; sourceTree = "<group>"; };
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>"; };
@@ -200,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 */
@@ -326,7 +326,7 @@
D5291C251C28220B00B702C9 /* MemoryStorage.swift */,
D5291C261C28220B00B702C9 /* StorageAware.swift */,
D5291C271C28220B00B702C9 /* StorageFactory.swift */,
57506FAD1EC29437009B71E9 /* ObjectMetadata.swift */,
57506FAD1EC29437009B71E9 /* CacheEntry.swift */,
);
path = Storage;
sourceTree = "<group>";
@@ -845,7 +845,7 @@
BDEDD36D1DBCE5D8007416A6 /* SyncCache.swift in Sources */,
BDEDD36A1DBCE5D8007416A6 /* NSDate+Cache.swift in Sources */,
BDEDD36F1DBCE5D8007416A6 /* DiskStorage.swift in Sources */,
57506FC01EC2E1BA009B71E9 /* ObjectMetadata.swift in Sources */,
57506FC01EC2E1BA009B71E9 /* CacheEntry.swift in Sources */,
D5A138C21EB29BFA00881A20 /* UIImage+Cache.swift in Sources */,
BDEDD3601DBCE5CE007416A6 /* BasicHybridCache.swift in Sources */,
BDEDD3701DBCE5D8007416A6 /* MemoryStorage.swift in Sources */,
@@ -928,7 +928,7 @@
D5291D911C283CFB00B702C9 /* String+Cache.swift in Sources */,
D5291D901C283CFB00B702C9 /* NSDate+Cache.swift in Sources */,
D5A138C41EB29C2100881A20 /* NSImage+Cache.swift in Sources */,
57506FBF1EC2E1B7009B71E9 /* ObjectMetadata.swift in Sources */,
57506FBF1EC2E1B7009B71E9 /* CacheEntry.swift in Sources */,
D5291D931C283CFB00B702C9 /* DiskStorage.swift in Sources */,
D5291D861C283CFB00B702C9 /* BasicHybridCache.swift in Sources */,
D5ACACCD1CD0207300567809 /* SyncHybridCache.swift in Sources */,
@@ -967,7 +967,7 @@
D5291C321C28220B00B702C9 /* StorageKind.swift in Sources */,
D5291C301C28220B00B702C9 /* Expiry.swift in Sources */,
D5291C331C28220B00B702C9 /* JSON+Cache.swift in Sources */,
57506FAE1EC29437009B71E9 /* ObjectMetadata.swift in Sources */,
57506FAE1EC29437009B71E9 /* CacheEntry.swift in Sources */,
D5A138C11EB29BFA00881A20 /* UIImage+Cache.swift in Sources */,
D5291C2F1C28220B00B702C9 /* Capsule.swift in Sources */,
D5291C2D1C28220B00B702C9 /* Config.swift in Sources */,
+30 -27
View File
@@ -102,38 +102,41 @@ 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)
return
}
guard let weakSelf = self else {
completion(object)
return
}
weakSelf.backStorage.object(key) { (object: T?) in
guard let object = object else {
completion(nil)
return
}
weakSelf.copyToFrontStorage(key, object: object, completion: completion)
}
func object<T: Cachable>(forKey key: String, completion: @escaping (_ object: T?) -> Void){
cacheEntry(forKey: key) { (entry: CacheEntry<T>?) in
completion(entry?.object)
}
}
private func copyToFrontStorage<T: Cachable>(_ key: String, object: T, completion: @escaping (_ object: T?) -> Void) {
guard let metadata = self.backStorage.objectMetadata(key) else {
completion(nil)
/**
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(entry)
return
}
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)
}
}
}
self.frontStorage.add(key, object: object, expiry: metadata.expiry, completion: { _ in
completion(object)
})
}
/**
+10 -1
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,4 @@
public struct CacheEntry<T> {
public let object: T
public let expiry: Expiry
}
+36 -21
View File
@@ -85,43 +85,58 @@ public final class DiskStorage: StorageAware {
/**
Gets information about the cached object.
- Parameter key: Unique key to identify the object in the cache
*/
public func objectMetadata(_ key: String) -> ObjectMetadata? {
do {
let attributes = try fileManager.attributesOfItem(atPath: filePath(key))
let fileModifiedDate = Expiry.date(attributes[FileAttributeKey.modificationDate] as! Date)
return ObjectMetadata(expiry: fileModifiedDate)
} catch {}
return nil
}
/**
Tries to retrieve the object from the disk 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 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.
+28 -21
View File
@@ -64,40 +64,47 @@ public final class MemoryStorage: StorageAware {
completion?()
}
}
/**
Gets information about the cached object.
- Parameter key: Unique key to identify the object in the cache
*/
public func objectMetadata(_ key: String) -> ObjectMetadata? {
guard let capsule = cache.object(forKey: key as AnyObject) as? Capsule else {
return nil
}
return ObjectMetadata(expiry: Expiry.date(capsule.expiryDate))
}
/**
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)
}
}
@@ -1,9 +0,0 @@
public struct ObjectMetadata {
let expiry: Expiry
}
extension ObjectMetadata: Equatable {}
public func ==(lhs: ObjectMetadata, rhs: ObjectMetadata) -> Bool {
return lhs.expiry.date == rhs.expiry.date
}
+3 -2
View File
@@ -16,11 +16,12 @@ public protocol CacheAware {
func add<T: Cachable>(_ key: String, object: T, expiry: Expiry, completion: (() -> Void)?)
/**
Gets information about the cached 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
*/
func objectMetadata(_ key: String) -> ObjectMetadata?
func cacheEntry<T: Cachable>(_ key: String, completion: @escaping (_ object: CacheEntry<T>?) -> Void)
/**
Tries to retrieve the object from the cache.
+42 -24
View File
@@ -65,11 +65,29 @@ 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))
@@ -77,36 +95,36 @@ class CacheSpec: QuickSpec {
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 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
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
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()
}
}
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()
}
}
}
}
}
}
+24 -18
View File
@@ -65,31 +65,37 @@ class DiskStorageSpec: QuickSpec {
}
}
describe("#objectMetadata") {
it("returns nil if object doesn't exist") {
let storage = DiskStorage(name: name)
let metadata = storage.objectMetadata(key)
expect(metadata).to(beNil())
}
it("returns object metadata if object exists") {
describe("#cacheEntry") {
it("returns nil if entry doesn't exist") {
let storage = DiskStorage(name: name)
waitUntil(timeout: 2.0) { done in
storage.add(key, object: object) {
let metadata = storage.objectMetadata(key)
let attributes = try! fileManager.attributesOfItem(atPath: storage.filePath(key))
let fileModifiedDate = Expiry.date(attributes[FileAttributeKey.modificationDate] as! Date)
let expectedMetadata = ObjectMetadata(expiry: fileModifiedDate)
expect(metadata).to(equal(expectedMetadata))
storage.cacheEntry(key) { (entry: CacheEntry<User>?) in
expect(entry).to(beNil())
done()
}
}
}
it("returns entry if object exists") {
let storage = DiskStorage(name: name)
waitUntil(timeout: 2.0) { done in
storage.add(key, object: object) {
storage.cacheEntry(key) { (entry: CacheEntry<User>?) in
let attributes = try! fileManager.attributesOfItem(atPath: storage.filePath(key))
let expiry = Expiry.date(attributes[FileAttributeKey.modificationDate] as! Date)
expect(entry?.object.firstName).to(equal(object.firstName))
expect(entry?.object.lastName).to(equal(object.lastName))
expect(entry?.expiry.date).to(equal(expiry.date))
done()
}
}
}
}
}
describe("#object") {
+17 -11
View File
@@ -48,26 +48,32 @@ class MemoryStorageSpec: QuickSpec {
}
}
describe("#objectMetadata") {
it("returns nil if object doesn't exist") {
let storage = MemoryStorage(name: name)
describe("#cacheEntry") {
it("returns nil if entry doesn't exist") {
let storage = DiskStorage(name: name)
let metadata = storage.objectMetadata(key)
expect(metadata).to(beNil())
waitUntil(timeout: 2.0) { done in
storage.cacheEntry(key) { (entry: CacheEntry<User>?) in
expect(entry).to(beNil())
done()
}
}
}
it("returns object metadata if object exists") {
it("returns entry if object exists") {
let storage = MemoryStorage(name: name)
let expiry = Expiry.date(Date())
waitUntil(timeout: 2.0) { done in
storage.add(key, object: object, expiry: expiry) {
let metadata = storage.objectMetadata(key)
let expectedMetadata = ObjectMetadata(expiry: expiry)
expect(metadata).to(equal(expectedMetadata))
done()
storage.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(expiry.date))
done()
}
}
}
}