-
Notifications
You must be signed in to change notification settings - Fork 7
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 #4
Comments
Hi there, I was following the originating thread and wanted to help set the context for this feature. Is there currently a feature doc around this cache invalidation that gives us an idea of what to expect from apollo in the future? |
Why dont we add a prop like deleted: true as you can see some of the mapped types has generated true something like that deleted: true would be ignored on all queries that are mapped to it |
An api that would allow for deletion by typename and id would be very helpful indeed. Refetching queries or updating only work for very simple use-cases.
|
I also would appreciate deleting all objects with a given type name, and related, but it would be nice if we could deep clone a cache, safely. |
It'd also be nice to be able to clear out the results of a field, maybe even given certain arguments. Example: I just deleted a Another example: I just disabled a |
I'm abusing I really don't get why I can't just invalidate a part of the store... Seems like a basic feature |
Below, a solution for this; hopefully could work for your use cases. It's kinda hacky 😞 , but does the job: In the example below, we needed to delete an Event -- so, to make it work, simply set your mutation's update to something similar to below: update: store => {
const deletedEvent = {
...store.data.get(`Event:${eventId /* <-- put your ID */}`),
id: '__DELETED__',
};
store.data.set(`Event:${eventId /* <-- put your ID */}`, deletedEvent);
} additionally, in your queries (which for me was the query for getting Events), set a filter as such: eventsData: data.events.filter(({ id }) => id !== '__DELETED__') |
Thanks, that does work (and is so hacky haha, but at this point anything goes) but the query is run before the update so it doesn't filter out the deleted item. I could use a getter for that but then I'm doing all the graphql stuff through vuex getters which seems like I'm going further away from proper solution 😛 I'm gonna see if I can a satisfactory solution by combining apollo-vue and some of these hacks |
@Christilut - to delete/invalidate a part of the store, I use the function below, and pass in the name of the query that should be deleted. The function will delete all instances of the query, regardless of parameters. I am using React, but in Vue it should basically be the same except the path to ROOT_QUERY might be slightly different.
In React, 'withApollo' gets exported as part of the component. The Vue equivalent would be required in order for the store to be available. |
Suggestion for an api name – usage:
|
Help me please sort out, how to use Should every component know queries for each other to reset? The case also well described here: #29 |
This feels like the best approach to me. Apollo should be able to detect that Manually keeping track of all queries that might be affected and all combinations of parameters is very tedious. However, there's no real standard for that marking something as deleted afaik. Maybe there should be one, but in the meantime I think the following approach would work best.
If a standard is established later for marking an item as deleted from the server, then most the code will already be in place - the main difference being we wouldn't need to call |
@herclogon in your example, since those three separate queries are independently cached, invalidating after you add a block would look something like this in the mutation's
Perhaps there could also be a |
@AdamYee Going this way each mutation should know queries which must be re-fetched. When you have a lot of components it will increase code viscosity, cause you must add I assume that a mutation should be able to mark part of cache |
@herclogon You're correct. This is the challenge when managing cache! I'm more just trying to help guide the discussion for the potential API.
I do realize and have experienced the increase in code viscosity (complexity?). In our codebase, we use a central mechanism that tracks cached queries which we care about and updates them after certain mutations. Shameless plug - my colleague (@haytko) was able to extract this pattern into a link, https://www.npmjs.com/package/apollo-link-watched-mutation. I think you'll find it solves that need to manage cache in one place. Something like |
Maybe additionally, or instead of The params could be the same in both options (passing query names, or specific objects with query and variables), but the difference would be that by invalidating query it is just marked as dirty, meaning that only once the query is reused it would be force fetched. It probably should respect fetchPolicy, e.g. cache-only won't refetch even if invalidated, but otherwise there would be a server hit. If the query is being watched, it is reused immediately, thus refetched with current variables right away. By refetching invalidated query, it would be marked as clean once again and additional query calls would be resolved normally according to fetchPolicy. I believe we would be able to happily resolve queries with filters from cache and after mutation invalidate all relevant queries at once, e.g. by query name regardless of variables. Of course it would require to know which queries to invalidate after certain mutation, but if we don't have live queries, we always have to know this on client side. |
@jpikora As for me it will be useful to have not |
If anyone is looking for a workaround in the meantime, good news, there is one. Just pass an update function like this to your mutation. const update = (proxy) => proxy.data.delete('AnyCacheKey'); And if you don't know the exact key, but want to match against a regex you could do something like this: const update = (proxy) => Object.keys(proxy.data.data).forEach((key) => {
if (key.match(/someregex/) {
proxy.data.delete(key);
}
}); |
@joshjg It works according to the apollo devTools/cache panel~! Is there any downside effect when deleting from |
@joshjg Thanks so much for this, it's far simpler than the workaround that I had implemented! One thing I noticed is that if the mutation you're using this on changes data that's being displayed on the current page without a re-route after the mutation completes then the Query does not seem to get updated. You need to manually call refetch to get it to grab the up to date information from the server. This is at least the case in the current version of react-apollo that I'm using. If anyone is interested I wrote up an implementation example of the above idea for dealing with paginated queries here https://medium.com/@martinseanhunt/how-to-invalidate-cached-data-in-apollo-and-handle-updating-paginated-queries-379e4b9e4698 |
@joshjg I'm using |
@Tam I had the same issue. I figured it was because the import isArray from 'lodash/isArray'
import isPlainObject from 'lodash/isPlainObject'
import { InMemoryCache } from 'apollo-cache-inmemory'
/**
* Recursively delete all properties matching with the given predicate function in the given value
* @param {Object} value
* @param {Function} predicate
* @return the number of deleted properties or indexes
*/
function deepDeleteAll(value, predicate) {
let count = 0
if (isArray(value)) {
value.forEach((item, index) => {
if (predicate(item)) {
value.splice(index, 1)
count++
} else {
count += deepDeleteAll(item, predicate)
}
})
} else if (isPlainObject(value)) {
Object.keys(value).forEach(key => {
if (predicate(value[key])) {
delete value[key]
count++
} else {
count += deepDeleteAll(value[key], predicate)
}
})
}
return count
}
/**
* Improve InMemoryCache prototype with a function deleting an entry and all its
* references in cache.
*/
InMemoryCache.prototype.delete = function(entry) {
// get entry id
const id = this.config.dataIdFromObject(entry)
// delete all entry references from cache
deepDeleteAll(this.data.data, ref => ref && (ref.type === 'id' && ref.id === id))
// delete entry from cache (and trigger UI refresh)
this.data.delete(id)
} In this manner, const update = cache => cache.delete(entry) |
In many libraries, it's not uncommon for destructive actions on collections to return the destroyed items. What I'd love to see is an optional extension to queries & mutations that's analogous, where your mutation resolver can return an object field that indicates deleted status. Something like: mutation DeleteEvent($id: ID) {
deleteEvent(eventId: $id) {
id
__typename
__deleted # hypothetical optional built-in boolean field: true indicates Event with this id no longer exists
}
} Then any cache can watch query and mutation results just like normal and when it encounters an object with I could see this fitting in nicely when deleting:
I don't know if the actual mechanism can look like what I showed above without some really big changes to the whole ecosystem, but maybe there's another mechanism to do this. |
Doesn't seem to work. I see the query is removed from the list but even calling refetch after has no change to UI. |
@developdeez at the end of the component do you have something along these lines?
|
Eviction always succeeds if the given options.rootId is contained by the cache, but it does not automatically trigger garbage collection, since the developer might want to perform serveral evictions before triggering a single garbage collection. Resolves apollographql/apollo-feature-requests#4. Supersedes #4681.
Eviction always succeeds if the given options.rootId is contained by the cache, but it does not automatically trigger garbage collection, since the developer might want to perform serveral evictions before triggering a single garbage collection. Resolves apollographql/apollo-feature-requests#4. Supersedes #4681.
Eviction always succeeds if the given options.rootId is contained by the cache, but it does not automatically trigger garbage collection, since the developer might want to perform serveral evictions before triggering a single garbage collection. Resolves apollographql/apollo-feature-requests#4. Supersedes #4681.
The new For that to work, I think you'd have to be able to evict fields rather than objects - ex. all instances of |
Agree with @dallonf regarding paginated requests. I'd like |
The community have solution: I'm using a heavily customized version of that package myself that does build abstractions like pagination that is very specific for my needs. |
@wtrocki Yes, apollo-cache-invalidation has been how I've solved this problem in the past, but it hasn't been updated in over a year and doesn't seem to have kept up with changes to Apollo's internals. |
Field-level eviction is coming in Apollo Client 3, thanks to apollographql/apollo-client#5643. Funny story: I vaguely remembered reading @dallonf's comment when I implemented that feature, which is why I made sure evicting a field by name would evict all instances of the field within the object, regardless of arguments… but I could not find that comment again for the life of me. Now, at long last, we are reunited! |
I use a similar workaround like some of the suggestions here and just wanted to share it. {
__typename: Post,
id: 1234,
__deleted: true
} So, Apollo updates the cached records automatically. Not even need to use the "update" function manually on the client. The only downside is, the record just doesn't get deleted from the cache actually. I simply use a lightly wrapped Very, very basic approach, but sometimes it can be messy when dealing with nested data structures. Instead of filtering these records with a "hacky" way like this and just hiding them from the UI, Apollo might delete the records when some sort of a This is a very simple idea, just wanted to share it. Just solves some of my use-cases. |
I haven't gotten to use Apollo Client 3.0 yet, but it seems like the field-level eviction described would be able to handle it. |
Yes I've been trying to wrap my head around and playing with v3 now that is possible. I wonder tho if you found a workaround for your use case using v2, before migrating everything to v3 |
For 1.x, I used https://github.com/lucasconstantino/apollo-cache-invalidation. But if I remember right, it's not compatible with 2.x. So I wound up resetting the entire store whenever anything needed to be invalidated. But this would often throw an unhelpful message: apollographql/apollo-client#3766 Soooo yes I'm very much looking forward to native support in 3.x! 😄 |
I likewise ran into that issue when trying to reset the store in 2.x and resorted to throwing away the entire Apollo Client object and recreating it from scratch after some sufficiently complex mutations. More flexible and reliable cache eviction methods are definitely welcomed. |
Is this still in roadmap? I need to clear all the permutations of a query, when something is changed by some user and i receive a notification with signalR |
If anyone on 3.0 wants to try out a beta we have been using for invalidation policies as an extension of the InMemoryCache it should solve some use cases: https://github.com/NerdWalletOSS/apollo-invalidation-policies |
@danReynolds Looks really cool. TTL is something that's def missing from Apollo. In the repo's example, when an My main problem has always been that when you delete an entity, all queries that were having it as a result had issues... |
The library is just using onWrite and onEvict policies to control when to run
but in general I've found it okay to leave the dangling refs in cached queries with the new read behaviour. |
@danReynolds That's perfect. Thanks, I somehow missed Ben's PRR on automatic filtering of dangling refs. Good to know that you can confirm no issues were found. |
We've adapted @Baloche's |
Any news on improving cache invalidation, I've read some interesting version 3 stuff for it but not an easy api to implement |
Migrated from: apollographql/apollo-client#621
The text was updated successfully, but these errors were encountered: