Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manipulating cache #73

Closed
nuke-dash opened this issue Mar 27, 2017 · 12 comments
Closed

Manipulating cache #73

nuke-dash opened this issue Mar 27, 2017 · 12 comments
Labels
enhancement Issues outlining new things we want to do or things that will make our lives as devs easier

Comments

@nuke-dash
Copy link

Is there a way to manipulate the cache?
Our back-end GraphQL implementation has an expiry property added to every object.
This is a date object and basically tells you if the object is still valid or not.
So I'm now looking for a way to use this property to either throw stuff from the cache.
Or prevent the cache from returning objects that are no longer valid.

@nuke-dash nuke-dash changed the title Manipulating cache? Manipulating cache Mar 27, 2017
@martijnwalraven
Copy link
Contributor

This is still very much work in progress, but we've recently added a NormalizedCache protocol to be able to plug in your own cache implementations. I think it might be enough to add your own implementation of loadRecords(forKeys:) that filters out expired records. Alternatively, you could add a periodic process to evict expired records from the cache, but that would be more difficult because you have to make sure there are no intervening reads or writes (ApolloStore is currently responsible for read/write locking).

The main use case we had in mind with this is to support persistent caches, and we've done some thinking about client cache policies with contributors from Airbnb. So please join the conversation so we can make sure to address your use cases too!

@martijnwalraven martijnwalraven added the enhancement Issues outlining new things we want to do or things that will make our lives as devs easier label Mar 27, 2017
@justinmakaila
Copy link
Contributor

justinmakaila commented Mar 28, 2017

So, I think I'm starting to get some clarity into how this could be accomplished within Apollo.

In my current GraphQL client implementation on iOS, I'm using a custom network layer provided by Moya, and I persist my data via CoreData, all supplemented with ReactiveSwift. We utilize CoreData to ensure that the user can use the app offline. My database layer was initially based off of the objc.io CoreData book, but has been modified to adopt more of a one way data flow. When a user submits some data to be saved, those changes are made immediately to the database, and the user continues on with their session. In the background, I create some ChangeProcessor objects related to the collections that have changes to query the database for subsets of collections to update the remote (inserts, updates, deletes, archives). The change processors are handed a managed object context to do some work on, and are expected to return that work in the form of a SignalProducer.

If there is anything that should block the user, I just observe that sync signal producer on the main thread.

Syncs happen by pushing local changes that have not been synchronized, then iterates through a collection of RemoteChangeProcessors that issue GraphQL queries against our remote.

I'd like to refactor this pattern going forward by routing all mutations and queries through the apollo client. i.e. When a user submits some data via form UI, I would grab the values of the form, and create a mutation. I would issue that mutation to the Apollo client, which would have some concept of a driver or middleware to handle the RecordSet.

To query some data, I'd follow the same patterns that are currently demonstrated in sample applications.

The driver (in the case of CoreData), given the cache policy, could map queries to NSPredicate instances, to be issued against a single NSManagedObjectContext, and mutations would just be reflected, iterated over, and the NSManagedObject's would be updated via key value coding.

All returned objects would be mapped to the immutable structs provided by apollo-codegen, bringing CoreData into the world of immutability.

I'm somewhat confused as to how we can handle updates and changes that were made offline and ensure they get up to the server in order

@martijnwalraven
Copy link
Contributor

I'd like to refactor this pattern going forward by routing all mutations and queries through the apollo client. i.e. When a user submits some data via form UI, I would grab the values of the form, and create a mutation. I would issue that mutation to the Apollo client, which would have some concept of a driver or middleware to handle the RecordSet.

I think that is pretty much the design I have in mind too. Some of this is still implicit in the current codebase because it is work in progress, but the idea is that ApolloStore acts as a coordinator to locally cached data, both in memory and persistent. ApolloClient will try to load data from the store (depending on the cache policy), and data received from the network is normalized and published to the store.

UI components can watch queries (or individual fragments), and will receive updates whenever relevant data in the store changes. This is based around simple callbacks, but it would also be a great fit for ReactiveSwift I think (I just want to keep the dependency optional).

