mst-gql
mst-gql copied to clipboard
Query Cache
I wanted to start a discussion around the cache in mst-gql
and get a read for how others think about it.
I've seen confusion in issues on this repo and within our own app about the relation of the cache data
from useQuery
to the store. Because the cache happens at a network level, data in the cache and locally (if being mutated in the local store) can get out of sync.
We've had an issue where data from the cache is merge into our store via. useCachedResults
resulting in incomplete data (@special-character will elaborate as he is the one that ran into this).
Our experience with mst-gql
comes 100% from an app with very dynamic data that can and does change often on the server and the client. Maybe the cache has a place when the graphQL data is more stable, but it still feels somewhat wrong for our store to essentially have two—potentially out of date—snapshots of the same data.
I don't really have a proposal yet, but it seems that at the very least work can be done to better explain the cache and where you would want to use it instead of the store.
For our example this is the data that we got back from our server when we query for deleted items:
{
"data": {
"deletedItems": [{
"__typename": "Item",
"id": "ck71gto7u0000onsek8kkuiw2",
"isOnServer": true,
"isComplete": false,
"isDeleted": true,
"title": "Task",
"createdAt": "2020-02-25T00:49:05.722Z",
"updatedAt": "2020-03-27T01:43:12.262Z",
"deletedAt": "2020-02-25T05:44:51.227Z",
"type": "TASK",
"location": "{}",
"weather": "{}",
"owner": {
"__typename": "User",
"id": "cEBicXCDIHgG9Sw8uvACwN0ss",
"isOnServer": true,
"email": "[email protected]"
},
"project": {
"__typename": "Project",
"id": "ck71ewgm50000onserfmn8u6a",
"isOnServer": true,
"title": "Gather",
"createdAt": "2020-02-25T04:50:53.598Z",
"deletedAt": null,
"updatedAt": "2020-03-27T01:40:46.248Z",
"isDeleted": false
},
"log": null
}]
}
}
This is the data that was stored in the query cache:
"query deletedItems { deletedItems {\n \n__typename\nid\nisOnServer\nisComplete\nisDeleted\ntitle\ncreatedAt\nupdatedAt\ndeletedAt\ntype\nlocation\nweather\nowner {\n \n__typename\nid\nisOnServer\nemail\n\n}\nproject {\n \n__typename\nid\nisOnServer\ntitle\ncreatedAt\ndeletedAt\nupdatedAt\nisDeleted\n\n}\nlog {\n \n__typename\nid\nisOnServer\ndate\ncreatedAt\nupdatedAt\ntype\n\n}\n\n } }undefined": {
"deletedItems": [
{
"__typename": "Item",
"id": "ck71gto7u0000onsek8kkuiw2"
}
]
}
},
So the two biggest issues with the caching in MSTGQL:
- It doesn't cache the entire response as @special-character described above. Also, I opened this a few weeks ago: https://github.com/mobxjs/mst-gql/issues/219
- There is no easy way to purge a particular response. You have to reach in and do something like
query.__queryCache.delete(query.querykey)
. You should be able to purge the cache ideally for specific calls regardless of the params passed in. For example, if you have a connection for pagination purposes. You may way to purge the entire list, which would mean you need to purge N queries (each query with "after" and "limit") for just a particular query type like "queryUsers".
For our example this is the data that we got back from our server when we query for deleted items:
{ "data": { "deletedItems": [{ "__typename": "Item", "id": "ck71gto7u0000onsek8kkuiw2", "isOnServer": true, "isComplete": false, "isDeleted": true, "title": "Task", "createdAt": "2020-02-25T00:49:05.722Z", "updatedAt": "2020-03-27T01:43:12.262Z", "deletedAt": "2020-02-25T05:44:51.227Z", "type": "TASK", "location": "{}", "weather": "{}", "owner": { "__typename": "User", "id": "cEBicXCDIHgG9Sw8uvACwN0ss", "isOnServer": true, "email": "[email protected]" }, "project": { "__typename": "Project", "id": "ck71ewgm50000onserfmn8u6a", "isOnServer": true, "title": "Gather", "createdAt": "2020-02-25T04:50:53.598Z", "deletedAt": null, "updatedAt": "2020-03-27T01:40:46.248Z", "isDeleted": false }, "log": null }] } }
This is the data that was stored in the query cache:
"query deletedItems { deletedItems {\n \n__typename\nid\nisOnServer\nisComplete\nisDeleted\ntitle\ncreatedAt\nupdatedAt\ndeletedAt\ntype\nlocation\nweather\nowner {\n \n__typename\nid\nisOnServer\nemail\n\n}\nproject {\n \n__typename\nid\nisOnServer\ntitle\ncreatedAt\ndeletedAt\nupdatedAt\nisDeleted\n\n}\nlog {\n \n__typename\nid\nisOnServer\ndate\ncreatedAt\nupdatedAt\ntype\n\n}\n\n } }undefined": { "deletedItems": [ { "__typename": "Item", "id": "ck71gto7u0000onsek8kkuiw2" } ] } },
Just added query cache management that addresses my issue. Have been using it for 3 hours but so far its good 😄
https://github.com/mobxjs/mst-gql/issues/211#issuecomment-629679108
Basically you can specify a cacheKey and ALL the requests that happen even with different after/before/limit on them will get appended to an array of queryKeys.
Then, since I know the names of the queryKeys for this section of requests, I can purge only these.
So, for example, if I destroy one item from a list. I can say "purge the cache", and then on the next update to React it won't even show temporarily the stale data and fetch the new stuff.
@Aryk Interesting, I will have to check out how you do this. Currently we don't really use the __queryCache
because we use local storage as our source of truth for what we have in our store. Have you ran into any issues with the __queryCache
only having partial models?
Yes, I did, I filed a bug on the deflateHelper here.
In the meantime, if you guys need to group requests and then purge them, I've hacked together this solution:
// @aryk: This needs to be added onto the RootStore for MSTGQL as an "extend"
const useQueryWithPaginationRootStoreExtension = self => ({
actions: {
__cacheDelete(key: string) {
self.__queryCache.delete(key);
},
}
});
const CacheEntry = types.model("CacheEntry", {
id: types.identifier,
queryKeys: types.array(types.string)
});
const QueryCacheManager = types.model("QueryCacheManager", {
cacheEntries: types.optional(types.map(CacheEntry), {}),
}).views(self => ({
getQueryKeys(cacheKey: string) {
return self.cacheEntries.has(cacheKey) ? self.cacheEntries.get(cacheKey).queryKeys as string[] : [];
},
})).actions(self => ({
add(cacheKey: string, queryKey: string) {
let queryKeys = self.getQueryKeys(cacheKey);
if (!queryKeys.includes(queryKey)) {
queryKeys = queryKeys.concat(queryKey);
self.cacheEntries.put({id: cacheKey, queryKeys});
}
},
deleteFromMSTGQLCache(cacheKey: string, store: any) {
self.getQueryKeys(cacheKey).forEach(key => {
if (store.__queryCache.has(key))
store.__cacheDelete(key);
});
}
}));
const queryCacheManager = QueryCacheManager.create();
Just curious, do any of you guys happen to know how long the cache will last on the network side if you don't invalidate or purge it in production?
I still haven't released my project yet. If a user backgrounds the app and reopens it, will the query cache still be in effect or does it essentially "reload" the app?
@Aryk Unless you do something to save the cache (like put it in local storage) it won't be saved across sessions by default.
Got it @chrisdrackett , so backgrounding the app effectively is like a reload on your simulator. Am I thinking about it the right way?
Wait, are you talking react native? There is a chance the cache stays around when being backgrounded in that case, but it’s a crap shoot depending on what else is going on on the device.
Yeah I meant on react native. Sorry I forgot this is a general library for web as well 😄
I guess I'll have to experiment and see what ends up happening.
any updates? I've had the same issue and fetchPolicy: "no-cache"
helped, but now I can't trust this cache at all