@tsed/platform-cache 8.0.0 → 8.0.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/package.json +12 -11
- package/src/decorators/useCache.ts +8 -0
- package/src/index.ts +11 -0
- package/src/interceptors/PlatformCacheInterceptor.spec.ts +650 -0
- package/src/interceptors/PlatformCacheInterceptor.ts +235 -0
- package/src/interfaces/PlatformCacheOptions.ts +27 -0
- package/src/interfaces/PlatformCachedObject.ts +7 -0
- package/src/interfaces/interfaces.ts +34 -0
- package/src/services/PlatformCache.spec.ts +363 -0
- package/src/services/PlatformCache.ts +204 -0
- package/src/utils/getPrefix.ts +13 -0
- package/src/utils/isEndpoint.ts +6 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import {isClass, isString, nameOf} from "@tsed/core";
|
|
2
|
+
import {BaseContext, Constant, DIContext, Inject, Interceptor, InterceptorContext, InterceptorMethods, InterceptorNext} from "@tsed/di";
|
|
3
|
+
import {deserialize, serialize} from "@tsed/json-mapper";
|
|
4
|
+
import {Logger} from "@tsed/logger";
|
|
5
|
+
import {IncomingMessage, ServerResponse} from "http";
|
|
6
|
+
|
|
7
|
+
import {PlatformCachedObject} from "../interfaces/PlatformCachedObject.js";
|
|
8
|
+
import {PlatformCacheOptions} from "../interfaces/PlatformCacheOptions.js";
|
|
9
|
+
import {PlatformCache} from "../services/PlatformCache.js";
|
|
10
|
+
import {getPrefix} from "../utils/getPrefix.js";
|
|
11
|
+
import {isEndpoint} from "../utils/isEndpoint.js";
|
|
12
|
+
const cleanHeaders = (headers: Record<string, unknown>, blacklist: string[]) => {
|
|
13
|
+
return Object.entries(headers)
|
|
14
|
+
.filter(([key]) => !blacklist.includes(key.toLowerCase()))
|
|
15
|
+
.reduce((headers, [key, value]) => {
|
|
16
|
+
return {
|
|
17
|
+
...headers,
|
|
18
|
+
[key]: value
|
|
19
|
+
};
|
|
20
|
+
}, {});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @platform
|
|
25
|
+
*/
|
|
26
|
+
@Interceptor()
|
|
27
|
+
export class PlatformCacheInterceptor implements InterceptorMethods {
|
|
28
|
+
@Inject()
|
|
29
|
+
protected cache: PlatformCache;
|
|
30
|
+
|
|
31
|
+
@Inject()
|
|
32
|
+
protected logger: Logger;
|
|
33
|
+
|
|
34
|
+
@Constant("cache.prefix", "")
|
|
35
|
+
protected prefix: string;
|
|
36
|
+
|
|
37
|
+
@Constant("cache.ignoreHeaders", ["content-length", "x-request-id", "cache-control", "vary", "content-encoding"])
|
|
38
|
+
protected blacklist: string[];
|
|
39
|
+
|
|
40
|
+
intercept(context: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext) {
|
|
41
|
+
if (this.cache.disabled()) {
|
|
42
|
+
return next();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!isEndpoint(context.target, context.propertyKey)) {
|
|
46
|
+
return this.cacheMethod(context, next);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.cacheResponse(context, next);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async canRefreshInBackground(
|
|
53
|
+
key: string,
|
|
54
|
+
{
|
|
55
|
+
refreshThreshold,
|
|
56
|
+
ttl
|
|
57
|
+
}: {
|
|
58
|
+
refreshThreshold?: number;
|
|
59
|
+
ttl: any;
|
|
60
|
+
},
|
|
61
|
+
next: Function
|
|
62
|
+
) {
|
|
63
|
+
const inQueue = await this.hasKeyInQueue(key);
|
|
64
|
+
|
|
65
|
+
if (refreshThreshold && !inQueue) {
|
|
66
|
+
await this.addKeyToQueue(key);
|
|
67
|
+
|
|
68
|
+
const currentTTL = await this.cache.ttl(key);
|
|
69
|
+
const calculatedTTL = this.cache.calculateTTL(currentTTL, ttl);
|
|
70
|
+
|
|
71
|
+
if (currentTTL === undefined || currentTTL < calculatedTTL - refreshThreshold) {
|
|
72
|
+
await next();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await this.deleteKeyFromQueue(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async cacheMethod(context: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext) {
|
|
80
|
+
const {key, type, ttl, collectionType, refreshThreshold, args, canCache} = this.getOptions(context);
|
|
81
|
+
|
|
82
|
+
const set = (result: any) => {
|
|
83
|
+
if (!canCache || (canCache && canCache(result))) {
|
|
84
|
+
const calculatedTTL = this.cache.calculateTTL(result, ttl);
|
|
85
|
+
const data = serialize(result, {type, collectionType});
|
|
86
|
+
this.cache.setCachedObject(key, data, {args, ttl: calculatedTTL});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const cachedObject = await this.cache.getCachedObject(key);
|
|
91
|
+
|
|
92
|
+
if (!cachedObject || this.cache.isForceRefresh()) {
|
|
93
|
+
const result = await next();
|
|
94
|
+
|
|
95
|
+
if (!(!cachedObject && this.cache.isForceRefresh())) {
|
|
96
|
+
set(result);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.canRefreshInBackground(key, {refreshThreshold, ttl}, async () => {
|
|
103
|
+
const result = await next();
|
|
104
|
+
|
|
105
|
+
await set(result);
|
|
106
|
+
}).catch((er) =>
|
|
107
|
+
this.logger.error({
|
|
108
|
+
event: "CACHE_ERROR",
|
|
109
|
+
method: "cacheMethod",
|
|
110
|
+
concerned_key: key,
|
|
111
|
+
class_name: nameOf(context.target),
|
|
112
|
+
property_key: context.propertyKey,
|
|
113
|
+
error_description: er.message,
|
|
114
|
+
stack: er.stack
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const {data} = cachedObject;
|
|
119
|
+
|
|
120
|
+
return deserialize(JSON.parse(data), {collectionType, type});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async cacheResponse(context: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext) {
|
|
124
|
+
const {request, response} = context.args[context.args.length - 1];
|
|
125
|
+
|
|
126
|
+
if (request.method !== "GET") {
|
|
127
|
+
return next();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const {key, ttl, args, $ctx} = this.getOptions(context);
|
|
131
|
+
|
|
132
|
+
const cachedObject = await this.cache.getCachedObject(key);
|
|
133
|
+
|
|
134
|
+
if (cachedObject && !(request.get("cache-control") === "no-cache")) {
|
|
135
|
+
return this.sendResponse(cachedObject, $ctx);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = await next();
|
|
139
|
+
|
|
140
|
+
const calculatedTTL = this.cache.calculateTTL(result, ttl);
|
|
141
|
+
|
|
142
|
+
$ctx.response.setHeaders({
|
|
143
|
+
"cache-control": `max-age=${calculatedTTL}`
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// cache final response with his headers and body
|
|
147
|
+
response.onEnd(() => {
|
|
148
|
+
this.cache.setCachedObject(key, response.getBody(), {
|
|
149
|
+
ttl: calculatedTTL,
|
|
150
|
+
args,
|
|
151
|
+
headers: cleanHeaders(response.getHeaders(), this.blacklist)
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
protected getArgs(context: InterceptorContext<unknown, PlatformCacheOptions>) {
|
|
159
|
+
return context.args.reduce((args, arg) => {
|
|
160
|
+
if (arg instanceof DIContext || arg instanceof IncomingMessage || arg instanceof ServerResponse) {
|
|
161
|
+
return args;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (isClass(arg)) {
|
|
165
|
+
return args.concat(serialize(arg));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return args.concat(arg);
|
|
169
|
+
}, []);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
protected getOptions(context: InterceptorContext<any, PlatformCacheOptions>) {
|
|
173
|
+
const $ctx = context.args[context.args.length - 1];
|
|
174
|
+
|
|
175
|
+
const {ttl, type, collectionType, key: k = this.cache.defaultKeyResolver(), refreshThreshold} = context.options || {};
|
|
176
|
+
|
|
177
|
+
let {canCache} = context.options || {};
|
|
178
|
+
|
|
179
|
+
const args = this.getArgs(context);
|
|
180
|
+
const keyArgs = isString(k) ? k : k(args, $ctx);
|
|
181
|
+
|
|
182
|
+
if (canCache && canCache === "no-nullish") {
|
|
183
|
+
canCache = (item: any) => ![null, undefined].includes(item);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
key: [...[this.prefix, ...getPrefix(context.target, context.propertyKey)].filter(Boolean), keyArgs].join(":"),
|
|
188
|
+
refreshThreshold,
|
|
189
|
+
ttl,
|
|
190
|
+
type,
|
|
191
|
+
args,
|
|
192
|
+
collectionType,
|
|
193
|
+
keyArgs,
|
|
194
|
+
canCache,
|
|
195
|
+
$ctx
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
protected async hasKeyInQueue(key: string) {
|
|
200
|
+
return !!(await this.cache.get(`$$queue:${key}`));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
protected async addKeyToQueue(key: string) {
|
|
204
|
+
await this.cache.set(`$$queue:${key}`, true, {ttl: 120});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
protected async deleteKeyFromQueue(key: string) {
|
|
208
|
+
await this.cache.del(`$$queue:${key}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
protected sendResponse(cachedObject: PlatformCachedObject, $ctx: BaseContext) {
|
|
212
|
+
const {headers, ttl} = cachedObject;
|
|
213
|
+
const {request, response} = $ctx;
|
|
214
|
+
|
|
215
|
+
const requestEtag = request.get("if-none-match");
|
|
216
|
+
|
|
217
|
+
if (requestEtag && headers.etag === requestEtag) {
|
|
218
|
+
response.status(304).setHeaders(headers).body(undefined);
|
|
219
|
+
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const data = JSON.parse(cachedObject.data);
|
|
224
|
+
|
|
225
|
+
$ctx.response
|
|
226
|
+
.setHeaders({
|
|
227
|
+
...headers,
|
|
228
|
+
"x-cached": "true",
|
|
229
|
+
"cache-control": `max-age=${ttl}`
|
|
230
|
+
})
|
|
231
|
+
.body(data);
|
|
232
|
+
|
|
233
|
+
return data;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {MetadataTypes} from "@tsed/core";
|
|
2
|
+
import {BaseContext} from "@tsed/di";
|
|
3
|
+
|
|
4
|
+
import type {Ttl} from "../services/PlatformCache.js";
|
|
5
|
+
|
|
6
|
+
export interface PlatformCacheOptions extends MetadataTypes {
|
|
7
|
+
/**
|
|
8
|
+
* key expiration ttl in seconds.
|
|
9
|
+
*/
|
|
10
|
+
ttl?: Ttl;
|
|
11
|
+
/**
|
|
12
|
+
* By default, the key is generated from the method name and the arguments passed to the method.
|
|
13
|
+
*/
|
|
14
|
+
prefix?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Algorithm used to generate the key. By default, the key is generated from the method name and the arguments passed to the method.
|
|
17
|
+
*/
|
|
18
|
+
key?: string | ((args: any[], ctx?: BaseContext) => string);
|
|
19
|
+
/**
|
|
20
|
+
* threshold to refresh the cache.
|
|
21
|
+
*/
|
|
22
|
+
refreshThreshold?: number;
|
|
23
|
+
/**
|
|
24
|
+
* The function determine if the result must be cached or not.
|
|
25
|
+
*/
|
|
26
|
+
canCache?: ((item: any) => boolean) | "no-nullish";
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {BaseContext} from "@tsed/di";
|
|
2
|
+
import type {Cache, CachingConfig, Store} from "cache-manager";
|
|
3
|
+
import type {FactoryStore} from "cache-manager/dist/caching.js";
|
|
4
|
+
|
|
5
|
+
import type {CacheManager} from "../services/PlatformCache.js";
|
|
6
|
+
|
|
7
|
+
export type PlatformCacheSettings<Config extends Record<string, any> = any, S extends Store = any> = CachingConfig<Config> & {
|
|
8
|
+
caches?: Cache[];
|
|
9
|
+
store?: "memory" | Store | FactoryStore<S, Config>;
|
|
10
|
+
max?: number;
|
|
11
|
+
keyResolver?: (args: any[], ctx: BaseContext) => string;
|
|
12
|
+
prefix?: string;
|
|
13
|
+
/**
|
|
14
|
+
* List of headers to ignore when caching the response. Default: ["content-length", "x-request-id", "cache-control", "vary", "content-encoding"]
|
|
15
|
+
*/
|
|
16
|
+
ignoreHeaders?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* You may pass in any other arguments these will be passed on to the `create` method of your store,
|
|
19
|
+
* otherwise they will be ignored.
|
|
20
|
+
*/
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
declare global {
|
|
25
|
+
namespace TsED {
|
|
26
|
+
interface Configuration {
|
|
27
|
+
cache?: PlatformCacheSettings | false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OnCreateCacheManager {
|
|
33
|
+
$onCreateCacheManager(cache: CacheManager): void | Promise<void>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
2
|
+
import {caching as cacheManager, multiCaching} from "cache-manager";
|
|
3
|
+
|
|
4
|
+
import {UseCache} from "../decorators/useCache.js";
|
|
5
|
+
import {getPrefix} from "../utils/getPrefix.js";
|
|
6
|
+
import {PlatformCache} from "./PlatformCache.js";
|
|
7
|
+
|
|
8
|
+
function createCacheFixture() {
|
|
9
|
+
const map = new Map();
|
|
10
|
+
return {
|
|
11
|
+
get: vi.fn().mockImplementation((key) => {
|
|
12
|
+
return Promise.resolve(map.get(key));
|
|
13
|
+
}),
|
|
14
|
+
set: vi.fn().mockImplementation((key, value) => {
|
|
15
|
+
return Promise.resolve(map.set(key, value));
|
|
16
|
+
}),
|
|
17
|
+
wrap: vi.fn().mockImplementation((key, fn) => {
|
|
18
|
+
return fn();
|
|
19
|
+
}),
|
|
20
|
+
reset: vi.fn().mockImplementation(() => {
|
|
21
|
+
map.clear();
|
|
22
|
+
}),
|
|
23
|
+
del: vi.fn().mockImplementation((key) => {
|
|
24
|
+
map.delete(key);
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
store: {
|
|
28
|
+
keys: vi.fn().mockImplementation(() => {
|
|
29
|
+
return [...map.keys()];
|
|
30
|
+
}),
|
|
31
|
+
ttl: vi.fn()
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
vi.mock("cache-manager");
|
|
37
|
+
|
|
38
|
+
describe("PlatformCache", () => {
|
|
39
|
+
describe("with legacy single cache", () => {
|
|
40
|
+
let caching: ReturnType<typeof createCacheFixture>;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
caching = createCacheFixture();
|
|
44
|
+
|
|
45
|
+
vi.mocked(cacheManager).mockReturnValue(caching as any);
|
|
46
|
+
|
|
47
|
+
await PlatformTest.create({
|
|
48
|
+
cache: {
|
|
49
|
+
store: {create: () => caching},
|
|
50
|
+
ttl: 300
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
return PlatformTest.reset();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should create single cache", async () => {
|
|
60
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
61
|
+
|
|
62
|
+
expect(cacheManager.disabled()).toEqual(false);
|
|
63
|
+
|
|
64
|
+
await cacheManager.set("key", "value");
|
|
65
|
+
await cacheManager.set("key2", "value2");
|
|
66
|
+
expect(await cacheManager.get("key")).toBe("value");
|
|
67
|
+
expect(await cacheManager.get("key2")).toBe("value2");
|
|
68
|
+
|
|
69
|
+
await cacheManager.del("key2");
|
|
70
|
+
|
|
71
|
+
expect(await cacheManager.get("key2")).toBeUndefined();
|
|
72
|
+
|
|
73
|
+
await cacheManager.reset();
|
|
74
|
+
|
|
75
|
+
expect(await cacheManager.get("key")).toBeUndefined();
|
|
76
|
+
|
|
77
|
+
const result = await cacheManager.wrap("key", () => {
|
|
78
|
+
return Promise.resolve("valuePromised");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(result).toBe("valuePromised");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe("with single cache", () => {
|
|
85
|
+
let caching: ReturnType<typeof createCacheFixture>;
|
|
86
|
+
|
|
87
|
+
beforeEach(async () => {
|
|
88
|
+
caching = createCacheFixture();
|
|
89
|
+
|
|
90
|
+
vi.mocked(cacheManager).mockReturnValue(caching as any);
|
|
91
|
+
|
|
92
|
+
await PlatformTest.create({
|
|
93
|
+
cache: {
|
|
94
|
+
store: "memory",
|
|
95
|
+
ttl: 300
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
return PlatformTest.reset();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should create single cache", async () => {
|
|
105
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
106
|
+
|
|
107
|
+
expect(cacheManager.disabled()).toEqual(false);
|
|
108
|
+
|
|
109
|
+
await cacheManager.set("key", "value");
|
|
110
|
+
await cacheManager.set("key2", "value2");
|
|
111
|
+
expect(await cacheManager.get("key")).toBe("value");
|
|
112
|
+
expect(await cacheManager.get("key2")).toBe("value2");
|
|
113
|
+
|
|
114
|
+
await cacheManager.del("key2");
|
|
115
|
+
|
|
116
|
+
expect(await cacheManager.get("key2")).toBeUndefined();
|
|
117
|
+
|
|
118
|
+
await cacheManager.reset();
|
|
119
|
+
|
|
120
|
+
expect(await cacheManager.get("key")).toBeUndefined();
|
|
121
|
+
|
|
122
|
+
const result = await cacheManager.wrap("key", () => {
|
|
123
|
+
return Promise.resolve("valuePromised");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(result).toBe("valuePromised");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should return the calculated calculateTTL", () => {
|
|
130
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
131
|
+
|
|
132
|
+
expect(cacheManager.calculateTTL({}, 400)).toBe(400);
|
|
133
|
+
expect(cacheManager.calculateTTL({}, () => 200)).toBe(200);
|
|
134
|
+
expect(cacheManager.calculateTTL({})).toBe(300);
|
|
135
|
+
expect(cacheManager.calculateTTL()).toBe(300);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should get matching keys", async () => {
|
|
139
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
140
|
+
|
|
141
|
+
await cacheManager.set("key", "value");
|
|
142
|
+
await cacheManager.set("key2", "value2");
|
|
143
|
+
|
|
144
|
+
const keys1 = await cacheManager.getMatchingKeys("key*");
|
|
145
|
+
const keys2 = await cacheManager.getMatchingKeys("key2");
|
|
146
|
+
|
|
147
|
+
expect(keys1).toEqual(["key", "key2"]);
|
|
148
|
+
expect(keys2).toEqual(["key2"]);
|
|
149
|
+
});
|
|
150
|
+
it("should get keys of a given class/method", async () => {
|
|
151
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
152
|
+
|
|
153
|
+
class Test {
|
|
154
|
+
@UseCache({
|
|
155
|
+
prefix: "TEST"
|
|
156
|
+
})
|
|
157
|
+
async test() {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const prefix = getPrefix(Test, "test");
|
|
161
|
+
|
|
162
|
+
await cacheManager.set(prefix + ":arg", "value");
|
|
163
|
+
await cacheManager.set(prefix + ":arg2", "value2");
|
|
164
|
+
|
|
165
|
+
const keys = await cacheManager.getKeysOf(Test, "test");
|
|
166
|
+
|
|
167
|
+
expect(keys).toEqual(["TEST:arg", "TEST:arg2"]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should delete matching keys (native)", async () => {
|
|
171
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
172
|
+
|
|
173
|
+
await cacheManager.set("key", "value");
|
|
174
|
+
await cacheManager.set("key2", "value2");
|
|
175
|
+
|
|
176
|
+
const keys1 = await cacheManager.keys("key*");
|
|
177
|
+
const keys2 = await cacheManager.deleteKeys("key*");
|
|
178
|
+
|
|
179
|
+
expect(keys1).toEqual(["key", "key2"]);
|
|
180
|
+
expect(keys2).toEqual(["key", "key2"]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should delete matching keys", async () => {
|
|
184
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
185
|
+
|
|
186
|
+
await cacheManager.set("key", "value");
|
|
187
|
+
await cacheManager.set("key2", "value2");
|
|
188
|
+
|
|
189
|
+
const keys1 = await cacheManager.getMatchingKeys("key*");
|
|
190
|
+
const keys2 = await cacheManager.deleteMatchingKeys("key*");
|
|
191
|
+
|
|
192
|
+
expect(keys1).toEqual(["key", "key2"]);
|
|
193
|
+
expect(keys2).toEqual(["key", "key2"]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("defaultKeyResolver()", () => {
|
|
197
|
+
it("return the default key resolver from settings", () => {
|
|
198
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
199
|
+
const keyResolver = cacheManager.defaultKeyResolver();
|
|
200
|
+
|
|
201
|
+
const value = keyResolver(["test"], {});
|
|
202
|
+
expect(value).toEqual("test");
|
|
203
|
+
|
|
204
|
+
const value2 = keyResolver([{test: "test"}], {});
|
|
205
|
+
expect(value2).toEqual('{"test":"test"}');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("ttl()", () => {
|
|
210
|
+
it("return calculateTTL of the given key", async () => {
|
|
211
|
+
const cacheManager: any = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
212
|
+
|
|
213
|
+
cacheManager.cache.store.ttl.mockReturnValue(10);
|
|
214
|
+
|
|
215
|
+
const result = await cacheManager.ttl("test");
|
|
216
|
+
expect(result).toEqual(10);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("return undefined when store isn't defined", async () => {
|
|
220
|
+
const cacheManager: any = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
221
|
+
|
|
222
|
+
delete cacheManager.cache.store.ttl;
|
|
223
|
+
|
|
224
|
+
const result = await cacheManager.ttl("test");
|
|
225
|
+
expect(result).toEqual(undefined);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
describe("getCachedObject()", () => {
|
|
229
|
+
it("should get key from cache", async () => {
|
|
230
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
231
|
+
|
|
232
|
+
await cacheManager.setCachedObject("key", {data: "data"}, {ttl: 10});
|
|
233
|
+
const result = await cacheManager.getCachedObject("key");
|
|
234
|
+
|
|
235
|
+
expect(result).toEqual({
|
|
236
|
+
data: '{"data":"data"}',
|
|
237
|
+
ttl: 10
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
it("should catch and log error", async () => {
|
|
241
|
+
const cacheManager: any = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
242
|
+
|
|
243
|
+
vi.spyOn(cacheManager, "get").mockRejectedValue(new Error("message"));
|
|
244
|
+
vi.spyOn(cacheManager.logger, "error");
|
|
245
|
+
|
|
246
|
+
await cacheManager.getCachedObject("key");
|
|
247
|
+
|
|
248
|
+
expect(cacheManager.get).toHaveBeenCalledWith("key");
|
|
249
|
+
expect(cacheManager.logger.error).toHaveBeenCalledWith({
|
|
250
|
+
error: new Error("message"),
|
|
251
|
+
event: "CACHE_ERROR",
|
|
252
|
+
method: "getCachedObject"
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
describe("setCachedObject()", () => {
|
|
257
|
+
it("should catch and log error", async () => {
|
|
258
|
+
const cacheManager: any = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
259
|
+
|
|
260
|
+
vi.spyOn(cacheManager, "set").mockRejectedValue(new Error("message"));
|
|
261
|
+
vi.spyOn(cacheManager.logger, "error");
|
|
262
|
+
|
|
263
|
+
await cacheManager.setCachedObject("key", {data: "data"}, {ttl: 10});
|
|
264
|
+
|
|
265
|
+
expect(cacheManager.set).toHaveBeenCalledWith("key", {data: '{"data":"data"}', ttl: 10}, {ttl: 10});
|
|
266
|
+
expect(cacheManager.logger.error).toHaveBeenCalledWith({
|
|
267
|
+
error: new Error("message"),
|
|
268
|
+
event: "CACHE_ERROR",
|
|
269
|
+
method: "setCachedObject"
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
describe("refresh()", () => {
|
|
274
|
+
it("should set the call context with forceRefresh to true", async () => {
|
|
275
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
276
|
+
|
|
277
|
+
expect(cacheManager.isForceRefresh()).toEqual(false);
|
|
278
|
+
|
|
279
|
+
await cacheManager.refresh(() => {
|
|
280
|
+
expect(cacheManager.isForceRefresh()).toEqual(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(cacheManager.isForceRefresh()).toEqual(false);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
describe("with multiple cache", () => {
|
|
288
|
+
let caching: ReturnType<typeof createCacheFixture>;
|
|
289
|
+
beforeEach(() => {
|
|
290
|
+
caching = createCacheFixture();
|
|
291
|
+
|
|
292
|
+
vi.mocked(multiCaching).mockReturnValue(caching as any);
|
|
293
|
+
|
|
294
|
+
return PlatformTest.create({
|
|
295
|
+
cache: {
|
|
296
|
+
caches: [{}, {}] as any[],
|
|
297
|
+
ttl: 300
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
afterEach(() => {
|
|
302
|
+
return PlatformTest.reset();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("should create multiple cache", async () => {
|
|
306
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
307
|
+
|
|
308
|
+
await cacheManager.set("key", "value");
|
|
309
|
+
await cacheManager.set("key2", "value2");
|
|
310
|
+
expect(await cacheManager.get("key")).toBe("value");
|
|
311
|
+
expect(await cacheManager.get("key2")).toBe("value2");
|
|
312
|
+
expect(await cacheManager.keys()).toEqual(["key", "key2"]);
|
|
313
|
+
|
|
314
|
+
await cacheManager.del("key2");
|
|
315
|
+
|
|
316
|
+
expect(await cacheManager.get("key2")).toBeUndefined();
|
|
317
|
+
|
|
318
|
+
await cacheManager.reset();
|
|
319
|
+
|
|
320
|
+
expect(await cacheManager.get("key")).toBeUndefined();
|
|
321
|
+
|
|
322
|
+
const result = await cacheManager.wrap("key", () => {
|
|
323
|
+
return Promise.resolve("valuePromised");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(result).toBe("valuePromised");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
describe("with disabled cache", () => {
|
|
330
|
+
beforeEach(() =>
|
|
331
|
+
PlatformTest.create({
|
|
332
|
+
cache: false
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
afterEach(() => PlatformTest.reset());
|
|
336
|
+
|
|
337
|
+
it("should mock methods", async () => {
|
|
338
|
+
const cacheManager = PlatformTest.get<PlatformCache>(PlatformCache);
|
|
339
|
+
|
|
340
|
+
expect(cacheManager.disabled()).toEqual(true);
|
|
341
|
+
|
|
342
|
+
await cacheManager.set("key", "value");
|
|
343
|
+
await cacheManager.set("key2", "value2");
|
|
344
|
+
expect(await cacheManager.get("key")).toBeUndefined();
|
|
345
|
+
expect(await cacheManager.get("key2")).toBeUndefined();
|
|
346
|
+
|
|
347
|
+
await cacheManager.del("key2");
|
|
348
|
+
|
|
349
|
+
expect(await cacheManager.get("key2")).toBeUndefined();
|
|
350
|
+
|
|
351
|
+
await cacheManager.reset();
|
|
352
|
+
|
|
353
|
+
expect(await cacheManager.get("key")).toBeUndefined();
|
|
354
|
+
expect(await cacheManager.keys()).toEqual([]);
|
|
355
|
+
|
|
356
|
+
const result = await cacheManager.wrap("key", () => {
|
|
357
|
+
return Promise.resolve("valuePromised");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(result).toBe("valuePromised");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|