koatty_cacheable 1.5.0 → 1.6.1

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,259 +1,316 @@
1
1
  /*!
2
2
  * @Author: richen
3
- * @Date: 2024-04-01 15:47:40
3
+ * @Date: 2025-06-09 13:04:37
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
9
  import { Helper } from 'koatty_lib';
9
10
  import { DefaultLogger } from 'koatty_logger';
10
11
  import { CacheStore } from 'koatty_store';
11
- import { IOCContainer } from 'koatty_container';
12
12
 
13
- /*
14
- * @Author: richen
15
- * @Date: 2020-07-06 19:53:43
16
- * @LastEditTime: 2024-04-01 15:44:15
17
- * @Description:
18
- * @Copyright (c) - <richenlin(at)gmail.com>
19
- */
20
- const longKey = 128;
21
- // storeCache
22
- const storeCache = {
23
- store: null
24
- };
25
- /**
26
- * get instances of storeCache
27
- *
28
- * @export
29
- * @param {Application} app
30
- * @returns {*} {CacheStore}
31
- */
32
- async function GetCacheStore(app) {
33
- var _a, _b;
34
- if (storeCache.store && storeCache.store.getConnection) {
35
- return storeCache.store;
36
- }
37
- const opt = (_b = (_a = app.config("CacheStore")) !== null && _a !== void 0 ? _a : app.config("CacheStore", "db")) !== null && _b !== void 0 ? _b : {};
38
- if (Helper.isEmpty(opt)) {
39
- DefaultLogger.Warn(`Missing CacheStore server configuration. Please write a configuration item with the key name 'CacheStore' in the db.ts file.`);
40
- }
41
- storeCache.store = CacheStore.getInstance(opt);
42
- if (!Helper.isFunction(storeCache.store.getConnection)) {
43
- throw Error(`CacheStore connection failed. `);
44
- }
45
- await storeCache.store.client.getConnection();
46
- return storeCache.store;
47
- }
48
- /**
49
- * initiation CacheStore connection and client.
50
- *
51
- */
52
- async function InitCacheStore() {
53
- if (storeCache.store !== null) {
54
- return;
55
- }
56
- const app = IOCContainer.getApp();
57
- app && app.once("appReady", async function () {
58
- await GetCacheStore(app);
59
- });
60
- }
61
- /**
62
- * Decorate this method to support caching.
63
- * The cache method returns a value to ensure that the next time
64
- * the method is executed with the same parameters, the results can be obtained
65
- * directly from the cache without the need to execute the method again.
66
- * CacheStore server config defined in db.ts.
67
- *
68
- * @export
69
- * @param {string} cacheName cache name
70
- * @param {CacheAbleOpt} [opt] cache options
71
- * e.g:
72
- * {
73
- * params: ["id"],
74
- * timeout: 30
75
- * }
76
- * Use the 'id' parameters of the method as cache subkeys, the cache expiration time 30s
77
- * @returns {MethodDecorator}
78
- */
79
- function CacheAble(cacheName, opt = {
80
- params: [],
81
- timeout: 300,
82
- }) {
83
- return (target, methodName, descriptor) => {
84
- const componentType = IOCContainer.getType(target);
85
- if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
86
- throw Error("This decorator only used in the service、component class.");
87
- }
88
- const { value, configurable, enumerable } = descriptor;
89
- opt = {
90
- ...{
91
- params: [],
92
- timeout: 300,
93
- }, ...opt
94
- };
95
- // 获取定义的参数位置
96
- const paramIndexes = getParamIndex(opt.params);
97
- descriptor = {
98
- configurable,
99
- enumerable,
100
- writable: true,
101
- async value(...props) {
102
- let cacheFlag = true;
103
- const store = await GetCacheStore(this.app).catch(() => {
104
- cacheFlag = false;
105
- DefaultLogger.Error("Get cache store instance failed.");
106
- return null;
107
- });
108
- if (cacheFlag) {
109
- // tslint:disable-next-line: one-variable-per-declaration
110
- let key = cacheName;
111
- if (props && props.length > 0) {
112
- for (const item of paramIndexes) {
113
- if (props[item] !== undefined) {
114
- const value = Helper.toString(props[item]);
115
- key += `:${opt.params[item]}:${value}`;
116
- }
117
- }
118
- // 防止key超长
119
- if (key.length > longKey) {
120
- key = Helper.murmurHash(key);
121
- }
122
- }
123
- let res = await store.get(key).catch(() => null);
124
- if (!Helper.isEmpty(res)) {
125
- return JSON.parse(res);
126
- }
127
- // tslint:disable-next-line: no-invalid-this
128
- res = await value.apply(this, props);
129
- // prevent cache penetration
130
- if (Helper.isTrueEmpty(res)) {
131
- res = "";
132
- }
133
- // async set store
134
- store.set(key, JSON.stringify(res), opt.timeout).catch(() => null);
135
- return res;
136
- }
137
- else {
138
- // tslint:disable-next-line: no-invalid-this
139
- return value.apply(this, props);
140
- }
141
- }
142
- };
143
- // bind app_ready hook event
144
- InitCacheStore();
145
- return descriptor;
146
- };
147
- }
148
- /**
149
- * Decorating the execution of this method will trigger a cache clear operation.
150
- * CacheStore server config defined in db.ts.
151
- *
152
- * @export
153
- * @param {string} cacheName cacheName cache name
154
- * @param {CacheEvictOpt} [opt] cache options
155
- * e.g:
156
- * {
157
- * params: ["id"],
158
- * delayedDoubleDeletion: true
159
- * }
160
- * Use the 'id' parameters of the method as cache subkeys,
161
- * and clear the cache after the method executed
162
- * @returns
163
- */
164
- function CacheEvict(cacheName, opt = {
165
- delayedDoubleDeletion: true,
166
- }) {
167
- return (target, methodName, descriptor) => {
168
- const componentType = IOCContainer.getType(target);
169
- if (componentType !== "SERVICE" && componentType !== "COMPONENT") {
170
- throw Error("This decorator only used in the service、component class.");
171
- }
172
- const { value, configurable, enumerable } = descriptor;
173
- // 获取定义的参数位置
174
- opt = {
175
- ...{
176
- eventTime: "Before",
177
- }, ...opt
178
- };
179
- const paramIndexes = getParamIndex(opt.params);
180
- descriptor = {
181
- configurable,
182
- enumerable,
183
- writable: true,
184
- async value(...props) {
185
- let cacheFlag = true;
186
- const store = await GetCacheStore(this.app).catch(() => {
187
- cacheFlag = false;
188
- DefaultLogger.Error("Get cache store instance failed.");
189
- return null;
190
- });
191
- if (cacheFlag) {
192
- let key = cacheName;
193
- if (props && props.length > 0) {
194
- for (const item of paramIndexes) {
195
- if (props[item] !== undefined) {
196
- const value = Helper.toString(props[item]);
197
- key += `:${opt.params[item]}:${value}`;
198
- }
199
- }
200
- // 防止key超长
201
- if (key.length > longKey) {
202
- key = Helper.murmurHash(key);
203
- }
204
- }
205
- // tslint:disable-next-line: no-invalid-this
206
- const res = await value.apply(this, props);
207
- store.del(key).catch(() => null);
208
- if (opt.delayedDoubleDeletion) {
209
- asyncDelayedExecution(2000, () => {
210
- store.del(key).catch(() => null);
211
- });
212
- }
213
- return res;
214
- }
215
- else {
216
- // tslint:disable-next-line: no-invalid-this
217
- return value.apply(this, props);
218
- }
219
- }
220
- };
221
- // bind app_ready hook event
222
- InitCacheStore();
223
- return descriptor;
224
- };
225
- }
226
- /**
227
- * @description:
228
- * @param {string[]} params
229
- * @return {*}
230
- */
231
- function getParamIndex(params) {
232
- const res = [];
233
- for (let i = 0; i < params.length; i++) {
234
- if (params.includes(params[i])) {
235
- res.push(i);
236
- }
237
- }
238
- return res;
239
- }
240
- /**
241
- *
242
- * @param ms
243
- * @returns
244
- */
245
- function delay(ms) {
246
- return new Promise(resolve => setTimeout(resolve, ms));
247
- }
248
- /**
249
- * async delayed execution func
250
- * @param ms
251
- * @param fn
252
- * @returns
253
- */
254
- async function asyncDelayedExecution(ms, fn) {
255
- await delay(ms); // delay ms second
256
- return fn();
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
+ const longKey = 128;
24
+ /**
25
+ * Extract parameter names from function signature
26
+ * @param func The function to extract parameters from
27
+ * @returns Array of parameter names
28
+ */
29
+ function getArgs(func) {
30
+ try {
31
+ // Match function parameters in parentheses
32
+ const args = func.toString().match(/.*?\(([^)]*)\)/);
33
+ if (args && args.length > 1) {
34
+ // Split parameters into array and clean them
35
+ return args[1].split(",").map(function (a) {
36
+ // Remove inline comments and whitespace
37
+ return a.replace(/\/\*.*\*\//, "").trim();
38
+ }).filter(function (ae) {
39
+ // Filter out empty strings
40
+ return ae;
41
+ });
42
+ }
43
+ return [];
44
+ }
45
+ catch (error) {
46
+ // Return empty array if parsing fails
47
+ return [];
48
+ }
49
+ }
50
+ /**
51
+ * Get parameter indexes based on parameter names
52
+ * @param funcParams Function parameter names
53
+ * @param params Target parameter names to find indexes for
54
+ * @returns Array of parameter indexes (-1 if not found)
55
+ */
56
+ function getParamIndex(funcParams, params) {
57
+ return params.map(param => funcParams.indexOf(param));
58
+ }
59
+ /**
60
+ * Generate cache key based on cache name and parameters
61
+ * @param cacheName base cache name
62
+ * @param paramIndexes parameter indexes
63
+ * @param paramNames parameter names
64
+ * @param props method arguments
65
+ * @returns generated cache key
66
+ */
67
+ function generateCacheKey(cacheName, paramIndexes, paramNames, props) {
68
+ let key = cacheName;
69
+ for (let i = 0; i < paramIndexes.length; i++) {
70
+ const paramIndex = paramIndexes[i];
71
+ if (paramIndex >= 0 && props[paramIndex] !== undefined) {
72
+ key += `:${paramNames[i]}:${Helper.toString(props[paramIndex])}`;
73
+ }
74
+ }
75
+ return key.length > longKey ? Helper.murmurHash(key) : key;
76
+ }
77
+ /**
78
+ * Create a delay promise
79
+ * @param ms Delay time in milliseconds
80
+ * @returns Promise that resolves after the specified delay
81
+ */
82
+ function delay(ms) {
83
+ return new Promise(resolve => setTimeout(resolve, ms));
84
+ }
85
+ /**
86
+ * Execute a function after a specified delay
87
+ * @param fn Function to execute
88
+ * @param ms Delay time in milliseconds
89
+ * @returns Promise that resolves with the function result
90
+ */
91
+ async function asyncDelayedExecution(fn, ms) {
92
+ await delay(ms);
93
+ return fn();
94
+ }
95
+
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
+ /*
175
+ * @Author: richen
176
+ * @Date: 2020-07-06 19:53:43
177
+ * @LastEditTime: 2024-11-07 15:53:46
178
+ * @Description:
179
+ * @Copyright (c) - <richenlin(at)gmail.com>
180
+ */
181
+ /**
182
+ * Decorate this method to support caching.
183
+ * The cache method returns a value to ensure that the next time
184
+ * the method is executed with the same parameters, the results can be obtained
185
+ * directly from the cache without the need to execute the method again.
186
+ * CacheStore server config defined in db.ts.
187
+ *
188
+ * @export
189
+ * @param {string} cacheName cache name
190
+ * @param {CacheAbleOpt} [opt] cache options
191
+ * e.g:
192
+ * {
193
+ * params: ["id"],
194
+ * timeout: 30
195
+ * }
196
+ * Use the 'id' parameters of the method as cache subkeys, the cache expiration time 30s
197
+ * @returns {MethodDecorator}
198
+ */
199
+ function CacheAble(cacheName, opt = {
200
+ params: [],
201
+ timeout: 300,
202
+ }) {
203
+ return (target, methodName, descriptor) => {
204
+ const componentType = IOCContainer.getType(target);
205
+ if (!["SERVICE", "COMPONENT"].includes(componentType)) {
206
+ throw Error("This decorator only used in the service、component class.");
207
+ }
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);
227
+ });
228
+ if (!Helper.isEmpty(res)) {
229
+ return JSON.parse(res);
230
+ }
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);
235
+ });
236
+ return result;
237
+ }
238
+ else {
239
+ // tslint:disable-next-line: no-invalid-this
240
+ return value.apply(this, props);
241
+ }
242
+ }
243
+ };
244
+ // bind app_ready hook event
245
+ InitCacheStore();
246
+ return descriptor;
247
+ };
248
+ }
249
+ /**
250
+ * Decorating the execution of this method will trigger a cache clear operation.
251
+ * CacheStore server config defined in db.ts.
252
+ *
253
+ * @export
254
+ * @param {string} cacheName cacheName cache name
255
+ * @param {CacheEvictOpt} [opt] cache options
256
+ * e.g:
257
+ * {
258
+ * params: ["id"],
259
+ * delayedDoubleDeletion: true
260
+ * }
261
+ * Use the 'id' parameters of the method as cache subkeys,
262
+ * and clear the cache after the method executed
263
+ * @returns
264
+ */
265
+ function CacheEvict(cacheName, opt = {
266
+ delayedDoubleDeletion: true,
267
+ }) {
268
+ return (target, methodName, descriptor) => {
269
+ const componentType = IOCContainer.getType(target);
270
+ if (!["SERVICE", "COMPONENT"].includes(componentType)) {
271
+ throw Error("This decorator only used in the service、component class.");
272
+ }
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);
291
+ store.del(key).catch((e) => {
292
+ DefaultLogger.error("Cache delete error:" + e.message);
293
+ });
294
+ if (opt.delayedDoubleDeletion) {
295
+ const delayTime = 5000;
296
+ asyncDelayedExecution(() => {
297
+ store.del(key).catch((e) => {
298
+ DefaultLogger.error("Cache double delete error:" + e.message);
299
+ });
300
+ }, delayTime);
301
+ }
302
+ return result;
303
+ }
304
+ else {
305
+ // If store is not available, execute method directly
306
+ return value.apply(this, props);
307
+ }
308
+ }
309
+ };
310
+ // bind app_ready hook event
311
+ InitCacheStore();
312
+ return descriptor;
313
+ };
257
314
  }
258
315
 
259
- export { CacheAble, CacheEvict, GetCacheStore };
316
+ export { CacheAble, CacheEvict, CloseCacheStore, GetCacheStore, InitCacheStore };