Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suspense cache: use Trie directly #10969

Merged
merged 9 commits into from
Jun 13, 2023
5 changes: 5 additions & 0 deletions .changeset/tasty-wasps-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@apollo/client': patch
---

Slightly decrease bundle size and memory footprint of `SuspenseCache` by changing how cache entries are stored internally.
28 changes: 11 additions & 17 deletions src/react/cache/SuspenseCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ interface SuspenseCacheOptions {
}

export class SuspenseCache {
private cacheKeys = new Trie<CacheKey>(
canUseWeakMap,
(cacheKey: CacheKey) => cacheKey
);

private queryRefs = new Map<CacheKey, QueryReference>();
private queryRefs = new Trie<{ current?: QueryReference }>(canUseWeakMap);
private options: SuspenseCacheOptions;

constructor(options: SuspenseCacheOptions = Object.create(null)) {
Expand All @@ -35,19 +30,18 @@ export class SuspenseCache {
cacheKey: CacheKey,
createObservable: () => ObservableQuery<TData>
) {
const stableCacheKey = this.cacheKeys.lookupArray(cacheKey);
const ref = this.queryRefs.lookupArray(cacheKey);

if (!this.queryRefs.has(stableCacheKey)) {
this.queryRefs.set(
stableCacheKey,
new QueryReference(createObservable(), {
key: stableCacheKey,
autoDisposeTimeoutMs: this.options.autoDisposeTimeoutMs,
onDispose: () => this.queryRefs.delete(stableCacheKey),
})
);
if (!ref.current) {
ref.current = new QueryReference(createObservable(), {
key: cacheKey,
autoDisposeTimeoutMs: this.options.autoDisposeTimeoutMs,
onDispose: () => {
delete ref.current;
},
});
}

return this.queryRefs.get(stableCacheKey)! as QueryReference<TData>;
return ref.current as QueryReference<TData>;
}
}
7 changes: 5 additions & 2 deletions src/react/hooks/__tests__/useBackgroundQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -697,8 +697,11 @@ describe('useBackgroundQuery', () => {
}
);

expect(directSuspenseCache['queryRefs'].size).toBe(1);
expect(contextSuspenseCache['queryRefs'].size).toBe(0);
expect(directSuspenseCache).toHaveSuspenseCacheEntryUsing(client, query);
expect(contextSuspenseCache).not.toHaveSuspenseCacheEntryUsing(
client,
query
);
});

it('passes context to the link', async () => {
Expand Down
52 changes: 39 additions & 13 deletions src/react/hooks/__tests__/useSuspenseQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,11 @@ describe('useSuspenseQuery', () => {
}
);

expect(directSuspenseCache['queryRefs'].size).toBe(1);
expect(contextSuspenseCache['queryRefs'].size).toBe(0);
expect(directSuspenseCache).toHaveSuspenseCacheEntryUsing(client, query);
expect(contextSuspenseCache).not.toHaveSuspenseCacheEntryUsing(
client,
query
);
});

