@jintianxiayu/cache-decorator 0.1.2 → 1.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +270 -133
  3. package/dist/adapters/ioredis-cache-client.d.ts +9 -0
  4. package/dist/adapters/ioredis-cache-client.d.ts.map +1 -0
  5. package/dist/adapters/ioredis-cache-client.js +73 -0
  6. package/dist/adapters/ioredis-cache-client.js.map +1 -0
  7. package/dist/adapters/node-redis-cache-client.d.ts +10 -0
  8. package/dist/adapters/node-redis-cache-client.d.ts.map +1 -0
  9. package/dist/adapters/node-redis-cache-client.js +81 -0
  10. package/dist/adapters/node-redis-cache-client.js.map +1 -0
  11. package/dist/adapters/redis-key-prefix.d.ts +22 -0
  12. package/dist/adapters/redis-key-prefix.d.ts.map +1 -0
  13. package/dist/adapters/redis-key-prefix.js +41 -0
  14. package/dist/adapters/redis-key-prefix.js.map +1 -0
  15. package/dist/core/cache-logger.d.ts +28 -0
  16. package/dist/core/cache-logger.d.ts.map +1 -0
  17. package/dist/core/cache-logger.js +70 -0
  18. package/dist/core/cache-logger.js.map +1 -0
  19. package/dist/core/cache-provider-registry.d.ts.map +1 -1
  20. package/dist/core/cache-provider-registry.js.map +1 -1
  21. package/dist/core/cache-provider.d.ts.map +1 -1
  22. package/dist/core/key-builder.d.ts.map +1 -1
  23. package/dist/core/key-builder.js +3 -2
  24. package/dist/core/key-builder.js.map +1 -1
  25. package/dist/core/native-cache.d.ts.map +1 -1
  26. package/dist/core/native-cache.js +2 -1
  27. package/dist/core/native-cache.js.map +1 -1
  28. package/dist/core/pending-cache.d.ts.map +1 -1
  29. package/dist/core/pending-cache.js +6 -3
  30. package/dist/core/pending-cache.js.map +1 -1
  31. package/dist/core/redis-cache-client.d.ts +156 -0
  32. package/dist/core/redis-cache-client.d.ts.map +1 -0
  33. package/dist/core/redis-cache-client.js +3 -0
  34. package/dist/core/redis-cache-client.js.map +1 -0
  35. package/dist/core/redis-cache.d.ts +39 -7
  36. package/dist/core/redis-cache.d.ts.map +1 -1
  37. package/dist/core/redis-cache.js +63 -22
  38. package/dist/core/redis-cache.js.map +1 -1
  39. package/dist/decorators/cache-evict.d.ts +1 -1
  40. package/dist/decorators/cache-evict.d.ts.map +1 -1
  41. package/dist/decorators/cache-evict.js +68 -14
  42. package/dist/decorators/cache-evict.js.map +1 -1
  43. package/dist/decorators/cache.d.ts +1 -1
  44. package/dist/decorators/cache.d.ts.map +1 -1
  45. package/dist/decorators/cache.js +85 -23
  46. package/dist/decorators/cache.js.map +1 -1
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +6 -0
  50. package/dist/index.js.map +1 -1
  51. package/jest.config.js +11 -14
  52. package/package.json +26 -29
  53. package/src/adapters/ioredis-cache-client.ts +89 -0
  54. package/src/adapters/node-redis-cache-client.ts +95 -0
  55. package/src/adapters/redis-key-prefix.ts +38 -0
  56. package/src/core/cache-logger.ts +105 -0
  57. package/src/core/cache-provider-registry.ts +39 -39
  58. package/src/core/cache-provider.ts +28 -28
  59. package/src/core/key-builder.ts +33 -31
  60. package/src/core/native-cache.ts +55 -53
  61. package/src/core/pending-cache.ts +29 -26
  62. package/src/core/redis-cache-client.ts +160 -0
  63. package/src/core/redis-cache.ts +104 -62
  64. package/src/decorators/cache-evict.ts +129 -73
  65. package/src/decorators/cache.ts +203 -129
  66. package/src/index.ts +11 -9
  67. package/test/cache-evict-logging.test.ts +362 -0
  68. package/test/cache-evict.test.ts +165 -165
  69. package/test/cache-logger.integration.test.ts +129 -0
  70. package/test/cache-logger.test.ts +153 -0
  71. package/test/cache-logging.test.ts +544 -0
  72. package/test/cache-provider-registry.test.ts +24 -24
  73. package/test/cache.test.ts +255 -256
  74. package/test/fixtures/cache-logger-child.mjs +108 -0
  75. package/test/helpers/legacy-redis-cache.ts +22 -0
  76. package/test/helpers/package-consumer.ts +231 -0
  77. package/test/helpers/redis-fixture.ts +142 -0
  78. package/test/ioredis-cache-client.test.ts +143 -0
  79. package/test/key-builder.test.ts +28 -28
  80. package/test/native-cache.test.ts +77 -71
  81. package/test/node-redis-cache-client.test.ts +149 -0
  82. package/test/pending-cache.test.ts +69 -56
  83. package/test/redis-cache-client-lifecycle.test.ts +112 -0
  84. package/test/redis-cache-client-types.test.ts +184 -0
  85. package/test/redis-cache-client.integration.test.ts +327 -0
  86. package/test/redis-cache-decorator.test.ts +201 -0
  87. package/test/redis-cache-package.integration.test.ts +328 -0
  88. package/test/redis-cache-provider.test.ts +269 -0
  89. package/test/type-contract/contract.ts +64 -0
  90. package/test/type-contract/tsconfig.json +12 -0
  91. package/tsconfig.json +8 -8
