UNPKG

6.04 kBJavaScriptView Raw
1function asPromise (fn) {
2 return new Promise((resolve, reject) => {
3 const args = [...arguments].slice(1);
4 args.push((err, res) => {
5 if (err) return reject(err);
6 resolve(res);
7 });
8 fn.apply(null, args);
9 });
10}
11
12function buildCache (client, opts) {
13 if (!client) {
14 throw Error('redis client is required.');
15 }
16
17 if (typeof opts === 'number') {
18 opts = {max: opts};
19 }
20
21 opts = Object.assign({
22 namespace: 'LRU-CACHE!',
23 score: () => new Date().getTime(),
24 increment: false
25 }, opts);
26
27 if (!opts.max) {
28 throw Error('max number of items in cache must be specified.');
29 }
30
31 const ZSET_KEY = `${opts.namespace}-i`;
32
33 function namedKey (key) {
34 if (!typeof key === 'string') {
35 return Promise.reject(Error('key should be a string.'));
36 }
37
38 return `${opts.namespace}-k-${key}`;
39 }
40
41 /*
42 * Remove a set of keys from the cache and the index, in a single transaction,
43 * to avoid orphan indexes or cache values.
44 */
45 const safeDelete = (keys) => {
46 if (keys.length) {
47 const multi = client.multi()
48 .zrem(ZSET_KEY, keys)
49 .del(keys);
50
51 return asPromise(multi.exec.bind(multi));
52 }
53
54 return Promise.resolve();
55 };
56
57 /*
58 * Gets the value for the given key and updates its timestamp score, only if
59 * already present in the zset. The result is JSON.parsed before returned.
60 */
61 const get = (key) => {
62 const score = -1 * opts.score(key);
63 key = namedKey(key);
64
65 const multi = client.multi()
66 .get(key);
67
68 if (opts.increment) {
69 multi.zadd(ZSET_KEY, 'XX', 'CH', 'INCR', score, key);
70 } else {
71 multi.zadd(ZSET_KEY, 'XX', 'CH', score, key);
72 }
73
74 return asPromise(multi.exec.bind(multi))
75 .then((results) => {
76 if (results[0] === null && results[1]) {
77 // value has been expired, remove from zset
78 return asPromise(client.zrem.bind(client), ZSET_KEY, key)
79 .then(() => null);
80 }
81 return JSON.parse(results[0]);
82 });
83 };
84
85 /*
86 * Save (add/update) the new value for the given key, and update its timestamp
87 * score. The value is JSON.stringified before saving.
88 *
89 * If there are more than opts.max items in the cache after the operation
90 * then remove each exceeded key from the zset index and its value from the
91 * cache (in a single transaction).
92 */
93 const set = (key, value, maxAge) => {
94 if (Glad.cache.disabled) return Promise.resolve();
95 const score = -1 * opts.score(key);
96 key = namedKey(key);
97 maxAge = maxAge || opts.maxAge;
98
99 const multi = client.multi();
100 if (maxAge) {
101 multi.set(key, JSON.stringify(value), 'PX', maxAge);
102 } else {
103 multi.set(key, JSON.stringify(value));
104 }
105
106 if (opts.increment) {
107 multi.zadd(ZSET_KEY, 'INCR', score, key);
108 } else {
109 multi.zadd(ZSET_KEY, score, key);
110 }
111
112 // we get zrange first then safe delete instead of just zremrange,
113 // that way we guarantee that zset is always in sync with available data in the cache
114 // also, include the last item inside the cache size, because we always want to
115 // preserve the one that was just set, even if it has same or less score than other.
116 multi.zrange(ZSET_KEY, opts.max - 1, -1);
117
118 return asPromise(multi.exec.bind(multi))
119 .then((results) => {
120 if (results[2].length > 1) { // the first one is inside the limit
121 let toDelete = results[2].slice(1);
122 if (toDelete.indexOf(key) !== -1) {
123 toDelete = results[2].slice(0, 1).concat(results[2].slice(2));
124 }
125 return safeDelete(toDelete);
126 }
127 })
128 .then(() => value);
129 };
130
131 /*
132 * Try to get the value of key from the cache. If missing, call function and store
133 * the result.
134 */
135 const getOrSet = (key, fn, maxAge) => get(key)
136 .then((result) => {
137 if (result === null) {
138 return Promise.resolve()
139 .then(fn)
140 .then((result) => set(key, result, maxAge));
141 }
142 return result;
143 });
144
145 /*
146 * Retrieve the value for key in the cache (if present), without updating the
147 * timestamp score. The result is JSON.parsed before returned.
148 */
149 const peek = (key) => {
150 key = namedKey(key);
151
152 return asPromise(client.get.bind(client), key)
153 .then((result) => {
154 if (result === null) {
155 // value may have been expired, remove from zset
156 return asPromise(client.zrem.bind(client), ZSET_KEY, key)
157 .then(() => null);
158 }
159 return JSON.parse(result);
160 });
161 };
162
163 /*
164 * Remove the value of key from the cache (and the zset index).
165 */
166 const del = (key) => safeDelete([namedKey(key)]);
167
168 /*
169 * Remove all items from cache and the zset index.
170 */
171 const reset = () => asPromise(client.zrange.bind(client), ZSET_KEY, 0, -1)
172 .then(safeDelete);
173
174 /*
175 * Return true if the given key is in the cache
176 */
177 const has = (key) => asPromise(client.get.bind(client), namedKey(key))
178 .then((result) => (!!result));
179
180 /*
181 * Return an array of the keys currently in the cache, most reacently accessed
182 * first.
183 */
184 const keys = () => asPromise(client.zrange.bind(client), ZSET_KEY, 0, opts.max - 1)
185 .then((results) => results.map((key) => key.slice(`${opts.namespace}-k-`.length)));
186
187 /*
188 * Return an array of the values currently in the cache, most reacently accessed
189 * first.
190 */
191 const values = () => asPromise(client.zrange.bind(client), ZSET_KEY, 0, opts.max - 1)
192 .then((results) => {
193 const multi = client.multi();
194 results.forEach((key) => multi.get(key));
195 return asPromise(multi.exec.bind(multi));
196 })
197 .then((results) => results.map(JSON.parse));
198
199 /*
200 * Return the amount of items currently in the cache.
201 */
202 const count = () => asPromise(client.zcard.bind(client), ZSET_KEY);
203
204 return {
205 get: get,
206 set: set,
207 getOrSet: getOrSet,
208 peek: peek,
209 del: del,
210 reset: reset,
211 has: has,
212 keys: keys,
213 values: values,
214 count: count
215 };
216}
217
218module.exports = buildCache;