I'm currently in the process of adding store read/write operations similar to the Apollo JS API (as described in this blog post). The idea is to allow you to use queries or fragments both to read and write to the store, basically giving you a typed denormalized interface to a normalized record store.

These read/write operations can also be used to implement optimistic updates, temporarily updating the in memory cache until the real results from the server are in.

So ApolloStore, together with helper classes like GraphQLExecutor, is responsible for coordinating all this. It also has a read/write lock and batches records loads with a 'data loader' implementation for example.

The idea is that this allows for a pretty minimal interface to pluggable cache implementations, basically just the NormalizedCache protocol. (This is just the start of course, we'll probably have to add to this as we start working on implementations and address more use cases.)

Does this make sense to you? How does this compare to what you have in mind? Do you think this would be sufficient to integrate with your existing CoreData store?

@justinmakaila
Copy link
Contributor

justinmakaila commented Mar 28, 2017

@martijnwalraven Is it currently possible to provide a custom NormalizedCache protocol to the ApolloStore publicly? I don't see any way to do it. loljk. I'm currently trying to get an example together for downloading the current user and saving it to the store.

@martijnwalraven
Copy link
Contributor

@justinmakaila: The code on master is very much work in progress, so things may be broken. If you do want to try it out, you'll also need to npm install -g apollo-codegen@next.

@justinmakaila
Copy link
Contributor

@martijnwalraven I just did that... I'm currently trying to use the NormalizedCache Protocol to fetch objects given a context:

struct CoreDataStoreService: NormalizedCache {
    private let managedObjectContext: NSManagedObjectContext
    
    init(context: NSManagedObjectContext) {
        self.managedObjectContext = context
    }
    
    func loadRecords(forKeys keys: [CacheKey]) -> Promise<[Record?]> {
        <#code#>
    }
    
    func merge(records: RecordSet) -> Promise<Set<CacheKey>> {
        <#code#>
    }
}

But I'm not sure how I can use CacheKey, given that it's only a string, to query CoreData (unless I queried all of my keyed collections), and I'm not sure what to return in the Promise, because the Record struct has an empty public interface

@martijnwalraven
Copy link
Contributor

martijnwalraven commented Mar 28, 2017

Yeah, I realize the interface is more geared towards a key-value store right now.

A Record is a flattened JSON object, with embedded objects replaced by References.

You can configure your own cacheKeyForObject function on ApolloClient to assign cache keys. This would usually return id (if globally unique) or a combination of __typename and id. By default, cache keys are created as a path from the query root (see here for some more pointers).

@justinmakaila
Copy link
Contributor

justinmakaila commented Mar 28, 2017

@martijnwalraven How would this work for singleton objects? i.e. the current user (we literally only ever have one around), and computed objects that are accessible from the root query?

@martijnwalraven
Copy link
Contributor

martijnwalraven commented Mar 28, 2017

The query root is also an object, and the record for that object contains fields (which may include arguments) that usually refer to other objects. This is an example of querying the hero field on the Star Wars schema for example.

So in your case, the currentUser field of QUERY_ROOT would contain a reference (by cache key) to the current user.

@justinmakaila
Copy link
Contributor

@martijnwalraven All of this is sounding great, and stuff that I'm really happy is happening.

This is example is actually going really well, aside from the fact that a few types are lacking public interfaces, such as Record and Promise. Is there a reason for this, or should I just throw a quick PR together?

@martijnwalraven
Copy link
Contributor

@justinmakaila: Really good to hear! I feel bad I haven't been able to document things better yet, but the code in master is a fairy substantial redesign, and I'm working on some more changes before the design is more stable.

The reason a lot of types are lacking public interfaces is because I didn't want to commit to an interface yet and wanted to be able to move fast without breaking other people's code. But I'm happy opening things up if that helps you test drive it, because that is the only way to get more people involved and figure out if this is actually working :)

@martijnwalraven
Copy link
Contributor

I'm going to close this issue because it isn't really actionable. Feel free to open issues for specific bugs or features. Also note we just released Apollo iOS 0.6.0-beta.1, which includes the starting point of cache manipulation. Make sure to use npm install -g apollo-codegen@next until it is out of beta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Issues outlining new things we want to do or things that will make our lives as devs easier
Projects
None yet
Development

No branches or pull requests

3 participants