koatty_cacheable 1.6.1 → 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,14 +1,142 @@
1
1
  /*!
2
2
  * @Author: richen
3
- * @Date: 2025-06-09 13:04:37
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
+
13
+ // storeCache
14
+ const storeCache = {
15
+ store: null
16
+ };
17
+ /**
18
+ * get instances of storeCache
19
+ *
20
+ * @export
21
+ * @param {Application} app
22
+ * @returns {*} {CacheStore}
23
+ */
24
+ async function GetCacheStore(app) {
25
+ if (storeCache.store && storeCache.store.getConnection) {
26
+ return storeCache.store;
27
+ }
28
+ let opt = {
29
+ type: "memory",
30
+ db: 0,
31
+ timeout: 30
32
+ };
33
+ if (app && Helper.isFunction(app.config)) {
34
+ opt = app.config("CacheStore") || app.config("CacheStore", "db");
35
+ if (Helper.isEmpty(opt)) {
36
+ DefaultLogger.Warn(`Missing CacheStore server configuration. Please write a configuration item with the key name 'CacheStore' in the db.ts file.`);
37
+ }
38
+ }
39
+ storeCache.store = CacheStore.getInstance(opt);
40
+ if (!Helper.isFunction(storeCache.store.getConnection)) {
41
+ throw Error(`CacheStore connection failed. `);
42
+ }
43
+ await storeCache.store.client.getConnection();
44
+ return storeCache.store;
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
+
85
+ /**
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>
93
+ */
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);
138
+ }
139
+ }
12
140
 
13
141
  /* eslint-disable @typescript-eslint/no-unused-vars */
14
142
  /*
@@ -93,84 +221,6 @@ async function asyncDelayedExecution(fn, ms) {
93
221
  return fn();
94
222
  }
95
223
 
96
- /*
97
- * @Description:
98
- * @Usage:
99
- * @Author: richen
100
- * @Date: 2024-11-07 16:00:02
101
- * @LastEditTime: 2024-11-07 16:00:05
102
- * @License: BSD (3-Clause)
103
- * @Copyright (c): <richenlin(at)gmail.com>
104
- */
105
- // storeCache
106
- const storeCache = {
107
- store: null
108
- };
109
- /**
110
- * get instances of storeCache
111
- *
112
- * @export
113
- * @param {Application} app
114
- * @returns {*} {CacheStore}
115
- */
116
- async function GetCacheStore(app) {
117
- if (storeCache.store && storeCache.store.getConnection) {
118
- return storeCache.store;
119
- }
120
- let opt = {
121
- type: "memory",
122
- db: 0,
123
- timeout: 30
124
- };
125
- if (app && Helper.isFunction(app.config)) {
126
- opt = app.config("CacheStore") || app.config("CacheStore", "db");
127
- if (Helper.isEmpty(opt)) {
128
- DefaultLogger.Warn(`Missing CacheStore server configuration. Please write a configuration item with the key name 'CacheStore' in the db.ts file.`);
129
- }
130
- }
131
- storeCache.store = CacheStore.getInstance(opt);
132
- if (!Helper.isFunction(storeCache.store.getConnection)) {
133
- throw Error(`CacheStore connection failed. `);
134
- }
135
- await storeCache.store.client.getConnection();
136
- return storeCache.store;
137
- }
138
- /**
139
- * initiation CacheStore connection and client.
140
- *
141
- */
142
- async function InitCacheStore() {
143
- if (storeCache.store) {
144
- return;
145
- }
146
- const app = IOCContainer.getApp();
147
- app?.once("appReady", async () => {
148
- await GetCacheStore(app);
149
- });
150
- }
151
- /**
152
- * Close cache store connection for cleanup (mainly for testing)
153
- */
154
- async function CloseCacheStore() {
155
- if (storeCache.store && storeCache.store.client) {
156
- try {
157
- const client = storeCache.store.client;
158
- if (typeof client.quit === 'function') {
159
- await client.quit();
160
- }
161
- else if (typeof client.close === 'function') {
162
- await client.close();
163
- }
164
- }
165
- catch {
166
- // Ignore cleanup errors
167
- }
168
- finally {
169
- storeCache.store = null;
170
- }
171
- }
172
- }
173
-
174
224
  /*
175
225
  * @Author: richen
176
226
  * @Date: 2020-07-06 19:53:43
@@ -178,6 +228,17 @@ async function CloseCacheStore() {
178
228
  * @Description:
179
229
  * @Copyright (c) - <richenlin(at)gmail.com>
180
230
  */
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";
181
242
  /**
182
243
  * Decorate this method to support caching.
183
244
  * The cache method returns a value to ensure that the next time
@@ -196,53 +257,80 @@ async function CloseCacheStore() {
196
257
  * Use the 'id' parameters of the method as cache subkeys, the cache expiration time 30s
197
258
  * @returns {MethodDecorator}
198
259
  */
