UNPKG

5.46 kBJavaScriptView Raw
1import { createInMemoryCache, defaultBuildResponseCacheKey, useResponseCache as useEnvelopResponseCache, } from '@envelop/response-cache';
2const operationIdByRequest = new WeakMap();
3// We trick Envelop plugin by passing operationId as sessionId so we can take it from cache key builder we pass to Envelop
4function sessionFactoryForEnvelop({ request }) {
5 return operationIdByRequest.get(request);
6}
7const cacheKeyFactoryForEnvelop = async function cacheKeyFactoryForEnvelop({ sessionId }) {
8 if (sessionId == null) {
9 throw new Error('[useResponseCache] This plugin is not configured correctly. Make sure you use this plugin with GraphQL Yoga');
10 }
11 return sessionId;
12};
13const getDocumentStringForEnvelop = (executionArgs) => {
14 const context = executionArgs.contextValue;
15 if (context.params?.query == null) {
16 throw new Error('[useResponseCache] This plugin is not configured correctly. Make sure you use this plugin with GraphQL Yoga');
17 }
18 return context.params.query;
19};
20export function useResponseCache(options) {
21 const buildResponseCacheKey = options?.buildResponseCacheKey || defaultBuildResponseCacheKey;
22 const cache = options.cache ?? createInMemoryCache();
23 const enabled = options.enabled ?? (() => true);
24 let logger;
25 return {
26 onYogaInit({ yoga }) {
27 logger = yoga.logger;
28 },
29 onPluginInit({ addPlugin }) {
30 addPlugin(useEnvelopResponseCache({
31 ...options,
32 enabled({ request }) {
33 return enabled(request);
34 },
35 cache,
36 getDocumentString: getDocumentStringForEnvelop,
37 session: sessionFactoryForEnvelop,
38 buildResponseCacheKey: cacheKeyFactoryForEnvelop,
39 shouldCacheResult({ cacheKey, result }) {
40 const shouldCached = options.shouldCacheResult
41 ? options.shouldCacheResult({ cacheKey, result })
42 : !result.errors?.length;
43 if (shouldCached) {
44 const extensions = (result.extensions || (result.extensions = {}));
45 const httpExtensions = (extensions.http || (extensions.http = {}));
46 const headers = (httpExtensions.headers || (httpExtensions.headers = {}));
47 headers['ETag'] = cacheKey;
48 headers['Last-Modified'] = new Date().toUTCString();
49 }
50 else {
51 logger.warn('[useResponseCache] Failed to cache due to errors');
52 }
53 return shouldCached;
54 },
55 }));
56 },
57 async onRequest({ request, fetchAPI, endResponse }) {
58 if (enabled(request)) {
59 const operationId = request.headers.get('If-None-Match');
60 if (operationId) {
61 const cachedResponse = await cache.get(operationId);
62 if (cachedResponse) {
63 const lastModifiedFromClient = request.headers.get('If-Modified-Since');
64 const lastModifiedFromCache = cachedResponse.extensions?.http?.headers?.['Last-Modified'];
65 if (
66 // This should be in the extensions already but we check it here to make sure
67 lastModifiedFromCache != null &&
68 // If the client doesn't send If-Modified-Since header, we assume the cache is valid
69 (lastModifiedFromClient == null ||
70 new Date(lastModifiedFromClient).getTime() >=
71 new Date(lastModifiedFromCache).getTime())) {
72 const okResponse = new fetchAPI.Response(null, {
73 status: 304,
74 headers: {
75 ETag: operationId,
76 },
77 });
78 endResponse(okResponse);
79 }
80 }
81 }
82 }
83 },
84 async onParams({ params, request, setResult }) {
85 const operationId = await buildResponseCacheKey({
86 documentString: params.query || '',
87 variableValues: params.variables,
88 operationName: params.operationName,
89 sessionId: await options.session(request),
90 });
91 operationIdByRequest.set(request, operationId);
92 if (enabled(request)) {
93 const cachedResponse = await cache.get(operationId);
94 if (cachedResponse) {
95 if (options.includeExtensionMetadata) {
96 setResult({
97 ...cachedResponse,
98 extensions: {
99 ...cachedResponse.extensions,
100 responseCache: {
101 hit: true,
102 },
103 },
104 });
105 }
106 else {
107 setResult(cachedResponse);
108 }
109 return;
110 }
111 }
112 },
113 };
114}
115export { createInMemoryCache };