Skip to content

Commit

Permalink
Replace ApolloStoreSubscriber didChangeKeys with ApolloStore.Activity…
Browse files Browse the repository at this point in the history
… and new method
  • Loading branch information
jimisaacs committed Apr 8, 2024
1 parent 5490ccf commit 97710f7
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 22 deletions.
6 changes: 6 additions & 0 deletions Tests/ApolloTests/Cache/StoreSubscriptionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
107 changes: 88 additions & 19 deletions apollo-ios/Sources/Apollo/ApolloStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import Foundation
import ApolloAPI
#endif

public typealias DidChangeKeysFunc = (Set<CacheKey>, 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 {
Expand All @@ -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<CacheKey>,
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<Apollo.CacheKey>)
/// 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<Apollo.CacheKey>)
}
}
}

private let cache: NormalizedCache
private let queue: DispatchQueue

Expand All @@ -36,9 +87,13 @@ public class ApolloStore {
self.queue = DispatchQueue(label: "com.apollographql.ApolloStore", attributes: .concurrent)
}

fileprivate func didChangeKeys(_ changedKeys: Set<CacheKey>, 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)
}
}
}

Expand All @@ -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, Swift.Error>) -> 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,
Expand All @@ -68,8 +127,9 @@ public class ApolloStore {
public func publish(records: RecordSet, identifier: UUID? = nil, callbackQueue: DispatchQueue = .main, completion: ((Result<Void, Swift.Error>) -> 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,
Expand Down Expand Up @@ -201,15 +261,20 @@ public class ApolloStore {
}

public class ReadTransaction {
fileprivate let cache: NormalizedCache

fileprivate lazy var loader: DataLoader<CacheKey, Record> = DataLoader(self.cache.loadRecords)
fileprivate weak var store: ApolloStore?
fileprivate lazy var loader: DataLoader<CacheKey, Record> = 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: GraphQLQuery>(query: Query) throws -> Query.Data {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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())
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
}

}
Expand Down
6 changes: 3 additions & 3 deletions apollo-ios/Sources/Apollo/DataLoader.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
final class DataLoader<Key: Hashable, Value> {
public typealias BatchLoad = (Set<Key>) throws -> [Key: Value]
public typealias BatchLoad = (Set<Key>) throws -> [Key: Value]?
private var batchLoad: BatchLoad

private var cache: [Key: Result<Value?, Error>] = [:]
Expand Down Expand Up @@ -29,12 +29,12 @@ final class DataLoader<Key: Hashable, Value> {
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() {
Expand Down
6 changes: 6 additions & 0 deletions apollo-ios/Sources/Apollo/GraphQLQueryWatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,10 @@ public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, Apollo
}
}
}

public func store(_ store: Apollo.ApolloStore,
activity: Apollo.ApolloStore.Activity,
contextIdentifier: UUID?) throws {
// not implemented, still relies on didChangeKeys
}
}

0 comments on commit 97710f7

Please sign in to comment.