exframe-cache-manager 2.2.0 → 2.2.2

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.
package/index.js CHANGED
@@ -1,272 +1,5 @@
1
- 'use strict';
1
+ const cacheManagerModule = require('./src/cacheManager/index');
2
2
 
3
- import util from 'node:util';
4
- import { setTimeout as waitTimeout } from 'node:timers/promises';
5
- import CacheManager from 'cache-manager';
6
- import RedisStore from 'cache-manager-redis-store';
7
- import { lazyInstrument } from 'exframe-metrics';
8
- import health from 'exframe-health';
9
- import service from 'exframe-service';
10
- import { generateShortId } from 'exframe-utilities';
3
+ const { cachemanager } = cacheManagerModule;
11
4
 
12
- const DEFAULT_TTL = 600;
13
-
14
- const db = 0;
15
-
16
- /**
17
- * Cache manager for storing stuff.
18
- * Options is required to pass the redis location
19
- * @param {Options} options
20
- */
21
- function cachemanager(options) {
22
- if (options.url) {
23
- options.host = undefined;
24
- options.port = undefined;
25
- }
26
-
27
- const redisCache = CacheManager.caching({
28
- store: RedisStore,
29
- ttl: DEFAULT_TTL,
30
- db,
31
- ...options
32
- });
33
-
34
- // @ts-ignore
35
- const redisStore = redisCache.store;
36
- const redisClient = redisStore.getClient();
37
- redisClient.setMaxListeners(0);
38
-
39
- redisClient.on('error', (e) => {
40
- console.log('Unhandled Redis Error', { errorDetails: e }); // eslint-disable-line
41
- });
42
-
43
- process.on('beforeExit', () => {
44
- if (redisClient.connected) {
45
- redisClient.removeAllListeners();
46
- redisClient.end(true);
47
- }
48
- });
49
-
50
- const instance = {
51
- id: generateShortId(),
52
- get cacheStorePool() {
53
- return redisClient;
54
- },
55
-
56
- /**
57
- *
58
- * @param {string} key
59
- * @param {object} context
60
- * @param {Options} options
61
- */
62
-
63
- async getItem(context, key, { timeout } = {}) {
64
- if (arguments.length === 1) {
65
- // context = key, param order was switched in function def to preserve backwards compatability
66
- return redisCache.get(context);
67
- }
68
-
69
- const controller = new AbortController();
70
- if (timeout) {
71
- const result = await Promise.race([
72
- redisCache.get(key),
73
- waitTimeout(timeout, false, { signal: controller.signal, ref: false })
74
- ]);
75
-
76
- if (!result) {
77
- context.log.info('CACHE TIMEOUT ERROR');
78
- return Promise.reject(new Error('Timed out attempting to retrieve item from cache.'));
79
- }
80
-
81
- controller.abort();
82
- return result;
83
- }
84
- return redisCache.get(key);
85
- },
86
-
87
- /**
88
- *
89
- * @template T
90
- * @param {string} key
91
- * @param {T} value
92
- * @param {number} [ttl]
93
- * @returns {Promise<T>}
94
- */
95
- async setItem(key, value, ttl) {
96
- await redisCache.set(key, value, {
97
- ttl: ttl || Number(process.env.REDIS_CACHE_TTL)
98
- });
99
-
100
- return value;
101
- },
102
-
103
- /**
104
- * Set the item if it does not exist yet. If the item already exists, do not set and return null.
105
- * @template T
106
- * @param {String} key
107
- * @param {T} value
108
- * @param {Number} ttl
109
- * @returns {Promise<T|null>}
110
- */
111
- async setItemIfNotExists(key, value, ttl) {
112
- const ttlToSet = ttl ?? (process.env.REDIS_CACHE_TTL
113
- ? Number(process.env.REDIS_CACHE_TTL)
114
- : DEFAULT_TTL);
115
-
116
- return new Promise((resolve, reject) => {
117
- redisClient.set(key, value, 'NX', 'EX', ttlToSet, (error, result) => {
118
- if (error) {
119
- return reject(error);
120
- }
121
-
122
- resolve(result === 'OK' ? value : null);
123
- });
124
- });
125
- },
126
-
127
- /**
128
- *
129
- * @param {string} key
130
- * @param {any} value
131
- */
132
- addSetItem(key, value) {
133
- return this.processRedisCommands(client => client.sadd(key, value));
134
- },
135
-
136
- /**
137
- *
138
- * @param {string} key
139
- * @param {any} value
140
- */
141
- removeSetItem(key, value) {
142
- return this.processRedisCommands(client => client.srem(key, value));
143
- },
144
-
145
- /**
146
- *
147
- * @param {string} key
148
- * @param {any} value
149
- * @returns {Promise<Boolean>}
150
- */
151
- isSetMember(key, value) {
152
- return this.processRedisCommands(async client => new Promise((res, rej) => {
153
- client.sismember(key, value, (ex, result) => {
154
- if (ex) return rej(ex);
155
- res(result === 1);
156
- });
157
- }));
158
- },
159
-
160
- /**
161
- * @returns {Promise<true>}
162
- */
163
- async healthCheck() {
164
- return this.processRedisCommands(client => client && client.server_info.loading === '0');
165
- },
166
-
167
- close() {
168
- return new Promise(res => {
169
- redisClient.removeAllListeners();
170
- redisClient.end(true);
171
- res();
172
- });
173
- },
174
-
175
- getClient() {
176
- return new Promise((resolve, reject) => {
177
- if (redisClient.connected) {
178
- return resolve(redisClient);
179
- }
180
-
181
- redisClient.on('ready', () => resolve(redisClient));
182
- redisClient.on('error', (e) => reject(e));
183
- });
184
- },
185
-
186
- /**
187
- *
188
- * @param {(client: any) => any} fn
189
- */
190
- async processRedisCommands(fn) {
191
- const client = await this.getClient();
192
- const result = await fn(client);
193
-
194
- return result;
195
- },
196
-
197
- /**
198
- *
199
- * @param {Context} context
200
- * @param {string | RegExp} pattern
201
- */
202
- flushCache(context, pattern) {
203
- return this.processRedisCommands(async (client) => {
204
- const scanAsync = util.promisify(client.scan).bind(client);
205
- const delAsync = util.promisify(client.del).bind(client);
206
- let cursor = '0';
207
- do {
208
- const result = await scanAsync(cursor, 'MATCH', pattern);
209
- ([cursor] = result);
210
- const keys = result[1];
211
- context.log.info('matched keys', {
212
- keys: result[1]
213
- });
214
- if (keys.length > 0) {
215
- await delAsync(...keys);
216
- }
217
- } while (cursor !== '0');
218
- });
219
- },
220
- redisClient,
221
- redisCache
222
- };
223
-
224
- health.add(`redis-${instance.id}`, async () => {
225
- try {
226
- const controller = new AbortController();
227
-
228
- const result = await Promise.race([
229
- instance.healthCheck(),
230
- waitTimeout(3000, false, { signal: controller.signal, ref: false })
231
- ]);
232
-
233
- if (!result) {
234
- throw new Error('Timed out waiting to verify health status');
235
- }
236
-
237
- controller.abort();
238
- return { status: 200, message: 'OK' };
239
- } catch (ex) { // eslint-disable-line no-unused-vars
240
- return { status: 503, message: 'Error connecting to Redis' };
241
- }
242
- }, { promotionTimeout: 60000 });
243
-
244
- service.registerResource(`exframe-cache-manager-${instance.id}`, { onSignal: () => instance.close(), order: 'last' });
245
-
246
- Object.keys(instance).forEach((key) => {
247
- if (typeof instance[key] === 'function') {
248
- instance[key] = lazyInstrument(instance[key].bind(instance), { metricPrefix: 'cache' });
249
- }
250
- });
251
-
252
- return instance;
253
- }
254
-
255
- // eslint-disable-next-line import/prefer-default-export
256
- export { cachemanager as create };
257
-
258
- /**
259
- * @typedef {RedisCacheManagerOptions & { store?: any, ttl?: number, db?: number }} Options
260
- */
261
-
262
- /**
263
- * @typedef {{ db?: number, url?: string, host?: string, port?: string }} RedisCacheManagerOptions
264
- */
265
-
266
- /**
267
- * @typedef {{ [key: string]: any, log: { [key: string]: LogLevel, info: LogLevel, error: LogLevel } }} Context
268
- */
269
-
270
- /**
271
- * @typedef {(message: string, meta: { [key: string]: any }) => void} LogLevel
272
- */
5
+ module.exports = { create: cachemanager };
package/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { cachemanager } from './src/cacheManager/index.js';
2
+
3
+ export default { create: cachemanager };
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "exframe-cache-manager",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Managing the cache",
5
- "main": "index.js",
6
5
  "type": "module",