199
- function CacheAble(cacheName, opt = {
200
- params: [],
201
- timeout: 300,
202
- }) {
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
+ }
203
272
  return (target, methodName, descriptor) => {
204
273
  const componentType = IOCContainer.getType(target);
205
274
  if (!["SERVICE", "COMPONENT"].includes(componentType)) {
206
275
  throw Error("This decorator only used in the service、component class.");
207
276
  }
208
- const { value, configurable, enumerable } = descriptor;
209
- const mergedOpt = { ...{ params: [], timeout: 300 }, ...opt };
210
- // Get the parameter list of the method
211
- const funcParams = getArgs(target[methodName]);
212
- // Get the defined parameter location
213
- const paramIndexes = getParamIndex(funcParams, mergedOpt.params || []);
214
- descriptor = {
215
- configurable,
216
- enumerable,
217
- writable: true,
218
- async value(...props) {
219
- const store = await GetCacheStore(this.app).catch((e) => {
220
- DefaultLogger.error("Get cache store instance failed." + e.message);
221
- return null;
222
- });
223
- if (store) {
224
- const key = generateCacheKey(cacheName, paramIndexes, mergedOpt.params, props);
225
- const res = await store.get(key).catch((e) => {
226
- 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;
227
305
  });
228
- if (!Helper.isEmpty(res)) {
229
- 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
+ }
230
315
  }
231
- const result = await value.apply(this, props);
232
- // async refresh store
233
- store.set(key, Helper.isJSONObj(result) ? JSON.stringify(result) : result, mergedOpt.timeout).catch((e) => {
234
- 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);
235
324
  });
236
325
  return result;
237
326
  }
238
- else {
239
- // tslint:disable-next-line: no-invalid-this
240
- 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);
241
331
  }
242
- }
332
+ })();
243
333
  };
244
- // bind app_ready hook event
245
- InitCacheStore();
246
334
  return descriptor;
247
335
  };
248
336
  }
@@ -262,55 +350,116 @@ function CacheAble(cacheName, opt = {
262
350
  * and clear the cache after the method executed
263
351
  * @returns
264
352
  */
265
- function CacheEvict(cacheName, opt = {
266
- delayedDoubleDeletion: true,
267
- }) {
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
+ }
268
365
  return (target, methodName, descriptor) => {
269
366
  const componentType = IOCContainer.getType(target);
270
367
  if (!["SERVICE", "COMPONENT"].includes(componentType)) {
271
368
  throw Error("This decorator only used in the service、component class.");
272
369
  }
273
- const { value, configurable, enumerable } = descriptor;
274
- opt = { ...{ delayedDoubleDeletion: true, }, ...opt };
275
- // Get the parameter list of the method
276
- const funcParams = getArgs(target[methodName]);
277
- // Get the defined parameter location
278
- const paramIndexes = getParamIndex(funcParams, opt.params || []);
279
- descriptor = {
280
- configurable,
281
- enumerable,
282
- writable: true,
283
- async value(...props) {
284
- const store = await GetCacheStore(this.app).catch((e) => {
285
- DefaultLogger.error("Get cache store instance failed." + e.message);
286
- return null;
287
- });
288
- if (store) {
289
- const key = generateCacheKey(cacheName, paramIndexes, opt.params || [], props);
290
- 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
291
399
  store.del(key).catch((e) => {
292
- DefaultLogger.error("Cache delete error:" + e.message);
400
+ logger.Debug("Cache delete error:" + e.message);
293
401
  });
294
- 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) {
295
408
  const delayTime = 5000;
296
409
  asyncDelayedExecution(() => {
297
410
  store.del(key).catch((e) => {
298
- DefaultLogger.error("Cache double delete error:" + e.message);
411
+ logger.Debug("Cache double delete error:" + e.message);
299
412
  });
300
413
  }, delayTime);
301
414
  }
302
415
  return result;
303
416
  }
304
- else {
305
- // If store is not available, execute method directly
306
- return value.apply(this, props);
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);
307
421
  }
308
- }
422
+ })();
309
423
  };
