apollo-ios-dev icon indicating copy to clipboard operation
apollo-ios-dev copied to clipboard

Replace ApolloStoreSubscriber didChangeKeys with ApolloStore.Activity…

Open jimisaacs opened this issue 10 months ago • 21 comments

The following changes are to support the need to hook up data residing in separate stores into the life-cycle of the data in ApolloStore. This PR represents 1 of 2 ways I considered accomplishing this.

  1. Implement NormalizeCache by wrapping another implementation of NormalizedCache and forwarding calls to it.
  2. A more robust ApolloStoreSubsciber, i.e. this PR

I decided that I was submitting 2 as a PR because I believe the interface is an improvement to what is currently in main (the didChangeKeys subscriber method). The didChangeKeys call follows a call to cache.merge(records:), which is covered in this PR's implementation as the following:

  public func store(_ store: Apollo.ApolloStore,
             activity: Apollo.ApolloStore.Activity,
             contextIdentifier: UUID?) throws {
    if case .did(perform: .merge, outcome: .changedKeys(let changedKeys)) = activity {
      self.store(store, didChangeKeys: changedKeys, contextIdentifier: contextIdentifier)
    }
  }

Example:

final class MyOtherStoreApolloStoreSubscriber {

    private class UnsubscribeRef {
        private let _unsubscribe: () -> Void
        public init(unsubscribe: @escaping () -> Void) {
            _unsubscribe = unsubscribe
        }
        deinit {
            _unsubscribe()
        }
    }
    private lazy var unsubscribeRefs = [String: UnsubscribeRef]()

    private func shouldSubscribeToMyOtherStore(for record: Apollo.Record) -> Bool {
        /* boolean to determine whether or not you should subscribe to my other store for the provided record */
        return unsubscribeRefs[record.key] == nil 
    }

    private func subscribeToMyOtherStore(_ otherStoreListener: () -> Void) -> () -> Void {
        /* stuff to subscribe to my other store in here */
        return { /* stuff to unsubscribe from my other store in here, weak self please! */ }
    }

    private func syncMyOtherStoreWith(store: ApolloStore, records: [Apollo.CacheKey: Apollo.Record]) throws {
        for (key, record) in records {
            if shouldSubscribeToMyOtherStore(for: record) {
                unsubscribeRefs[key] = UnsubscribeRef(unsubscribe: subscribeToMyOtherStore({
                    /* stuff to handle my other store changes in here */
                }))
            }
        }
    }
}

extension MyOtherStoreApolloStoreSubscriber: ApolloStoreSubscriber {

    public func store(_ store: Apollo.ApolloStore, didChangeKeys changedKeys: Set<Apollo.CacheKey>, contextIdentifier: UUID?) {
        /* not implemented, this is now deprecated */
    }

    public func store(_ store: Apollo.ApolloStore, activity: Apollo.ApolloStore.Activity, contextIdentifier: UUID?) throws {
        switch activity {
        case .will(perform: .merge(records: let records)):
            try syncMyOtherStoreWith(store: store, records: records.storage)
        case .did(perform: .loadRecords, outcome: .records(let records)):
            try syncMyOtherStoreWith(store: store, records: records)
        case .did(perform: .removeRecord(for: let key), outcome: .success):
            unsubscribeRefs.removeValue(forKey: key)
        case .did(perform: .removeRecords(matching: let pattern), outcome: .success):
            for key in unsubscribeRefs.keys {
                if key.range(of: pattern, options: .caseInsensitive) != nil {
                    unsubscribeRefs.removeValue(forKey: key)
                }
            }
        case .did(perform: .clear, outcome: .success):
            unsubscribeRefs.removeAll()
        default:
            return
        }
    }
}

jimisaacs avatar Apr 08 '24 05:04 jimisaacs