1 | function 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 |
|
12 | function 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 |
|
43 |
|
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 |
|
59 |
|
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 |
|
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 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
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 |
|
113 |
|
114 |
|
115 |
|
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) {
|
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 |
|
133 |
|
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 |
|
156 | return asPromise(client.zrem.bind(client), ZSET_KEY, key)
|
157 | .then(() => null);
|
158 | }
|
159 | return JSON.parse(result);
|
160 | });
|
161 | };
|
162 |
|
163 | |
164 |
|
165 |
|
166 | const del = (key) => safeDelete([namedKey(key)]);
|
167 |
|
168 | |
169 |
|
170 |
|
171 | const reset = () => asPromise(client.zrange.bind(client), ZSET_KEY, 0, -1)
|
172 | .then(safeDelete);
|
173 |
|
174 | |
175 |
|
176 |
|
177 | const has = (key) => asPromise(client.get.bind(client), namedKey(key))
|
178 | .then((result) => (!!result));
|
179 |
|
180 | |
181 |
|
182 |
|
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 |
|
189 |
|
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 |
|
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 |
|
218 | module.exports = buildCache;
|