@envelop/response-cache 5.1.0 → 5.2.0-alpha-20230625183613-3b72bf4a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cjs/plugin.js CHANGED
@@ -233,32 +233,63 @@ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache
233
233
  },
234
234
  }));
235
235
  }
236
- return {
237
- onExecuteDone({ result, setResult }) {
238
- if ((0, core_1.isAsyncIterable)(result)) {
239
- // eslint-disable-next-line no-console
240
- console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
241
- return;
242
- }
243
- const processedResult = processResult(result);
244
- if (skip) {
245
- return;
246
- }
247
- if (!shouldCacheResult({ cacheKey, result: processedResult })) {
248
- return;
236
+ async function maybeCacheResult(result, setResult) {
237
+ const processedResult = processResult(result);
238
+ if (skip) {
239
+ return;
240
+ }
241
+ if (!shouldCacheResult({ cacheKey, result: processedResult })) {
242
+ return;
243
+ }
244
+ // we only use the global ttl if no currentTtl has been determined.
245
+ const finalTtl = currentTtl ?? globalTtl;
246
+ if (finalTtl === 0) {
247
+ if (includeExtensionMetadata) {
248
+ setResult(resultWithMetadata(processedResult, { hit: false, didCache: false }));
249
249
  }
250
- // we only use the global ttl if no currentTtl has been determined.
251
- const finalTtl = currentTtl ?? globalTtl;
252
- if (finalTtl === 0) {
253
- if (includeExtensionMetadata) {
254
- setResult(resultWithMetadata(processedResult, { hit: false, didCache: false }));
255
- }
250
+ return;
251
+ }
252
+ cache.set(cacheKey, processedResult, identifier.values(), finalTtl);
253
+ if (includeExtensionMetadata) {
254
+ setResult(resultWithMetadata(processedResult, { hit: false, didCache: true, ttl: finalTtl }));
255
+ }
256
+ }
257
+ return {
258
+ onExecuteDone(payload) {
259
+ if (!(0, core_1.isAsyncIterable)(payload.result)) {
260
+ maybeCacheResult(payload.result, payload.setResult);
256
261
  return;
257
262
  }
258
- cache.set(cacheKey, processedResult, identifier.values(), finalTtl);
259
- if (includeExtensionMetadata) {
260
- setResult(resultWithMetadata(processedResult, { hit: false, didCache: true, ttl: finalTtl }));
261
- }
263
+ // When the result is an AsyncIterable, it means the query is using @defer or @stream.
264
+ // This means we have to build the final result by merging the incremental results.
265
+ // The merged result is then used to know if we should cache it and to calculate the ttl.
266
+ let result = {};
267
+ return {
268
+ onNext(payload) {
269
+ const { data, errors, extensions, incremental, hasNext } = payload.result;
270
+ if (data) {
271
+ // This is the first result with the initial data payload sent to the client. We use it as the base result
272
+ if (data) {
273
+ result = { data };
274
+ }
275
+ if (errors) {
276
+ result.errors = errors;
277
+ }
278
+ if (extensions) {
279
+ result.extensions = extensions;
280
+ }
281
+ }
282
+ if (incremental) {
283
+ for (const patch of incremental) {
284
+ mergePatch(result, patch);
285
+ }
286
+ }
287
+ if (!hasNext) {
288
+ // The query is complete, we can process the final result
289
+ maybeCacheResult(result, payload.setResult);
290
+ }
291
+ },
292
+ };
262
293
  },
263
294
  };
264
295
  },
@@ -286,3 +317,44 @@ function calculateTtl(typeTtl, currentTtl) {
286
317
  exports.cacheControlDirective = `
287
318
  directive @cacheControl(maxAge: Int) on FIELD_DEFINITION | OBJECT
288
319
  `;
320
+ function mergePatch(result, patch) {
321
+ // All errors and extensions are merged together in the final result
322
+ if (patch.errors) {
323
+ result.errors = [...(result.errors ?? []), ...patch.errors];
324
+ }
325
+ if (patch.extensions) {
326
+ result.extensions = { ...result.extensions, ...patch.extensions };
327
+ }
328
+ // We need to follow the path to the point where the patch should be applied
329
+ let target = result;
330
+ const path = ['data', ...(patch.path ?? [])];
331
+ for (let i = 0; i < path.length - 1; i++) {
332
+ target = target[path[i]];
333
+ }
334
+ const prop = path[path.length - 1];
335
+ const items = patch.items;
336
+ const data = patch.data;
337
+ if (items) {
338
+ // This is a stream patch, the last path segment should be a number and is the index at which we should begin to insert the new items
339
+ const start = +prop;
340
+ for (let i = 0; i < items.length; i++) {
341
+ target[start + i] = deepMerge(target[start + i], items[i]);
342
+ }
343
+ }
344
+ else if (data !== undefined) {
345
+ // This is a defer patch.
346
+ target[prop] = deepMerge(target[prop], data);
347
+ }
348
+ }
349
+ function deepMerge(target, source) {
350
+ if (typeof target === 'object' && target != null) {
351
+ target = Array.isArray(target) ? [...target] : { ...target };
352
+ for (const key of Object.keys(source)) {
353
+ target[key] = deepMerge(target[key], source[key]);
354
+ }
355
+ return target;
356
+ }
357
+ else {
358
+ return source;
359
+ }
360
+ }
package/esm/plugin.js CHANGED
@@ -226,32 +226,63 @@ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl
226
226
  },
227
227
  }));
228
228
  }
