@jintianxiayu/cache-decorator 1.0.0 → 1.0.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/CHANGELOG.md +25 -11
  2. package/README.md +364 -270
  3. package/dist/core/cache-error.d.ts +75 -0
  4. package/dist/core/cache-error.d.ts.map +1 -0
  5. package/dist/core/cache-error.js +168 -0
  6. package/dist/core/cache-error.js.map +1 -0
  7. package/dist/core/cache-logger.d.ts +3 -2
  8. package/dist/core/cache-logger.d.ts.map +1 -1
  9. package/dist/core/cache-logger.js +2 -0
  10. package/dist/core/cache-logger.js.map +1 -1
  11. package/dist/decorators/cache-evict.d.ts.map +1 -1
  12. package/dist/decorators/cache-evict.js +15 -10
  13. package/dist/decorators/cache-evict.js.map +1 -1
  14. package/dist/decorators/cache.d.ts +6 -0
  15. package/dist/decorators/cache.d.ts.map +1 -1
  16. package/dist/decorators/cache.js +113 -36
  17. package/dist/decorators/cache.js.map +1 -1
  18. package/jest.config.js +11 -11
  19. package/package.json +1 -1
  20. package/src/adapters/ioredis-cache-client.ts +89 -89
  21. package/src/adapters/node-redis-cache-client.ts +95 -95
  22. package/src/adapters/redis-key-prefix.ts +38 -38
  23. package/src/core/cache-error.ts +233 -0
  24. package/src/core/cache-logger.ts +116 -105
  25. package/src/core/key-builder.ts +33 -33
  26. package/src/core/native-cache.ts +55 -55
  27. package/src/core/pending-cache.ts +29 -29
  28. package/src/core/redis-cache-client.ts +160 -160
  29. package/src/core/redis-cache.ts +104 -104
  30. package/src/decorators/cache-evict.ts +137 -129
  31. package/src/decorators/cache.ts +323 -203
  32. package/src/index.ts +11 -11
  33. package/test/cache-evict-logging.test.ts +398 -362
  34. package/test/cache-logger.integration.test.ts +159 -129
  35. package/test/cache-logger.test.ts +156 -153
  36. package/test/cache-logging.test.ts +929 -544
  37. package/test/cache.test.ts +1017 -255
  38. package/test/fixtures/cache-logger-child.mjs +108 -108
  39. package/test/fixtures/cache-provider-failure-child.mjs +70 -0
  40. package/test/helpers/legacy-redis-cache.ts +41 -22
  41. package/test/helpers/package-consumer.ts +231 -231
  42. package/test/helpers/redis-fixture.ts +142 -142
  43. package/test/ioredis-cache-client.test.ts +143 -143
  44. package/test/legacy-redis-cache.test.ts +51 -0
  45. package/test/native-cache.test.ts +77 -77
  46. package/test/node-redis-cache-client.test.ts +149 -149
  47. package/test/pending-cache.test.ts +69 -69
  48. package/test/redis-cache-client-lifecycle.test.ts +112 -112
  49. package/test/redis-cache-client-types.test.ts +184 -184
  50. package/test/redis-cache-client.integration.test.ts +355 -327
  51. package/test/redis-cache-decorator.test.ts +601 -201
  52. package/test/redis-cache-provider.test.ts +269 -269
  53. package/test/type-contract/contract.ts +119 -64
  54. package/test/type-contract/tsconfig.json +12 -12
  55. package/tsconfig.json +8 -8