6
+ "main": "index.js",
7
+ "module": "index.mjs",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./index.mjs",
11
+ "require": "./index.js",
12
+ "default": "./index.mjs"
13
+ }
14
+ },
7
15
  "config": {
8
16
  "reporter": "mocha-exzeo-reporter"
9
17
  },
@@ -47,5 +55,5 @@
47
55
  "url": "https://bitbucket.org/exzeo-usa/exframe",
48
56
  "directory": "packages/exframe-cache-manager"
49
57
  },
50
- "gitHead": "25463e1a2459e770277640472111e372cacf4aa5"
58
+ "gitHead": "3e1aa0feed298e624741cedcf3195c8725a23e34"
51
59
  }
@@ -0,0 +1,272 @@
1
+ 'use strict';
2
+
3
+ import util from 'node:util';
4
+ import { setTimeout as waitTimeout } from 'node:timers/promises';
5
+ import CacheManager from 'cache-manager';
6
+ import RedisStore from 'cache-manager-redis-store';
7
+ import { lazyInstrument } from 'exframe-metrics';
8
+ import health from 'exframe-health';
9
+ import service from 'exframe-service';
10
+ import { generateShortId } from 'exframe-utilities';
11
+
12
+ const DEFAULT_TTL = 600;
13
+
14
+ const db = 0;
15
+
16
+ /**
17
+ * Cache manager for storing stuff.
18
+ * Options is required to pass the redis location
19
+ * @param {Options} options
20
+ */
21
+ function cachemanager(options) {
22
+ if (options.url) {
23
+ options.host = undefined;
24
+ options.port = undefined;
25
+ }
26
+
27
+ const redisCache = CacheManager.caching({
28
+ store: RedisStore,
29
+ ttl: DEFAULT_TTL,
30
+ db,
31
+ ...options
32
+ });
33
+
34
+ // @ts-ignore
35
+ const redisStore = redisCache.store;
36
+ const redisClient = redisStore.getClient();
37
+ redisClient.setMaxListeners(0);
38
+
39
+ redisClient.on('error', (e) => {
40
+ console.log('Unhandled Redis Error', { errorDetails: e }); // eslint-disable-line
41
+ });
42
+
43
+ process.on('beforeExit', () => {
44
+ if (redisClient.connected) {
45
+ redisClient.removeAllListeners();
46
+ redisClient.end(true);
47
+ }
48
+ });
49
+
50
+ const instance = {
51
+ id: generateShortId(),
52
+ get cacheStorePool() {
53
+ return redisClient;
54
+ },
55
+
56
+ /**
57
+ *
58
+ * @param {string} key
59
+ * @param {object} context
60
+ * @param {Options} options
61
+ */
62
+
63
+ async getItem(context, key, { timeout } = {}) {
64
+ if (arguments.length === 1) {
65
+ // context = key, param order was switched in function def to preserve backwards compatability
66
+ return redisCache.get(context);
67
+ }
68
+
69
+ const controller = new AbortController();
70
+ if (timeout) {
71
+ const result = await Promise.race([
72
+ redisCache.get(key),
73
+ waitTimeout(timeout, false, { signal: controller.signal, ref: false })
74
+ ]);
75
+
76
+ if (!result) {
77
+ context.log.info('CACHE TIMEOUT ERROR');
78
+ return Promise.reject(new Error('Timed out attempting to retrieve item from cache.'));
79
+ }
80
+
81
+ controller.abort();
82
+ return result;
83
+ }
84
+ return redisCache.get(key);
85
+ },
86
+
87
+ /**
88
+ *
89
+ * @template T
90
+ * @param {string} key
91
+ * @param {T} value
92
+ * @param {number} [ttl]
93
+ * @returns {Promise<T>}
94
+ */
95
+ async setItem(key, value, ttl) {
96
+ await redisCache.set(key, value, {
97
+ ttl: ttl || Number(process.env.REDIS_CACHE_TTL)
98
+ });
99
+
100
+ return value;
101
+ },
102
+
103
+ /**
104
+ * Set the item if it does not exist yet. If the item already exists, do not set and return null.
105
+ * @template T
106
+ * @param {String} key
107
+ * @param {T} value
108
+ * @param {Number} ttl
109
+ * @returns {Promise<T|null>}
110
+ */
111
+ async setItemIfNotExists(key, value, ttl) {
112
+ const ttlToSet = ttl ?? (process.env.REDIS_CACHE_TTL
113
+ ? Number(process.env.REDIS_CACHE_TTL)
114
+ : DEFAULT_TTL);
115
+
116
+ return new Promise((resolve, reject) => {
117
+ redisClient.set(key, value, 'NX', 'EX', ttlToSet, (error, result) => {
118
+ if (error) {
119
+ return reject(error);
120
+ }
121
+
122
+ resolve(result === 'OK' ? value : null);
123
+ });
124
+ });
125
+ },
126
+
127
+ /**
128
+ *
129
+ * @param {string} key
130
+ * @param {any} value
131
+ */
132
+ addSetItem(key, value) {
133
+ return this.processRedisCommands(client => client.sadd(key, value));
134
+ },
135
+
136
+ /**
137
+ *
138
+ * @param {string} key
139
+ * @param {any} value
140
+ */
141
+ removeSetItem(key, value) {
142
+ return this.processRedisCommands(client => client.srem(key, value));
143
+ },
144
+
145
+ /**
146
+ *
147
+ * @param {string} key
148
+ * @param {any} value
149
+ * @returns {Promise<Boolean>}
150
+ */
151
+ isSetMember(key, value) {
152
+ return this.processRedisCommands(async client => new Promise((res, rej) => {
153
+ client.sismember(key, value, (ex, result) => {
154
+ if (ex) return rej(ex);
155
+ res(result === 1);
156
+ });
157
+ }));
158
+ },
159
+
160
+ /**
161
+ * @returns {Promise<true>}
162
+ */
163
+ async healthCheck() {
164
+ return this.processRedisCommands(client => client && client.server_info.loading === '0');
165
+ },
166
+
167
+ close() {
168
+ return new Promise(res => {
169
+ redisClient.removeAllListeners();
170
+ redisClient.end(true);
171
+ res();
172
+ });
173
+ },
174
+
175
+ getClient() {
176
+ return new Promise((resolve, reject) => {
177
+ if (redisClient.connected) {
178
+ return resolve(redisClient);
179
+ }
180
+
181
+ redisClient.on('ready', () => resolve(redisClient));
182
+ redisClient.on('error', (e) => reject(e));
183
+ });
184
+ },
185
+
186
+ /**
187
+ *
188
+ * @param {(client: any) => any} fn
189
+ */
190
+ async processRedisCommands(fn) {
191
+ const client = await this.getClient();
192
+ const result = await fn(client);
193
+
194
+ return result;
195
+ },
196
+
197
+ /**
198
+ *
199
+ * @param {Context} context
200
+ * @param {string | RegExp} pattern
201
+ */
202
+ flushCache(context, pattern) {
203
+ return this.processRedisCommands(async (client) => {
204
+ const scanAsync = util.promisify(client.scan).bind(client);
205
+ const delAsync = util.promisify(client.del).bind(client);
206
+ let cursor = '0';
207
+ do {
208
+ const result = await scanAsync(cursor, 'MATCH', pattern);
209
+ ([cursor] = result);
210
+ const keys = result[1];
211
+ context.log.info('matched keys', {
212
+ keys: result[1]
213
+ });
214
+ if (keys.length > 0) {
215
+ await delAsync(...keys);
216
+ }
217
+ } while (cursor !== '0');
218
+ });
219
+ },
220
+ redisClient,
221
+ redisCache
222
+ };
223
+
224
+ health.add(`redis-${instance.id}`, async () => {
225
+ try {
226
+ const controller = new AbortController();
227
+
228
+ const result = await Promise.race([
229
+ instance.healthCheck(),
230
+ waitTimeout(3000, false, { signal: controller.signal, ref: false })
231
+ ]);
232
+
233
+ if (!result) {
234
+ throw new Error('Timed out waiting to verify health status');
235
+ }
236
+
237
+ controller.abort();
238
+ return { status: 200, message: 'OK' };
239
+ } catch (ex) { // eslint-disable-line no-unused-vars
240
+ return { status: 503, message: 'Error connecting to Redis' };
241
+ }
242
+ }, { promotionTimeout: 60000 });
243
+
244
+ service.registerResource(`exframe-cache-manager-${instance.id}`, { onSignal: () => instance.close(), order: 'last' });
245
+
246
+ Object.keys(instance).forEach((key) => {
247
+ if (typeof instance[key] === 'function') {
248
+ instance[key] = lazyInstrument(instance[key].bind(instance), { metricPrefix: 'cache' });
249
+ }
250
+ });
251
+
252
+ return instance;
253
+ }
254
+
255
+ // eslint-disable-next-line import/prefer-default-export
256
+ export { cachemanager };
257
+
258
+ /**
259
+ * @typedef {RedisCacheManagerOptions & { store?: any, ttl?: number, db?: number }} Options
260
+ */
261
+
262
+ /**
263
+ * @typedef {{ db?: number, url?: string, host?: string, port?: string }} RedisCacheManagerOptions
264
+ */
265
+
266
+ /**
267
+ * @typedef {{ [key: string]: any, log: { [key: string]: LogLevel, info: LogLevel, error: LogLevel } }} Context
268
+ */
269
+
270
+ /**
271
+ * @typedef {(message: string, meta: { [key: string]: any }) => void} LogLevel
272
+ */
@@ -6,7 +6,7 @@ import service from 'exframe-service';
6
6
  import health from 'exframe-health';
