Skip to content

Commit cafc43f

Browse files
authored
check enable only at execution phase and don't stop execution (#2094)
1 parent a8936e9 commit cafc43f

File tree

4 files changed

+202
-100
lines changed

4 files changed

+202
-100
lines changed

.changeset/breezy-pots-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@envelop/response-cache': patch
3+
---
4+
5+
Run enabled once only in execution

packages/plugins/response-cache/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,37 @@ When configuring the `useResponseCache`, you can choose the type of cache:
3636
- In-Memory LRU Cache (default)
3737
- Redis Cache (see: `@envelop/response-cache-redis`)
3838

39+
### Note on Plugin ordering
40+
41+
This plugin rely on a custom executor to work. This means that this plugin should in most cases
42+
placed last in the plugin list. Otherwise, some other plugin might override the custom executor.
43+
44+
For example, this would not work:
45+
46+
```ts
47+
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
48+
import { envelop, useEngine } from '@envelop/core'
49+
import { useResponseCache } from '@envelop/response-cache'
50+
51+
// Don't
52+
const getEnveloped = envelop({
53+
plugins: [
54+
useResponseCache(),
55+
// Here, useEngine will override the `execute` function, leading to a non working cache.
56+
useEngine({ parse, validate, specifiedRules, execute, subscribe }),
57+
]
58+
})
59+
60+
// Do
61+
const getEnveloped = envelop({
62+
plugins: [
63+
useEngine({ parse, validate, specifiedRules, execute, subscribe }),
64+
// Here, the plugin can control the `execute` function
65+
useResponseCache(),
66+
]
67+
})
68+
```
69+
3970
### In-Memory Cache
4071

4172
The in-memory LRU cache is used by default.

packages/plugins/response-cache/src/plugin.ts