229
- return {
230
- onExecuteDone({ result, setResult }) {
231
- if (isAsyncIterable(result)) {
232
- // eslint-disable-next-line no-console
233
- console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
234
- return;
235
- }
236
- const processedResult = processResult(result);
237
- if (skip) {
238
- return;
239
- }
240
- if (!shouldCacheResult({ cacheKey, result: processedResult })) {
241
- return;
229
+ async function maybeCacheResult(result, setResult) {
230
+ const processedResult = processResult(result);
231
+ if (skip) {
232
+ return;
233
+ }
234
+ if (!shouldCacheResult({ cacheKey, result: processedResult })) {
235
+ return;
236
+ }
237
+ // we only use the global ttl if no currentTtl has been determined.
238
+ const finalTtl = currentTtl ?? globalTtl;
239
+ if (finalTtl === 0) {
240
+ if (includeExtensionMetadata) {
241
+ setResult(resultWithMetadata(processedResult, { hit: false, didCache: false }));
242
242
  }
243
- // we only use the global ttl if no currentTtl has been determined.
244
- const finalTtl = currentTtl ?? globalTtl;
245
- if (finalTtl === 0) {
246
- if (includeExtensionMetadata) {
247
- setResult(resultWithMetadata(processedResult, { hit: false, didCache: false }));
248
- }
243
+ return;
244
+ }
245
+ cache.set(cacheKey, processedResult, identifier.values(), finalTtl);
246
+ if (includeExtensionMetadata) {
247
+ setResult(resultWithMetadata(processedResult, { hit: false, didCache: true, ttl: finalTtl }));
248
+ }
249
+ }
250
+ return {
251
+ onExecuteDone(payload) {
252
+ if (!isAsyncIterable(payload.result)) {
253
+ maybeCacheResult(payload.result, payload.setResult);
249
254
  return;
250
255
  }
251
- cache.set(cacheKey, processedResult, identifier.values(), finalTtl);
252
- if (includeExtensionMetadata) {
253
- setResult(resultWithMetadata(processedResult, { hit: false, didCache: true, ttl: finalTtl }));
254
- }
256
+ // When the result is an AsyncIterable, it means the query is using @defer or @stream.
257
+ // This means we have to build the final result by merging the incremental results.
258
+ // The merged result is then used to know if we should cache it and to calculate the ttl.
259
+ let result = {};
260
+ return {
261
+ onNext(payload) {
262
+ const { data, errors, extensions, incremental, hasNext } = payload.result;
263
+ if (data) {
264
+ // This is the first result with the initial data payload sent to the client. We use it as the base result
265
+ if (data) {
266
+ result = { data };
267
+ }
268
+ if (errors) {
269
+ result.errors = errors;
270
+ }
271
+ if (extensions) {
272
+ result.extensions = extensions;
273
+ }
274
+ }
275
+ if (incremental) {
276
+ for (const patch of incremental) {
277
+ mergePatch(result, patch);
278
+ }
279
+ }
280
+ if (!hasNext) {
281
+ // The query is complete, we can process the final result
282
+ maybeCacheResult(result, payload.setResult);
283
+ }
284
+ },
285
+ };
255
286
  },
256
287
  };
257
288
  },
@@ -278,3 +309,44 @@ function calculateTtl(typeTtl, currentTtl) {
278
309
  export const cacheControlDirective = /* GraphQL */ `
279
310
  directive @cacheControl(maxAge: Int) on FIELD_DEFINITION | OBJECT
280
311
  `;
312
+ function mergePatch(result, patch) {
313
+ // All errors and extensions are merged together in the final result
314
+ if (patch.errors) {
315
+ result.errors = [...(result.errors ?? []), ...patch.errors];
316
+ }
317
+ if (patch.extensions) {
318
+ result.extensions = { ...result.extensions, ...patch.extensions };
319
+ }
320
+ // We need to follow the path to the point where the patch should be applied
321
+ let target = result;
322
+ const path = ['data', ...(patch.path ?? [])];
323
+ for (let i = 0; i < path.length - 1; i++) {
324
+ target = target[path[i]];
325
+ }
326
+ const prop = path[path.length - 1];
327
+ const items = patch.items;
328
+ const data = patch.data;
329
+ if (items) {
330
+ // This is a stream patch, the last path segment should be a number and is the index at which we should begin to insert the new items
331
+ const start = +prop;
332
+ for (let i = 0; i < items.length; i++) {
333
+ target[start + i] = deepMerge(target[start + i], items[i]);
334
+ }
335
+ }
336
+ else if (data !== undefined) {
337
+ // This is a defer patch.
338
+ target[prop] = deepMerge(target[prop], data);
339
+ }
340
+ }
341
+ function deepMerge(target, source) {
342
+ if (typeof target === 'object' && target != null) {
343
+ target = Array.isArray(target) ? [...target] : { ...target };
344
+ for (const key of Object.keys(source)) {
345
+ target[key] = deepMerge(target[key], source[key]);
346
+ }
347
+ return target;
348
+ }
349
+ else {
350
+ return source;
351
+ }
352
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@envelop/response-cache",
3
- "version": "5.1.0",
3
+ "version": "5.2.0-alpha-20230625183613-3b72bf4a",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
- "@envelop/core": "^4.0.0",
6
+ "@envelop/core": "4.0.1-alpha-20230625183613-3b72bf4a",
7
7
  "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
8
8
  },
9
9
  "dependencies": {
10
+ "@graphql-tools/executor": "^1.1.0",
10
11
  "@graphql-tools/utils": "^10.0.0",
11
12
  "@whatwg-node/fetch": "^0.9.0",
12
13
  "fast-json-stable-stringify": "^2.1.0",