Skip to content

Commit 1b9d1d2

Browse files
committed
Add 'CacheClearingPolicy' for users to have more control over cleanup
1 parent 472b176 commit 1b9d1d2

9 files changed

Lines changed: 262 additions & 95 deletions

File tree

Sources/Apollo/ApolloClient.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,12 @@ extension ApolloClient: ApolloClientProtocol {
8080
}
8181
}
8282

83-
public func clearCache(callbackQueue: DispatchQueue = .main,
84-
completion: ((Result<Void, Error>) -> Void)? = nil) {
85-
self.store.clearCache(completion: completion)
83+
public func clearCache(
84+
usingPolicy policy: CacheClearingPolicy,
85+
callbackQueue: DispatchQueue = .main,
86+
completion: ((Result<Void, Error>) -> Void)? = nil
87+
) {
88+
self.store.clearCache(usingPolicy: policy, callbackQueue: callbackQueue, completion: completion)
8689
}
8790

8891
@discardableResult public func fetch<Query: GraphQLQuery>(query: Query,

Sources/Apollo/ApolloClientProtocol.swift

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ public protocol ApolloClientProtocol: class {
99
/// A function that returns a cache key for a particular result object. If it returns `nil`, a default cache key based on the field path will be used.
1010
var cacheKeyForObject: CacheKeyForObject? { get set }
1111

12-
/// Clears the underlying cache.
13-
/// Be aware: In more complex setups, the same underlying cache can be used across multiple instances, so if you call this on one instance, it'll clear that cache across all instances which share that cache.
14-
///
12+
/// Clears the cache store according to the specified policy.
13+
/// - Warning: The cache may be used by other clients. Calling this method will affect all clients using the same cache!
1514
/// - Parameters:
16-
/// - callbackQueue: The queue to fall back on. Should default to the main queue.
17-
/// - completion: [optional] A completion closure to execute when clearing has completed. Should default to nil.
18-
func clearCache(callbackQueue: DispatchQueue, completion: ((Result<Void, Error>) -> Void)?)
15+
/// - policy: The cache cleaning policy to use.
16+
/// - callbackQueue: An optional queue to execute the completion handler on. Should default to the `.main` queue.
17+
/// - completion: An optional completion closure to execute when the cache has been cleared. Should default to `nil`.
18+
func clearCache(usingPolicy policy: CacheClearingPolicy,
19+
callbackQueue: DispatchQueue,
20+
completion: ((Result<Void, Error>) -> Void)?)
1921

2022
/// Fetches a query from the server or from the local cache, depending on the current contents of the cache and the specified cache policy.
2123
///
@@ -79,3 +81,16 @@ public protocol ApolloClientProtocol: class {
7981
queue: DispatchQueue,
8082
resultHandler: @escaping GraphQLResultHandler<Subscription.Data>) -> Cancellable
8183
}
84+
85+
// MARK: conveniences
86+
87+
extension ApolloClientProtocol {
88+
/// Clears all records from the cache store.
89+
/// - Warning: The cache may be used by other clients. Calling this method will affect all clients using the same cache!
90+
/// - Parameters:
91+
/// - callbackQueue: An optional queue to execute the completion handler on. The default is `.main`.
92+
/// - completion: An optional completion closure to execute when the cache has been cleared. The default is `nil`.
93+
func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Result<Void, Error>) -> Void)? = nil) {
94+
self.clearCache(usingPolicy: .allRecords, callbackQueue: callbackQueue, completion: completion)
95+
}
96+
}

Sources/Apollo/ApolloStore.swift

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,34 @@ public final class ApolloStore {
4545
}
4646
}
4747

