Skip to content

Commit

Permalink
Merge pull request #6274 from apollographql/skip-writing-fresh-cache-…
Browse files Browse the repository at this point in the history
…results

Optimization: skip writing still-fresh result objects back into the EntityStore.
  • Loading branch information
benjamn authored May 13, 2020
2 parents 381c4d2 + 32b3f42 commit 635ca49
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 72 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"@wry/equality": "^0.1.9",
"fast-json-stable-stringify": "^2.0.0",
"graphql-tag": "^2.10.2",
"optimism": "^0.11.5",
"optimism": "^0.12.1",
"symbol-observable": "^1.2.0",
"ts-invariant": "^0.4.4",
"tslib": "^1.10.0",
Expand Down
83 changes: 83 additions & 0 deletions src/cache/inmemory/__tests__/writeToStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2002,4 +2002,87 @@ describe('writing to the store', () => {
expect(Object.isFrozen(result.scalarFieldWithObjectValue.b)).toBe(true);
expect(Object.isFrozen(result.scalarFieldWithObjectValue.c)).toBe(true);
});

it("should skip writing still-fresh result objects", function () {
const cache = new InMemoryCache({
typePolicies: {
Todo: {
fields: {
text: {
merge(_, text: string) {
mergeCounts[text] = ~~mergeCounts[text] + 1;
return text;
},
},
},
},
},
});

const mergeCounts: Record<string, number> = Object.create(null);

const query = gql`
query {
todos {
id
text
}
}
`;

expect(mergeCounts).toEqual({});

cache.writeQuery({
query,
data: {
todos: [
{ __typename: "Todo", id: 1, text: "first" },
{ __typename: "Todo", id: 2, text: "second" },
],
},
});

expect(mergeCounts).toEqual({ first: 1, second: 1 });

function read() {
return cache.readQuery<{ todos: any[] }>({ query })!.todos;
}

const twoTodos = read();

expect(mergeCounts).toEqual({ first: 1, second: 1 });

const threeTodos = [
...twoTodos,
{ __typename: "Todo", id: 3, text: "third" },
];

cache.writeQuery({
query,
data: {
todos: threeTodos,
},
});

expect(mergeCounts).toEqual({ first: 1, second: 1, third: 1 });

const threeTodosAgain = read();
twoTodos.forEach((todo, i) => expect(todo).toBe(threeTodosAgain[i]));

const fourTodos = [
threeTodosAgain[2],
threeTodosAgain[0],
{ __typename: "Todo", id: 4, text: "fourth" },
threeTodosAgain[1],
];

cache.writeQuery({
query,
data: {
todos: fourTodos,
},
});

expect(mergeCounts).toEqual({ first: 1, second: 1, third: 1, fourth: 1 });
});
});
9 changes: 4 additions & 5 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,10 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {

this.storeWriter = new StoreWriter({
policies: this.policies,
});

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

const cache = this;
Expand Down
2 changes: 2 additions & 0 deletions src/cache/inmemory/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,8 @@ export class Policies {

export interface ReadMergeContext {
variables: Record<string, any>;
// A JSON.stringify-serialized version of context.variables.
varString: string;
toReference: ToReferenceFunction;
getFieldValue: FieldValueGetter;
}
Expand Down
116 changes: 72 additions & 44 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 } from 'optimism';
import { wrap, OptimisticWrapperFunction } from 'optimism';
import { invariant, InvariantError } from 'ts-invariant';

import {
Expand Down Expand Up @@ -47,8 +47,6 @@ interface ExecContext extends ReadMergeContext {
policies: Policies;
fragmentMap: FragmentMap;
variables: VariableMap;
// A JSON.stringify-serialized version of context.variables.
varString: string;
path: (string | number)[];
};

Expand Down Expand Up @@ -89,45 +87,6 @@ export interface StoreReaderConfig {
export class StoreReader {
constructor(private config: StoreReaderConfig) {
this.config = { addTypename: true, ...config };

const {
executeSelectionSet,
executeSubSelectedArray,
} = this;

this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
objectOrReference,
context,
}: ExecSelectionSetOptions) {
if (supportsResultCaching(context.store)) {
return context.store.makeCacheKey(
selectionSet,
context.varString,
isReference(objectOrReference)
? objectOrReference.__ref
: objectOrReference,
);
}
}
});

this.executeSubSelectedArray = wrap((options: ExecSubSelectedArrayOptions) => {
return executeSubSelectedArray.call(this, options);
}, {
makeCacheKey({ field, array, context }) {
if (supportsResultCaching(context.store)) {
return context.store.makeCacheKey(
field,
array,
context.varString,
);
}
}
});
}

/**
Expand Down Expand Up @@ -200,7 +159,54 @@ export class StoreReader {
};
}

private executeSelectionSet({
public isFresh(
result: Record<string, any>,
store: NormalizedCache,
parent: StoreObject | Reference,
selectionSet: SelectionSetNode,
varString: string,
): boolean {
if (supportsResultCaching(store) &&
this.knownResults.get(result) === selectionSet) {
const latest = this.executeSelectionSet.peek(
store, selectionSet, parent, varString);
if (latest && result === latest.result) {
return true;
}
}
return false;
}

// Cached version of execSelectionSetImpl.
private executeSelectionSet: OptimisticWrapperFunction<
[ExecSelectionSetOptions], // Actual arguments tuple type.
ExecResult, // Actual return type.
// Arguments type after keyArgs translation.
[NormalizedCache, SelectionSetNode, StoreObject | Reference, string]
> = wrap(options => this.execSelectionSetImpl(options), {
keyArgs(options) {
return [
options.context.store,
options.selectionSet,
options.objectOrReference,
options.context.varString,
];
},
// Note that the parameters of makeCacheKey are determined by the
// array returned by keyArgs.
makeCacheKey(store, selectionSet, parent, varString) {
if (supportsResultCaching(store)) {
return store.makeCacheKey(
selectionSet,
isReference(parent) ? parent.__ref : parent,
varString,
);
}
}
});

// Uncached version of executeSelectionSet.
private execSelectionSetImpl({
selectionSet,
objectOrReference,
context,
Expand Down Expand Up @@ -342,10 +348,32 @@ export class StoreReader {
Object.freeze(finalResult.result);
}

// Store this result with its selection set so that we can quickly
// recognize it again in the StoreReader#isFresh method.
this.knownResults.set(finalResult.result, selectionSet);

return finalResult;
}

private executeSubSelectedArray({
private knownResults = new WeakMap<Record<string, any>, SelectionSetNode>();

// Cached version of execSubSelectedArrayImpl.
private executeSubSelectedArray = wrap((options: ExecSubSelectedArrayOptions) => {
return this.execSubSelectedArrayImpl(options);
}, {
makeCacheKey({ field, array, context }) {
if (supportsResultCaching(context.store)) {
return context.store.makeCacheKey(
field,
array,
context.varString,
);
}
}
});

// Uncached version of executeSubSelectedArray.
private execSubSelectedArrayImpl({
field,
array,
context,
Expand Down
Loading

0 comments on commit 635ca49

Please sign in to comment.