310
- // bind app_ready hook event
311
- InitCacheStore();
312
424
  return descriptor;
313
425
  };
314
426
  }
315
427
 
316
- export { CacheAble, CacheEvict, CloseCacheStore, GetCacheStore, InitCacheStore };
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 };
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koatty_cacheable",
3
- "version": "1.6.1",
3
+ "version": "2.0.0",
4
4
  "description": "Cacheable for koatty.",
5
5
  "scripts": {
6
6
  "build": "npm run build:js && npm run build:dts && npm run build:doc && npm run build:cp",
@@ -16,7 +16,7 @@
16
16
  "release:pre": "npm run release -- --prerelease",
17
17
  "release:major": "npm run release -- --release-as major",
18
18
  "release:minor": "npm run release -- --release-as minor",
19
- "test": "npm run eslint && jest --passWithNoTests",
19
+ "test": "NODE_ENV=test npm run eslint && NODE_ENV=test jest --passWithNoTests",
20
20
  "test:cov": "jest --collectCoverage --detectOpenHandles",
21
21
  "version": "conventional-changelog -p angular -i CHANGELOG.md -s"
22
22
  },
@@ -67,6 +67,7 @@
67
67
  "@types/koa": "^2.x.x",
68
68
  "@types/lodash": "^4.x.x",
69
69
  "@types/node": "^22.x.x",
70
+ "@types/ws": "^8.18.1",
70
71
  "@typescript-eslint/eslint-plugin": "^8.x.x",
71
72
  "@typescript-eslint/parser": "^8.x.x",
72
73
  "conventional-changelog-cli": "^5.x.x",
@@ -87,12 +88,14 @@
87
88
  },
88
89
  "dependencies": {
89
90
  "koatty_container": "^1.x.x",
91
+ "koatty_core": "^1.x.x",
90
92
  "koatty_lib": "^1.x.x",
91
93
  "koatty_logger": "^2.x.x",
92
- "koatty_store": "^1.8.0"
94
+ "koatty_store": "^1.x.x"
93
95
  },
94
96
  "peerDependencies": {
95
97
  "koatty_container": "^1.x.x",
98
+ "koatty_core": "^1.x.x",
96
99
  "koatty_lib": "^1.x.x",
97
100
  "koatty_logger": "^2.x.x"
98
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koatty_cacheable",
3
- "version": "1.6.1",
3
+ "version": "2.0.0",
4
4
  "description": "Cacheable for koatty.",
5
5
  "scripts": {
6
6
  "build": "npm run build:js && npm run build:dts && npm run build:doc && npm run build:cp",
@@ -16,7 +16,7 @@
16
16
  "release:pre": "npm run release -- --prerelease",
17
17
  "release:major": "npm run release -- --release-as major",
18
18
  "release:minor": "npm run release -- --release-as minor",
19
- "test": "npm run eslint && jest --passWithNoTests",
19
+ "test": "NODE_ENV=test npm run eslint && NODE_ENV=test jest --passWithNoTests",
20
20
  "test:cov": "jest --collectCoverage --detectOpenHandles",
21
21
  "version": "conventional-changelog -p angular -i CHANGELOG.md -s"
22
22
  },
@@ -67,6 +67,7 @@
67
67
  "@types/koa": "^2.x.x",
68
68
  "@types/lodash": "^4.x.x",
69
69
  "@types/node": "^22.x.x",
70
+ "@types/ws": "^8.18.1",
70
71
  "@typescript-eslint/eslint-plugin": "^8.x.x",
71
72
  "@typescript-eslint/parser": "^8.x.x",
72
73
  "conventional-changelog-cli": "^5.x.x",
@@ -87,12 +88,14 @@
87
88
  },
88
89
  "dependencies": {
89
90
  "koatty_container": "^1.x.x",
91
+ "koatty_core": "^1.x.x",
90
92
  "koatty_lib": "^1.x.x",
91
93
  "koatty_logger": "^2.x.x",
92
- "koatty_store": "^1.8.0"
94
+ "koatty_store": "^1.x.x"
93
95
  },
94
96
  "peerDependencies": {
95
97
  "koatty_container": "^1.x.x",
98
+ "koatty_core": "^1.x.x",
96
99
  "koatty_lib": "^1.x.x",
97
100
  "koatty_logger": "^2.x.x"
98
101
  }