mst-gql icon indicating copy to clipboard operation
mst-gql copied to clipboard

Query Cache

Open chrisdrackett opened this issue 4 years ago • 12 comments

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.

chrisdrackett avatar May 13 '20 17:05 chrisdrackett

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"
        }
      ]
    }
  },

special-character avatar May 13 '20 17:05 special-character

So the two biggest issues with the caching in MSTGQL:

  1. 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
  2. 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"
        }
      ]
    }
  },

Aryk avatar May 16 '20 12:05 Aryk

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 avatar May 16 '20 17:05 Aryk

@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?

special-character avatar May 20 '20 20:05 special-character

Yes, I did, I filed a bug on the deflateHelper here.

Aryk avatar May 20 '20 23:05 Aryk

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();

Aryk avatar May 29 '20 16:05 Aryk

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 avatar May 29 '20 16:05 Aryk

@Aryk Unless you do something to save the cache (like put it in local storage) it won't be saved across sessions by default.

chrisdrackett avatar May 29 '20 16:05 chrisdrackett

Got it @chrisdrackett , so backgrounding the app effectively is like a reload on your simulator. Am I thinking about it the right way?

Aryk avatar May 30 '20 09:05 Aryk

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.

chrisdrackett avatar May 30 '20 14:05 chrisdrackett

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.

Aryk avatar May 30 '20 16:05 Aryk

any updates? I've had the same issue and fetchPolicy: "no-cache" helped, but now I can't trust this cache at all

arodik avatar Sep 13 '21 18:09 arodik