Skip to content

Commit

Permalink
add atuomatic removal of entries on eviction
Browse files Browse the repository at this point in the history
  • Loading branch information
Sofian Hnaide committed Apr 22, 2021
1 parent 9e20661 commit cb18b39
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 7 deletions.
89 changes: 87 additions & 2 deletions src/cache/inmemory/__tests__/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import gql from 'graphql-tag';
import { stripSymbols } from '../../../utilities/testing/stripSymbols';
import { StoreObject } from '../types';
import { StoreReader } from '../readFromStore';
import { makeReference, InMemoryCache, Reference, isReference } from '../../../core';
import { makeReference, InMemoryCache, Reference, isReference, InMemoryCacheConfig } from '../../../core';
import { Cache } from '../../core/types/Cache';
import { MissingFieldError } from '../../core/types/common';
import { defaultNormalizedCacheFactory, readQueryFromStore } from './helpers';
Expand Down Expand Up @@ -1337,7 +1337,7 @@ describe('reading from the store', () => {
});
});

it("propagates eviction signals to parent queries", () => {
function getEvictionTestData(options: InMemoryCacheConfig = {}) {
const cache = new InMemoryCache({
typePolicies: {
Deity: {
Expand All @@ -1363,6 +1363,7 @@ describe('reading from the store', () => {
},
},
},
...options,
});

const rulerQuery = gql`
Expand Down Expand Up @@ -1421,6 +1422,31 @@ describe('reading from the store', () => {
});
}

function evictAndGC(name: string) {
/*
* The number of evicted entries is 1 (the evicted entry) + the number of
* gc'ed entries.
*/
let evicted = 1;
cache.evict({
id: cache.identify({ __typename: "Deity", name }),
});
evicted += cache.gc().length;
return evicted;
}

return {
watch,
cache,
rulerQuery,
diffs,
children,
evictAndGC,
};
}

it("propagates eviction signals to parent queries", () => {
const { watch, cache, rulerQuery, diffs, children } = getEvictionTestData();
const cancel = watch();

function devour(name: string) {
Expand Down Expand Up @@ -1827,4 +1853,63 @@ describe('reading from the store', () => {
},
});
});

it('clears cached entries when garbage collected', () => {
const { cache, rulerQuery, evictAndGC } = getEvictionTestData();

/*
* TODO: find a better way to spy on forgetKey.
*/
const reader = (cache as any).storeReader;
const executeSelectionSetSpy = jest.spyOn((reader as any)['executeSelectionSet'], 'forgetKey');
const executeSubSelectedArraySpy = jest.spyOn((reader as any)['executeSubSelectedArray'], 'forgetKey');

cache.watch({
query: rulerQuery,
immediate: true,
optimistic: true,
callback: () => {},
});
expect(executeSelectionSetSpy).not.toBeCalled();
expect(executeSubSelectedArraySpy).not.toBeCalled();

/*
* Since each Deity entry has exactly one cache entry at this point we
* expect the forget count to match.
*/
const evicted = evictAndGC('Cronus');
expect(executeSelectionSetSpy).toBeCalledTimes(evicted);
expect(executeSubSelectedArraySpy).toBeCalledTimes(evicted);
});

it('does not clear caches when resultCaching is disabled', () => {
const { cache, evictAndGC, rulerQuery } = getEvictionTestData({ resultCaching: false });
/*
* TODO: find a better way to spy.
*/
const reader = (cache as any).storeReader;
const executeSelectionSetEvictionHandler = reader['executeSelectionSetEvictionHandlers'];
const executeSubSelectedEvictionHandler = reader['executeSubSelectedArrayEvictionHandlers'];
const executeSelectionSetSpy = jest.spyOn(reader['executeSelectionSet'], 'forgetKey');
const executeSubSelectedArraySpy = jest.spyOn(reader['executeSubSelectedArray'], 'forgetKey');

cache.watch({
query: rulerQuery,
immediate: true,
optimistic: true,
callback: () => {},
});
expect(executeSelectionSetSpy).not.toBeCalled();
expect(executeSubSelectedArraySpy).not.toBeCalled();

evictAndGC('Cronus');

/*
* Even after evicition no clearing happens since resultCaching is disabled.
*/
expect(executeSelectionSetSpy).not.toBeCalled();
expect(executeSubSelectedArraySpy).not.toBeCalled();
expect(executeSelectionSetEvictionHandler.size).toEqual(0);
expect(executeSubSelectedEvictionHandler.size).toEqual(0);
});
});
10 changes: 8 additions & 2 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,9 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {

// Request garbage collection of unreachable normalized entities.
public gc() {
return this.optimisticData.gc();
const evicted = this.optimisticData.gc();
this.storeReader.onEvict(evicted);
return evicted;
}

// Call this method to ensure the given root ID remains in the cache after
Expand Down Expand Up @@ -312,7 +314,11 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
// this.txCount still seems like a good idea, for uniformity with
// the other update methods.
++this.txCount;
return this.optimisticData.evict(options);
const evicted = this.optimisticData.evict(options);
if (evicted) {
this.storeReader.onEvict([ options.id! ]);
}
return evicted;
} finally {
if (!--this.txCount && options.broadcast !== false) {
this.broadcastWatches();
Expand Down
49 changes: 46 additions & 3 deletions src/cache/inmemory/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
DiffQueryAgainstStoreOptions,
NormalizedCache,
ReadMergeModifyContext,
StoreValue,
} from './types';
import { supportsResultCaching } from './entityStore';
import { getTypenameFromStoreObject } from './helpers';
Expand Down Expand Up @@ -75,6 +76,7 @@ type ExecSubSelectedArrayOptions = {
field: FieldNode;
array: any[];
context: ReadContext;
ref: StoreValue;
};

export interface StoreReaderConfig {
Expand All @@ -98,10 +100,25 @@ export class StoreReader {
ExecResult<any>,
[ExecSubSelectedArrayOptions]>;

private executeSelectionSetEvictionHandlers: Map<StoreValue, any[]> = new Map();
private executeSubSelectedArrayEvictionHandlers: Map<StoreValue, any[]> = new Map();

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

this.executeSelectionSet = wrap(options => this.execSelectionSetImpl(options), {
this.executeSelectionSet = wrap(options => {
const key = this.executeSelectionSet.getKey(options);
if (key !== void 0) {
const ref = options.objectOrReference.__ref;
let handlers = this.executeSelectionSetEvictionHandlers.get(ref);
if (!handlers) {
handlers = [];
this.executeSelectionSetEvictionHandlers.set(ref, handlers);
}
handlers!.push(key);
}
return this.execSelectionSetImpl(options);
}, {
keyArgs(options) {
return [
options.selectionSet,
Expand All @@ -122,8 +139,17 @@ export class StoreReader {
}
}
});

this.executeSubSelectedArray = wrap((options: ExecSubSelectedArrayOptions) => {
this.executeSubSelectedArray = wrap((options: ExecSubSelectedArrayOptions) => {
const key = this.executeSubSelectedArray.getKey(options);
if (key !== void 0) {
const ref = options.ref;
let handlers = this.executeSubSelectedArrayEvictionHandlers.get(ref);
if (!handlers) {
handlers = [];
this.executeSubSelectedArrayEvictionHandlers.set(ref, handlers);
}
handlers!.push(key);
}
return this.execSubSelectedArrayImpl(options);
}, {
max: this.config.resultCachMaxSize,
Expand All @@ -139,6 +165,20 @@ export class StoreReader {
});
}

public onEvict(evicted: string[]) {
evicted.forEach(dataId => {
this.executeSelectionSetEvictionHandlers.get(dataId)?.forEach((k) => {
this.executeSelectionSet.forgetKey(k);
});
this.executeSelectionSetEvictionHandlers.delete(dataId);

this.executeSubSelectedArrayEvictionHandlers.get(dataId)?.forEach((k) => {
this.executeSubSelectedArray.forgetKey(k);
});
this.executeSubSelectedArrayEvictionHandlers.delete(dataId);
});
}

/**
* Given a store and a query, return as much of the result as possible and
* identify if any data was missing from the store.
Expand Down Expand Up @@ -300,6 +340,7 @@ export class StoreReader {
field: selection,
array: fieldValue,
context,
ref: objectOrReference.__ref,
}));

} else if (!selection.selectionSet) {
Expand Down Expand Up @@ -369,6 +410,7 @@ export class StoreReader {
field,
array,
context,
ref,
}: ExecSubSelectedArrayOptions): ExecResult {
let missing: MissingFieldError[] | undefined;

Expand Down Expand Up @@ -401,6 +443,7 @@ export class StoreReader {
field,
array: item,
context,
ref,
}), i);
}

Expand Down

0 comments on commit cb18b39

Please sign in to comment.