Skip to content

Commit

Permalink
add resultCachMaxSize config
Browse files Browse the repository at this point in the history
  • Loading branch information
Sofian Hnaide committed Apr 20, 2021
1 parent d77b6b4 commit be42d88
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 70 deletions.
114 changes: 114 additions & 0 deletions src/cache/inmemory/__tests__/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1421,6 +1421,120 @@ describe('Cache', () => {
expect(numBroadcasts).toEqual(1);
});
});

describe('config', () => {
function setupTestData(config?: InMemoryCacheConfig) {
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
book: {
keyArgs: [ "isbn" ],
}
}
},
Book: {
keyFields: ["isbn"],
},
},
...config,
});

const query = gql`
query GetBook($isbn: String!) {
book(isbn: $isbn) {
title
}
}
`;

function writeValue(i: number) {
const isbn = i;
const title = `Book number: ${i}`;
cache.writeQuery({
query,
variables: { isbn },
data: {
book: {
__typename: "Book",
title,
isbn,
},
},
});
}

function readValue(i : number) {
return cache.readQuery({ query, variables: { isbn: i } });
}

return { writeValue, readValue };
}

it("should return the same result when resultCaching is enabled (default)", () => {
const { writeValue, readValue } = setupTestData();

/*
* Write the first value.
*/
writeValue(0);
const result1 = readValue(0);
const result2 = readValue(0);

/*
* Verify that the values matchs.
*/
expect(result2).toBe(result1);
});

it("should return the different result when resultCaching is disabled", () => {
const { writeValue, readValue } = setupTestData({ resultCaching: false });

/*
* Write the first value.
*/
writeValue(0);
const result1 = readValue(0);
const result2 = readValue(0);

/*
* Verify that the values are different.
*/
expect(result2).not.toBe(result1);
});

it("should set cache limit when resultCachMaxSize is set", () => {
const resultCachMaxSize = 100;
const { writeValue, readValue } = setupTestData({ resultCachMaxSize });

let i = 0;
/*
* Write the first value.
*/
writeValue(i);
const result1 = readValue(0);
let result2 = readValue(0);

/*
* Verify that initially the values matchs.
*/
expect(result2).toBe(result1);

/*
* Keep writing new values and reading them until the cache limit hits
* and the first query is no longer in the cache, if exceeded the max
* size and the value are still equal, fail early as we don't need to wait
* to timeout.
*/
while (result2 === result1 && i < resultCachMaxSize) {
i++;
writeValue(i);
readValue(i);
result2 = readValue(0);
}
expect(result2).not.toBe(result1);
});
});
});

describe("InMemoryCache#broadcastWatches", function () {
Expand Down
64 changes: 36 additions & 28 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import './fixPolyfills';

import { DocumentNode } from 'graphql';
import { dep, wrap } from 'optimism';
import { dep, OptimisticWrapperFunction, wrap } from 'optimism';

import { ApolloCache } from '../core/cache';
import { Cache } from '../core/types/Cache';
Expand Down Expand Up @@ -33,6 +33,7 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig {
resultCaching?: boolean;
possibleTypes?: PossibleTypesMap;
typePolicies?: TypePolicies;
resultCachMaxSize?: number;
}

const defaultConfig: InMemoryCacheConfig = {
Expand All @@ -54,6 +55,11 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
private storeReader: StoreReader;
private storeWriter: StoreWriter;

private maybeBroadcastWatch: OptimisticWrapperFunction<
[Cache.WatchOptions, boolean?],
any,
[Cache.WatchOptions]>;

// Dynamically imported code can augment existing typePolicies or
// possibleTypes by calling cache.policies.addTypePolicies or
// cache.policies.addPossibletypes.
Expand Down Expand Up @@ -93,8 +99,37 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
this.storeReader = new StoreReader({
cache: this,
addTypename: this.addTypename,
resultCachMaxSize: this.config.resultCachMaxSize,
}),
);

this.maybeBroadcastWatch = wrap((
c: Cache.WatchOptions,
fromOptimisticTransaction?: boolean,
) => {
return this.broadcastWatch.call(this, c, !!fromOptimisticTransaction);
}, {
max: this.config.resultCachMaxSize,
makeCacheKey: (c: Cache.WatchOptions) => {
// Return a cache key (thus enabling result caching) only if we're
// currently using a data store that can track cache dependencies.
const store = c.optimistic ? this.optimisticData : this.data;
if (supportsResultCaching(store)) {
const { optimistic, rootId, variables } = c;
return store.makeCacheKey(
c.query,
// Different watches can have the same query, optimistic
// status, rootId, and variables, but if their callbacks are
// different, the (identical) result needs to be delivered to
// each distinct callback. The easiest way to achieve that
// separation is to include c.callback in the cache key for
// maybeBroadcastWatch calls. See issue #5733.
c.callback,
JSON.stringify({ optimistic, rootId, variables }),
);
}
}
});
}

public restore(data: NormalizedCacheObject): this {
Expand Down Expand Up @@ -368,33 +403,6 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
}
}