48-
/// Clears the instance of the cache. Note that a cache can be shared across multiple `ApolloClient` objects, so clearing that underlying cache will clear it for all clients.
49-
///
50-
/// - Returns: A promise which fulfills when the Cache is cleared.
48+
/// Clears all records from the cache.
49+
/// - Warning: If this cache is shared between multiple `ApolloClient` objects, each client will be affected by the change in the cache.
50+
/// - Parameters:
51+
/// - callbackQueue: An optional callback queue to execute the `completion` handler on. The default is `.main`.
52+
/// - completion: An optional completion handler to execute when the cache has been cleared according the specified policy.
53+
/// - Returns: A promise which fulfills when the cache has been cleared.
5154
public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Result<Void, Error>) -> Void)? = nil) {
52-
queue.async(flags: .barrier) {
53-
self.cacheLock.withWriteLock {
54-
self.cache.clearPromise()
55-
}.andThen {
56-
DispatchQueue.apollo.returnResultAsyncIfNeeded(on: callbackQueue,
57-
action: completion,
58-
result: .success(()))
59-
}
55+
self.clearCache(usingPolicy: .allRecords, callbackQueue: callbackQueue, completion: completion)
56+
}
57+
58+
/// Clears the cache according to the specified policy.
59+
/// - Warning: If this cache is shared between multiple `ApolloClient` objects, each client will be affected by the change in the cache.
60+
/// - Parameters:
61+
/// - policy: The cache clearing policy to use during cleanup.
62+
/// - callbackQueue: An optional callback queue to execute the `completion` handler on. The default is `.main`.
63+
/// - completion: An optional completion handler to execute when the cache has been cleared according the specified policy.
64+
/// - Returns: A promise which fulfills when the cache has been cleared.
65+
public func clearCache(
66+
usingPolicy policy: CacheClearingPolicy,
67+
callbackQueue: DispatchQueue = .main,
68+
completion: ((Result<Void, Error>) -> Void)? = nil
69+
) {
70+
self.queue.async(flags: .barrier) {
71+
self.cacheLock
72+
.withWriteLock { self.cache.clearPromise(policy) }
73+
.andThen {
74+
DispatchQueue.apollo.returnResultAsyncIfNeeded(on: callbackQueue, action: completion, result: .success(()))
75+
}
6076
}
6177
}
6278

@@ -380,9 +396,9 @@ internal extension NormalizedCache {
380396
}
381397
}
382398

