@envelop/response-cache 2.4.0-alpha-26c4ae2.0 → 2.4.0-alpha-8a8cd36.0

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.
Files changed (2) hide show
  1. package/README.md +491 -0
  2. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,491 @@
1
+ ## `@envelop/response-cache`
2
+
3
+ - Skip the execution phase and reduce server load by caching execution results in-memory.
4
+ - Customize cache entry time to live based on fields and types within the execution result.
5
+ - Automatically invalidate the cache based on mutation selection sets.
6
+ - Customize invalidation through the cache api (e.g. listen to a database write log).
7
+ - Implement your own global cache (e.g. using another key/value store) by implementing the `Cache` interface.
8
+
9
+ [Check out the GraphQL Response Cache Guide for more information](https://envelop.dev/docs/guides/adding-a-graphql-response-cache)
10
+
11
+ > Watch [Episode #34 of `graphql.wtf`](https://graphql.wtf/episodes/34-response-cache-plugin-with-envelop) for a quick introduction to using Response Cache plugin with Envelop:
12
+
13
+ <iframe
14
+ width="100%"
15
+ height="400"
16
+ src="https://www.youtube.com/embed/1EBphPltkA4"
17
+ title="YouTube video player"
18
+ frameBorder="0"
19
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
20
+ allowFullScreen
21
+ ></iframe>
22
+
23
+ ## Getting Started
24
+
25
+ ```bash
26
+ yarn add @envelop/response-cache
27
+ ```
28
+
29
+ ## Usage Example
30
+
31
+ When configuring the `useResponseCache`, you can choose the type of cache:
32
+
33
+ - In-Memory LRU Cache (default)
34
+ - Redis Cache (see: `@envelop/response-cache-redis`)
35
+
36
+ ### In-Memory Cache
37
+
38
+ The in-memory LRU cache is used by default.
39
+
40
+ ```ts
41
+ import { envelop } from '@envelop/core';
42
+ import { useResponseCache } from '@envelop/response-cache';
43
+
44
+ const getEnveloped = envelop({
45
+ plugins: [
46
+ // ... other plugins ...
47
+ useResponseCache(),
48
+ ],
49
+ });
50
+ ```
51
+
52
+ Or, you may create the in-memory LRU cache explicitly.
53
+
54
+ ```ts
55
+ import { envelop } from '@envelop/core';
56
+ import { useResponseCache, createInMemoryCache } from '@envelop/response-cache';
57
+
58
+ const cache = createInMemoryCache();
59
+
60
+ const getEnveloped = envelop({
61
+ plugins: [
62
+ // ... other plugins ...
63
+ useResponseCache({ cache }),
64
+ ],
65
+ });
66
+ ```
67
+
68
+ > Note: The in-memory LRU cache is not suitable for serverless deployments. Instead, consider the Redis cache provided by `@envelop/response-cache-redis`.
69
+
70
+ ### Redis Cache
71
+
72
+ ```bash
73
+ yarn add @envelop/response-cache-redis
74
+ ```
75
+
76
+ In order to use the Redis cache, you need to:
77
+
78
+ - Create a Redis database
79
+ - Collect the connection settings (or its connection string), e.g., `host`, `port`, `username`, `password`, `tls`, etc.
80
+ - Create and configure a [Redis client](https://github.com/luin/ioredis) with your [connection settings](https://github.com/luin/ioredis/blob/master/API.md#Redis) and any [additional options](https://github.com/luin/ioredis/blob/master/API.md#new_Redis_new)
81
+ - Create an instance of the Redis Cache and set to the `useResponseCache` plugin options
82
+
83
+ ```ts
84
+ import { envelop } from '@envelop/core';
85
+ import { useResponseCache } from '@envelop/response-cache';
86
+ import { createRedisCache } from '@envelop/response-cache-redis';
87
+
88
+ import Redis from 'ioredis';
89
+
90
+ const redis = new Redis({
91
+ host: 'my-redis-db.example.com',
92
+ port: '30652',
93
+ password: '1234567890',
94
+ });
95
+
96
+ const redis = new Redis("rediss://:1234567890@my-redis-db.example.com:30652");
97
+
98
+ const cache = createRedisCache({ redis });
99
+
100
+ const getEnveloped = envelop({
101
+ plugins: [
102
+ // ... other plugins ...
103
+ useResponseCache({ cache }),
104
+ ],
105
+ });
106
+ ```
107
+
108
+ > Note: In the Recipes below, be sure to provide your Redis `cache` instance with `useResponseCache({ cache })`.
109
+
110
+ ## Recipes
111
+
112
+ ### Cache with maximum TTL
113
+
114
+ ```ts
115
+ import { envelop } from '@envelop/core';
116
+ import { useResponseCache } from '@envelop/response-cache';
117
+
118
+ const getEnveloped = envelop({
119
+ plugins: [
120
+ // ... other plugins ...
121
+ useResponseCache({
122
+ ttl: 2000, // cached execution results become stale after 2 seconds
123
+ }),
124
+ ],
125
+ });
126
+ ```
127
+
128
+ > Note: Setting `ttl: 0` will disable TTL for all types. You can use that if you wish to disable caching for all type, and then enable caching for specific types using `ttlPerType`.
129
+
130
+ ### Cache with custom TTL per object type
131
+
132
+ ```ts
133
+ import { envelop } from '@envelop/core';
134
+ import { useResponseCache } from '@envelop/response-cache';
135
+
136
+ const getEnveloped = envelop({
137
+ plugins: [
138
+ // ... other plugins ...
139
+ useResponseCache({
140
+ ttl: 2000,
141
+ ttlPerType: {
142
+ // cached execution results that contain a `Stock` object become stale after 500ms
143
+ Stock: 500,
144
+ },
145
+ }),
146
+ ],
147
+ });
148
+ ```
149
+
150
+ ### Cache with custom TTL per schema coordinate
151
+
152
+ ```ts
153
+ import { envelop } from '@envelop/core';
154
+ import { useResponseCache } from '@envelop/response-cache';
155
+
156
+ const getEnveloped = envelop({
157
+ plugins: [
158
+ // ... other plugins ...
159
+ useResponseCache({
160
+ ttl: 2000,
161
+ ttlPerSchemaCoordinate: {
162
+ // cached execution results that select the `Query.user` field become stale after 100ms
163
+ 'Query.rocketCoordinates': 100,
164
+ },
165
+ }),
166
+ ],
167
+ });
168
+ ```
169
+
170
+ ### Cache based on session/user
171
+
172
+ ```ts
173
+ import { envelop } from '@envelop/core';
174
+ import { useResponseCache } from '@envelop/response-cache';
175
+
176
+ const getEnveloped = envelop({
177
+ plugins: [
178
+ // ... other plugins ...
179
+ useResponseCache({
180
+ ttl: 2000,
181
+ // context is the GraphQL context used for execution
182
+ session: context => String(context.user?.id),
183
+ }),
184
+ ],
185
+ });
186
+ ```
187
+
188
+ ### Disable cache based on session/user
189
+
190
+ ```ts
191
+ import { envelop } from '@envelop/core';
192
+ import { useResponseCache } from '@envelop/response-cache';
193
+
194
+ const getEnveloped = envelop({
195
+ plugins: [
196
+ // ... other plugins ...
197
+ useResponseCache({
198
+ ttl: 2000,
199
+ // context is the GraphQL context used for execution
200
+ enabled: context => context.user?.role !== 'admin',
201
+ }),
202
+ ],
203
+ });
204
+ ```
205
+
206
+ ### Customize if result should be cached
207
+
208
+ You can define a custom function used to check if a query operation execution result should be cached.
209
+
210
+ ```ts
211
+ type ShouldCacheResultFunction = (params: { result: ExecutionResult }) => Boolean;
212
+ ```
213
+
214
+ This is useful for advanced use-cases. E.g. if you want to
215
+ cache results with certain error types.
216
+
217
+ By default, the `defaultShouldCacheResult` function is used which never caches any query operation execution results that includes any errors (unexpected, EnvelopError, or GraphQLError).
218
+
219
+ ```ts
220
+ import { envelop } from '@envelop/core';
221
+ import { useResponseCache, ShouldCacheResultFunction } from '@envelop/response-cache';
222
+
223
+ export const defaultShouldCacheResult: ShouldCacheResultFunction = (params): Boolean => {
224
+ // cache any query operation execution result
225
+ // even if it includes errors
226
+ return true;
227
+ };
228
+
229
+ const getEnveloped = envelop({
230
+ plugins: [
231
+ // ... other plugins ...
232
+ useResponseCache({
233
+ shouldCacheResult = myCustomShouldCacheResult,
234
+ }),
235
+ ],
236
+ });
237
+ ```
238
+
239
+ ### Cache Introspection query operations
240
+
241
+ By default introspection query operations are not cached. In case you want to cache them you can do so with the `ttlPerSchemaCoordinate` parameter.
242
+
243
+ **Infinite caching**
244
+
245
+ ```ts
246
+ import { envelop } from '@envelop/core';
247
+ import { useResponseCache } from '@envelop/response-cache';
248
+
249
+ const getEnveloped = envelop({
250
+ plugins: [
251
+ // ... other plugins ...
252
+ useResponseCache({
253
+ ttlPerSchemaCoordinate: {
254
+ 'Query.__schema': undefined, // cache infinitely
255
+ },
256
+ }),
257
+ ],
258
+ });
259
+ ```
260
+
261
+ **TTL caching**
262
+
263
+ ```ts
264
+ import { envelop } from '@envelop/core';
265
+ import { useResponseCache } from '@envelop/response-cache';
266
+
267
+ const getEnveloped = envelop({
268
+ plugins: [
269
+ // ... other plugins ...
270
+ useResponseCache({
271
+ ttlPerSchemaCoordinate: {
272
+ 'Query.__schema': 10_000, // cache for 10 seconds
273
+ },
274
+ }),
275
+ ],
276
+ });
277
+ ```
278
+
279
+ ### Cache with maximum TTL
280
+
281
+ ```ts
282
+ import { envelop } from '@envelop/core';
283
+ import { useResponseCache } from '@envelop/response-cache';
284
+
285
+ const getEnveloped = envelop({
286
+ plugins: [
287
+ // ... other plugins ...
288
+ useResponseCache({
289
+ ttl: 2000, // cached execution results become stale after 2 seconds
290
+ }),
291
+ ],
292
+ });
293
+ ```
294
+
295
+ ### Customize the fields that are used for building the cache ID
296
+
297
+ ```ts
298
+ import { envelop } from '@envelop/core';
299
+ import { useResponseCache } from '@envelop/response-cache';
300
+
301
+ const getEnveloped = envelop({
302
+ plugins: [
303
+ // ... other plugins ...
304
+ useResponseCache({
305
+ ttl: 2000,
306
+ // use the `_id` instead of `id` field.
307
+ idFields: ['_id'],
308
+ }),
309
+ ],
310
+ });
311
+ ```
312
+
313
+ ### Disable automatic cache invalidation via mutations
314
+
315
+ ```ts
316
+ import { envelop } from '@envelop/core';
317
+ import { useResponseCache } from '@envelop/response-cache';
318
+
319
+ const getEnveloped = envelop({
320
+ plugins: [
321
+ // ... other plugins ...
322
+ useResponseCache({
323
+ ttl: 2000,
324
+ // some might prefer invalidating based on a database write log
325
+ invalidateViaMutation: false,
326
+ }),
327
+ ],
328
+ });
329
+ ```
330
+
331
+ ### Invalidate Cache based on custom logic
332
+
333
+ ```ts
334
+ import { envelop } from '@envelop/core';
335
+ import { useResponseCache, createInMemoryCache } from '@envelop/response-cache';
336
+ import { emitter } from './eventEmitter';
337
+
338
+ // we create our cache instance, which allows calling all methods on it
339
+ const cache = createInMemoryCache();
340
+
341
+ const getEnveloped = envelop({
342
+ plugins: [
343
+ // ... other plugins ...
344
+ useResponseCache({
345
+ ttl: 2000,
346
+ // we pass the cache instance to the request.
347
+ cache,
348
+ }),
349
+ ],
350
+ });
351
+
352
+ emitter.on('invalidate', resource => {
353
+ cache.invalidate([
354
+ {
355
+ typename: resource.type,
356
+ id: resource.id,
357
+ },
358
+ ]);
359
+ });
360
+ ```
361
+
362
+ ### Customize how cache ids are built
363
+
364
+ ```ts
365
+ import { envelop } from '@envelop/core';
366
+ import { useResponseCache, createInMemoryCache } from '@envelop/response-cache';
367
+ import { emitter } from './eventEmitter';
368
+
369
+ // we create our cache instance, which allows calling all methods on it
370
+ const cache = createInMemoryCache({
371
+ // in relay we have global unique ids, no need to use `typename:id`
372
+ makeId: (typename, id) => id ?? typename,
373
+ });
374
+
375
+ const getEnveloped = envelop({
376
+ plugins: [
377
+ // ... other plugins ...
378
+ useResponseCache({
379
+ ttl: 2000,
380
+ // we pass the cache instance to the request.
381
+ cache,
382
+ }),
383
+ ],
384
+ });
385
+ ```
386
+
387
+ ### Expose cache metadata via extensions
388
+
389
+ For debugging or monitoring it might be useful to know whether a response got served from the cache or not.
390
+
391
+ ```ts
392
+ const getEnveloped = envelop({
393
+ plugins: [
394
+ // ... other plugins ...
395
+ useResponseCache({
396
+ ttl: 2000,
397
+ includeExtensionMetadata: true,
398
+ }),
399
+ ],
400
+ });
401
+ ```
402
+
403
+ This option will attach the following fields to the execution result if set to true (or `process.env["NODE_ENV"]` is `"development"`).
404
+
405
+ - `extension.responseCache.hit` - Whether the result was served form the cache or not
406
+ - `extension.responseCache.invalidatedEntities` - Entities that got invalidated by a mutation operation
407
+
408
+ #### Examples:
409
+
410
+ **Cache miss (response is generated by executing the query):**
411
+
412
+ ```graphql
413
+ query UserById {
414
+ user(id: "1") {
415
+ id
416
+ name
417
+ }
418
+ }
419
+ ```
420
+
421
+ ```json
422
+ {
423
+ "result": {
424
+ "user": {
425
+ "id": "1",
426
+ "name": "Laurin"
427
+ }
428
+ },
429
+ "extensions": {
430
+ "responseCache": {
431
+ "hit": false
432
+ }
433
+ }
434
+ }
435
+ ```
436
+
437
+ **Cache hit (response served from response cache):**
438
+
439
+ ```graphql
440
+ query UserById {
441
+ user(id: "1") {
442
+ id
443
+ name
444
+ }
445
+ }
446
+ ```
447
+
448
+ ```json
449
+ {
450
+ "result": {
451
+ "user": {
452
+ "id": "1",
453
+ "name": "Laurin"
454
+ }
455
+ },
456
+ "extensions": {
457
+ "responseCache": {
458
+ "hit": true
459
+ }
460
+ }
461
+ }
462
+ ```
463
+
464
+ **Invalidation via Mutation:**
465
+
466
+ ```graphql
467
+ mutation SetNameMutation {
468
+ userSetName(name: "NotLaurin") {
469
+ user {
470
+ id
471
+ name
472
+ }
473
+ }
474
+ }
475
+ ```
476
+
477
+ ```json
478
+ {
479
+ "result": {
480
+ "userSetName": {
481
+ "user": {
482
+ "id": "1",
483
+ "name": "Laurin"
484
+ }
485
+ }
486
+ },
487
+ "extensions": {
488
+ "invalidatedEntities": [{ "id": "1", "typename": "User" }]
489
+ }
490
+ }
491
+ ```
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@envelop/response-cache",
3
- "version": "2.4.0-alpha-26c4ae2.0",
3
+ "version": "2.4.0-alpha-8a8cd36.0",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
- "@envelop/core": "2.4.0-alpha-26c4ae2.0",
6
+ "@envelop/core": "2.4.0-alpha-8a8cd36.0",
7
7
  "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
8
8
  },
9
9
  "dependencies": {