Skip to content

Commit

Permalink
Implement EntityStore#makeCacheKey to simplify result caching.
Browse files Browse the repository at this point in the history
Although all the components of a key returned by makeCacheKey are
important, there's usually one that "anchors" the rest, because it
survives the longest and changes the least. In the case of the result
caching system, the current EntityStore object is that anchor.

This commit formalizes that anchoring role by making the EntityStore
literally responsible for generating cache keys based on query AST
objects, variables, and other frequently changing inputs.

Especially given the recent introduction of CacheGroup logic, it's easier
to reason about the makeCacheKey functions if we put part of their
implementation behind an abstraction. This abstraction also means we no
longer need to pass a KeyTrie into the StoreReader and StoreWriter
constructors, as the KeyTrie (context.store.group.keyMaker) now resides
within the EntityStore, which is provided as part of the ExecContext
(context.store).
  • Loading branch information
benjamn committed Dec 3, 2019
1 parent a60b065 commit ec6ab66
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 44 deletions.
36 changes: 22 additions & 14 deletions src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { dep, OptimisticDependencyFunction } from 'optimism';
import { dep, OptimisticDependencyFunction, KeyTrie } from 'optimism';
import { invariant } from 'ts-invariant';
import { isReference, StoreValue } from '../../utilities/graphql/storeUtils';
import {
DeepMerger,
ReconcilerFunction,
} from '../../utilities/common/mergeDeep';
import { isEqual } from '../../utilities/common/isEqual';
import { canUseWeakMap } from '../../utilities/common/canUse';
import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types';
import {
getTypenameFromStoreObject,
Expand Down Expand Up @@ -247,23 +248,26 @@ export abstract class EntityStore implements NormalizedCache {
}
return this.refs[dataId];
}

// Used to compute cache keys specific to this.group.
public makeCacheKey(...args: any[]) {
return this.group.keyMaker.lookupArray(args);
}
}

// A single CacheGroup represents a set of one or more EntityStore objects,
// typically the Root store in a CacheGroup by itself, and all active Layer
// stores in a group together. A single EntityStore object belongs to one
// and only one CacheGroup, store.group. The CacheGroup is responsible for
// tracking dependencies, so store.group serves as a convenient key for
// storing cached results that should be invalidated when/if those
// dependencies change (see the various makeCachekey functions in
// inMemoryCache.ts and readFromStore.ts). If we used the EntityStore
// objects themselves as cache keys (that is, store rather than
// store.group), the cache would become unnecessarily fragmented by all the
// different Layer objects. Instead, the CacheGroup approach allows all
// optimistic Layer objects in the same linked list to belong to one
// CacheGroup, with the non-optimistic Root object belonging to another
// CacheGroup, allowing resultCaching dependencies to be tracked separately
// for optimistic and non-optimistic entity data.
// stores in a group together. A single EntityStore object belongs to only
// one CacheGroup, store.group. The CacheGroup is responsible for tracking
// dependencies, so store.group is helpful for generating unique keys for
// cached results that need to be invalidated when/if those dependencies
// change. If we used the EntityStore objects themselves as cache keys (that
// is, store rather than store.group), the cache would become unnecessarily
// fragmented by all the different Layer objects. Instead, the CacheGroup
// approach allows all optimistic Layer objects in the same linked list to
// belong to one CacheGroup, with the non-optimistic Root object belonging
// to another CacheGroup, allowing resultCaching dependencies to be tracked
// separately for optimistic and non-optimistic entity data.
class CacheGroup {
private d: OptimisticDependencyFunction<string> | null = null;

Expand All @@ -286,6 +290,10 @@ class CacheGroup {
);
}
}

// Used by the EntityStore#makeCacheKey method to compute cache keys
// specific to this CacheGroup.
public readonly keyMaker = new KeyTrie<object>(canUseWeakMap);
}

