Skip to content

Commit

Permalink
Response caching for batched requests (#3759)
Browse files Browse the repository at this point in the history
  • Loading branch information
enisdenjo authored Feb 14, 2025
1 parent d1aa532 commit bba7a83
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 10 deletions.
7 changes: 7 additions & 0 deletions .changeset/grumpy-llamas-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-yoga/plugin-response-cache': patch
---

Provide cache key per oparation in a batched request

Instead of per request, which would give out the same cache key for every operation in a batched request.
207 changes: 207 additions & 0 deletions packages/plugins/response-cache/__tests__/response-cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1261,3 +1261,210 @@ it('gets the context in "session" and "buildResponseCacheKey"', async () => {
context,
});
});

it('should work correctly with batching and async race conditions', async () => {
const store = new Map<
string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
>();
const yoga = createYoga({
maskedErrors: false,
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
a: String!
c: String!
e: String!
}
`,
resolvers: {
Query: {
a: () => 'b',
c: () => 'd',
e: () => 'f',
},
},
}),
batching: true,
plugins: [
useResponseCache({
session: () => null,
includeExtensionMetadata: true,
cache: {
get(key) {
return new Promise(resolve => {
setTimeout(() => {
// resolve hit in the next tick creating an async race condition
resolve(store.get(key));
});
});
},
set(key, value) {
store.set(key, value);
return Promise.resolve();
},
invalidate() {
throw new Error('Unexpected cache invalidate');
},
},
}),
],
});

async function execute() {
const res = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify([
{
query: /* GraphQL */ `
{
a
e
}
`,
},
{
query: /* GraphQL */ `
{
a
c
}
`,
},
{
query: /* GraphQL */ `
{
e
c
}
`,
},
]),
});
return res.json();
}

await expect(execute()).resolves.toMatchInlineSnapshot(`
[
{
"data": {
"a": "b",
"e": "f",
},
"extensions": {
"responseCache": {
"didCache": true,
"hit": false,
"ttl": null,
},
},
},
{
"data": {
"a": "b",
"c": "d",
},
"extensions": {
"responseCache": {
"didCache": true,
"hit": false,
"ttl": null,
},
},
},
{
"data": {
"c": "d",
"e": "f",
},
"extensions": {
"responseCache": {
"didCache": true,
"hit": false,
"ttl": null,
},
},
},
]
`);

await expect(execute()).resolves.toMatchInlineSnapshot(`
[
{
"data": {
"a": "b",
"e": "f",
},
"extensions": {
"responseCache": {
"hit": true,
},
},
},
{
"data": {
"a": "b",
"c": "d",
},
"extensions": {
"responseCache": {
"hit": true,
},
},
},
{
"data": {
"c": "d",
"e": "f",
},
"extensions": {
"responseCache": {
"hit": true,
},
},
},
]
`);

await expect(execute()).resolves.toMatchInlineSnapshot(`
[
{
"data": {
"a": "b",
"e": "f",
},
"extensions": {
"responseCache": {
"hit": true,
},
},
},
{
"data": {
"a": "b",
"c": "d",
},
"extensions": {
"responseCache": {
"hit": true,
},
},
},
{
"data": {
"c": "d",
"e": "f",
},
"extensions": {
"responseCache": {
"hit": true,
},
},
},
]
`);
});
13 changes: 3 additions & 10 deletions packages/plugins/response-cache/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type UseResponseCacheParameter<TContext = YogaInitialContext> = Omit<
buildResponseCacheKey?: BuildResponseCacheKeyFunction;
};

const operationIdByRequest = new WeakMap<Request, string>();
const operationIdByContext = new WeakMap<YogaInitialContext, string>();
const sessionByRequest = new WeakMap<Request, Maybe<string>>();

function sessionFactoryForEnvelop({ request }: YogaInitialContext) {
Expand All @@ -40,14 +40,7 @@ function sessionFactoryForEnvelop({ request }: YogaInitialContext) {

const cacheKeyFactoryForEnvelop: EnvelopBuildResponseCacheKeyFunction =
async function cacheKeyFactoryForEnvelop({ context }) {
const request = (context as YogaInitialContext).request;
if (request == null) {
throw new Error(
'[useResponseCache] This plugin is not configured correctly. Make sure you use this plugin with GraphQL Yoga',
);
}

const operationId = operationIdByRequest.get(request);
const operationId = operationIdByContext.get(context as YogaInitialContext);
if (operationId == null) {
throw new Error(
'[useResponseCache] This plugin is not configured correctly. Make sure you use this plugin with GraphQL Yoga',
Expand Down Expand Up @@ -164,7 +157,7 @@ export function useResponseCache<TContext = YogaInitialContext>(
request,
context,
});
operationIdByRequest.set(request, operationId);
operationIdByContext.set(context as YogaInitialContext, operationId);
sessionByRequest.set(request, sessionId);
if (enabled(request, context as TContext)) {
const cachedResponse = await cache.get(operationId);
Expand Down

0 comments on commit bba7a83

Please sign in to comment.