private maybeBroadcastWatch = wrap((
c: Cache.WatchOptions,
fromOptimisticTransaction?: boolean,
) => {
return this.broadcastWatch.call(this, c, !!fromOptimisticTransaction);
}, {
makeCacheKey: (c: Cache.WatchOptions) => {
// Return a cache key (thus enabling result caching) only if we're
// currently using a data store that can track cache dependencies.
const store = c.optimistic ? this.optimisticData : this.data;
if (supportsResultCaching(store)) {
const { optimistic, rootId, variables } = c;
return store.makeCacheKey(
c.query,
// Different watches can have the same query, optimistic
// status, rootId, and variables, but if their callbacks are
// different, the (identical) result needs to be delivered to
// each distinct callback. The easiest way to achieve that
// separation is to include c.callback in the cache key for
// maybeBroadcastWatch calls. See issue #5733.
c.callback,
JSON.stringify({ optimistic, rootId, variables }),
);
}
}
});

private watchDep = dep<Cache.WatchOptions>();

// This method is wrapped by maybeBroadcastWatch, which is called by
Expand Down
94 changes: 52 additions & 42 deletions src/cache/inmemory/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,63 @@ type ExecSubSelectedArrayOptions = {
export interface StoreReaderConfig {
cache: InMemoryCache,
addTypename?: boolean;
resultCachMaxSize?: number;
}

export class StoreReader {

// cached version of executeSelectionset
private executeSelectionSet: OptimisticWrapperFunction<
[ExecSelectionSetOptions], // Actual arguments tuple type.
ExecResult, // Actual return type.
// Arguments type after keyArgs translation.
[SelectionSetNode, StoreObject | Reference, ReadMergeModifyContext]>;

// cached version of executeSubSelectedArray
private executeSubSelectedArray: OptimisticWrapperFunction<
[ExecSubSelectedArrayOptions],
ExecResult<any>,
[ExecSubSelectedArrayOptions]>;

constructor(private config: StoreReaderConfig) {
this.config = { addTypename: true, ...config };

this.executeSelectionSet = wrap(options => this.execSelectionSetImpl(options), {
keyArgs(options) {
return [
options.selectionSet,
options.objectOrReference,
options.context,
];
},
max: this.config.resultCachMaxSize,
// Note that the parameters of makeCacheKey are determined by the
// array returned by keyArgs.
makeCacheKey(selectionSet, parent, context) {
if (supportsResultCaching(context.store)) {
return context.store.makeCacheKey(
selectionSet,
isReference(parent) ? parent.__ref : parent,
context.varString,
);
}
}
});

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

/**
Expand Down Expand Up @@ -152,33 +204,6 @@ export class StoreReader {
return false;
}

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

// Uncached version of executeSelectionSet.
private execSelectionSetImpl({
selectionSet,
Expand Down Expand Up @@ -339,21 +364,6 @@ export class StoreReader {

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,
Expand Down

0 comments on commit be42d88

Please sign in to comment.