@envelop/response-cache 8.2.0-alpha-20251013081815-d6f74fceb1a32fd336b2664f90f87872e1bbf2fe → 8.2.0-alpha-20251217212728-de569f3d7fe45e330eca177b69ee1c89fd5dc498

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/README.md CHANGED
@@ -863,3 +863,67 @@ mutation SetNameMutation {
863
863
  }
864
864
  }
865
865
  ```
866
+
867
+ #### Get scope of the query
868
+
869
+ Useful for building a cache with more flexibility (e.g. generate a key that is shared across all
870
+ sessions when `PUBLIC`).
871
+
872
+ ```ts
873
+ import jsonStableStringify from 'fast-json-stable-stringify'
874
+ import { execute, parse, subscribe, validate } from 'graphql'
875
+ import { envelop } from '@envelop/core'
876
+ import { hashSHA256, useResponseCache } from '@envelop/response-cache'
877
+
878
+ const schema = buildSchema(/* GraphQL */ `
879
+ ${cacheControlDirective}
880
+ type PrivateProfile @cacheControl(scope: PRIVATE) {
881
+ # ...
882
+ }
883
+
884
+ type Profile {
885
+ privateData: String @cacheControl(scope: PRIVATE)
886
+ }
887
+ `)
888
+
889
+ const getEnveloped = envelop({
890
+ parse,
891
+ validate,
892
+ execute,
893
+ subscribe,
894
+ plugins: [
895
+ // ... other plugins ...
896
+ useResponseCache({
897
+ ttl: 2000,
898
+ session: request => getSessionId(request),
899
+ buildResponseCacheKey: ({
900
+ sessionId,
901
+ documentString,
902
+ operationName,
903
+ variableValues,
904
+ extras
905
+ }) =>
906
+ hashSHA256(
907
+ [
908
+ // Use it to put a unique key for every session when `PUBLIC`
909
+ extras(schema).scope === 'PUBLIC' ? 'PUBLIC' : sessionId,
910
+ documentString,
911
+ operationName ?? '',
912
+ jsonStableStringify(variableValues ?? {})
913
+ ].join('|')
914
+ ),
915
+ scopePerSchemaCoordinate: {
916
+ // Set scope for an entire query
917
+ 'Query.getProfile': 'PRIVATE',
918
+ // Set scope for an entire type
919
+ PrivateProfile: 'PRIVATE',
920
+ // Set scope for a single field
921
+ 'Profile.privateData': 'PRIVATE'
922
+ }
923
+ })
924
+ ]
925
+ })
926
+ ```
927
+
928
+ > Note: The use of this callback will increase the ram usage since it memoizes the scope for each
929
+ > query in a weak map.
package/cjs/plugin.js CHANGED
@@ -7,6 +7,7 @@ exports.resultWithMetadata = resultWithMetadata;
7
7
  const tslib_1 = require("tslib");
8
8
  const fast_json_stable_stringify_1 = tslib_1.__importDefault(require("fast-json-stable-stringify"));
9
9
  const graphql_1 = require("graphql");
10
+ const lru_cache_1 = require("lru-cache");
10
11
  const core_1 = require("@envelop/core");
11
12
  const utils_1 = require("@graphql-tools/utils");
12
13
  const promise_helpers_1 = require("@whatwg-node/promise-helpers");
@@ -96,41 +97,62 @@ const getDocumentWithMetadataAndTTL = (0, utils_1.memoize4)(function addTypeName
96
97
  };
97
98
  return [(0, graphql_1.visit)(document, (0, graphql_1.visitWithTypeInfo)(typeInfo, visitor)), ttl];
98
99
  });
99
- function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache)(), ttl: globalTtl = Infinity, session, enabled, ignoredTypes = [], ttlPerType, ttlPerSchemaCoordinate = {}, scopePerSchemaCoordinate = {}, idFields = ['id'], invalidateViaMutation = true, buildResponseCacheKey = exports.defaultBuildResponseCacheKey, getDocumentString = defaultGetDocumentString, shouldCacheResult = exports.defaultShouldCacheResult, onTtl, includeExtensionMetadata = typeof process !== 'undefined'
100
+ const DOCUMENTS_SCOPE_MAX = 1000;
101
+ const DOCUMENTS_SCOPE_TTL = 3600000;
102
+ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache)(), ttl: globalTtl = Infinity, session, enabled, ignoredTypes = [], ttlPerType, idFields = ['id'], invalidateViaMutation = true, ignoreSessionIdForPublicScope = false, buildResponseCacheKey = exports.defaultBuildResponseCacheKey, getDocumentString = defaultGetDocumentString, shouldCacheResult = exports.defaultShouldCacheResult, onTtl, includeExtensionMetadata = typeof process !== 'undefined'
100
103
  ? // eslint-disable-next-line dot-notation
101
104
  process.env['NODE_ENV'] === 'development' || !!process.env['DEBUG']
102
- : false, }) {
105
+ : false, ...options }) {
103
106
  const cacheFactory = typeof cache === 'function' ? (0, utils_1.memoize1)(cache) : () => cache;
104
107
  const ignoredTypesMap = new Set(ignoredTypes);
105
- const typePerSchemaCoordinateMap = new Map();
106
108
  enabled = enabled ? (0, utils_1.memoize1)(enabled) : enabled;
107
- // never cache Introspections
108
- ttlPerSchemaCoordinate = { 'Query.__schema': 0, ...ttlPerSchemaCoordinate };
109
+ const configPerSchemaCoordinate = {
110
+ // never cache Introspections
111
+ ttl: { 'Query.__schema': 0, ...options.ttlPerSchemaCoordinate },
112
+ scope: { ...options.scopePerSchemaCoordinate },
113
+ };
109
114
  if (ttlPerType) {
110
115
  // eslint-disable-next-line no-console
111
116
  console.warn('[useResponseCache] `ttlForType` is deprecated. To migrate, merge it with `ttlForSchemaCoordinate` option');
112
117
  for (const [typeName, ttl] of Object.entries(ttlPerType)) {
113
- ttlPerSchemaCoordinate[typeName] = ttl;
118
+ configPerSchemaCoordinate.ttl[typeName] = ttl;
114
119
  }
115
120
  }
116
- const documentMetadataOptions = {
117
- queries: { invalidateViaMutation, ttlPerSchemaCoordinate },
118
- mutations: { invalidateViaMutation }, // remove ttlPerSchemaCoordinate for mutations to skip TTL calculation
121
+ const makeSchemaConfig = function makeSchemaConfig(schema) {
122
+ const ttl = { ...configPerSchemaCoordinate.ttl };
123
+ const scope = { ...configPerSchemaCoordinate.scope };
124
+ return {
125
+ schema,
126
+ perSchemaCoordinate: { ttl, scope, type: new Map() },
127
+ idFieldByTypeName: new Map(),
128
+ publicDocuments: new lru_cache_1.LRUCache({
129
+ max: DOCUMENTS_SCOPE_MAX,
130
+ ttl: DOCUMENTS_SCOPE_TTL,
131
+ }),
132
+ documentMetadataOptions: {
133
+ // Do not override mutations metadata to keep a stable reference for memoization
134
+ mutations: { invalidateViaMutation },
135
+ queries: { invalidateViaMutation, ttlPerSchemaCoordinate: ttl },
136
+ },
137
+ isPrivate(typeName, data) {
138
+ if (scope[typeName] === 'PRIVATE') {
139
+ return true;
140
+ }
141
+ return data
142
+ ? Object.keys(data).some(fieldName => scope[`${typeName}.${fieldName}`] === 'PRIVATE')
143
+ : false;
144
+ },
145
+ };
119
146
  };
120
- const idFieldByTypeName = new Map();
121
- let schema;
122
- function isPrivate(typeName, data) {
123
- if (scopePerSchemaCoordinate[typeName] === 'PRIVATE') {
124
- return true;
125
- }
126
- return Object.keys(data).some(fieldName => scopePerSchemaCoordinate[`${typeName}.${fieldName}`] === 'PRIVATE');
127
- }
147
+ const schemaConfigs = new WeakMap();
128
148
  return {
129
- onSchemaChange({ schema: newSchema }) {
130
- if (schema === newSchema) {
149
+ onSchemaChange({ schema }) {
150
+ if (schemaConfigs.has(schema)) {
131
151
  return;
132
152
  }
133
- schema = newSchema;
153
+ const config = makeSchemaConfig(schema);
154
+ schemaConfigs.set(schema, config);
155
+ // Reset all configs, to avoid keeping stale field configuration
134
156
  const directive = schema.getDirective('cacheControl');
135
157
  (0, utils_1.mapSchema)(schema, {
136
158
  ...(directive && {
@@ -138,10 +160,10 @@ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache
138
160
  const cacheControlAnnotations = (0, utils_1.getDirective)(schema, type, 'cacheControl');
139
161
  cacheControlAnnotations?.forEach(cacheControl => {
140
162
  if (cacheControl.maxAge != null) {
141
- ttlPerSchemaCoordinate[type.name] = cacheControl.maxAge * 1000;
163
+ config.perSchemaCoordinate.ttl[type.name] = cacheControl.maxAge * 1000;
142
164
  }
143
165
  if (cacheControl.scope) {
144
- scopePerSchemaCoordinate[type.name] = cacheControl.scope;
166
+ config.perSchemaCoordinate.scope[type.name] = cacheControl.scope;
145
167
  }
146
168
  });
147
169
  return type;
@@ -150,18 +172,18 @@ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache
150
172
  [utils_1.MapperKind.FIELD]: (fieldConfig, fieldName, typeName) => {
151
173
  const schemaCoordinates = `${typeName}.${fieldName}`;
152
174
  const resultTypeNames = unwrapTypenames(fieldConfig.type);
153
- typePerSchemaCoordinateMap.set(schemaCoordinates, resultTypeNames);
154
- if (idFields.includes(fieldName) && !idFieldByTypeName.has(typeName)) {
155
- idFieldByTypeName.set(typeName, fieldName);
175
+ config.perSchemaCoordinate.type.set(schemaCoordinates, resultTypeNames);
176
+ if (idFields.includes(fieldName) && !config.idFieldByTypeName.has(typeName)) {
177
+ config.idFieldByTypeName.set(typeName, fieldName);
156
178
  }
157
179
  if (directive) {
158
180
  const cacheControlAnnotations = (0, utils_1.getDirective)(schema, fieldConfig, 'cacheControl');
159
181
  cacheControlAnnotations?.forEach(cacheControl => {
160
182
  if (cacheControl.maxAge != null) {
161
- ttlPerSchemaCoordinate[schemaCoordinates] = cacheControl.maxAge * 1000;
183
+ config.perSchemaCoordinate.ttl[schemaCoordinates] = cacheControl.maxAge * 1000;
162
184
  }
163
185
  if (cacheControl.scope) {
164
- scopePerSchemaCoordinate[schemaCoordinates] = cacheControl.scope;
186
+ config.perSchemaCoordinate.scope[schemaCoordinates] = cacheControl.scope;
165
187
  }
166
188
  });
167
189
  }
@@ -173,11 +195,25 @@ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache
173
195
  if (enabled && !enabled(onExecuteParams.args.contextValue)) {
174
196
  return;
175
197
  }
198
+ const { schema } = onExecuteParams.args;
199
+ if (!schemaConfigs.has(schema)) {
200
+ // eslint-disable-next-line no-console
201
+ console.error('[response-cache] Unknown schema, operation ignored');
202
+ return;
203
+ }
204
+ const config = schemaConfigs.get(schema);
176
205
  const identifier = new Map();
177
206
  const types = new Set();
178
207
  let currentTtl;
208
+ let isPrivate = false;
179
209
  let skip = false;
180
- const sessionId = session(onExecuteParams.args.contextValue);
210
+ const documentString = getDocumentString(onExecuteParams.args);
211
+ // Verify if we already know this document is public or not. If it is public, we should not
212
+ // take the session ID into account. If not, we keep the default behavior of letting user
213
+ // decide if a session id should be used to build the key
214
+ const sessionId = ignoreSessionIdForPublicScope && config.publicDocuments.get(documentString)
215
+ ? undefined
216
+ : session(onExecuteParams.args.contextValue);
181
217
  function setExecutor({ execute, onExecuteDone, }) {
182
218
  let executed = false;
183
219
  onExecuteParams.setExecuteFn(args => {
@@ -198,21 +234,21 @@ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache
198
234
  if (skip) {
199
235
  return;
200
236
  }
201
- if (ignoredTypesMap.has(entity.typename) ||
202
- (!sessionId && isPrivate(entity.typename, data))) {
237
+ isPrivate ||= config.isPrivate(entity.typename, data);
238
+ if (ignoredTypesMap.has(entity.typename) || (!sessionId && isPrivate)) {
203
239
  skip = true;
204
240
  return;
205
241
  }
206
242
  // in case the entity has no id, we attempt to extract it from the data
207
243
  if (!entity.id) {
208
- const idField = idFieldByTypeName.get(entity.typename);
244
+ const idField = config.idFieldByTypeName.get(entity.typename);
209
245
  if (idField) {
210
246
  entity.id = data[idField];
211
247
  }
212
248
  }
213
249
  types.add(entity.typename);
214
- if (entity.typename in ttlPerSchemaCoordinate) {
215
- const maybeTtl = ttlPerSchemaCoordinate[entity.typename];
250
+ if (entity.typename in config.perSchemaCoordinate.ttl) {
251
+ const maybeTtl = config.perSchemaCoordinate.ttl[entity.typename];
216
252
  currentTtl = calculateTtl(maybeTtl, currentTtl);
217
253
  }
218
254
  if (entity.id != null) {
@@ -221,10 +257,10 @@ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache
221
257
  for (const fieldName in data) {
222
258
  const fieldData = data[fieldName];
223
259
  if (fieldData == null || (Array.isArray(fieldData) && fieldData.length === 0)) {
224
- const inferredTypes = typePerSchemaCoordinateMap.get(`${entity.typename}.${fieldName}`);
260
+ const inferredTypes = config.perSchemaCoordinate.type.get(`${entity.typename}.${fieldName}`);
225
261
  inferredTypes?.forEach(inferredType => {
226
- if (inferredType in ttlPerSchemaCoordinate) {
227
- const maybeTtl = ttlPerSchemaCoordinate[inferredType];
262
+ if (inferredType in config.perSchemaCoordinate.ttl) {
263
+ const maybeTtl = config.perSchemaCoordinate.ttl[inferredType];
228
264
  currentTtl = calculateTtl(maybeTtl, currentTtl);
229
265
  }
230
266
  identifier.set(inferredType, { typename: inferredType });
@@ -233,9 +269,11 @@ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache
233
269
  }
234
270
  }
235
271
  function invalidateCache(result, setResult) {
272
+ let changed = false;
236
273
  if (result.data) {
237
- collectEntities(result.data, onEntity);
238
- setStringifyWithoutMetadata(result);
274
+ result = { ...result };
275
+ result.data = removeMetadataFieldsFromResult(result.data, onEntity);
276
+ changed = true;
239
277
  }
240
278
  const cacheInstance = cacheFactory(onExecuteParams.args.contextValue);
241
279
  if (cacheInstance == null) {
@@ -251,13 +289,16 @@ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache
251
289
  }));
252
290
  }
253
291
  }
292
+ if (changed) {
293
+ setResult(result);
294
+ }
254
295
  }
255
296
  if (invalidateViaMutation !== false) {
256
297
  const operationAST = (0, graphql_1.getOperationAST)(onExecuteParams.args.document, onExecuteParams.args.operationName);
257
298
  if (operationAST?.operation === 'mutation') {
258
299
  return setExecutor({
259
300
  execute(args) {
260
- const [document] = getDocumentWithMetadataAndTTL(args.document, documentMetadataOptions.mutations, args.schema, idFieldByTypeName);
301
+ const [document] = getDocumentWithMetadataAndTTL(args.document, config.documentMetadataOptions.mutations, args.schema, config.idFieldByTypeName);
261
302
  return onExecuteParams.executeFn({ ...args, document });
262
303
  },
263
304
  onExecuteDone({ result, setResult }) {
@@ -270,30 +311,36 @@ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache
270
311
  }
271
312
  }
272
313
  return (0, promise_helpers_1.handleMaybePromise)(() => buildResponseCacheKey({
273
- documentString: getDocumentString(onExecuteParams.args),
314
+ sessionId,
315
+ documentString,
274
316
  variableValues: onExecuteParams.args.variableValues,
275
317
  operationName: onExecuteParams.args.operationName,
276
- sessionId,
277
318
  context: onExecuteParams.args.contextValue,
278
319
  }), cacheKey => {
279
320
  const cacheInstance = cacheFactory(onExecuteParams.args.contextValue);
280
321
  if (cacheInstance == null) {
281
322
  // eslint-disable-next-line no-console
282
323
  console.warn('[useResponseCache] Cache instance is not available for the context. Skipping cache lookup.');
324
+ return;
283
325
  }
284
- return (0, promise_helpers_1.handleMaybePromise)(() => cacheInstance.get(cacheKey), cachedResponse => {
285
- if (cachedResponse != null) {
286
- return setExecutor({
287
- execute: () => includeExtensionMetadata
288
- ? resultWithMetadata(cachedResponse, { hit: true })
289
- : cachedResponse,
290
- });
326
+ function maybeCacheResult(result, setResult) {
327
+ if (result.data) {
328
+ result.data = removeMetadataFieldsFromResult(result.data, onEntity);
291
329
  }
292
- function maybeCacheResult(result, setResult) {
293
- if (result.data) {
294
- collectEntities(result.data, onEntity);
295
- setStringifyWithoutMetadata(result);
330
+ return (0, promise_helpers_1.handleMaybePromise)(() => {
331
+ if (!skip && ignoreSessionIdForPublicScope && !isPrivate && sessionId) {
332
+ config.publicDocuments.set(documentString, true);
333
+ return buildResponseCacheKey({
334
+ // Build a public key for this document
335
+ sessionId: undefined,
336
+ documentString,
337
+ variableValues: onExecuteParams.args.variableValues,
338
+ operationName: onExecuteParams.args.operationName,
339
+ context: onExecuteParams.args.contextValue,
340
+ });
296
341
  }
342
+ return cacheKey;
343
+ }, cacheKey => {
297
344
  // we only use the global ttl if no currentTtl has been determined.
298
345
  let finalTtl = currentTtl ?? globalTtl;
299
346
  if (onTtl) {
@@ -312,10 +359,19 @@ function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache
312
359
  if (includeExtensionMetadata) {
313
360
  setResult(resultWithMetadata(result, { hit: false, didCache: true, ttl: finalTtl }));
314
361
  }
362
+ });
363
+ }
364
+ return (0, promise_helpers_1.handleMaybePromise)(() => cacheInstance.get(cacheKey), cachedResponse => {
365
+ if (cachedResponse != null) {
366
+ return setExecutor({
367
+ execute: () => includeExtensionMetadata
368
+ ? resultWithMetadata(cachedResponse, { hit: true })
369
+ : cachedResponse,
370
+ });
315
371
  }
316
372
  return setExecutor({
317
373
  execute(args) {
318
- const [document, ttl] = getDocumentWithMetadataAndTTL(args.document, documentMetadataOptions.queries, schema, idFieldByTypeName);
374
+ const [document, ttl] = getDocumentWithMetadataAndTTL(args.document, config.documentMetadataOptions.queries, schema, config.idFieldByTypeName);
319
375
  currentTtl = ttl;
320
376
  return onExecuteParams.executeFn({ ...args, document });
321
377
  },
@@ -340,7 +396,7 @@ function handleAsyncIterableResult(handler) {
340
396
  onNext(payload) {
341
397
  // This is the first result with the initial data payload sent to the client. We use it as the base result
342
398
  if (payload.result.data) {
343
- result.data = structuredClone(payload.result.data);
399
+ result.data = payload.result.data;
344
400
  }
345
401
  if (payload.result.errors) {
346
402
  result.errors = payload.result.errors;
@@ -352,10 +408,7 @@ function handleAsyncIterableResult(handler) {
352
408
  const { incremental, hasNext } = payload.result;
353
409
  if (incremental) {
354
410
  for (const patch of incremental) {
355
- (0, utils_1.mergeIncrementalResult)({
356
- executionResult: result,
357
- incrementalResult: structuredClone(patch),
358
- });
411
+ (0, utils_1.mergeIncrementalResult)({ executionResult: result, incrementalResult: patch });
359
412
  }
360
413
  }
361
414
  if (!hasNext) {
@@ -363,22 +416,30 @@ function handleAsyncIterableResult(handler) {
363
416
  handler(result, payload.setResult);
364
417
  }
365
418
  }
366
- if (payload.result.data) {
367
- collectEntities(payload.result.data);
419
+ const newResult = { ...payload.result };
420
+ // Handle initial/single result
421
+ if (newResult.data) {
422
+ newResult.data = removeMetadataFieldsFromResult(newResult.data);
368
423
  }
369
424
  // Handle Incremental results
370
- if ('hasNext' in payload.result && payload.result.incremental) {
371
- payload.result.incremental = payload.result.incremental.map(value => {
425
+ if ('hasNext' in newResult && newResult.incremental) {
426
+ newResult.incremental = newResult.incremental.map(value => {
372
427
  if ('items' in value && value.items) {
373
- collectEntities(value.items);
428
+ return {
429
+ ...value,
430
+ items: removeMetadataFieldsFromResult(value.items),
431
+ };
374
432
  }
375
433
  if ('data' in value && value.data) {
376
- collectEntities(value.data);
434
+ return {
435
+ ...value,
436
+ data: removeMetadataFieldsFromResult(value.data),
437
+ };
377
438
  }
378
439
  return value;
379
440
  });
380
441
  }
381
- setStringifyWithoutMetadata(payload.result);
442
+ payload.setResult(newResult);
382
443
  },
383
444
  };
384
445
  }
@@ -423,45 +484,38 @@ exports.cacheControlDirective = `
423
484
 
