@jintianxiayu/cache-decorator 0.1.2

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.
Files changed (55) hide show
  1. package/README.md +133 -0
  2. package/dist/core/cache-provider-registry.d.ts +30 -0
  3. package/dist/core/cache-provider-registry.d.ts.map +1 -0
  4. package/dist/core/cache-provider-registry.js +52 -0
  5. package/dist/core/cache-provider-registry.js.map +1 -0
  6. package/dist/core/cache-provider.d.ts +34 -0
  7. package/dist/core/cache-provider.d.ts.map +1 -0
  8. package/dist/core/cache-provider.js +3 -0
  9. package/dist/core/cache-provider.js.map +1 -0
  10. package/dist/core/key-builder.d.ts +14 -0
  11. package/dist/core/key-builder.d.ts.map +1 -0
  12. package/dist/core/key-builder.js +38 -0
  13. package/dist/core/key-builder.js.map +1 -0
  14. package/dist/core/native-cache.d.ts +18 -0
  15. package/dist/core/native-cache.d.ts.map +1 -0
  16. package/dist/core/native-cache.js +46 -0
  17. package/dist/core/native-cache.js.map +1 -0
  18. package/dist/core/pending-cache.d.ts +12 -0
  19. package/dist/core/pending-cache.d.ts.map +1 -0
  20. package/dist/core/pending-cache.js +29 -0
  21. package/dist/core/pending-cache.js.map +1 -0
  22. package/dist/core/redis-cache.d.ts +20 -0
  23. package/dist/core/redis-cache.d.ts.map +1 -0
  24. package/dist/core/redis-cache.js +58 -0
  25. package/dist/core/redis-cache.js.map +1 -0
  26. package/dist/decorators/cache-evict.d.ts +23 -0
  27. package/dist/decorators/cache-evict.d.ts.map +1 -0
  28. package/dist/decorators/cache-evict.js +54 -0
  29. package/dist/decorators/cache-evict.js.map +1 -0
  30. package/dist/decorators/cache.d.ts +37 -0
  31. package/dist/decorators/cache.d.ts.map +1 -0
  32. package/dist/decorators/cache.js +74 -0
  33. package/dist/decorators/cache.js.map +1 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +25 -0
  37. package/dist/index.js.map +1 -0
  38. package/jest.config.js +14 -0
  39. package/package.json +30 -0
  40. package/src/core/cache-provider-registry.ts +53 -0
  41. package/src/core/cache-provider.ts +37 -0
  42. package/src/core/key-builder.ts +31 -0
  43. package/src/core/native-cache.ts +53 -0
  44. package/src/core/pending-cache.ts +26 -0
  45. package/src/core/redis-cache.ts +62 -0
  46. package/src/decorators/cache-evict.ts +73 -0
  47. package/src/decorators/cache.ts +129 -0
  48. package/src/index.ts +9 -0
  49. package/test/cache-evict.test.ts +178 -0
  50. package/test/cache-provider-registry.test.ts +36 -0
  51. package/test/cache.test.ts +256 -0
  52. package/test/key-builder.test.ts +37 -0
  53. package/test/native-cache.test.ts +71 -0
  54. package/test/pending-cache.test.ts +56 -0
  55. package/tsconfig.json +8 -0
