@jintianxiayu/cache-decorator 0.1.3 → 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.
- package/CHANGELOG.md +18 -0
- package/README.md +270 -131
- package/dist/adapters/ioredis-cache-client.d.ts +9 -0
- package/dist/adapters/ioredis-cache-client.d.ts.map +1 -0
- package/dist/adapters/ioredis-cache-client.js +73 -0
- package/dist/adapters/ioredis-cache-client.js.map +1 -0
- package/dist/adapters/node-redis-cache-client.d.ts +10 -0
- package/dist/adapters/node-redis-cache-client.d.ts.map +1 -0
- package/dist/adapters/node-redis-cache-client.js +81 -0
- package/dist/adapters/node-redis-cache-client.js.map +1 -0
- package/dist/adapters/redis-key-prefix.d.ts +22 -0
- package/dist/adapters/redis-key-prefix.d.ts.map +1 -0
- package/dist/adapters/redis-key-prefix.js +41 -0
- package/dist/adapters/redis-key-prefix.js.map +1 -0
- package/dist/core/cache-logger.d.ts +28 -0
- package/dist/core/cache-logger.d.ts.map +1 -0
- package/dist/core/cache-logger.js +70 -0
- package/dist/core/cache-logger.js.map +1 -0
- package/dist/core/cache-provider-registry.d.ts.map +1 -1
- package/dist/core/cache-provider-registry.js.map +1 -1
- package/dist/core/cache-provider.d.ts.map +1 -1
- package/dist/core/key-builder.d.ts.map +1 -1
- package/dist/core/key-builder.js +3 -2
- package/dist/core/key-builder.js.map +1 -1
- package/dist/core/native-cache.d.ts.map +1 -1
- package/dist/core/native-cache.js +2 -1
- package/dist/core/native-cache.js.map +1 -1
- package/dist/core/pending-cache.d.ts.map +1 -1
- package/dist/core/pending-cache.js +6 -3
- package/dist/core/pending-cache.js.map +1 -1
- package/dist/core/redis-cache-client.d.ts +156 -0
- package/dist/core/redis-cache-client.d.ts.map +1 -0
- package/dist/core/redis-cache-client.js +3 -0
- package/dist/core/redis-cache-client.js.map +1 -0
- package/dist/core/redis-cache.d.ts +39 -7
- package/dist/core/redis-cache.d.ts.map +1 -1
- package/dist/core/redis-cache.js +63 -22
- package/dist/core/redis-cache.js.map +1 -1
- package/dist/decorators/cache-evict.d.ts +1 -1
- package/dist/decorators/cache-evict.d.ts.map +1 -1
- package/dist/decorators/cache-evict.js +68 -14
- package/dist/decorators/cache-evict.js.map +1 -1
- package/dist/decorators/cache.d.ts +1 -1
- package/dist/decorators/cache.d.ts.map +1 -1
- package/dist/decorators/cache.js +85 -23
- package/dist/decorators/cache.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/jest.config.js +11 -11
- package/package.json +26 -29
- package/src/adapters/ioredis-cache-client.ts +89 -0
- package/src/adapters/node-redis-cache-client.ts +95 -0
- package/src/adapters/redis-key-prefix.ts +38 -0
- package/src/core/cache-logger.ts +105 -0
- package/src/core/key-builder.ts +33 -31
- package/src/core/native-cache.ts +55 -53
- package/src/core/pending-cache.ts +29 -26
- package/src/core/redis-cache-client.ts +160 -0
- package/src/core/redis-cache.ts +104 -56
- package/src/decorators/cache-evict.ts +129 -69
- package/src/decorators/cache.ts +203 -125
- package/src/index.ts +11 -8
- package/test/cache-evict-logging.test.ts +362 -0
- package/test/cache-logger.integration.test.ts +129 -0
- package/test/cache-logger.test.ts +153 -0
- package/test/cache-logging.test.ts +544 -0
- package/test/cache.test.ts +255 -256
- package/test/fixtures/cache-logger-child.mjs +108 -0
- package/test/helpers/legacy-redis-cache.ts +22 -0
- package/test/helpers/package-consumer.ts +231 -0
- package/test/helpers/redis-fixture.ts +142 -0
- package/test/ioredis-cache-client.test.ts +143 -0
- package/test/native-cache.test.ts +77 -71
- package/test/node-redis-cache-client.test.ts +149 -0
- package/test/pending-cache.test.ts +69 -58
- package/test/redis-cache-client-lifecycle.test.ts +112 -0
- package/test/redis-cache-client-types.test.ts +184 -0
- package/test/redis-cache-client.integration.test.ts +327 -0
- package/test/redis-cache-decorator.test.ts +201 -0
- package/test/redis-cache-package.integration.test.ts +328 -0
- package/test/redis-cache-provider.test.ts +269 -0
- package/test/type-contract/contract.ts +64 -0
- package/test/type-contract/tsconfig.json +12 -0
- package/tsconfig.json +8 -8
package/src/core/redis-cache.ts
CHANGED
|
@@ -1,56 +1,104 @@
|
|
|
1
|
-
import { CacheProvider } from './cache-provider';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Redis
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
export class RedisCacheProvider implements CacheProvider {
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
*
|
|
44
|
-
* @
|
|
45
|
-
*/
|
|
46
|
-
async
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
1
|
+
import { CacheProvider } from './cache-provider';
|
|
2
|
+
import { RedisCacheClient } from './redis-cache-client';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Redis 缓存提供者,在客户端无关的字符串命令之上维护统一缓存协议。
|
|
6
|
+
* 适用于分布式场景,支持跨进程及跨客户端共享缓存。
|
|
7
|
+
*/
|
|
8
|
+
export class RedisCacheProvider implements CacheProvider {
|
|
9
|
+
private readonly client: RedisCacheClient;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 创建复用调用方 Redis 连接的缓存提供者。
|
|
13
|
+
* @param client 已适配的最小 Redis 缓存客户端。
|
|
14
|
+
*/
|
|
15
|
+
constructor(client: RedisCacheClient) {
|
|
16
|
+
this.client = client;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 按既有协议读取并反序列化缓存值。
|
|
21
|
+
* @param key 未经 Provider 改写的逻辑缓存键。
|
|
22
|
+
* @returns 解析后的业务值;Redis miss 时返回 undefined。
|
|
23
|
+
* @throws GET 命令失败时传播客户端原始错误。
|
|
24
|
+
*/
|
|
25
|
+
async get<T>(key: string): Promise<T | undefined> {
|
|
26
|
+
const value = await this.client.get(key);
|
|
27
|
+
if (value === null) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(value) as T;
|
|
32
|
+
} catch {
|
|
33
|
+
// 兼容既有协议:不是合法 JSON 的字符串按原值返回。
|
|
34
|
+
return value as unknown as T;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 按既有字符串或 JSON 协议写入缓存值。
|
|
40
|
+
* @param key 未经 Provider 改写的逻辑缓存键。
|
|
41
|
+
* @param value 需要缓存的业务值。
|
|
42
|
+
* @param ttl 可选的正整数秒级 TTL;undefined 或 0 表示永不过期。
|
|
43
|
+
* @returns 写入命令成功后完成。
|
|
44
|
+
* @throws 值不能序列化时抛 TypeError,TTL 非法时抛 RangeError,命令失败时传播原始错误。
|
|
45
|
+
*/
|
|
46
|
+
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
47
|
+
const ttlSeconds = this.normalizeTtl(ttl);
|
|
48
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
49
|
+
if (typeof serialized !== 'string') {
|
|
50
|
+
throw new TypeError('Cache value must serialize to a string');
|
|
51
|
+
}
|
|
52
|
+
if (ttlSeconds === undefined) {
|
|
53
|
+
await this.client.set({ key, value: serialized });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await this.client.set({ key, value: serialized, ttlSeconds });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 幂等删除一个逻辑缓存键。
|
|
61
|
+
* @param key 未经 Provider 改写的逻辑缓存键。
|
|
62
|
+
* @returns 删除命令成功后完成。
|
|
63
|
+
* @throws DEL 命令失败时传播客户端原始错误。
|
|
64
|
+
*/
|
|
65
|
+
async delete(key: string): Promise<void> {
|
|
66
|
+
await this.client.deleteMany([key]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 清空客户端当前选择的整个 Redis 数据库。
|
|
71
|
+
* @returns FLUSHDB 命令成功后完成。
|
|
72
|
+
* @throws 清库命令失败时传播客户端原始错误。
|
|
73
|
+
*/
|
|
74
|
+
async clear(): Promise<void> {
|
|
75
|
+
await this.client.flushDatabase();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 使用游标扫描并按页删除匹配的逻辑缓存键,避免阻塞 Redis。
|
|
80
|
+
* @param pattern 保留 glob 语义的逻辑 pattern,例如 user:*。
|
|
81
|
+
* @returns 游标归零且所有已扫描页面删除成功后完成。
|
|
82
|
+
* @throws SCAN 或任一页 DEL 失败时传播客户端原始错误,并停止后续页面。
|
|
83
|
+
*/
|
|
84
|
+
async deleteByPattern(pattern: string): Promise<void> {
|
|
85
|
+
let cursor = '0';
|
|
86
|
+
do {
|
|
87
|
+
const page = await this.client.scan({ cursor, pattern, count: 100 });
|
|
88
|
+
cursor = page.cursor;
|
|
89
|
+
if (page.keys.length > 0) {
|
|
90
|
+
await this.client.deleteMany(page.keys);
|
|
91
|
+
}
|
|
92
|
+
} while (cursor !== '0');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private normalizeTtl(ttl: number | undefined): number | undefined {
|
|
96
|
+
if (ttl === undefined || ttl === 0) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
if (!Number.isFinite(ttl) || !Number.isInteger(ttl) || ttl < 1) {
|
|
100
|
+
throw new RangeError('Redis cache TTL must be a positive finite integer or zero');
|
|
101
|
+
}
|
|
102
|
+
return ttl;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -1,69 +1,129 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
* @param
|
|
25
|
-
* @
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return KeyBuilder.build(cacheName,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
import { cacheProviderLabel, logCacheEvent, type CacheLogContext } from '../core/cache-logger';
|
|
2
|
+
import type { CacheProvider } from '../core/cache-provider';
|
|
3
|
+
import { CacheProviderRegistry } from '../core/cache-provider-registry';
|
|
4
|
+
import { KeyBuilder } from '../core/key-builder';
|
|
5
|
+
import type { CacheKeyResolver, CacheOptions } from './cache';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @CacheEvict 装饰器配置项
|
|
9
|
+
*/
|
|
10
|
+
export interface CacheEvictOptions extends Pick<CacheOptions, 'key'> {
|
|
11
|
+
/**
|
|
12
|
+
* 是否清除所有条目,默认为 false
|
|
13
|
+
*/
|
|
14
|
+
allEntries?: boolean;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 指定 CacheProvider 名称
|
|
18
|
+
*/
|
|
19
|
+
providerName?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 解析缓存 key
|
|
24
|
+
* @param keyResolver key 解析器
|
|
25
|
+
* @param args 方法参数数组
|
|
26
|
+
* @param logContext 不包含业务参数和值的日志上下文
|
|
27
|
+
* @returns 解析后的缓存 key
|
|
28
|
+
*/
|
|
29
|
+
function resolveCacheKey(
|
|
30
|
+
keyResolver: CacheKeyResolver | undefined,
|
|
31
|
+
args: unknown[],
|
|
32
|
+
logContext: CacheLogContext
|
|
33
|
+
): string {
|
|
34
|
+
if (keyResolver === undefined || keyResolver === null) {
|
|
35
|
+
return KeyBuilder.build(logContext.cacheName, args);
|
|
36
|
+
}
|
|
37
|
+
if (typeof keyResolver === 'string') {
|
|
38
|
+
return KeyBuilder.build(logContext.cacheName, [keyResolver]);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return KeyBuilder.build(logContext.cacheName, [keyResolver(...args)]);
|
|
42
|
+
} catch (_error) {
|
|
43
|
+
logCacheEvent('cache.key_fallback', { ...logContext, reason: 'resolver_error' });
|
|
44
|
+
return KeyBuilder.build(logContext.cacheName, args);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 获取淘汰操作使用的 Provider,并在解析失败时记录原始注册表错误。
|
|
50
|
+
* @param providerName decorator 配置中的 Provider 名称。
|
|
51
|
+
* @param logContext 当前方法的稳定日志上下文。
|
|
52
|
+
* @returns 已注册的缓存 Provider。
|
|
53
|
+
* @throws Provider 注册表抛出的原始错误。
|
|
54
|
+
*/
|
|
55
|
+
function resolveCacheProvider(providerName: string | undefined, logContext: CacheLogContext): CacheProvider {
|
|
56
|
+
try {
|
|
57
|
+
return CacheProviderRegistry.get(providerName);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'provider_resolution', error });
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 保持 fire-and-forget 语义发起单 key 删除,只处理调用当下可观察的同步失败。
|
|
66
|
+
* @param provider 当前淘汰使用的 Provider。
|
|
67
|
+
* @param cacheKey 待删除的完整 key;不会进入日志元数据。
|
|
68
|
+
* @param logContext 当前方法的稳定日志上下文。
|
|
69
|
+
* @returns 无返回值;异步删除 Promise 不会被等待或消费。
|
|
70
|
+
* @throws Provider delete 同步抛出的原始错误。
|
|
71
|
+
*/
|
|
72
|
+
function dispatchCacheDelete(provider: CacheProvider, cacheKey: string, logContext: CacheLogContext): void {
|
|
73
|
+
try {
|
|
74
|
+
provider.delete(cacheKey);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'evict', error });
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
logCacheEvent('cache.evict_dispatched', { ...logContext, scope: 'key' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 缓存清除装饰器
|
|
84
|
+
* 在方法执行后清除对应的缓存条目
|
|
85
|
+
* @param cacheName 缓存名称
|
|
86
|
+
* @param options 配置项
|
|
87
|
+
*/
|
|
88
|
+
export function CacheEvict(
|
|
89
|
+
cacheName: string,
|
|
90
|
+
options?: CacheEvictOptions
|
|
91
|
+
): (_target: object, _propertyKey: string, descriptor: PropertyDescriptor) => void {
|
|
92
|
+
return function (_target: object, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
93
|
+
const originalMethod = descriptor.value;
|
|
94
|
+
const logContext: CacheLogContext = {
|
|
95
|
+
cacheName,
|
|
96
|
+
methodName: propertyKey,
|
|
97
|
+
providerName: cacheProviderLabel(options?.providerName),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
descriptor.value = async function (...args: unknown[]) {
|
|
101
|
+
let result: unknown;
|
|
102
|
+
try {
|
|
103
|
+
result = await originalMethod.apply(this, args);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logCacheEvent('cache.evict_skipped', { ...logContext, reason: 'business_error' });
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const provider = resolveCacheProvider(options?.providerName, logContext);
|
|
110
|
+
|
|
111
|
+
if (options?.allEntries) {
|
|
112
|
+
try {
|
|
113
|
+
await provider.deleteByPattern(cacheName + '*');
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'evict', error });
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
logCacheEvent('cache.evict_completed', { ...logContext, scope: 'allEntries' });
|
|
119
|
+
} else {
|
|
120
|
+
const cacheKey = resolveCacheKey(options?.key, args, logContext);
|
|
121
|
+
dispatchCacheDelete(provider, cacheKey, logContext);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { CacheProviderRegistry } from '../core/cache-provider-registry';
|