it('ensures a valid fetch policy is used', () => {
Expand Down Expand Up @@ -678,7 +681,7 @@ describe('useSuspenseQuery', () => {
);

expect(client.getObservableQueries().size).toBe(1);
expect(suspenseCache['queryRefs'].size).toBe(1);
expect(suspenseCache).toHaveSuspenseCacheEntryUsing(client, query);

unmount();

Expand All @@ -687,7 +690,7 @@ describe('useSuspenseQuery', () => {
await wait(0);

expect(client.getObservableQueries().size).toBe(0);
expect(suspenseCache['queryRefs'].size).toBe(0);
expect(suspenseCache).not.toHaveSuspenseCacheEntryUsing(client, query);
});

it('tears down all queries when rendering with multiple variable sets', async () => {
Expand Down Expand Up @@ -716,7 +719,12 @@ describe('useSuspenseQuery', () => {
});

expect(client.getObservableQueries().size).toBe(2);
expect(suspenseCache['queryRefs'].size).toBe(2);
expect(suspenseCache).toHaveSuspenseCacheEntryUsing(client, query, {
variables: { id: '1' },
});
expect(suspenseCache).toHaveSuspenseCacheEntryUsing(client, query, {
variables: { id: '2' },
});

unmount();

Expand All @@ -725,7 +733,13 @@ describe('useSuspenseQuery', () => {
await wait(0);

expect(client.getObservableQueries().size).toBe(0);
expect(suspenseCache['queryRefs'].size).toBe(0);

expect(suspenseCache).not.toHaveSuspenseCacheEntryUsing(client, query, {
variables: { id: '1' },
});
expect(suspenseCache).not.toHaveSuspenseCacheEntryUsing(client, query, {
variables: { id: '2' },
});
});

it('tears down all queries when multiple clients are used', async () => {
Expand Down Expand Up @@ -773,9 +787,16 @@ describe('useSuspenseQuery', () => {
});
});

const variables = { id: '1' };

expect(client1.getObservableQueries().size).toBe(1);
expect(client2.getObservableQueries().size).toBe(1);
expect(suspenseCache['queryRefs'].size).toBe(2);
expect(suspenseCache).toHaveSuspenseCacheEntryUsing(client1, query, {
variables,
});
expect(suspenseCache).toHaveSuspenseCacheEntryUsing(client2, query, {
variables,
});

unmount();

Expand All @@ -785,7 +806,12 @@ describe('useSuspenseQuery', () => {

expect(client1.getObservableQueries().size).toBe(0);
expect(client2.getObservableQueries().size).toBe(0);
expect(suspenseCache['queryRefs'].size).toBe(0);
expect(suspenseCache).not.toHaveSuspenseCacheEntryUsing(client1, query, {
variables,
});
expect(suspenseCache).not.toHaveSuspenseCacheEntryUsing(client2, query, {
variables,
});
});

it('tears down the query if the component never renders again after suspending', async () => {
Expand Down Expand Up @@ -834,12 +860,12 @@ describe('useSuspenseQuery', () => {
link.simulateComplete();

expect(client.getObservableQueries().size).toBe(1);
expect(suspenseCache['queryRefs'].size).toBe(1);
expect(suspenseCache).toHaveSuspenseCacheEntryUsing(client, query);

jest.advanceTimersByTime(30_000);

expect(client.getObservableQueries().size).toBe(0);
expect(suspenseCache['queryRefs'].size).toBe(0);
expect(suspenseCache).not.toHaveSuspenseCacheEntryUsing(client, query);

jest.useRealTimers();

Expand Down Expand Up @@ -895,12 +921,12 @@ describe('useSuspenseQuery', () => {
link.simulateComplete();

expect(client.getObservableQueries().size).toBe(1);
expect(suspenseCache['queryRefs'].size).toBe(1);
expect(suspenseCache).toHaveSuspenseCacheEntryUsing(client, query);

jest.advanceTimersByTime(5_000);

expect(client.getObservableQueries().size).toBe(0);
expect(suspenseCache['queryRefs'].size).toBe(0);
expect(suspenseCache).not.toHaveSuspenseCacheEntryUsing(client, query);

jest.useRealTimers();

Expand Down Expand Up @@ -957,7 +983,7 @@ describe('useSuspenseQuery', () => {
jest.advanceTimersByTime(30_000);

expect(client.getObservableQueries().size).toBe(1);
expect(suspenseCache['queryRefs'].size).toBe(1);
expect(suspenseCache).toHaveSuspenseCacheEntryUsing(client, query);

jest.useRealTimers();
});
Expand Down
18 changes: 17 additions & 1 deletion src/testing/matchers/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import type { DocumentNode } from '../../core';
import type {
ApolloClient,
DocumentNode,
OperationVariables,
} from '../../core';

interface ApolloCustomMatchers<R = void> {
/**
* Used to determine if two GraphQL query documents are equal to each other by
* comparing their printed values. The document must be parsed by `gql`.
*/
toMatchDocument(document: DocumentNode): R;

/**
* Used to determine if the Suspense cache has a cache entry.
*/
toHaveSuspenseCacheEntryUsing(
client: ApolloClient<unknown>,
query: DocumentNode,
options?: {
variables?: OperationVariables;
queryKey?: string | number | any[];
}
): R;
}

declare global {
Expand Down
2 changes: 2 additions & 0 deletions src/testing/matchers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { expect } from '@jest/globals';
import { toMatchDocument } from './toMatchDocument';
import { toHaveSuspenseCacheEntryUsing } from './toHaveSuspenseCacheEntryUsing';

expect.extend({
toHaveSuspenseCacheEntryUsing,
toMatchDocument,
});
39 changes: 39 additions & 0 deletions src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { MatcherFunction } from 'expect';
import type { DocumentNode } from 'graphql';
import type { ApolloClient, OperationVariables } from '../../core';
import { SuspenseCache } from '../../react';
import { canonicalStringify } from '../../cache';

export const toHaveSuspenseCacheEntryUsing: MatcherFunction<
[
client: ApolloClient<unknown>,
query: DocumentNode,
options: {
variables?: OperationVariables;
queryKey?: string | number | any[];
}
]
> = function (
suspenseCache,
client,
query,
{ variables, queryKey = [] } = Object.create(null)
) {
if (!(suspenseCache instanceof SuspenseCache)) {
throw new Error('Actual must be an instance of `SuspenseCache`');
}

const cacheKey = (
[client, query, canonicalStringify(variables)] as any[]
).concat(queryKey);
const queryRef = suspenseCache['queryRefs'].lookupArray(cacheKey)?.current;

return {
pass: !!queryRef,
message: () => {
return `Expected suspense cache ${
queryRef ? 'not ' : ''
}to have cache entry using key`;
},
};
};