-
Notifications
You must be signed in to change notification settings - Fork 130
/
Copy pathredis-cache.ts
115 lines (96 loc) · 4.24 KB
/
redis-cache.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import Redis from 'ioredis';
import type { Cache } from '@envelop/response-cache';
export type BuildRedisEntityId = (typename: string, id: number | string) => string;
export type BuildRedisOperationResultCacheKey = (responseId: string) => string;
export type RedisCacheParameter = {
/**
* Redis instance
* @see Redis.Redis https://github.com/luin/ioredis
*/
redis: Redis.Redis;
/**
* Customize how the cache entity id is built.
* By default the typename is concatenated with the id e.g. `User:1`
*/
buildRedisEntityId?: BuildRedisEntityId;
/**
* Customize how the cache key that stores the operations associated with the response is built.
* By default `operations` is concatenated with the responseId e.g. `operations:arZm3tCKgGmpu+a5slrpSH9vjSQ=`
*/
buildRedisOperationResultCacheKey?: BuildRedisOperationResultCacheKey;
};
export const createRedisCache = (params: RedisCacheParameter): Cache => {
const store = params.redis;
const buildRedisEntityId = params?.buildRedisEntityId ?? defaultBuildRedisEntityId;
const buildRedisOperationResultCacheKey = params?.buildRedisOperationResultCacheKey ?? defaultBuildRedisOperationResultCacheKey;
async function buildEntityInvalidationsKeys(entity: string): Promise<string[]> {
const keysToInvalidate: string[] = [entity];
// find the responseIds for the entity
const responseIds = await store.smembers(entity);
// and add each response to be invalidated since they contained the entity data
responseIds.forEach(responseId => {
keysToInvalidate.push(responseId);
keysToInvalidate.push(buildRedisOperationResultCacheKey(responseId));
});
// if invalidating an entity like Comment, then also invalidate Comment:1, Comment:2, etc
if (!entity.includes(':')) {
const entityKeys = await store.keys(`${entity}:*`);
for (const entityKey of entityKeys) {
// and invalidate any responses in each of those entity keys
const entityResponseIds = await store.smembers(entityKey);
// if invalidating an entity check for associated operations containing that entity
// and invalidate each response since they contained the entity data
entityResponseIds.forEach(responseId => {
keysToInvalidate.push(responseId);
keysToInvalidate.push(buildRedisOperationResultCacheKey(responseId));
});
// then the entityKeys like Comment:1, Comment:2 etc to be invalidated
keysToInvalidate.push(entityKey);
}
}
return keysToInvalidate;
}
return {
async set(responseId, result, collectedEntities, ttl) {
const pipeline = store.pipeline();
if (ttl === Infinity) {
pipeline.set(responseId, JSON.stringify(result));
} else {
// set the ttl in milliseconds
pipeline.set(responseId, JSON.stringify(result), 'PX', ttl);
}
const responseKey = buildRedisOperationResultCacheKey(responseId);
for (const { typename, id } of collectedEntities) {
// Adds a key for the typename => response
pipeline.sadd(typename, responseId);
// Adds a key for the operation => typename
pipeline.sadd(responseKey, typename);
if (id) {
const entityId = buildRedisEntityId(typename, id);
// Adds a key for the typename:id => response
pipeline.sadd(entityId, responseId);
// Adds a key for the operation => typename:id
pipeline.sadd(responseKey, entityId);
}
}
await pipeline.exec();
},
async get(responseId) {
const result = await store.get(responseId);
return result && JSON.parse(result);
},
async invalidate(entitiesToRemove) {
const invalidationKeys: string[][] = [];
for (const { typename, id } of entitiesToRemove) {
invalidationKeys.push(await buildEntityInvalidationsKeys(id != null ? buildRedisEntityId(typename, id) : typename));
}
const keys = invalidationKeys.flat();
if (keys.length > 0) {
await store.del(keys);
}
},
};
};
export const defaultBuildRedisEntityId: BuildRedisEntityId = (typename, id) => `${typename}:${id}`;
export const defaultBuildRedisOperationResultCacheKey: BuildRedisOperationResultCacheKey = responseId =>
`operations:${responseId}`;