diff --git a/docs/docs/cache.md b/docs/docs/cache.md
index 148d17e4c34..ccf00e2df6f 100644
--- a/docs/docs/cache.md
+++ b/docs/docs/cache.md
@@ -256,6 +256,17 @@ Only `GET` endpoints are cached. Also, HTTP server routes that use the native re
@@PlatformCacheInterceptor@@.
:::
+::: tip Force refresh
+Send the `Cache-Control: no-cache` header with your request (or set the same header on the @@PlatformContext@@) to
+bypass @@PlatformCacheInterceptor@@ temporarily. This lets you fetch a fresh response without clearing the cache and is
+useful for debugging, warm-up scripts, or administrative tooling. You can override this behavior via the `byPass`
+option on @@UseCache@@:
+
+- Services/methods that aren't tied to HTTP default to `byPass: false`, meaning caching is always considered unless you opt out.
+- Controllers/endpoints default to `byPass: "no-cache"`, so sending `Cache-Control: no-cache` skips both the lookup and write for that request without touching existing entries.
+- Supply a predicate `byPass: (args, $ctx) => boolean` when you need finer control (tenant-based bypass, admin flag, etc.).
+ :::
+
## Cache a value
Because @@UseCache@@ uses @@PlatformCacheInterceptor@@ and not a middleware, you can also apply the decorator on any
@@ -450,9 +461,9 @@ background if current `ttl` is under 45 minutes.
## Refresh cached value
-A service method response can be cached by using the `@UseCache` decorator. Sometimes, we need to explicitly refresh the cached data because the consumed data backend state has changed.
-because the consumed data backend state has changed. By implementing a notifications service, the backend data can trigger an event to tell your API that
-the data has changed.
+A service method response can be cached by using the `@UseCache` decorator. Sometimes, we need to explicitly refresh the
+cached data because the consumed data backend state has changed. By implementing a notifications service, the backend
+data can trigger an event to tell your API that the data has changed.
Here is short example:
@@ -491,7 +502,8 @@ export class NotificationsService {
This small example will force the data refresh.
::: tip
-If you have several cached method calls, then the refresh will also be done on all of these methods called by the function passed to `PlatformCache.refresh()`.
+If you have several cached method calls, then the refresh will also be done on all of these methods called by the
+function passed to `PlatformCache.refresh()`.
:::
## Multi caching
diff --git a/docs/package.json b/docs/package.json
index 3655c7a8958..344fa5b6e2a 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -13,7 +13,7 @@
"@tsed/vitepress-theme": "1.5.7",
"@vueuse/core": "10.11.0",
"axios": "1.12.0",
- "lodash": "4.17.21",
+ "lodash": "4.17.23",
"lucide-vue-next": "^0.436.0",
"vitepress": "1.5.0"
},
diff --git a/docs/yarn.lock b/docs/yarn.lock
index 8f523281722..b461d04889e 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -754,7 +754,7 @@ __metadata:
"@vueuse/core": "npm:10.11.0"
autoprefixer: "npm:^10.4.19"
axios: "npm:1.12.0"
- lodash: "npm:4.17.21"
+ lodash: "npm:4.17.23"
lucide-vue-next: "npm:^0.436.0"
postcss: "npm:^8.4.39"
tailwindcss: "npm:3.4.4"
@@ -2187,10 +2187,10 @@ __metadata:
languageName: node
linkType: hard
-"lodash@npm:4.17.21":
- version: 4.17.21
- resolution: "lodash@npm:4.17.21"
- checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532
+"lodash@npm:4.17.23":
+ version: 4.17.23
+ resolution: "lodash@npm:4.17.23"
+ checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233
languageName: node
linkType: hard
diff --git a/packages/di/src/common/interfaces/InterceptorContext.ts b/packages/di/src/common/interfaces/InterceptorContext.ts
index 5227ee28a52..19318b2d82e 100644
--- a/packages/di/src/common/interfaces/InterceptorContext.ts
+++ b/packages/di/src/common/interfaces/InterceptorContext.ts
@@ -42,7 +42,7 @@ export interface InterceptorNext {
export interface InterceptorContext {
target: Klass;
propertyKey: string | symbol;
- args: any[];
+ args: unknown[];
next: InterceptorNext;
options?: Opts;
}
diff --git a/packages/platform/platform-cache/readme.md b/packages/platform/platform-cache/readme.md
index 44b9193b3d2..6e417b1271e 100644
--- a/packages/platform/platform-cache/readme.md
+++ b/packages/platform/platform-cache/readme.md
@@ -35,6 +35,16 @@ A package of Ts.ED framework. See website: https://tsed.devdocs/cache.html
npm install --save @tsed/platform-cache
```
+## Force refresh
+
+Send the header `Cache-Control: no-cache` (or set it directly on the `PlatformContext`) to bypass
+`PlatformCacheInterceptor` for a single request. This is handy when you want a fresh response without wiping the cache.
+You can override or extend this behavior with the `byPass` option on `@UseCache`:
+
+- Service methods default to `byPass: false`, so they always consult the cache unless you explicitly return `true` from your predicate.
+- HTTP endpoints default to `byPass: "no-cache"`, which automatically honours the `Cache-Control: no-cache` header for one-off refreshes.
+- Provide a function (`byPass: (args, $ctx) => boolean`) to build custom strategies (e.g., bypass for admins or based on payload).
+
## Contributors
Please read [contributing guidelines here](https://tsed.devcontributing.html).
diff --git a/packages/platform/platform-cache/src/interceptors/PlatformCacheInterceptor.spec.ts b/packages/platform/platform-cache/src/interceptors/PlatformCacheInterceptor.spec.ts
index 8d89e1587ea..2a5aac56ade 100644
--- a/packages/platform/platform-cache/src/interceptors/PlatformCacheInterceptor.spec.ts
+++ b/packages/platform/platform-cache/src/interceptors/PlatformCacheInterceptor.spec.ts
@@ -1,4 +1,5 @@
import {isClass} from "@tsed/core";
+import {runInContext} from "@tsed/di";
import {serialize} from "@tsed/json-mapper";
import {PlatformTest} from "@tsed/platform-http/testing";
@@ -105,10 +106,13 @@ describe("PlatformCacheInterceptor", () => {
describe("canRefreshInBackground()", () => {
it("should refresh key in background", async () => {
const {cache, interceptor} = await getInterceptorFixture();
+ const $ctx = PlatformTest.createRequestContext();
const next = vi.fn();
- await interceptor.canRefreshInBackground("key", {refreshThreshold: 300, ttl: 10000}, next);
+ await runInContext($ctx, () => {
+ return interceptor.canRefreshInBackground("key", {refreshThreshold: 300, ttl: 10000}, next, $ctx);
+ });
expect(cache.get).toHaveBeenCalledWith("$$queue:key");
expect(cache.set).toHaveBeenCalledWith("$$queue:key", true, {ttl: 120});
@@ -120,10 +124,21 @@ describe("PlatformCacheInterceptor", () => {
const {cache, interceptor} = await getInterceptorFixture({
ttl: 9700
});
+ const $ctx = PlatformTest.createRequestContext();
const next = vi.fn();
- await interceptor.canRefreshInBackground("key", {refreshThreshold: 300, ttl: 10000}, next);
+ await runInContext($ctx, () =>
+ interceptor.canRefreshInBackground(
+ "key",
+ {
+ refreshThreshold: 300,
+ ttl: 10000
+ },
+ next,
+ $ctx
+ )
+ );
expect(cache.get).toHaveBeenCalledWith("$$queue:key");
expect(cache.ttl).toHaveBeenCalledWith("key");
@@ -133,6 +148,39 @@ describe("PlatformCacheInterceptor", () => {
});
});
describe("cacheMethod()", () => {
+ it("should bypass method cache when byPass returns true", async () => {
+ const {cache, interceptor} = await getInterceptorFixture();
+ cache.getCachedObject = vi.fn();
+ cache.setCachedObject = vi.fn();
+ cache.defaultKeyResolver = () => defaultKeyResolver;
+
+ class Test {
+ @UseCache({
+ ttl: 10000,
+ byPass: () => true
+ })
+ test(arg: string) {
+ return "";
+ }
+ }
+
+ const next = vi.fn().mockResolvedValue("fresh");
+ const context: any = {
+ target: Test,
+ propertyKey: "test",
+ args: ["value"],
+ options: {
+ ttl: 10000,
+ byPass: () => true
+ }
+ };
+
+ const result = await interceptor.cacheMethod(context, next);
+
+ expect(cache.getCachedObject).not.toHaveBeenCalled();
+ expect(cache.setCachedObject).not.toHaveBeenCalled();
+ expect(result).toEqual("fresh");
+ });
it("should return the cached response", async () => {
const {cache, interceptor} = await getInterceptorFixture();
cache.getCachedObject = vi.fn().mockResolvedValue({
@@ -454,20 +502,14 @@ describe("PlatformCacheInterceptor", () => {
}
const next = vi.fn();
- const $ctx = {
- request: {
- method: "GET",
- url: "/",
- get: vi.fn()
- },
- response: {
- getBody: vi.fn().mockReturnValue({
- data: "data"
- }),
- setHeaders: vi.fn(),
- onEnd: vi.fn()
- }
- };
+ const $ctx = PlatformTest.createRequestContext();
+ vi.spyOn($ctx.request, "get");
+ vi.spyOn($ctx, "get").mockReturnValue(undefined);
+ vi.spyOn($ctx.response, "getBody").mockReturnValue({
+ data: "data"
+ });
+ vi.spyOn($ctx.response, "setHeaders").mockReturnThis();
+ vi.spyOn($ctx.response, "onEnd").mockImplementation(() => $ctx.response);
const context: any = {
target: Test,
propertyKey: "test",
@@ -483,9 +525,9 @@ describe("PlatformCacheInterceptor", () => {
const result = await interceptor.cacheResponse(context, next);
- expect(cache.getCachedObject).toHaveBeenCalledWith('Test:test:value:{"request":{"method":"GET","url":"/"},"response":{}}');
+ expect(cache.getCachedObject).toHaveBeenCalledWith("Test:test:value");
expect(result).toEqual(undefined);
- expect((interceptor as any).sendResponse).toHaveBeenCalledWith({data: '{"data":"data"}'}, $ctx);
+ expect((interceptor as any).sendResponse).toHaveBeenCalledWith({data: '{"data":"data"}'});
expect($ctx.request.get).toHaveBeenCalledWith("cache-control");
/*
@@ -526,20 +568,14 @@ describe("PlatformCacheInterceptor", () => {
}
const next = vi.fn();
- const $ctx = {
- request: {
- method: "GET",
- url: "/",
- get: vi.fn()
- },
- response: {
- getBody: vi.fn().mockReturnValue({
- data: "data"
- }),
- setHeaders: vi.fn(),
- onEnd: vi.fn()
- }
- };
+ const $ctx = PlatformTest.createRequestContext();
+ vi.spyOn($ctx.request, "get");
+ vi.spyOn($ctx, "get").mockReturnValue(undefined);
+ vi.spyOn($ctx.response, "getBody").mockReturnValue({
+ data: "data"
+ });
+ vi.spyOn($ctx.response, "setHeaders").mockReturnThis();
+ vi.spyOn($ctx.response, "onEnd").mockImplementation(() => $ctx.response);
const context: any = {
target: Test,
propertyKey: "test",
@@ -556,9 +592,9 @@ describe("PlatformCacheInterceptor", () => {
const result = await interceptor.cacheResponse(context, next);
- expect(cache.getCachedObject).toHaveBeenCalledWith('TEST:value:{"request":{"method":"GET","url":"/"},"response":{}}');
+ expect(cache.getCachedObject).toHaveBeenCalledWith("TEST:value");
expect(result).toEqual(undefined);
- expect((interceptor as any).sendResponse).toHaveBeenCalledWith({data: '{"data":"data"}'}, $ctx);
+ expect((interceptor as any).sendResponse).toHaveBeenCalledWith({data: '{"data":"data"}'});
expect($ctx.request.get).toHaveBeenCalledWith("cache-control");
/*
@@ -568,13 +604,15 @@ describe("PlatformCacheInterceptor", () => {
expect(cache.setCachedObject).toHaveBeenCalledWith()
*/
});
- it("should call the method and set the cache", async () => {
+ it("should bypass cached response when cache-control is no-cache", async () => {
const cache = {
get: vi.fn().mockResolvedValue(false),
set: vi.fn().mockResolvedValue(false),
del: vi.fn().mockResolvedValue(true),
calculateTTL: vi.fn().mockReturnValue(10000),
- getCachedObject: vi.fn().mockResolvedValue(undefined),
+ getCachedObject: vi.fn().mockResolvedValue({
+ data: JSON.stringify({data: "cached"})
+ }),
setCachedObject: vi.fn().mockResolvedValue("test"),
defaultKeyResolver: () => defaultKeyResolver
};
@@ -595,24 +633,141 @@ describe("PlatformCacheInterceptor", () => {
}
}
+ const next = vi.fn().mockResolvedValue({data: "fresh"});
+ const $ctx = PlatformTest.createRequestContext();
+ vi.spyOn($ctx.request, "get").mockImplementation((key: string) => (key === "cache-control" ? "no-cache" : undefined));
+ vi.spyOn($ctx, "get").mockReturnValue(undefined);
+ vi.spyOn($ctx.response, "setHeaders").mockReturnThis();
+ vi.spyOn($ctx.response, "onEnd").mockImplementation((cb) => {
+ cb && cb();
+ return $ctx.response;
+ });
+ vi.spyOn($ctx.response, "getBody").mockReturnValue({data: "fresh"});
+ vi.spyOn($ctx.response, "getHeaders").mockReturnValue({
+ "x-key": "key"
+ });
+
+ const context: any = {
+ target: Test,
+ propertyKey: "test",
+ args: ["value", $ctx],
+ options: {
+ ttl: 10000,
+ refreshThreshold: 1000
+ }
+ };
+
+ const sendResponseSpy = vi.spyOn(interceptor as any, "sendResponse").mockReturnValue(undefined);
+
+ const result = await interceptor.cacheResponse(context, next);
+
+ expect(cache.getCachedObject).not.toHaveBeenCalled();
+ expect(next).toHaveBeenCalledWith();
+ expect(sendResponseSpy).not.toHaveBeenCalled();
+ expect($ctx.request.get).toHaveBeenCalledWith("cache-control");
+ expect($ctx.response.setHeaders).toHaveBeenCalledWith({
+ "cache-control": `max-age=10000`
+ });
+ expect(result).toEqual({data: "fresh"});
+ expect(cache.setCachedObject).not.toHaveBeenCalled();
+ });
+ it("should ignore cache-control header when byPass is false", async () => {
+ const cache = {
+ get: vi.fn().mockResolvedValue(false),
+ set: vi.fn().mockResolvedValue(false),
+ del: vi.fn().mockResolvedValue(true),
+ calculateTTL: vi.fn().mockReturnValue(10000),
+ getCachedObject: vi.fn().mockResolvedValue({
+ data: JSON.stringify({data: "data"})
+ }),
+ setCachedObject: vi.fn().mockResolvedValue("test"),
+ defaultKeyResolver: () => defaultKeyResolver
+ };
+ const interceptor = await PlatformTest.invoke(PlatformCacheInterceptor, [
+ {
+ token: PlatformCache,
+ use: cache
+ }
+ ]);
+
+ class Test {
+ @UseCache({
+ ttl: 10000,
+ refreshThreshold: 1000,
+ byPass: false
+ })
+ test(arg: string) {
+ return "";
+ }
+ }
+
const next = vi.fn();
- const $ctx = {
- request: {
- method: "GET",
- url: "/",
- get: vi.fn()
- },
- response: {
- getBody: vi.fn().mockReturnValue({
- data: "data"
- }),
- getHeaders: vi.fn().mockReturnValue({
- "x-key": "key"
- }),
- setHeaders: vi.fn(),
- onEnd: vi.fn()
+ const $ctx = PlatformTest.createRequestContext();
+ vi.spyOn($ctx.request, "get").mockImplementation((key: string) => (key === "cache-control" ? "no-cache" : undefined));
+ vi.spyOn($ctx, "get").mockReturnValue(undefined);
+ vi.spyOn($ctx.response, "setHeaders").mockReturnThis();
+ const context: any = {
+ target: Test,
+ propertyKey: "test",
+ args: ["value", $ctx],
+ options: {
+ ttl: 10000,
+ refreshThreshold: 1000,
+ byPass: false
}
};
+
+ vi.spyOn(interceptor, "canRefreshInBackground").mockResolvedValue();
+ const sendResponseSpy = vi.spyOn(interceptor as any, "sendResponse").mockResolvedValue(undefined);
+
+ await interceptor.cacheResponse(context, next);
+
+ expect(cache.getCachedObject).toHaveBeenCalledWith("Test:test:value");
+ expect(sendResponseSpy).toHaveBeenCalledWith({data: '{"data":"data"}'});
+ expect(next).not.toHaveBeenCalled();
+ });
+ it("should call the method and set the cache", async () => {
+ const cache = {
+ get: vi.fn().mockResolvedValue(false),
+ set: vi.fn().mockResolvedValue(false),
+ del: vi.fn().mockResolvedValue(true),
+ calculateTTL: vi.fn().mockReturnValue(10000),
+ getCachedObject: vi.fn().mockResolvedValue(undefined),
+ setCachedObject: vi.fn().mockResolvedValue("test"),
+ defaultKeyResolver: () => defaultKeyResolver
+ };
+ const interceptor = await PlatformTest.invoke(PlatformCacheInterceptor, [
+ {
+ token: PlatformCache,
+ use: cache
+ }
+ ]);
+
+ class Test {
+ @UseCache({
+ ttl: 10000,
+ refreshThreshold: 1000
+ })
+ test(arg: string) {
+ return "";
+ }
+ }
+
+ const next = vi.fn();
+ const $ctx = PlatformTest.createRequestContext();
+ vi.spyOn($ctx.request, "get");
+ vi.spyOn($ctx, "get").mockReturnValue(undefined);
+ vi.spyOn($ctx.response, "getBody").mockReturnValue({
+ data: "data"
+ });
+ vi.spyOn($ctx.response, "getHeaders").mockReturnValue({
+ "x-key": "key"
+ });
+ vi.spyOn($ctx.response, "setHeaders").mockReturnThis();
+ vi.spyOn($ctx.response, "onEnd").mockImplementation((cb) => {
+ cb && cb();
+ return $ctx.response;
+ });
const context: any = {
target: Test,
propertyKey: "test",
@@ -628,19 +783,17 @@ describe("PlatformCacheInterceptor", () => {
const result = await interceptor.cacheResponse(context, next);
- expect(cache.getCachedObject).toHaveBeenCalledWith('Test:test:value:{"request":{"method":"GET","url":"/"},"response":{}}');
+ expect(cache.getCachedObject).toHaveBeenCalledWith("Test:test:value");
expect(result).toEqual(undefined);
expect($ctx.response.setHeaders).toHaveBeenCalledWith({
"cache-control": `max-age=10000`
});
- await $ctx.response.onEnd.mock.calls[0][0]();
-
expect(cache.setCachedObject).toHaveBeenCalledWith(
- 'Test:test:value:{"request":{"method":"GET","url":"/"},"response":{}}',
+ "Test:test:value",
{data: "data"},
{
- args: ["value", {request: {method: "GET", url: "/"}, response: {}}],
+ args: ["value"],
headers: {"x-key": "key"},
ttl: 10000
}
diff --git a/packages/platform/platform-cache/src/interceptors/PlatformCacheInterceptor.ts b/packages/platform/platform-cache/src/interceptors/PlatformCacheInterceptor.ts
index 42d9a63533c..f17033af102 100644
--- a/packages/platform/platform-cache/src/interceptors/PlatformCacheInterceptor.ts
+++ b/packages/platform/platform-cache/src/interceptors/PlatformCacheInterceptor.ts
@@ -1,7 +1,18 @@
import {IncomingMessage, ServerResponse} from "node:http";
import {isClass, isString, nameOf} from "@tsed/core";
-import {BaseContext, Constant, DIContext, Inject, Interceptor, InterceptorContext, InterceptorMethods, InterceptorNext} from "@tsed/di";
+import {
+ type BaseContext,
+ Constant,
+ context,
+ DIContext,
+ Inject,
+ Interceptor,
+ InterceptorContext,
+ InterceptorMethods,
+ InterceptorNext,
+ runInContext
+} from "@tsed/di";
import {deserialize, serialize} from "@tsed/json-mapper";
import {Logger} from "@tsed/logger";
@@ -10,6 +21,7 @@ import {PlatformCacheOptions} from "../interfaces/PlatformCacheOptions.js";
import {PlatformCache} from "../services/PlatformCache.js";
import {getPrefix} from "../utils/getPrefix.js";
import {isEndpoint} from "../utils/isEndpoint.js";
+
const cleanHeaders = (headers: Record, blacklist: string[]) => {
return Object.entries(headers)
.filter(([key]) => !blacklist.includes(key.toLowerCase()))
@@ -21,6 +33,30 @@ const cleanHeaders = (headers: Record, blacklist: string[]) =>
}, {});
};
+interface Response {
+ setHeader(key: string, value: string): this;
+
+ setHeaders(headers: Record): this;
+
+ body(data: unknown): this;
+
+ status(status: number): this;
+
+ onEnd(cb: any): void;
+
+ getBody(): unknown;
+
+ getHeaders(): Record;
+}
+
+type Context = DIContext & {
+ request?: {
+ get(key: string): string | undefined;
+ method: string;
+ };
+ response?: Response;
+};
+
/**
* @platform
*/
@@ -59,7 +95,8 @@ export class PlatformCacheInterceptor implements InterceptorMethods {
refreshThreshold?: number;
ttl: any;
},
- next: Function
+ next: Function,
+ $ctx?: Context
) {
const inQueue = await this.hasKeyInQueue(key);
@@ -69,16 +106,26 @@ export class PlatformCacheInterceptor implements InterceptorMethods {
const currentTTL = await this.cache.ttl(key);
const calculatedTTL = this.cache.calculateTTL(currentTTL, ttl);
- if (currentTTL === undefined || currentTTL < calculatedTTL - refreshThreshold) {
- await next();
+ try {
+ if (currentTTL === undefined || currentTTL < calculatedTTL - refreshThreshold) {
+ if ($ctx) {
+ await runInContext($ctx, () => next());
+ } else {
+ await next();
+ }
+ }
+ } finally {
+ await this.deleteKeyFromQueue(key);
}
-
- await this.deleteKeyFromQueue(key);
}
}
async cacheMethod(context: InterceptorContext, next: InterceptorNext) {
- const {key, type, ttl, collectionType, refreshThreshold, args, canCache} = this.getOptions(context);
+ const {key, type, ttl, collectionType, refreshThreshold, args, canCache, $ctx, byPass} = this.getOptions(context, false);
+
+ if (this.shouldByPassCache(byPass, args, $ctx)) {
+ return next();
+ }
const set = (result: any) => {
if (!canCache || (canCache && canCache(result))) {
@@ -100,11 +147,16 @@ export class PlatformCacheInterceptor implements InterceptorMethods {
return result;
}
- this.canRefreshInBackground(key, {refreshThreshold, ttl}, async () => {
- const result = await next();
+ this.canRefreshInBackground(
+ key,
+ {refreshThreshold, ttl},
+ async () => {
+ const result = await next();
- await set(result);
- }).catch((er) =>
+ await set(result);
+ },
+ $ctx
+ ).catch((er) =>
this.logger.error({
event: "CACHE_ERROR",
method: "cacheMethod",
@@ -121,43 +173,57 @@ export class PlatformCacheInterceptor implements InterceptorMethods {
return deserialize(JSON.parse(data), {collectionType, type});
}
- async cacheResponse(context: InterceptorContext, next: InterceptorNext) {
- const {request, response} = context.args[context.args.length - 1];
+ async cacheResponse(interceptorContext: InterceptorContext, next: InterceptorNext) {
+ const {key, ttl, args, $ctx, byPass} = this.getOptions(interceptorContext, "no-cache");
+ const currentCtx = $ctx || context();
- if (request.method !== "GET") {
+ if (this.getMethod(currentCtx) !== "GET") {
return next();
}
- const {key, ttl, args, $ctx} = this.getOptions(context);
+ const shouldByPass = this.shouldByPassCache(byPass, args, currentCtx);
+ const useCache = !shouldByPass;
- const cachedObject = await this.cache.getCachedObject(key);
+ if (useCache) {
+ const cachedObject = await this.cache.getCachedObject(key);
- if (cachedObject && !(request.get("cache-control") === "no-cache")) {
- return this.sendResponse(cachedObject, $ctx);
+ if (cachedObject) {
+ return this.sendResponse(cachedObject);
+ }
}
const result = await next();
-
const calculatedTTL = this.cache.calculateTTL(result, ttl);
- $ctx.response.setHeaders({
+ currentCtx.response?.setHeaders({
"cache-control": `max-age=${calculatedTTL}`
});
- // cache final response with his headers and body
- response.onEnd(() => {
- this.cache.setCachedObject(key, response.getBody(), {
+ currentCtx.response?.onEnd(() => {
+ if (!useCache) {
+ return;
+ }
+
+ this.cache.setCachedObject(key, currentCtx.response!.getBody(), {
ttl: calculatedTTL,
args,
- headers: cleanHeaders(response.getHeaders(), this.blacklist)
+ headers: cleanHeaders(currentCtx.response!.getHeaders(), this.blacklist)
});
});
return result;
}
- protected getArgs(context: InterceptorContext) {
- return context.args.reduce((args, arg) => {
+ protected getMethod($ctx = context()) {
+ return $ctx.request?.method;
+ }
+
+ protected noCache($ctx = context()) {
+ return $ctx.request?.get("cache-control") === "no-cache" || $ctx.get("cache-control") === "no-cache";
+ }
+
+ protected getArgs(context: InterceptorContext): unknown[] {
+ return context.args.reduce((args: unknown[], arg) => {
if (arg instanceof DIContext || arg instanceof IncomingMessage || arg instanceof ServerResponse) {
return args;
}
@@ -167,25 +233,35 @@ export class PlatformCacheInterceptor implements InterceptorMethods {
}
return args.concat(arg);
- }, []);
+ }, [] as unknown[]) as unknown[];
}
- protected getOptions(context: InterceptorContext) {
- const $ctx = context.args[context.args.length - 1];
+ protected getOptions(
+ interceptorContext: InterceptorContext,
+ defaultByPass: PlatformCacheOptions["byPass"] = false
+ ) {
+ const $ctx = this.getContextFromArgs(interceptorContext.args) || context();
- const {ttl, type, collectionType, key: k = this.cache.defaultKeyResolver(), refreshThreshold} = context.options || {};
+ const {
+ ttl,
+ type,
+ collectionType,
+ key: k = this.cache.defaultKeyResolver(),
+ refreshThreshold,
+ byPass = defaultByPass
+ } = interceptorContext.options || {};
- let {canCache} = context.options || {};
+ let {canCache} = interceptorContext.options || {};
- const args = this.getArgs(context);
- const keyArgs = isString(k) ? k : k(args, $ctx);
+ const args = this.getArgs(interceptorContext);
+ const keyArgs = isString(k) ? k : k(args as any[], $ctx as any);
if (canCache && canCache === "no-nullish") {
canCache = (item: any) => ![null, undefined].includes(item);
}
return {
- key: [...[this.prefix, ...getPrefix(context.target, context.propertyKey)].filter(Boolean), keyArgs].join(":"),
+ key: [...[this.prefix, ...getPrefix(interceptorContext.target, interceptorContext.propertyKey)].filter(Boolean), keyArgs].join(":"),
refreshThreshold,
ttl,
type,
@@ -193,10 +269,27 @@ export class PlatformCacheInterceptor implements InterceptorMethods {
collectionType,
keyArgs,
canCache,
- $ctx
+ $ctx,
+ byPass
};
}
+ protected getContextFromArgs(args: unknown[]): Context | undefined {
+ return args.find((arg): arg is Context => arg instanceof DIContext);
+ }
+
+ protected shouldByPassCache(byPass: PlatformCacheOptions["byPass"], args: unknown[], $ctx?: Context) {
+ if (!byPass) {
+ return false;
+ }
+
+ if (byPass === "no-cache") {
+ return this.noCache($ctx);
+ }
+
+ return typeof byPass === "function" ? byPass(args as any[], $ctx as BaseContext) : !!byPass;
+ }
+
protected async hasKeyInQueue(key: string) {
return !!(await this.cache.get(`$$queue:${key}`));
}
@@ -209,28 +302,31 @@ export class PlatformCacheInterceptor implements InterceptorMethods {
await this.cache.del(`$$queue:${key}`);
}
- protected sendResponse(cachedObject: PlatformCachedObject, $ctx: BaseContext) {
+ protected sendResponse(cachedObject: PlatformCachedObject) {
const {headers, ttl} = cachedObject;
- const {request, response} = $ctx;
+ const $ctx = context();
+ if ($ctx.request && $ctx.response) {
+ const requestEtag = $ctx.request.get("if-none-match");
- const requestEtag = request.get("if-none-match");
+ if (requestEtag && headers?.etag === requestEtag) {
+ $ctx.response.status(304).setHeaders(headers).body(undefined);
- if (requestEtag && headers.etag === requestEtag) {
- response.status(304).setHeaders(headers).body(undefined);
+ return undefined;
+ }
- return undefined;
- }
+ const data = JSON.parse(cachedObject.data);
- const data = JSON.parse(cachedObject.data);
+ $ctx.response
+ .setHeaders({
+ ...headers,
+ "x-cached": "true",
+ "cache-control": `max-age=${ttl}`
+ })
+ .body(data);
- $ctx.response
- .setHeaders({
- ...headers,
- "x-cached": "true",
- "cache-control": `max-age=${ttl}`
- })
- .body(data);
+ return cachedObject.data;
+ }
- return data;
+ return cachedObject.data;
}
}
diff --git a/packages/platform/platform-cache/src/interfaces/PlatformCacheOptions.ts b/packages/platform/platform-cache/src/interfaces/PlatformCacheOptions.ts
index 94a6cc53f91..3deff41a175 100644
--- a/packages/platform/platform-cache/src/interfaces/PlatformCacheOptions.ts
+++ b/packages/platform/platform-cache/src/interfaces/PlatformCacheOptions.ts
@@ -24,4 +24,11 @@ export interface PlatformCacheOptions extends MetadataTypes {
* The function determine if the result must be cached or not.
*/
canCache?: ((item: any) => boolean) | "no-nullish";
+ /**
+ * Configure the bypass cache strategy.
+ * - `false` (default for services) never bypasses the cache.
+ * - `"no-cache"` (default for HTTP endpoints) bypasses when the `Cache-Control: no-cache` header is present.
+ * - Provide a function to decide dynamically based on the current call arguments/context.
+ */
+ byPass?: false | "no-cache" | ((args: any[], $ctx?: BaseContext) => boolean);
}
diff --git a/packages/platform/platform-test-sdk/src/tests/testCache.ts b/packages/platform/platform-test-sdk/src/tests/testCache.ts
index 1e89ae3ff40..aff6452b41e 100644
--- a/packages/platform/platform-test-sdk/src/tests/testCache.ts
+++ b/packages/platform/platform-test-sdk/src/tests/testCache.ts
@@ -143,7 +143,6 @@ export function testCache(options: PlatformTestingSdkOpts) {
const response2 = await request.get("/rest/caches/scenario-1").set("cache-control", "no-cache").expect(200);
expect(response2.text).toContain("hello world");
- expect(response2.headers["cache-control"]).toMatch(/max-age=300/);
expect(response2.headers["x-cached"]).toBeUndefined();
expect(response2.headers["etag"]).toEqual(response2.headers["etag"]);
});