424
485
  directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT
425
486
  `;
426
- function collectEntities(data, onEntity) {
487
+ function removeMetadataFieldsFromResult(data, onEntity) {
427
488
  if (Array.isArray(data)) {
428
- for (const record of data) {
429
- collectEntities(record, onEntity);
430
- }
431
- return;
489
+ return data.map(record => removeMetadataFieldsFromResult(record, onEntity));
432
490
  }
433
491
  if (typeof data !== 'object' || data == null) {
434
- return;
492
+ return data;
435
493
  }
436
494
  const dataPrototype = Object.getPrototypeOf(data);
437
- // TODO: For some reason, when running in Jest, `structuredClone` result have a weird prototype
438
- // that doesn't equal Object.prototype as it should.
439
- // As a workaround, we can just check that there is no parent prototype, which should be
440
- // the case only when it's the Object prototype
441
- // Rollback once migrated to Bun or vitest
442
- //
443
- // if (dataPrototype != null && dataPrototype !== Object.prototype) {
444
- if (dataPrototype != null && Object.getPrototypeOf(dataPrototype) !== null) {
445
- // It is not a plain object, like a Date, don't inspect further
446
- return;
495
+ if (dataPrototype != null && dataPrototype !== Object.prototype) {
496
+ return data;
447
497
  }
498
+ // clone the data to avoid mutation
499
+ data = { ...data };
448
500
  const typename = data.__responseCacheTypeName ?? data.__typename;
449
501
  if (typeof typename === 'string') {
450
502
  const entity = { typename };
503
+ delete data.__responseCacheTypeName;
451
504
  if (data.__responseCacheId &&
452
505
  (typeof data.__responseCacheId === 'string' || typeof data.__responseCacheId === 'number')) {
453
506
  entity.id = data.__responseCacheId;
507
+ delete data.__responseCacheId;
454
508
  }
455
509
  onEntity?.(entity, data);
456
510
  }
457
511
  for (const key in data) {
458
- collectEntities(data[key], onEntity);
512
+ const value = data[key];
513
+ if (Array.isArray(value)) {
514
+ data[key] = removeMetadataFieldsFromResult(value, onEntity);
515
+ }
516
+ if (value !== null && typeof value === 'object') {
517
+ data[key] = removeMetadataFieldsFromResult(value, onEntity);
518
+ }
459
519
  }
520
+ return data;
460
521
  }
461
- function setStringifyWithoutMetadata(result) {
462
- result.stringify = stringifyWithoutMetadata;
463
- return result;
464
- }
465
- const stringifyWithoutMetadata = result => {
466
- return JSON.stringify(result, (key, value) => key === '__responseCacheId' || key === '__responseCacheTypeName' ? undefined : value);
467
- };
package/esm/plugin.js CHANGED
@@ -1,5 +1,6 @@
1
- import jsonStableStringify from 'fast-json-stable-stringify';
1
+ import stringify from 'fast-json-stable-stringify';
2
2
  import { getOperationAST, isListType, isNonNullType, isUnionType, Kind, print, TypeInfo, visit, visitWithTypeInfo, } from 'graphql';
3
+ import { LRUCache } from 'lru-cache';
3
4
  import { getDocumentString, isAsyncIterable, } from '@envelop/core';
4
5
  import { getDirective, MapperKind, mapSchema, memoize1, memoize4, mergeIncrementalResult, } from '@graphql-tools/utils';
5
6
  import { handleMaybePromise } from '@whatwg-node/promise-helpers';
@@ -12,7 +13,7 @@ import { createInMemoryCache } from './in-memory-cache.js';
12
13
  export const defaultBuildResponseCacheKey = (params) => hashSHA256([
13
14
  params.documentString,
14
15
  params.operationName ?? '',
15
- jsonStableStringify(params.variableValues ?? {}),
16
+ stringify(params.variableValues ?? {}),
16
17
  params.sessionId ?? '',
17
18
  ].join('|'));
18
19
  /**
@@ -87,41 +88,62 @@ const getDocumentWithMetadataAndTTL = memoize4(function addTypeNameToDocument(do
87
88
  };
88
89
  return [visit(document, visitWithTypeInfo(typeInfo, visitor)), ttl];
89
90
  });
90
- export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl = Infinity, session, enabled, ignoredTypes = [], ttlPerType, ttlPerSchemaCoordinate = {}, scopePerSchemaCoordinate = {}, idFields = ['id'], invalidateViaMutation = true, buildResponseCacheKey = defaultBuildResponseCacheKey, getDocumentString = defaultGetDocumentString, shouldCacheResult = defaultShouldCacheResult, onTtl, includeExtensionMetadata = typeof process !== 'undefined'
91
+ const DOCUMENTS_SCOPE_MAX = 1000;
92
+ const DOCUMENTS_SCOPE_TTL = 3600000;
93
+ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl = Infinity, session, enabled, ignoredTypes = [], ttlPerType, idFields = ['id'], invalidateViaMutation = true, ignoreSessionIdForPublicScope = false, buildResponseCacheKey = defaultBuildResponseCacheKey, getDocumentString = defaultGetDocumentString, shouldCacheResult = defaultShouldCacheResult, onTtl, includeExtensionMetadata = typeof process !== 'undefined'
91
94
  ? // eslint-disable-next-line dot-notation
92
95
  process.env['NODE_ENV'] === 'development' || !!process.env['DEBUG']
93
- : false, }) {
96
+ : false, ...options }) {
94
97
  const cacheFactory = typeof cache === 'function' ? memoize1(cache) : () => cache;
95
98
  const ignoredTypesMap = new Set(ignoredTypes);
96
- const typePerSchemaCoordinateMap = new Map();
97
99
  enabled = enabled ? memoize1(enabled) : enabled;
98
- // never cache Introspections
99
- ttlPerSchemaCoordinate = { 'Query.__schema': 0, ...ttlPerSchemaCoordinate };
100
+ const configPerSchemaCoordinate = {
101
+ // never cache Introspections
102
+ ttl: { 'Query.__schema': 0, ...options.ttlPerSchemaCoordinate },
103
+ scope: { ...options.scopePerSchemaCoordinate },
104
+ };
100
105
  if (ttlPerType) {
101
106
  // eslint-disable-next-line no-console
102
107
  console.warn('[useResponseCache] `ttlForType` is deprecated. To migrate, merge it with `ttlForSchemaCoordinate` option');
103
108
  for (const [typeName, ttl] of Object.entries(ttlPerType)) {
104
- ttlPerSchemaCoordinate[typeName] = ttl;
109
+ configPerSchemaCoordinate.ttl[typeName] = ttl;
105
110
  }
106
111
  }
107
- const documentMetadataOptions = {
108
- queries: { invalidateViaMutation, ttlPerSchemaCoordinate },
109
- mutations: { invalidateViaMutation }, // remove ttlPerSchemaCoordinate for mutations to skip TTL calculation
112
+ const makeSchemaConfig = function makeSchemaConfig(schema) {
113
+ const ttl = { ...configPerSchemaCoordinate.ttl };
114
+ const scope = { ...configPerSchemaCoordinate.scope };
115
+ return {
116
+ schema,
117
+ perSchemaCoordinate: { ttl, scope, type: new Map() },
118
+ idFieldByTypeName: new Map(),
119
+ publicDocuments: new LRUCache({
120
+ max: DOCUMENTS_SCOPE_MAX,
121
+ ttl: DOCUMENTS_SCOPE_TTL,
122
+ }),
123
+ documentMetadataOptions: {
124
+ // Do not override mutations metadata to keep a stable reference for memoization
125
+ mutations: { invalidateViaMutation },
126
+ queries: { invalidateViaMutation, ttlPerSchemaCoordinate: ttl },
127
+ },
128
+ isPrivate(typeName, data) {
129
+ if (scope[typeName] === 'PRIVATE') {
130
+ return true;
131
+ }
132
+ return data
133
+ ? Object.keys(data).some(fieldName => scope[`${typeName}.${fieldName}`] === 'PRIVATE')
134
+ : false;
135
+ },
136
+ };
110
137
  };
111
- const idFieldByTypeName = new Map();
112
- let schema;
113
- function isPrivate(typeName, data) {
114
- if (scopePerSchemaCoordinate[typeName] === 'PRIVATE') {
115
- return true;
116
- }
117
- return Object.keys(data).some(fieldName => scopePerSchemaCoordinate[`${typeName}.${fieldName}`] === 'PRIVATE');
118
- }
138
+ const schemaConfigs = new WeakMap();
119
139
  return {
120
- onSchemaChange({ schema: newSchema }) {
121
- if (schema === newSchema) {
140
+ onSchemaChange({ schema }) {
141
+ if (schemaConfigs.has(schema)) {
122
142
  return;
123
143
  }
124
- schema = newSchema;
144
+ const config = makeSchemaConfig(schema);
145
+ schemaConfigs.set(schema, config);
146
+ // Reset all configs, to avoid keeping stale field configuration
125
147
  const directive = schema.getDirective('cacheControl');
126
148
  mapSchema(schema, {
127
149
  ...(directive && {
@@ -129,10 +151,10 @@ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl
129
151
  const cacheControlAnnotations = getDirective(schema, type, 'cacheControl');
130
152
  cacheControlAnnotations?.forEach(cacheControl => {
131
153
  if (cacheControl.maxAge != null) {
132
- ttlPerSchemaCoordinate[type.name] = cacheControl.maxAge * 1000;
154
+ config.perSchemaCoordinate.ttl[type.name] = cacheControl.maxAge * 1000;
133
155
  }
134
156
  if (cacheControl.scope) {
135
- scopePerSchemaCoordinate[type.name] = cacheControl.scope;
157
+ config.perSchemaCoordinate.scope[type.name] = cacheControl.scope;
136
158
  }
137
159
  });
138
160
  return type;
@@ -141,18 +163,18 @@ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl
141
163
  [MapperKind.FIELD]: (fieldConfig, fieldName, typeName) => {
142
164
  const schemaCoordinates = `${typeName}.${fieldName}`;
143
165
  const resultTypeNames = unwrapTypenames(fieldConfig.type);
144
- typePerSchemaCoordinateMap.set(schemaCoordinates, resultTypeNames);
145
- if (idFields.includes(fieldName) && !idFieldByTypeName.has(typeName)) {
146
- idFieldByTypeName.set(typeName, fieldName);
166
+ config.perSchemaCoordinate.type.set(schemaCoordinates, resultTypeNames);
167
+ if (idFields.includes(fieldName) && !config.idFieldByTypeName.has(typeName)) {
168
+ config.idFieldByTypeName.set(typeName, fieldName);
147
169
  }
148
170
  if (directive) {
149
171
  const cacheControlAnnotations = getDirective(schema, fieldConfig, 'cacheControl');
150
172
  cacheControlAnnotations?.forEach(cacheControl => {
151
173
  if (cacheControl.maxAge != null) {
152
- ttlPerSchemaCoordinate[schemaCoordinates] = cacheControl.maxAge * 1000;
174
+ config.perSchemaCoordinate.ttl[schemaCoordinates] = cacheControl.maxAge * 1000;
153
175
  }
154
176
  if (cacheControl.scope) {
155
- scopePerSchemaCoordinate[schemaCoordinates] = cacheControl.scope;
177
+ config.perSchemaCoordinate.scope[schemaCoordinates] = cacheControl.scope;
156
178
  }
157
179
  });
158
180
  }
@@ -164,11 +186,25 @@ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl
164
186
  if (enabled && !enabled(onExecuteParams.args.contextValue)) {
165
187
  return;
166
188
  }
189
+ const { schema } = onExecuteParams.args;
190
+ if (!schemaConfigs.has(schema)) {
191
+ // eslint-disable-next-line no-console
192
+ console.error('[response-cache] Unknown schema, operation ignored');
193
+ return;
194
+ }
195
+ const config = schemaConfigs.get(schema);
167
196
  const identifier = new Map();
168
197
  const types = new Set();
169
198
  let currentTtl;
199
+ let isPrivate = false;
170
200
  let skip = false;
171
- const sessionId = session(onExecuteParams.args.contextValue);
201
+ const documentString = getDocumentString(onExecuteParams.args);
202
+ // Verify if we already know this document is public or not. If it is public, we should not
203
+ // take the session ID into account. If not, we keep the default behavior of letting user
204
+ // decide if a session id should be used to build the key
205
+ const sessionId = ignoreSessionIdForPublicScope && config.publicDocuments.get(documentString)
206
+ ? undefined
207
+ : session(onExecuteParams.args.contextValue);
172
208
  function setExecutor({ execute, onExecuteDone, }) {
173
209
  let executed = false;
174
210
  onExecuteParams.setExecuteFn(args => {
@@ -189,21 +225,21 @@ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl
189
225
  if (skip) {
190
226
  return;
191
227
  }
192
- if (ignoredTypesMap.has(entity.typename) ||
193
- (!sessionId && isPrivate(entity.typename, data))) {
228
+ isPrivate ||= config.isPrivate(entity.typename, data);
229
+ if (ignoredTypesMap.has(entity.typename) || (!sessionId && isPrivate)) {
194
230
  skip = true;
195
231
  return;
196
232
  }
197
233
  // in case the entity has no id, we attempt to extract it from the data
198
234
  if (!entity.id) {
199
- const idField = idFieldByTypeName.get(entity.typename);
235
+ const idField = config.idFieldByTypeName.get(entity.typename);
200
236
  if (idField) {
201
237
  entity.id = data[idField];
202
238
  }
203
239
  }
204
240
  types.add(entity.typename);
205
- if (entity.typename in ttlPerSchemaCoordinate) {
206
- const maybeTtl = ttlPerSchemaCoordinate[entity.typename];
241
+ if (entity.typename in config.perSchemaCoordinate.ttl) {
242
+ const maybeTtl = config.perSchemaCoordinate.ttl[entity.typename];
207
243
  currentTtl = calculateTtl(maybeTtl, currentTtl);
208
244
  }
209
245
  if (entity.id != null) {
@@ -212,10 +248,10 @@ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl
212
248
  for (const fieldName in data) {
213
249
  const fieldData = data[fieldName];
214
250
  if (fieldData == null || (Array.isArray(fieldData) && fieldData.length === 0)) {
215
- const inferredTypes = typePerSchemaCoordinateMap.get(`${entity.typename}.${fieldName}`);
251
+ const inferredTypes = config.perSchemaCoordinate.type.get(`${entity.typename}.${fieldName}`);
216
252
  inferredTypes?.forEach(inferredType => {
217
- if (inferredType in ttlPerSchemaCoordinate) {
218
- const maybeTtl = ttlPerSchemaCoordinate[inferredType];
253
+ if (inferredType in config.perSchemaCoordinate.ttl) {
254
+ const maybeTtl = config.perSchemaCoordinate.ttl[inferredType];
219
255
  currentTtl = calculateTtl(maybeTtl, currentTtl);
220
256
  }
221
257
  identifier.set(inferredType, { typename: inferredType });
@@ -224,9 +260,11 @@ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl
224
260
  }
225
261
  }
226
262
  function invalidateCache(result, setResult) {
263
+ let changed = false;
227
264
  if (result.data) {
228
- collectEntities(result.data, onEntity);
229
- setStringifyWithoutMetadata(result);
265
+ result = { ...result };
266
+ result.data = removeMetadataFieldsFromResult(result.data, onEntity);
267
+ changed = true;
230
268
  }
231
269
  const cacheInstance = cacheFactory(onExecuteParams.args.contextValue);
232
270
  if (cacheInstance == null) {
@@ -242,13 +280,16 @@ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl
242
280
  }));
243
281
  }
244
282
  }
283
+ if (changed) {
284
+ setResult(result);
285
+ }
245
286
  }
246
287
  if (invalidateViaMutation !== false) {
247
288
  const operationAST = getOperationAST(onExecuteParams.args.document, onExecuteParams.args.operationName);
248
289
  if (operationAST?.operation === 'mutation') {
249
290
  return setExecutor({
250
291
  execute(args) {
251
- const [document] = getDocumentWithMetadataAndTTL(args.document, documentMetadataOptions.mutations, args.schema, idFieldByTypeName);
292
+ const [document] = getDocumentWithMetadataAndTTL(args.document, config.documentMetadataOptions.mutations, args.schema, config.idFieldByTypeName);
252
293
  return onExecuteParams.executeFn({ ...args, document });
253
294
  },
254
295
  onExecuteDone({ result, setResult }) {
@@ -261,30 +302,36 @@ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl
261
302
  }
262
303
  }
263
304
  return handleMaybePromise(() => buildResponseCacheKey({
264
- documentString: getDocumentString(onExecuteParams.args),
305
+ sessionId,
306
+ documentString,
265
307
  variableValues: onExecuteParams.args.variableValues,
266
308
  operationName: onExecuteParams.args.operationName,
267
- sessionId,
268
309
  context: onExecuteParams.args.contextValue,
269
310
  }), cacheKey => {
270
311
  const cacheInstance = cacheFactory(onExecuteParams.args.contextValue);
271
312
  if (cacheInstance == null) {
272
313
  // eslint-disable-next-line no-console
273
314
  console.warn('[useResponseCache] Cache instance is not available for the context. Skipping cache lookup.');
315
+ return;
274
316
  }
275
- return handleMaybePromise(() => cacheInstance.get(cacheKey), cachedResponse => {
276
- if (cachedResponse != null) {
277
- return setExecutor({
278
- execute: () => includeExtensionMetadata
279
- ? resultWithMetadata(cachedResponse, { hit: true })
280
- : cachedResponse,
281
- });
317
+ function maybeCacheResult(result, setResult) {
318
+ if (result.data) {
319
+ result.data = removeMetadataFieldsFromResult(result.data, onEntity);
282
320
  }
283
- function maybeCacheResult(result, setResult) {
284
- if (result.data) {
285
- collectEntities(result.data, onEntity);
286
- setStringifyWithoutMetadata(result);
321
+ return handleMaybePromise(() => {
322
+ if (!skip && ignoreSessionIdForPublicScope && !isPrivate && sessionId) {
323
+ config.publicDocuments.set(documentString, true);
324
+ return buildResponseCacheKey({
325
+ // Build a public key for this document
326
+ sessionId: undefined,
327
+ documentString,
328
+ variableValues: onExecuteParams.args.variableValues,
329
+ operationName: onExecuteParams.args.operationName,
330
+ context: onExecuteParams.args.contextValue,
331
+ });
287
332
  }
333
+ return cacheKey;
334
+ }, cacheKey => {
288
335
  // we only use the global ttl if no currentTtl has been determined.
289
336
  let finalTtl = currentTtl ?? globalTtl;
290
337
  if (onTtl) {
@@ -303,10 +350,19 @@ export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl
303
350
  if (includeExtensionMetadata) {
304
351
  setResult(resultWithMetadata(result, { hit: false, didCache: true, ttl: finalTtl }));
305
352
  }
353
+ });
354
+ }
355
+ return handleMaybePromise(() => cacheInstance.get(cacheKey), cachedResponse => {
356
+ if (cachedResponse != null) {
357
+ return setExecutor({
358
+ execute: () => includeExtensionMetadata
359
+ ? resultWithMetadata(cachedResponse, { hit: true })
360
+ : cachedResponse,
361
+ });
306
362
  }
307
363
  return setExecutor({
308
364
  execute(args) {
309
- const [document, ttl] = getDocumentWithMetadataAndTTL(args.document, documentMetadataOptions.queries, schema, idFieldByTypeName);
365
+ const [document, ttl] = getDocumentWithMetadataAndTTL(args.document, config.documentMetadataOptions.queries, schema, config.idFieldByTypeName);
310
366
  currentTtl = ttl;
311
367
  return onExecuteParams.executeFn({ ...args, document });
312
368
  },
@@ -331,7 +387,7 @@ function handleAsyncIterableResult(handler) {
331
387
  onNext(payload) {
332
388
  // This is the first result with the initial data payload sent to the client. We use it as the base result
333
389
  if (payload.result.data) {
334
- result.data = structuredClone(payload.result.data);
390
+ result.data = payload.result.data;
335
391
  }
336
392
  if (payload.result.errors) {
337
393
  result.errors = payload.result.errors;
@@ -343,10 +399,7 @@ function handleAsyncIterableResult(handler) {
343
399
  const { incremental, hasNext } = payload.result;
344
400
  if (incremental) {
345
401
  for (const patch of incremental) {
346
- mergeIncrementalResult({
347
- executionResult: result,
348
- incrementalResult: structuredClone(patch),
349
- });
402
+ mergeIncrementalResult({ executionResult: result, incrementalResult: patch });
350
403
  }
351
404
  }
352
405
  if (!hasNext) {
@@ -354,22 +407,30 @@ function handleAsyncIterableResult(handler) {
354
407
  handler(result, payload.setResult);
355
408
  }
356
409
  }
357
- if (payload.result.data) {
358
- collectEntities(payload.result.data);
410
+ const newResult = { ...payload.result };
411
+ // Handle initial/single result
412
+ if (newResult.data) {
413
+ newResult.data = removeMetadataFieldsFromResult(newResult.data);
359
414
  }
360
415
  // Handle Incremental results
361
- if ('hasNext' in payload.result && payload.result.incremental) {
362
- payload.result.incremental = payload.result.incremental.map(value => {
416
+ if ('hasNext' in newResult && newResult.incremental) {
417
+ newResult.incremental = newResult.incremental.map(value => {
363
418
  if ('items' in value && value.items) {
364
- collectEntities(value.items);
419
+ return {
420
+ ...value,
421
+ items: removeMetadataFieldsFromResult(value.items),
422
+ };
365
423
  }
366
424
  if ('data' in value && value.data) {
367
- collectEntities(value.data);
425
+ return {
426
+ ...value,
427
+ data: removeMetadataFieldsFromResult(value.data),
428
+ };
368
429
  }
369
430
  return value;
370
431
  });
371
432
  }
372
- setStringifyWithoutMetadata(payload.result);
433
+ payload.setResult(newResult);
373
434
  },
374
435
  };
375
436
  }
@@ -414,45 +475,38 @@ export const cacheControlDirective = /* GraphQL */ `
414
475
 
