@tsed/platform-cache 8.25.3 → 8.25.4

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,9 +1,7 @@
1
- import { __decorate, __metadata } from "tslib";
2
1
  import { IncomingMessage, ServerResponse } from "node:http";
3
2
  import { isClass, isString, nameOf } from "@tsed/core";
4
- import { Constant, context, DIContext, Inject, Interceptor, runInContext } from "@tsed/di";
3
+ import { constant, context, DIContext, inject, interceptor, logger, runInContext } from "@tsed/di";
5
4
  import { deserialize, serialize } from "@tsed/json-mapper";
6
- import { Logger } from "@tsed/logger";
7
5
  import { PlatformCache } from "../services/PlatformCache.js";
8
6
  import { getPrefix } from "../utils/getPrefix.js";
9
7
  import { isEndpoint } from "../utils/isEndpoint.js";
@@ -20,7 +18,19 @@ const cleanHeaders = (headers, blacklist) => {
20
18
  /**
21
19
  * @platform
22
20
  */
23
- let PlatformCacheInterceptor = class PlatformCacheInterceptor {
21
+ export class PlatformCacheInterceptor {
22
+ constructor() {
23
+ this.cache = inject(PlatformCache);
24
+ this.prefix = constant("cache.prefix", "");
25
+ this.blacklist = constant("cache.ignoreHeaders", [
26
+ "content-length",
27
+ "x-request-id",
28
+ "cache-control",
29
+ "vary",
30
+ "content-encoding"
31
+ ]);
32
+ }
33
+ static #refreshLocks = new Map();
24
34
  intercept(context, next) {
25
35
  if (this.cache.disabled()) {
26
36
  return next();
@@ -30,14 +40,24 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
30
40
  }
31
41
  return this.cacheResponse(context, next);
32
42
  }
33
- async canRefreshInBackground(key, { refreshThreshold, ttl }, next, $ctx) {
34
- const inQueue = await this.hasKeyInQueue(key);
35
- if (refreshThreshold && !inQueue) {
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
+ }
36
54
  await this.addKeyToQueue(key);
37
55
  const currentTTL = await this.cache.ttl(key);
38
- const calculatedTTL = this.cache.calculateTTL(currentTTL, ttl);
56
+ const calculatedTTL = cachedTTL ?? (typeof ttl === "number" ? ttl : undefined);
57
+ const refreshThresholdWithJitter = this.getRefreshThresholdWithJitter(key, refreshThreshold);
39
58
  try {
40
- if (currentTTL === undefined || currentTTL < calculatedTTL - refreshThreshold) {
59
+ if (calculatedTTL !== undefined && (currentTTL === undefined || currentTTL < calculatedTTL - refreshThresholdWithJitter)) {
60
+ await this.addRefreshCooldown(key, refreshThresholdWithJitter);
41
61
  if ($ctx) {
42
62
  await runInContext($ctx, () => next());
43
63
  }
@@ -50,6 +70,9 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
50
70
  await this.deleteKeyFromQueue(key);
51
71
  }
52
72
  }
73
+ finally {
74
+ release();
75
+ }
53
76
  }
54
77
  async cacheMethod(context, next) {
55
78
  const { key, type, ttl, collectionType, refreshThreshold, args, canCache, $ctx, byPass } = this.getOptions(context, false);
@@ -60,7 +83,7 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
60
83
  if (!canCache || (canCache && canCache(result))) {
61
84
  const calculatedTTL = this.cache.calculateTTL(result, ttl);
62
85
  const data = serialize(result, { type, collectionType });
63
- this.cache.setCachedObject(key, data, { args, ttl: calculatedTTL });
86
+ this.cache.setCachedObject(key, data, { args, ...(calculatedTTL !== undefined && { ttl: calculatedTTL }) });
64
87
  }
65
88
  };
66
89
  const cachedObject = await this.cache.getCachedObject(key);
@@ -71,10 +94,10 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
71
94
  }
72
95
  return result;
73
96
  }
74
- this.canRefreshInBackground(key, { refreshThreshold, ttl }, async () => {
97
+ this.canRefreshInBackground(key, { refreshThreshold, ttl, cachedTTL: cachedObject.ttl }, async () => {
75
98
  const result = await next();
76
99
  await set(result);
77
- }, $ctx).catch((er) => this.logger.error({
100
+ }, $ctx).catch((er) => logger().error({
78
101
  event: "CACHE_ERROR",
79
102
  method: "cacheMethod",
80
103
  concerned_key: key,
@@ -102,17 +125,19 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
102
125
  }
103
126
  const result = await next();
104
127
  const calculatedTTL = this.cache.calculateTTL(result, ttl);
105
- currentCtx.response?.setHeaders({
106
- "cache-control": `max-age=${calculatedTTL}`
107
- });
128
+ if (calculatedTTL !== undefined) {
129
+ currentCtx.response?.setHeaders({
130
+ "cache-control": `max-age=${calculatedTTL}`
131
+ });
132
+ }
108
133
  currentCtx.response?.onEnd(() => {
109
134
  if (!useCache) {
110
135
  return;
111
136
  }
112
137
  this.cache.setCachedObject(key, currentCtx.response.getBody(), {
113
- ttl: calculatedTTL,
114
138
  args,
115
- headers: cleanHeaders(currentCtx.response.getHeaders(), this.blacklist)
139
+ headers: cleanHeaders(currentCtx.response.getHeaders(), this.blacklist),
140
+ ...(calculatedTTL !== undefined && { ttl: calculatedTTL })
116
141
  });
117
142
  });
118
143
  return result;
@@ -177,6 +202,41 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
177
202
  async deleteKeyFromQueue(key) {
178
203
  await this.cache.del(`$$queue:${key}`);
179
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
+ }
180
240
  sendResponse(cachedObject) {
181
241
  const { headers, ttl } = cachedObject;
182
242
  const $ctx = context();
@@ -191,31 +251,12 @@ let PlatformCacheInterceptor = class PlatformCacheInterceptor {
191
251
  .setHeaders({
192
252
  ...headers,
193
253
  "x-cached": "true",
194
- "cache-control": `max-age=${ttl}`
254
+ ...(ttl !== undefined && { "cache-control": `max-age=${ttl}` })
195
255
  })
196
256
  .body(data);
197
257
  return cachedObject.data;
198
258
  }
199
259
  return cachedObject.data;
200
260
  }
201
- };
202
- __decorate([
203
- Inject(),
204
- __metadata("design:type", PlatformCache)
205
- ], PlatformCacheInterceptor.prototype, "cache", void 0);
206
- __decorate([
207
- Inject(),
208
- __metadata("design:type", Logger)
209
- ], PlatformCacheInterceptor.prototype, "logger", void 0);
210
- __decorate([
211
- Constant("cache.prefix", ""),
212
- __metadata("design:type", String)
213
- ], PlatformCacheInterceptor.prototype, "prefix", void 0);
214
- __decorate([
215
- Constant("cache.ignoreHeaders", ["content-length", "x-request-id", "cache-control", "vary", "content-encoding"]),
216
- __metadata("design:type", Array)
217
- ], PlatformCacheInterceptor.prototype, "blacklist", void 0);
218
- PlatformCacheInterceptor = __decorate([
219
- Interceptor()
220
- ], PlatformCacheInterceptor);
221
- export { PlatformCacheInterceptor };
261
+ }
262
+ interceptor(PlatformCacheInterceptor);
@@ -1,9 +1,8 @@
1
- import { __decorate, __metadata } from "tslib";
2
1
  import { AsyncLocalStorage } from "node:async_hooks";
3
2
  import { isClass, isFunction, isString } from "@tsed/core";
4
- import { Configuration, Inject, InjectorService, Module } from "@tsed/di";
3
+ import { constant, injectable, logger, ProviderScope, ProviderType } from "@tsed/di";
4
+ import { $asyncEmit } from "@tsed/hooks";
5
5
  import { deserialize, serialize } from "@tsed/json-mapper";
6
- import { Logger } from "@tsed/logger";
7
6
  import { getPrefix } from "../utils/getPrefix.js";
8
7
  const defaultKeyResolver = (args) => {
9
8
  return args.map((arg) => (isClass(arg) ? JSON.stringify(serialize(arg)) : arg)).join(":");
@@ -12,16 +11,16 @@ const storage = new AsyncLocalStorage();
12
11
  /**
13
12
  * @platform
14
13
  */
15
- let PlatformCache = class PlatformCache {
14
+ export class PlatformCache {
16
15
  #cache;
17
16
  get cache() {
18
17
  return this.#cache;
19
18
  }
20
19
  async $onInit() {
21
- const settings = this.settings.get("cache");
20
+ const settings = constant("cache");
22
21
  if (settings) {
23
22
  this.#cache = await this.createCacheManager(settings);
24
- await this.injector.emit("$onCreateCacheManager", this.#cache);
23
+ await $asyncEmit("$onCreateCacheManager", [this.#cache]);
25
24
  }
26
25
  }
27
26
  getKeysOf(target, propertyKey) {
@@ -29,13 +28,13 @@ let PlatformCache = class PlatformCache {
29
28
  return this.keys(`${prefix.join(":")}:*`);
30
29
  }
31
30
  disabled() {
32
- return !this.settings.get("cache");
31
+ return !constant("cache");
33
32
  }
34
33
  defaultKeyResolver() {
35
- return this.settings.get("cache.keyResolver", defaultKeyResolver);
34
+ return constant("cache.keyResolver", defaultKeyResolver);
36
35
  }
37
36
  defaultTtl() {
38
- return this.settings.get("cache.ttl");
37
+ return constant("cache.ttl");
39
38
  }
40
39
  calculateTTL(result, currentTtl) {
41
40
  const ttl = currentTtl === undefined ? this.defaultTtl() : currentTtl;
@@ -65,7 +64,7 @@ let PlatformCache = class PlatformCache {
65
64
  return await this.get(key);
66
65
  }
67
66
  catch (er) {
68
- this.logger.error({
67
+ logger().error({
69
68
  event: "CACHE_ERROR",
70
69
  method: "getCachedObject",
71
70
  error: er
@@ -74,15 +73,15 @@ let PlatformCache = class PlatformCache {
74
73
  }
75
74
  async setCachedObject(key, data, opts) {
76
75
  try {
76
+ const { ttl, ...rest } = opts;
77
77
  await this.set(key, {
78
- ...opts,
78
+ ...rest,
79
+ ...(ttl !== undefined && { ttl }),
79
80
  data: JSON.stringify(data)
80
- }, {
81
- ttl: opts.ttl
82
- });
81
+ }, ttl !== undefined ? { ttl } : undefined);
83
82
  }
84
83
  catch (er) {
85
- this.logger.error({
84
+ logger().error({
86
85
  event: "CACHE_ERROR",
87
86
  method: "setCachedObject",
88
87
  error: er
@@ -142,20 +141,5 @@ let PlatformCache = class PlatformCache {
142
141
  }
143
142
  return store;
144
143
  }
145
- };
146
- __decorate([
147
- Configuration(),
148
- __metadata("design:type", Object)
149
- ], PlatformCache.prototype, "settings", void 0);
150
- __decorate([
151
- Inject(),
152
- __metadata("design:type", InjectorService)
153
- ], PlatformCache.prototype, "injector", void 0);
154
- __decorate([
155
- Inject(),
156
- __metadata("design:type", Logger)
157
- ], PlatformCache.prototype, "logger", void 0);
158
- PlatformCache = __decorate([
159
- Module()
160
- ], PlatformCache);
161
- export { PlatformCache };
144
+ }
145
+ injectable(PlatformCache).type(ProviderType.MODULE).scope(ProviderScope.SINGLETON);
@@ -1,5 +1,4 @@
1
1
  import { type BaseContext, DIContext, InterceptorContext, InterceptorMethods, InterceptorNext } from "@tsed/di";
2
- import { Logger } from "@tsed/logger";
3
2
  import { PlatformCachedObject } from "../interfaces/PlatformCachedObject.js";
4
3
  import { PlatformCacheOptions } from "../interfaces/PlatformCacheOptions.js";
5
4
  import { PlatformCache } from "../services/PlatformCache.js";
@@ -23,14 +22,15 @@ type Context = DIContext & {
23
22
  * @platform
24
23
  */
25
24
  export declare class PlatformCacheInterceptor implements InterceptorMethods {
25
+ #private;
26
26
  protected cache: PlatformCache;
27
- protected logger: Logger;
28
27
  protected prefix: string;
29
28
  protected blacklist: string[];
30
29
  intercept(context: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext): unknown;
31
- canRefreshInBackground(key: string, { refreshThreshold, ttl }: {
30
+ canRefreshInBackground(key: string, { refreshThreshold, ttl, cachedTTL }: {
32
31
  refreshThreshold?: number;
33
32
  ttl: any;
33
+ cachedTTL?: number;
34
34
  }, next: Function, $ctx?: Context): Promise<void>;
35
35
  cacheMethod(context: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext): Promise<any>;
36
36
  cacheResponse(interceptorContext: InterceptorContext<any, PlatformCacheOptions>, next: InterceptorNext): Promise<any>;
@@ -54,6 +54,11 @@ export declare class PlatformCacheInterceptor implements InterceptorMethods {
54
54
  protected hasKeyInQueue(key: string): Promise<boolean>;
55
55
  protected addKeyToQueue(key: string): Promise<void>;
56
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>;
57
62
  protected sendResponse(cachedObject: PlatformCachedObject): any;
58
63
  }
59
64
  export {};
@@ -1,5 +1,5 @@
1
1
  export interface PlatformCachedObject {
2
- ttl: number;
2
+ ttl?: number;
3
3
  args: any[];
4
4
  data: any;
5
5
  expires: number;
@@ -1,7 +1,5 @@
1
1
  import { Type } from "@tsed/core";
2
- import { Configuration, InjectorService } from "@tsed/di";
3
2
  import { JsonDeserializerOptions } from "@tsed/json-mapper";
4
- import { Logger } from "@tsed/logger";
5
3
  import type { Cache, CachingConfig, MultiCache } from "cache-manager";
6
4
  import { PlatformCacheSettings } from "../interfaces/interfaces.js";
7
5
  import { PlatformCachedObject } from "../interfaces/PlatformCachedObject.js";
@@ -12,23 +10,20 @@ export type Ttl = number | ((result: any) => number);
12
10
  */
13
11
  export declare class PlatformCache {
14
12
  #private;
15
- protected settings: Configuration;
16
- protected injector: InjectorService;
17
- protected logger: Logger;
18
13
  get cache(): CacheManager | undefined;
19
14
  $onInit(): Promise<void>;
20
15
  getKeysOf(target: Type<any>, propertyKey: string | symbol): Promise<string[]>;
21
16
  disabled(): boolean;
22
17
  defaultKeyResolver(): (args: any[], ctx?: any) => string;
23
- defaultTtl(): Ttl;
24
- calculateTTL(result?: any, currentTtl?: Ttl): number;
18
+ defaultTtl(): Ttl | undefined;
19
+ calculateTTL(result?: any, currentTtl?: Ttl): number | undefined;
25
20
  ttl(key: string): Promise<void> | Promise<number>;
26
21
  wrap<T>(key: string, fetch: () => Promise<T>, ttl?: number): Promise<T>;
27
22
  get<T>(key: string, options?: JsonDeserializerOptions): Promise<T | undefined>;
28
23
  set<T>(key: string, value: any, options?: CachingConfig<T>): Promise<T | undefined>;
29
24
  getCachedObject(key: string): Promise<PlatformCachedObject | undefined>;
30
25
  setCachedObject(key: string, data: any, opts: {
31
- ttl: number;
26
+ ttl?: number;
32
27
  } & Record<string, any>): Promise<void>;
33
28
  del(key: string): Promise<void>;
34
29
  reset(): Promise<void>;
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.25.3",
5
+ "version": "8.25.4",
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.25.3",
32
- "@tsed/core": "8.25.3",
33
- "@tsed/di": "8.25.3",
34
- "@tsed/json-mapper": "8.25.3",
35
- "@tsed/schema": "8.25.3",
36
- "@tsed/typescript": "8.25.3",
31
+ "@tsed/barrels": "8.25.4",
32
+ "@tsed/core": "8.25.4",
33
+ "@tsed/di": "8.25.4",
34
+ "@tsed/json-mapper": "8.25.4",
35
+ "@tsed/schema": "8.25.4",
36
+ "@tsed/typescript": "8.25.4",
37
37
  "@types/micromatch": "^4.0.9",
38
38
  "eslint": "9.12.0",
39
39
  "typescript": "5.9.3",