383-
func clearPromise() -> Promise<Void> {
399+
func clearPromise(_ policy: CacheClearingPolicy) -> Promise<Void> {
384400
return Promise { fulfill, reject in
385-
self.clear(callbackQueue: nil) { result in
401+
self.clear(policy, callbackQueue: nil) { result in
386402
switch result {
387403
case .success(let success):
388404
fulfill(success)

Sources/Apollo/InMemoryNormalizedCache.swift

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ public final class InMemoryNormalizedCache: NormalizedCache {
44
private var records: RecordSet
55
private let recordsLock = NSRecursiveLock()
66

7-
public init(records: RecordSet = RecordSet()) {
7+
public init(records: RecordSet = .init()) {
88
self.records = records
99
}
1010

1111
public func loadRecords(forKeys keys: [CacheKey],
1212
callbackQueue: DispatchQueue?,
1313
completion: @escaping (Result<[RecordRow?], Error>) -> Void) {
14-
self.recordsLock.lock()
15-
let records = keys.map { self.records[$0] }
16-
self.recordsLock.unlock()
14+
let records = self.threadSafe { keys.map{ self.records[$0] } }
1715
DispatchQueue.apollo.returnResultAsyncIfNeeded(on: callbackQueue,
1816
action: completion,
1917
result: .success(records))
@@ -22,17 +20,18 @@ public final class InMemoryNormalizedCache: NormalizedCache {
2220
public func merge(records: RecordSet,
2321
callbackQueue: DispatchQueue?,
2422
completion: @escaping (Result<Set<CacheKey>, Error>) -> Void) {
25-
self.recordsLock.lock()
26-
let cacheKeys = self.records.merge(records: records)
27-
self.recordsLock.unlock()
23+
let cacheKeys = self.threadSafe { self.records.merge(records: records) }
2824
DispatchQueue.apollo.returnResultAsyncIfNeeded(on: callbackQueue,
2925
action: completion,
3026
result: .success(cacheKeys))
3127
}
3228

33-
public func clear(callbackQueue: DispatchQueue?,
34-
completion: ((Result<Void, Error>) -> Void)?) {
35-
clearImmediately()
29+
public func clear(
30+
_ clearingPolicy: CacheClearingPolicy,
31+
callbackQueue: DispatchQueue?,
32+
completion: ((Result<Void, Error>) -> Void)?
33+
) {
34+
self.clearImmediately(clearingPolicy)
3635

3736
guard let completion = completion else {
3837
return
@@ -43,9 +42,14 @@ public final class InMemoryNormalizedCache: NormalizedCache {
4342
result: .success(()))
4443
}
4544

46-
public func clearImmediately() {
45+
public func clearImmediately(_ clearingPolicy: CacheClearingPolicy) {
46+
self.threadSafe { self.records.clear(clearingPolicy) }
47+
}
48+
49+
private func threadSafe<Result>(_ operation: () -> Result) -> Result {
4750
self.recordsLock.lock()
48-
self.records.clear()
51+
let result = operation()
4952
self.recordsLock.unlock()
53+
return result
5054
}
5155
}

Sources/Apollo/NormalizedCache.swift

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,46 @@
11
import Foundation
22

3+
/// An object that drives behavior of clear operations on a given cache.
4+
public struct CacheClearingPolicy: Equatable {
5+
/// Clears all records in the cache.
6+
public static let allRecords = CacheClearingPolicy(.allRecords)
7+
/// Clears all records whose key matches the provided glob pattern.
8+
/// - Parameter pattern: The glob pattern to use for key matching. For example `*pollo` will match both `Apollo` and `pollo`.
9+
public static func allMatchingKeyPattern(_ pattern: String) -> CacheClearingPolicy { .init(.allMatchingKeyPattern(pattern)) }
10+
/// Clears the the first (oldest) records in the cache up to the given limit.
11+
/// - Parameter k: The number of records to remove from the cache.
12+
public static func first(_ k: Int) -> CacheClearingPolicy { .init(.first(k)) }
13+
/// Clears the the last (most recent) records in the cache up to the given limit.
14+
/// - Parameter k: The number of records to remove from the cache.
15+
public static func last(_ k: Int) -> CacheClearingPolicy { .init(.last(k)) }
16+
17+
// actual policy storage
18+
19+
/// This is public due to language requirements for cross-target access.
20+
/// Do not use this value, nor its type as they are an implementation detail.
21+
public let _value: _Policy
22+
private init(_ policy: _Policy) { self._value = policy }
23+
24+
/// This is public due to language requirements for cross-target access.
25+
/// Do not use this type, nor any of its values, as they are an implementation detail.
26+
public enum _Policy: Equatable {
27+
case allRecords
28+
case allMatchingKeyPattern(String)
29+
case first(Int)
30+
case last(Int)
31+
32+
public static func ==(lhs: _Policy, rhs: _Policy) -> Bool {
33+
switch (lhs, rhs) {
34+
case (.allRecords, .allRecords): return true
35+
case let (.allMatchingKeyPattern(left), .allMatchingKeyPattern(right)): return left == right
36+
case let (.first(left), .first(right)): return left == right
37+
case let (.last(left), .last(right)): return left == right
38+
default: return false
39+
}
40+
}
41+
}
42+
}
43+
344
public protocol NormalizedCache {
445

546
/// Loads records corresponding to the given keys.
@@ -22,14 +63,31 @@ public protocol NormalizedCache {
2263
callbackQueue: DispatchQueue?,
2364
completion: @escaping (Result<Set<CacheKey>, Error>) -> Void)
2465

25-
// Clears all records
26-
///
66+
/// Clears records from the cache according to the policy provided.
2767
/// - Parameters:
28-
/// - callbackQueue: [optional] An alternate queue to fire the completion closure on. If nil, will fire on the current queue.
29-
/// - completion: [optional] A completion closure to fire when the clear function has completed.
30-
func clear(callbackQueue: DispatchQueue?,
68+
/// - policy: The policy to use in determining which records to clear.
69+
/// - callbackQueue: An optional queue to execute the completion closure on.
70+
/// - completion: An optional completion closure to execute when the cache clearing has completed.
71+
func clear(_ clearingPolicy: CacheClearingPolicy,
72+
callbackQueue: DispatchQueue?,
3173
completion: ((Result<Void, Error>) -> Void)?)
3274

33-
// Clears all records synchronously
34-
func clearImmediately() throws
75+
/// Clears records from the cache synchronously according to the policy provided.
76+
/// - Parameter policy: The policy to use in determining which records to clear.
77+
func clearImmediately(_ clearingPolicy: CacheClearingPolicy) throws
78+
}
79+
80+
extension NormalizedCache {
81+
/// Clears all records in the cache.
82+
/// - Parameters:
83+
/// - callbackQueue: An optional queue to execute the completion closure on.
84+
/// - completion: An optional completion closure to execute when the cache clearing has completed.
85+
public func clear(callbackQueue: DispatchQueue? = nil, completion: ((Result<Void, Error>) -> Void)? = nil) {
86+
self.clear(.allRecords, callbackQueue: callbackQueue, completion: completion)
87+
}
88+
89+
/// Clears all records in the cache synchronously.
90+
public func clearImmediately() throws {
91+
try self.clearImmediately(.allRecords)
92+
}
3593
}

Sources/Apollo/RecordSet.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@ public struct RecordSet {
2323
self.storage[row.record.key] = row
2424
}
2525

26-
public mutating func clear() {
27-
storage.removeAll()
26+
public mutating func clear(_ clearingPolicy: CacheClearingPolicy = .allRecords) {
27+
switch clearingPolicy._value {
28+
case let .first(count): self.storage = .init(self.storage.dropFirst(count))
29+
30+
case let .last(count): self.storage = .init(self.storage.dropLast(count))
31+
32+
case let .allMatchingKeyPattern(pattern): self.storage = self.storage.filter { !$0.key.contains(pattern) }
33+
34+
case .allRecords: fallthrough
35+
default: self.storage.removeAll()
36+
}
2837
}
2938

3039
public mutating func insert<S: Sequence>(contentsOf rows: S) where S.Iterator.Element == RecordRow {

Sources/ApolloSQLite/SQLiteNormalizedCache.swift

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,30 @@ public final class SQLiteNormalizedCache {
125125
return try self.db.prepare(query).map { try parse(row: $0) }
126126
}
127127

128-
private func clearRecords() throws {
129-
try self.db.run(records.delete())
130-
if self.shouldVacuumOnClear {
131-
try self.db.prepare("VACUUM;").run()
128+
private func clearRecords(accordingTo policy: CacheClearingPolicy) throws {
129+
switch policy._value {
130+
case let .first(count):
131+
let firstKRecords = records.select(self.id).order(self.id.asc).limit(count)
132+
try self.db.run(firstKRecords.delete())
133+
134+
case let .last(count):
135+
let lastKRecords = records.select(self.id).order(self.id.desc).limit(count)
136+
try self.db.run(lastKRecords.delete())
137+
138+
case let .allMatchingKeyPattern(pattern):
139+
let matchingRecords = records.where(
140+
self.key.like(pattern.replacingOccurrences(of: "*", with: "%"))
141+
)
142+
try self.db.run(matchingRecords.delete())
143+
144+
case .allRecords: fallthrough
145+
default:
146+
try self.db.run(records.delete())
132147
}
148+
149+
guard self.shouldVacuumOnClear else { return }
150+
151+
try self.db.prepare("VACUUM;").run()
133152
}
134153

135154
private func parse(row: Row) throws -> RecordRow {
@@ -217,10 +236,14 @@ extension SQLiteNormalizedCache: NormalizedCache {
217236
result: result)
218237
}
219238

220-
public func clear(callbackQueue: DispatchQueue?, completion: ((Swift.Result<Void, Error>) -> Void)?) {
239+
public func clear(
240+
_ clearingPolicy: CacheClearingPolicy,
241+
callbackQueue: DispatchQueue?,
242+
completion: ((Swift.Result<Void, Error>) -> Void)?
243+
) {
221244
let result: Swift.Result<Void, Error>
222245
do {
223-
try clearImmediately()
246+
try self.clearRecords(accordingTo: clearingPolicy)
224247
result = .success(())
225248
} catch {
226249
result = .failure(error)
@@ -231,8 +254,8 @@ extension SQLiteNormalizedCache: NormalizedCache {
231254
result: result)
232255
}
233256

234-
public func clearImmediately() throws {
235-
try clearRecords()
257+
public func clearImmediately(_ clearingPolicy: CacheClearingPolicy) throws {
258+
try self.clearRecords(accordingTo: clearingPolicy)
236259
}
237260
}
238261

0 commit comments

Comments
 (0)