Skip to content

Commit

Permalink
Merge pull request #8188 from apollographql/allow-opting-out-of-canon…
Browse files Browse the repository at this point in the history
…ization

Allow selectively opting out of cache result canonization.
  • Loading branch information
benjamn committed May 11, 2021
2 parents 51f5378 + 44bdaca commit 40fca2e
Show file tree
Hide file tree
Showing 12 changed files with 477 additions and 64 deletions.
63 changes: 62 additions & 1 deletion docs/source/api/cache/InMemoryCache.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@ By specifying the ID of another cached object, you can query arbitrary cached da
</td>
</tr>

<tr>
<td>

###### `canonizeResults`

`Boolean`
</td>
<td>

If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results.

The default value is `true`.
</td>
</tr>

</tbody>
</table>

Expand Down Expand Up @@ -235,6 +250,21 @@ The default value is `true`.
</td>
</tr>

<tr>
<td>

###### `overwrite`

`Boolean`
</td>
<td>

If `true`, ignore existing cache data when calling `merge` functions, allowing incoming data to replace existing data, without warnings about data loss.

The default value is `false`.
</td>
</tr>

</tbody>
</table>

Expand Down Expand Up @@ -382,6 +412,22 @@ A map of any GraphQL variable names and values required by `fragment`.
</td>
</tr>


<tr>
<td>

###### `canonizeResults`

`Boolean`
</td>
<td>

If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results.

The default value is `true`.
</td>
</tr>

</tbody>
</table>

Expand Down Expand Up @@ -505,6 +551,21 @@ The default value is `true`.
</td>
</tr>

<tr>
<td>

###### `overwrite`

`Boolean`
</td>
<td>

If `true`, ignore existing cache data when calling `merge` functions, allowing incoming data to replace existing data, without warnings about data loss.

The default value is `false`.
</td>
</tr>

</tbody>
</table>

Expand Down Expand Up @@ -624,7 +685,7 @@ See [Modifier function API](#modifier-function-api) below.

If `true`, also modifies the optimistically cached values for included fields.

The default value is `false`.
The default value is `false`.
</td>
</tr>

Expand Down
7 changes: 2 additions & 5 deletions src/cache/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,8 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
optimistic = !!options.optimistic,
): QueryType | null {
return this.read({
...options,
rootId: options.id || 'ROOT_QUERY',
query: options.query,
variables: options.variables,
returnPartialData: options.returnPartialData,
optimistic,
});
}
Expand All @@ -159,10 +157,9 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
optimistic = !!options.optimistic,
): FragmentType | null {
return this.read({
...options,
query: this.getFragmentDoc(options.fragment, options.fragmentName),
variables: options.variables,
rootId: options.id,
returnPartialData: options.returnPartialData,
optimistic,
});
}
Expand Down
1 change: 1 addition & 0 deletions src/cache/core/types/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export namespace Cache {
previousResult?: any;
optimistic: boolean;
returnPartialData?: boolean;
canonizeResults?: boolean;
}

export interface WriteOptions<TResult = any, TVariables = any>
Expand Down
12 changes: 12 additions & 0 deletions src/cache/core/types/DataProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ export namespace DataProxy {
* readQuery method can be omitted. Defaults to false.
*/
optimistic?: boolean;
/**
* Whether to canonize cache results before returning them. Canonization
* takes some extra time, but it speeds up future deep equality comparisons.
* Defaults to true.
*/
canonizeResults?: boolean;
}

export interface ReadFragmentOptions<TData, TVariables>
Expand All @@ -82,6 +88,12 @@ export namespace DataProxy {
* readQuery method can be omitted. Defaults to false.
*/
optimistic?: boolean;
/**
* Whether to canonize cache results before returning them. Canonization
* takes some extra time, but it speeds up future deep equality comparisons.
* Defaults to true.
*/
canonizeResults?: boolean;
}

export interface WriteOptions<TData> {
Expand Down
114 changes: 114 additions & 0 deletions src/cache/inmemory/__tests__/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2010,4 +2010,118 @@ describe('reading from the store', () => {
expect(result1.abc).toBe(abc);
expect(result2.abc).toBe(abc);
});

it("readQuery can opt out of canonization", function () {
let count = 0;

const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
count() {
return count++;
},
},
},
},
});