@@ -0,0 +1,95 @@
1
+ import { NodeRedisCacheClientOptions, NodeRedisCacheClientSource, RedisCacheClient } from '../core/redis-cache-client';
2
+ import { createRedisScanPattern, stripRedisKeyPrefix } from './redis-key-prefix';
3
+
4
+ function normalizeGetResponse(response: unknown): string | null {
5
+ if (typeof response === 'string' || response === null) {
6
+ return response;
7
+ }
8
+ throw new TypeError('Expected Redis GET to return a string or null');
9
+ }
10
+
11
+ function validateOkResponse(command: string, response: unknown): void {
12
+ if (response !== 'OK') {
13
+ throw new TypeError(`Expected Redis ${command} to return "OK"`);
14
+ }
15
+ }
16
+
17
+ function validateDeleteResponse(response: unknown): void {
18
+ if (typeof response !== 'number' || !Number.isInteger(response) || response < 0) {
19
+ throw new TypeError('Expected Redis DEL to return a non-negative integer');
20
+ }
21
+ }
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
26
+
27
+ function normalizeScanResponse(
28
+ response: unknown,
29
+ keyPrefix: string
30
+ ): { readonly cursor: string; readonly keys: readonly string[] } {
31
+ if (
32
+ !isRecord(response) ||
33
+ typeof response.cursor !== 'string' ||
34
+ !Array.isArray(response.keys) ||
35
+ !response.keys.every((key: unknown) => typeof key === 'string')
36
+ ) {
37
+ throw new TypeError('Expected node-redis SCAN to return a { cursor, keys } object');
38
+ }
39
+ return {
40
+ cursor: response.cursor,
41
+ keys: response.keys.map((key: string) => stripRedisKeyPrefix(keyPrefix, key)),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * 将调用方的 node-redis 连接适配为客户端无关的缓存命令,不创建或关闭连接。
47
+ * @param client 使用默认字符串响应映射的 node-redis 普通客户端。
48
+ * @param options 可选的字面 key 前缀;缺省时不改写逻辑 key。
49
+ * @returns 复用该连接并统一物理 key 前缀的 Redis 缓存客户端。
50
+ * @throws 返回对象的命令方法传播客户端错误;非标准响应拒绝为 TypeError。
51
+ */
52
+ export function createNodeRedisCacheClient(
53
+ client: NodeRedisCacheClientSource,
54
+ options?: NodeRedisCacheClientOptions
55
+ ): RedisCacheClient {
56
+ const keyPrefix = options?.keyPrefix ?? '';
57
+ const createPhysicalKey = (key: string): string => `${keyPrefix}${key}`;
58
+ return {
59
+ /** @inheritdoc */
60
+ async get(key: string): Promise<string | null> {
61
+ return normalizeGetResponse(await client.get(createPhysicalKey(key)));
62
+ },
63
+ /** @inheritdoc */
64
+ async set({ key, value, ttlSeconds }): Promise<void> {
65
+ const physicalKey = createPhysicalKey(key);
66
+ if (ttlSeconds === undefined) {
67
+ validateOkResponse('SET', await client.set(physicalKey, value));
68
+ return;
69
+ }
70
+ validateOkResponse('SETEX', await client.setEx(physicalKey, ttlSeconds, value));
71
+ },
72
+ /** @inheritdoc */
73
+ async deleteMany(keys: readonly string[]): Promise<void> {
74
+ if (keys.length === 0) {
75
+ return;
76
+ }
77
+ validateDeleteResponse(await client.del(keys.map(createPhysicalKey)));
78
+ },
79
+ /** @inheritdoc */
80
+ async scan({ cursor, pattern, count }): Promise<{
81
+ readonly cursor: string;
82
+ readonly keys: readonly string[];
83
+ }> {
84
+ const response = await client.scan(cursor, {
85
+ MATCH: createRedisScanPattern(keyPrefix, pattern),
86
+ COUNT: count,
87
+ });
88
+ return normalizeScanResponse(response, keyPrefix);
89
+ },
90
+ /** @inheritdoc */
91
+ async flushDatabase(): Promise<void> {
92
+ validateOkResponse('FLUSHDB', await client.flushDb());
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,38 @@
1
+ const REDIS_GLOB_META_CHARACTERS = new Set(['\\', '*', '?', '[', ']']);
2
+
3
+ /**
4
+ * 将普通字符串转换为 Redis glob pattern 中的字面量片段。
5
+ * @param value 仅作为字面前缀使用的字符串。
6
+ * @returns 对反斜杠、星号、问号和方括号增加反斜杠转义后的片段。
7
+ */
8
+ export function escapeRedisGlobLiteral(value: string): string {
9
+ let escaped = '';
10
+ for (const character of value) {
11
+ escaped += REDIS_GLOB_META_CHARACTERS.has(character) ? `\\${character}` : character;
12
+ }
13
+ return escaped;
14
+ }
15
+
16
+ /**
17
+ * 为逻辑 glob pattern 增加按字面量解释的 Redis key 前缀。
18
+ * @param keyPrefix 调用方配置的物理 key 前缀。
19
+ * @param pattern Provider 传入且需要保留 glob 语义的逻辑 pattern。
20
+ * @returns 可直接交给 Redis SCAN MATCH 的物理 pattern。
21
+ */
22
+ export function createRedisScanPattern(keyPrefix: string, pattern: string): string {
23
+ return `${escapeRedisGlobLiteral(keyPrefix)}${pattern}`;
24
+ }
25
+
26
+ /**
27
+ * 验证扫描结果属于指定物理命名空间并恢复逻辑 key。
28
+ * @param keyPrefix 调用方配置的物理 key 前缀。
29
+ * @param physicalKey Redis SCAN 返回的物理 key。
30
+ * @returns 只剥离一次字面前缀后的逻辑 key。
31
+ * @throws 扫描结果不属于指定前缀时抛出 TypeError,避免扩大后续删除范围。
32
+ */
33
+ export function stripRedisKeyPrefix(keyPrefix: string, physicalKey: string): string {
34
+ if (!physicalKey.startsWith(keyPrefix)) {
35
+ throw new TypeError('Redis SCAN returned a key outside the configured key prefix');
36
+ }
37
+ return physicalKey.slice(keyPrefix.length);
38
+ }
@@ -0,0 +1,105 @@
1
+ import { LoggerFactory, type LoggerInterface } from '@jintianxiayu/logger';
2
+
3
+ const CACHE_LOGGER_NAME = '@jintianxiayu/cache-decorator';
4
+
5
+ export type CacheLogEvent =
6
+ | 'cache.pending_hit'
7
+ | 'cache.hit'
8
+ | 'cache.miss'
9
+ | 'cache.write_dispatched'
10
+ | 'cache.evict_dispatched'
11
+ | 'cache.evict_completed'
12
+ | 'cache.key_fallback'
13
+ | 'cache.evict_skipped'
14
+ | 'cache.operation_failed';
15
+
16
+ type CacheLogLevel = 'debug' | 'warn' | 'error';
17
+
18
+ interface CacheLogDefinition {
19
+ readonly level: CacheLogLevel;
20
+ readonly message: string;
21
+ }
22
+
23
+ /** 缓存日志只接受与决策相关的稳定字段,避免业务数据和 cache key 被意外带入输出。 */
24
+ export interface CacheLogContext {
25
+ readonly cacheName: string;
26
+ readonly methodName: string;
27
+ readonly providerName: string;
28
+ readonly entryType?: 'value' | 'error';
29
+ readonly scope?: 'key' | 'allEntries';
30
+ readonly reason?: 'resolver_error' | 'business_error';
31
+ readonly operation?: 'provider_resolution' | 'read' | 'write' | 'evict';
32
+ readonly error?: unknown;
33
+ }
34
+
35
+ /** 日志定义映射中 K 为稳定事件名,V 为固定的日志级别与无业务数据消息。 */
36
+ const CACHE_LOG_DEFINITIONS: Readonly<Record<CacheLogEvent, CacheLogDefinition>> = {
37
+ 'cache.pending_hit': { level: 'debug', message: 'Cache pending request reused' },
38
+ 'cache.hit': { level: 'debug', message: 'Cache entry hit' },
39
+ 'cache.miss': { level: 'debug', message: 'Cache entry missed' },
40
+ 'cache.write_dispatched': { level: 'debug', message: 'Cache write dispatched' },
41
+ 'cache.evict_dispatched': { level: 'debug', message: 'Cache eviction dispatched' },
42
+ 'cache.evict_completed': { level: 'debug', message: 'Cache eviction completed' },
43
+ 'cache.key_fallback': { level: 'warn', message: 'Cache key resolver failed; using default key' },
44
+ 'cache.evict_skipped': { level: 'warn', message: 'Cache eviction skipped after business failure' },
45
+ 'cache.operation_failed': { level: 'error', message: 'Cache operation failed' },
46
+ };
47
+
48
+ let cacheLogger: LoggerInterface | undefined;
49
+
50
+ function getCacheLogger(): LoggerInterface {
51
+ cacheLogger ??= LoggerFactory.getLogger(CACHE_LOGGER_NAME);
52
+ return cacheLogger;
53
+ }
54
+
55
+ /**
56
+ * 按稳定事件定义调用对应 Logger level,不提供 info 分支以防高频缓存事件误入 info。
57
+ * @param logger 应用共享的 cache 命名 Logger。
58
+ * @param definition 当前事件固定的 level 与 message。
59
+ * @param metadata 只包含缓存决策白名单字段的结构化元数据。
60
+ * @returns 无返回值。
61
+ * @throws Logger 同步写入失败时透传给外层安全边界处理。
62
+ */
63
+ function writeCacheLog(
64
+ logger: LoggerInterface,
65
+ definition: CacheLogDefinition,
66
+ metadata: CacheLogContext & { readonly event: CacheLogEvent }
67
+ ): void {
68
+ if (definition.level === 'debug') {
69
+ logger.debug(definition.message, metadata);
70
+ return;
71
+ }
72
+ if (definition.level === 'warn') {
73
+ logger.warn(definition.message, metadata);
74
+ return;
75
+ }
76
+ logger.error(definition.message, metadata);
77
+ }
78
+
79
+ /**
80
+ * 提交一条不会影响缓存或业务结果的结构化日志。
81
+ * @param event 决定固定 message 与 level 的缓存事件。
82
+ * @param context 不包含参数、值或 key 的缓存决策上下文。
83
+ * @returns 无返回值;Logger 获取或同步写入失败时静默结束。
84
+ * @throws 不主动抛出异常。
85
+ */
86
+ export function logCacheEvent(event: CacheLogEvent, context: CacheLogContext): void {
87
+ try {
88
+ writeCacheLog(getCacheLogger(), CACHE_LOG_DEFINITIONS[event], { event, ...context });
89
+ } catch (_error) {
90
+ return;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 将 decorator 配置中的 Provider 名称转换为稳定日志标签。
96
+ * @param providerName 用户显式配置的 Provider 名称。
97
+ * @returns 非空显式名称;缺省或空字符串返回 `default`。
98
+ * @throws 不主动抛出异常。
99
+ */
100
+ export function cacheProviderLabel(providerName: string | undefined): string {
101
+ if (providerName === undefined || providerName.length === 0) {
102
+ return 'default';
103
+ }
104
+ return providerName;
105
+ }
@@ -8,46 +8,46 @@ let defaultName: string | undefined;
8
8
  * 用于注册和获取不同类型的 CacheProvider
9
9
  */
10
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');
11
+ /**
12
+ * 注册缓存提供者
13
+ * @param name 提供者名称
14
+ * @param provider 缓存提供者实例
15
+ */
16
+ static register(name: string, provider: CacheProvider): void {
17
+ providers.set(name, provider);
30
18
  }
31
- const provider = providers.get(providerName);
32
- if (!provider) {
33
- throw new Error(`CacheProvider "${providerName}" not found`);
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;
34
36
  }
35
- return provider;
36
- }
37
37
 
38
- /**
39
- * 设置默认缓存提供者
40
- * @param name 提供者名称
41
- */
42
- static setDefault(name: string): void {
43
- defaultName = name;
44
- }
38
+ /**
39
+ * 设置默认缓存提供者
40
+ * @param name 提供者名称
41
+ */
42
+ static setDefault(name: string): void {
43
+ defaultName = name;
44
+ }
45
45
 
46
- /**
47
- * 清空注册表
48
- */
49
- static clear(): void {
50
- providers.clear();
51
- defaultName = undefined;
52
- }
53
- }
46
+ /**
47
+ * 清空注册表
48
+ */
49
+ static clear(): void {
50
+ providers.clear();
51
+ defaultName = undefined;
52
+ }
53
+ }
@@ -3,35 +3,35 @@
3
3
  * 支持可插拔的缓存实现(Memory、Redis、自定义)
4
4
  */
5
5
  export interface CacheProvider {
6
- /**
7
- * 获取缓存值
8
- * @param key 缓存键
9
- * @returns 缓存值,不存在或已过期返回 undefined
10
- */
11
- get<T>(key: string): T | undefined | Promise<T | undefined>;
6
+ /**
7
+ * 获取缓存值
8
+ * @param key 缓存键
9
+ * @returns 缓存值,不存在或已过期返回 undefined
10
+ */
11
+ get<T>(key: string): T | undefined | Promise<T | undefined>;
12
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>;
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
20
 
21
- /**
22
- * 删除指定缓存
23
- * @param key 缓存键
24
- */
25
- delete(key: string): void | Promise<void>;
21
+ /**
22
+ * 删除指定缓存
23
+ * @param key 缓存键
24
+ */
25
+ delete(key: string): void | Promise<void>;
26
26
 
27
- /**
28
- * 清除所有缓存
29
- */
30
- clear(): void | Promise<void>;
27
+ /**
28
+ * 清除所有缓存
29
+ */
30
+ clear(): void | Promise<void>;
31
31
 
32
- /**
33
- * 根据模式删除匹配的缓存键
34
- * @param pattern glob模式字符串(如 user:*),末尾的 * 作为前缀匹配
35
- */
36
- deleteByPattern(pattern: string): void | Promise<void>;
37
- }
32
+ /**
33
+ * 根据模式删除匹配的缓存键
34
+ * @param pattern glob模式字符串(如 user:*),末尾的 * 作为前缀匹配
35
+ */
36
+ deleteByPattern(pattern: string): void | Promise<void>;
37
+ }
@@ -1,31 +1,33 @@
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
- }
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) {
19
+ return 'null';
20
+ }
21
+ if (typeof arg === 'object') {
22
+ try {
23
+ return JSON.stringify(arg);
24
+ } catch {
25
+ return String(arg);
26
+ }
27
+ }
28
+ return String(arg);
29
+ })
30
+ .join('&');
31
+ return `${cacheName}:${serializedArgs}`;
32
+ }
33
+ }
@@ -1,53 +1,55 @@
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
- }
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) {
21
+ return undefined;
22
+ }
23
+ if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
24
+ this.cache.delete(key);
25
+ return undefined;
26
+ }
27
+ return entry.value as T;
28
+ }
29
+
30
+ set<T>(key: string, value: T, ttl?: number): void {
31
+ const expiresAt = ttl ? Date.now() + ttl * 1000 : null;
32
+ this.cache.set(key, { value, expiresAt });
33
+ }
34
+
35
+ delete(key: string): void {
36
+ this.cache.delete(key);
37
+ }
38
+
39
+ clear(): void {
40
+ this.cache.clear();
41
+ }
42
+
43
+ /**
44
+ * 根据模式删除匹配的缓存键
45
+ * @param pattern glob模式字符串(如 user:*),末尾的 * 作为前缀匹配
46
+ */
47
+ deleteByPattern(pattern: string): void {
48
+ const prefix = pattern.replace(/\*$/, '');
49
+ for (const key of this.cache.keys()) {
50
+ if (key.startsWith(prefix)) {
51
+ this.cache.delete(key);
52
+ }
53
+ }
54
+ }
55
+ }
@@ -1,26 +1,29 @@
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
- }
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
+ const deleteSettledPromise = (): void => {
15
+ if (this.pending.get(key) === promise) {
16
+ this.pending.delete(key);
17
+ }
18
+ };
19
+ void promise.then(deleteSettledPromise, deleteSettledPromise);
20
+ }
21
+
22
+ delete(key: string): void {
23
+ this.pending.delete(key);
24
+ }
25
+
26
+ clear(): void {
27
+ this.pending.clear();
28
+ }
29
+ }