7
7
  import { setTimeout as waitTimeout } from 'node:timers/promises';
8
8
 
9
- import { create } from '../index.js';
9
+ import cacheManager from '../index.mjs';
10
10
 
11
11
  const { prometheusClient } = service;
12
12
  const userId = 'auth0|1234567890';
@@ -36,7 +36,7 @@ context('Test User Profile Middleware', () => {
36
36
  it('adds a health check with the instance ID in the name, check function, and liveness timeout of 60 seconds', () => {
37
37
  const healthAddSpy = sinonInstance.spy(health, 'add');
38
38
 
39
- const instance = create(options);
39
+ const instance = cacheManager.create(options);
40
40
  expect(healthAddSpy.calledOnce).to.equal(true);
41
41
  expect(healthAddSpy.calledWithMatch(`redis-${instance.id}`, match.func, { promotionTimeout: 60000 })).to.be.true;
42
42
  });
@@ -44,7 +44,7 @@ context('Test User Profile Middleware', () => {
44
44
  it('registers a service resource with the instance ID in the name, onSignal function, and order of "last"', () => {
45
45
  const registerSpy = sinonInstance.spy(service, 'registerResource');
46
46
 
47
- const instance = create(options);
47
+ const instance = cacheManager.create(options);
48
48
  expect(registerSpy.calledOnce).to.equal(true);
49
49
  expect(registerSpy.calledWithMatch(`exframe-cache-manager-${instance.id}`, match({
50
50
  onSignal: match.func,
@@ -54,7 +54,7 @@ context('Test User Profile Middleware', () => {
54
54
  });
55
55
 
56
56
  describe('instance', () => {
57
- const app = create(options);
57
+ const app = cacheManager.create(options);
58
58
 
59
59
  beforeEach(() => {
60
60
  prometheusClient.register.clear();
@@ -178,7 +178,7 @@ context('Test User Profile Middleware', () => {
178
178
  });
179
179
 
180
180
  it('close drains the redis pool', async () => {
181
- const app1 = create(options);
181
+ const app1 = cacheManager.create(options);
182
182
  await app1.setItem(cacheKey, 'someValue');
183
183
  const value = await app1.getItem(cacheKey);
184
184
  expect(value).to.eql('someValue');