koatty_cacheable 1.6.0 → 2.0.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/dist/index.mjs CHANGED
@@ -1,25 +1,15 @@
1
1
  /*!
2
2
  * @Author: richen
3
- * @Date: 2024-11-07 16:06:44
3
+ * @Date: 2025-06-23 01:59:54
4
4
  * @License: BSD (3-Clause)
5
5
  * @Copyright (c) - <richenlin(at)gmail.com>
6
6
  * @HomePage: https://koatty.org/
7
7
  */
8
- import { IOCContainer } from 'koatty_container';
8
+ import { DefaultLogger, Logger } from 'koatty_logger';
9
9
  import { Helper } from 'koatty_lib';
10
- import { DefaultLogger } from 'koatty_logger';
11
10
  import { CacheStore } from 'koatty_store';
11
+ import { IOCContainer } from 'koatty_container';
12
12
 
13
- /* eslint-disable @typescript-eslint/no-unused-vars */
14
- /*
15
- * @Description:
16
- * @Usage:
17
- * @Author: richen
18
- * @Date: 2024-11-07 13:54:24
19
- * @LastEditTime: 2024-11-07 15:25:36
20
- * @License: BSD (3-Clause)
21
- * @Copyright (c): <richenlin(at)gmail.com>
22
- */
23
13
  // storeCache
24
14
  const storeCache = {
25
15
  store: null
@@ -53,65 +43,181 @@ async function GetCacheStore(app) {
53
43
  await storeCache.store.client.getConnection();
54
44
  return storeCache.store;
55
45
  }
46
+
47
+ /*
48
+ * @Author: richen
49
+ * @Date: 2020-07-06 19:53:43
50
+ * @LastEditTime: 2025-06-23 15:53:46
51
+ * @Description:
52
+ * @Copyright (c) - <richenlin(at)gmail.com>
53
+ */
54
+ class CacheManager {
55
+ static instance;
56
+ cacheStore = null;
57
+ defaultTimeout = 300;
58
+ defaultDelayedDoubleDeletion = true;
59
+ static getInstance() {
60
+ if (!CacheManager.instance) {
61
+ CacheManager.instance = new CacheManager();
62
+ }
63
+ return CacheManager.instance;
64
+ }
65
+ setCacheStore(store) {
66
+ this.cacheStore = store;
67
+ }
68
+ getCacheStore() {
69
+ return this.cacheStore;
70
+ }
71
+ setDefaultConfig(timeout, delayedDoubleDeletion) {
72
+ if (timeout !== undefined)
73
+ this.defaultTimeout = timeout;
74
+ if (delayedDoubleDeletion !== undefined)
75
+ this.defaultDelayedDoubleDeletion = delayedDoubleDeletion;
76
+ }
77
+ getDefaultTimeout() {
78
+ return this.defaultTimeout;
79
+ }
80
+ getDefaultDelayedDoubleDeletion() {
81
+ return this.defaultDelayedDoubleDeletion;
82
+ }
83
+ }
84
+
56
85
  /**
57
- * initiation CacheStore connection and client.
58
- *
86
+ * @Description: Cache injector, unified processing of all cache decorators
87
+ * @Usage:
88
+ * @Author: richen
89
+ * @Date: 2025-01-10 14:00:00
90
+ * @LastEditTime: 2025-01-10 14:00:00
91
+ * @License: BSD (3-Clause)
92
+ * @Copyright (c): <richenlin(at)gmail.com>
59
93
  */
60
- async function InitCacheStore() {
61
- if (storeCache.store) {
62
- return;
94
+ // import { Helper } from 'koatty_lib';
95
+ // Create logger instance
96
+ const logger$1 = new Logger();
97
+ /**
98
+ * Cache injector - initialize global cache manager and store
99
+ * @param options Cache options
100
+ * @param app Koatty application instance
101
+ */
102
+ async function injectCache(options, app) {
103
+ try {
104
+ logger$1.Debug('Initializing cache system...');
105
+ // Get cache store instance
106
+ const store = await GetCacheStore(app);
107
+ if (!store) {
108
+ logger$1.Warn('Cache store unavailable, cache system disabled');
109
+ return;
110
+ }
111
+ // Initialize global cache manager
112
+ const cacheManager = CacheManager.getInstance();
113
+ cacheManager.setCacheStore(store);
114
+ // Set default configuration
115
+ cacheManager.setDefaultConfig(options.cacheTimeout || 300, options.delayedDoubleDeletion !== undefined ? options.delayedDoubleDeletion : true);
116
+ logger$1.Info(`Cache system initialized successfully with timeout: ${options.cacheTimeout || 300}s`);
117
+ }
118
+ catch (error) {
119
+ logger$1.Error('Cache system initialization failed:', error);
120
+ }
121
+ }
122
+ /**
123
+ * Close cache store connection
124
+ * @param app Koatty application instance
125
+ */
126
+ async function closeCacheStore(_app) {
127
+ try {
128
+ logger$1.Debug('Closing cache store connection...');
129
+ // Reset global cache manager
130
+ const cacheManager = CacheManager.getInstance();
131
+ const store = cacheManager.getCacheStore();
132
+ await store?.close();
133
+ cacheManager.setCacheStore(null);
134
+ logger$1.Info('Cache store connection closed');
135
+ }
136
+ catch (error) {
137
+ logger$1.Error('Error closing cache store connection:', error);
63
138
  }
64
- const app = IOCContainer.getApp();
65
- app?.once("appReady", async () => {
66
- await GetCacheStore(app);
67
- });
68
139
  }
140
+
141
+ /* eslint-disable @typescript-eslint/no-unused-vars */
142
+ /*
143
+ * @Description:
144
+ * @Usage:
145
+ * @Author: richen
146
+ * @Date: 2024-11-07 13:54:24
147
+ * @LastEditTime: 2024-11-07 15:25:36
148
+ * @License: BSD (3-Clause)
149
+ * @Copyright (c): <richenlin(at)gmail.com>
150
+ */
151
+ const longKey = 128;
69
152
  /**
70
- * @description:
71
- * @param {*} func
72
- * @return {*}
153
+ * Extract parameter names from function signature
154
+ * @param func The function to extract parameters from
155
+ * @returns Array of parameter names
73
156
  */
74
157
  function getArgs(func) {
75
- // 首先匹配函数括弧里的参数
76
- const args = func.toString().match(/.*?\(([^)]*)\)/);
77
- if (args.length > 1) {
78
- // 分解参数成数组
79
- return args[1].split(",").map(function (a) {
80
- // 去空格和内联注释
81
- return a.replace(/\/\*.*\*\//, "").trim();
82
- }).filter(function (ae) {
83
- // 确保没有undefineds
84
- return ae;
85
- });
158
+ try {
159
+ // Match function parameters in parentheses
160
+ const args = func.toString().match(/.*?\(([^)]*)\)/);
161
+ if (args && args.length > 1) {
162
+ // Split parameters into array and clean them
163
+ return args[1].split(",").map(function (a) {
164
+ // Remove inline comments and whitespace
165
+ return a.replace(/\/\*.*\*\//, "").trim();
166
+ }).filter(function (ae) {
167
+ // Filter out empty strings
168
+ return ae;
169
+ });
170
+ }
171
+ return [];
172
+ }
173
+ catch (error) {
174
+ // Return empty array if parsing fails
175
+ return [];
86
176
  }
87
- return [];
88
177
  }
89
178
  /**
90
- * @description:
91
- * @param {string[]} funcParams
92
- * @param {string[]} params
93
- * @return {*}
179
+ * Get parameter indexes based on parameter names
180
+ * @param funcParams Function parameter names
181
+ * @param params Target parameter names to find indexes for
182
+ * @returns Array of parameter indexes (-1 if not found)
94
183
  */
95
184
  function getParamIndex(funcParams, params) {
96
185
  return params.map(param => funcParams.indexOf(param));
97
186
  }
98
187
  /**
99
- *
100
- * @param ms
101
- * @returns
188
+ * Generate cache key based on cache name and parameters
189
+ * @param cacheName base cache name
190
+ * @param paramIndexes parameter indexes
191
+ * @param paramNames parameter names
192
+ * @param props method arguments
193
+ * @returns generated cache key
194
+ */
195
+ function generateCacheKey(cacheName, paramIndexes, paramNames, props) {
196
+ let key = cacheName;
197
+ for (let i = 0; i < paramIndexes.length; i++) {
198
+ const paramIndex = paramIndexes[i];
199
+ if (paramIndex >= 0 && props[paramIndex] !== undefined) {
200
+ key += `:${paramNames[i]}:${Helper.toString(props[paramIndex])}`;
201
+ }
202
+ }
203
+ return key.length > longKey ? Helper.murmurHash(key) : key;
204
+ }
205
+ /**
206
+ * Create a delay promise
207
+ * @param ms Delay time in milliseconds
208
+ * @returns Promise that resolves after the specified delay
102
209
  */
103
210
  function delay(ms) {
104
211
  return new Promise(resolve => setTimeout(resolve, ms));
105
212
  }
106
213
  /**
107
- * async delayed execution func
108
- * @param fn
109
- * @param ms
110
- * @returns
214
+ * Execute a function after a specified delay
215
+ * @param fn Function to execute
216
+ * @param ms Delay time in milliseconds
217
+ * @returns Promise that resolves with the function result
111
218
  */
112
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
113
219
  async function asyncDelayedExecution(fn, ms) {
114
- await delay(ms); // delay ms second
220
+ await delay(ms);
115
221
  return fn();
116
222
  }
117
223
 
@@ -122,7 +228,17 @@ async function asyncDelayedExecution(fn, ms) {
122
228
  * @Description:
123
229
  * @Copyright (c) - <richenlin(at)gmail.com>
124
230
  */
125
- const longKey = 128;
231
+ // Create logger instance
232
+ const logger = new Logger();
233
+ // Define cache decorator types
234
+ var DecoratorType;
235
+ (function (DecoratorType) {
236
+ DecoratorType["CACHE_EVICT"] = "CACHE_EVICT";
237
+ DecoratorType["CACHE_ABLE"] = "CACHE_ABLE";
238
+ })(DecoratorType || (DecoratorType = {}));
239
+ // IOC container key constant
240
+ const COMPONENT_CACHE = "COMPONENT_CACHE";
241
+ const CACHE_METADATA_KEY = "CACHE_METADATA_KEY";
126
242
  /**
127
243
  * Decorate this method to support caching.
128
244
  * The cache method returns a value to ensure that the next time
@@ -141,59 +257,80 @@ const longKey = 128;
141
257
  * Use the 'id' parameters of the method as cache subkeys, the cache expiration time 30s
142
258
  * @returns {MethodDecorator}
143
259
  */
144
- function CacheAble(cacheName, opt = {
145
- params: [],
146
- timeout: 300,
147
- }) {
260
+ function CacheAble(cacheNameOrOpt, opt = {}) {
261
+ // Handle overloaded parameters
262
+ let cacheName;
263
+ let options;
264
+ if (typeof cacheNameOrOpt === 'string') {
265
+ cacheName = cacheNameOrOpt;
266
+ options = opt;
267
+ }
268
+ else {
269
+ options = cacheNameOrOpt || {};
270
+ cacheName = options.cacheName;
271
+ }
148
272
  return (target, methodName, descriptor) => {
149
273
  const componentType = IOCContainer.getType(target);
150
274
  if (!["SERVICE", "COMPONENT"].includes(componentType)) {
151
275
  throw Error("This decorator only used in the service、component class.");
152
276
  }
153
- const { value, configurable, enumerable } = descriptor;
154
- const mergedOpt = { ...{ params: [], timeout: 300 }, ...opt };
155
- // Get the parameter list of the method
156
- const funcParams = getArgs(target[methodName]);
157
- // Get the defined parameter location
158
- const paramIndexes = getParamIndex(funcParams, opt.params);
159
- descriptor = {
160
- configurable,
161
- enumerable,
162
- writable: true,
163
- async value(...props) {
164
- const store = await GetCacheStore(this.app).catch((e) => {
165
- DefaultLogger.Error("Get cache store instance failed." + e.message);
166
- return null;
167
- });
168
- if (store) {
169
- let key = cacheName;
170
- for (const item of paramIndexes) {
171
- if (props[item] !== undefined) {
172
- key += `:${mergedOpt.params[item]}:${Helper.toString(props[item])}`;
173
- }
174
- }
175
- key = key.length > longKey ? Helper.murmurHash(key) : key;
176
- const res = await store.get(key).catch((e) => {
177
- DefaultLogger.error("Cache get error:" + e.message);
277
+ // Generate cache name if not provided
278
+ const finalCacheName = cacheName || `${target.constructor.name}:${String(methodName)}`;
279
+ // Get original method
280
+ const originalMethod = descriptor.value;
281
+ if (!Helper.isFunction(originalMethod)) {
282
+ throw new Error(`CacheAble decorator can only be applied to methods`);
283
+ }
284
+ // Create wrapped method
285
+ descriptor.value = function (...args) {
286
+ const cacheManager = CacheManager.getInstance();
287
+ const store = cacheManager.getCacheStore();
288
+ // If cache store is not available, execute original method directly
289
+ if (!store) {
290
+ logger.Debug(`Cache store not available for ${finalCacheName}, executing original method`);
291
+ return originalMethod.apply(this, args);
292
+ }
293
+ // Get method parameter list
294
+ const funcParams = getArgs(originalMethod);
295
+ // Get cache parameter positions
296
+ const paramIndexes = getParamIndex(funcParams, options.params || []);
297
+ return (async () => {
298
+ try {
299
+ // Generate cache key
300
+ const key = generateCacheKey(finalCacheName, paramIndexes, options.params || [], args);
301
+ // Try to get data from cache
302
+ const cached = await store.get(key).catch((e) => {
303
+ logger.Debug("Cache get error:" + e.message);
304
+ return null;
178
305
  });
179
- if (!Helper.isEmpty(res)) {
180
- return JSON.parse(res);
306
+ if (!Helper.isEmpty(cached)) {
307
+ logger.Debug(`Cache hit for key: ${key}`);
308
+ try {
309
+ return JSON.parse(cached);
310
+ }
311
+ catch {
312
+ // If parse fails, return as string (for simple values)
313
+ return cached;
314
+ }
181
315
  }
182
- const result = await value.apply(this, props);
183
- // async refresh store
184
- store.set(key, Helper.isJSONObj(result) ? JSON.stringify(result) : result, mergedOpt.timeout).catch((e) => {
185
- DefaultLogger.error("Cache set error:" + e.message);
316
+ logger.Debug(`Cache miss for key: ${key}`);
317
+ // Execute original method
318
+ const result = await originalMethod.apply(this, args);
319
+ // Use decorator timeout if specified, otherwise use global default
320
+ const timeout = options.timeout || cacheManager.getDefaultTimeout();
321
+ // Asynchronously set cache
322
+ store.set(key, Helper.isJSONObj(result) ? JSON.stringify(result) : result, timeout).catch((e) => {
323
+ logger.Debug("Cache set error:" + e.message);
186
324
  });
187
325
  return result;
188
326
  }
189
- else {
190
- // tslint:disable-next-line: no-invalid-this
191
- return value.apply(this, props);
327
+ catch (error) {
328
+ logger.Debug(`CacheAble wrapper error: ${error.message}`);
329
+ // If cache operation fails, execute original method directly
330
+ return originalMethod.apply(this, args);
192
331
  }
193
- }
332
+ })();
194
333
  };
195
- // bind app_ready hook event
196
- InitCacheStore();
197
334
  return descriptor;
198
335
  };
199
336
  }
@@ -213,60 +350,116 @@ function CacheAble(cacheName, opt = {
213
350
  * and clear the cache after the method executed
214
351
  * @returns
215
352
  */
216
- function CacheEvict(cacheName, opt = {
217
- delayedDoubleDeletion: true,
218
- }) {
353
+ function CacheEvict(cacheNameOrOpt, opt = {}) {
354
+ // Handle overloaded parameters
355
+ let cacheName;
356
+ let options;
357
+ if (typeof cacheNameOrOpt === 'string') {
358
+ cacheName = cacheNameOrOpt;
359
+ options = opt;
360
+ }
361
+ else {
362
+ options = cacheNameOrOpt || {};
363
+ cacheName = options.cacheName;
364
+ }
219
365
  return (target, methodName, descriptor) => {
220
366
  const componentType = IOCContainer.getType(target);
221
367
  if (!["SERVICE", "COMPONENT"].includes(componentType)) {
222
368
  throw Error("This decorator only used in the service、component class.");
223
369
  }
224
- const { value, configurable, enumerable } = descriptor;
225
- opt = { ...{ delayedDoubleDeletion: true, }, ...opt };
226
- // Get the parameter list of the method
227
- const funcParams = getArgs(target[methodName]);
228
- // Get the defined parameter location
229
- const paramIndexes = getParamIndex(funcParams, opt.params);
230
- descriptor = {
231
- configurable,
232
- enumerable,
233
- writable: true,
234
- async value(...props) {
235
- const store = await GetCacheStore(this.app).catch((e) => {
236
- DefaultLogger.Error("Get cache store instance failed." + e.message);
237
- return null;
238
- });
239
- if (store) {
240
- let key = cacheName;
241
- for (const item of paramIndexes) {
242
- if (props[item] !== undefined) {
243
- key += `:${opt.params[item]}:${Helper.toString(props[item])}`;
244
- }
245
- }
246
- key = key.length > longKey ? Helper.murmurHash(key) : key;
247
- const result = await value.apply(this, props);
370
+ // Save class to IOC container for tracking
371
+ IOCContainer.saveClass("COMPONENT", target, COMPONENT_CACHE);
372
+ // Generate cache name if not provided
373
+ const finalCacheName = cacheName || `${target.constructor.name}:${String(methodName)}`;
374
+ // Get original method
375
+ const originalMethod = descriptor.value;
376
+ if (!Helper.isFunction(originalMethod)) {
377
+ throw new Error(`CacheEvict decorator can only be applied to methods`);
378
+ }
379
+ // Create wrapped method
380
+ descriptor.value = function (...args) {
381
+ const cacheManager = CacheManager.getInstance();
382
+ const store = cacheManager.getCacheStore();
383
+ // If cache store is not available, execute original method directly
384
+ if (!store) {
385
+ logger.Debug(`Cache store not available for ${finalCacheName}, executing original method`);
386
+ return originalMethod.apply(this, args);
387
+ }
388
+ // Get method parameter list
389
+ const funcParams = getArgs(originalMethod);
390
+ // Get cache parameter positions
391
+ const paramIndexes = getParamIndex(funcParams, options.params || []);
392
+ return (async () => {
393
+ try {
394
+ // Generate cache key
395
+ const key = generateCacheKey(finalCacheName, paramIndexes, options.params || [], args);
396
+ // Execute original method
397
+ const result = await originalMethod.apply(this, args);
398
+ // Immediately clear cache
248
399
  store.del(key).catch((e) => {
249
- DefaultLogger.Error("Cache delete error:" + e.message);
400
+ logger.Debug("Cache delete error:" + e.message);
250
401
  });
251
- if (opt.delayedDoubleDeletion) {
402
+ // Use decorator setting if specified, otherwise use global default
403
+ const enableDelayedDeletion = options.delayedDoubleDeletion !== undefined
404
+ ? options.delayedDoubleDeletion
405
+ : cacheManager.getDefaultDelayedDoubleDeletion();
406
+ // Delayed double deletion strategy
407
+ if (enableDelayedDeletion !== false) {
408
+ const delayTime = 5000;
252
409
  asyncDelayedExecution(() => {
253
410
  store.del(key).catch((e) => {
254
- DefaultLogger.error("Cache double delete error:" + e.message);
411
+ logger.Debug("Cache double delete error:" + e.message);
255
412
  });
256
- }, 5000);
257
- return result;
258
- }
259
- else {
260
- // tslint:disable-next-line: no-invalid-this
261
- return value.apply(this, props);
413
+ }, delayTime);
262
414
  }
415
+ return result;
263
416
  }
264
- }
417
+ catch (error) {
418
+ logger.Debug(`CacheEvict wrapper error: ${error.message}`);
419
+ // If cache operation fails, execute original method directly
420
+ return originalMethod.apply(this, args);
421
+ }
422
+ })();
265
423
  };
266
- // bind app_ready hook event
267
- InitCacheStore();
268
424
  return descriptor;
269
425
  };
270
426
  }
271
427
 
272
- export { CacheAble, CacheEvict, GetCacheStore };
428
+ /*
429
+ * @Description:
430
+ * @Usage:
431
+ * @Author: richen
432
+ * @Date: 2024-11-07 16:00:02
433
+ * @LastEditTime: 2024-11-07 16:00:05
434
+ * @License: BSD (3-Clause)
435
+ * @Copyright (c): <richenlin(at)gmail.com>
436
+ */
437
+ /**
438
+ * defaultOptions
439
+ */
440
+ const defaultOptions = {
441
+ cacheTimeout: 300,
442
+ delayedDoubleDeletion: true,
443
+ redisConfig: {
444
+ host: "localhost",
445
+ port: 6379,
446
+ password: "",
447
+ db: 0,
448
+ keyPrefix: "redlock:"
449
+ }
450
+ };
451
+ /**
452
+ * @param options - The options for the scheduled job
453
+ * @param app - The Koatty application instance
454
+ */
455
+ async function KoattyCache(options, app) {
456
+ options = { ...defaultOptions, ...options };
457
+ // inject cache decorator
458
+ await injectCache(options, app);
459
+ // Register cleanup on app stop
460
+ app.on('appStop', async () => {
461
+ await closeCacheStore();
462
+ });
463
+ }
464
+
465
+ export { CACHE_METADATA_KEY, CacheAble, CacheEvict, DecoratorType, KoattyCache };