Skip to content

Commit

Permalink
Add FinalizationRegistry test of cache.gc, and make it pass.
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamn committed Jun 28, 2021
1 parent 2118266 commit be7199b
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 5 deletions.
102 changes: 102 additions & 0 deletions scripts/memory/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,108 @@ describe("garbage collection", () => {
}));
});

itAsync("should release cache.storeReader if requested via cache.gc", (resolve, reject) => {
const expectedKeys = {
__proto__: null,
StoreReader1: true,
ObjectCanon1: true,
StoreReader2: true,
ObjectCanon2: true,
StoreReader3: false,
ObjectCanon3: false,
};

const registry = makeRegistry(key => {
// Referring to client here should keep the client itself alive
// until after the ObservableQuery is (or should have been)
// collected. Collecting the ObservableQuery just because the whole
// client instance was collected is not interesting.
assert.strictEqual(client instanceof ApolloClient, true);
if (key in expectedKeys) {
assert.strictEqual(expectedKeys[key], true, key);
}
delete expectedKeys[key];
if (Object.keys(expectedKeys).every(key => !expectedKeys[key])) {
setTimeout(resolve, 100);
}
}, reject);

const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
local() {
return "hello";
},
},
},
},
});

const client = new ApolloClient({ cache });

(function () {
const query = gql`query { local }`;
const obsQuery = client.watchQuery({ query });

function register(suffix) {
const reader = cache["storeReader"];
registry.register(reader, "StoreReader" + suffix);
registry.register(reader.canon, "ObjectCanon" + suffix);
}

register(1);

const sub = obsQuery.subscribe({
next(result) {
assert.deepStrictEqual(result.data, {
local: "hello",
});

assert.strictEqual(
cache.readQuery({ query }),
result.data,
);

assert.deepStrictEqual(cache.gc(), []);

// Nothing changes because we merely called cache.gc().
assert.strictEqual(
cache.readQuery({ query }),
result.data,
);

assert.deepStrictEqual(cache.gc({
// Now reset the result cache but preserve reader.canon, so the
// results will be === even though they have to be recomputed.
resetResultCache: true,
resetResultIdentities: false,
}), []);

register(2);

const dataAfterResetWithSameCanon = cache.readQuery({ query });
assert.strictEqual(dataAfterResetWithSameCanon, result.data);

assert.deepStrictEqual(cache.gc({
// Finally, do a full reset of the result caching system, including
// discarding reader.canon, so === result identity is lost.
resetResultCache: true,
resetResultIdentities: true,
}), []);

register(3);

const dataAfterFullReset = cache.readQuery({ query });
assert.notStrictEqual(dataAfterFullReset, result.data);
assert.deepStrictEqual(dataAfterFullReset, result.data);

sub.unsubscribe();
},
});
})();
});

itAsync("should collect ObservableQuery after tear-down", (resolve, reject) => {
const expectedKeys = new Set([
"ObservableQuery",
Expand Down
15 changes: 10 additions & 5 deletions src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,11 +507,20 @@ export type FieldValueGetter = EntityStore["getFieldValue"];
class CacheGroup {
private d: OptimisticDependencyFunction<string> | null = null;

// Used by the EntityStore#makeCacheKey method to compute cache keys
// specific to this CacheGroup.
public keyMaker: Trie<object>;

constructor(
public readonly caching: boolean,
private parent: CacheGroup | null = null,
) {
this.d = caching ? dep<string>() : null;
this.resetCaching();
}

public resetCaching() {
this.d = this.caching ? dep<string>() : null;
this.keyMaker = new Trie(canUseWeakMap);
}

public depend(dataId: string, storeFieldName: string) {
Expand Down Expand Up @@ -547,10 +556,6 @@ class CacheGroup {
);
}
}

// Used by the EntityStore#makeCacheKey method to compute cache keys
// specific to this CacheGroup.
public readonly keyMaker = new Trie<object>(canUseWeakMap);
}

function makeDepKey(dataId: string, storeFieldName: string) {
Expand Down
8 changes: 8 additions & 0 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
}
}
});

// Since we have thrown away all the cached functions that depend on the
// CacheGroup dependencies maintained by EntityStore, we should also reset
// all CacheGroup dependency information.
new Set([
this.data.group,
this.optimisticData.group,
]).forEach(group => group.resetCaching());
}

public restore(data: NormalizedCacheObject): this {
Expand Down

0 comments on commit be7199b

Please sign in to comment.