@jintianxiayu/cache-decorator 0.1.3 → 1.0.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/CHANGELOG.md +24 -0
- package/README.md +306 -79
- 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-error.d.ts +75 -0
- package/dist/core/cache-error.d.ts.map +1 -0
- package/dist/core/cache-error.js +168 -0
- package/dist/core/cache-error.js.map +1 -0
- package/dist/core/cache-logger.d.ts +29 -0
- package/dist/core/cache-logger.d.ts.map +1 -0
- package/dist/core/cache-logger.js +72 -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 +7 -1
- package/dist/decorators/cache.d.ts.map +1 -1
- package/dist/decorators/cache.js +169 -24
- 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 +1 -1
- 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-error.ts +233 -0
- package/src/core/cache-logger.ts +116 -0
- package/src/core/key-builder.ts +3 -1
- package/src/core/native-cache.ts +3 -1
- package/src/core/pending-cache.ts +6 -3
- package/src/core/redis-cache-client.ts +160 -0
- package/src/core/redis-cache.ts +68 -20
- package/src/decorators/cache-evict.ts +76 -16
- package/src/decorators/cache.ts +220 -26
- package/src/index.ts +3 -0
- package/test/cache-evict-logging.test.ts +362 -0
- package/test/cache-logger.integration.test.ts +129 -0
- package/test/cache-logger.test.ts +156 -0
- package/test/cache-logging.test.ts +852 -0
- package/test/cache.test.ts +717 -34
- package/test/fixtures/cache-logger-child.mjs +108 -0
- package/test/helpers/legacy-redis-cache.ts +41 -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/legacy-redis-cache.test.ts +51 -0
- package/test/native-cache.test.ts +9 -3
- package/test/node-redis-cache-client.test.ts +149 -0
- package/test/pending-cache.test.ts +11 -0
- 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 +355 -0
- package/test/redis-cache-decorator.test.ts +540 -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 +119 -0
- package/test/type-contract/tsconfig.json +12 -0
- package/tsconfig.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { cacheProviderLabel, logCacheEvent, type CacheLogContext } from '../core/cache-logger';
|
|
2
|
+
import type { CacheProvider } from '../core/cache-provider';
|
|
1
3
|
import { CacheProviderRegistry } from '../core/cache-provider-registry';
|
|
2
4
|
import { KeyBuilder } from '../core/key-builder';
|
|
3
|
-
import {
|
|
5
|
+
import type { CacheKeyResolver, CacheOptions } from './cache';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* @CacheEvict 装饰器配置项
|
|
@@ -19,46 +21,104 @@ export interface CacheEvictOptions extends Pick<CacheOptions, 'key'> {
|
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* 解析缓存 key
|
|
22
|
-
* @param cacheName 缓存名称
|
|
23
24
|
* @param keyResolver key 解析器
|
|
24
25
|
* @param args 方法参数数组
|
|
26
|
+
* @param logContext 不包含业务参数和值的日志上下文
|
|
25
27
|
* @returns 解析后的缓存 key
|
|
26
28
|
*/
|
|
27
|
-
function resolveCacheKey(
|
|
29
|
+
function resolveCacheKey(
|
|
30
|
+
keyResolver: CacheKeyResolver | undefined,
|
|
31
|
+
args: unknown[],
|
|
32
|
+
logContext: CacheLogContext
|
|
33
|
+
): string {
|
|
28
34
|
if (keyResolver === undefined || keyResolver === null) {
|
|
29
|
-
return KeyBuilder.build(cacheName, args);
|
|
35
|
+
return KeyBuilder.build(logContext.cacheName, args);
|
|
30
36
|
}
|
|
31
37
|
if (typeof keyResolver === 'string') {
|
|
32
|
-
return KeyBuilder.build(cacheName, [keyResolver]);
|
|
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);
|
|
33
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 {
|
|
34
56
|
try {
|
|
35
|
-
return
|
|
36
|
-
} catch {
|
|
37
|
-
|
|
57
|
+
return CacheProviderRegistry.get(providerName);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'provider_resolution', error });
|
|
60
|
+
throw error;
|
|
38
61
|
}
|
|
39
62
|
}
|
|
40
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
|
+
|
|
41
82
|
/**
|
|
42
83
|
* 缓存清除装饰器
|
|
43
84
|
* 在方法执行后清除对应的缓存条目
|
|
44
85
|
* @param cacheName 缓存名称
|
|
45
86
|
* @param options 配置项
|
|
46
87
|
*/
|
|
47
|
-
export function CacheEvict(
|
|
48
|
-
|
|
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) {
|
|
49
93
|
const originalMethod = descriptor.value;
|
|
94
|
+
const logContext: CacheLogContext = {
|
|
95
|
+
cacheName,
|
|
96
|
+
methodName: propertyKey,
|
|
97
|
+
providerName: cacheProviderLabel(options?.providerName),
|
|
98
|
+
};
|
|
50
99
|
|
|
51
100
|
descriptor.value = async function (...args: unknown[]) {
|
|
52
|
-
|
|
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
|
+
}
|
|
53
108
|
|
|
54
|
-
const
|
|
55
|
-
const provider = CacheProviderRegistry.get(providerName);
|
|
109
|
+
const provider = resolveCacheProvider(options?.providerName, logContext);
|
|
56
110
|
|
|
57
111
|
if (options?.allEntries) {
|
|
58
|
-
|
|
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' });
|
|
59
119
|
} else {
|
|
60
|
-
const cacheKey = resolveCacheKey(
|
|
61
|
-
provider
|
|
120
|
+
const cacheKey = resolveCacheKey(options?.key, args, logContext);
|
|
121
|
+
dispatchCacheDelete(provider, cacheKey, logContext);
|
|
62
122
|
}
|
|
63
123
|
|
|
64
124
|
return result;
|
package/src/decorators/cache.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
+
import {
|
|
3
|
+
decodeCacheError,
|
|
4
|
+
encodeCacheError,
|
|
5
|
+
isCurrentCacheErrorPayload,
|
|
6
|
+
normalizeCacheErrorPolicy,
|
|
7
|
+
type CacheErrorCodec,
|
|
8
|
+
type CacheErrorPolicy,
|
|
9
|
+
type NormalizedCacheErrorPolicy,
|
|
10
|
+
} from '../core/cache-error';
|
|
11
|
+
import { cacheProviderLabel, logCacheEvent, type CacheLogContext } from '../core/cache-logger';
|
|
12
|
+
import type { CacheProvider } from '../core/cache-provider';
|
|
2
13
|
import { CacheProviderRegistry } from '../core/cache-provider-registry';
|
|
3
14
|
import { KeyBuilder } from '../core/key-builder';
|
|
4
15
|
import { PendingCache } from '../core/pending-cache';
|
|
@@ -11,6 +22,8 @@ import { PendingCache } from '../core/pending-cache';
|
|
|
11
22
|
*/
|
|
12
23
|
export type CacheKeyResolver = null | string | ((...args: unknown[]) => string);
|
|
13
24
|
|
|
25
|
+
export type { CacheErrorCodec, CacheErrorPolicy };
|
|
26
|
+
|
|
14
27
|
/**
|
|
15
28
|
* @Cache 装饰器配置项
|
|
16
29
|
*/
|
|
@@ -32,6 +45,11 @@ export interface CacheOptions {
|
|
|
32
45
|
* - function: 调用函数后使用 KeyBuilder.build(cacheName, [result])
|
|
33
46
|
*/
|
|
34
47
|
key?: CacheKeyResolver;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 业务异常缓存策略;省略时不向 Provider 持久化业务异常。
|
|
51
|
+
*/
|
|
52
|
+
errorCache?: CacheErrorPolicy;
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
/**
|
|
@@ -53,26 +71,176 @@ interface ErrorCacheEntry {
|
|
|
53
71
|
*/
|
|
54
72
|
type CacheEntry<T> = SuccessCacheEntry<T> | ErrorCacheEntry;
|
|
55
73
|
|
|
74
|
+
interface CacheWriteRequest<T> {
|
|
75
|
+
readonly provider: CacheProvider;
|
|
76
|
+
readonly cacheKey: string;
|
|
77
|
+
readonly entry: CacheEntry<T>;
|
|
78
|
+
readonly entryType: 'value' | 'error';
|
|
79
|
+
readonly ttl: number | undefined;
|
|
80
|
+
readonly logContext: CacheLogContext;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type CacheReadResult<T> =
|
|
84
|
+
| { readonly type: 'value'; readonly value: T }
|
|
85
|
+
| { readonly type: 'error'; readonly error: unknown }
|
|
86
|
+
| { readonly type: 'miss' };
|
|
87
|
+
|
|
88
|
+
interface BusinessErrorRequest {
|
|
89
|
+
readonly error: unknown;
|
|
90
|
+
readonly errorPolicy: NormalizedCacheErrorPolicy | undefined;
|
|
91
|
+
readonly provider: CacheProvider;
|
|
92
|
+
readonly cacheKey: string;
|
|
93
|
+
readonly logContext: CacheLogContext;
|
|
94
|
+
}
|
|
95
|
+
|
|
56
96
|
const pendingCache = new PendingCache();
|
|
57
97
|
|
|
98
|
+
/**
|
|
99
|
+
* 获取配置指向的缓存 Provider,并在解析失败时记录原始错误。
|
|
100
|
+
* @param providerName decorator 配置中的 Provider 名称。
|
|
101
|
+
* @param logContext 当前方法的稳定日志上下文。
|
|
102
|
+
* @returns 已注册的缓存 Provider。
|
|
103
|
+
* @throws Provider 注册表抛出的原始错误。
|
|
104
|
+
*/
|
|
105
|
+
function resolveCacheProvider(providerName: string | undefined, logContext: CacheLogContext): CacheProvider {
|
|
106
|
+
try {
|
|
107
|
+
return CacheProviderRegistry.get(providerName);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'provider_resolution', error });
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 保持 fire-and-forget 语义提交缓存写入,仅捕获调用当下可观察的同步失败。
|
|
116
|
+
* @param request 写入所需 Provider、entry 和无业务数据日志上下文。
|
|
117
|
+
* @returns 无返回值;异步写入 Promise 不会被等待或消费。
|
|
118
|
+
* @throws Provider set 同步抛出的原始错误。
|
|
119
|
+
*/
|
|
120
|
+
function dispatchCacheWrite<T>(request: CacheWriteRequest<T>): void {
|
|
121
|
+
try {
|
|
122
|
+
request.provider.set(request.cacheKey, request.entry, request.ttl);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
logCacheEvent('cache.operation_failed', { ...request.logContext, operation: 'write', error });
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
logCacheEvent('cache.write_dispatched', { ...request.logContext, entryType: request.entryType });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 提交异常条目写入;同步 Provider 失败只记录基础设施错误,不遮蔽已经发生的业务异常。
|
|
132
|
+
* @param request 异常写入所需 Provider、entry、TTL 和稳定日志上下文。
|
|
133
|
+
* @returns 无返回值;异步写入 Promise 保持既有 fire-and-forget 边界。
|
|
134
|
+
*/
|
|
135
|
+
function dispatchCacheErrorWrite(request: CacheWriteRequest<never>): void {
|
|
136
|
+
try {
|
|
137
|
+
request.provider.set(request.cacheKey, request.entry, request.ttl);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
logCacheEvent('cache.operation_failed', { ...request.logContext, operation: 'write', error });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
logCacheEvent('cache.write_dispatched', { ...request.logContext, entryType: 'error' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isErrorCacheEntry<T>(entry: CacheEntry<T>): entry is ErrorCacheEntry {
|
|
146
|
+
return typeof entry === 'object' && entry !== null && 'error' in entry;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 按当前异常策略分类 Provider 条目,并只把可成功解码的当前版本异常视为命中。
|
|
151
|
+
* @param entry Provider 返回的缓存联合条目。
|
|
152
|
+
* @param errorPolicy 当前装饰器归一化后的异常策略。
|
|
153
|
+
* @param logContext 当前方法的稳定日志上下文。
|
|
154
|
+
* @returns value/error 命中结果,或需要继续业务流程的 miss。
|
|
155
|
+
*/
|
|
156
|
+
function resolveCachedEntry<T>(
|
|
157
|
+
entry: CacheEntry<T>,
|
|
158
|
+
errorPolicy: NormalizedCacheErrorPolicy | undefined,
|
|
159
|
+
logContext: CacheLogContext
|
|
160
|
+
): CacheReadResult<T> {
|
|
161
|
+
if (!isErrorCacheEntry(entry)) {
|
|
162
|
+
logCacheEvent('cache.hit', { ...logContext, entryType: 'value' });
|
|
163
|
+
return { type: 'value', value: entry.value };
|
|
164
|
+
}
|
|
165
|
+
if (!isCurrentCacheErrorPayload(entry.error)) {
|
|
166
|
+
logCacheEvent('cache.error_cache_skipped', { ...logContext, reason: 'legacy_entry' });
|
|
167
|
+
return { type: 'miss' };
|
|
168
|
+
}
|
|
169
|
+
if (errorPolicy === undefined) {
|
|
170
|
+
logCacheEvent('cache.error_cache_skipped', { ...logContext, reason: 'disabled_entry' });
|
|
171
|
+
return { type: 'miss' };
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const error = decodeCacheError(entry.error, errorPolicy);
|
|
175
|
+
logCacheEvent('cache.hit', { ...logContext, entryType: 'error' });
|
|
176
|
+
return { type: 'error', error };
|
|
177
|
+
} catch (_error) {
|
|
178
|
+
logCacheEvent('cache.error_cache_failed', { ...logContext, phase: 'decode' });
|
|
179
|
+
return { type: 'miss' };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 按固定顺序评估筛选器、编码异常并发起独立 TTL 写入,任何策略失败都保留原业务异常。
|
|
185
|
+
* @param request 本次业务异常、当前策略、Provider、完整 key 与稳定日志上下文。
|
|
186
|
+
* @returns 无返回值。
|
|
187
|
+
*/
|
|
188
|
+
function handleBusinessError(request: BusinessErrorRequest): void {
|
|
189
|
+
const { error, errorPolicy, provider, cacheKey, logContext } = request;
|
|
190
|
+
if (errorPolicy === undefined) {
|
|
191
|
+
logCacheEvent('cache.error_cache_skipped', { ...logContext, reason: 'disabled' });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (errorPolicy.shouldCache !== undefined) {
|
|
195
|
+
let accepted: boolean;
|
|
196
|
+
try {
|
|
197
|
+
accepted = errorPolicy.shouldCache(error);
|
|
198
|
+
if (typeof accepted !== 'boolean') {
|
|
199
|
+
throw new TypeError('Cache error shouldCache must return a boolean');
|
|
200
|
+
}
|
|
201
|
+
} catch (_error) {
|
|
202
|
+
logCacheEvent('cache.error_cache_failed', { ...logContext, phase: 'predicate' });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!accepted) {
|
|
206
|
+
logCacheEvent('cache.error_cache_skipped', { ...logContext, reason: 'predicate_rejected' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let entry: ErrorCacheEntry;
|
|
212
|
+
try {
|
|
213
|
+
entry = { error: encodeCacheError(error, errorPolicy) };
|
|
214
|
+
} catch (_error) {
|
|
215
|
+
logCacheEvent('cache.error_cache_failed', { ...logContext, phase: 'encode' });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
dispatchCacheErrorWrite({ provider, cacheKey, entry, entryType: 'error', ttl: errorPolicy.ttl, logContext });
|
|
219
|
+
}
|
|
220
|
+
|
|
58
221
|
/**
|
|
59
222
|
* 解析缓存 key
|
|
60
|
-
* @param cacheName 缓存名称
|
|
61
223
|
* @param keyResolver key 解析器
|
|
62
224
|
* @param args 方法参数数组
|
|
225
|
+
* @param logContext 不包含业务参数和值的日志上下文
|
|
63
226
|
* @returns 解析后的缓存 key
|
|
64
227
|
*/
|
|
65
|
-
function resolveCacheKey(
|
|
228
|
+
function resolveCacheKey(
|
|
229
|
+
keyResolver: CacheKeyResolver | undefined,
|
|
230
|
+
args: unknown[],
|
|
231
|
+
logContext: CacheLogContext
|
|
232
|
+
): string {
|
|
66
233
|
if (keyResolver === undefined || keyResolver === null) {
|
|
67
|
-
return KeyBuilder.build(cacheName, args);
|
|
234
|
+
return KeyBuilder.build(logContext.cacheName, args);
|
|
68
235
|
}
|
|
69
236
|
if (typeof keyResolver === 'string') {
|
|
70
|
-
return KeyBuilder.build(cacheName, [keyResolver]);
|
|
237
|
+
return KeyBuilder.build(logContext.cacheName, [keyResolver]);
|
|
71
238
|
}
|
|
72
239
|
try {
|
|
73
|
-
return KeyBuilder.build(cacheName, [keyResolver(...args)]);
|
|
74
|
-
} catch {
|
|
75
|
-
|
|
240
|
+
return KeyBuilder.build(logContext.cacheName, [keyResolver(...args)]);
|
|
241
|
+
} catch (_error) {
|
|
242
|
+
logCacheEvent('cache.key_fallback', { ...logContext, reason: 'resolver_error' });
|
|
243
|
+
return KeyBuilder.build(logContext.cacheName, args);
|
|
76
244
|
}
|
|
77
245
|
}
|
|
78
246
|
|
|
@@ -82,38 +250,64 @@ function resolveCacheKey(cacheName: string, keyResolver: CacheKeyResolver | unde
|
|
|
82
250
|
* @param cacheName 缓存名称
|
|
83
251
|
* @param options 配置项
|
|
84
252
|
*/
|
|
85
|
-
export function Cache(
|
|
86
|
-
|
|
253
|
+
export function Cache(
|
|
254
|
+
cacheName: string,
|
|
255
|
+
options?: CacheOptions
|
|
256
|
+
): (_target: object, _propertyKey: string, descriptor: PropertyDescriptor) => void {
|
|
257
|
+
const errorPolicy = normalizeCacheErrorPolicy(options?.errorCache);
|
|
258
|
+
return function <T>(_target: object, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
87
259
|
const originalMethod = descriptor.value;
|
|
260
|
+
const logContext: CacheLogContext = {
|
|
261
|
+
cacheName,
|
|
262
|
+
methodName: propertyKey,
|
|
263
|
+
providerName: cacheProviderLabel(options?.providerName),
|
|
264
|
+
};
|
|
88
265
|
|
|
89
|
-
descriptor.value =
|
|
90
|
-
const cacheKey = resolveCacheKey(
|
|
91
|
-
const providerName = options?.providerName;
|
|
92
|
-
const provider = CacheProviderRegistry.get(providerName);
|
|
93
|
-
|
|
94
|
-
const cached = await provider.get(cacheKey);
|
|
95
|
-
if (cached !== undefined) {
|
|
96
|
-
const entry = cached as CacheEntry<T>;
|
|
97
|
-
if ('error' in entry) {
|
|
98
|
-
throw entry.error;
|
|
99
|
-
}
|
|
100
|
-
return entry.value;
|
|
101
|
-
}
|
|
266
|
+
descriptor.value = function (...args: unknown[]): Promise<T> {
|
|
267
|
+
const cacheKey = resolveCacheKey(options?.key, args, logContext);
|
|
102
268
|
|
|
103
269
|
const pending = pendingCache.get<T>(cacheKey);
|
|
104
270
|
if (pending) {
|
|
271
|
+
logCacheEvent('cache.pending_hit', logContext);
|
|
105
272
|
return pending;
|
|
106
273
|
}
|
|
107
274
|
|
|
108
275
|
const promise = (async () => {
|
|
276
|
+
const provider = resolveCacheProvider(options?.providerName, logContext);
|
|
277
|
+
let cached: CacheEntry<T> | undefined;
|
|
278
|
+
try {
|
|
279
|
+
cached = await provider.get<CacheEntry<T>>(cacheKey);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'read', error });
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
if (cached !== undefined) {
|
|
285
|
+
const cachedResult = resolveCachedEntry(cached, errorPolicy, logContext);
|
|
286
|
+
if (cachedResult.type === 'value') {
|
|
287
|
+
return cachedResult.value;
|
|
288
|
+
}
|
|
289
|
+
if (cachedResult.type === 'error') {
|
|
290
|
+
throw cachedResult.error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
logCacheEvent('cache.miss', logContext);
|
|
294
|
+
|
|
295
|
+
let result: T;
|
|
109
296
|
try {
|
|
110
|
-
|
|
111
|
-
provider.set(cacheKey, { value: result } as CacheEntry<T>, options?.ttl);
|
|
112
|
-
return result;
|
|
297
|
+
result = (await originalMethod.apply(this, args)) as T;
|
|
113
298
|
} catch (error) {
|
|
114
|
-
|
|
299
|
+
handleBusinessError({ error, errorPolicy, provider, cacheKey, logContext });
|
|
115
300
|
throw error;
|
|
116
301
|
}
|
|
302
|
+
dispatchCacheWrite({
|
|
303
|
+
provider,
|
|
304
|
+
cacheKey,
|
|
305
|
+
entry: { value: result },
|
|
306
|
+
entryType: 'value',
|
|
307
|
+
ttl: options?.ttl,
|
|
308
|
+
logContext,
|
|
309
|
+
});
|
|
310
|
+
return result;
|
|
117
311
|
})();
|
|
118
312
|
|
|
119
313
|
pendingCache.set(cacheKey, promise);
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
export { createIoredisCacheClient } from './adapters/ioredis-cache-client';
|
|
2
|
+
export { createNodeRedisCacheClient } from './adapters/node-redis-cache-client';
|
|
1
3
|
export * from './core/cache-provider';
|
|
2
4
|
export * from './core/cache-provider-registry';
|
|
3
5
|
export * from './core/key-builder';
|
|
4
6
|
export * from './core/native-cache';
|
|
5
7
|
export * from './core/pending-cache';
|
|
8
|
+
export * from './core/redis-cache-client';
|
|
6
9
|
export * from './core/redis-cache';
|
|
7
10
|
export * from './decorators/cache';
|
|
8
11
|
export * from './decorators/cache-evict';
|