415
476
  directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT
416
477
  `;
417
- function collectEntities(data, onEntity) {
478
+ function removeMetadataFieldsFromResult(data, onEntity) {
418
479
  if (Array.isArray(data)) {
419
- for (const record of data) {
420
- collectEntities(record, onEntity);
421
- }
422
- return;
480
+ return data.map(record => removeMetadataFieldsFromResult(record, onEntity));
423
481
  }
424
482
  if (typeof data !== 'object' || data == null) {
425
- return;
483
+ return data;
426
484
  }
427
485
  const dataPrototype = Object.getPrototypeOf(data);
428
- // TODO: For some reason, when running in Jest, `structuredClone` result have a weird prototype
429
- // that doesn't equal Object.prototype as it should.
430
- // As a workaround, we can just check that there is no parent prototype, which should be
431
- // the case only when it's the Object prototype
432
- // Rollback once migrated to Bun or vitest
433
- //
434
- // if (dataPrototype != null && dataPrototype !== Object.prototype) {
435
- if (dataPrototype != null && Object.getPrototypeOf(dataPrototype) !== null) {
436
- // It is not a plain object, like a Date, don't inspect further
437
- return;
486
+ if (dataPrototype != null && dataPrototype !== Object.prototype) {
487
+ return data;
438
488
  }
489
+ // clone the data to avoid mutation
490
+ data = { ...data };
439
491
  const typename = data.__responseCacheTypeName ?? data.__typename;
440
492
  if (typeof typename === 'string') {
441
493
  const entity = { typename };
494
+ delete data.__responseCacheTypeName;
442
495
  if (data.__responseCacheId &&
443
496
  (typeof data.__responseCacheId === 'string' || typeof data.__responseCacheId === 'number')) {
444
497
  entity.id = data.__responseCacheId;
498
+ delete data.__responseCacheId;
445
499
  }
446
500
  onEntity?.(entity, data);
447
501
  }
448
502
  for (const key in data) {
449
- collectEntities(data[key], onEntity);
503
+ const value = data[key];
504
+ if (Array.isArray(value)) {
505
+ data[key] = removeMetadataFieldsFromResult(value, onEntity);
506
+ }
507
+ if (value !== null && typeof value === 'object') {
508
+ data[key] = removeMetadataFieldsFromResult(value, onEntity);
509
+ }
450
510
  }
511
+ return data;
451
512
  }
452
- function setStringifyWithoutMetadata(result) {
453
- result.stringify = stringifyWithoutMetadata;
454
- return result;
455
- }
456
- const stringifyWithoutMetadata = result => {
457
- return JSON.stringify(result, (key, value) => key === '__responseCacheId' || key === '__responseCacheTypeName' ? undefined : value);
458
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envelop/response-cache",
3
- "version": "8.2.0-alpha-20251013081815-d6f74fceb1a32fd336b2664f90f87872e1bbf2fe",
3
+ "version": "8.2.0-alpha-20251217212728-de569f3d7fe45e330eca177b69ee1c89fd5dc498",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
6
  "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
@@ -22,6 +22,8 @@ export type ShouldCacheResultFunction = (params: {
22
22
  cacheKey: string;
23
23
  result: ExecutionResult;
24
24
  }) => boolean;
25
+ export type TTLPerSchemaCoordinate = Record<string, number | undefined>;
26
+ export type ScopePerSchemaCoordinate = Record<string, 'PRIVATE' | 'PUBLIC' | undefined>;
25
27
  export type UseResponseCacheParameter<PluginContext extends Record<string, any> = {}> = {
26
28
  cache?: Cache | ((ctx: Record<string, any>) => Cache);
27
29
  /**
@@ -40,8 +42,25 @@ export type UseResponseCacheParameter<PluginContext extends Record<string, any>
40
42
  * In the unusual case where you actually want to cache introspection query operations,
41
43
  * you need to provide the value `{ 'Query.__schema': undefined }`.
42
44
  */
43
- ttlPerSchemaCoordinate?: Record<string, number | undefined>;
44
- scopePerSchemaCoordinate?: Record<string, 'PRIVATE' | 'PUBLIC' | undefined>;
45
+ ttlPerSchemaCoordinate?: TTLPerSchemaCoordinate;
46
+ /**
47
+ * Define the scope (PUBLIC or PRIVATE) by schema coordinate.
48
+ * The default scope for all types and fields is PUBLIC
49
+ *
50
+ * If an operation contains a PRIVATE type or field, the result will be cached only if a session
51
+ * id is found for this request.
52
+ *
53
+ * Note: To share cache of responses with a PUBLIC scope between all users, enable `ignoreSessionIdForPublicScope`
54
+ */
55
+ scopePerSchemaCoordinate?: ScopePerSchemaCoordinate;
56
+ /**
57
+ * If enabled, a response with a PUBLIC scope will be cached with an operation key ignoring the
58
+ * session ID. This allows to improve cache hit further, but scope should be carefully defined
59
+ * to avoid any private data.
60
+ *
61
+ * @default false.
62
+ */
63
+ ignoreSessionIdForPublicScope?: boolean;
45
64
  /**
46
65
  * Allows to cache responses based on the resolved session id.
47
66
  * Return a unique value for each session.
@@ -151,6 +170,10 @@ export type ResponseCacheExtensions = {
151
170
  export type ResponseCacheExecutionResult = ExecutionResult<ObjMap<unknown>, {
152
171
  responseCache?: ResponseCacheExtensions;
153
172
  }>;
154
- export declare function useResponseCache<PluginContext extends Record<string, any> = {}>({ cache, ttl: globalTtl, session, enabled, ignoredTypes, ttlPerType, ttlPerSchemaCoordinate, scopePerSchemaCoordinate, idFields, invalidateViaMutation, buildResponseCacheKey, getDocumentString, shouldCacheResult, onTtl, includeExtensionMetadata, }: UseResponseCacheParameter<PluginContext>): Plugin<PluginContext>;
173
+ export type CacheControlDirective = {
174
+ maxAge?: number;
175
+ scope?: 'PUBLIC' | 'PRIVATE';
176
+ };
177
+ export declare function useResponseCache<PluginContext extends Record<string, any> = {}>({ cache, ttl: globalTtl, session, enabled, ignoredTypes, ttlPerType, idFields, invalidateViaMutation, ignoreSessionIdForPublicScope, buildResponseCacheKey, getDocumentString, shouldCacheResult, onTtl, includeExtensionMetadata, ...options }: UseResponseCacheParameter<PluginContext>): Plugin<PluginContext>;
155
178
  export declare function resultWithMetadata(result: ExecutionResult, metadata: ResponseCacheExtensions): ResponseCacheExecutionResult;
156
179
  export declare const cacheControlDirective = "\n enum CacheControlScope {\n PUBLIC\n PRIVATE\n }\n\n directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT\n";
@@ -22,6 +22,8 @@ export type ShouldCacheResultFunction = (params: {
22
22
  cacheKey: string;
23
23
  result: ExecutionResult;
24
24
  }) => boolean;
25
+ export type TTLPerSchemaCoordinate = Record<string, number | undefined>;
26
+ export type ScopePerSchemaCoordinate = Record<string, 'PRIVATE' | 'PUBLIC' | undefined>;
25
27
  export type UseResponseCacheParameter<PluginContext extends Record<string, any> = {}> = {
26
28
  cache?: Cache | ((ctx: Record<string, any>) => Cache);
27
29
  /**
@@ -40,8 +42,25 @@ export type UseResponseCacheParameter<PluginContext extends Record<string, any>
40
42
  * In the unusual case where you actually want to cache introspection query operations,
41
43
  * you need to provide the value `{ 'Query.__schema': undefined }`.
42
44
  */
43
- ttlPerSchemaCoordinate?: Record<string, number | undefined>;
44
- scopePerSchemaCoordinate?: Record<string, 'PRIVATE' | 'PUBLIC' | undefined>;
45
+ ttlPerSchemaCoordinate?: TTLPerSchemaCoordinate;
46
+ /**
47
+ * Define the scope (PUBLIC or PRIVATE) by schema coordinate.
48
+ * The default scope for all types and fields is PUBLIC
49
+ *
50
+ * If an operation contains a PRIVATE type or field, the result will be cached only if a session
51
+ * id is found for this request.
52
+ *
53
+ * Note: To share cache of responses with a PUBLIC scope between all users, enable `ignoreSessionIdForPublicScope`
54
+ */
55
+ scopePerSchemaCoordinate?: ScopePerSchemaCoordinate;
56
+ /**
57
+ * If enabled, a response with a PUBLIC scope will be cached with an operation key ignoring the
58
+ * session ID. This allows to improve cache hit further, but scope should be carefully defined
59
+ * to avoid any private data.
60
+ *
61
+ * @default false.
62
+ */
63
+ ignoreSessionIdForPublicScope?: boolean;
45
64
  /**
46
65
  * Allows to cache responses based on the resolved session id.
47
66
  * Return a unique value for each session.
@@ -151,6 +170,10 @@ export type ResponseCacheExtensions = {
151
170
  export type ResponseCacheExecutionResult = ExecutionResult<ObjMap<unknown>, {
152
171
  responseCache?: ResponseCacheExtensions;
153
172
  }>;
154
- export declare function useResponseCache<PluginContext extends Record<string, any> = {}>({ cache, ttl: globalTtl, session, enabled, ignoredTypes, ttlPerType, ttlPerSchemaCoordinate, scopePerSchemaCoordinate, idFields, invalidateViaMutation, buildResponseCacheKey, getDocumentString, shouldCacheResult, onTtl, includeExtensionMetadata, }: UseResponseCacheParameter<PluginContext>): Plugin<PluginContext>;
173
+ export type CacheControlDirective = {
174
+ maxAge?: number;
175
+ scope?: 'PUBLIC' | 'PRIVATE';
176
+ };
177
+ export declare function useResponseCache<PluginContext extends Record<string, any> = {}>({ cache, ttl: globalTtl, session, enabled, ignoredTypes, ttlPerType, idFields, invalidateViaMutation, ignoreSessionIdForPublicScope, buildResponseCacheKey, getDocumentString, shouldCacheResult, onTtl, includeExtensionMetadata, ...options }: UseResponseCacheParameter<PluginContext>): Plugin<PluginContext>;
155
178
  export declare function resultWithMetadata(result: ExecutionResult, metadata: ResponseCacheExtensions): ResponseCacheExecutionResult;
156
179
  export declare const cacheControlDirective = "\n enum CacheControlScope {\n PUBLIC\n PRIVATE\n }\n\n directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT\n";