@prairielearn/cache 1.0.0
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/.turbo/turbo-build.log +0 -0
- package/README.md +57 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +184 -0
- package/dist/index.js.map +1 -0
- package/package.json +20 -0
- package/src/index.ts +178 -0
- package/tsconfig.json +8 -0
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# `@prairielearn/cache`
|
|
2
|
+
|
|
3
|
+
Utilities to help connect to and store information in a cache. This package _does not_ load configurations directly. Instead, configs should be passed in when the package is initialized upon loading the application. Then, the package can be used in the throughout the application to interact with the cache.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
First, you will need to initialize the library with the cache type that you are intending to use, a prefix that will be the start of all of your cache keys, and optionally, a redis server URL if that is the cache type being used:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { cache } from '@prairielearn/cache';
|
|
11
|
+
import { config } from 'lib/config.js';
|
|
12
|
+
|
|
13
|
+
await cache.init({
|
|
14
|
+
type: config.cacheType,
|
|
15
|
+
keyPrefix: config.cacheKeyPrefix,
|
|
16
|
+
redisUrl: config.redisUrl,
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
In this example, we are using a config file that has stored the cache configurations and we are passing those in to initialize our cache.
|
|
21
|
+
|
|
22
|
+
Additionally, the cache package can be used as a class to construct multiple instances. To do so, you must first contsruct a new cache. You can then pass is your arguments and initalize a new cache:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { cache } from '@prairielearn/cache';
|
|
26
|
+
|
|
27
|
+
const myCache = new cache();
|
|
28
|
+
|
|
29
|
+
myCache.init({
|
|
30
|
+
cachetype: 'redis',
|
|
31
|
+
cacheKeyPrefix: 'prairielearn-cache2:',
|
|
32
|
+
redisUrl: 'redis://localhost:6379/',
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
After initializing, we can use our `set`, `get`, `del`, `reset` or `close` functions to interact with the cache. Note, that `set`, `get`, and `del` have required arguments. Calling `set` will require the intended KEY, VALUE, and length of time to store the data (in milliseconds). Calling `get` or `del` will require the KEY for the intended result.
|
|
37
|
+
|
|
38
|
+
The following example will store `foo: bar` for 10 minutes:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
await cache.set('foo', 'bar', 600000);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The following example will use the key `foo` to retrieve the value `bar`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
await cache.get('foo');
|
|
48
|
+
// returns bar
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The following example will use the key `foo` to delete the key value pair `foo: bar`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
await cache.del('foo');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Using `reset()` will clear the currently stored data in the cache. Using `close()` will disable the currently used cache and, if using Redis, close the connection.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Redis } from 'ioredis';
|
|
2
|
+
import { LRUCache } from 'lru-cache';
|
|
3
|
+
declare class Cache {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
type: string;
|
|
6
|
+
memoryCache?: LRUCache<string, string>;
|
|
7
|
+
redisClient?: Redis;
|
|
8
|
+
keyPrefix: string;
|
|
9
|
+
init(config: {
|
|
10
|
+
type: 'none' | 'memory' | 'redis';
|
|
11
|
+
keyPrefix: string;
|
|
12
|
+
redisUrl?: string | null;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
set(key: string, value: any, maxAgeMS: number): void;
|
|
15
|
+
del(key: string): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the value for the corresponding key if it exists in the cache; null otherwise.
|
|
18
|
+
*/
|
|
19
|
+
get(key: string): Promise<any>;
|
|
20
|
+
/**
|
|
21
|
+
* Clear all entries from the cache.
|
|
22
|
+
*/
|
|
23
|
+
reset(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Releases any connections associated with the cache.
|
|
26
|
+
*/
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare const cache: Cache;
|
|
30
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.cache = void 0;
|
|
30
|
+
const ioredis_1 = require("ioredis");
|
|
31
|
+
const lru_cache_1 = require("lru-cache");
|
|
32
|
+
const logger_1 = require("@prairielearn/logger");
|
|
33
|
+
const Sentry = __importStar(require("@prairielearn/sentry"));
|
|
34
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
35
|
+
class Cache {
|
|
36
|
+
enabled = false;
|
|
37
|
+
type = 'none';
|
|
38
|
+
memoryCache;
|
|
39
|
+
redisClient;
|
|
40
|
+
keyPrefix = '';
|
|
41
|
+
async init(config) {
|
|
42
|
+
this.type = config.type;
|
|
43
|
+
this.keyPrefix = config.keyPrefix;
|
|
44
|
+
if (!this.type || this.type === 'none') {
|
|
45
|
+
// No caching
|
|
46
|
+
this.enabled = false;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (this.type === 'redis') {
|
|
50
|
+
if (!config.redisUrl)
|
|
51
|
+
throw new Error('redisUrl not set in config');
|
|
52
|
+
this.enabled = true;
|
|
53
|
+
this.redisClient = new ioredis_1.Redis(config.redisUrl);
|
|
54
|
+
this.redisClient.on('error', (err) => {
|
|
55
|
+
logger_1.logger.error('Redis error', err);
|
|
56
|
+
Sentry.captureException(err);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else if (this.type === 'memory') {
|
|
60
|
+
this.enabled = true;
|
|
61
|
+
this.memoryCache = new lru_cache_1.LRUCache({
|
|
62
|
+
// The in-memory cache is really only suited for development, so we'll
|
|
63
|
+
// hardcode a relatively low limit here.
|
|
64
|
+
max: 1000,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
throw new Error(`Unknown cache type "${this.type}"`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
set(key, value, maxAgeMS) {
|
|
72
|
+
if (!this.enabled)
|
|
73
|
+
return;
|
|
74
|
+
const scopedKey = this.keyPrefix + key;
|
|
75
|
+
switch (this.type) {
|
|
76
|
+
case 'memory': {
|
|
77
|
+
(0, node_assert_1.default)(this.memoryCache, 'Memory cache is enabled but not configured');
|
|
78
|
+
this.memoryCache.set(scopedKey, JSON.stringify(value), { ttl: maxAgeMS });
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'redis': {
|
|
82
|
+
// This returns a promise, but we don't want to wait for this data
|
|
83
|
+
// to reach the cache before continuing, and we don't *really*
|
|
84
|
+
// care if it errors.
|
|
85
|
+
//
|
|
86
|
+
// We don't log the error because it contains the cached value,
|
|
87
|
+
// which can be huge and which fills up the logs.
|
|
88
|
+
(0, node_assert_1.default)(this.redisClient, 'Redis client is enabled but not configured');
|
|
89
|
+
this.redisClient
|
|
90
|
+
.set(scopedKey, JSON.stringify(value), 'PX', maxAgeMS)
|
|
91
|
+
.catch((_err) => logger_1.logger.error('Cache set error', { key, scopedKey, maxAgeMS }));
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async del(key) {
|
|
97
|
+
if (!this.enabled)
|
|
98
|
+
return;
|
|
99
|
+
const scopedKey = this.keyPrefix + key;
|
|
100
|
+
switch (this.type) {
|
|
101
|
+
case 'memory': {
|
|
102
|
+
(0, node_assert_1.default)(this.memoryCache, 'Memory cache is enabled but not configured');
|
|
103
|
+
this.memoryCache.delete(scopedKey);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'redis': {
|
|
107
|
+
(0, node_assert_1.default)(this.redisClient, 'Redis client is enabled but not configured');
|
|
108
|
+
await this.redisClient.del(scopedKey);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Returns the value for the corresponding key if it exists in the cache; null otherwise.
|
|
115
|
+
*/
|
|
116
|
+
async get(key) {
|
|
117
|
+
if (!this.enabled)
|
|
118
|
+
return null;
|
|
119
|
+
const scopedKey = this.keyPrefix + key;
|
|
120
|
+
switch (this.type) {
|
|
121
|
+
case 'memory': {
|
|
122
|
+
(0, node_assert_1.default)(this.memoryCache, 'Memory cache is enabled but not configured');
|
|
123
|
+
const value = this.memoryCache.get(scopedKey);
|
|
124
|
+
if (typeof value === 'string') {
|
|
125
|
+
return JSON.parse(value);
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
case 'redis': {
|
|
130
|
+
(0, node_assert_1.default)(this.redisClient, 'Redis client is enabled but not configured');
|
|
131
|
+
const value = await this.redisClient.get(scopedKey);
|
|
132
|
+
if (typeof value === 'string') {
|
|
133
|
+
return JSON.parse(value);
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
default: {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Clear all entries from the cache.
|
|
144
|
+
*/
|
|
145
|
+
async reset() {
|
|
146
|
+
if (!this.enabled)
|
|
147
|
+
return;
|
|
148
|
+
switch (this.type) {
|
|
149
|
+
case 'memory': {
|
|
150
|
+
(0, node_assert_1.default)(this.memoryCache, 'Memory cache is enabled but not configured');
|
|
151
|
+
this.memoryCache.clear();
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'redis': {
|
|
155
|
+
let cursor = '0';
|
|
156
|
+
do {
|
|
157
|
+
(0, node_assert_1.default)(this.redisClient, 'Redis client is enabled but not configured');
|
|
158
|
+
const reply = await this.redisClient.scan(cursor, 'MATCH', `${this.keyPrefix}*`, 'COUNT', 1000);
|
|
159
|
+
cursor = reply[0];
|
|
160
|
+
const keys = reply[1];
|
|
161
|
+
if (keys.length > 0) {
|
|
162
|
+
(0, node_assert_1.default)(this.redisClient, 'Redis client is enabled but not configured');
|
|
163
|
+
await this.redisClient.del(keys);
|
|
164
|
+
}
|
|
165
|
+
} while (cursor !== '0');
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Releases any connections associated with the cache.
|
|
172
|
+
*/
|
|
173
|
+
async close() {
|
|
174
|
+
if (!this.enabled)
|
|
175
|
+
return;
|
|
176
|
+
this.enabled = false;
|
|
177
|
+
if (this.type === 'redis') {
|
|
178
|
+
(0, node_assert_1.default)(this.redisClient, 'Redis client is enabled but not configured');
|
|
179
|
+
await this.redisClient.quit();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
exports.cache = new Cache();
|
|
184
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@prairielearn/cache",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"dev": "tsc --watch --preserveWatchOutput"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@prairielearn/logger": "^1.0.12",
|
|
11
|
+
"@prairielearn/sentry": "^1.2.1",
|
|
12
|
+
"ioredis": "^5.3.2",
|
|
13
|
+
"lru-cache": "^10.2.0",
|
|
14
|
+
"zod": "^3.22.4"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@prairielearn/tsconfig": "*",
|
|
18
|
+
"typescript": "^5.3.3"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Redis } from 'ioredis';
|
|
2
|
+
import { LRUCache } from 'lru-cache';
|
|
3
|
+
import { logger } from '@prairielearn/logger';
|
|
4
|
+
import * as Sentry from '@prairielearn/sentry';
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
class Cache {
|
|
8
|
+
enabled = false;
|
|
9
|
+
type = 'none';
|
|
10
|
+
memoryCache?: LRUCache<string, string>;
|
|
11
|
+
redisClient?: Redis;
|
|
12
|
+
keyPrefix = '';
|
|
13
|
+
|
|
14
|
+
async init(config: {
|
|
15
|
+
type: 'none' | 'memory' | 'redis';
|
|
16
|
+
keyPrefix: string;
|
|
17
|
+
redisUrl?: string | null;
|
|
18
|
+
}) {
|
|
19
|
+
this.type = config.type;
|
|
20
|
+
this.keyPrefix = config.keyPrefix;
|
|
21
|
+
if (!this.type || this.type === 'none') {
|
|
22
|
+
// No caching
|
|
23
|
+
this.enabled = false;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (this.type === 'redis') {
|
|
28
|
+
if (!config.redisUrl) throw new Error('redisUrl not set in config');
|
|
29
|
+
this.enabled = true;
|
|
30
|
+
this.redisClient = new Redis(config.redisUrl);
|
|
31
|
+
this.redisClient.on('error', (err) => {
|
|
32
|
+
logger.error('Redis error', err);
|
|
33
|
+
Sentry.captureException(err);
|
|
34
|
+
});
|
|
35
|
+
} else if (this.type === 'memory') {
|
|
36
|
+
this.enabled = true;
|
|
37
|
+
this.memoryCache = new LRUCache({
|
|
38
|
+
// The in-memory cache is really only suited for development, so we'll
|
|
39
|
+
// hardcode a relatively low limit here.
|
|
40
|
+
max: 1000,
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(`Unknown cache type "${this.type}"`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
set(key: string, value: any, maxAgeMS: number) {
|
|
48
|
+
if (!this.enabled) return;
|
|
49
|
+
|
|
50
|
+
const scopedKey = this.keyPrefix + key;
|
|
51
|
+
|
|
52
|
+
switch (this.type) {
|
|
53
|
+
case 'memory': {
|
|
54
|
+
assert(this.memoryCache, 'Memory cache is enabled but not configured');
|
|
55
|
+
this.memoryCache.set(scopedKey, JSON.stringify(value), { ttl: maxAgeMS });
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case 'redis': {
|
|
60
|
+
// This returns a promise, but we don't want to wait for this data
|
|
61
|
+
// to reach the cache before continuing, and we don't *really*
|
|
62
|
+
// care if it errors.
|
|
63
|
+
//
|
|
64
|
+
// We don't log the error because it contains the cached value,
|
|
65
|
+
// which can be huge and which fills up the logs.
|
|
66
|
+
assert(this.redisClient, 'Redis client is enabled but not configured');
|
|
67
|
+
this.redisClient
|
|
68
|
+
.set(scopedKey, JSON.stringify(value), 'PX', maxAgeMS)
|
|
69
|
+
.catch((_err) => logger.error('Cache set error', { key, scopedKey, maxAgeMS }));
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async del(key: string) {
|
|
76
|
+
if (!this.enabled) return;
|
|
77
|
+
|
|
78
|
+
const scopedKey = this.keyPrefix + key;
|
|
79
|
+
|
|
80
|
+
switch (this.type) {
|
|
81
|
+
case 'memory': {
|
|
82
|
+
assert(this.memoryCache, 'Memory cache is enabled but not configured');
|
|
83
|
+
this.memoryCache.delete(scopedKey);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case 'redis': {
|
|
88
|
+
assert(this.redisClient, 'Redis client is enabled but not configured');
|
|
89
|
+
await this.redisClient.del(scopedKey);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns the value for the corresponding key if it exists in the cache; null otherwise.
|
|
97
|
+
*/
|
|
98
|
+
async get(key: string): Promise<any> {
|
|
99
|
+
if (!this.enabled) return null;
|
|
100
|
+
|
|
101
|
+
const scopedKey = this.keyPrefix + key;
|
|
102
|
+
|
|
103
|
+
switch (this.type) {
|
|
104
|
+
case 'memory': {
|
|
105
|
+
assert(this.memoryCache, 'Memory cache is enabled but not configured');
|
|
106
|
+
const value = this.memoryCache.get(scopedKey);
|
|
107
|
+
if (typeof value === 'string') {
|
|
108
|
+
return JSON.parse(value);
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'redis': {
|
|
114
|
+
assert(this.redisClient, 'Redis client is enabled but not configured');
|
|
115
|
+
const value = await this.redisClient.get(scopedKey);
|
|
116
|
+
if (typeof value === 'string') {
|
|
117
|
+
return JSON.parse(value);
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
default: {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Clear all entries from the cache.
|
|
130
|
+
*/
|
|
131
|
+
async reset() {
|
|
132
|
+
if (!this.enabled) return;
|
|
133
|
+
|
|
134
|
+
switch (this.type) {
|
|
135
|
+
case 'memory': {
|
|
136
|
+
assert(this.memoryCache, 'Memory cache is enabled but not configured');
|
|
137
|
+
this.memoryCache.clear();
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case 'redis': {
|
|
142
|
+
let cursor = '0';
|
|
143
|
+
do {
|
|
144
|
+
assert(this.redisClient, 'Redis client is enabled but not configured');
|
|
145
|
+
const reply = await this.redisClient.scan(
|
|
146
|
+
cursor,
|
|
147
|
+
'MATCH',
|
|
148
|
+
`${this.keyPrefix}*`,
|
|
149
|
+
'COUNT',
|
|
150
|
+
1000,
|
|
151
|
+
);
|
|
152
|
+
cursor = reply[0];
|
|
153
|
+
|
|
154
|
+
const keys = reply[1];
|
|
155
|
+
if (keys.length > 0) {
|
|
156
|
+
assert(this.redisClient, 'Redis client is enabled but not configured');
|
|
157
|
+
await this.redisClient.del(keys);
|
|
158
|
+
}
|
|
159
|
+
} while (cursor !== '0');
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Releases any connections associated with the cache.
|
|
166
|
+
*/
|
|
167
|
+
async close() {
|
|
168
|
+
if (!this.enabled) return;
|
|
169
|
+
this.enabled = false;
|
|
170
|
+
|
|
171
|
+
if (this.type === 'redis') {
|
|
172
|
+
assert(this.redisClient, 'Redis client is enabled but not configured');
|
|
173
|
+
await this.redisClient.quit();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const cache = new Cache();
|