@tsed/platform-cache 8.23.0 → 8.24.1

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
- if (currentTTL === undefined || currentTTL < calculatedTTL - refreshThreshold) {
40
- await next();
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(context, next) {
78
- const { request, response } = context.args[context.args.length - 1];
79
- if (request.method !== "GET") {
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 { key, ttl, args, $ctx } = this.getOptions(context);
83
- const cachedObject = await this.cache.getCachedObject(key);
84
- if (cachedObject && !(request.get("cache-control") === "no-cache")) {
85
- return this.sendResponse(cachedObject, $ctx);
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
- $ctx.response.setHeaders({
105
+ currentCtx.response?.setHeaders({
90
106
  "cache-control": `max-age=${calculatedTTL}`
91
107
  });
92
- // cache final response with his headers and body
93
- response.onEnd(() => {
94
- this.cache.setCachedObject(key, response.getBody(), {
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(context) {
114
- const $ctx = context.args[context.args.length - 1];
115
- const { ttl, type, collectionType, key: k = this.cache.defaultKeyResolver(), refreshThreshold } = context.options || {};
116
- let { canCache } = context.options || {};
117
- const args = this.getArgs(context);
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(context.target, context.propertyKey)].filter(Boolean), keyArgs].join(":"),
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, $ctx) {
180
+ sendResponse(cachedObject) {
144
181
  const { headers, ttl } = cachedObject;
145
- const { request, response } = $ctx;
146
- const requestEtag = request.get("if-none-match");
147
- if (requestEtag && headers.etag === requestEtag) {
148
- response.status(304).setHeaders(headers).body(undefined);
149
- return undefined;
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
- const data = JSON.parse(cachedObject.data);
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(context: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext): Promise<any>;
21
- protected getArgs(context: InterceptorContext<unknown, PlatformCacheOptions>): any;
22
- protected getOptions(context: InterceptorContext<any, PlatformCacheOptions>): {
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: any;
45
+ args: unknown[];
28
46
  collectionType: any;
29
47
  keyArgs: string;
30
48
  canCache: ((item: any) => boolean) | undefined;
31
- $ctx: any;
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, $ctx: BaseContext): any;
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.23.0",
5
+ "version": "8.24.1",
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.23.0",
32
- "@tsed/core": "8.23.0",
33
- "@tsed/di": "8.23.0",
34
- "@tsed/json-mapper": "8.23.0",
35
- "@tsed/schema": "8.23.0",
36
- "@tsed/typescript": "8.23.0",
31
+ "@tsed/barrels": "8.24.1",
32
+ "@tsed/core": "8.24.1",
33
+ "@tsed/di": "8.24.1",
34
+ "@tsed/json-mapper": "8.24.1",
35
+ "@tsed/schema": "8.24.1",
36
+ "@tsed/typescript": "8.24.1",
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).