From ec6ab66f43dd9dcba6fc5bbddfcafde08fd7e80b Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Mon, 2 Dec 2019 11:32:03 -0500 Subject: [PATCH] Implement EntityStore#makeCacheKey to simplify result caching. 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). --- src/cache/inmemory/entityStore.ts | 36 ++++++++++++++++++----------- src/cache/inmemory/inMemoryCache.ts | 7 +----- src/cache/inmemory/readFromStore.ts | 31 ++++++------------------- 3 files changed, 30 insertions(+), 44 deletions(-) diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts index 635a21971bf..df5bc670bb3 100644 --- a/src/cache/inmemory/entityStore.ts +++ b/src/cache/inmemory/entityStore.ts @@ -1,4 +1,4 @@ -import { dep, OptimisticDependencyFunction } from 'optimism'; +import { dep, OptimisticDependencyFunction, KeyTrie } from 'optimism'; import { invariant } from 'ts-invariant'; import { isReference, StoreValue } from '../../utilities/graphql/storeUtils'; import { @@ -6,6 +6,7 @@ import { 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, @@ -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 | null = null; @@ -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(canUseWeakMap); } function makeDepKey(dataId: string, storeFieldName?: string) { diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index 13ac80df595..24077d8a23e 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -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, @@ -50,7 +48,6 @@ export class InMemoryCache extends ApolloCache { private typenameDocumentCache = new Map(); private storeReader: StoreReader; private storeWriter: StoreWriter; - private cacheKeyRoot = new KeyTrie(canUseWeakMap); // Set this while in a transaction to prevent broadcasts... // don't forget to turn it back on! @@ -87,7 +84,6 @@ export class InMemoryCache extends ApolloCache { this.storeReader = new StoreReader({ addTypename: this.addTypename, - cacheKeyRoot: this.cacheKeyRoot, policies: this.policies, }); @@ -102,8 +98,7 @@ export class InMemoryCache extends ApolloCache { 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 }), ); diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index b914c637145..bfca9b7daa7 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -5,7 +5,7 @@ import { InlineFragmentNode, SelectionSetNode, } from 'graphql'; -import { wrap, KeyTrie } from 'optimism'; +import { wrap } from 'optimism'; import { invariant } from 'ts-invariant'; import { @@ -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 { @@ -75,20 +74,12 @@ type ExecSubSelectedArrayOptions = { export interface StoreReaderConfig { addTypename?: boolean; - cacheKeyRoot?: KeyTrie; policies: Policies; } export class StoreReader { constructor(private config: StoreReaderConfig) { - const cacheKeyRoot = - config && config.cacheKeyRoot || new KeyTrie(canUseWeakMap); - - this.config = { - addTypename: true, - cacheKeyRoot, - ...config, - }; + this.config = { addTypename: true, ...config }; const { executeSelectionSet, @@ -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, ); } } @@ -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),