Skip to content
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

[WIP] Fix various apollo-cache-inmemory 1.3.x bugs. #3964

Merged
merged 15 commits into from
Oct 5, 2018

Conversation

benjamn
Copy link
Member

@benjamn benjamn commented Oct 1, 2018

Since publishing apollo-cache-inmemory@1.3.0 officially, several bugs have been reported, so we have removed the latest tag from 1.3.0 and restored the old 1.2.10 version as latest. The most recent 1.3.x release is now tagged as next, so you can install it by running

npm install apollo-cache-inmemory@next

The more information we include in the cache keys used by the InMemoryCache, the more different cached results can be distinguished from one another. It's a bug if a change in some input value can change the result without also producing a different cache key. The typical solution is to include the relevant input data in the cache key returned by makeCacheKey. Note that not all input data needs to be included explicitly in the cache key, since some inputs are simply derived from other inputs.

Related bugs, possibly also fixed by this PR:

This is important because cache.readFragment passes a rootId to
readQueryFromStore that is not always "ROOT_QUERY".

cc @carllippert
Using a different fragmentMatcher function may produce different results,
so it should factor into the StoreReader cache keys. This still assumes
any given fragment matcher function is pure, in the sense that it will
always return the same result given the same inputs.

Also reverted 1acbef6 so that the
defaultFragmentMatcher function is always the same function object in case
no custom fragment matcher function is provided.

@carllippert This may help with your fragment matching issues.
@benjamn benjamn self-assigned this Oct 1, 2018
I accidentally forgot to remove the file:... versions for apollo-cache and
apollo-utilities from apollo-cache-inmemory/package.json.

I have set the npm dist-tag for 1.3.x to @next for the time being, with
1.2.10 as @latest, so version 1.3.1 was never (and never will be) the
@latest version.
@carllippert
Copy link

So coming here from the slack channel to show whats going on. Mind you I don't even like the code but its where the bug lives. Having trouble making good looking code with apollo-link-state for interconnected state. Anyways.

This function amount other things is meant to add or remove an item from a list. It reads a fragment and determines what to do. It works the first time it is called. But the second time it is called the readFragment always returns the first fragment it ever returned. So the first ID passed to it will always be returned.

`setActiveBatchPen: (__, { pen_id }, { cache }) => {

            let id = `__Pen:${pen_id}`;
            let mountedID = `MountedPen:${pen_id}`;

            let pen = cache.readFragment({ fragment: penFragment, id: id});
          
            let mountedPen = cache.readFragment({ fragment: mountedPenFragment, id: mountedID});
           
            let previousActivePens = cache.readQuery({ query: activeBatchPens });
           
            let data = {};

            let selectedPen = previousActivePens.activeBatchPens.find((pen) => { return pen.pen_id == pen_id });
            
            //if the pen was selected before do 
            if(selectedPen) {
                
                //filter out the old pen that we are unselecting
                data.activeBatchPens = previousActivePens.activeBatchPens.filter((pen) => { return pen.pen_id != selectedPen.pen_id});
              
                //set selected in mounted pen for use in modals
                if(mountedPen){
                    cache.writeFragment({fragment: mountedPenFragment, id: mountedID, data: { ...mountedPen, selected: false }});
                }

            } else {

                let farmQuery = cache.readQuery({ query: activeFarm });

                 farm_id = farmQuery.activeFarm.farm_id;
           
                let feedingEvents = cache.readQuery({ query: feedingEventsQuery, variables: { farm_id }});
                let feedingEventsBlob = getFeedingEventsForPenForFeeding(feedingEvents.feedingEvents, pen_id);

                //add to activeBatchPens

                let newPen = Object.assign({}, 
                                            {...pen},
                                            {
                                            id: pen_id,
                                            __typename: 'ActiveBatchPen',
                                            adjustmentValue: 0,
                                            batchSizeError: 0,
                                            newTotalFedWeight: 0,
                                            dropOrder: 0,
                                            dropStartWeight: 0,
                                            plannedWeight: 0,
                                            dropEndWeight: 0,},
                                            {feeding_event_id: genUUID()},
                                            { feedingEvents: feedingEventsBlob.suggestedEvents });

                ///set inital weight for pen depending on a bunch of conditions. 
                //set totalWeight if we have suggestedEvents.
                if(feedingEventsBlob.suggestedEvents.length > 0) {
                   newPen.plannedWeight = feedingEventsBlob.suggestedEvents[0].weight;
                   newPen.newTotalFedWeight = feedingEventsBlob.suggestedEvents[0].weight;
                } else {
                     //if no suggestedEvents set num_of_hed * rationWeight
                     //get ration
                     let ration = cache.readQuery({query: selectedRation});

                     if(ration.selectedRation.length > 0){
                         //sum the ingredientweights to get a total ration weight
                         let ingSum = 0;

                         ration.selectedRation[0].currentVersion.ingredients.forEach((ing) => {
                            ingSum += ing.weight;
                         });

                         let penTotal = ingSum * pen.num_of_head;

                         newPen.plannedWeight = penTotal;
                         newPen.newTotalFedWeight = penTotal;

                     } else {
                         //no ration  so leave the weights at zero. 
                     }
                }
               
                data.activeBatchPens = previousActivePens.activeBatchPens.concat([newPen]);


                if(mountedPen){
                    cache.writeFragment({fragment: mountedPenFragment, id: mountedID, data: { ...mountedPen, selected: true }});
                }
            }

            //write some metadata about the pens and ingredients all togethor

            let totalBatchWeight = 0;

            data.activeBatchPens.forEach((pen) => {
                totalBatchWeight += pen.plannedWeight;
            });

            //write total batch weight to cache
            cache.writeData({ data: { totalBatchWeight: totalBatchWeight}});
            
            //write pens with feeding events to cache
            cache.writeQuery({ query: activeBatchPens, data });
        
            return null;    
        },`

