Skip to content

Instantly share code, notes, and snippets.

@danReynolds
Last active June 23, 2020 20:31
Show Gist options
  • Save danReynolds/5db6e39871086f66ed82a05b7de19d38 to your computer and use it in GitHub Desktop.
Save danReynolds/5db6e39871086f66ed82a05b7de19d38 to your computer and use it in GitHub Desktop.
Apollo Cache layering
Let's examine how the Apollo cache layers optimistic data, primarily from the performTransaction API:
https://github.com/apollographql/apollo-client/blob/master/src/cache/inmemory/inMemoryCache.ts#L255
When executing a transaction like writing an optimistic mutation result, it will first call `addLayer` on its optimisticData.
This will create a new layer which as it is instantiated, calls the `perform` function that was passed to it. It stores a reference to the layer that instantiated it as its parent.
Perform is called with the newly instantiated layer. It sets both the data reference and optimisticData reference equal to the new layer instance, so that any changes to data or optimisticData that occur during the transaction are set onto the new layer and not its parent and not the root entity store.
After the transaction is done, it resets the data and optimistic data reference to what they were before and perform finishes, returning control to addLayer, which returns the newly created layer that contains the changes added from the transaction. The new layer ONLY contains its own changes, it isn't a merge with the data above it.
But, the call site where addLayer returns then sets the cache's optimisticData reference equal to the return value of addLayer, the newly created layer.
This means that after one optimistic result transaction finishes, the cache's data reference is still the Root, but the cache's optimistic data reference is now the added layer.
When the mutation finishes and the actual result comes back from the server, it goes to remove that optimistic layer:
https://github.com/apollographql/apollo-client/blob/master/src/core/QueryManager.ts#L280:L280
which calls removeLayer on the optimisticData reference:
https://github.com/apollographql/apollo-client/blob/master/src/cache/inmemory/inMemoryCache.ts#L246
The layers are a linked list. Remove layer will recursively go up the list towards the root until it finds the layer that is being removed. It will then delete all data on that layer and returns the parent layer above it.
The child layer below the removed layer in the list then has a reference to the grandparent and then creates a new layer on top of the grandparent, replaying its transaction.
Let's run through this process for 2 scenarios:
a) A single-layered optimistic mutation
1. Optimistic mutation adds a layer, updating cache's optimisticData reference to new layer.
2. Muutation returns server response.
3. Remove layer is called on the cache's optimisticData reference, which is the optimistic mutation layer.
4. The optimistic mutation layer recursively calls removeLayer on its parent, the root layer.
5. The root layer does nothing and returns itself.
6. The optimistic mutation layer receives its parent, then deletes all of its own data it had added during its original transaction.
7. The optimistic mutation returns its parent, the root layer to removeOptimistic.
8. removeOptimistic resets the cache's optimisticData to the return value of removeLayer, in this case the root layer.
This case is pretty simple, because no new added layers are re-layered on top in the linked list because the direct parent is the root.
b) A double-layered outbound optimistic mutation
1. First optimistic mutation adds a layer.
2. 2nd optimistic mutation adds a layer.
current cache references are now:
```
this.data === root
this.optimisticData === optimisticLayer2
```
and the layer linked list looks like:
```
optimisticLayer2->.parent->optimisticLayer1->.parent->root
```
3. First mutation returns from the server, merging its changes via `cache.write` with the cache's data reference, the root layer.
4. Remove layer is called on the cache's optimisticData reference via cache.removeOptimistic, which is optimisticLayer2.
5. remove layer is recursively called on the current layer's parent, optimisticLayer1.
6. remove layer is recursively called on the current layer's parent, root.
7. Root returns itself.
7. optimisticLayer1 receives root, and then checks to see if it is the layer removeLayer was called for. It is, so it deletes all its data and returns its parent, the root.
8. optimisticLayer2 receives root, and then checks to see if it is the layer removeLayer was called for. It is not, so it then calls addLayer on its received grandparent, the root layer.
9. The new layer optimisticLayer3 is created by replaying optimisticLayer2's transaction on itself.
10 removeLayer for optimisticLayer2 returns optimisticLayer3 to cache.removeOptimistic, which then sets cache.optimisticData to optimisticLayer3.
At this point, the cache's references are:
```
this.data === Root
this.optimisticData === optimisticLayer3
```
11. 2nd mutation returns from the server, writings its result to the cache's data reference and triggering remove layer process. This falls back to scenario a).
c) What if the 2nd mutation had finished first?
1. 2nd mutation returns from the server. Merges its result onto root layer.
2. removeLayer is called on optimisticLayer2.
3. removeLayer is called on optimisticLayer1.
4. removeLayer is called on root
5. Root returns itself
6. optimisticLayer1 returns itself
7. optimisticLayer2 deletes its data, returns optimisticLayer1
8. Cache's optimisticData reference is updated to optimisticLayer1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment