@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +306 -79
  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-error.d.ts +75 -0
  16. package/dist/core/cache-error.d.ts.map +1 -0
  17. package/dist/core/cache-error.js +168 -0
  18. package/dist/core/cache-error.js.map +1 -0
  19. package/dist/core/cache-logger.d.ts +29 -0
  20. package/dist/core/cache-logger.d.ts.map +1 -0
  21. package/dist/core/cache-logger.js +72 -0
  22. package/dist/core/cache-logger.js.map +1 -0
  23. package/dist/core/cache-provider-registry.d.ts.map +1 -1
  24. package/dist/core/cache-provider-registry.js.map +1 -1
  25. package/dist/core/cache-provider.d.ts.map +1 -1
  26. package/dist/core/key-builder.d.ts.map +1 -1
  27. package/dist/core/key-builder.js +3 -2
  28. package/dist/core/key-builder.js.map +1 -1
  29. package/dist/core/native-cache.d.ts.map +1 -1
  30. package/dist/core/native-cache.js +2 -1
  31. package/dist/core/native-cache.js.map +1 -1
  32. package/dist/core/pending-cache.d.ts.map +1 -1
  33. package/dist/core/pending-cache.js +6 -3
  34. package/dist/core/pending-cache.js.map +1 -1
  35. package/dist/core/redis-cache-client.d.ts +156 -0
  36. package/dist/core/redis-cache-client.d.ts.map +1 -0
  37. package/dist/core/redis-cache-client.js +3 -0
  38. package/dist/core/redis-cache-client.js.map +1 -0
  39. package/dist/core/redis-cache.d.ts +39 -7
  40. package/dist/core/redis-cache.d.ts.map +1 -1
  41. package/dist/core/redis-cache.js +63 -22
  42. package/dist/core/redis-cache.js.map +1 -1
  43. package/dist/decorators/cache-evict.d.ts +1 -1
  44. package/dist/decorators/cache-evict.d.ts.map +1 -1
  45. package/dist/decorators/cache-evict.js +68 -14
  46. package/dist/decorators/cache-evict.js.map +1 -1
  47. package/dist/decorators/cache.d.ts +7 -1
  48. package/dist/decorators/cache.d.ts.map +1 -1
  49. package/dist/decorators/cache.js +169 -24
  50. package/dist/decorators/cache.js.map +1 -1
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +6 -0
  54. package/dist/index.js.map +1 -1
  55. package/jest.config.js +1 -1
  56. package/package.json +26 -29
  57. package/src/adapters/ioredis-cache-client.ts +89 -0
  58. package/src/adapters/node-redis-cache-client.ts +95 -0
  59. package/src/adapters/redis-key-prefix.ts +38 -0
  60. package/src/core/cache-error.ts +233 -0
  61. package/src/core/cache-logger.ts +116 -0
  62. package/src/core/key-builder.ts +3 -1
  63. package/src/core/native-cache.ts +3 -1
  64. package/src/core/pending-cache.ts +6 -3
  65. package/src/core/redis-cache-client.ts +160 -0
  66. package/src/core/redis-cache.ts +68 -20
  67. package/src/decorators/cache-evict.ts +76 -16
  68. package/src/decorators/cache.ts +220 -26
  69. package/src/index.ts +3 -0
  70. package/test/cache-evict-logging.test.ts +362 -0
  71. package/test/cache-logger.integration.test.ts +129 -0
  72. package/test/cache-logger.test.ts +156 -0
  73. package/test/cache-logging.test.ts +852 -0
  74. package/test/cache.test.ts +717 -34
  75. package/test/fixtures/cache-logger-child.mjs +108 -0
  76. package/test/helpers/legacy-redis-cache.ts +41 -0
  77. package/test/helpers/package-consumer.ts +231 -0
  78. package/test/helpers/redis-fixture.ts +142 -0
  79. package/test/ioredis-cache-client.test.ts +143 -0
  80. package/test/legacy-redis-cache.test.ts +51 -0
  81. package/test/native-cache.test.ts +9 -3
  82. package/test/node-redis-cache-client.test.ts +149 -0
  83. package/test/pending-cache.test.ts +11 -0
  84. package/test/redis-cache-client-lifecycle.test.ts +112 -0
  85. package/test/redis-cache-client-types.test.ts +184 -0
  86. package/test/redis-cache-client.integration.test.ts +355 -0
  87. package/test/redis-cache-decorator.test.ts +540 -0
  88. package/test/redis-cache-package.integration.test.ts +328 -0
  89. package/test/redis-cache-provider.test.ts +269 -0
  90. package/test/type-contract/contract.ts +119 -0
  91. package/test/type-contract/tsconfig.json +12 -0
  92. package/tsconfig.json +1 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # @jintianxiayu/cache-decorator
2
+
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 业务异常缓存改为默认关闭,并新增独立 TTL、筛选器与可移植 codec 策略;Redis 异常条目采用版本化 envelope,共享 key 的读取方必须先完成 patch 升级再启用策略。
8
+
9
+ ## 1.0.0
10
+
11
+ ### Major Changes
12
+
13
+ - 本次发布包含以下变更:
14
+
15
+ - `@jintianxiayu/logger`:重构日志配置、命名 Logger、Plain/JSON 输出、轮转文件、元数据规范化与脱敏、异步 `traceId` 上下文、进程错误处理及可等待的关闭流程。BREAKING:包根不再导出 `createLogger`、`ConfigLoader` 和 `LogPosition`;`LoggerContext.set/clear/getStore` 改为通过 `withContext/get` 使用,配置结构及脱敏字段格式同步调整,运行时要求 Node.js 20.19+/22.13+/24+。
16
+ - `@jintianxiayu/cache-decorator`:修复并发缓存 Promise 复用及拒绝后的清理;解耦 `RedisCacheProvider` 与具体 Redis SDK,新增 `RedisCacheClient`、ioredis 和 node-redis 适配工厂,并保持 Redis key、值格式及秒级 TTL 兼容;新增缓存命中、回填、淘汰和基础设施失败的结构化日志。BREAKING:调用方必须显式包装并管理 Redis 连接、直接声明所用 Redis 客户端依赖,同时提供兼容的 `@jintianxiayu/logger` peer 并在首次缓存调用前初始化 Logger。
17
+ - `@jintianxiayu/lock-decorator`:解耦 `RedisLockProvider` 与 ioredis,新增 `RedisLockClient`、ioredis 和 node-redis 适配工厂,锁协议及连接所有权语义保持不变;新增锁获取、重试、续期、业务执行和释放的结构化日志。Watchdog 续期 Promise 拒绝会被记录并停止后续续期,不再形成未处理拒绝,已开始的业务仍继续执行并在结束时尝试释放锁。BREAKING:调用方必须显式包装现有 Redis 连接、直接声明客户端依赖,同时提供兼容的 `@jintianxiayu/logger` peer 并在首次锁调用前初始化 Logger。
18
+ - `@jintianxiayu/http-client-decorator`:同步适配 logger 0.2 的 peer 依赖与异步上下文用法;调用方应通过 `LoggerContext.withContext` 建立请求上下文,tracing 中间件继续从中读取并注入 `traceId`。
19
+ - 仓库开发与发布流程迁移到 pnpm 11 原生 workspace,并统一 TypeScript NodeNext、Jest、清理、构建和本地发布配置。
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies:
24
+ - @jintianxiayu/logger@1.0.0
package/README.md CHANGED
@@ -1,131 +1,358 @@
1
1
  # @jintianxiayu/cache-decorator
2
2
 
3
- 方法缓存装饰器,为 TypeScript 方法提供声明式缓存功能。
3
+ TypeScript 异步方法提供声明式缓存、缓存清除、请求合并,以及 Memory、Redis 和自定义缓存后端。
4
+
5
+ ## 目录
6
+
7
+ - [安装](#安装)
8
+ - [快速开始](#快速开始)
9
+ - [异常缓存策略](#异常缓存策略)
10
+ - [缓存日志](#缓存日志)
11
+ - [Redis](#redis)
12
+ - [异常缓存迁移](#异常缓存迁移)
13
+ - [API](#api)
4
14
 
5
15
  ## 安装
6
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
+
7
25
  ```bash
8
- npm install @jintianxiayu/cache-decorator reflect-metadata
26
+ pnpm add redis
27
+ # 或
28
+ pnpm add ioredis
9
29
  ```
10
30
 
11
- ## 特性
12
-
13
- - `@Cache` 装饰器:方法缓存,支持 TTL 过期
14
- - `@CacheEvict` 装饰器:缓存清除
15
- - 可插拔的缓存后端(Memory / Redis / 自定义)
16
- - 请求合并:并发场景下返回同一个 Promise
17
- - 错误结果缓存:防止缓存穿透
18
-
19
- ## 重要提示
20
-
21
- > ⚠️ **@Cache 和 @CacheEvict 装饰器仅适用于返回 Promise 的方法**
22
- >
23
- > 这两个装饰器都会将被装饰的方法替换为异步函数。如果装饰返回非 Promise 的同步方法,调用方会收到 Promise 对象而非期望的返回值,导致难以排查的 bug。
24
- >
25
- > ```typescript
26
- > // ✅ 正确:async 方法或返回 Promise 的方法
27
- > @Cache('data')
28
- > async getData() { ... }
29
- >
30
- > @CacheEvict('data')
31
- > async clearData() { ... }
32
- >
33
- > // ❌ 错误:返回非 Promise 的同步方法不要使用这些装饰器
34
- > @Cache('data')
35
- > getData() { return 'result'; } // 调用方得到 Promise,不是 'result'
36
- > ```
37
-
38
- ## 使用示例
31
+ ## 快速开始
39
32
 
40
33
  ```typescript
41
34
  import 'reflect-metadata';
42
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
+ });
43
51
 
44
- // 注册默认 Provider
45
52
  CacheProviderRegistry.register('memory', new MemoryCacheProvider());
46
53
  CacheProviderRegistry.setDefault('memory');
47
54
 
48
55
  class UserService {
49
- @Cache('user-cache', { ttl: 60000 })
50
- async getUser(id: number) {
56
+ @Cache('user-cache', { ttl: 60 })
57
+ async getUser(id: number): Promise<{ id: number; name: string }> {
51
58
  return { id, name: 'test' };
52
59
  }
53
60
 
54
61
  @CacheEvict('user-cache')
55
- async updateUser(id: number) {
62
+ async updateUser(id: number): Promise<{ id: number; updated: boolean }> {
56
63
  return { id, updated: true };
57
64
  }
58
65
  }
59
66
  ```
60
67
 
68
+ `ttl` 的单位是秒;`undefined` 或 `0` 表示不设置过期时间。
69
+
70
+ 业务异常默认不写入 Provider。只有显式配置 `errorCache` 时才会持久化异常,且异常必须使用独立的有限 TTL。
71
+
72
+ > `@Cache` 和 `@CacheEvict` 仅适用于返回 Promise 的方法。装饰同步方法会让调用方收到 Promise,而不是原返回值。
73
+
61
74
  ### 自定义缓存 Key
62
75
 
63
- 通过 `key` 选项自定义缓存 key 生成逻辑:
76
+ `key` 可以是固定字符串或根据方法参数计算的函数:
64
77
 
65
78
  ```typescript
66
79
  class UserService {
67
- // 使用字符串作为 key,所有调用共享同一缓存
68
- @Cache('config', { key: 'global-config' })
69
- async getConfig() {
70
- return { ... };
71
- }
72
-
73
- // 使用函数基于参数生成 key
74
- @Cache('user', { key: (...args) => String(args[0]) })
75
- async getUser(id: number) {
76
- return { id };
77
- }
78
-
79
- // 清除时使用相同的 key 函数
80
- @CacheEvict('user', { key: (...args) => String(args[0]) })
81
- async deleteUser(id: number) {
82
- return { id, deleted: true };
83
- }
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
+ }
84
94
  }
85
95
  ```
86
96
 
87
- ## API
97
+ ## 异常缓存策略
88
98
 
89
- ### @Cache(cacheName, options?)
99
+ 默认关闭异常缓存可以避免数据库、网络、超时、限流等瞬时故障被持续放大。只有能够安全复用的稳定业务异常才应通过白名单筛选器短时缓存:
90
100
 
91
- 缓存装饰器。**必须用于 async 方法**。
101
+ ```typescript
102
+ class UserNotFoundError extends Error {}
92
103
 
93
- - `cacheName`: 缓存名称
94
- - `options.ttl`: 过期时间(秒)
95
- - `options.providerName`: 指定 CacheProvider
96
- - `options.key`: 自定义缓存 key,支持以下形式:
97
- - `undefined/null`: 使用默认逻辑,基于方法参数生成 key
98
- - `string`: 直接作为 key 值,格式为 `cacheName:keyValue`
99
- - `function`: 接收方法参数数组,返回自定义字符串
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
+ ```
100
117
 
101
- ### @CacheEvict(cacheName, options?)
118
+ - `errorCache` 省略时,业务异常不会持久化;相同 key 的执行中调用仍会复用同一个 pending Promise。
119
+ - `errorCache.ttl` 必须是大于零的有限整数秒,不能继承正常结果的 `ttl`。非法值会在 legacy decorator 求值时抛出 `RangeError`。
120
+ - `shouldCache` 省略时会接受所有业务异常;返回 `false` 或自身抛错时按 fail-closed 跳过写入,并继续抛出原业务异常。
121
+ - Provider 解析/读取、key resolver、Logger 和 codec 的故障不会作为业务异常缓存。
122
+ - 异常写入沿用 fire-and-forget 边界;同步可观察的 Provider 写入失败不会替换已经发生的业务异常。
102
123
 
103
- 缓存清除装饰器。**必须用于返回 Promise 的方法**。
124
+ 默认 codec 会把标准 `Error` 保存为只包含 `name` 和 `message` 的 JSON payload。缓存命中会创建新的 `Error`,不保留原对象身份、`stack`、自定义原型或任意自有属性。非 `Error` 抛出值必须能安全 JSON 往返;`undefined`、函数、symbol、循环引用和非有限数会跳过写入。
104
125
 
105
- - `cacheName`: 缓存名称
106
- - `options.allEntries`: 清除该缓存名称下的所有条目(而非所有缓存),默认 false
107
- - `options.providerName`: 指定 CacheProvider
108
- - `options.key`: 自定义缓存 key,与 `@Cache` 行为一致。当 `allEntries: true` 时忽略
126
+ 错误消息仍可能包含敏感信息。需要脱敏或恢复领域错误类型时,应提供成对的自定义 codec,并确保共享同一 cache key 的所有进程使用兼容协议:
109
127
 
110
- ### CacheProviderRegistry
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
+ }
111
137
 
112
- 全局缓存提供者注册表。
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
+ };
113
152
 
114
- ```typescript
115
- CacheProviderRegistry.register('memory', new MemoryCacheProvider());
116
- CacheProviderRegistry.setDefault('memory');
117
- CacheProviderRegistry.get('memory');
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
+ }
118
165
  ```
119
166
 
120
- ### MemoryCacheProvider
167
+ ## 缓存日志
121
168
 
122
- 内存 Map 实现,适用于单机应用。
169
+ 缓存装饰器使用名称固定为 `@jintianxiayu/cache-decorator` 的 Logger。应用必须安装兼容版本的
170
+ `@jintianxiayu/logger`,并在第一次调用被装饰方法前完成 `LoggerFactory.init()`;应用退出时仍由应用统一调用
171
+ `LoggerFactory.shutdown()`。仅导入包、声明 decorator 或定义 class 不会提前获取 Logger,cache 包也不会自行初始化或关闭
172
+ Logger。
123
173
 
124
- ### RedisCacheProvider
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 handler;异步失败不会被描述为成功或完成。只有本来就会等待的全量淘汰可记录
194
+ `evict_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 错误。
125
200
 
126
- Redis 实现,适用于分布式场景。
201
+ ## Redis
202
+
203
+ `RedisCacheProvider` 从 0.2.0 起必须接收已经适配的 `RedisCacheClient`。库不会创建、连接、重连或关闭 Redis
204
+ 连接,也不会修改连接选项与错误监听器;这些生命周期操作始终由应用负责。
205
+
206
+ ### node-redis
207
+
208
+ ```typescript
209
+ import { createClient } from 'redis';
210
+ import { CacheProviderRegistry, RedisCacheProvider, createNodeRedisCacheClient } from '@jintianxiayu/cache-decorator';
211
+
212
+ const redis = createClient({ url: process.env.REDIS_URL });
213
+
214
+ export async function startCache(): Promise<void> {
215
+ redis.on('error', (error: Error) => console.error('Redis error', error));
216
+ await redis.connect();
217
+ const client = createNodeRedisCacheClient(redis, { keyPrefix: 'my-app:' });
218
+ CacheProviderRegistry.register('redis', new RedisCacheProvider(client));
219
+ CacheProviderRegistry.setDefault('redis');
220
+ }
221
+
222
+ export async function stopCache(): Promise<void> {
223
+ if (redis.isOpen) {
224
+ await redis.quit();
225
+ }
226
+ }
227
+ ```
228
+
229
+ ### ioredis
127
230
 
128
231
  ```typescript
129
232
  import Redis from 'ioredis';
130
- const redisProvider = new RedisCacheProvider(new Redis());
233
+ import { CacheProviderRegistry, RedisCacheProvider, createIoredisCacheClient } from '@jintianxiayu/cache-decorator';
234
+
235
+ const redis = new Redis(process.env.REDIS_URL ?? 'redis://127.0.0.1:6379', {
236
+ lazyConnect: true,
237
+ keyPrefix: 'my-app:',
238
+ });
239
+
240
+ export async function startCache(): Promise<void> {
241
+ redis.on('error', (error: Error) => console.error('Redis error', error));
242
+ await redis.connect();
243
+ CacheProviderRegistry.register('redis', new RedisCacheProvider(createIoredisCacheClient(redis)));
244
+ CacheProviderRegistry.setDefault('redis');
245
+ }
246
+
247
+ export function stopCache(): void {
248
+ redis.disconnect();
249
+ }
131
250
  ```
251
+
252
+ ### 自定义 RedisCacheClient
253
+
254
+ 其他 Redis SDK 或封装只需实现最小字符串命令接口,不必继承内置适配器:
255
+
256
+ ```typescript
257
+ import { CacheProviderRegistry, RedisCacheProvider, type RedisCacheClient } from '@jintianxiayu/cache-decorator';
258
+
259
+ interface StorageCommands {
260
+ read(key: string): Promise<string | null>;
261
+ write(key: string, value: string, ttlSeconds?: number): Promise<void>;
262
+ remove(keys: readonly string[]): Promise<void>;
263
+ scan(cursor: string, pattern: string, count: number): Promise<{ cursor: string; keys: readonly string[] }>;
264
+ clearDatabase(): Promise<void>;
265
+ }
266
+
267
+ declare const storage: StorageCommands;
268
+
269
+ const client: RedisCacheClient = {
270
+ get: (key) => storage.read(key),
271
+ set: ({ key, value, ttlSeconds }) => storage.write(key, value, ttlSeconds),
272
+ deleteMany: (keys) => storage.remove(keys),
273
+ scan: ({ cursor, pattern, count }) => storage.scan(cursor, pattern, count),
274
+ flushDatabase: () => storage.clearDatabase(),
275
+ };
276
+
277
+ CacheProviderRegistry.register('redis', new RedisCacheProvider(client));
278
+ ```
279
+
280
+ ### 前缀与数据兼容
281
+
282
+ - ioredis 继续使用连接的 `keyPrefix`;适配器会补齐 `SCAN` 不自动处理前缀的差异。
283
+ - node-redis 没有透明 `keyPrefix`,应在 `createNodeRedisCacheClient(client, { keyPrefix })` 中传入等价的字面前缀。
284
+ - 两种客户端连接同一 database、使用相同最终物理 key 时,可以互读既有缓存。字符串、JSON、miss 映射和秒级 TTL
285
+ 格式均与 0.1.3 保持一致,无需重写或清空数据。
286
+ - 原始字符串读取时仍会优先尝试 `JSON.parse`;例如缓存文本 `"true"` 会读为布尔值 `true`,这是既有兼容行为。
287
+
288
+ `deleteByPattern()` 使用 `SCAN COUNT 100` 分页删除。它不是原子快照;并发写入时不承诺一次删除全部匹配项,命令中途失败后可幂等重试。
289
+
290
+ > **危险:`RedisCacheProvider.clear()` 会执行 `FLUSHDB`,清空连接当前选择的整个 Redis database。**
291
+ > `keyPrefix` 不会缩小其范围;共享 database 时不要调用它。
292
+
293
+ ## 异常缓存迁移
294
+
295
+ 异常条目的外层仍是 `{ error }`,内容改为带 kind/version 的 JSON envelope。新版本会旁路所有旧版未版本化异常条目;当前装饰器未启用 `errorCache` 时也会旁路新版异常条目。旁路不会自动删除 key,后续业务成功可用正常 `{ value }` 覆盖,或由调用方使用现有精确淘汰能力清理。
296
+
297
+ 共享 Redis cache key 的系统必须采用两阶段升级:
298
+
299
+ 1. 先把所有读取方升级到包含安全旁路逻辑的新 major,并保持 `errorCache` 省略。
300
+ 2. 确认所有读取方升级完成、共享 key 的 codec 配置兼容后,再启用有限 TTL 的 `errorCache`。
301
+
302
+ 旧版本可以把新 envelope 解析为普通 JSON,但会按旧逻辑抛出未解码对象;因此混合版本期间不得写入新版异常条目。回滚到旧 major 前,应先停止异常 envelope 写入并精确清理受影响的异常 key,不要默认执行数据库级 `clear()`。
303
+
304
+ ### 异常与支持范围
305
+
306
+ - Redis 命令拒绝会保留原始错误;不受支持的返回结构会抛出 `TypeError`。
307
+ - TTL 必须是正有限整数秒,或使用 `0`/`undefined` 表示永不过期;其他值会在发送命令前抛出 `RangeError`。
308
+ - 0.2.x 首版验证 ioredis 5 和 node-redis 5 的普通单实例连接及默认字符串/数字响应。
309
+ - Cluster 跨分片扫描、Sentinel 专项切换、自定义 RESP Buffer 映射、自动故障切换和连接管理不在首版支持范围。
310
+
311
+ ### 从 0.1.3 迁移
312
+
313
+ 旧构造方式:
314
+
315
+ ```typescript
316
+ const provider = new RedisCacheProvider(redis);
317
+ ```
318
+
319
+ 新构造方式:
320
+
321
+ ```typescript
322
+ const provider = new RedisCacheProvider(createIoredisCacheClient(redis));
323
+ ```
324
+
325
+ 升级时保留原 database 和物理前缀,并由应用直接声明客户端依赖、先连接、最后自行关闭连接。需要回退时锁定
326
+ `@jintianxiayu/cache-decorator@0.1.3` 并恢复旧构造方式;数据格式未改变,不需要清理 Redis。
327
+
328
+ ## API
329
+
330
+ ### @Cache(cacheName, options?)
331
+
332
+ 缓存装饰器,必须用于异步方法。
333
+
334
+ - `cacheName`:缓存名称。
335
+ - `options.ttl`:过期时间,单位为秒。
336
+ - `options.providerName`:指定已注册的 `CacheProvider`。
337
+ - `options.key`:`undefined`/`null` 使用默认参数 key;字符串使用固定 key;函数根据方法参数返回 key。
338
+ - `options.errorCache`:可选异常策略;包含必填正整数秒级 `ttl`,以及可选同步 `shouldCache` 和成对 `codec`。
339
+
340
+ ### @CacheEvict(cacheName, options?)
341
+
342
+ 缓存清除装饰器,必须用于异步方法。
343
+
344
+ - `options.allEntries`:为 `true` 时删除该缓存名称下的匹配项,而不是执行数据库级 `clear()`。
345
+ - `options.providerName`:指定已注册的 `CacheProvider`。
346
+ - `options.key`:行为与 `@Cache` 一致;`allEntries: true` 时忽略。
347
+
348
+ ### CacheProviderRegistry
349
+
350
+ 全局 Provider 注册表,使用 `register(name, provider)` 注册,并通过 `setDefault(name)` 选择默认 Provider。
351
+
352
+ ### MemoryCacheProvider
353
+
354
+ 基于进程内 Map 的实现,适用于单进程缓存。
355
+
356
+ ### RedisCacheProvider
357
+
358
+ 维护字符串/JSON、miss、TTL 和扫描删除协议;必须传入 `RedisCacheClient`,可使用内置适配工厂或自定义实现。
@@ -0,0 +1,9 @@
1
+ import { IoredisCacheClientSource, RedisCacheClient } from '../core/redis-cache-client';
2
+ /**
3
+ * 将调用方的 ioredis 连接适配为客户端无关的缓存命令,不创建或关闭连接。
4
+ * @param client 使用默认字符串响应映射的 ioredis 普通客户端。
5
+ * @returns 复用该连接并恢复逻辑 key 的 Redis 缓存客户端。
6
+ * @throws 返回对象的命令方法传播客户端错误;非标准响应拒绝为 TypeError。
7
+ */
8
+ export declare function createIoredisCacheClient(client: IoredisCacheClientSource): RedisCacheClient;
9
+ //# sourceMappingURL=ioredis-cache-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ioredis-cache-client.d.ts","sourceRoot":"","sources":["../../src/adapters/ioredis-cache-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAyCxF;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,wBAAwB,GAAG,gBAAgB,CAyC3F"}
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createIoredisCacheClient = createIoredisCacheClient;
4
+ const redis_key_prefix_1 = require("./redis-key-prefix");
5
+ function normalizeGetResponse(response) {
6
+ if (typeof response === 'string' || response === null) {
7
+ return response;
8
+ }
9
+ throw new TypeError('Expected Redis GET to return a string or null');
10
+ }
11
+ function validateOkResponse(command, response) {
12
+ if (response !== 'OK') {
13
+ throw new TypeError(`Expected Redis ${command} to return "OK"`);
14
+ }
15
+ }
16
+ function validateDeleteResponse(response) {
17
+ if (typeof response !== 'number' || !Number.isInteger(response) || response < 0) {
18
+ throw new TypeError('Expected Redis DEL to return a non-negative integer');
19
+ }
20
+ }
21
+ function normalizeScanResponse(response, keyPrefix) {
22
+ if (!Array.isArray(response) ||
23
+ response.length !== 2 ||
24
+ typeof response[0] !== 'string' ||
25
+ !Array.isArray(response[1]) ||
26
+ !response[1].every((key) => typeof key === 'string')) {
27
+ throw new TypeError('Expected ioredis SCAN to return a [cursor, keys] tuple');
28
+ }
29
+ return {
30
+ cursor: response[0],
31
+ keys: response[1].map((key) => (0, redis_key_prefix_1.stripRedisKeyPrefix)(keyPrefix, key)),
32
+ };
33
+ }
34
+ /**
35
+ * 将调用方的 ioredis 连接适配为客户端无关的缓存命令,不创建或关闭连接。
36
+ * @param client 使用默认字符串响应映射的 ioredis 普通客户端。
37
+ * @returns 复用该连接并恢复逻辑 key 的 Redis 缓存客户端。
38
+ * @throws 返回对象的命令方法传播客户端错误;非标准响应拒绝为 TypeError。
39
+ */
40
+ function createIoredisCacheClient(client) {
41
+ const keyPrefix = client.options.keyPrefix ?? '';
42
+ return {
43
+ /** @inheritdoc */
44
+ async get(key) {
45
+ return normalizeGetResponse(await client.get(key));
46
+ },
47
+ /** @inheritdoc */
48
+ async set({ key, value, ttlSeconds }) {
49
+ if (ttlSeconds === undefined) {
50
+ validateOkResponse('SET', await client.set(key, value));
51
+ return;
52
+ }
53
+ validateOkResponse('SETEX', await client.setex(key, ttlSeconds, value));
54
+ },
55
+ /** @inheritdoc */
56
+ async deleteMany(keys) {
57
+ if (keys.length === 0) {
58
+ return;
59
+ }
60
+ validateDeleteResponse(await client.del(...keys));
61
+ },
62
+ /** @inheritdoc */
63
+ async scan({ cursor, pattern, count }) {
64
+ const response = await client.scan(cursor, 'MATCH', (0, redis_key_prefix_1.createRedisScanPattern)(keyPrefix, pattern), 'COUNT', count);
65
+ return normalizeScanResponse(response, keyPrefix);
66
+ },
67
+ /** @inheritdoc */
68
+ async flushDatabase() {
69
+ validateOkResponse('FLUSHDB', await client.flushdb());
70
+ },
71
+ };
72
+ }
73
+ //# sourceMappingURL=ioredis-cache-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ioredis-cache-client.js","sourceRoot":"","sources":["../../src/adapters/ioredis-cache-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { NodeRedisCacheClientOptions, NodeRedisCacheClientSource, RedisCacheClient } from '../core/redis-cache-client';
2
+ /**
3
+ * 将调用方的 node-redis 连接适配为客户端无关的缓存命令,不创建或关闭连接。
4
+ * @param client 使用默认字符串响应映射的 node-redis 普通客户端。
5
+ * @param options 可选的字面 key 前缀;缺省时不改写逻辑 key。
6
+ * @returns 复用该连接并统一物理 key 前缀的 Redis 缓存客户端。
7
+ * @throws 返回对象的命令方法传播客户端错误;非标准响应拒绝为 TypeError。
8
+ */
9
+ export declare function createNodeRedisCacheClient(client: NodeRedisCacheClientSource, options?: NodeRedisCacheClientOptions): RedisCacheClient;
10
+ //# sourceMappingURL=node-redis-cache-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-redis-cache-client.d.ts","sourceRoot":"","sources":["../../src/adapters/node-redis-cache-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AA4CvH;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACtC,MAAM,EAAE,0BAA0B,EAClC,OAAO,CAAC,EAAE,2BAA2B,GACtC,gBAAgB,CAwClB"}
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNodeRedisCacheClient = createNodeRedisCacheClient;
4
+ const redis_key_prefix_1 = require("./redis-key-prefix");
5
+ function normalizeGetResponse(response) {
6
+ if (typeof response === 'string' || response === null) {
7
+ return response;
8
+ }
9
+ throw new TypeError('Expected Redis GET to return a string or null');
10
+ }
11
+ function validateOkResponse(command, response) {
12
+ if (response !== 'OK') {
13
+ throw new TypeError(`Expected Redis ${command} to return "OK"`);
14
+ }
15
+ }
16
+ function validateDeleteResponse(response) {
17
+ if (typeof response !== 'number' || !Number.isInteger(response) || response < 0) {
18
+ throw new TypeError('Expected Redis DEL to return a non-negative integer');
19
+ }
20
+ }
21
+ function isRecord(value) {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
23
+ }
24
+ function normalizeScanResponse(response, keyPrefix) {
25
+ if (!isRecord(response) ||
26
+ typeof response.cursor !== 'string' ||
27
+ !Array.isArray(response.keys) ||
28
+ !response.keys.every((key) => typeof key === 'string')) {
29
+ throw new TypeError('Expected node-redis SCAN to return a { cursor, keys } object');
30
+ }
31
+ return {
32
+ cursor: response.cursor,
33
+ keys: response.keys.map((key) => (0, redis_key_prefix_1.stripRedisKeyPrefix)(keyPrefix, key)),
34
+ };
35
+ }
36
+ /**
37
+ * 将调用方的 node-redis 连接适配为客户端无关的缓存命令,不创建或关闭连接。
38
+ * @param client 使用默认字符串响应映射的 node-redis 普通客户端。
39
+ * @param options 可选的字面 key 前缀;缺省时不改写逻辑 key。
40
+ * @returns 复用该连接并统一物理 key 前缀的 Redis 缓存客户端。
41
+ * @throws 返回对象的命令方法传播客户端错误;非标准响应拒绝为 TypeError。
42
+ */
43
+ function createNodeRedisCacheClient(client, options) {
44
+ const keyPrefix = options?.keyPrefix ?? '';
45
+ const createPhysicalKey = (key) => `${keyPrefix}${key}`;
46
+ return {
47
+ /** @inheritdoc */
48
+ async get(key) {
49
+ return normalizeGetResponse(await client.get(createPhysicalKey(key)));
50
+ },
51
+ /** @inheritdoc */
52
+ async set({ key, value, ttlSeconds }) {
53
+ const physicalKey = createPhysicalKey(key);
54
+ if (ttlSeconds === undefined) {
55
+ validateOkResponse('SET', await client.set(physicalKey, value));
56
+ return;
57
+ }
58
+ validateOkResponse('SETEX', await client.setEx(physicalKey, ttlSeconds, value));
59
+ },
60
+ /** @inheritdoc */
61
+ async deleteMany(keys) {
62
+ if (keys.length === 0) {
63
+ return;
64
+ }
65
+ validateDeleteResponse(await client.del(keys.map(createPhysicalKey)));
66
+ },
67
+ /** @inheritdoc */
68
+ async scan({ cursor, pattern, count }) {
69
+ const response = await client.scan(cursor, {
70
+ MATCH: (0, redis_key_prefix_1.createRedisScanPattern)(keyPrefix, pattern),
71
+ COUNT: count,
72
+ });
73
+ return normalizeScanResponse(response, keyPrefix);
74
+ },
75
+ /** @inheritdoc */
76
+ async flushDatabase() {
77
+ validateOkResponse('FLUSHDB', await client.flushDb());
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=node-redis-cache-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-redis-cache-client.js","sourceRoot":"","sources":["../../src/adapters/node-redis-cache-client.ts"],"names":[],"mappings":"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"}