@tsed/platform-cache 8.23.0 → 8.24.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
3
|
import { isClass, isString, nameOf } from "@tsed/core";
|
|
4
|
-
import { Constant, DIContext, Inject, Interceptor } from "@tsed/di";
|
|
4
|
+
import { Constant, context, DIContext, Inject, Interceptor, runInContext } from "@tsed/di";
|
|
5
5
|
import { deserialize, serialize } from "@tsed/json-mapper";
|
|
6
6
|
import { Logger } from "@tsed/logger";
|
|
7
7
|
import { PlatformCache } from "../services/PlatformCache.js";
|
|
@@ -30,20 +30,32 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
|
|
|
30
30
|
}
|
|
31
31
|
return this.cacheResponse(context, next);
|
|
32
32
|
}
|
|
33
|
-
async canRefreshInBackground(key, { refreshThreshold, ttl }, next) {
|
|
33
|
+
async canRefreshInBackground(key, { refreshThreshold, ttl }, next, $ctx) {
|
|
34
34
|
const inQueue = await this.hasKeyInQueue(key);
|
|
35
35
|
if (refreshThreshold && !inQueue) {
|
|
36
36
|
await this.addKeyToQueue(key);
|
|
37
37
|
const currentTTL = await this.cache.ttl(key);
|
|
38
38
|
const calculatedTTL = this.cache.calculateTTL(currentTTL, ttl);
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
try {
|
|
40
|
+
if (currentTTL === undefined || currentTTL < calculatedTTL - refreshThreshold) {
|
|
41
|
+
if ($ctx) {
|
|
42
|
+
await runInContext($ctx, () => next());
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
await next();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await this.deleteKeyFromQueue(key);
|
|
41
51
|
}
|
|
42
|
-
await this.deleteKeyFromQueue(key);
|
|
43
52
|
}
|
|
44
53
|
}
|
|
45
54
|
async cacheMethod(context, next) {
|
|
46
|
-
const { key, type, ttl, collectionType, refreshThreshold, args, canCache } = this.getOptions(context);
|
|
55
|
+
const { key, type, ttl, collectionType, refreshThreshold, args, canCache, $ctx, byPass } = this.getOptions(context, false);
|
|
56
|
+
if (this.shouldByPassCache(byPass, args, $ctx)) {
|
|
57
|
+
return next();
|
|
58
|
+
}
|
|
47
59
|
const set = (result) => {
|
|
48
60
|
if (!canCache || (canCache && canCache(result))) {
|
|
49
61
|
const calculatedTTL = this.cache.calculateTTL(result, ttl);
|
|
@@ -62,7 +74,7 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
|
|
|
62
74
|
this.canRefreshInBackground(key, { refreshThreshold, ttl }, async () => {
|
|
63
75
|
const result = await next();
|
|
64
76
|
await set(result);
|
|
65
|
-
}).catch((er) => this.logger.error({
|
|
77
|
+
}, $ctx).catch((er) => this.logger.error({
|
|
66
78
|
event: "CACHE_ERROR",
|
|
67
79
|
method: "cacheMethod",
|
|
68
80
|
concerned_key: key,
|
|
@@ -74,31 +86,43 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
|
|
|
74
86
|
const { data } = cachedObject;
|
|
75
87
|
return deserialize(JSON.parse(data), { collectionType, type });
|
|
76
88
|
}
|
|
77
|
-
async cacheResponse(
|
|
78
|
-
const {
|
|
79
|
-
|
|
89
|
+
async cacheResponse(interceptorContext, next) {
|
|
90
|
+
const { key, ttl, args, $ctx, byPass } = this.getOptions(interceptorContext, "no-cache");
|
|
91
|
+
const currentCtx = $ctx || context();
|
|
92
|
+
if (this.getMethod(currentCtx) !== "GET") {
|
|
80
93
|
return next();
|
|
81
94
|
}
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
|
|
95
|
+
const shouldByPass = this.shouldByPassCache(byPass, args, currentCtx);
|
|
96
|
+
const useCache = !shouldByPass;
|
|
97
|
+
if (useCache) {
|
|
98
|
+
const cachedObject = await this.cache.getCachedObject(key);
|
|
99
|
+
if (cachedObject) {
|
|
100
|
+
return this.sendResponse(cachedObject);
|
|
101
|
+
}
|
|
86
102
|
}
|
|
87
103
|
const result = await next();
|
|
88
104
|
const calculatedTTL = this.cache.calculateTTL(result, ttl);
|
|
89
|
-
|
|
105
|
+
currentCtx.response?.setHeaders({
|
|
90
106
|
"cache-control": `max-age=${calculatedTTL}`
|
|
91
107
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
currentCtx.response?.onEnd(() => {
|
|
109
|
+
if (!useCache) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.cache.setCachedObject(key, currentCtx.response.getBody(), {
|
|
95
113
|
ttl: calculatedTTL,
|
|
96
114
|
args,
|
|
97
|
-
headers: cleanHeaders(response.getHeaders(), this.blacklist)
|
|
115
|
+
headers: cleanHeaders(currentCtx.response.getHeaders(), this.blacklist)
|
|
98
116
|
});
|
|
99
117
|
});
|
|
100
118
|
return result;
|
|
101
119
|
}
|
|
120
|
+
getMethod($ctx = context()) {
|
|
121
|
+
return $ctx.request?.method;
|
|
122
|
+
}
|
|
123
|
+
noCache($ctx = context()) {
|
|
124
|
+
return $ctx.request?.get("cache-control") === "no-cache" || $ctx.get("cache-control") === "no-cache";
|
|
125
|
+
}
|
|
102
126
|
getArgs(context) {
|
|
103
127
|
return context.args.reduce((args, arg) => {
|
|
104
128
|
if (arg instanceof DIContext || arg instanceof IncomingMessage || arg instanceof ServerResponse) {
|
|
@@ -110,17 +134,17 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
|
|
|
110
134
|
return args.concat(arg);
|
|
111
135
|
}, []);
|
|
112
136
|
}
|
|
113
|
-
getOptions(
|
|
114
|
-
const $ctx =
|
|
115
|
-
const { ttl, type, collectionType, key: k = this.cache.defaultKeyResolver(), refreshThreshold } =
|
|
116
|
-
let { canCache } =
|
|
117
|
-
const args = this.getArgs(
|
|
137
|
+
getOptions(interceptorContext, defaultByPass = false) {
|
|
138
|
+
const $ctx = this.getContextFromArgs(interceptorContext.args) || context();
|
|
139
|
+
const { ttl, type, collectionType, key: k = this.cache.defaultKeyResolver(), refreshThreshold, byPass = defaultByPass } = interceptorContext.options || {};
|
|
140
|
+
let { canCache } = interceptorContext.options || {};
|
|
141
|
+
const args = this.getArgs(interceptorContext);
|
|
118
142
|
const keyArgs = isString(k) ? k : k(args, $ctx);
|
|
119
143
|
if (canCache && canCache === "no-nullish") {
|
|
120
144
|
canCache = (item) => ![null, undefined].includes(item);
|
|
121
145
|
}
|
|
122
146
|
return {
|
|
123
|
-
key: [...[this.prefix, ...getPrefix(
|
|
147
|
+
key: [...[this.prefix, ...getPrefix(interceptorContext.target, interceptorContext.propertyKey)].filter(Boolean), keyArgs].join(":"),
|
|
124
148
|
refreshThreshold,
|
|
125
149
|
ttl,
|
|
126
150
|
type,
|
|
@@ -128,9 +152,22 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
|
|
|
128
152
|
collectionType,
|
|
129
153
|
keyArgs,
|
|
130
154
|
canCache,
|
|
131
|
-
$ctx
|
|
155
|
+
$ctx,
|
|
156
|
+
byPass
|
|
132
157
|
};
|
|
133
158
|
}
|
|
159
|
+
getContextFromArgs(args) {
|
|
160
|
+
return args.find((arg) => arg instanceof DIContext);
|
|
161
|
+
}
|
|
162
|
+
shouldByPassCache(byPass, args, $ctx) {
|
|
163
|
+
if (!byPass) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
if (byPass === "no-cache") {
|
|
167
|
+
return this.noCache($ctx);
|
|
168
|
+
}
|
|
169
|
+
return typeof byPass === "function" ? byPass(args, $ctx) : !!byPass;
|
|
170
|
+
}
|
|
134
171
|
async hasKeyInQueue(key) {
|
|
135
172
|
return !!(await this.cache.get(`$$queue:${key}`));
|
|
136
173
|
}
|
|
@@ -140,23 +177,26 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
|
|
|
140
177
|
async deleteKeyFromQueue(key) {
|
|
141
178
|
await this.cache.del(`$$queue:${key}`);
|
|
142
179
|
}
|
|
143
|
-
sendResponse(cachedObject
|
|
180
|
+
sendResponse(cachedObject) {
|
|
144
181
|
const { headers, ttl } = cachedObject;
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
182
|
+
const $ctx = context();
|
|
183
|
+
if ($ctx.request && $ctx.response) {
|
|
184
|
+
const requestEtag = $ctx.request.get("if-none-match");
|
|
185
|
+
if (requestEtag && headers?.etag === requestEtag) {
|
|
186
|
+
$ctx.response.status(304).setHeaders(headers).body(undefined);
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const data = JSON.parse(cachedObject.data);
|
|
190
|
+
$ctx.response
|
|
191
|
+
.setHeaders({
|
|
192
|
+
...headers,
|
|
193
|
+
"x-cached": "true",
|
|
194
|
+
"cache-control": `max-age=${ttl}`
|
|
195
|
+
})
|
|
196
|
+
.body(data);
|
|
197
|
+
return cachedObject.data;
|
|
150
198
|
}
|
|
151
|
-
|
|
152
|
-
$ctx.response
|
|
153
|
-
.setHeaders({
|
|
154
|
-
...headers,
|
|
155
|
-
"x-cached": "true",
|
|
156
|
-
"cache-control": `max-age=${ttl}`
|
|
157
|
-
})
|
|
158
|
-
.body(data);
|
|
159
|
-
return data;
|
|
199
|
+
return cachedObject.data;
|
|
160
200
|
}
|
|
161
201
|
};
|
|
162
202
|
__decorate([
|
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import { BaseContext, InterceptorContext, InterceptorMethods, InterceptorNext } from "@tsed/di";
|
|
1
|
+
import { type BaseContext, DIContext, InterceptorContext, InterceptorMethods, InterceptorNext } from "@tsed/di";
|
|
2
2
|
import { Logger } from "@tsed/logger";
|
|
3
3
|
import { PlatformCachedObject } from "../interfaces/PlatformCachedObject.js";
|
|
4
4
|
import { PlatformCacheOptions } from "../interfaces/PlatformCacheOptions.js";
|
|
5
5
|
import { PlatformCache } from "../services/PlatformCache.js";
|
|
6
|
+
interface Response {
|
|
7
|
+
setHeader(key: string, value: string): this;
|
|
8
|
+
setHeaders(headers: Record<string, unknown>): this;
|
|
9
|
+
body(data: unknown): this;
|
|
10
|
+
status(status: number): this;
|
|
11
|
+
onEnd(cb: any): void;
|
|
12
|
+
getBody(): unknown;
|
|
13
|
+
getHeaders(): Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
type Context = DIContext & {
|
|
16
|
+
request?: {
|
|
17
|
+
get(key: string): string | undefined;
|
|
18
|
+
method: string;
|
|
19
|
+
};
|
|
20
|
+
response?: Response;
|
|
21
|
+
};
|
|
6
22
|
/**
|
|
7
23
|
* @platform
|
|
8
24
|
*/
|
|
@@ -15,23 +31,29 @@ export declare class PlatformCacheInterceptor implements InterceptorMethods {
|
|
|
15
31
|
canRefreshInBackground(key: string, { refreshThreshold, ttl }: {
|
|
16
32
|
refreshThreshold?: number;
|
|
17
33
|
ttl: any;
|
|
18
|
-
}, next: Function): Promise<void>;
|
|
34
|
+
}, next: Function, $ctx?: Context): Promise<void>;
|
|
19
35
|
cacheMethod(context: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext): Promise<any>;
|
|
20
|
-
cacheResponse(
|
|
21
|
-
protected
|
|
22
|
-
protected
|
|
36
|
+
cacheResponse(interceptorContext: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext): Promise<any>;
|
|
37
|
+
protected getMethod($ctx?: Context): string | undefined;
|
|
38
|
+
protected noCache($ctx?: Context): boolean;
|
|
39
|
+
protected getArgs(context: InterceptorContext<unknown, PlatformCacheOptions>): unknown[];
|
|
40
|
+
protected getOptions(interceptorContext: InterceptorContext<any, PlatformCacheOptions>, defaultByPass?: PlatformCacheOptions["byPass"]): {
|
|
23
41
|
key: string;
|
|
24
42
|
refreshThreshold: number | undefined;
|
|
25
43
|
ttl: import("../services/PlatformCache.js").Ttl | undefined;
|
|
26
44
|
type: any;
|
|
27
|
-
args:
|
|
45
|
+
args: unknown[];
|
|
28
46
|
collectionType: any;
|
|
29
47
|
keyArgs: string;
|
|
30
48
|
canCache: ((item: any) => boolean) | undefined;
|
|
31
|
-
$ctx:
|
|
49
|
+
$ctx: Context;
|
|
50
|
+
byPass: false | "no-cache" | ((args: any[], $ctx?: BaseContext) => boolean);
|
|
32
51
|
};
|
|
52
|
+
protected getContextFromArgs(args: unknown[]): Context | undefined;
|
|
53
|
+
protected shouldByPassCache(byPass: PlatformCacheOptions["byPass"], args: unknown[], $ctx?: Context): boolean;
|
|
33
54
|
protected hasKeyInQueue(key: string): Promise<boolean>;
|
|
34
55
|
protected addKeyToQueue(key: string): Promise<void>;
|
|
35
56
|
protected deleteKeyFromQueue(key: string): Promise<void>;
|
|
36
|
-
protected sendResponse(cachedObject: PlatformCachedObject
|
|
57
|
+
protected sendResponse(cachedObject: PlatformCachedObject): any;
|
|
37
58
|
}
|
|
59
|
+
export {};
|
|
@@ -22,4 +22,11 @@ export interface PlatformCacheOptions extends MetadataTypes {
|
|
|
22
22
|
* The function determine if the result must be cached or not.
|
|
23
23
|
*/
|
|
24
24
|
canCache?: ((item: any) => boolean) | "no-nullish";
|
|
25
|
+
/**
|
|
26
|
+
* Configure the bypass cache strategy.
|
|
27
|
+
* - `false` (default for services) never bypasses the cache.
|
|
28
|
+
* - `"no-cache"` (default for HTTP endpoints) bypasses when the `Cache-Control: no-cache` header is present.
|
|
29
|
+
* - Provide a function to decide dynamically based on the current call arguments/context.
|
|
30
|
+
*/
|
|
31
|
+
byPass?: false | "no-cache" | ((args: any[], $ctx?: BaseContext) => boolean);
|
|
25
32
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@tsed/platform-cache",
|
|
3
3
|
"description": "Platform cache module for Ts.ED Framework",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.
|
|
5
|
+
"version": "8.24.0",
|
|
6
6
|
"source": "./src/index.ts",
|
|
7
7
|
"main": "./lib/esm/index.js",
|
|
8
8
|
"module": "./lib/esm/index.js",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"tslib": "2.7.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@tsed/barrels": "8.
|
|
32
|
-
"@tsed/core": "8.
|
|
33
|
-
"@tsed/di": "8.
|
|
34
|
-
"@tsed/json-mapper": "8.
|
|
35
|
-
"@tsed/schema": "8.
|
|
36
|
-
"@tsed/typescript": "8.
|
|
31
|
+
"@tsed/barrels": "8.24.0",
|
|
32
|
+
"@tsed/core": "8.24.0",
|
|
33
|
+
"@tsed/di": "8.24.0",
|
|
34
|
+
"@tsed/json-mapper": "8.24.0",
|
|
35
|
+
"@tsed/schema": "8.24.0",
|
|
36
|
+
"@tsed/typescript": "8.24.0",
|
|
37
37
|
"@types/micromatch": "^4.0.9",
|
|
38
38
|
"eslint": "9.12.0",
|
|
39
39
|
"typescript": "5.9.3",
|
package/readme.md
CHANGED
|
@@ -35,6 +35,16 @@ A package of Ts.ED framework. See website: https://tsed.devdocs/cache.html
|
|
|
35
35
|
npm install --save @tsed/platform-cache
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Force refresh
|
|
39
|
+
|
|
40
|
+
Send the header `Cache-Control: no-cache` (or set it directly on the `PlatformContext`) to bypass
|
|
41
|
+
`PlatformCacheInterceptor` for a single request. This is handy when you want a fresh response without wiping the cache.
|
|
42
|
+
You can override or extend this behavior with the `byPass` option on `@UseCache`:
|
|
43
|
+
|
|
44
|
+
- Service methods default to `byPass: false`, so they always consult the cache unless you explicitly return `true` from your predicate.
|
|
45
|
+
- HTTP endpoints default to `byPass: "no-cache"`, which automatically honours the `Cache-Control: no-cache` header for one-off refreshes.
|
|
46
|
+
- Provide a function (`byPass: (args, $ctx) => boolean`) to build custom strategies (e.g., bypass for admins or based on payload).
|
|
47
|
+
|
|
38
48
|
## Contributors
|
|
39
49
|
|
|
40
50
|
Please read [contributing guidelines here](https://tsed.devcontributing.html).
|