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

feat(response-cache): onTtl hook to manipulate the cached response #2392

Merged
merged 4 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/great-bees-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@envelop/response-cache': minor
---

New `onTtl` hook to manipulate TTL of the cached response
21 changes: 19 additions & 2 deletions packages/plugins/response-cache/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
getDirective,
MapperKind,
mapSchema,
MaybePromise,
memoize1,
memoize4,
mergeIncrementalResult,
Expand All @@ -51,7 +52,7 @@ export type BuildResponseCacheKeyFunction = (params: {
sessionId: Maybe<string>;
/** GraphQL Context */
context: ExecutionArgs['contextValue'];
}) => Promise<string>;
}) => MaybePromise<string>;

export type GetDocumentStringFunction = (executionArgs: ExecutionArgs) => string;

Expand Down Expand Up @@ -147,8 +148,17 @@ export type UseResponseCacheParameter<PluginContext extends Record<string, any>
* Use this function to customize the behavior, such as caching results that have an EnvelopError.
*/
shouldCacheResult?: ShouldCacheResultFunction;
/**
* Hook that when TTL is calculated, allows to modify the TTL value.
*/
onTtl?: ResponseCacheOnTtlFunction<PluginContext>;
};

export type ResponseCacheOnTtlFunction<PluginContext> = (payload: {
ttl: number;
context: PluginContext;
}) => number;

/**
* Default function used for building the response cache key.
* It is exported here for advanced use-cases. E.g. if you want to short circuit and serve responses from the cache on a global level in order to completely by-pass the GraphQL flow.
Expand Down Expand Up @@ -306,6 +316,7 @@ export function useResponseCache<PluginContext extends Record<string, any> = {}>
buildResponseCacheKey = defaultBuildResponseCacheKey,
getDocumentString = defaultGetDocumentString,
shouldCacheResult = defaultShouldCacheResult,
onTtl,
includeExtensionMetadata = typeof process !== 'undefined'
? // eslint-disable-next-line dot-notation
process.env['NODE_ENV'] === 'development' || !!process.env['DEBUG']
Expand Down Expand Up @@ -562,7 +573,13 @@ export function useResponseCache<PluginContext extends Record<string, any> = {}>
}

// we only use the global ttl if no currentTtl has been determined.
const finalTtl = currentTtl ?? globalTtl;
let finalTtl = currentTtl ?? globalTtl;
if (onTtl) {
finalTtl = onTtl({
ttl: finalTtl,
context: onExecuteParams.args.contextValue,
});
}

if (skip || !shouldCacheResult({ cacheKey, result }) || finalTtl === 0) {
if (includeExtensionMetadata) {
Expand Down
57 changes: 57 additions & 0 deletions packages/plugins/response-cache/test/response-cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
cacheControlDirective,
createInMemoryCache,
defaultBuildResponseCacheKey,
ResponseCacheOnTtlFunction,
useResponseCache,
} from '../src/index.js';

Expand Down Expand Up @@ -4230,3 +4231,59 @@ it('correctly remove cache keys from incremental delivery result', async () => {
},
});
});

it('manipulates the TTL', async () => {
const schema = makeExecutableSchema({
typeDefs: /* GraphQL */ `
type Query {
foo: String
}
`,
resolvers: {
Query: {
foo: () => 'bar',
},
},
});
const expectedFinalTtl = 5000;
const onTtlOriginal: ResponseCacheOnTtlFunction<unknown> = () => expectedFinalTtl;
const onTtl = jest.fn(onTtlOriginal);
const testkit = createTestkit(
[
useResponseCache({
session: () => null,
ttl: 1000,
ttlPerSchemaCoordinate: { 'Query.foo': 2000 },
includeExtensionMetadata: true,
onTtl,
}),
],
schema,
);

const operation = /* GraphQL */ `
query {
foo
}
`;

const context = {};

const result = await testkit.execute(operation, {}, context);
expect(result).toEqual({
data: {
foo: 'bar',
},
extensions: {
responseCache: {
didCache: true,
hit: false,
ttl: expectedFinalTtl,
},
},
});
expect(onTtl).toHaveBeenCalledWith({
ttl: 2000,
context,
});
});
Loading