const canon = cache["storeReader"]["canon"];

const query = gql`
query {
count
}
`;

function readQuery(canonizeResults: boolean) {
return cache.readQuery<{
count: number;
}>({
query,
canonizeResults,
});
}

const nonCanonicalQueryResult0 = readQuery(false);
expect(canon.isCanonical(nonCanonicalQueryResult0)).toBe(false);
expect(nonCanonicalQueryResult0).toEqual({ count: 0 });

const canonicalQueryResult0 = readQuery(true);
expect(canon.isCanonical(canonicalQueryResult0)).toBe(true);
// The preservation of { count: 0 } proves the result didn't have to be
// recomputed, but merely canonized.
expect(canonicalQueryResult0).toEqual({ count: 0 });

cache.evict({
fieldName: "count",
});

const canonicalQueryResult1 = readQuery(true);
expect(canon.isCanonical(canonicalQueryResult1)).toBe(true);
expect(canonicalQueryResult1).toEqual({ count: 1 });

const nonCanonicalQueryResult1 = readQuery(false);
// Since we already read a canonical result, we were able to reuse it when
// reading the non-canonical result.
expect(nonCanonicalQueryResult1).toBe(canonicalQueryResult1);
});

it("readFragment can opt out of canonization", function () {
let count = 0;

const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
count() {
return count++;
},
},
},
},
});

const canon = cache["storeReader"]["canon"];

const fragment = gql`
fragment CountFragment on Query {
count
}
`;

function readFragment(canonizeResults: boolean) {
return cache.readFragment<{
count: number;
}>({
id: "ROOT_QUERY",
fragment,
canonizeResults,
});
}

const canonicalFragmentResult1 = readFragment(true);
expect(canon.isCanonical(canonicalFragmentResult1)).toBe(true);
expect(canonicalFragmentResult1).toEqual({ count: 0 });

const nonCanonicalFragmentResult1 = readFragment(false);
// Since we already read a canonical result, we were able to reuse it when
// reading the non-canonical result.
expect(nonCanonicalFragmentResult1).toBe(canonicalFragmentResult1);

cache.evict({
fieldName: "count",
});

const nonCanonicalFragmentResult2 = readFragment(false);
expect(readFragment(false)).toBe(nonCanonicalFragmentResult2);
expect(canon.isCanonical(nonCanonicalFragmentResult2)).toBe(false);
expect(nonCanonicalFragmentResult2).toEqual({ count: 1 });
expect(readFragment(false)).toBe(nonCanonicalFragmentResult2);

const canonicalFragmentResult2 = readFragment(true);
expect(readFragment(true)).toBe(canonicalFragmentResult2);
expect(canon.isCanonical(canonicalFragmentResult2)).toBe(true);
expect(canonicalFragmentResult2).toEqual({ count: 1 });
});
});
8 changes: 2 additions & 6 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,8 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
} = options;
try {
return this.storeReader.diffQueryAgainstStore<T>({
...options,
store: options.optimistic ? this.optimisticData : this.data,
query: options.query,
variables: options.variables,
rootId: options.rootId,
config: this.config,
returnPartialData,
}).result || null;
Expand Down Expand Up @@ -223,11 +221,9 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {

public diff<T>(options: Cache.DiffOptions): Cache.DiffResult<T> {
return this.storeReader.diffQueryAgainstStore({
...options,
store: options.optimistic ? this.optimisticData : this.data,
rootId: options.id || "ROOT_QUERY",
query: options.query,
variables: options.variables,
returnPartialData: options.returnPartialData,
config: this.config,
});
}
Expand Down
4 changes: 4 additions & 0 deletions src/cache/inmemory/object-canon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export class ObjectCanon {
keys?: SortedKeysInfo;
}>(canUseWeakMap);

public isCanonical(value: any): boolean {
return isObjectOrArray(value) && this.known.has(value);
}

// Make the ObjectCanon assume this value has already been
// canonicalized.
private passes = new WeakMap<object, object>();
Expand Down

0 comments on commit 40fca2e

Please sign in to comment.