@@ -0,0 +1,37 @@
1
+ import 'reflect-metadata';
2
+ /**
3
+ * 缓存 key 解析器类型
4
+ * - null: 使用自动生成逻辑
5
+ * - string: 直接作为 key 值的一部分
6
+ * - function: 接收方法参数数组,返回自定义字符串
7
+ */
8
+ export type CacheKeyResolver = null | string | ((...args: unknown[]) => string);
9
+ /**
10
+ * @Cache 装饰器配置项
11
+ */
12
+ export interface CacheOptions {
13
+ /**
14
+ * 过期时间(秒)
15
+ */
16
+ ttl?: number;
17
+ /**
18
+ * 指定 CacheProvider 名称
19
+ */
20
+ providerName?: string;
21
+ /**
22
+ * 自定义缓存 key 生成逻辑
23
+ * - undefined/null: 使用默认逻辑 KeyBuilder.build(cacheName, args)
24
+ * - string: 使用 KeyBuilder.build(cacheName, [key])
25
+ * - function: 调用函数后使用 KeyBuilder.build(cacheName, [result])
26
+ */
27
+ key?: CacheKeyResolver;
28
+ }
29
+ /**
30
+ * 缓存装饰器
31
+ * 为方法添加声明式缓存功能,支持 TTL 过期和请求合并
32
+ * @param cacheName 缓存名称
33
+ * @param options 配置项
34
+ */
35
+ export declare function Cache(cacheName: string, options?: CacheOptions): <T>(_target: object, _propertyKey: string, descriptor: PropertyDescriptor) => void;
36
+ export { CacheProviderRegistry } from '../core/cache-provider-registry';
37
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/decorators/cache.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAK1B;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,MAAM,CAAC,CAAC;AAEhF;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,gBAAgB,CAAC;CACxB;AA4CD;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,IAC5C,CAAC,EAChB,SAAS,MAAM,EACf,cAAc,MAAM,EACpB,YAAY,kBAAkB,UAsCjC;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC"}
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CacheProviderRegistry = void 0;
4
+ exports.Cache = Cache;
5
+ require("reflect-metadata");
6
+ const cache_provider_registry_1 = require("../core/cache-provider-registry");
7
+ const key_builder_1 = require("../core/key-builder");
8
+ const pending_cache_1 = require("../core/pending-cache");
9
+ const pendingCache = new pending_cache_1.PendingCache();
10
+ /**
11
+ * 解析缓存 key
12
+ * @param cacheName 缓存名称
13
+ * @param keyResolver key 解析器
14
+ * @param args 方法参数数组
15
+ * @returns 解析后的缓存 key
16
+ */
17
+ function resolveCacheKey(cacheName, keyResolver, args) {
18
+ if (keyResolver === undefined || keyResolver === null) {
19
+ return key_builder_1.KeyBuilder.build(cacheName, args);
20
+ }
21
+ if (typeof keyResolver === 'string') {
22
+ return key_builder_1.KeyBuilder.build(cacheName, [keyResolver]);
23
+ }
24
+ try {
25
+ return key_builder_1.KeyBuilder.build(cacheName, [keyResolver(...args)]);
26
+ }
27
+ catch {
28
+ return key_builder_1.KeyBuilder.build(cacheName, args);
29
+ }
30
+ }
31
+ /**
32
+ * 缓存装饰器
33
+ * 为方法添加声明式缓存功能,支持 TTL 过期和请求合并
34
+ * @param cacheName 缓存名称
35
+ * @param options 配置项
36
+ */
37
+ function Cache(cacheName, options) {
38
+ return function (_target, _propertyKey, descriptor) {
39
+ const originalMethod = descriptor.value;
40
+ descriptor.value = async function (...args) {
41
+ const cacheKey = resolveCacheKey(cacheName, options?.key, args);
42
+ const providerName = options?.providerName;
43
+ const provider = cache_provider_registry_1.CacheProviderRegistry.get(providerName);
44
+ const cached = await provider.get(cacheKey);
45
+ if (cached !== undefined) {
46
+ const entry = cached;
47
+ if ('error' in entry) {
48
+ throw entry.error;
49
+ }
50
+ return entry.value;
51
+ }
52
+ const pending = pendingCache.get(cacheKey);
53
+ if (pending) {
54
+ return pending;
55
+ }
56
+ const promise = (async () => {
57
+ try {
58
+ const result = await originalMethod.apply(this, args);
59
+ provider.set(cacheKey, { value: result }, options?.ttl);
60
+ return result;
61
+ }
62
+ catch (error) {
63
+ provider.set(cacheKey, { error }, options?.ttl);
64
+ throw error;
65
+ }
66
+ })();
67
+ pendingCache.set(cacheKey, promise);
68
+ return promise;
69
+ };
70
+ };
71
+ }
72
+ var cache_provider_registry_2 = require("../core/cache-provider-registry");
73
+ Object.defineProperty(exports, "CacheProviderRegistry", { enumerable: true, get: function () { return cache_provider_registry_2.CacheProviderRegistry; } });
74
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/decorators/cache.ts"],"names":[],"mappings":";;;AAoFA,sBA0CC;AA9HD,4BAA0B;AAC1B,6EAAwE;AACxE,qDAAiD;AACjD,yDAAqD;AAoDrD,MAAM,YAAY,GAAG,IAAI,4BAAY,EAAE,CAAC;AAExC;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,SAAiB,EAAE,WAAyC,EAAE,IAAe;IACpG,IAAI,WAAW,KAAK,SAAS,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACtD,OAAO,wBAAU,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IACD,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QACpC,OAAO,wBAAU,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IACpD,CAAC;IACD,IAAI,CAAC;QACH,OAAO,wBAAU,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,wBAAU,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAgB,KAAK,CAAC,SAAiB,EAAE,OAAsB;IAC7D,OAAO,UACL,OAAe,EACf,YAAoB,EACpB,UAA8B;QAE9B,MAAM,cAAc,GAAG,UAAU,CAAC,KAAK,CAAC;QAExC,UAAU,CAAC,KAAK,GAAG,KAAK,WAAW,GAAG,IAAe;YACnD,MAAM,QAAQ,GAAG,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;YAChE,MAAM,YAAY,GAAG,OAAO,EAAE,YAAY,CAAC;YAC3C,MAAM,QAAQ,GAAG,+CAAqB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;YAEzD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAM,KAAK,GAAG,MAAuB,CAAC;gBACtC,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;oBACrB,MAAM,KAAK,CAAC,KAAK,CAAC;gBACpB,CAAC;gBACD,OAAO,KAAK,CAAC,KAAK,CAAC;YACrB,CAAC;YAED,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAI,QAAQ,CAAC,CAAC;YAC9C,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,OAAO,CAAC;YACjB,CAAC;YAED,MAAM,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;gBAC1B,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;oBACtD,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAmB,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;oBACzE,OAAO,MAAM,CAAC;gBAChB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAmB,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;oBACjE,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC,CAAC,EAAE,CAAC;YAEL,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACpC,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,2EAAwE;AAA/D,gIAAA,qBAAqB,OAAA"}
@@ -0,0 +1,9 @@
1
+ export * from './core/cache-provider';
2
+ export * from './core/cache-provider-registry';
3
+ export * from './core/key-builder';
4
+ export * from './core/native-cache';
5
+ export * from './core/pending-cache';
6
+ export * from './core/redis-cache';
7
+ export * from './decorators/cache';
8
+ export * from './decorators/cache-evict';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,uBAAuB,CAAC;AACtC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./core/cache-provider"), exports);
18
+ __exportStar(require("./core/cache-provider-registry"), exports);
19
+ __exportStar(require("./core/key-builder"), exports);
20
+ __exportStar(require("./core/native-cache"), exports);
21
+ __exportStar(require("./core/pending-cache"), exports);
22
+ __exportStar(require("./core/redis-cache"), exports);
23
+ __exportStar(require("./decorators/cache"), exports);
24
+ __exportStar(require("./decorators/cache-evict"), exports);
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AACA,wDAAsC;AACtC,iEAA+C;AAC/C,qDAAmC;AACnC,sDAAoC;AACpC,uDAAqC;AACrC,qDAAmC;AACnC,qDAAmC;AACnC,2DAAyC"}
package/jest.config.js ADDED
@@ -0,0 +1,14 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>'],
5
+ testMatch: ['**/test/**/*.test.ts'],
6
+ moduleFileExtensions: ['ts', 'js', 'json'],
7
+ collectCoverageFrom: [
8
+ 'src/**/*.ts',
9
+ '!src/**/*.d.ts'
10
+ ],
11
+ transform: {
12
+ '^.+\\.ts$': 'ts-jest'
13
+ }
14
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@jintianxiayu/cache-decorator",
3
+ "version": "0.1.2",
4
+ "description": "Method caching decorators for TypeScript",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "jest",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "reflect-metadata": "^0.2.2"
14
+ },
15
+ "devDependencies": {
16
+ "@types/ioredis": "^4.28.10",
17
+ "@types/node": "^22.10.0",
18
+ "ioredis": "^5.10.1",
19
+ "jest": "^29.7.0",
20
+ "ts-jest": "^29.2.0",
21
+ "typescript": "^5.7.0"
22
+ },
23
+ "peerDependencies": {
24
+ "reflect-metadata": "^0.2.2"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "gitHead": "357e736abbe6b25a707663c1b9e027b2e8b532a6"
30
+ }
@@ -0,0 +1,53 @@
1
+ import { CacheProvider } from './cache-provider';
2
+
3
+ const providers = new Map<string, CacheProvider>();
4
+ let defaultName: string | undefined;
5
+
6
+ /**
7
+ * 全局缓存提供者注册表
8
+ * 用于注册和获取不同类型的 CacheProvider
9
+ */
10
+ export class CacheProviderRegistry {
11
+ /**
12
+ * 注册缓存提供者
13
+ * @param name 提供者名称
14
+ * @param provider 缓存提供者实例
15
+ */
16
+ static register(name: string, provider: CacheProvider): void {
17
+ providers.set(name, provider);
18
+ }
19
+
20
+ /**
21
+ * 获取缓存提供者
22
+ * @param name 提供者名称,不指定则使用默认提供者
23
+ * @returns 缓存提供者实例
24
+ * @throws 未注册且无默认提供者时抛出错误
25
+ */
26
+ static get(name?: string): CacheProvider {
27
+ const providerName = name || defaultName;
28
+ if (!providerName) {
29
+ throw new Error('No CacheProvider registered and no default set');
30
+ }
31
+ const provider = providers.get(providerName);
32
+ if (!provider) {
33
+ throw new Error(`CacheProvider "${providerName}" not found`);
34
+ }
35
+ return provider;
36
+ }
37
+
38
+ /**
39
+ * 设置默认缓存提供者
40
+ * @param name 提供者名称
41
+ */
42
+ static setDefault(name: string): void {
43
+ defaultName = name;
44
+ }
45
+
46
+ /**
47
+ * 清空注册表
48
+ */
49
+ static clear(): void {
50
+ providers.clear();
51
+ defaultName = undefined;
52
+ }
53
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 缓存提供者接口,定义缓存存储后端的统一契约
3
+ * 支持可插拔的缓存实现(Memory、Redis、自定义)
4
+ */
5
+ export interface CacheProvider {
6
+ /**
7
+ * 获取缓存值
8
+ * @param key 缓存键
9
+ * @returns 缓存值,不存在或已过期返回 undefined
10
+ */
11
+ get<T>(key: string): T | undefined | Promise<T | undefined>;
12
+
13
+ /**
14
+ * 设置缓存值
15
+ * @param key 缓存键
16
+ * @param value 缓存值
17
+ * @param ttl 过期时间(秒),不设置则永不过期
18
+ */
19
+ set<T>(key: string, value: T, ttl?: number): void | Promise<void>;
20
+
21
+ /**
22
+ * 删除指定缓存
23
+ * @param key 缓存键
24
+ */
25
+ delete(key: string): void | Promise<void>;
26
+
27
+ /**
28
+ * 清除所有缓存
29
+ */
30
+ clear(): void | Promise<void>;
31
+
32
+ /**
33
+ * 根据模式删除匹配的缓存键
34
+ * @param pattern glob模式字符串(如 user:*),末尾的 * 作为前缀匹配
35
+ */
36
+ deleteByPattern(pattern: string): void | Promise<void>;
37
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 缓存 Key 构建器
3
+ * 负责将缓存名称和参数转换为统一的 key 字符串
4
+ */
5
+ export class KeyBuilder {
6
+ /**
7
+ * 构建缓存 key
8
+ * @param cacheName 缓存名称
9
+ * @param args 方法参数列表
10
+ * @returns 格式为 {cacheName}:{serializedArgs} 的 key 字符串
11
+ */
12
+ static build(cacheName: string, args: unknown[]): string {
13
+ if (args.length === 0) {
14
+ return cacheName;
15
+ }
16
+ const serializedArgs = args
17
+ .map(arg => {
18
+ if (arg === null || arg === undefined) return 'null';
19
+ if (typeof arg === 'object') {
20
+ try {
21
+ return JSON.stringify(arg);
22
+ } catch {
23
+ return String(arg);
24
+ }
25
+ }
26
+ return String(arg);
27
+ })
28
+ .join('&');
29
+ return `${cacheName}:${serializedArgs}`;
30
+ }
31
+ }
@@ -0,0 +1,53 @@
1
+ import { CacheProvider } from './cache-provider';
2
+
3
+ /**
4
+ * 缓存条目结构
5
+ */
6
+ interface CacheEntry<T> {
7
+ value: T;
8
+ expiresAt: number | null;
9
+ }
10
+
11
+ /**
12
+ * 内存缓存提供者,基于 Map 实现
13
+ * 适用于单机应用,缓存随进程重启清除
14
+ */
15
+ export class MemoryCacheProvider implements CacheProvider {
16
+ private cache = new Map<string, CacheEntry<unknown>>();
17
+
18
+ get<T>(key: string): T | undefined {
19
+ const entry = this.cache.get(key);
20
+ if (!entry) return undefined;
21
+ if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
22
+ this.cache.delete(key);
23
+ return undefined;
24
+ }
25
+ return entry.value as T;
26
+ }
27
+
28
+ set<T>(key: string, value: T, ttl?: number): void {
29
+ const expiresAt = ttl ? Date.now() + ttl * 1000 : null;
30
+ this.cache.set(key, { value, expiresAt });
31
+ }
32
+
33
+ delete(key: string): void {
34
+ this.cache.delete(key);
35
+ }
36
+
37
+ clear(): void {
38
+ this.cache.clear();
39
+ }
40
+
41
+ /**
42
+ * 根据模式删除匹配的缓存键
43
+ * @param pattern glob模式字符串(如 user:*),末尾的 * 作为前缀匹配
44
+ */
45
+ deleteByPattern(pattern: string): void {
46
+ const prefix = pattern.replace(/\*$/, '');
47
+ for (const key of this.cache.keys()) {
48
+ if (key.startsWith(prefix)) {
49
+ this.cache.delete(key);
50
+ }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 请求合并缓存
3
+ * 用于在并发场景下返回同一个 Promise,避免重复执行
4
+ */
5
+ export class PendingCache {
6
+ private pending = new Map<string, Promise<unknown>>();
7
+
8
+ get<T>(key: string): Promise<T> | undefined {
9
+ return this.pending.get(key) as Promise<T> | undefined;
10
+ }
11
+
12
+ set<T>(key: string, promise: Promise<T>): void {
13
+ this.pending.set(key, promise);
14
+ promise.finally(() => {
15
+ this.pending.delete(key);
16
+ });
17
+ }
18
+
19
+ delete(key: string): void {
20
+ this.pending.delete(key);
21
+ }
22
+
23
+ clear(): void {
24
+ this.pending.clear();
25
+ }
26
+ }
@@ -0,0 +1,62 @@
1
+ import { CacheProvider } from './cache-provider';
2
+ import Redis from 'ioredis';
3
+
4
+ /**
5
+ * Redis 缓存提供者,基于 ioredis 实现
6
+ * 适用于分布式场景,支持跨进程共享缓存
7
+ */
8
+ export class RedisCacheProvider implements CacheProvider {
9
+ private redis: Redis;
10
+
11
+ constructor(redis?: Redis) {
12
+ this.redis = redis ?? new Redis();
13
+ }
14
+
15
+ async get<T>(key: string): Promise<T | undefined> {
16
+ const value = await this.redis.get(key);
17
+ if (value === null) return undefined;
18
+ try {
19
+ return JSON.parse(value) as T;
20
+ } catch {
21
+ return value as unknown as T;
22
+ }
23
+ }
24
+
25
+ async set<T>(key: string, value: T, ttl?: number): Promise<void> {
26
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
27
+ if (ttl) {
28
+ await this.redis.setex(key, ttl, serialized);
29
+ } else {
30
+ await this.redis.set(key, serialized);
31
+ }
32
+ }
33
+
34
+ async delete(key: string): Promise<void> {
35
+ await this.redis.del(key);
36
+ }
37
+
38
+ async clear(): Promise<void> {
39
+ await this.redis.flushdb();
40
+ }
41
+
42
+ /**
43
+ * 根据模式删除匹配的缓存键
44
+ * @param pattern glob模式字符串(如 user:*),使用 SCAN 游标迭代避免阻塞 Redis
45
+ */
46
+ async deleteByPattern(pattern: string): Promise<void> {
47
+ let cursor = '0';
48
+ do {
49
+ const [nextCursor, keys] = await this.redis.scan(
50
+ cursor,
51
+ 'MATCH',
52
+ pattern,
53
+ 'COUNT',
54
+ 100,
55
+ );
56
+ cursor = nextCursor;
57
+ if (keys.length > 0) {
58
+ await this.redis.del(...keys);
59
+ }
60
+ } while (cursor !== '0');
61
+ }
62
+ }
@@ -0,0 +1,73 @@
1
+ import { CacheProviderRegistry } from '../core/cache-provider-registry';
2
+ import { KeyBuilder } from '../core/key-builder';
3
+ import { CacheOptions, CacheKeyResolver } from './cache';
4
+
5
+ /**
6
+ * @CacheEvict 装饰器配置项
7
+ */
8
+ export interface CacheEvictOptions extends Pick<CacheOptions, 'key'> {
9
+ /**
10
+ * 是否清除所有条目,默认为 false
11
+ */
12
+ allEntries?: boolean;
13
+
14
+ /**
15
+ * 指定 CacheProvider 名称
16
+ */
17
+ providerName?: string;
18
+ }
19
+
20
+ /**
21
+ * 解析缓存 key
22
+ * @param cacheName 缓存名称
23
+ * @param keyResolver key 解析器
24
+ * @param args 方法参数数组
25
+ * @returns 解析后的缓存 key
26
+ */
27
+ function resolveCacheKey(cacheName: string, keyResolver: CacheKeyResolver | undefined, args: unknown[]): string {
28
+ if (keyResolver === undefined || keyResolver === null) {
29
+ return KeyBuilder.build(cacheName, args);
30
+ }
31
+ if (typeof keyResolver === 'string') {
32
+ return KeyBuilder.build(cacheName, [keyResolver]);
33
+ }
34
+ try {
35
+ return KeyBuilder.build(cacheName, [keyResolver(...args)]);
36
+ } catch {
37
+ return KeyBuilder.build(cacheName, args);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 缓存清除装饰器
43
+ * 在方法执行后清除对应的缓存条目
44
+ * @param cacheName 缓存名称
45
+ * @param options 配置项
46
+ */
47
+ export function CacheEvict(cacheName: string, options?: CacheEvictOptions) {
48
+ return function (
49
+ _target: object,
50
+ _propertyKey: string,
51
+ descriptor: PropertyDescriptor,
52
+ ) {
53
+ const originalMethod = descriptor.value;
54
+
55
+ descriptor.value = async function (...args: unknown[]) {
56
+ const result = await originalMethod.apply(this, args);
57
+
58
+ const providerName = options?.providerName;
59
+ const provider = CacheProviderRegistry.get(providerName);
60
+
61
+ if (options?.allEntries) {
62
+ await provider.deleteByPattern(cacheName + '*');
63
+ } else {
64
+ const cacheKey = resolveCacheKey(cacheName, options?.key, args);
65
+ provider.delete(cacheKey);
66
+ }
67
+
68
+ return result;
69
+ };
70
+ };
71
+ }
72
+
73
+ export { CacheProviderRegistry } from '../core/cache-provider-registry';
@@ -0,0 +1,129 @@
1
+ import 'reflect-metadata';
2
+ import { CacheProviderRegistry } from '../core/cache-provider-registry';
3
+ import { KeyBuilder } from '../core/key-builder';
4
+ import { PendingCache } from '../core/pending-cache';
5
+
6
+ /**
7
+ * 缓存 key 解析器类型
8
+ * - null: 使用自动生成逻辑
9
+ * - string: 直接作为 key 值的一部分
10
+ * - function: 接收方法参数数组,返回自定义字符串
11
+ */
12
+ export type CacheKeyResolver = null | string | ((...args: unknown[]) => string);
13
+
14
+ /**
15
+ * @Cache 装饰器配置项
16
+ */
17
+ export interface CacheOptions {
18
+ /**
19
+ * 过期时间(秒)
20
+ */
21
+ ttl?: number;
22
+
23
+ /**
24
+ * 指定 CacheProvider 名称
25
+ */
26
+ providerName?: string;
27
+
28
+ /**
29
+ * 自定义缓存 key 生成逻辑
30
+ * - undefined/null: 使用默认逻辑 KeyBuilder.build(cacheName, args)
31
+ * - string: 使用 KeyBuilder.build(cacheName, [key])
32
+ * - function: 调用函数后使用 KeyBuilder.build(cacheName, [result])
33
+ */
34
+ key?: CacheKeyResolver;
35
+ }
36
+
37
+ /**
38
+ * 成功缓存条目
39
+ */
40
+ interface SuccessCacheEntry<T> {
41
+ value: T;
42
+ }
43
+
44
+ /**
45
+ * 错误缓存条目
46
+ */
47
+ interface ErrorCacheEntry {
48
+ error: unknown;
49
+ }
50
+
51
+ /**
52
+ * 缓存条目联合类型
53
+ */
54
+ type CacheEntry<T> = SuccessCacheEntry<T> | ErrorCacheEntry;
55
+
56
+ const pendingCache = new PendingCache();
57
+
58
+ /**
59
+ * 解析缓存 key
60
+ * @param cacheName 缓存名称
61
+ * @param keyResolver key 解析器
62
+ * @param args 方法参数数组
63
+ * @returns 解析后的缓存 key
64
+ */
65
+ function resolveCacheKey(cacheName: string, keyResolver: CacheKeyResolver | undefined, args: unknown[]): string {
66
+ if (keyResolver === undefined || keyResolver === null) {
67
+ return KeyBuilder.build(cacheName, args);
68
+ }
69
+ if (typeof keyResolver === 'string') {
70
+ return KeyBuilder.build(cacheName, [keyResolver]);
71
+ }
72
+ try {
73
+ return KeyBuilder.build(cacheName, [keyResolver(...args)]);
74
+ } catch {
75
+ return KeyBuilder.build(cacheName, args);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 缓存装饰器
81
+ * 为方法添加声明式缓存功能,支持 TTL 过期和请求合并
82
+ * @param cacheName 缓存名称
83
+ * @param options 配置项
84
+ */
85
+ export function Cache(cacheName: string, options?: CacheOptions) {
86
+ return function <T>(
87
+ _target: object,
88
+ _propertyKey: string,
89
+ descriptor: PropertyDescriptor,
90
+ ) {
91
+ const originalMethod = descriptor.value;
92
+
93
+ descriptor.value = async function (...args: unknown[]) {
94
+ const cacheKey = resolveCacheKey(cacheName, options?.key, args);
95
+ const providerName = options?.providerName;
96
+ const provider = CacheProviderRegistry.get(providerName);
97
+
98
+ const cached = await provider.get(cacheKey);
99
+ if (cached !== undefined) {
100
+ const entry = cached as CacheEntry<T>;
101
+ if ('error' in entry) {
102
+ throw entry.error;
103
+ }
104
+ return entry.value;
105
+ }
106
+
107
+ const pending = pendingCache.get<T>(cacheKey);
108
+ if (pending) {
109
+ return pending;
110
+ }
111
+
112
+ const promise = (async () => {
113
+ try {
114
+ const result = await originalMethod.apply(this, args);
115
+ provider.set(cacheKey, { value: result } as CacheEntry<T>, options?.ttl);
116
+ return result;
117
+ } catch (error) {
118
+ provider.set(cacheKey, { error } as CacheEntry<T>, options?.ttl);
119
+ throw error;
120
+ }
121
+ })();
122
+
123
+ pendingCache.set(cacheKey, promise);
124
+ return promise;
125
+ };
126
+ };
127
+ }
128
+
129
+ export { CacheProviderRegistry } from '../core/cache-provider-registry';