@tsed/platform-cache 8.26.1 → 8.27.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/lib/esm/index.js CHANGED
@@ -8,5 +8,6 @@ export * from "./interfaces/interfaces.js";
8
8
  export * from "./interfaces/PlatformCachedObject.js";
9
9
  export * from "./interfaces/PlatformCacheOptions.js";
10
10
  export * from "./services/PlatformCache.js";
11
+ export * from "./services/PlatformCacheRefreshService.js";
11
12
  export * from "./utils/getPrefix.js";
12
13
  export * from "./utils/isEndpoint.js";
@@ -1,9 +1,9 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { isClass, isString, nameOf } from "@tsed/core";
3
- import { constant, context, DIContext, inject, interceptor, logger, runInContext } from "@tsed/di";
3
+ import { constant, context, DIContext, inject, interceptor, logger } from "@tsed/di";
4
4
  import { deserialize, serialize } from "@tsed/json-mapper";
5
5
  import { PlatformCache } from "../services/PlatformCache.js";
6
- import { getPrefix } from "../utils/getPrefix.js";
6
+ import { PlatformCacheRefreshService } from "../services/PlatformCacheRefreshService.js";
7
7
  import { isEndpoint } from "../utils/isEndpoint.js";
8
8
  const cleanHeaders = (headers, blacklist) => {
9
9
  return Object.entries(headers)
@@ -21,6 +21,7 @@ const cleanHeaders = (headers, blacklist) => {
21
21
  export class PlatformCacheInterceptor {
22
22
  constructor() {
23
23
  this.cache = inject(PlatformCache);
24
+ this.refreshService = inject(PlatformCacheRefreshService);
24
25
  this.prefix = constant("cache.prefix", "");
25
26
  this.blacklist = constant("cache.ignoreHeaders", [
26
27
  "content-length",
@@ -30,7 +31,6 @@ export class PlatformCacheInterceptor {
30
31
  "content-encoding"
31
32
  ]);
32
33
  }
33
- static #refreshLocks = new Map();
34
34
  intercept(context, next) {
35
35
  if (this.cache.disabled()) {
36
36
  return next();
@@ -40,40 +40,6 @@ export class PlatformCacheInterceptor {
40
40
  }
41
41
  return this.cacheResponse(context, next);
42
42
  }
43
- async canRefreshInBackground(key, { refreshThreshold, ttl, cachedTTL }, next, $ctx) {
44
- if (!refreshThreshold) {
45
- return;
46
- }
47
- const release = await this.acquireRefreshLock(key);
48
- try {
49
- const inQueue = await this.hasKeyInQueue(key);
50
- const inCooldown = await this.hasRefreshCooldown(key);
51
- if (inQueue || inCooldown) {
52
- return;
53
- }
54
- await this.addKeyToQueue(key);
55
- const currentTTL = await this.cache.ttl(key);
56
- const calculatedTTL = cachedTTL ?? (typeof ttl === "number" ? ttl : undefined);
57
- const refreshThresholdWithJitter = this.getRefreshThresholdWithJitter(key, refreshThreshold);
58
- try {
59
- if (calculatedTTL !== undefined && (currentTTL === undefined || currentTTL < calculatedTTL - refreshThresholdWithJitter)) {
60
- await this.addRefreshCooldown(key, refreshThresholdWithJitter);
61
- if ($ctx) {
62
- await runInContext($ctx, () => next());
63
- }
64
- else {
65
- await next();
66
- }
67
- }
68
- }
69
- finally {
70
- await this.deleteKeyFromQueue(key);
71
- }
72
- }
73
- finally {
74
- release();
75
- }
76
- }
77
43
  async cacheMethod(context, next) {
78
44
  const { key, type, ttl, collectionType, refreshThreshold, args, canCache, $ctx, byPass } = this.getOptions(context, false);
79
45
  if (this.shouldByPassCache(byPass, args, $ctx)) {
@@ -94,10 +60,12 @@ export class PlatformCacheInterceptor {
94
60
  }
95
61
  return result;
96
62
  }
97
- this.canRefreshInBackground(key, { refreshThreshold, ttl, cachedTTL: cachedObject.ttl }, async () => {
63
+ this.refreshService
64
+ .refreshInBackground(key, { refreshThreshold, ttl, cachedTTL: cachedObject.ttl }, async () => {
98
65
  const result = await next();
99
66
  await set(result);
100
- }, $ctx).catch((er) => logger().error({
67
+ }, $ctx)
68
+ .catch((er) => logger().error({
101
69
  event: "CACHE_ERROR",
102
70
  method: "cacheMethod",
103
71
  concerned_key: key,
@@ -169,7 +137,7 @@ export class PlatformCacheInterceptor {
169
137
  canCache = (item) => ![null, undefined].includes(item);
170
138
  }
171
139
  return {
172
- key: [...[this.prefix, ...getPrefix(interceptorContext.target, interceptorContext.propertyKey)].filter(Boolean), keyArgs].join(":"),
140
+ key: this.cache.buildEntryKey(interceptorContext.target, interceptorContext.propertyKey, keyArgs),
173
141
  refreshThreshold,
174
142
  ttl,
175
143
  type,
@@ -193,50 +161,6 @@ export class PlatformCacheInterceptor {
193
161
  }
194
162
  return typeof byPass === "function" ? byPass(args, $ctx) : !!byPass;
195
163
  }
196
- async hasKeyInQueue(key) {
197
- return !!(await this.cache.get(`$$queue:${key}`));
198
- }
199
- async addKeyToQueue(key) {
200
- await this.cache.set(`$$queue:${key}`, true, { ttl: 120 });
201
- }
202
- async deleteKeyFromQueue(key) {
203
- await this.cache.del(`$$queue:${key}`);
204
- }
205
- async acquireRefreshLock(key) {
206
- for (;;) {
207
- const lock = PlatformCacheInterceptor.#refreshLocks.get(key);
208
- if (!lock) {
209
- let releaseFn;
210
- const currentLock = new Promise((resolve) => {
211
- releaseFn = resolve;
212
- });
213
- PlatformCacheInterceptor.#refreshLocks.set(key, currentLock);
214
- return () => {
215
- if (PlatformCacheInterceptor.#refreshLocks.get(key) === currentLock) {
216
- PlatformCacheInterceptor.#refreshLocks.delete(key);
217
- }
218
- releaseFn?.();
219
- };
220
- }
221
- await lock;
222
- }
223
- }
224
- getRefreshCooldownKey(key) {
225
- return `$$refresh-cooldown:${key}`;
226
- }
227
- getRefreshThresholdWithJitter(key, refreshThreshold) {
228
- const maxJitter = Math.max(1, Math.floor(refreshThreshold * 0.1));
229
- const hash = key.split("").reduce((acc, char) => (acc * 31 + char.charCodeAt(0)) | 0, 0);
230
- const jitter = Math.abs(hash) % (maxJitter + 1);
231
- return Math.max(1, refreshThreshold - jitter);
232
- }
233
- async hasRefreshCooldown(key) {
234
- return !!(await this.cache.get(this.getRefreshCooldownKey(key)));
235
- }
236
- async addRefreshCooldown(key, refreshThreshold) {
237
- const cooldownTTL = Math.max(1, Math.floor(refreshThreshold / 2));
238
- await this.cache.set(this.getRefreshCooldownKey(key), true, { ttl: cooldownTTL });
239
- }
240
164
  sendResponse(cachedObject) {
241
165
  const { headers, ttl } = cachedObject;
242
166
  const $ctx = context();
@@ -24,8 +24,40 @@ export class PlatformCache {
24
24
  }
25
25
  }
26
26
  getKeysOf(target, propertyKey) {
27
- const prefix = getPrefix(target, propertyKey);
28
- return this.keys(`${prefix.join(":")}:*`);
27
+ return this.keys(this.buildEntryPattern(target, propertyKey));
28
+ }
29
+ /**
30
+ * Returns the global cache key prefix from settings (`cache.prefix`).
31
+ */
32
+ cachePrefix() {
33
+ return constant("cache.prefix", "");
34
+ }
35
+ /**
36
+ * Builds the stable namespace segments used by cached entries for a method.
37
+ * The namespace combines the global cache prefix (if configured) and the method prefix.
38
+ */
39
+ buildNamespace(target, propertyKey) {
40
+ return [this.cachePrefix(), ...getPrefix(target, propertyKey)].filter(Boolean);
41
+ }
42
+ /**
43
+ * Builds the final cache key for a method call.
44
+ */
45
+ buildEntryKey(target, propertyKey, keyArgs) {
46
+ return [...this.buildNamespace(target, propertyKey), keyArgs].join(":");
47
+ }
48
+ /**
49
+ * Builds a key pattern scoped to a cached method namespace.
50
+ * Useful for listing/invalidation of keys produced by the same cached method.
51
+ */
52
+ buildEntryPattern(target, propertyKey, suffix = "*") {
53
+ return [...this.buildNamespace(target, propertyKey), suffix].join(":");
54
+ }
55
+ /**
56
+ * Builds internal keys used by cache housekeeping features
57
+ * (refresh queue and refresh cooldown markers).
58
+ */
59
+ buildInternalKey(namespace, key) {
60
+ return `$$${namespace}:${key}`;
29
61
  }
30
62
  disabled() {
31
63
  return !constant("cache");
@@ -0,0 +1,90 @@
1
+ import { inject, injectable, ProviderScope, ProviderType, runInContext } from "@tsed/di";
2
+ import { PlatformCache } from "./PlatformCache.js";
3
+ /**
4
+ * @platform
5
+ */
6
+ export class PlatformCacheRefreshService {
7
+ constructor() {
8
+ this.cache = inject(PlatformCache);
9
+ }
10
+ static #refreshLocks = new Map();
11
+ async hasKeyInQueue(key) {
12
+ return !!(await this.cache.get(this.cache.buildInternalKey("queue", key)));
13
+ }
14
+ async addKeyToQueue(key) {
15
+ await this.cache.set(this.cache.buildInternalKey("queue", key), true, { ttl: 120 });
16
+ }
17
+ async deleteKeyFromQueue(key) {
18
+ await this.cache.del(this.cache.buildInternalKey("queue", key));
19
+ }
20
+ getRefreshThresholdWithJitter(key, refreshThreshold) {
21
+ const maxJitter = Math.max(1, Math.floor(refreshThreshold * 0.1));
22
+ const hash = key.split("").reduce((acc, char) => (acc * 31 + char.charCodeAt(0)) | 0, 0);
23
+ const jitter = Math.abs(hash) % (maxJitter + 1);
24
+ return Math.max(1, refreshThreshold - jitter);
25
+ }
26
+ async hasRefreshCooldown(key) {
27
+ return !!(await this.cache.get(this.getRefreshCooldownKey(key)));
28
+ }
29
+ async addRefreshCooldown(key, refreshThreshold) {
30
+ const cooldownTTL = Math.max(1, Math.floor(refreshThreshold / 2));
31
+ await this.cache.set(this.getRefreshCooldownKey(key), true, { ttl: cooldownTTL });
32
+ }
33
+ async refreshInBackground(key, { refreshThreshold, ttl, cachedTTL }, next, $ctx) {
34
+ if (!refreshThreshold) {
35
+ return;
36
+ }
37
+ const release = await this.acquireRefreshLock(key);
38
+ try {
39
+ const inQueue = await this.hasKeyInQueue(key);
40
+ const inCooldown = await this.hasRefreshCooldown(key);
41
+ if (inQueue || inCooldown) {
42
+ return;
43
+ }
44
+ await this.addKeyToQueue(key);
45
+ const currentTTL = await this.cache.ttl(key);
46
+ const calculatedTTL = cachedTTL ?? (typeof ttl === "number" ? ttl : undefined);
47
+ const refreshThresholdWithJitter = this.getRefreshThresholdWithJitter(key, refreshThreshold);
48
+ try {
49
+ if (calculatedTTL !== undefined && (currentTTL === undefined || currentTTL < calculatedTTL - refreshThresholdWithJitter)) {
50
+ await this.addRefreshCooldown(key, refreshThresholdWithJitter);
51
+ if ($ctx) {
52
+ await runInContext($ctx, () => next());
53
+ }
54
+ else {
55
+ await next();
56
+ }
57
+ }
58
+ }
59
+ finally {
60
+ await this.deleteKeyFromQueue(key);
61
+ }
62
+ }
63
+ finally {
64
+ release();
65
+ }
66
+ }
67
+ async acquireRefreshLock(key) {
68
+ for (;;) {
69
+ const lock = PlatformCacheRefreshService.#refreshLocks.get(key);
70
+ if (!lock) {
71
+ let releaseFn;
72
+ const currentLock = new Promise((resolve) => {
73
+ releaseFn = resolve;
74
+ });
75
+ PlatformCacheRefreshService.#refreshLocks.set(key, currentLock);
76
+ return () => {
77
+ if (PlatformCacheRefreshService.#refreshLocks.get(key) === currentLock) {
78
+ PlatformCacheRefreshService.#refreshLocks.delete(key);
79
+ }
80
+ releaseFn?.();
81
+ };
82
+ }
83
+ await lock;
84
+ }
85
+ }
86
+ getRefreshCooldownKey(key) {
87
+ return this.cache.buildInternalKey("refresh-cooldown", key);
88
+ }
89
+ }
90
+ injectable(PlatformCacheRefreshService).type(ProviderType.MODULE).scope(ProviderScope.SINGLETON);
@@ -8,5 +8,6 @@ export * from "./interfaces/interfaces.js";
8
8
  export * from "./interfaces/PlatformCachedObject.js";
9
9
  export * from "./interfaces/PlatformCacheOptions.js";
10
10
  export * from "./services/PlatformCache.js";
11
+ export * from "./services/PlatformCacheRefreshService.js";
11
12
  export * from "./utils/getPrefix.js";
12
13
  export * from "./utils/isEndpoint.js";
@@ -2,6 +2,7 @@ import { type BaseContext, DIContext, InterceptorContext, InterceptorMethods, In
2
2
  import { PlatformCachedObject } from "../interfaces/PlatformCachedObject.js";
3
3
  import { PlatformCacheOptions } from "../interfaces/PlatformCacheOptions.js";
4
4
  import { PlatformCache } from "../services/PlatformCache.js";
5
+ import { PlatformCacheRefreshService } from "../services/PlatformCacheRefreshService.js";
5
6
  interface Response {
6
7
  setHeader(key: string, value: string): this;
7
8
  setHeaders(headers: Record<string, unknown>): this;
@@ -22,16 +23,11 @@ type Context = DIContext & {
22
23
  * @platform
23
24
  */
24
25
  export declare class PlatformCacheInterceptor implements InterceptorMethods {
25
- #private;
26
26
  protected cache: PlatformCache;
27
+ protected refreshService: PlatformCacheRefreshService;
27
28
  protected prefix: string;
28
29
  protected blacklist: string[];
29
30
  intercept(context: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext): unknown;
30
- canRefreshInBackground(key: string, { refreshThreshold, ttl, cachedTTL }: {
31
- refreshThreshold?: number;
32
- ttl: any;
33
- cachedTTL?: number;
34
- }, next: Function, $ctx?: Context): Promise<void>;
35
31
  cacheMethod(context: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext): Promise<any>;
36
32
  cacheResponse(interceptorContext: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext): Promise<any>;
37
33
  protected getMethod($ctx?: Context): string | undefined;
@@ -51,14 +47,6 @@ export declare class PlatformCacheInterceptor implements InterceptorMethods {
51
47
  };
52
48
  protected getContextFromArgs(args: unknown[]): Context | undefined;
53
49
  protected shouldByPassCache(byPass: PlatformCacheOptions["byPass"], args: unknown[], $ctx?: Context): boolean;
54
- protected hasKeyInQueue(key: string): Promise<boolean>;
55
- protected addKeyToQueue(key: string): Promise<void>;
56
- protected deleteKeyFromQueue(key: string): Promise<void>;
57
- protected acquireRefreshLock(key: string): Promise<() => void>;
58
- protected getRefreshCooldownKey(key: string): string;
59
- protected getRefreshThresholdWithJitter(key: string, refreshThreshold: number): number;
60
- protected hasRefreshCooldown(key: string): Promise<boolean>;
61
- protected addRefreshCooldown(key: string, refreshThreshold: number): Promise<void>;
62
50
  protected sendResponse(cachedObject: PlatformCachedObject): any;
63
51
  }
64
52
  export {};
@@ -13,6 +13,29 @@ export declare class PlatformCache {
13
13
  get cache(): CacheManager | undefined;
14
14
  $onInit(): Promise<void>;
15
15
  getKeysOf(target: Type<any>, propertyKey: string | symbol): Promise<string[]>;
16
+ /**
17
+ * Returns the global cache key prefix from settings (`cache.prefix`).
18
+ */
19
+ cachePrefix(): string;
20
+ /**
21
+ * Builds the stable namespace segments used by cached entries for a method.
22
+ * The namespace combines the global cache prefix (if configured) and the method prefix.
23
+ */
24
+ buildNamespace(target: Type<any>, propertyKey: string | symbol): (string | symbol)[];
25
+ /**
26
+ * Builds the final cache key for a method call.
27
+ */
28
+ buildEntryKey(target: Type<any>, propertyKey: string | symbol, keyArgs: string): string;
29
+ /**
30
+ * Builds a key pattern scoped to a cached method namespace.
31
+ * Useful for listing/invalidation of keys produced by the same cached method.
32
+ */
33
+ buildEntryPattern(target: Type<any>, propertyKey: string | symbol, suffix?: string): string;
34
+ /**
35
+ * Builds internal keys used by cache housekeeping features
36
+ * (refresh queue and refresh cooldown markers).
37
+ */
38
+ buildInternalKey(namespace: "queue" | "refresh-cooldown", key: string): string;
16
39
  disabled(): boolean;
17
40
  defaultKeyResolver(): (args: any[], ctx?: any) => string;
18
41
  defaultTtl(): Ttl | undefined;
@@ -0,0 +1,22 @@
1
+ import { BaseContext } from "@tsed/di";
2
+ import { PlatformCache } from "./PlatformCache.js";
3
+ /**
4
+ * @platform
5
+ */
6
+ export declare class PlatformCacheRefreshService {
7
+ #private;
8
+ protected cache: PlatformCache;
9
+ hasKeyInQueue(key: string): Promise<boolean>;
10
+ addKeyToQueue(key: string): Promise<void>;
11
+ deleteKeyFromQueue(key: string): Promise<void>;
12
+ getRefreshThresholdWithJitter(key: string, refreshThreshold: number): number;
13
+ hasRefreshCooldown(key: string): Promise<boolean>;
14
+ addRefreshCooldown(key: string, refreshThreshold: number): Promise<void>;
15
+ refreshInBackground(key: string, { refreshThreshold, ttl, cachedTTL }: {
16
+ refreshThreshold?: number;
17
+ ttl: any;
18
+ cachedTTL?: number;
19
+ }, next: Function, $ctx?: BaseContext): Promise<void>;
20
+ acquireRefreshLock(key: string): Promise<() => void>;
21
+ protected getRefreshCooldownKey(key: string): string;
22
+ }
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.26.1",
5
+ "version": "8.27.0",
6
6
  "source": "./src/index.ts",
7
7
  "main": "./lib/esm/index.js",
8
8
  "module": "./lib/esm/index.js",
@@ -28,16 +28,16 @@
28
28
  "tslib": ">=2.7.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@tsed/barrels": "8.26.1",
32
- "@tsed/core": "8.26.1",
33
- "@tsed/di": "8.26.1",
34
- "@tsed/json-mapper": "8.26.1",
35
- "@tsed/schema": "8.26.1",
36
- "@tsed/typescript": "8.26.1",
31
+ "@tsed/barrels": "8.27.0",
32
+ "@tsed/core": "8.27.0",
33
+ "@tsed/di": "8.27.0",
34
+ "@tsed/json-mapper": "8.27.0",
35
+ "@tsed/schema": "8.27.0",
36
+ "@tsed/typescript": "8.27.0",
37
37
  "@types/micromatch": "^4.0.9",
38
38
  "eslint": "9.12.0",
39
39
  "typescript": "6.0.2",
40
- "vitest": "3.2.4"
40
+ "vitest": "4.1.4"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@tsed/core": ">=8.0.0",