diff --git a/Tests/ApolloTests/Cache/StoreSubscriptionTests.swift b/Tests/ApolloTests/Cache/StoreSubscriptionTests.swift index 34650500d..e1b202fa5 100644 --- a/Tests/ApolloTests/Cache/StoreSubscriptionTests.swift +++ b/Tests/ApolloTests/Cache/StoreSubscriptionTests.swift @@ -67,5 +67,11 @@ class StoreSubscriptionTests: XCTestCase { expectation.fulfill() } } + + func store(_ store: Apollo.ApolloStore, + activity: Apollo.ApolloStore.Activity, + contextIdentifier: UUID?) throws { + // not implemented, still relies on didChangeKeys + } } } diff --git a/apollo-ios/Sources/Apollo/ApolloStore.swift b/apollo-ios/Sources/Apollo/ApolloStore.swift index a960b9c21..ec990126d 100644 --- a/apollo-ios/Sources/Apollo/ApolloStore.swift +++ b/apollo-ios/Sources/Apollo/ApolloStore.swift @@ -3,8 +3,6 @@ import Foundation import ApolloAPI #endif -public typealias DidChangeKeysFunc = (Set, UUID?) -> Void - /// The `ApolloStoreSubscriber` provides a means to observe changes to items in the ApolloStore. /// This protocol is available for advanced use cases only. Most users will prefer using `ApolloClient.watch(query:)`. public protocol ApolloStoreSubscriber: AnyObject { @@ -15,13 +13,66 @@ public protocol ApolloStoreSubscriber: AnyObject { /// - store: The store which made the changes /// - changedKeys: The list of changed keys /// - contextIdentifier: [optional] A unique identifier for the request that kicked off this change, to assist in de-duping cache hits for watchers. + /// @deprecated func store(_ store: ApolloStore, didChangeKeys changedKeys: Set, contextIdentifier: UUID?) + + /// A callback that can be received by subscribers for a particular activity case described in `ApolloStore.Activity` + /// + /// - Parameters: + /// - store: The store which made the changes + /// - activity: The activity that triggered this callback + /// - contextIdentifier: [optional] A unique identifier for the request that kicked off this change, to assist in de-duping cache hits for watchers. + func store(_ store: ApolloStore, + activity: ApolloStore.Activity, + contextIdentifier: UUID?) throws } /// The `ApolloStore` class acts as a local cache for normalized GraphQL results. public class ApolloStore { + public enum Activity { + /// Peceived by subscribers BEFORE an action is executed, where the action can be prevented if an error is thrown. + /// - Parameters: + /// - perform: The type of action being performed, e.g., load, merge, remove + case will(perform: Action) + + /// Received by subscribers AFTER an action has been executed. + /// - Parameters: + /// - perform: The type of action that was performed, e.g., load, merge, remove + /// - result: The result of the action, including any relevant data or changed keys + case did(perform: Action, outcome: Action.Outcome) + + // Enum to define the types of actions performed in the store + public enum Action { + /// Received by subscribers for records to be loaded from the database for the provided keys. + /// - Parameters: + /// - forKeys: The keys that were provided to the store to load records for + case loadRecords(forKeys: Set) + /// Received by subscribers for records to be merged into the database. + /// - Parameters: + /// - records: The records that will be merged into the store + case merge(records: RecordSet) + /// Received by subscribers for a record to be removed from the database. + /// - Parameters: + /// - for: The key for of record that was removed + case removeRecord(for: CacheKey) + /// Received by subscribers for records matching the provided pattern to be removed from the database. + /// - Parameters: + /// - matching: The pattern for whcih matching records were removed + case removeRecords(matching: Apollo.CacheKey) + /// Received by subscribers for when the database is cleared. + case clear + + // Enum to represent the outcome of an action, which can be customized to include more data as needed + public enum Outcome { + case success + case records([Apollo.CacheKey: Apollo.Record]) + case changedKeys(Set) + } + } + } + private let cache: NormalizedCache private let queue: DispatchQueue @@ -36,9 +87,13 @@ public class ApolloStore { self.queue = DispatchQueue(label: "com.apollographql.ApolloStore", attributes: .concurrent) } - fileprivate func didChangeKeys(_ changedKeys: Set, identifier: UUID?) { + fileprivate func notify(_ activity: ApolloStore.Activity, identifier: UUID?) throws { for subscriber in self.subscribers { - subscriber.store(self, didChangeKeys: changedKeys, contextIdentifier: identifier) + try subscriber.store(self, activity: activity, contextIdentifier: identifier) + // TODO: Remove this after a round of deprecation + if case .did(perform: .merge(records: _), outcome: .changedKeys(let changedKeys)) = activity { + subscriber.store(self, didChangeKeys: changedKeys, contextIdentifier: identifier) + } } } @@ -49,7 +104,11 @@ public class ApolloStore { /// - completion: [optional] A completion block to be called after records are merged into the cache. public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Result) -> Void)? = nil) { queue.async(flags: .barrier) { - let result = Result { try self.cache.clear() } + let result = Result { + try self.notify(.will(perform: .clear), identifier: nil) + try self.cache.clear() + try self.notify(.did(perform: .clear, outcome: .success), identifier: nil) + } DispatchQueue.returnResultAsyncIfNeeded( on: callbackQueue, action: completion, @@ -68,8 +127,9 @@ public class ApolloStore { public func publish(records: RecordSet, identifier: UUID? = nil, callbackQueue: DispatchQueue = .main, completion: ((Result) -> Void)? = nil) { queue.async(flags: .barrier) { do { + try self.notify(.will(perform: .merge(records: records)), identifier: identifier) let changedKeys = try self.cache.merge(records: records) - self.didChangeKeys(changedKeys, identifier: identifier) + try self.notify(.did(perform: .merge(records: records), outcome: .changedKeys(changedKeys)), identifier: identifier) DispatchQueue.returnResultAsyncIfNeeded( on: callbackQueue, action: completion, @@ -201,15 +261,20 @@ public class ApolloStore { } public class ReadTransaction { - fileprivate let cache: NormalizedCache - - fileprivate lazy var loader: DataLoader = DataLoader(self.cache.loadRecords) + fileprivate weak var store: ApolloStore? + fileprivate lazy var loader: DataLoader = DataLoader { [weak store] keys in + guard let store else { return nil } + try store.notify(.will(perform: .loadRecords(forKeys: keys)), identifier: nil) + let records = try store.cache.loadRecords(forKeys: keys) + try store.notify(.did(perform: .loadRecords(forKeys: keys), outcome: .records(records)), identifier: nil) + return records + } fileprivate lazy var executor = GraphQLExecutor( executionSource: CacheDataExecutionSource(transaction: self) ) fileprivate init(store: ApolloStore) { - self.cache = store.cache + self.store = store } public func read(query: Query) throws -> Query.Data { @@ -260,10 +325,7 @@ public class ApolloStore { public final class ReadWriteTransaction: ReadTransaction { - fileprivate var updateChangedKeysFunc: DidChangeKeysFunc? - override init(store: ApolloStore) { - self.updateChangedKeysFunc = store.didChangeKeys super.init(store: store) } @@ -321,6 +383,8 @@ public class ApolloStore { withKey key: CacheKey, variables: GraphQLOperation.Variables? = nil ) throws { + guard let store else { return } + let normalizer = ResultNormalizerFactory.selectionSetDataNormalizer() let executor = GraphQLExecutor(executionSource: SelectionSetModelExecutionSource()) @@ -333,15 +397,14 @@ public class ApolloStore { accumulator: normalizer ) - let changedKeys = try self.cache.merge(records: records) + try store.notify(.will(perform: .merge(records: records)), identifier: nil) + let changedKeys = try store.cache.merge(records: records) // Remove cached records, so subsequent reads // within the same transaction will reload the updated value. loader.removeAll() - if let didChangeKeysFunc = self.updateChangedKeysFunc { - didChangeKeysFunc(changedKeys, nil) - } + try store.notify(.did(perform: .merge(records: records), outcome: .changedKeys(changedKeys)), identifier: nil) } /// Removes the object for the specified cache key. Does not cascade @@ -351,7 +414,10 @@ public class ApolloStore { /// - Parameters: /// - key: The cache key to remove the object for public func removeObject(for key: CacheKey) throws { - try self.cache.removeRecord(for: key) + guard let store else { return } + try store.notify(.will(perform: .removeRecord(for: key)), identifier: nil) + try store.cache.removeRecord(for: key) + try store.notify(.did(perform: .removeRecord(for: key), outcome: .success), identifier: nil) } /// Removes records with keys that match the specified pattern. This method will only @@ -368,7 +434,10 @@ public class ApolloStore { /// - Parameters: /// - pattern: The pattern that will be applied to find matching keys. public func removeObjects(matching pattern: CacheKey) throws { - try self.cache.removeRecords(matching: pattern) + guard let store else { return } + try store.notify(.will(perform: .removeRecords(matching: pattern)), identifier: nil) + try store.cache.removeRecords(matching: pattern) + try store.notify(.did(perform: .removeRecords(matching: pattern), outcome: .success), identifier: nil) } } diff --git a/apollo-ios/Sources/Apollo/DataLoader.swift b/apollo-ios/Sources/Apollo/DataLoader.swift index fa959c22f..d41a0d8dc 100644 --- a/apollo-ios/Sources/Apollo/DataLoader.swift +++ b/apollo-ios/Sources/Apollo/DataLoader.swift @@ -1,5 +1,5 @@ final class DataLoader { - public typealias BatchLoad = (Set) throws -> [Key: Value] + public typealias BatchLoad = (Set) throws -> [Key: Value]? private var batchLoad: BatchLoad private var cache: [Key: Result] = [:] @@ -29,12 +29,12 @@ final class DataLoader { let values = try batchLoad(pendingLoads) for key in pendingLoads { - cache[key] = .success(values[key]) + cache[key] = .success(values?[key]) } pendingLoads.removeAll() - return values[key] + return values?[key] } func removeAll() { diff --git a/apollo-ios/Sources/Apollo/GraphQLQueryWatcher.swift b/apollo-ios/Sources/Apollo/GraphQLQueryWatcher.swift index cc9eb3bca..296e3394d 100644 --- a/apollo-ios/Sources/Apollo/GraphQLQueryWatcher.swift +++ b/apollo-ios/Sources/Apollo/GraphQLQueryWatcher.swift @@ -128,4 +128,10 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo } } } + + public func store(_ store: Apollo.ApolloStore, + activity: Apollo.ApolloStore.Activity, + contextIdentifier: UUID?) throws { + // not implemented, still relies on didChangeKeys + } }