This repository was archived by the owner on Oct 11, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathindex.js
107 lines (92 loc) · 2.84 KB
/
index.js
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
/* @flow strict
*
* TagCache
* Allows one to cache data with tags and invalidate based on them
*
* Under the hood this stores one set of keys per tag (ID) and the data per key in Redis:
* - `tags:asdf-123` = `db.table('threads').get('asdf-345') db.table('threads').get('asdf-234') ...`
* - `data:db.table('threads').get('asdf-345')` = `{ "id": "asdf-345", "content": { "title": "Hello" }, ... }`
*/
import Redis, { type RedisOptions } from 'ioredis';
export type CacheData = ?mixed;
export type Options = {
defaultTimeout?: number,
redis?: RedisOptions,
};
class TagCache {
redis: Redis;
options: Options;
constructor(options?: Options = {}) {
this.redis = new Redis(options.redis || {});
this.options = options;
}
get = async (...keys: Array<string>): Promise<?CacheData> => {
try {
return this.redis.mget(keys.map(key => `data:${key}`)).then(res => {
try {
// Special case for single element gets
if (res.length === 1) return JSON.parse(res[0]);
return res.map(elem => JSON.parse(elem));
} catch (err) {
return res;
}
});
} catch (err) {
return Promise.reject(err);
}
};
set = async (
key: string,
data: CacheData,
tags: Array<string>,
options?: {
timeout?: number,
} = {}
): Promise<void> => {
try {
// NOTE(@mxstbr): This is a multi execution because if any of the commands is invalid
// we don't want to execute anything
const multi = await this.redis.multi();
// Add the key to each of the tag sets
tags.forEach(tag => {
multi.sadd(`tags:${tag}`, key);
});
const timeout =
(options && options.timeout) || this.options.defaultTimeout;
// Add the data to the key
if (typeof timeout === 'number') {
multi.set(`data:${key}`, JSON.stringify(data), 'ex', timeout);
} else {
multi.set(`data:${key}`, JSON.stringify(data));
}
await multi.exec();
return;
} catch (err) {
return Promise.reject(err);
}
};
// How invalidation by tag works:
// 1. Get all the keys associated with all the passed-in tags (tags:${tag})
// 2. Delete all the keys data (data:${key})
// 3. Delete all the tags (tags:${tag})
invalidate = async (...tags: Array<string>): Promise<void> => {
try {
// NOTE(@mxstbr): [].concat.apply([],...) flattens the array
const keys = [].concat.apply(
[],
await Promise.all(tags.map(tag => this.redis.smembers(`tags:${tag}`)))
);
const pipeline = await this.redis.pipeline();
keys.forEach(key => {
pipeline.del(`data:${key}`);
});
tags.forEach(tag => {
pipeline.del(`tags:${tag}`);
});
await pipeline.exec();
} catch (err) {
return Promise.reject(err);
}
};
}
export default TagCache;