function makeDepKey(dataId: string, storeFieldName?: string) {
Expand Down
7 changes: 1 addition & 6 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import './fixPolyfills';

import { DocumentNode, SelectionSetNode } from 'graphql';
import { wrap } from 'optimism';
import { KeyTrie } from 'optimism';

import { ApolloCache, Transaction } from '../core/cache';
import { Cache } from '../core/types/Cache';
import { addTypenameToDocument } from '../../utilities/graphql/transform';
import { FragmentMap } from '../../utilities/graphql/fragments';
import { canUseWeakMap } from '../../utilities/common/canUse';
import {
ApolloReducerConfig,
NormalizedCacheObject,
Expand Down Expand Up @@ -50,7 +48,6 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
private typenameDocumentCache = new Map<DocumentNode, DocumentNode>();
private storeReader: StoreReader;
private storeWriter: StoreWriter;
private cacheKeyRoot = new KeyTrie<object>(canUseWeakMap);

// Set this while in a transaction to prevent broadcasts...
// don't forget to turn it back on!
Expand Down Expand Up @@ -87,7 +84,6 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {

this.storeReader = new StoreReader({
addTypename: this.addTypename,
cacheKeyRoot: this.cacheKeyRoot,
policies: this.policies,
});

Expand All @@ -102,8 +98,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
const store = c.optimistic ? cache.optimisticData : cache.data;
if (supportsResultCaching(store)) {
const { optimistic, rootId, variables } = c;
return cache.cacheKeyRoot.lookup(
store.group,
return store.makeCacheKey(
c.query,
JSON.stringify({ optimistic, rootId, variables }),
);
Expand Down
31 changes: 7 additions & 24 deletions src/cache/inmemory/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
InlineFragmentNode,
SelectionSetNode,
} from 'graphql';
import { wrap, KeyTrie } from 'optimism';
import { wrap } from 'optimism';
import { invariant } from 'ts-invariant';

import {
Expand All @@ -17,7 +17,6 @@ import {
makeReference,
StoreValue,
} from '../../utilities/graphql/storeUtils';
import { canUseWeakMap } from '../../utilities/common/canUse';
import { createFragmentMap, FragmentMap } from '../../utilities/graphql/fragments';
import { shouldInclude } from '../../utilities/graphql/directives';
import {
Expand Down Expand Up @@ -75,20 +74,12 @@ type ExecSubSelectedArrayOptions = {

export interface StoreReaderConfig {
addTypename?: boolean;
cacheKeyRoot?: KeyTrie<object>;
policies: Policies;
}

export class StoreReader {
constructor(private config: StoreReaderConfig) {
const cacheKeyRoot =
config && config.cacheKeyRoot || new KeyTrie<object>(canUseWeakMap);

this.config = {
addTypename: true,
cacheKeyRoot,
...config,
};
this.config = { addTypename: true, ...config };

const {
executeSelectionSet,
Expand All @@ -104,17 +95,12 @@ export class StoreReader {
context,
}: ExecSelectionSetOptions) {
if (supportsResultCaching(context.store)) {
return cacheKeyRoot.lookup(
// EntityStore objects share the same store.group if their
// dependencies are tracked together (for example, optimistic
// versus non-optimistic data), so we can reduce cache key
// diversity by using context.store.group here instead of just
// context.store, which promotes reusability of cached
// optimistic results.
context.store.group,
return context.store.makeCacheKey(
selectionSet,
JSON.stringify(context.variables),
isReference(objectOrReference) ? objectOrReference.__ref : objectOrReference,
isReference(objectOrReference)
? objectOrReference.__ref
: objectOrReference,
);
}
}
Expand All @@ -125,10 +111,7 @@ export class StoreReader {
}, {
makeCacheKey({ field, array, context }) {
if (supportsResultCaching(context.store)) {
return cacheKeyRoot.lookup(
// See comment above about why context.store.group is used
// here, instead of context.store.
context.store.group,
return context.store.makeCacheKey(
field,
array,
JSON.stringify(context.variables),
Expand Down

0 comments on commit ec6ab66

Please sign in to comment.