Lines changed: 123 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import jsonStableStringify from 'fast-json-stable-stringify';
22
import {
3+
ASTVisitor,
34
DocumentNode,
45
ExecutionArgs,
56
getOperationAST,
@@ -16,6 +17,7 @@ import {
1617
Maybe,
1718
ObjMap,
1819
OnExecuteDoneHookResult,
20+
OnExecuteHookResult,
1921
Plugin,
2022
} from '@envelop/core';
2123
import {
@@ -205,63 +207,77 @@ export type ResponseCacheExecutionResult = ExecutionResult<
205207
{ responseCache?: ResponseCacheExtensions }
206208
>;
207209

208-
const originalDocumentMap = new WeakMap<DocumentNode, DocumentNode>();
209-
const addEntityInfosToDocument = memoize4(function addTypeNameToDocument(
210+
const getDocumentWithMetadataAndTTL = memoize4(function addTypeNameToDocument(
210211
document: DocumentNode,
211-
addTypeNameToDocumentOpts: { invalidateViaMutation: boolean },
212+
{
213+
invalidateViaMutation,
214+
ttlPerSchemaCoordinate,
215+
}: {
216+
invalidateViaMutation: boolean;
217+
ttlPerSchemaCoordinate?: Record<string, number | undefined>;
218+
},
212219
schema: any,
213220
idFieldByTypeName: Map<string, string>,
214-
): DocumentNode {
221+
): [DocumentNode, number | undefined] {
215222
const typeInfo = new TypeInfo(schema);
216-
const newDocument = visit(
217-
document,
218-
visitWithTypeInfo(typeInfo, {
219-
OperationDefinition: {
220-
enter(node): void | false {
221-
if (!addTypeNameToDocumentOpts.invalidateViaMutation && node.operation === 'mutation') {
222-
return false;
223-
}
224-
if (node.operation === 'subscription') {
225-
return false;
226-
}
227-
},
223+
let ttl: number | undefined;
224+
const visitor: ASTVisitor = {
225+
OperationDefinition: {
226+
enter(node): void | false {
227+
if (!invalidateViaMutation && node.operation === 'mutation') {
228+
return false;
229+
}
230+
if (node.operation === 'subscription') {
231+
return false;
232+
}
228233
},
229-
SelectionSet(node, _key) {
234+
},
235+
...(ttlPerSchemaCoordinate != null && {
236+
Field(fieldNode) {
230237
const parentType = typeInfo.getParentType();
231-
const idField = parentType && idFieldByTypeName.get(parentType.name);
232-
return {
233-
...node,
234-
selections: [
235-
{
236-
kind: Kind.FIELD,
237-
name: {
238-
kind: Kind.NAME,
239-
value: '__typename',
240-
},
241-
alias: {
242-
kind: Kind.NAME,
243-
value: '__responseCacheTypeName',
244-
},
245-
},
246-
...(idField
247-
? [
248-
{
249-
kind: Kind.FIELD,
250-
name: { kind: Kind.NAME, value: idField },
251-
alias: { kind: Kind.NAME, value: '__responseCacheId' },
252-
},
253-
]
254-
: []),
255-
256-
...node.selections,
257-
],
258-
};
238+
if (parentType) {
239+
const schemaCoordinate = `${parentType.name}.${fieldNode.name.value}`;
240+
const maybeTtl = ttlPerSchemaCoordinate[schemaCoordinate];
241+
if (maybeTtl !== undefined) {
242+
ttl = calculateTtl(maybeTtl, ttl);
243+
}
244+
}
259245
},
260246
}),
261-
);
247+
SelectionSet(node, _key) {
248+
const parentType = typeInfo.getParentType();
249+
const idField = parentType && idFieldByTypeName.get(parentType.name);
250+
return {
251+
...node,
252+
selections: [
253+
{
254+
kind: Kind.FIELD,
255+
name: {
256+
kind: Kind.NAME,
257+
value: '__typename',
258+
},
259+
alias: {
260+
kind: Kind.NAME,
261+
value: '__responseCacheTypeName',
262+
},
263+
},
264+
...(idField
265+
? [
266+
{
267+
kind: Kind.FIELD,
268+
name: { kind: Kind.NAME, value: idField },
269+
alias: { kind: Kind.NAME, value: '__responseCacheId' },
270+
},
271+
]
272+
: []),
273+
274+
...node.selections,
275+
],
276+
};
277+
},
278+
};
262279

263-
originalDocumentMap.set(newDocument, document);
264-
return newDocument;
280+
return [visit(document, visitWithTypeInfo(typeInfo, visitor)), ttl];
265281
});
266282

267283
export function useResponseCache<PluginContext extends Record<string, any> = {}>({
@@ -289,7 +305,10 @@ export function useResponseCache<PluginContext extends Record<string, any> = {}>
289305

290306
// never cache Introspections
291307
ttlPerSchemaCoordinate = { 'Query.__schema': 0, ...ttlPerSchemaCoordinate };
292-
const addTypeNameToDocumentOpts = { invalidateViaMutation };
308+
const documentMetadataOptions = {
309+
queries: { invalidateViaMutation, ttlPerSchemaCoordinate },
310+
mutations: { invalidateViaMutation }, // remove ttlPerSchemaCoordinate for mutations to skip TTL calculation
311+
};
293312
const idFieldByTypeName = new Map<string, string>();
294313
let schema: any;
295314

@@ -303,22 +322,6 @@ export function useResponseCache<PluginContext extends Record<string, any> = {}>
303322
}
304323

305324
return {
306-
onParse() {
307-
return ({ result, replaceParseResult, context }) => {
308-
if (enabled && !enabled(context)) {
309-
return;
310-
}
311-
if (!originalDocumentMap.has(result) && result.kind === Kind.DOCUMENT) {
312-
const newDocument = addEntityInfosToDocument(
313-
result,
314-
addTypeNameToDocumentOpts,
315-
schema,
316-
idFieldByTypeName,
317-
);
318-
replaceParseResult(newDocument);
319-
}
320-
};
321-
},
322325
onSchemaChange({ schema: newSchema }) {
323326
if (schema === newSchema) {
324327
return;
@@ -373,11 +376,35 @@ export function useResponseCache<PluginContext extends Record<string, any> = {}>
373376
}
374377
const identifier = new Map<string, CacheEntityRecord>();
375378
const types = new Set<string>();
379+
let currentTtl: number | undefined;
380+
let skip = false;
376381

377382
const sessionId = session(onExecuteParams.args.contextValue);
378383

379-
let currentTtl: number | undefined;
380-
let skip = false;
384+
function setExecutor({
385+
execute,
386+
onExecuteDone,
387+
}: {
388+
execute: typeof onExecuteParams.executeFn;
389+
onExecuteDone?: OnExecuteHookResult<PluginContext>['onExecuteDone'];
390+
}): OnExecuteHookResult<PluginContext> {
391+
let executed = false;
392+
onExecuteParams.setExecuteFn(args => {
393+
executed = true;
394+
return execute(args);
395+
});
396+
return {
397+
onExecuteDone(params) {
398+
if (!executed) {
399+
// eslint-disable-next-line no-console
400+
console.warn(
401+
'[useResponseCache] The cached execute function was not called, another plugin might have overwritten it. Please check your plugin order.',
402+
);
403+
}
404+
return onExecuteDone?.(params);
405+
},
406+
};
407+
}
381408

382409
function processResult(data: any) {
383410
if (data == null || typeof data !== 'object') {
@@ -450,15 +477,24 @@ export function useResponseCache<PluginContext extends Record<string, any> = {}>
450477
);
451478

452479
if (operationAST?.operation === 'mutation') {
453-
return {
480+
return setExecutor({
481+
execute(args) {
482+
const [document] = getDocumentWithMetadataAndTTL(
483+
args.document,
484+
documentMetadataOptions.mutations,
485+
args.schema,
486+
idFieldByTypeName,
487+
);
488+
return onExecuteParams.executeFn({ ...args, document });
489+
},
454490
onExecuteDone({ result, setResult }) {
455491
if (isAsyncIterable(result)) {
456492
return handleAsyncIterableResult(invalidateCache);
457493
}
458494

459495
return invalidateCache(result, setResult);
460496
},
461-
};
497+
});
462498
}
463499
}
464500

@@ -473,33 +509,12 @@ export function useResponseCache<PluginContext extends Record<string, any> = {}>
473509
const cachedResponse = (await cache.get(cacheKey)) as ResponseCacheExecutionResult;
474510

475511
if (cachedResponse != null) {
476-
if (includeExtensionMetadata) {
477-
onExecuteParams.setResultAndStopExecution(
478-
resultWithMetadata(cachedResponse, { hit: true }),
479-
);
480-
} else {
481-
onExecuteParams.setResultAndStopExecution(cachedResponse);
482-
}
483-
return;
484-
}
485-
486-
if (ttlPerSchemaCoordinate) {
487-
const typeInfo = new TypeInfo(onExecuteParams.args.schema);
488-
visit(
489-
onExecuteParams.args.document,
490-
visitWithTypeInfo(typeInfo, {
491-
Field(fieldNode) {
492-
const parentType = typeInfo.getParentType();
493-
if (parentType) {
494-
const schemaCoordinate = `${parentType.name}.${fieldNode.name.value}`;
495-
const maybeTtl = ttlPerSchemaCoordinate[schemaCoordinate];
496-
if (maybeTtl !== undefined) {
497-
currentTtl = calculateTtl(maybeTtl, currentTtl);
498-
}
499-
}
500-
},
501-
}),
502-
);
512+
return setExecutor({
513+
execute: () =>
514+
includeExtensionMetadata
515+
? resultWithMetadata(cachedResponse, { hit: true })
516+
: cachedResponse,
517+
});
503518
}
504519

505520
function maybeCacheResult(
@@ -523,15 +538,25 @@ export function useResponseCache<PluginContext extends Record<string, any> = {}>
523538
}
524539
}
525540

526-
return {
541+
return setExecutor({
542+
execute(args) {
543+
const [document, ttl] = getDocumentWithMetadataAndTTL(
544+
args.document,
545+
documentMetadataOptions.queries,
546+
schema,
547+
idFieldByTypeName,
548+
);
549+
currentTtl = ttl;
550+
return onExecuteParams.executeFn({ ...args, document });
551+
},
527552
onExecuteDone({ result, setResult }) {
528553
if (isAsyncIterable(result)) {
529554
return handleAsyncIterableResult(maybeCacheResult);
530555
}
531556

532557
return maybeCacheResult(result, setResult);
533558
},
534-
};
559+
});
535560
},
536561
};
537562
}

0 commit comments

Comments
 (0)