You can see in this pic that the second time around calling this when a new fragment ID, in this case '__Pen:xxx-xxx' you get a different fragment. The fragment of the id passed on the previous call.
image

I updated to apollo-cache-inmemory 1.3.2 still this bug persists unfortunately.
Other deps.
react-apollo 2.2.3
apollo-client 2.4.2
react-native 0.57.1

I also rely on _.merge from lodash to connect all my resolvers. The only reason I bring that up is for some reason it seems occasionally my debugger will show undefined defaults or consts from other resolvers into debugger scope. Unsure what thats about.

@jelteliekens
Copy link

jelteliekens commented Oct 2, 2018

I'm experiencing exact the same problem. The first time it reads the correct fragment but the next time it will always return the first fragment even if the id is different.

Updating the package (reverts to 1.2.10) did resolve the problem for now.

@benjamn
Copy link
Member Author

benjamn commented Oct 2, 2018

@jelteliekens Can you confirm you were using apollo-cache-inmemory@1.3.2 rather than 1.3.0?

@benjamn
Copy link
Member Author

benjamn commented Oct 2, 2018

So, I thought I had a reproduction, but I was mistakenly still using apollo-cache-inmemory@1.3.0 instead of 1.3.2, and the problem went away for me after updating to 1.3.2. Granted, my reproduction was pretty simplistic (two consecutive client.readFragments with different IDs), but I would still like to make absolutely sure you're not somehow using 1.3.0, like I was!

Before this change, we were retrieving an object from the cache every time
we wanted to look up a single property of that object.
This should help with github.com/apollographql/react-apollo/issues/2442,
as long as the known properties of query document nodes do not participate
in cycles.
This reverts commit acd2a67.

This should fix #3970, because my attempt to replicate the behavior of
addTypenameToDocument without actually cloning the document failed to
account for some important subtleties about where exactly __typename
fields should be added.
@benjamn benjamn changed the title Include root ID and fragment matcher in StoreReader#executeStoreQuery cache key. [WIP] Fix various apollo-cache-inmemory 1.3.x bugs. Oct 3, 2018
@benjamn
Copy link
Member Author

benjamn commented Oct 3, 2018

If apollo-cache-inmemory@1.3.2 didn't fix the problem originally reported by @carllippert, I'm not sure the changes in 1.3.3 will help, but it might be worth a try, if you have a moment to run

npm install apollo-cache-inmemory@1.3.3

@jelteliekens
Copy link

@benjamn Apparently I also was using version 1.3.0. After updating to 1.3.3 this morning the problem went away so I think it's fixed now. Thanks!

benjamn and others added 3 commits October 3, 2018 10:11
As explained here, with astonishing honesty:
https://github.com/facebook/react-native/blob/98a6f19d7c41e1d4fd3c04d07bb2c4e627f21724/Libraries/vendor/core/Map.js#L44-L50

As reported most recently here:
apollographql/react-apollo#2442 (comment)

For the record, I have previously complained that these polyfills are
simply buggy and should be replaced:
#3394 (comment)

While that is still true, this workaround should moot the debate. 🐮
@benjamn benjamn force-pushed the include-root-ID-in-executeStoreQuery-cache-key branch from 69c678b to 07f3f47 Compare October 3, 2018 15:32
@carllippert
Copy link

DUDE. It worked! Fragment match bug is fixed in 1.3.3 🎉 Pushing fix to our alpha channel right now to really see how the performance improvements are with 1.3 ( since this all started trying to improve performance ). Thanks so much for the fix!

@benjamn
Copy link
Member Author

benjamn commented Oct 4, 2018

Awesome! By the way, the QueryKeyMaker issue is being tracked in apollographql/react-apollo#2442, in case you see that again.

@fbartho
Copy link

fbartho commented Oct 4, 2018

@benjamn -- Please let me know if you need any changes from apollo-link-rest or apollo-link-state to be more "spec-friendly" for these performance changes!

Fixes apollographql/react-apollo#2442.

