Skip to content

Commit 66cca77

Browse files
add auto clear object strategy
1 parent 174c2a6 commit 66cca77

14 files changed

+422
-22
lines changed

Source/Shared/Configuration/DiskConfig.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ public struct DiskConfig {
66
/// Expiry date that will be applied by default for every added object
77
/// if it's not overridden in the add(key: object: expiry: completion:) method
88
public let expiry: Expiry
9+
10+
/// ExpirationMode that will be applied for every added object
11+
public let expirationMode: ExpirationMode
12+
913
/// Maximum size of the disk cache storage (in bytes)
1014
public let maxSize: UInt
1115
/// A folder to store the disk cache contents. Defaults to a prefixed directory in Caches if nil
@@ -15,11 +19,15 @@ public struct DiskConfig {
1519
/// Support only on iOS and tvOS.
1620
public let protectionType: FileProtectionType?
1721

18-
public init(name: String, expiry: Expiry = .never,
22+
public init(name: String,
23+
expiry: Expiry = .never,
24+
expirationMode: ExpirationMode = .auto,
1925
maxSize: UInt = 0, directory: URL? = nil,
2026
protectionType: FileProtectionType? = nil) {
2127
self.name = name
2228
self.expiry = expiry
29+
self.expirationMode = expirationMode
30+
2331
self.maxSize = maxSize
2432
self.directory = directory
2533
self.protectionType = protectionType
Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
import Foundation
22

33
public struct MemoryConfig {
4-
/// Expiry date that will be applied by default for every added object
5-
/// if it's not overridden in the add(key: object: expiry: completion:) method
6-
public let expiry: Expiry
7-
/// The maximum number of objects in memory the cache should hold.
8-
/// If 0, there is no count limit. The default value is 0.
9-
public let countLimit: UInt
10-
11-
/// The maximum total cost that the cache can hold before it starts evicting objects.
12-
/// If 0, there is no total cost limit. The default value is 0
13-
public let totalCostLimit: UInt
14-
15-
public init(expiry: Expiry = .never, countLimit: UInt = 0, totalCostLimit: UInt = 0) {
16-
self.expiry = expiry
17-
self.countLimit = countLimit
18-
self.totalCostLimit = totalCostLimit
19-
}
4+
/// Expiry date that will be applied by default for every added object
5+
/// if it's not overridden in the add(key: object: expiry: completion:) method
6+
public let expiry: Expiry
7+
8+
/// ExpirationMode that will be applied for every added object
9+
public let expirationMode: ExpirationMode
10+
11+
/// The maximum number of objects in memory the cache should hold.
12+
/// If 0, there is no count limit. The default value is 0.
13+
public let countLimit: UInt
14+
15+
/// The maximum total cost that the cache can hold before it starts evicting objects.
16+
/// If 0, there is no total cost limit. The default value is 0
17+
public let totalCostLimit: UInt
18+
19+
public init(expiry: Expiry = .never, expirationMode: ExpirationMode = .auto, countLimit: UInt = 0, totalCostLimit: UInt = 0) {
20+
self.expiry = expiry
21+
self.expirationMode = expirationMode
22+
23+
self.countLimit = countLimit
24+
self.totalCostLimit = totalCostLimit
25+
26+
}
2027
}

Source/Shared/Storage/AsyncStorage.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,9 @@ public extension AsyncStorage {
127127
return storage
128128
}
129129
}
130+
131+
public extension AsyncStorage {
132+
func applyExpiratonMode(_ expirationMode: ExpirationMode) {
133+
self.innerStorage.applyExpiratonMode(expirationMode)
134+
}
135+
}

Source/Shared/Storage/DiskStorage.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import Foundation
1+
#if canImport(UIKit)
2+
import UIKit
3+
#elseif canImport(AppKit) && !targetEnvironment(macCatalyst)
4+
import AppKit
5+
#endif
26

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

1822
private let transformer: Transformer<Value>
1923
private let hasher = Hasher.constantAccrossExecutions()
24+
25+
private var didEnterBackgroundObserver: NSObjectProtocol?
26+
2027

2128
// MARK: - Initialization
2229
public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<Value>) throws {
@@ -54,6 +61,29 @@ final public class DiskStorage<Key: Hashable, Value> {
5461
self.fileManager = fileManager
5562
self.path = path
5663
self.transformer = transformer
64+
applyExpiratonMode(self.config.expirationMode)
65+
}
66+
67+
public func applyExpiratonMode(_ expirationMode: ExpirationMode) {
68+
if let didEnterBackgroundObserver = didEnterBackgroundObserver {
69+
NotificationCenter.default.removeObserver(didEnterBackgroundObserver)
70+
}
71+
72+
if expirationMode == .auto {
73+
didEnterBackgroundObserver =
74+
NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification,
75+
object: nil,
76+
queue: nil) { [weak self] _ in
77+
guard let `self` = self else { return }
78+
try? self.removeExpiredObjects()
79+
}
80+
}
81+
}
82+
83+
deinit {
84+
if let didEnterBackgroundObserver = didEnterBackgroundObserver {
85+
NotificationCenter.default.removeObserver(didEnterBackgroundObserver)
86+
}
5787
}
5888
}
5989

Source/Shared/Storage/HybridStorage.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,10 @@ public extension HybridStorage {
180180
return self.diskStorage.totalSize
181181
}
182182
}
183+
184+
public extension HybridStorage {
185+
func applyExpiratonMode(_ expirationMode: ExpirationMode) {
186+
self.memoryStorage.applyExpiratonMode(expirationMode)
187+
self.diskStorage.applyExpiratonMode(expirationMode)
188+
}
189+
}

