-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
feat(NormalizedCache): use a Map-like cache abstraction #2362
Conversation
This feature enables a swappable cache implementation. For example, you might want to use Map, which is faster than writing keys to an Object. This also allows for custom use cases, such as emitting events upon .set() or .delete() (think Observables), which was otherwise impossible without the use of Proxies, which were only available in ES6. First PR to close apollographql#2293.
@niieani: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Meteor Contributor Agreement here: https://contribute.meteor.com/ |
Generated by 🚫 dangerJS |
Deploy preview ready! Built with commit 07c9609 |
Codecov Report
@@ Coverage Diff @@
## master #2362 +/- ##
==========================================
- Coverage 86.04% 85.96% -0.08%
==========================================
Files 36 39 +3
Lines 2049 2145 +96
Branches 490 500 +10
==========================================
+ Hits 1763 1844 +81
- Misses 277 292 +15
Partials 9 9
Continue to review full report at Codecov.
|
@niieani I'm super excited about this! I'm going to label it as post 2.0 though since I don't want to introduce quite this large of a change right before launch. I'll be able to give it an indepth review next week! |
@@ -0,0 +1,35 @@ | |||
import { StoreValue } from 'apollo-utilities'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the normalized store is specific to apollo-cache-inmemory
, this would be better there, instead of part of the base cache api
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a moment there I thought you're commenting about StoreValue
, which I didn't touch. 😃
Fixed!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@niieani This is looking fantastic! A minor request for a file move, but otherwise I'd like to move this into a testing phase!
Would you be able to describe some of the usages you have developed from this change, as well as add a couple new tests for using something like Map
as the cache base?
After that, I'd like to cut a 2.1.0@next
to give you (and others) the chance to try this out and profile it performance wise. Essentially I want to make sure the added weight / complexity of these changes are worth it for the possible performance gain. If Map
is fasters, I think we should adjust this PR to make it the default storage for the InMemoryCache.
Looking forward to working with you to ship this! 💪
While adding the MapCache and running all the test suites with it, I've noticed a few failing tests (mainly: "will preserve equality with custom resolvers"). That was odd. It turned out 'dataId' can sometimes be a number, when the value is set, but read as a string, when the value is being retrieved (!). A side effect of the original, Object-based store was that indexer properties always have to be strings, so even if the code did `cache[number]`, the JS engine would coerce that number to a string, and thus getting that same dataId (whether via a number or a string) would resolve to the proper underlying value. To solve this issue, I've added a helper function `getNormalizedDataId`, which ensures that `dataId` is in fact, not a number. If this is a bug, and not a feature, then it should probably be corrected at the source, not in the implementation of the cache. It might also just be a problem with the test itself that I'm not seeing.
@niieani I applied your changes and created an ApolloCache that uses https://github.com/apollographql/apollo-angular/tree/1.0-ngrx/packages/apollo-angular-cache-ngrx It still work in progress, more a proof of concept but it looks so goooooood. |
Hey @jbaxleyiii! Thanks for your review. I've moved the typings as you requested and added an additional implementation of Another thing: I originally wanted to do While adding the MapCache and running all the test suites with it, I've noticed a few failing ones (mainly: "will preserve equality with custom resolvers"). That was odd. It turned out If this is a bug, and not a feature, then it should probably be corrected at the source, not in the implementation of the cache. It might also just be a problem with the test itself that I'm not seeing? apollo-client/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts Lines 838 to 856 in 7aa8cff
The result contains As far as this goes:
Details will have to wait a bit, since I'm still working out some problems with how mutations work in our implementation. In short, we want to be able to create non-GQL functions that can operate directly on the store through a local schema, but co-operate and extend real GQL endpoints through schema-stiching. This way we can gradually migrate our app to GQL, while adding Observable GQL querying for data fetched by other, legacy means (like REST). @kamilkisiela glad it's working out for you! You can probably re-use my trick for testing your implementation by running the
You'll probably run into the same issue with string/number |
@@ -0,0 +1,20 @@ | |||
jest.mock('../objectCache', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes me so happy
import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types'; | ||
|
||
function getNormalizedDataId(dataId: string | number): string { | ||
return typeof dataId === 'number' ? String(dataId) : dataId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could also just directly cast this instead of type check here. The previous cache impl always cast to a string because the id was assumed a global id or combination of __typename and the id cast to a string.
So I think we could just do ${id}
instead of doing the check / cast here.
I'm happy to make that change and push it up either before or after this merge!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we can do ${id}
or String(id)
. In testing, I saw that the dataId
is sometimes also a Symbol
. This also causes issues when running the tests in jest
v21, since the expect().toEqual()
has changed the behavior in v21 and started to compare Symbols too. Tests don't account for that, so they break these some cases...
@niieani this is an excellent PR! I'm going to pull the trigger and merge it in for a 2.1 release! 🎉 Would you be open to writing an article about this or the work you are doing with Apollo for the Apollo Blog? We would love to feature your contributions and use case. @kamilkisiela the same goes for all of the new incredible Angular work you are doing! I'm always so thrilled to see Angular and Apollo working well together! |
@jbaxleyiii Thanks for merging! The work here was done as a part of a project we're working on at Base Lab, and we're planning to write something for our dev blog and open source its parts. However, I'll ask our publishing team, they might be open to cross-posting for better exposure :) |
This feature enables a swappable cache implementation. For example, you might want to use Map, which is faster than writing keys to an Object. This also allows for custom use cases, such as emitting events upon .set() or .delete() (think Observables), which was otherwise impossible without the use of Proxies, which were only available in ES6.
First PR to close #2293 (another part would be to open up the Array storing
optimistic
updates).The change is as non-breaking as possible. Unless you passed in the
store
to one of theapollo-cache-inmemory
functions, such as:writeQueryToStore
orwriteResultToStore
, there are no changes necessary. If you did access the cache's functions directly, all you need to do is add a.toObject()
call — see the changes to the tests for an example.Looking forward to your reviews @jbaxleyiii and @kamilkisiela!
Checklist: