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