package/README.md CHANGED
@@ -1,270 +1,364 @@
1
- # @jintianxiayu/cache-decorator
2
-
3
- 为 TypeScript 异步方法提供声明式缓存、缓存清除、请求合并,以及 Memory、Redis 和自定义缓存后端。
4
-
5
- ## 目录
6
-
7
- - [安装](#安装)
8
- - [快速开始](#快速开始)
9
- - [缓存日志](#缓存日志)
10
- - [Redis](#redis)
11
- - [API](#api)
12
-
13
- ## 安装
14
-
15
- Logger 是必需的 peer dependency;基础包不绑定任何 Redis SDK:
16
-
17
- ```bash
18
- pnpm add @jintianxiayu/cache-decorator @jintianxiayu/logger reflect-metadata
19
- ```
20
-
21
- 需要 Redis 时,只安装应用实际使用的客户端:
22
-
23
- ```bash
24
- pnpm add redis
25
- # 或
26
- pnpm add ioredis
27
- ```
28
-
29
- ## 快速开始
30
-
31
- ```typescript
32
- import 'reflect-metadata';
33
- import { Cache, CacheEvict, CacheProviderRegistry, MemoryCacheProvider } from '@jintianxiayu/cache-decorator';
34
- import { LoggerFactory } from '@jintianxiayu/logger';
35
-
36
- LoggerFactory.init({
37
- root: {
38
- level: 'info',
39
- console: { enabled: true },
40
- file: { enabled: false },
41
- },
42
- loggers: {
43
- '@jintianxiayu/cache-decorator': {
44
- level: 'debug',
45
- console: { enabled: true, format: 'json' },
46
- },
47
- },
48
- });
49
-
50
- CacheProviderRegistry.register('memory', new MemoryCacheProvider());
51
- CacheProviderRegistry.setDefault('memory');
52
-
53
- class UserService {
54
- @Cache('user-cache', { ttl: 60 })
55
- async getUser(id: number): Promise<{ id: number; name: string }> {
56
- return { id, name: 'test' };
57
- }
58
-
59
- @CacheEvict('user-cache')
60
- async updateUser(id: number): Promise<{ id: number; updated: boolean }> {
61
- return { id, updated: true };
62
- }
63
- }
64
- ```
65
-
66
- `ttl` 的单位是秒;`undefined` 或 `0` 表示不设置过期时间。
67
-
68
- > `@Cache` `@CacheEvict` 仅适用于返回 Promise 的方法。装饰同步方法会让调用方收到 Promise,而不是原返回值。
69
-
70
- ### 自定义缓存 Key
71
-
72
- `key` 可以是固定字符串或根据方法参数计算的函数:
73
-
74
- ```typescript
75
- class UserService {
76
- @Cache('config', { key: 'global-config' })
77
- async getConfig(): Promise<{ enabled: boolean }> {
78
- return { enabled: true };
79
- }
80
-
81
- @Cache('user', { key: (...args: unknown[]) => String(args[0]) })
82
- async getUser(id: number): Promise<{ id: number }> {
83
- return { id };
84
- }
85
-
86
- @CacheEvict('user', { key: (...args: unknown[]) => String(args[0]) })
87
- async deleteUser(id: number): Promise<{ id: number; deleted: boolean }> {
88
- return { id, deleted: true };
89
- }
90
- }
91
- ```
92
-
93
- ## 缓存日志
94
-
95
- 缓存装饰器使用名称固定为 `@jintianxiayu/cache-decorator` 的 Logger。应用必须安装兼容版本的
96
- `@jintianxiayu/logger`,并在第一次调用被装饰方法前完成 `LoggerFactory.init()`;应用退出时仍由应用统一调用
97
- `LoggerFactory.shutdown()`。仅导入包、声明 decorator 或定义 class 不会提前获取 Logger,cache 包也不会自行初始化或关闭
98
- Logger。
99
-
100
- 日志的 level、console/file transport、格式和脱敏只由同名 Logger profile 控制,不需要也不支持
101
- `CacheOptions.logging`、`debug` 或 Logger 回调。高频正常决策默认使用 `debug`,可恢复回退使用 `warn`,缓存基础设施失败使用
102
- `error`;逐调用事件不使用 `info`。
103
-
104
- | level | event | 含义 |
105
- | ------- | ------------------------ | ------------------------------------------------------ |
106
- | `debug` | `cache.pending_hit` | 复用相同 key 的执行中 Promise |
107
- | `debug` | `cache.hit` | 命中 value 或 error 缓存条目 |
108
- | `debug` | `cache.miss` | Provider 正常返回未命中 |
109
- | `debug` | `cache.write_dispatched` | `set()` 已同步返回控制权,不表示异步写入成功 |
110
- | `debug` | `cache.evict_dispatched` | 单 key `delete()` 已同步返回控制权,不表示异步删除成功 |
111
- | `debug` | `cache.evict_completed` | 已等待的 `deleteByPattern()` 正常完成 |
112
- | `warn` | `cache.key_fallback` | 自定义 key resolver 失败,已回退默认 key |
113
- | `warn` | `cache.evict_skipped` | 业务方法失败,淘汰被跳过 |
114
- | `error` | `cache.operation_failed` | Provider 解析或可观察的读取、写入、淘汰操作失败 |
115
-
116
- `write_dispatched` 和 `evict_dispatched` 只描述调用已发起。为保持既有时序,decorator 不等待 `set()` 或单 key
117
- `delete()` 返回的 Promise,也不为日志附加 rejection handler;异步失败不会被描述为成功或完成。只有本来就会等待的全量淘汰可记录
118
- `evict_completed`。
119
-
120
- 每条日志只包含 `event`、`cacheName`、`methodName`、`providerName`,并按事件增加 `entryType`、`scope`、
121
- `reason`、`operation` 或基础设施 `error`。日志不会包含方法参数、业务返回值、缓存值、业务异常内容或完整逻辑/物理 cache
122
- key,也不会为日志额外序列化这些值。Provider 错误和现有 `LoggerContext` `traceId` 继续由 Logger 统一规范化、脱敏和关联;
123
- cache 包不直接读写 LoggerContext。Logger 自身同步失败会被隔离,不会替换缓存结果、业务结果或原始 Provider 错误。
124
-
125
- ## Redis
126
-
127
- `RedisCacheProvider` 从 0.2.0 起必须接收已经适配的 `RedisCacheClient`。库不会创建、连接、重连或关闭 Redis
128
- 连接,也不会修改连接选项与错误监听器;这些生命周期操作始终由应用负责。
129
-
130
- ### node-redis
131
-
132
- ```typescript
133
- import { createClient } from 'redis';
134
- import { CacheProviderRegistry, RedisCacheProvider, createNodeRedisCacheClient } from '@jintianxiayu/cache-decorator';
135
-
136
- const redis = createClient({ url: process.env.REDIS_URL });
137
-
138
- export async function startCache(): Promise<void> {
139
- redis.on('error', (error: Error) => console.error('Redis error', error));
140
- await redis.connect();
141
- const client = createNodeRedisCacheClient(redis, { keyPrefix: 'my-app:' });
142
- CacheProviderRegistry.register('redis', new RedisCacheProvider(client));
143
- CacheProviderRegistry.setDefault('redis');
144
- }
145
-
146
- export async function stopCache(): Promise<void> {
147
- if (redis.isOpen) {
148
- await redis.quit();
149
- }
150
- }
151
- ```
152
-
153
- ### ioredis
154
-
155
- ```typescript
156
- import Redis from 'ioredis';
157
- import { CacheProviderRegistry, RedisCacheProvider, createIoredisCacheClient } from '@jintianxiayu/cache-decorator';
158
-
159
- const redis = new Redis(process.env.REDIS_URL ?? 'redis://127.0.0.1:6379', {
160
- lazyConnect: true,
161
- keyPrefix: 'my-app:',
162
- });
163
-
164
- export async function startCache(): Promise<void> {
165
- redis.on('error', (error: Error) => console.error('Redis error', error));
166
- await redis.connect();
167
- CacheProviderRegistry.register('redis', new RedisCacheProvider(createIoredisCacheClient(redis)));
168
- CacheProviderRegistry.setDefault('redis');
169
- }
170
-
171
- export function stopCache(): void {
172
- redis.disconnect();
173
- }
174
- ```
175
-
176
- ### 自定义 RedisCacheClient
177
-
178
- 其他 Redis SDK 或封装只需实现最小字符串命令接口,不必继承内置适配器:
179
-
180
- ```typescript
181
- import { CacheProviderRegistry, RedisCacheProvider, type RedisCacheClient } from '@jintianxiayu/cache-decorator';
182
-
183
- interface StorageCommands {
184
- read(key: string): Promise<string | null>;
185
- write(key: string, value: string, ttlSeconds?: number): Promise<void>;
186
- remove(keys: readonly string[]): Promise<void>;
187
- scan(cursor: string, pattern: string, count: number): Promise<{ cursor: string; keys: readonly string[] }>;
188
- clearDatabase(): Promise<void>;
189
- }
190
-
191
- declare const storage: StorageCommands;
192
-
193
- const client: RedisCacheClient = {
194
- get: (key) => storage.read(key),
195
- set: ({ key, value, ttlSeconds }) => storage.write(key, value, ttlSeconds),
196
- deleteMany: (keys) => storage.remove(keys),
197
- scan: ({ cursor, pattern, count }) => storage.scan(cursor, pattern, count),
198
- flushDatabase: () => storage.clearDatabase(),
199
- };
200
-
201
- CacheProviderRegistry.register('redis', new RedisCacheProvider(client));
202
- ```
203
-
204
- ### 前缀与数据兼容
205
-
206
- - ioredis 继续使用连接的 `keyPrefix`;适配器会补齐 `SCAN` 不自动处理前缀的差异。
207
- - node-redis 没有透明 `keyPrefix`,应在 `createNodeRedisCacheClient(client, { keyPrefix })` 中传入等价的字面前缀。
208
- - 两种客户端连接同一 database、使用相同最终物理 key 时,可以互读既有缓存。字符串、JSON、miss 映射和秒级 TTL
209
- 格式均与 0.1.3 保持一致,无需重写或清空数据。
210
- - 原始字符串读取时仍会优先尝试 `JSON.parse`;例如缓存文本 `"true"` 会读为布尔值 `true`,这是既有兼容行为。
211
-
212
- `deleteByPattern()` 使用 `SCAN COUNT 100` 分页删除。它不是原子快照;并发写入时不承诺一次删除全部匹配项,命令中途失败后可幂等重试。
213
-
214
- > **危险:`RedisCacheProvider.clear()` 会执行 `FLUSHDB`,清空连接当前选择的整个 Redis database。**
215
- > `keyPrefix` 不会缩小其范围;共享 database 时不要调用它。
216
-
217
- ### 异常与支持范围
218
-
219
- - Redis 命令拒绝会保留原始错误;不受支持的返回结构会抛出 `TypeError`。
220
- - TTL 必须是正有限整数秒,或使用 `0`/`undefined` 表示永不过期;其他值会在发送命令前抛出 `RangeError`。
221
- - 0.2.x 首版验证 ioredis 5 node-redis 5 的普通单实例连接及默认字符串/数字响应。
222
- - Cluster 跨分片扫描、Sentinel 专项切换、自定义 RESP Buffer 映射、自动故障切换和连接管理不在首版支持范围。
223
-
224
- ### 0.1.3 迁移
225
-
226
- 旧构造方式:
227
-
228
- ```typescript
229
- const provider = new RedisCacheProvider(redis);
230
- ```
231
-
232
- 新构造方式:
233
-
234
- ```typescript
235
- const provider = new RedisCacheProvider(createIoredisCacheClient(redis));
236
- ```
237
-
238
- 升级时保留原 database 和物理前缀,并由应用直接声明客户端依赖、先连接、最后自行关闭连接。需要回退时锁定
239
- `@jintianxiayu/cache-decorator@0.1.3` 并恢复旧构造方式;数据格式未改变,不需要清理 Redis。
240
-
241
- ## API
242
-
243
- ### @Cache(cacheName, options?)
244
-
245
- 缓存装饰器,必须用于异步方法。
246
-
247
- - `cacheName`:缓存名称。
248
- - `options.ttl`:过期时间,单位为秒。
249
- - `options.providerName`:指定已注册的 `CacheProvider`。
250
- - `options.key`:`undefined`/`null` 使用默认参数 key;字符串使用固定 key;函数根据方法参数返回 key。
251
-
252
- ### @CacheEvict(cacheName, options?)
253
-
254
- 缓存清除装饰器,必须用于异步方法。
255
-
256
- - `options.allEntries`:为 `true` 时删除该缓存名称下的匹配项,而不是执行数据库级 `clear()`。
257
- - `options.providerName`:指定已注册的 `CacheProvider`。
258
- - `options.key`:行为与 `@Cache` 一致;`allEntries: true` 时忽略。
259
-
260
- ### CacheProviderRegistry
261
-
262
- 全局 Provider 注册表,使用 `register(name, provider)` 注册,并通过 `setDefault(name)` 选择默认 Provider。
263
-
264
- ### MemoryCacheProvider
265
-
266
- 基于进程内 Map 的实现,适用于单进程缓存。
267
-
268
- ### RedisCacheProvider
269
-
270
- 维护字符串/JSON、miss、TTL 和扫描删除协议;必须传入 `RedisCacheClient`,可使用内置适配工厂或自定义实现。
1
+ # @jintianxiayu/cache-decorator
2
+
3
+ 为 TypeScript 异步方法提供声明式缓存、缓存清除、请求合并,以及 Memory、Redis 和自定义缓存后端。
4
+
5
+ ## 目录
6
+
7
+ - [安装](#安装)
8
+ - [快速开始](#快速开始)
9
+ - [异常缓存策略](#异常缓存策略)
10
+ - [缓存日志](#缓存日志)
11
+ - [Redis](#redis)
12
+ - [异常缓存迁移](#异常缓存迁移)
13
+ - [API](#api)
14
+
15
+ ## 安装
16
+
17
+ Logger 是必需的 peer dependency;基础包不绑定任何 Redis SDK:
18
+
19
+ ```bash
20
+ pnpm add @jintianxiayu/cache-decorator @jintianxiayu/logger reflect-metadata
21
+ ```
22
+
23
+ 需要 Redis 时,只安装应用实际使用的客户端:
24
+
25
+ ```bash
26
+ pnpm add redis
27
+ # 或
28
+ pnpm add ioredis
29
+ ```
30
+
31
+ ## 快速开始
32
+
33
+ ```typescript
34
+ import 'reflect-metadata';
35
+ import { Cache, CacheEvict, CacheProviderRegistry, MemoryCacheProvider } from '@jintianxiayu/cache-decorator';
36
+ import { LoggerFactory } from '@jintianxiayu/logger';
37
+
38
+ LoggerFactory.init({
39
+ root: {
40
+ level: 'info',
41
+ console: { enabled: true },
42
+ file: { enabled: false },
43
+ },
44
+ loggers: {
45
+ '@jintianxiayu/cache-decorator': {
46
+ level: 'debug',
47
+ console: { enabled: true, format: 'json' },
48
+ },
49
+ },
50
+ });
51
+
52
+ CacheProviderRegistry.register('memory', new MemoryCacheProvider());
53
+ CacheProviderRegistry.setDefault('memory');
54
+
55
+ class UserService {
56
+ @Cache('user-cache', { ttl: 60 })
57
+ async getUser(id: number): Promise<{ id: number; name: string }> {
58
+ return { id, name: 'test' };
59
+ }
60
+
61
+ @CacheEvict('user-cache')
62
+ async updateUser(id: number): Promise<{ id: number; updated: boolean }> {
63
+ return { id, updated: true };
64
+ }
65
+ }
66
+ ```
67
+
68
+ `ttl` 的单位是秒;`undefined` `0` 表示不设置过期时间。
69
+
70
+ 业务异常默认不写入 Provider。只有显式配置 `errorCache` 时才会持久化异常,且异常必须使用独立的有限 TTL。
71
+
72
+ > `@Cache` 和 `@CacheEvict` 仅适用于返回 Promise 的方法。装饰同步方法会让调用方收到 Promise,而不是原返回值。
73
+
74
+ ### 自定义缓存 Key
75
+
76
+ `key` 可以是固定字符串或根据方法参数计算的函数:
77
+
78
+ ```typescript
79
+ class UserService {
80
+ @Cache('config', { key: 'global-config' })
81
+ async getConfig(): Promise<{ enabled: boolean }> {
82
+ return { enabled: true };
83
+ }
84
+
85
+ @Cache('user', { key: (...args: unknown[]) => String(args[0]) })
86
+ async getUser(id: number): Promise<{ id: number }> {
87
+ return { id };
88
+ }
89
+
90
+ @CacheEvict('user', { key: (...args: unknown[]) => String(args[0]) })
91
+ async deleteUser(id: number): Promise<{ id: number; deleted: boolean }> {
92
+ return { id, deleted: true };
93
+ }
94
+ }
95
+ ```
96
+
97
+ ## 异常缓存策略
98
+
99
+ 默认关闭异常缓存可以避免数据库、网络、超时、限流等瞬时故障被持续放大。只有能够安全复用的稳定业务异常才应通过白名单筛选器短时缓存:
100
+
101
+ ```typescript
102
+ class UserNotFoundError extends Error {}
103
+
104
+ class UserService {
105
+ @Cache('user', {
106
+ ttl: 300,
107
+ errorCache: {
108
+ ttl: 10,
109
+ shouldCache: (error: unknown): boolean => error instanceof UserNotFoundError,
110
+ },
111
+ })
112
+ async getUser(id: number): Promise<{ id: number }> {
113
+ throw new UserNotFoundError(`User ${id} not found`);
114
+ }
115
+ }
116
+ ```
117
+
118
+ - `errorCache` 省略时,业务异常不会持久化;相同 key 的执行中调用仍会复用同一个 pending Promise。
119
+ - `errorCache.ttl` 必须是大于零的有限整数秒,不能继承正常结果的 `ttl`。非法值会在 legacy decorator 求值时抛出 `RangeError`。
120
+ - `shouldCache` 省略时会接受所有业务异常;返回 `false` 或自身抛错时按 fail-closed 跳过写入,并继续抛出原业务异常。
121
+ - Provider 解析或读取失败会旁路本次全部缓存操作,既不会作为业务异常缓存,也不会阻断业务方法。
122
+ - 异常写入沿用 fire-and-forget 边界;Provider 同步抛错或异步拒绝都不会替换已经发生的业务异常。
123
+
124
+ 默认 codec 会把标准 `Error` 保存为只包含 `name` 和 `message` 的 JSON payload。缓存命中会创建新的 `Error`,不保留原对象身份、`stack`、自定义原型或任意自有属性。非 `Error` 抛出值必须能安全 JSON 往返;`undefined`、函数、symbol、循环引用和非有限数会跳过写入。
125
+
126
+ 错误消息仍可能包含敏感信息。需要脱敏或恢复领域错误类型时,应提供成对的自定义 codec,并确保共享同一 cache key 的所有进程使用兼容协议:
127
+
128
+ ```typescript
129
+ import { Cache, type CacheErrorCodec } from '@jintianxiayu/cache-decorator';
130
+
131
+ class UserNotFoundError extends Error {
132
+ constructor(readonly code: string) {
133
+ super('User not found');
134
+ this.name = 'UserNotFoundError';
135
+ }
136
+ }
137
+
138
+ const userNotFoundCodec: CacheErrorCodec = {
139
+ encode(error: unknown): unknown {
140
+ if (!(error instanceof UserNotFoundError)) {
141
+ throw new TypeError('Unsupported error');
142
+ }
143
+ return { code: error.code };
144
+ },
145
+ decode(payload: unknown): unknown {
146
+ if (typeof payload !== 'object' || payload === null || !('code' in payload)) {
147
+ throw new TypeError('Invalid error payload');
148
+ }
149
+ return new UserNotFoundError(String(payload.code));
150
+ },
151
+ };
152
+
153
+ class UserService {
154
+ @Cache('user', {
155
+ errorCache: {
156
+ ttl: 10,
157
+ shouldCache: (error: unknown): boolean => error instanceof UserNotFoundError,
158
+ codec: userNotFoundCodec,
159
+ },
160
+ })
161
+ async getUser(): Promise<never> {
162
+ throw new UserNotFoundError('USER_NOT_FOUND');
163
+ }
164
+ }
165
+ ```
166
+
167
+ ## 缓存日志
168
+
169
+ 缓存装饰器使用名称固定为 `@jintianxiayu/cache-decorator` 的 Logger。应用必须安装兼容版本的
170
+ `@jintianxiayu/logger`,并在第一次调用被装饰方法前完成 `LoggerFactory.init()`;应用退出时仍由应用统一调用
171
+ `LoggerFactory.shutdown()`。仅导入包、声明 decorator 或定义 class 不会提前获取 Logger,cache 包也不会自行初始化或关闭
172
+ Logger。
173
+
174
+ 日志的 level、console/file transport、格式和脱敏只由同名 Logger profile 控制,不需要也不支持
175
+ `CacheOptions.logging`、`debug` 或 Logger 回调。高频正常决策默认使用 `debug`,可恢复回退使用 `warn`,缓存基础设施失败使用
176
+ `error`;逐调用事件不使用 `info`。
177
+
178
+ | level | event | 含义 |
179
+ | ------- | --------------------------- | ------------------------------------------------------ |
180
+ | `debug` | `cache.pending_hit` | 复用相同 key 的执行中 Promise |
181
+ | `debug` | `cache.hit` | 命中 value 或当前策略可解码的 error 条目 |
182
+ | `debug` | `cache.miss` | Provider 正常返回未命中或异常条目被安全旁路 |
183
+ | `debug` | `cache.write_dispatched` | `set()` 已同步返回控制权,不表示异步写入成功 |
184
+ | `debug` | `cache.error_cache_skipped` | 策略禁用、筛选拒绝或存量异常条目被安全旁路 |
185
+ | `debug` | `cache.evict_dispatched` | key `delete()` 已同步返回控制权,不表示异步删除成功 |
186
+ | `debug` | `cache.evict_completed` | 已等待的 `deleteByPattern()` 正常完成 |
187
+ | `warn` | `cache.key_fallback` | 自定义 key resolver 失败,已回退默认 key |
188
+ | `warn` | `cache.error_cache_failed` | 异常筛选、encode 或 decode 失败,已 fail-closed |
189
+ | `warn` | `cache.evict_skipped` | 业务方法失败,淘汰被跳过 |
190
+ | `error` | `cache.operation_failed` | Provider 解析或可观察的读取、写入、淘汰操作失败 |
191
+
192
+ `write_dispatched` 和 `evict_dispatched` 只描述调用已发起。为保持既有时序,decorator 不等待 `set()` 或单 key
193
+ `delete()` 返回的 Promise,但会消费其 rejection 并记录 `cache.operation_failed`,避免形成未处理的 Promise rejection。只有本来就会
194
+ 等待的全量淘汰可记录 `evict_completed`;全量淘汰失败不会记录 completed,也不会替换业务结果。
195
+
196
+ 每条日志只包含 `event`、`cacheName`、`methodName`、`providerName`,并按事件增加 `entryType`、`scope`、
197
+ `reason`、`phase`、`operation` 或基础设施 `error`。日志不会包含方法参数、业务返回值、缓存值、业务异常内容、codec
198
+ payload、策略回调错误或完整逻辑/物理 cache key,也不会为日志额外序列化这些值。Provider 错误和现有
199
+ `LoggerContext` 的 `traceId` 继续由 Logger 统一规范化、脱敏和关联;cache 包不直接读写 LoggerContext。Logger 自身同步失败会被隔离,不会改变缓存命中结果或 Provider 故障后的业务旁路结果。
200
+
201
+ ## Provider 故障语义
202
+
203
+ `@Cache` 与 `@CacheEvict` 固定采用 fail-open:Provider 不存在,或缓存读取、写入、单 key 删除、全量删除同步抛错或异步拒绝时,装饰器记录 `cache.operation_failed`,但仍向调用方返回业务成功结果或传播原始业务异常。读取失败不会记录为正常 miss,也不会在本次调用中写入正常或异常条目;淘汰失败不会记录为 completed。该行为没有配置开关。
204
+
205
+ fail-open 只存在于装饰器编排边界。直接调用 `CacheProvider`、`RedisCacheProvider` 或 Redis adapter 时,原始异常仍会正常抛出或拒绝,便于基础设施代码显式处理。装饰器不会自动重试、设置超时、切换到 Memory 或其他 Provider,也不负责 Memory 容量、淘汰策略或进程内存治理。
206
+
207
+ ## Redis
208
+
209
+ `RedisCacheProvider` 0.2.0 起必须接收已经适配的 `RedisCacheClient`。库不会创建、连接、重连或关闭 Redis
210
+ 连接,也不会修改连接选项与错误监听器;这些生命周期操作始终由应用负责。
211
+
212
+ ### node-redis
213
+
214
+ ```typescript
215
+ import { createClient } from 'redis';
216
+ import { CacheProviderRegistry, RedisCacheProvider, createNodeRedisCacheClient } from '@jintianxiayu/cache-decorator';
217
+
218
+ const redis = createClient({ url: process.env.REDIS_URL });
219
+
220
+ export async function startCache(): Promise<void> {
221
+ redis.on('error', (error: Error) => console.error('Redis error', error));
222
+ await redis.connect();
223
+ const client = createNodeRedisCacheClient(redis, { keyPrefix: 'my-app:' });
224
+ CacheProviderRegistry.register('redis', new RedisCacheProvider(client));
225
+ CacheProviderRegistry.setDefault('redis');
226
+ }
227
+
228
+ export async function stopCache(): Promise<void> {
229
+ if (redis.isOpen) {
230
+ await redis.quit();
231
+ }
232
+ }
233
+ ```
234
+
235
+ ### ioredis
236
+
237
+ ```typescript
238
+ import Redis from 'ioredis';
239
+ import { CacheProviderRegistry, RedisCacheProvider, createIoredisCacheClient } from '@jintianxiayu/cache-decorator';
240
+
241
+ const redis = new Redis(process.env.REDIS_URL ?? 'redis://127.0.0.1:6379', {
242
+ lazyConnect: true,
243
+ keyPrefix: 'my-app:',
244
+ });
245
+
246
+ export async function startCache(): Promise<void> {
247
+ redis.on('error', (error: Error) => console.error('Redis error', error));
248
+ await redis.connect();
249
+ CacheProviderRegistry.register('redis', new RedisCacheProvider(createIoredisCacheClient(redis)));
250
+ CacheProviderRegistry.setDefault('redis');
251
+ }
252
+
253
+ export function stopCache(): void {
254
+ redis.disconnect();
255
+ }
256
+ ```
257
+
258
+ ### 自定义 RedisCacheClient
259
+
260
+ 其他 Redis SDK 或封装只需实现最小字符串命令接口,不必继承内置适配器:
261
+
262
+ ```typescript
263
+ import { CacheProviderRegistry, RedisCacheProvider, type RedisCacheClient } from '@jintianxiayu/cache-decorator';
264
+
265
+ interface StorageCommands {
266
+ read(key: string): Promise<string | null>;
267
+ write(key: string, value: string, ttlSeconds?: number): Promise<void>;
268
+ remove(keys: readonly string[]): Promise<void>;
269
+ scan(cursor: string, pattern: string, count: number): Promise<{ cursor: string; keys: readonly string[] }>;
270
+ clearDatabase(): Promise<void>;
271
+ }
272
+
273
+ declare const storage: StorageCommands;
274
+
275
+ const client: RedisCacheClient = {
276
+ get: (key) => storage.read(key),
277
+ set: ({ key, value, ttlSeconds }) => storage.write(key, value, ttlSeconds),
278
+ deleteMany: (keys) => storage.remove(keys),
279
+ scan: ({ cursor, pattern, count }) => storage.scan(cursor, pattern, count),
280
+ flushDatabase: () => storage.clearDatabase(),
281
+ };
282
+
283
+ CacheProviderRegistry.register('redis', new RedisCacheProvider(client));
284
+ ```
285
+
286
+ ### 前缀与数据兼容
287
+
288
+ - ioredis 继续使用连接的 `keyPrefix`;适配器会补齐 `SCAN` 不自动处理前缀的差异。
289
+ - node-redis 没有透明 `keyPrefix`,应在 `createNodeRedisCacheClient(client, { keyPrefix })` 中传入等价的字面前缀。
290
+ - 两种客户端连接同一 database、使用相同最终物理 key 时,可以互读既有缓存。字符串、JSON、miss 映射和秒级 TTL
291
+ 格式均与 0.1.3 保持一致,无需重写或清空数据。
292
+ - 原始字符串读取时仍会优先尝试 `JSON.parse`;例如缓存文本 `"true"` 会读为布尔值 `true`,这是既有兼容行为。
293
+
294
+ `deleteByPattern()` 使用 `SCAN COUNT 100` 分页删除。它不是原子快照;并发写入时不承诺一次删除全部匹配项,命令中途失败后可幂等重试。
295
+
296
+ > **危险:`RedisCacheProvider.clear()` 会执行 `FLUSHDB`,清空连接当前选择的整个 Redis database。**
297
+ > `keyPrefix` 不会缩小其范围;共享 database 时不要调用它。
298
+
299
+ ## 异常缓存迁移
300
+
301
+ 异常条目的外层仍是 `{ error }`,内容改为带 kind/version 的 JSON envelope。新版本会旁路所有旧版未版本化异常条目;当前装饰器未启用 `errorCache` 时也会旁路新版异常条目。旁路不会自动删除 key,后续业务成功可用正常 `{ value }` 覆盖,或由调用方使用现有精确淘汰能力清理。
302
+
303
+ 共享 Redis cache key 的系统必须采用两阶段升级:
304
+
305
+ 1. 先把所有读取方升级到包含安全旁路逻辑的新 major,并保持 `errorCache` 省略。
306
+ 2. 确认所有读取方升级完成、共享 key 的 codec 配置兼容后,再启用有限 TTL 的 `errorCache`。
307
+
308
+ 旧版本可以把新 envelope 解析为普通 JSON,但会按旧逻辑抛出未解码对象;因此混合版本期间不得写入新版异常条目。回滚到旧 major 前,应先停止异常 envelope 写入并精确清理受影响的异常 key,不要默认执行数据库级 `clear()`。
309
+
310
+ ### 异常与支持范围
311
+
312
+ - Redis 命令拒绝会保留原始错误;不受支持的返回结构会抛出 `TypeError`。
313
+ - TTL 必须是正有限整数秒,或使用 `0`/`undefined` 表示永不过期;其他值会在发送命令前抛出 `RangeError`。
314
+ - 0.2.x 首版验证 ioredis 5 和 node-redis 5 的普通单实例连接及默认字符串/数字响应。
315
+ - Cluster 跨分片扫描、Sentinel 专项切换、自定义 RESP Buffer 映射、自动故障切换和连接管理不在首版支持范围。
316
+
317
+ ### 从 0.1.3 迁移
318
+
319
+ 旧构造方式:
320
+
321
+ ```typescript
322
+ const provider = new RedisCacheProvider(redis);
323
+ ```
324
+
325
+ 新构造方式:
326
+
327
+ ```typescript
328
+ const provider = new RedisCacheProvider(createIoredisCacheClient(redis));
329
+ ```
330
+
331
+ 升级时保留原 database 和物理前缀,并由应用直接声明客户端依赖、先连接、最后自行关闭连接。需要回退时锁定
332
+ `@jintianxiayu/cache-decorator@0.1.3` 并恢复旧构造方式;数据格式未改变,不需要清理 Redis。
333
+
334
+ ## API
335
+
336
+ ### @Cache(cacheName, options?)
337
+
338
+ 缓存装饰器,必须用于异步方法。
339
+
340
+ - `cacheName`:缓存名称。
341
+ - `options.ttl`:过期时间,单位为秒。
342
+ - `options.providerName`:指定已注册的 `CacheProvider`。
343
+ - `options.key`:`undefined`/`null` 使用默认参数 key;字符串使用固定 key;函数根据方法参数返回 key。
344
+ - `options.errorCache`:可选异常策略;包含必填正整数秒级 `ttl`,以及可选同步 `shouldCache` 和成对 `codec`。
345
+
346
+ ### @CacheEvict(cacheName, options?)
347
+
348
+ 缓存清除装饰器,必须用于异步方法。
349
+
350
+ - `options.allEntries`:为 `true` 时删除该缓存名称下的匹配项,而不是执行数据库级 `clear()`。
351
+ - `options.providerName`:指定已注册的 `CacheProvider`。
352
+ - `options.key`:行为与 `@Cache` 一致;`allEntries: true` 时忽略。
353
+
354
+ ### CacheProviderRegistry
355
+
356
+ 全局 Provider 注册表,使用 `register(name, provider)` 注册,并通过 `setDefault(name)` 选择默认 Provider。
357
+
358
+ ### MemoryCacheProvider
359
+
360
+ 基于进程内 Map 的实现,适用于单进程缓存。
361
+
362
+ ### RedisCacheProvider
363
+
364
+ 维护字符串/JSON、miss、TTL 和扫描删除协议;必须传入 `RedisCacheClient`,可使用内置适配工厂或自定义实现。