This temporarily mitigates the prototype chain bug that will be
permanently fixed by this React Native PR that I submitted yesterday:
facebook/react-native#21492

I would like to find a way to bundle a different Map polyfill in React
Native apps, without increasing bundle sizes for non-RN apps, but that has
proven tricky so far. For future reference, this seems to be the way to do
it (thanks to @peggyrayzis for the tip):
https://facebook.github.io/react-native/docs/platform-specific-code#platform-specific-extensions

Until a new version of React Native is released with these changes
included, the simplest way to fix the problem is simply to avoid storing
any prototype objects in a Map, so that's what I've done in this commit.

We are already wrapping Object.freeze and friends in src/fixPolyfills.ts,
which should make it possible to use frozen objects as Map keys (the other
bug that I addressed in the React Native PR linked above).
@benjamn benjamn force-pushed the include-root-ID-in-executeStoreQuery-cache-key branch from 1fe97f9 to cbd0314 Compare October 5, 2018 19:46
@benjamn benjamn merged commit 4f14c31 into master Oct 5, 2018
@benjamn
Copy link
Member Author

benjamn commented Oct 5, 2018

@fbartho I don't think I have any warnings or suggestions for you right now. The problems with the Map polyfill are React Native-specific, and should be fixed soon.

@benjamn benjamn deleted the include-root-ID-in-executeStoreQuery-cache-key branch October 5, 2018 19:59
@fbartho
Copy link

fbartho commented Oct 5, 2018

@benjamn Speaking as a user & contributor to apollo / apollo-link-rest / apollo-link-state on react-native, I'm invested 😉 -- My question was actually also more about if you need help testing any of these changes with apollo-link-rest.

benjamn added a commit that referenced this pull request Feb 18, 2020
React Native used to have Map and Set polyfills that relied on tagging
objects with a special __MAP_POLYFILL_INTERNAL_HASH__ property, which
breaks down when the object to be tagged happens to be frozen.

In 833072e (#3964) I introduced a
workaround to make sure objects got tagged before becoming non-extensible,
which robustly solved the problem for any Map and Set polyfills that rely
on an object tagging strategy.

Note that Apollo Client freezes objects only in development (using
maybeDeepFreeze), so the problem fixPolyfills.ts was intended to solve was
only ever a problem in development.

I also submitted a PR to React Native that make their Map and Set
polyfills store non-extensible objects in an array, rather than attempting
to tag them: facebook/react-native#21492. Those
changes were first released in React Native 0.59.0, so technically the
fixPolyfills.ts logic should not be necessary for anyone using that
version or later.

Since then, React Native has stopped using any polyfills for Map and Set
(yay!), so the need for the workaround has been even further reduced:
facebook/react-native@93b9ac7

Those changes were first released in React Native 0.61.0, which is still
the latest minor version (0.61.5 is latest). I'm not sure how many people
are still using older versions of React Native, or what sort of LTS
policies they have. Expo SDK 36 uses 0.61.4, for what it's worth:
https://docs.expo.io/versions/latest/sdk/overview/

In any case, I think we can eliminate these polyfills from the default
bundle, as long as we take some care to include them when bundling for
React Native. This strategy uses a combination of techniques for selective
bundling in React Native:
https://facebook.github.io/react-native/docs/platform-specific-code#platform-specific-extensions
facebook/react-native#2208
benjamn added a commit that referenced this pull request Feb 18, 2020
React Native used to have Map and Set polyfills that relied on tagging
objects with a special __MAP_POLYFILL_INTERNAL_HASH__ property, which
breaks down when the object to be tagged happens to be frozen.

In 833072e (#3964) I introduced a
workaround to make sure objects got tagged before becoming non-extensible,
which robustly solved the problem for any Map and Set polyfills that rely
on an object tagging strategy.

Note that Apollo Client freezes objects only in development (using
maybeDeepFreeze), so the problem fixPolyfills.ts was intended to solve was
only ever a problem in development.

I also submitted a PR to React Native that make their Map and Set
polyfills store non-extensible objects in an array, rather than attempting
to tag them: facebook/react-native#21492. Those
changes were first released in React Native 0.59.0, so technically the
fixPolyfills.ts logic should not be necessary for anyone using that
version or later.

Since then, React Native has stopped using any polyfills for Map and Set
(yay!), so the need for the workaround has been even further reduced:
facebook/react-native@93b9ac7

Those changes were first released in React Native 0.61.0, which is still
the latest minor version (0.61.5 is latest). I'm not sure how many people
are still using older versions of React Native, or what sort of LTS
policies they have. Expo SDK 36 uses 0.61.4, for what it's worth:
https://docs.expo.io/versions/latest/sdk/overview/

In any case, I think we can eliminate these polyfills from the default
bundle, as long as we take some care to include them when bundling for
React Native. This strategy uses a combination of techniques for selective
bundling in React Native:
https://facebook.github.io/react-native/docs/platform-specific-code#platform-specific-extensions
facebook/react-native#2208
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 17, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants