-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Umbrella issue: Cache invalidation & deletion #621
Comments
@sandervanhooft we don't have cache invalidation right now, but it's a feature we definitely want to implement in the future. See #41 which is sort of related. |
I'd also like some way to invalidate the cached version of a query after a mutation, but for a different reason: the server-side query logic is fairly complex (especially with sorting), and trying to patch in the results of the mutation via updateQueries is simply too complicated in this case. However, I don't want to force an immediate refetch because it may be a while before I actually need the results of the query. An example of this is an issue tracking system with custom filtering and sorting: I don't want to repeat the custom filtering and sorting logic on the client especially since it's all written in mongo query language. And it will often be the case that a bunch of issues will be updated in sequence before I need to re-query the issue database, at which time I don't mind waiting a little bit longer to get the non-optimistic query result. So ideally I would just set a dirty bit on that query and let everything happen automatically after that. |
Thinking about this some more: What I'd like to see is something very much like the API for updateQueries:
The invalidateQueries hook is called with exactly the same arguments as updateQueries, however the return result is a boolean, where true means it should remove the query result from the cache and false means the cache should be unchanged. This strategy is very flexible, in that it allows the mutation code to decide on a case-by-case basis which queries should be invalidated based on both the previous query result and the result of the mutation. (Alternatively, you could overload the existing updateQueries to handle this by returning a special sentinel value instead of an updated query result.) The effect of invalidating a query removes the query results from the cache but does not immediately cause a refetch. Instead, a refetch will occur the next time the query is run. |
Your solution looks interesting. Would it support Promises? In that case I could set a timer Promise which returns true after a specific period of time. |
My suggestion would work for your first use case but not the second, which would require a very different approach (and may already be handled in the current framework). Specifically, what I'm asking for is a way to invalidation the cache in response to a mutation. That's why I suggest that the new feature be part of the mutation API since it's very similar in concept to the existing updateQueries feature. Your second use case has to do with invalidating the cache after a time interval and is unrelated to mutations. It seems like you might be able to get what you want using Apollo's polling APIs. |
Popping in on this... I'd like to add that I think this is very badly needed. I'm actually surprised it's not baked into the core of the framework. Without the ability to invalidate parts of the cache, it's almost impossible to cache a paginated list. |
Also surprised this isn't in the core. |
We also need this. Our current approach is:
We are currently doing a manual
That said, together with my colleague I just brainstormed a way which should work for our needs:
The first is actually matching the comment by @viridia - the second is something which could be useful - alternatively this is something the invalidation does automatically. |
@swernerx yes, it's still on the radar! We trying to simplify the core and API before we add more features though, so this won't be in 1.0, but it could very well be in 1.1! |
I'm feeling the need to have a field based invalidation strategy. As all resources are available in a normalized form under Given this schema:
With a data id resolver such as: const dataIdFromObject = ({ __typename, id }) => __typename + id With Person type resources with ids 1, 2, and 3 being found at the apollo store as:
{
apollo: {
...
data: {
Person1: {
id: 1,
name: 'Lucas',
relative: {
type: "id"
id: "Person2"
generated: false
}
},
Person2: {
id: 2,
name: 'John',
relative: null
},
Person3: {
id: 3,
name: 'John',
relative: null
},
}
}
} And having a query somewhere in system such as:
I could then perform a mutation to change the relative of person 1 to be person 3 and force invalidation as following:: client.mutate({
mutation: setRelative,
variables: { person: 1, relative: 3 },
invalidateFields: (previousQueryResult, { mutationResult }) => {
return {
'Person1': {
'relative': true
}
}
}
)} Edit: I do understand that |
@lucasconstantino An additional caveat of |
@dallonf We patched the updateQueries caveat in a recent release of react-apollo. You should give it a try! |
Ok, I've worked on this issue and ended up with a project to solve field based cache invalidation: This is how you would use it, following the example on my previous comment: import { invalidateFields } from 'apollo-cache-invalidation'
client.mutate({
mutation: setRelative,
variables: { person: 1, relative: 3 },
update: invalidateFields(() => [
['Person1', 'relative']
])
)} As you can see, invalidateFields method is only a higher-order function to create a valid Further documentation can be found in the project's page. Keep in mind this can be used for dynamic cache-key invalidation at any level, so to invalidate the invalidateFields(() => [[/^Person/, 'relative']]) If you people find this useful, care to provide some feedback. |
@lucasconstantino I think this is really cool! It's exactly the kind of cache control we hoped to enable with the generic store read and write functions and
Overall this is a really great effort and I look forward to seeing similar things like it being built on top of the new imperative store API! |
@helfer I've renamed the project to apollo-cache-invalidation. I'll look at you other considerations in the morning ;) Thanks for the productive feedback! |
@helfer I've looked into your second observation, and I think I found a dead end here. Studying the QueryManager class, and specifically the Testing it locally, I was able to fire a refetch from that exact spot, using The big problem here, I think, is that the field based invalidation isn't really current compatible with the approaches Apollo Client has on cache clearing or altering. Both Well, as far as I could look it up, apollo-cache-invalidation cannot on it's own fix the stale data problem, meaning I would have to pull-request Apollo Client at least on some minor routines. What do you think? Should I proceed, or am I missing something here? By the way: I guess being informed that |
@lucasconstantino I think I have similar use case. I want to invalidate some queries after mutation and it doesn't matter are they active or stale. And I don't want to refetch them immediately because there could be a lot of them and maybe user will never open pages that are using them again. Did I understand your last message correctly that you were trying to solve similar problem with |
@nosovsh apollo-cache-invalidate will basically purge current data from the cache. In the current state of things, it will work as expected for new queries the user eventually perform on that removed data, but if there are any active observable queries (queries currently watching for updates) related to the removed data, these queries will not refetch, but serve the same old data they had prior to the cache clearing. To solve this problem, I'm working on a pull-request to apollo-client to allow the user decide when new data should be refetched in case something goes stale: #1461. It is still a work in progress, and I'm not sure hold long it will take for something similar to go into core. |
I'm not sure how much this really adds to the conversation, but I spent a whole lot of time typing this out in a dupe issue, so I may as well add it here. :) Here is a use case my team commonly runs into that is not well covered by the existing cache control methods and would greatly benefit from field-based cache invalidation: Let's say I have a paginated list field in my schema: type Query {
widgets($limit: Int = 15, $offset: Int = 0): WidgetList
}
type Widget {
id: ID!
name: String
}
type WidgetList {
totalCount: Int!
count: Int!
limit: Int!
offset: Int!
data: [Widget]!
} There is table in the app powered by this So let's throw a simple mutation into this mix... type Mutations {
createWidget($name: String!) : Widget
} When I fire this mutation, there is really no telling where it will be inserted into the list, given the aforementioned complex sorting logic. The only logical update I can make to the state of the store is to flag either the entire field of Unless I'm missing something, there doesn't seem to be any way to handle this case in Apollo Client. |
@dallonf We have similar use case. Plus we can not refetch @lucasconstantino yeah seems active observable queries making problems in such case. I will look deeper in what you propose in PR |
@dallonf you could experiment using the more sophisticated mutation option Sorry to promote my project here again, but this is exactly the kind of situation I built it for: apollo-cache-invalidate. Basically, following the schema you presented, you could invalidate all your paginated results (because they truly are invalid now) at once with: import { invalidateFields, ROOT } from 'apollo-cache-invalidation'
import gql from 'graphql-tag'
import { client } from './client' // Apollo Client instance.
const mutation = gql`
mutation NewWidget($name: String!) {
createWidget(name: $name) {
id
}
}
`
const update = invalidateFields((proxy, result) => [
[ROOT, /^widgets.+/]
])
client.mutate({ mutation, update, variables: { name: 'New widget name' } }) But - and here goes a big but - this currently would invalidate cache only for non instantiate queries, meaning if the Hope it helps. |
@lucasconstantino Aha, thanks! That does solve my use case for now. (I had tried your module, but didn't realize the regex was necessary to capture field arguments). |
I have a very simple scenario:
I've read several issues around here but still can't find a good way of doing this except manually deleting the data from the store on the Since cache is stored with variables, I don't know which list the data should be added to, so it's just better to invalidate all loaded lists from that field. However there doesn't seem to be an API for this. I can use |
@Draiken the function in my comment above deletes the whole field from the cache regardless of variables. I agree it's frustrating that there still isn't a proper solution. |
Simple and rather blunt workaround for the case when you just need to invalidate the entire query cache, can be like this: const CACHE_TTL = 180000; // 2 minutes
let client;
let created;
// Use this function to get an access to the GraphQL client object
const ensureClient = () => {
// Expire the cache once in a while
if (!client || Date.now() - created > CACHE_TTL) {
created = Date.now();
client = new ApolloClient({
link: new HttpLink({uri: `your_gql_api_endpoint_here`}),
cache: new InMemoryCache()
});
}
return client;
}; |
@dr-nafanya I think there is a method available for this: |
I've read tons of workarounds, but that's not solving the problem. You have to cherry-pick a bunch of workarounds (or build them yourself) for basic usage of this library. It's like it is built to work with the "to-do" app and nothing more complex than that. We need an official implementation or at least documentation guiding users on how to deal with all of these edge cases (if you can call Cache#remove an edge case). I'm more than willing to help on anything, but I'm afraid if I just fork and try to implement this, it will be forgotten like this issue or the 38 currently open PRs... Right now I'm mentally cursing who chose this library for a production system 😕 |
Is there any up to date roadmap for is project? |
I'm using this workaround, but it is pretty bad because I need to refetch all my queries again because my client store is empty but at least doing this my App is always consistent. export const mutateAndResetStore = async (client, fn) => {
await fn();
// TODO fixing problem with cache, but we need to have a better way
client.resetStore();
};
mutateAndResetStore(client, () =>
// my mutation call
saveGroup({
...
... we need a real solution ASAP. |
I think the best way to solve delete & cache issue is to add GraphQL directive:
Execution of this query should delete |
@anton-kabysh interesting. I think this might create confusion on what exactly is being removed, though; the cached information, or the entity itself. |
I think that we "just" need a way to update the cache by Something like this: client.readCache({key: 'Post:1'}); // {data: {...}}
client.writeCache({
key: 'Post:1',
data: {
title: 'oh yeah',
},
});
client.deleteCache({key: 'Post1'}); Note that with this solution, you can also update every results cached (and this is exactly what I wanted!) |
@fabien0102 With this solution, I don't think you can add a result to a query though. If I made a query like |
@lucasconstantino Sure, the bare |
@yopcop Sure, my solution only works for update and delete a part of a query, but it's better than the actual solution (and I'm also aware that is a hard problem and no easy solution exists). Sometime is definitively easier to invalidate previous queries. For this kind of complex queries, I think that a powerfull solution can be to be able to query the queries Example to illustrate: // my apollo store
posts(fromDate: "2018-01-01", toDate: "2018-03-31")
posts(fromDate: "2018-01-01", toDate: "2018-04-31")
posts(fromDate: "2018-01-01", toDate: "2018-05-31")
// how to update this
client.readQueries({
query: gql`query Post($from: Date!, $to: Date!) posts(fromDate: $from, toDate: $to) { ... }`,
variables: (from, to) => isAfter(from, "2018-01-01") && isBefore(to, "2018-04-31")
}); So basicly like a (after I never put my hands on the Apollo code base, so I really don't know if it's possible, it's just to share ideas 😉) |
Wouldn't changing proxy.writeData({ id: `MyType:${id}`, data: null }); to delete the object instead of having no effect be sufficient here? For my case at least, it would be a very elegant, easy and intuitive solution. |
CC: @smilykoch @martinjlowm worth following this umbrella issue on cache invalidation. |
There is a need to automate garbage collection inside the cache. The cache presents very limited API to the world, mostly allowing reads/writes through queries and fragments. Let's look at the queries for a moment. The query has a concept of being active. Once query is activated, and results are fetched, it denormalises the response to the cache. It cannot delete it, because other queries might end up using the same results. What the query can't do is to reference other queries, so there is no way to make cycles. This make a reference counter based GC viable. Let's suppose that the underlying cache object holds a reference counter. Once the result is being written to/materialised from the cache, the query can collect all references objects, hold on them into a private To prune specific query data and enable potential garbage collection from cache, you have to adjust refcount for all associated objects and clean that Once in a while the cache could simply filter out all keys that have refcount 0. That eviction could be easily triggered with few strategies:
The The remaining issue is with |
I've talked with @stubailo about being able to write |
Writing |
I believe that #3394 might help a great deal with solving this issue. It's basically a ref-count system. Once cache entries will register their dependant queries, cache can be pruned for every entry that doesn't have any dependants. |
Here's my my work around, I would love something like this built in of course done better where I dont need to set manual IDs: https://gist.github.com/riccoski/224bfaa911fb8854bb19e0a609748e34 The function stores in the cache a reference IDs along with a timestamp then checks against it which determines the fetch policy |
Why is it so hard to write a |
This really needs to get resolved. It's not even an edge case or rare use scenario. Every single CRUD app will need to deal with this issue. |
Let me try making a proposal for a solution - if the maintainers are OK with it, I or someone else could work on a PR to implement it in Originally, I wanted to start with the existing evict function, but I don't think it'll work without breaking changes, so I may as well call it something different. Let's call it public deleteQuery<TVariables = any>(
options: DataProxy.Query<TVariables>,
): { success: bool } You could use it like this, after adding a const CACHE_CLEAR_QUERY = gql`
query ClearWidgets($gizmo: ID!, $page: Int!, $limit: Int, $search: String) {
gizmoById(id: $gizmo) {
widgets(page: $page, search: $search, limit: $limit)
}
}
`;
proxy.deleteQuery(CACHE_CLEAR_QUERY, {
variables: {
page: () => true, // clear all pages
// only clear search queries that could match the widget we just added
search: value => !value || newWidget.name.indexOf(value) !== -1,
gizmo: newWidget.gizmo.id,
},
}); A couple of important notes here:
After items have been removed from the cache in this way, any currently active queries that are displaying this data will automatically and immediately refetch. The part of this I'm least certain about is the ability for a query to be "incomplete" and not specify subfields - some feature needs to exist so that you can clear an entire array instead of, say, just the |
I've spent a whole day trying to figure out how to delete something from my cache/store. Is there a solution for this? I have finished 90% of my app with Apollo and this hit me right in the face. There really is no way to delete something? |
To help provide a more clear separation between feature requests / discussions and bugs, and to help clean up the feature request / discussion backlog, Apollo Client feature requests / discussions are now being managed under the https://github.com/apollographql/apollo-feature-requests repository. This issue has been migrated to: apollographql/apollo-feature-requests#4 |
Is there a nice and easy way to set a condition to invalidate a cached query against? Think about time-to-live (TTL) or custom conditions.
For example (pseudo-code warning):
query(...).invalidateIf(fiveMinutesHavePassed())
or
query(...).invalidateIf(state.user.hasNewMessages)
forceFetch
serves its purpose, but I think the cache invalidation condition should be able to live close to the query(cache) itself. This way I don't need to check manually if there's a forceFetch required when I rerender a container. The query already knows when it's outdated.The text was updated successfully, but these errors were encountered: