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

Automatically clear expired objects #294

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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 Source/Shared/Configuration/DiskConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ public struct DiskConfig {
/// Expiry date that will be applied by default for every added object
/// if it's not overridden in the add(key: object: expiry: completion:) method
public let expiry: Expiry

/// ExpirationMode that will be applied for every added object
public let expirationMode: ExpirationMode

/// Maximum size of the disk cache storage (in bytes)
public let maxSize: UInt
/// A folder to store the disk cache contents. Defaults to a prefixed directory in Caches if nil
Expand All @@ -15,11 +19,15 @@ public struct DiskConfig {
/// Support only on iOS and tvOS.
public let protectionType: FileProtectionType?

public init(name: String, expiry: Expiry = .never,
public init(name: String,
expiry: Expiry = .never,
expirationMode: ExpirationMode = .auto,
maxSize: UInt = 0, directory: URL? = nil,
protectionType: FileProtectionType? = nil) {
self.name = name
self.expiry = expiry
self.expirationMode = expirationMode

self.maxSize = maxSize
self.directory = directory
self.protectionType = protectionType
Expand Down
39 changes: 23 additions & 16 deletions Source/Shared/Configuration/MemoryConfig.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import Foundation

public struct MemoryConfig {
/// Expiry date that will be applied by default for every added object
/// if it's not overridden in the add(key: object: expiry: completion:) method
public let expiry: Expiry
/// The maximum number of objects in memory the cache should hold.
/// If 0, there is no count limit. The default value is 0.
public let countLimit: UInt

/// The maximum total cost that the cache can hold before it starts evicting objects.
/// If 0, there is no total cost limit. The default value is 0
public let totalCostLimit: UInt

public init(expiry: Expiry = .never, countLimit: UInt = 0, totalCostLimit: UInt = 0) {
self.expiry = expiry
self.countLimit = countLimit
self.totalCostLimit = totalCostLimit
}
/// Expiry date that will be applied by default for every added object
/// if it's not overridden in the add(key: object: expiry: completion:) method
public let expiry: Expiry

/// ExpirationMode that will be applied for every added object
public let expirationMode: ExpirationMode

/// The maximum number of objects in memory the cache should hold.
/// If 0, there is no count limit. The default value is 0.
public let countLimit: UInt

/// The maximum total cost that the cache can hold before it starts evicting objects.
/// If 0, there is no total cost limit. The default value is 0
public let totalCostLimit: UInt

public init(expiry: Expiry = .never, expirationMode: ExpirationMode = .auto, countLimit: UInt = 0, totalCostLimit: UInt = 0) {
self.expiry = expiry
self.expirationMode = expirationMode

self.countLimit = countLimit
self.totalCostLimit = totalCostLimit

}
}
6 changes: 6 additions & 0 deletions Source/Shared/Storage/AsyncStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,9 @@ public extension AsyncStorage {
return storage
}
}

public extension AsyncStorage {
func applyExpiratonMode(_ expirationMode: ExpirationMode) {
self.innerStorage.applyExpiratonMode(expirationMode)
}
}
32 changes: 31 additions & 1 deletion Source/Shared/Storage/DiskStorage.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import Foundation
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit
#endif

/// Save objects to file on disk
final public class DiskStorage<Key: Hashable, Value> {
Expand All @@ -17,6 +21,9 @@ final public class DiskStorage<Key: Hashable, Value> {

private let transformer: Transformer<Value>
private let hasher = Hasher.constantAccrossExecutions()

private var didEnterBackgroundObserver: NSObjectProtocol?


// MARK: - Initialization
public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<Value>) throws {
Expand Down Expand Up @@ -54,6 +61,29 @@ final public class DiskStorage<Key: Hashable, Value> {
self.fileManager = fileManager
self.path = path
self.transformer = transformer
applyExpiratonMode(self.config.expirationMode)
}

public func applyExpiratonMode(_ expirationMode: ExpirationMode) {
if let didEnterBackgroundObserver = didEnterBackgroundObserver {
Copy link

Choose a reason for hiding this comment

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

Suggested change
if let didEnterBackgroundObserver = didEnterBackgroundObserver {
if let didEnterBackgroundObserver {

NotificationCenter.default.removeObserver(didEnterBackgroundObserver)
}

if expirationMode == .auto {
didEnterBackgroundObserver =
NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification,
object: nil,
queue: nil) { [weak self] _ in
guard let `self` = self else { return }
Copy link

Choose a reason for hiding this comment

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

Suggested change
guard let `self` = self else { return }
guard let self else { return }

try? self.removeExpiredObjects()
}
}
}

deinit {
if let didEnterBackgroundObserver = didEnterBackgroundObserver {
NotificationCenter.default.removeObserver(didEnterBackgroundObserver)
}
}
}

Expand Down
7 changes: 7 additions & 0 deletions Source/Shared/Storage/HybridStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,10 @@ public extension HybridStorage {
return self.diskStorage.totalSize
}
}

public extension HybridStorage {
func applyExpiratonMode(_ expirationMode: ExpirationMode) {
self.memoryStorage.applyExpiratonMode(expirationMode)
self.diskStorage.applyExpiratonMode(expirationMode)
}
}
40 changes: 38 additions & 2 deletions Source/Shared/Storage/MemoryStorage.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import Foundation
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit
#endif

public class MemoryStorage<Key: Hashable, Value>: StorageAware {
final class WrappedKey: NSObject {
Expand All @@ -22,11 +26,39 @@ public class MemoryStorage<Key: Hashable, Value>: StorageAware {
fileprivate var keys = Set<Key>()
/// Configuration
fileprivate let config: MemoryConfig

/// The closure to be called when the key has been removed
public var onRemove: ((Key) -> Void)?

public var didEnterBackgroundObserver: NSObjectProtocol?

public init(config: MemoryConfig) {
self.config = config
self.cache.countLimit = Int(config.countLimit)
self.cache.totalCostLimit = Int(config.totalCostLimit)
applyExpiratonMode(self.config.expirationMode)
}

public func applyExpiratonMode(_ expirationMode: ExpirationMode) {
if let didEnterBackgroundObserver = didEnterBackgroundObserver {
NotificationCenter.default.removeObserver(didEnterBackgroundObserver)
}
if expirationMode == .auto {
didEnterBackgroundObserver =
NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification,
object: nil,
queue: nil)
{ [weak self] _ in
guard let `self` = self else { return }
Copy link

Choose a reason for hiding this comment

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

Suggested change
guard let `self` = self else { return }
guard let self else { return }

self.removeExpiredObjects()
}
}
}

deinit {
if let didEnterBackgroundObserver = didEnterBackgroundObserver {
NotificationCenter.default.removeObserver(didEnterBackgroundObserver)
}
}
}

Expand All @@ -41,7 +73,10 @@ extension MemoryStorage {

public func setObject(_ object: Value, forKey key: Key, expiry: Expiry? = nil) {
let capsule = MemoryCapsule(value: object, expiry: .date(expiry?.date ?? config.expiry.date))
cache.setObject(capsule, forKey: WrappedKey(key))

/// MemoryLayout.size(ofValue:) return the contiguous memory footprint of the given instance , so cost is always MemoryCapsule size (8 bytes)
let cost = MemoryLayout.size(ofValue: capsule)
cache.setObject(capsule, forKey: WrappedKey(key), cost: cost)
keys.insert(key)
}

Expand All @@ -66,6 +101,7 @@ extension MemoryStorage {
public func removeObject(forKey key: Key) {
cache.removeObject(forKey: WrappedKey(key))
keys.remove(key)
onRemove?(key)
}

public func entry(forKey key: Key) throws -> Entry<Value> {
Expand Down
6 changes: 6 additions & 0 deletions Source/Shared/Storage/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,9 @@ public extension Storage {
return self.hybridStorage.diskStorage.totalSize
}
}

public extension Storage {
func applyExpiratonMode(_ expirationMode: ExpirationMode) {
self.hybridStorage.applyExpiratonMode(expirationMode)
Copy link

Choose a reason for hiding this comment

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

Suggested change
self.hybridStorage.applyExpiratonMode(expirationMode)
hybridStorage.applyExpiratonMode(expirationMode)

}
}
6 changes: 6 additions & 0 deletions Source/Shared/Storage/SyncStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@ public extension SyncStorage {
return storage
}
}

public extension SyncStorage {
func applyExpiratonMode(_ expirationMode: ExpirationMode) {
self.innerStorage.applyExpiratonMode(expirationMode)
}
}
64 changes: 64 additions & 0 deletions Tests/iOS/Tests/Storage/AsyncStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,68 @@ final class AsyncStorageTests: XCTestCase {

wait(for: [expectation], timeout: 1)
}

func testAutoClearAllExpiredObjectWhenApplicationEnterBackground() {
let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
let expiry2: Expiry = .date(Date().addingTimeInterval(10))
let key1 = "item1"
let key2 = "item2"
var key1Removed = false
var key2Removed = false
storage.innerStorage.memoryStorage.onRemove = { key in
key1Removed = true
key2Removed = true
XCTAssertTrue(key1Removed)
XCTAssertTrue(key2Removed)
}

storage.innerStorage.diskStorage.onRemove = { path in
key1Removed = true
key2Removed = true
XCTAssertTrue(key1Removed)
XCTAssertTrue(key2Removed)
}

storage.setObject(user, forKey: key1, expiry: expiry1) { _ in

}
storage.setObject(user, forKey: key2, expiry: expiry2) { _ in

}
///Device enters background
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
}

func testManualManageExpirationMode() {
storage.applyExpiratonMode(.manual)
let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
let expiry2: Expiry = .date(Date().addingTimeInterval(60))
let key1 = "item1"
let key2 = "item2"

var key1Removed = false
var key2Removed = false
storage.innerStorage.memoryStorage.onRemove = { key in
key1Removed = true
key2Removed = true
XCTAssertFalse(key1Removed)
XCTAssertFalse(key2Removed)
}

storage.innerStorage.diskStorage.onRemove = { path in
key1Removed = true
key2Removed = true
XCTAssertFalse(key1Removed)
XCTAssertFalse(key2Removed)
}

storage.setObject(user, forKey: key1, expiry: expiry1) { _ in

}
storage.setObject(user, forKey: key2, expiry: expiry2) { _ in

}
///Device enters background
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
}
}
32 changes: 32 additions & 0 deletions Tests/iOS/Tests/Storage/DiskStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,5 +215,37 @@ final class DiskStorageTests: XCTestCase {
let filePath = "\(storage.path)/\(storage.makeFileName(for: key))"
XCTAssertEqual(storage.makeFilePath(for: key), filePath)
}

func testAutoClearAllExpiredObjectWhenApplicationEnterBackground() {
let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
let expiry2: Expiry = .date(Date().addingTimeInterval(10))
let key1 = "item1"
let key2 = "item2"
let filePathForKey1 = storage.makeFilePath(for: key1)
storage.onRemove = { key in
XCTAssertTrue(key == filePathForKey1)
}
try? storage.setObject(testObject, forKey: key1, expiry: expiry1)
try? storage.setObject(testObject, forKey: key2, expiry: expiry2)
///Device enters background
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
}

func testManualManageExpirationMode() {
storage.applyExpiratonMode(.manual)
let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
let expiry2: Expiry = .date(Date().addingTimeInterval(10))
let key1 = "item1"
let key2 = "item2"
var success = true
storage.onRemove = { key in
success = false
XCTAssertTrue(success)
}
try? storage.setObject(testObject, forKey: key1, expiry: expiry1)
try? storage.setObject(testObject, forKey: key2, expiry: expiry2)
///Device enters background
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
}
}

55 changes: 55 additions & 0 deletions Tests/iOS/Tests/Storage/HybridStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,59 @@ final class HybridStorageTests: XCTestCase {
storage.removeAllKeyObservers()
XCTAssertTrue(storage.keyObservations.isEmpty)
}

func testAutoClearAllExpiredObjectWhenApplicationEnterBackground() {
let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
let expiry2: Expiry = .date(Date().addingTimeInterval(10))
let key1 = "item1"
let key2 = "item2"
var key1Removed = false
var key2Removed = false
storage.memoryStorage.onRemove = { key in
key1Removed = true
key2Removed = true
XCTAssertTrue(key1Removed)
XCTAssertTrue(key2Removed)
}

storage.diskStorage.onRemove = { path in
key1Removed = true
key2Removed = true
XCTAssertTrue(key1Removed)
XCTAssertTrue(key2Removed)
}

try? storage.setObject(testObject, forKey: key1, expiry: expiry1)
try? storage.setObject(testObject, forKey: key2, expiry: expiry2)
///Device enters background
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
}

func testManualManageExpirationMode() {
storage.applyExpiratonMode(.manual)
let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
let expiry2: Expiry = .date(Date().addingTimeInterval(10))
let key1 = "item1"
let key2 = "item2"
var key1Removed = false
var key2Removed = false
storage.memoryStorage.onRemove = { key in
key1Removed = true
key2Removed = true
XCTAssertFalse(key1Removed)
XCTAssertFalse(key2Removed)
}

storage.diskStorage.onRemove = { path in
key1Removed = true
key2Removed = true
XCTAssertFalse(key1Removed)
XCTAssertFalse(key2Removed)
}

try? storage.setObject(testObject, forKey: key1, expiry: expiry1)
try? storage.setObject(testObject, forKey: key2, expiry: expiry2)
///Device enters background
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
}
}
Loading