Source/Shared/Storage/MemoryStorage.swift

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import Foundation
1+
#if canImport(UIKit)
2+
import UIKit
3+
#elseif canImport(AppKit) && !targetEnvironment(macCatalyst)
4+
import AppKit
5+
#endif
26

37
public class MemoryStorage<Key: Hashable, Value>: StorageAware {
48
final class WrappedKey: NSObject {
@@ -22,11 +26,39 @@ public class MemoryStorage<Key: Hashable, Value>: StorageAware {
2226
fileprivate var keys = Set<Key>()
2327
/// Configuration
2428
fileprivate let config: MemoryConfig
29+
30+
/// The closure to be called when the key has been removed
31+
public var onRemove: ((Key) -> Void)?
32+
33+
public var didEnterBackgroundObserver: NSObjectProtocol?
2534

2635
public init(config: MemoryConfig) {
2736
self.config = config
2837
self.cache.countLimit = Int(config.countLimit)
2938
self.cache.totalCostLimit = Int(config.totalCostLimit)
39+
applyExpiratonMode(self.config.expirationMode)
40+
}
41+
42+
public func applyExpiratonMode(_ expirationMode: ExpirationMode) {
43+
if let didEnterBackgroundObserver = didEnterBackgroundObserver {
44+
NotificationCenter.default.removeObserver(didEnterBackgroundObserver)
45+
}
46+
if expirationMode == .auto {
47+
didEnterBackgroundObserver =
48+
NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification,
49+
object: nil,
50+
queue: nil)
51+
{ [weak self] _ in
52+
guard let `self` = self else { return }
53+
self.removeExpiredObjects()
54+
}
55+
}
56+
}
57+
58+
deinit {
59+
if let didEnterBackgroundObserver = didEnterBackgroundObserver {
60+
NotificationCenter.default.removeObserver(didEnterBackgroundObserver)
61+
}
3062
}
3163
}
3264

@@ -41,8 +73,10 @@ extension MemoryStorage {
4173

4274
public func setObject(_ object: Value, forKey key: Key, expiry: Expiry? = nil) {
4375
let capsule = MemoryCapsule(value: object, expiry: .date(expiry?.date ?? config.expiry.date))
44-
let const = MemoryLayout.size(ofValue: capsule)
45-
cache.setObject(capsule, forKey: WrappedKey(key), cost: const)
76+
77+
/// MemoryLayout.size(ofValue:) return the contiguous memory footprint of the given instance , so cost is always MemoryCapsule size (8 bytes)
78+
let cost = MemoryLayout.size(ofValue: capsule)
79+
cache.setObject(capsule, forKey: WrappedKey(key), cost: cost)
4680
keys.insert(key)
4781
}
4882

@@ -67,6 +101,7 @@ extension MemoryStorage {
67101
public func removeObject(forKey key: Key) {
68102
cache.removeObject(forKey: WrappedKey(key))
69103
keys.remove(key)
104+
onRemove?(key)
70105
}
71106

72107
public func entry(forKey key: Key) throws -> Entry<Value> {

Source/Shared/Storage/Storage.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,9 @@ public extension Storage {
123123
return self.hybridStorage.diskStorage.totalSize
124124
}
125125
}
126+
127+
public extension Storage {
128+
func applyExpiratonMode(_ expirationMode: ExpirationMode) {
129+
self.hybridStorage.applyExpiratonMode(expirationMode)
130+
}
131+
}

Source/Shared/Storage/SyncStorage.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,9 @@ public extension SyncStorage {
7373
return storage
7474
}
7575
}
76+
77+
public extension SyncStorage {
78+
func applyExpiratonMode(_ expirationMode: ExpirationMode) {
79+
self.innerStorage.applyExpiratonMode(expirationMode)
80+
}
81+
}

Tests/iOS/Tests/Storage/AsyncStorageTests.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,68 @@ final class AsyncStorageTests: XCTestCase {
6262

6363
wait(for: [expectation], timeout: 1)
6464
}
65+
66+
func testAutoClearAllExpiredObjectWhenApplicationEnterBackground() {
67+
let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
68+
let expiry2: Expiry = .date(Date().addingTimeInterval(10))
69+
let key1 = "item1"
70+
let key2 = "item2"
71+
var key1Removed = false
72+
var key2Removed = false
73+
storage.innerStorage.memoryStorage.onRemove = { key in
74+
key1Removed = true
75+
key2Removed = true
76+
XCTAssertTrue(key1Removed)
77+
XCTAssertTrue(key2Removed)
78+
}
79+
80+
storage.innerStorage.diskStorage.onRemove = { path in
81+
key1Removed = true
82+
key2Removed = true
83+
XCTAssertTrue(key1Removed)
84+
XCTAssertTrue(key2Removed)
85+
}
86+
87+
storage.setObject(user, forKey: key1, expiry: expiry1) { _ in
88+
89+
}
90+
storage.setObject(user, forKey: key2, expiry: expiry2) { _ in
91+
92+
}
93+
///Device enters background
94+
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
95+
}
96+
97+
func testManualManageExpirationMode() {
98+
storage.applyExpiratonMode(.manual)
99+
let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
100+
let expiry2: Expiry = .date(Date().addingTimeInterval(60))
101+
let key1 = "item1"
102+
let key2 = "item2"
103+
104+
var key1Removed = false
105+
var key2Removed = false
106+
storage.innerStorage.memoryStorage.onRemove = { key in
107+
key1Removed = true
108+
key2Removed = true
109+
XCTAssertFalse(key1Removed)
110+
XCTAssertFalse(key2Removed)
111+
}
112+
113+
storage.innerStorage.diskStorage.onRemove = { path in
114+
key1Removed = true
115+
key2Removed = true
116+
XCTAssertFalse(key1Removed)
117+
XCTAssertFalse(key2Removed)
118+
}
119+
120+
storage.setObject(user, forKey: key1, expiry: expiry1) { _ in
121+
122+
}
123+
storage.setObject(user, forKey: key2, expiry: expiry2) { _ in
124+
125+
}
126+
///Device enters background
127+
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
128+
}
65129
}

Tests/iOS/Tests/Storage/DiskStorageTests.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,5 +215,37 @@ final class DiskStorageTests: XCTestCase {
215215
let filePath = "\(storage.path)/\(storage.makeFileName(for: key))"
216216
XCTAssertEqual(storage.makeFilePath(for: key), filePath)
217217
}
218+
219+
func testAutoClearAllExpiredObjectWhenApplicationEnterBackground() {
220+
let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
221+
let expiry2: Expiry = .date(Date().addingTimeInterval(10))
222+
let key1 = "item1"
223+
let key2 = "item2"
224+
let filePathForKey1 = storage.makeFilePath(for: key1)
225+
storage.onRemove = { key in
226+
XCTAssertTrue(key == filePathForKey1)
227+
}
228+
try? storage.setObject(testObject, forKey: key1, expiry: expiry1)
229+
try? storage.setObject(testObject, forKey: key2, expiry: expiry2)
230+
///Device enters background
231+
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
232+
}
233+
234+
func testManualManageExpirationMode() {
235+
storage.applyExpiratonMode(.manual)
236+
let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
237+
let expiry2: Expiry = .date(Date().addingTimeInterval(10))
238+
let key1 = "item1"
239+
let key2 = "item2"
240+
var success = true
241+
storage.onRemove = { key in
242+
success = false
243+
XCTAssertTrue(success)
244+
}
245+
try? storage.setObject(testObject, forKey: key1, expiry: expiry1)
246+
try? storage.setObject(testObject, forKey: key2, expiry: expiry2)
247+
///Device enters background
248+
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
249+
}
218250
}
219251

0 commit comments

Comments
 (0)