@jintianxiayu/cache-decorator 1.0.1 → 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.
- package/CHANGELOG.md +8 -0
- package/README.md +11 -5
- package/dist/decorators/cache-evict.d.ts.map +1 -1
- package/dist/decorators/cache-evict.js +15 -10
- package/dist/decorators/cache-evict.js.map +1 -1
- package/dist/decorators/cache.d.ts.map +1 -1
- package/dist/decorators/cache.js +23 -29
- package/dist/decorators/cache.js.map +1 -1
- package/package.json +1 -1
- package/src/decorators/cache-evict.ts +19 -11
- package/src/decorators/cache.ts +34 -30
- package/test/cache-evict-logging.test.ts +47 -11
- package/test/cache-logger.integration.test.ts +30 -0
- package/test/cache-logging.test.ts +95 -18
- package/test/cache.test.ts +84 -6
- package/test/fixtures/cache-logger-child.mjs +108 -108
- package/test/fixtures/cache-provider-failure-child.mjs +70 -0
- package/test/redis-cache-decorator.test.ts +65 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @jintianxiayu/cache-decorator
|
|
2
2
|
|
|
3
|
+
## 1.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 缓存装饰器固定改为 fail-open:Provider 解析、读取、写入或淘汰失败时保留业务成功值或原始业务异常,并消费非等待写入与单 key 删除的异步 rejection。直接调用 Provider 仍保持 fail-fast。
|
|
8
|
+
|
|
9
|
+
这是维护者因公共 API 未调整而批准的 patch 版本例外,计划从 1.0.1 升级到 1.0.2;运行时错误语义不兼容,下游不得再依赖装饰器传播 Provider rejection。
|
|
10
|
+
|
|
3
11
|
## 1.0.1
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -118,8 +118,8 @@ class UserService {
|
|
|
118
118
|
- `errorCache` 省略时,业务异常不会持久化;相同 key 的执行中调用仍会复用同一个 pending Promise。
|
|
119
119
|
- `errorCache.ttl` 必须是大于零的有限整数秒,不能继承正常结果的 `ttl`。非法值会在 legacy decorator 求值时抛出 `RangeError`。
|
|
120
120
|
- `shouldCache` 省略时会接受所有业务异常;返回 `false` 或自身抛错时按 fail-closed 跳过写入,并继续抛出原业务异常。
|
|
121
|
-
- Provider
|
|
122
|
-
- 异常写入沿用 fire-and-forget
|
|
121
|
+
- Provider 解析或读取失败会旁路本次全部缓存操作,既不会作为业务异常缓存,也不会阻断业务方法。
|
|
122
|
+
- 异常写入沿用 fire-and-forget 边界;Provider 同步抛错或异步拒绝都不会替换已经发生的业务异常。
|
|
123
123
|
|
|
124
124
|
默认 codec 会把标准 `Error` 保存为只包含 `name` 和 `message` 的 JSON payload。缓存命中会创建新的 `Error`,不保留原对象身份、`stack`、自定义原型或任意自有属性。非 `Error` 抛出值必须能安全 JSON 往返;`undefined`、函数、symbol、循环引用和非有限数会跳过写入。
|
|
125
125
|
|
|
@@ -190,13 +190,19 @@ Logger。
|
|
|
190
190
|
| `error` | `cache.operation_failed` | Provider 解析或可观察的读取、写入、淘汰操作失败 |
|
|
191
191
|
|
|
192
192
|
`write_dispatched` 和 `evict_dispatched` 只描述调用已发起。为保持既有时序,decorator 不等待 `set()` 或单 key
|
|
193
|
-
`delete()` 返回的 Promise
|
|
194
|
-
`evict_completed
|
|
193
|
+
`delete()` 返回的 Promise,但会消费其 rejection 并记录 `cache.operation_failed`,避免形成未处理的 Promise rejection。只有本来就会
|
|
194
|
+
等待的全量淘汰可记录 `evict_completed`;全量淘汰失败不会记录 completed,也不会替换业务结果。
|
|
195
195
|
|
|
196
196
|
每条日志只包含 `event`、`cacheName`、`methodName`、`providerName`,并按事件增加 `entryType`、`scope`、
|
|
197
197
|
`reason`、`phase`、`operation` 或基础设施 `error`。日志不会包含方法参数、业务返回值、缓存值、业务异常内容、codec
|
|
198
198
|
payload、策略回调错误或完整逻辑/物理 cache key,也不会为日志额外序列化这些值。Provider 错误和现有
|
|
199
|
-
`LoggerContext` 的 `traceId` 继续由 Logger 统一规范化、脱敏和关联;cache 包不直接读写 LoggerContext。Logger
|
|
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 容量、淘汰策略或进程内存治理。
|
|
200
206
|
|
|
201
207
|
## Redis
|
|
202
208
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-evict.d.ts","sourceRoot":"","sources":["../../src/decorators/cache-evict.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,SAAS,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC;IAChE;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"cache-evict.d.ts","sourceRoot":"","sources":["../../src/decorators/cache-evict.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,SAAS,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC;IAChE;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAmED;;;;;GAKG;AACH,wBAAgB,UAAU,CACtB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,iBAAiB,GAC5B,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,KAAK,IAAI,CAuCjF;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC"}
|
|
@@ -28,11 +28,10 @@ function resolveCacheKey(keyResolver, args, logContext) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* 获取淘汰操作使用的 Provider
|
|
31
|
+
* 获取淘汰操作使用的 Provider,并把解析失败转换为缓存旁路。
|
|
32
32
|
* @param providerName decorator 配置中的 Provider 名称。
|
|
33
33
|
* @param logContext 当前方法的稳定日志上下文。
|
|
34
|
-
* @returns 已注册的缓存 Provider。
|
|
35
|
-
* @throws Provider 注册表抛出的原始错误。
|
|
34
|
+
* @returns 已注册的缓存 Provider;解析失败时返回 undefined。
|
|
36
35
|
*/
|
|
37
36
|
function resolveCacheProvider(providerName, logContext) {
|
|
38
37
|
try {
|
|
@@ -40,26 +39,29 @@ function resolveCacheProvider(providerName, logContext) {
|
|
|
40
39
|
}
|
|
41
40
|
catch (error) {
|
|
42
41
|
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...logContext, operation: 'provider_resolution', error });
|
|
43
|
-
|
|
42
|
+
return undefined;
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
/**
|
|
47
|
-
* 保持 fire-and-forget 语义发起单 key
|
|
46
|
+
* 保持 fire-and-forget 语义发起单 key 删除,并消费同步或异步 Provider 失败。
|
|
48
47
|
* @param provider 当前淘汰使用的 Provider。
|
|
49
48
|
* @param cacheKey 待删除的完整 key;不会进入日志元数据。
|
|
50
49
|
* @param logContext 当前方法的稳定日志上下文。
|
|
51
|
-
* @returns
|
|
52
|
-
* @throws Provider delete 同步抛出的原始错误。
|
|
50
|
+
* @returns 无返回值;删除失败仅记录日志,不影响业务结果。
|
|
53
51
|
*/
|
|
54
52
|
function dispatchCacheDelete(provider, cacheKey, logContext) {
|
|
53
|
+
let operation;
|
|
55
54
|
try {
|
|
56
|
-
provider.delete(cacheKey);
|
|
55
|
+
operation = provider.delete(cacheKey);
|
|
57
56
|
}
|
|
58
57
|
catch (error) {
|
|
59
58
|
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...logContext, operation: 'evict', error });
|
|
60
|
-
|
|
59
|
+
return;
|
|
61
60
|
}
|
|
62
61
|
(0, cache_logger_1.logCacheEvent)('cache.evict_dispatched', { ...logContext, scope: 'key' });
|
|
62
|
+
void Promise.resolve(operation).catch((error) => {
|
|
63
|
+
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...logContext, operation: 'evict', error });
|
|
64
|
+
});
|
|
63
65
|
}
|
|
64
66
|
/**
|
|
65
67
|
* 缓存清除装饰器
|
|
@@ -85,13 +87,16 @@ function CacheEvict(cacheName, options) {
|
|
|
85
87
|
throw error;
|
|
86
88
|
}
|
|
87
89
|
const provider = resolveCacheProvider(options?.providerName, logContext);
|
|
90
|
+
if (provider === undefined) {
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
88
93
|
if (options?.allEntries) {
|
|
89
94
|
try {
|
|
90
95
|
await provider.deleteByPattern(cacheName + '*');
|
|
91
96
|
}
|
|
92
97
|
catch (error) {
|
|
93
98
|
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...logContext, operation: 'evict', error });
|
|
94
|
-
|
|
99
|
+
return result;
|
|
95
100
|
}
|
|
96
101
|
(0, cache_logger_1.logCacheEvent)('cache.evict_completed', { ...logContext, scope: 'allEntries' });
|
|
97
102
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-evict.js","sourceRoot":"","sources":["../../src/decorators/cache-evict.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"cache-evict.js","sourceRoot":"","sources":["../../src/decorators/cache-evict.ts"],"names":[],"mappings":";;;AA4FA,gCA0CC;AAtID,uDAA+F;AAE/F,6EAAwE;AACxE,qDAAiD;AAkBjD;;;;;;GAMG;AACH,SAAS,eAAe,CACpB,WAAyC,EACzC,IAAe,EACf,UAA2B;IAE3B,IAAI,WAAW,KAAK,SAAS,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACpD,OAAO,wBAAU,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,wBAAU,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,CAAC;QACD,OAAO,wBAAU,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1E,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QACd,IAAA,4BAAa,EAAC,oBAAoB,EAAE,EAAE,GAAG,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;QACjF,OAAO,wBAAU,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACxD,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,SAAS,oBAAoB,CACzB,YAAgC,EAChC,UAA2B;IAE3B,IAAI,CAAC;QACD,OAAO,+CAAqB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAA,4BAAa,EAAC,wBAAwB,EAAE,EAAE,GAAG,UAAU,EAAE,SAAS,EAAE,qBAAqB,EAAE,KAAK,EAAE,CAAC,CAAC;QACpG,OAAO,SAAS,CAAC;IACrB,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,SAAS,mBAAmB,CAAC,QAAuB,EAAE,QAAgB,EAAE,UAA2B;IAC/F,IAAI,SAA+B,CAAC;IACpC,IAAI,CAAC;QACD,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAA,4BAAa,EAAC,wBAAwB,EAAE,EAAE,GAAG,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACtF,OAAO;IACX,CAAC;IACD,IAAA,4BAAa,EAAC,wBAAwB,EAAE,EAAE,GAAG,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACzE,KAAK,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACrD,IAAA,4BAAa,EAAC,wBAAwB,EAAE,EAAE,GAAG,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;GAKG;AACH,SAAgB,UAAU,CACtB,SAAiB,EACjB,OAA2B;IAE3B,OAAO,UAAU,OAAe,EAAE,WAAmB,EAAE,UAA8B;QACjF,MAAM,cAAc,GAAG,UAAU,CAAC,KAAK,CAAC;QACxC,MAAM,UAAU,GAAoB;YAChC,SAAS;YACT,UAAU,EAAE,WAAW;YACvB,YAAY,EAAE,IAAA,iCAAkB,EAAC,OAAO,EAAE,YAAY,CAAC;SAC1D,CAAC;QAEF,UAAU,CAAC,KAAK,GAAG,KAAK,WAAW,GAAG,IAAe;YACjD,IAAI,MAAe,CAAC;YACpB,IAAI,CAAC;gBACD,MAAM,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACpD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACb,IAAA,4BAAa,EAAC,qBAAqB,EAAE,EAAE,GAAG,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;gBAClF,MAAM,KAAK,CAAC;YAChB,CAAC;YAED,MAAM,QAAQ,GAAG,oBAAoB,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;YACzE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBACzB,OAAO,MAAM,CAAC;YAClB,CAAC;YAED,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC;gBACtB,IAAI,CAAC;oBACD,MAAM,QAAQ,CAAC,eAAe,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC;gBACpD,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACb,IAAA,4BAAa,EAAC,wBAAwB,EAAE,EAAE,GAAG,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;oBACtF,OAAO,MAAM,CAAC;gBAClB,CAAC;gBACD,IAAA,4BAAa,EAAC,uBAAuB,EAAE,EAAE,GAAG,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC;YACnF,CAAC;iBAAM,CAAC;gBACJ,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;gBACjE,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;YACxD,CAAC;YAED,OAAO,MAAM,CAAC;QAClB,CAAC,CAAC;IACN,CAAC,CAAC;AACN,CAAC;AAED,2EAAwE;AAA/D,gIAAA,qBAAqB,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/decorators/cache.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAKH,KAAK,eAAe,EACpB,KAAK,gBAAgB,EAExB,MAAM,qBAAqB,CAAC;AAO7B;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,MAAM,CAAC,CAAC;AAEhF,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,YAAY;IACzB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,gBAAgB,CAAC;IAEvB;;OAEG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/decorators/cache.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAKH,KAAK,eAAe,EACpB,KAAK,gBAAgB,EAExB,MAAM,qBAAqB,CAAC;AAO7B;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,MAAM,CAAC,CAAC;AAEhF,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,YAAY;IACzB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,gBAAgB,CAAC;IAEvB;;OAEG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AA6LD;;;;;GAKG;AACH,wBAAgB,KAAK,CACjB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,YAAY,GACvB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,KAAK,IAAI,CAsEjF;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC"}
|
package/dist/decorators/cache.js
CHANGED
|
@@ -10,11 +10,10 @@ const key_builder_1 = require("../core/key-builder");
|
|
|
10
10
|
const pending_cache_1 = require("../core/pending-cache");
|
|
11
11
|
const pendingCache = new pending_cache_1.PendingCache();
|
|
12
12
|
/**
|
|
13
|
-
* 获取配置指向的缓存 Provider
|
|
13
|
+
* 获取配置指向的缓存 Provider,并把解析失败转换为缓存旁路。
|
|
14
14
|
* @param providerName decorator 配置中的 Provider 名称。
|
|
15
15
|
* @param logContext 当前方法的稳定日志上下文。
|
|
16
|
-
* @returns 已注册的缓存 Provider。
|
|
17
|
-
* @throws Provider 注册表抛出的原始错误。
|
|
16
|
+
* @returns 已注册的缓存 Provider;解析失败时返回 undefined。
|
|
18
17
|
*/
|
|
19
18
|
function resolveCacheProvider(providerName, logContext) {
|
|
20
19
|
try {
|
|
@@ -22,39 +21,27 @@ function resolveCacheProvider(providerName, logContext) {
|
|
|
22
21
|
}
|
|
23
22
|
catch (error) {
|
|
24
23
|
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...logContext, operation: 'provider_resolution', error });
|
|
25
|
-
|
|
24
|
+
return undefined;
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
27
|
/**
|
|
29
|
-
* 保持 fire-and-forget
|
|
28
|
+
* 保持 fire-and-forget 语义提交缓存写入,并消费同步或异步 Provider 失败。
|
|
30
29
|
* @param request 写入所需 Provider、entry 和无业务数据日志上下文。
|
|
31
|
-
* @returns
|
|
32
|
-
* @throws Provider set 同步抛出的原始错误。
|
|
30
|
+
* @returns 无返回值;写入失败仅记录日志,不影响业务结果。
|
|
33
31
|
*/
|
|
34
32
|
function dispatchCacheWrite(request) {
|
|
33
|
+
let operation;
|
|
35
34
|
try {
|
|
36
|
-
request.provider.set(request.cacheKey, request.entry, request.ttl);
|
|
35
|
+
operation = request.provider.set(request.cacheKey, request.entry, request.ttl);
|
|
37
36
|
}
|
|
38
37
|
catch (error) {
|
|
39
38
|
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...request.logContext, operation: 'write', error });
|
|
40
|
-
|
|
39
|
+
return;
|
|
41
40
|
}
|
|
42
41
|
(0, cache_logger_1.logCacheEvent)('cache.write_dispatched', { ...request.logContext, entryType: request.entryType });
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 提交异常条目写入;同步 Provider 失败只记录基础设施错误,不遮蔽已经发生的业务异常。
|
|
46
|
-
* @param request 异常写入所需 Provider、entry、TTL 和稳定日志上下文。
|
|
47
|
-
* @returns 无返回值;异步写入 Promise 保持既有 fire-and-forget 边界。
|
|
48
|
-
*/
|
|
49
|
-
function dispatchCacheErrorWrite(request) {
|
|
50
|
-
try {
|
|
51
|
-
request.provider.set(request.cacheKey, request.entry, request.ttl);
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
42
|
+
void Promise.resolve(operation).catch((error) => {
|
|
54
43
|
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...request.logContext, operation: 'write', error });
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
(0, cache_logger_1.logCacheEvent)('cache.write_dispatched', { ...request.logContext, entryType: 'error' });
|
|
44
|
+
});
|
|
58
45
|
}
|
|
59
46
|
function isErrorCacheEntry(entry) {
|
|
60
47
|
return typeof entry === 'object' && entry !== null && 'error' in entry;
|
|
@@ -125,7 +112,7 @@ function handleBusinessError(request) {
|
|
|
125
112
|
(0, cache_logger_1.logCacheEvent)('cache.error_cache_failed', { ...logContext, phase: 'encode' });
|
|
126
113
|
return;
|
|
127
114
|
}
|
|
128
|
-
|
|
115
|
+
dispatchCacheWrite({ provider, cacheKey, entry, entryType: 'error', ttl: errorPolicy.ttl, logContext });
|
|
129
116
|
}
|
|
130
117
|
/**
|
|
131
118
|
* 解析缓存 key
|
|
@@ -173,16 +160,23 @@ function Cache(cacheName, options) {
|
|
|
173
160
|
}
|
|
174
161
|
const promise = (async () => {
|
|
175
162
|
const provider = resolveCacheProvider(options?.providerName, logContext);
|
|
176
|
-
|
|
163
|
+
if (provider === undefined) {
|
|
164
|
+
return (await originalMethod.apply(this, args));
|
|
165
|
+
}
|
|
166
|
+
let lookupResult;
|
|
177
167
|
try {
|
|
178
|
-
|
|
168
|
+
const entry = await provider.get(cacheKey);
|
|
169
|
+
lookupResult = entry === undefined ? { type: 'miss', provider } : { type: 'hit', provider, entry };
|
|
179
170
|
}
|
|
180
171
|
catch (error) {
|
|
181
172
|
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...logContext, operation: 'read', error });
|
|
182
|
-
|
|
173
|
+
lookupResult = { type: 'bypass' };
|
|
174
|
+
}
|
|
175
|
+
if (lookupResult.type === 'bypass') {
|
|
176
|
+
return (await originalMethod.apply(this, args));
|
|
183
177
|
}
|
|
184
|
-
if (
|
|
185
|
-
const cachedResult = resolveCachedEntry(
|
|
178
|
+
if (lookupResult.type === 'hit') {
|
|
179
|
+
const cachedResult = resolveCachedEntry(lookupResult.entry, errorPolicy, logContext);
|
|
186
180
|
if (cachedResult.type === 'value') {
|
|
187
181
|
return cachedResult.value;
|
|
188
182
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/decorators/cache.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/decorators/cache.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -46,37 +46,42 @@ function resolveCacheKey(
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* 获取淘汰操作使用的 Provider
|
|
49
|
+
* 获取淘汰操作使用的 Provider,并把解析失败转换为缓存旁路。
|
|
50
50
|
* @param providerName decorator 配置中的 Provider 名称。
|
|
51
51
|
* @param logContext 当前方法的稳定日志上下文。
|
|
52
|
-
* @returns 已注册的缓存 Provider。
|
|
53
|
-
* @throws Provider 注册表抛出的原始错误。
|
|
52
|
+
* @returns 已注册的缓存 Provider;解析失败时返回 undefined。
|
|
54
53
|
*/
|
|
55
|
-
function resolveCacheProvider(
|
|
54
|
+
function resolveCacheProvider(
|
|
55
|
+
providerName: string | undefined,
|
|
56
|
+
logContext: CacheLogContext
|
|
57
|
+
): CacheProvider | undefined {
|
|
56
58
|
try {
|
|
57
59
|
return CacheProviderRegistry.get(providerName);
|
|
58
60
|
} catch (error) {
|
|
59
61
|
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'provider_resolution', error });
|
|
60
|
-
|
|
62
|
+
return undefined;
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/**
|
|
65
|
-
* 保持 fire-and-forget 语义发起单 key
|
|
67
|
+
* 保持 fire-and-forget 语义发起单 key 删除,并消费同步或异步 Provider 失败。
|
|
66
68
|
* @param provider 当前淘汰使用的 Provider。
|
|
67
69
|
* @param cacheKey 待删除的完整 key;不会进入日志元数据。
|
|
68
70
|
* @param logContext 当前方法的稳定日志上下文。
|
|
69
|
-
* @returns
|
|
70
|
-
* @throws Provider delete 同步抛出的原始错误。
|
|
71
|
+
* @returns 无返回值;删除失败仅记录日志,不影响业务结果。
|
|
71
72
|
*/
|
|
72
73
|
function dispatchCacheDelete(provider: CacheProvider, cacheKey: string, logContext: CacheLogContext): void {
|
|
74
|
+
let operation: void | Promise<void>;
|
|
73
75
|
try {
|
|
74
|
-
provider.delete(cacheKey);
|
|
76
|
+
operation = provider.delete(cacheKey);
|
|
75
77
|
} catch (error) {
|
|
76
78
|
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'evict', error });
|
|
77
|
-
|
|
79
|
+
return;
|
|
78
80
|
}
|
|
79
81
|
logCacheEvent('cache.evict_dispatched', { ...logContext, scope: 'key' });
|
|
82
|
+
void Promise.resolve(operation).catch((error: unknown) => {
|
|
83
|
+
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'evict', error });
|
|
84
|
+
});
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
/**
|
|
@@ -107,13 +112,16 @@ export function CacheEvict(
|
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
const provider = resolveCacheProvider(options?.providerName, logContext);
|
|
115
|
+
if (provider === undefined) {
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
110
118
|
|
|
111
119
|
if (options?.allEntries) {
|
|
112
120
|
try {
|
|
113
121
|
await provider.deleteByPattern(cacheName + '*');
|
|
114
122
|
} catch (error) {
|
|
115
123
|
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'evict', error });
|
|
116
|
-
|
|
124
|
+
return result;
|
|
117
125
|
}
|
|
118
126
|
logCacheEvent('cache.evict_completed', { ...logContext, scope: 'allEntries' });
|
|
119
127
|
} else {
|
package/src/decorators/cache.ts
CHANGED
|
@@ -80,6 +80,11 @@ interface CacheWriteRequest<T> {
|
|
|
80
80
|
readonly logContext: CacheLogContext;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
type CacheLookupResult<T> =
|
|
84
|
+
| { readonly type: 'hit'; readonly provider: CacheProvider; readonly entry: CacheEntry<T> }
|
|
85
|
+
| { readonly type: 'miss'; readonly provider: CacheProvider }
|
|
86
|
+
| { readonly type: 'bypass' };
|
|
87
|
+
|
|
83
88
|
type CacheReadResult<T> =
|
|
84
89
|
| { readonly type: 'value'; readonly value: T }
|
|
85
90
|
| { readonly type: 'error'; readonly error: unknown }
|
|
@@ -96,50 +101,40 @@ interface BusinessErrorRequest {
|
|
|
96
101
|
const pendingCache = new PendingCache();
|
|
97
102
|
|
|
98
103
|
/**
|
|
99
|
-
* 获取配置指向的缓存 Provider
|
|
104
|
+
* 获取配置指向的缓存 Provider,并把解析失败转换为缓存旁路。
|
|
100
105
|
* @param providerName decorator 配置中的 Provider 名称。
|
|
101
106
|
* @param logContext 当前方法的稳定日志上下文。
|
|
102
|
-
* @returns 已注册的缓存 Provider。
|
|
103
|
-
* @throws Provider 注册表抛出的原始错误。
|
|
107
|
+
* @returns 已注册的缓存 Provider;解析失败时返回 undefined。
|
|
104
108
|
*/
|
|
105
|
-
function resolveCacheProvider(
|
|
109
|
+
function resolveCacheProvider(
|
|
110
|
+
providerName: string | undefined,
|
|
111
|
+
logContext: CacheLogContext
|
|
112
|
+
): CacheProvider | undefined {
|
|
106
113
|
try {
|
|
107
114
|
return CacheProviderRegistry.get(providerName);
|
|
108
115
|
} catch (error) {
|
|
109
116
|
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'provider_resolution', error });
|
|
110
|
-
|
|
117
|
+
return undefined;
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
/**
|
|
115
|
-
* 保持 fire-and-forget
|
|
122
|
+
* 保持 fire-and-forget 语义提交缓存写入,并消费同步或异步 Provider 失败。
|
|
116
123
|
* @param request 写入所需 Provider、entry 和无业务数据日志上下文。
|
|
117
|
-
* @returns
|
|
118
|
-
* @throws Provider set 同步抛出的原始错误。
|
|
124
|
+
* @returns 无返回值;写入失败仅记录日志,不影响业务结果。
|
|
119
125
|
*/
|
|
120
126
|
function dispatchCacheWrite<T>(request: CacheWriteRequest<T>): void {
|
|
127
|
+
let operation: void | Promise<void>;
|
|
121
128
|
try {
|
|
122
|
-
request.provider.set(request.cacheKey, request.entry, request.ttl);
|
|
129
|
+
operation = request.provider.set(request.cacheKey, request.entry, request.ttl);
|
|
123
130
|
} catch (error) {
|
|
124
131
|
logCacheEvent('cache.operation_failed', { ...request.logContext, operation: 'write', error });
|
|
125
|
-
|
|
132
|
+
return;
|
|
126
133
|
}
|
|
127
134
|
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) {
|
|
135
|
+
void Promise.resolve(operation).catch((error: unknown) => {
|
|
139
136
|
logCacheEvent('cache.operation_failed', { ...request.logContext, operation: 'write', error });
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
logCacheEvent('cache.write_dispatched', { ...request.logContext, entryType: 'error' });
|
|
137
|
+
});
|
|
143
138
|
}
|
|
144
139
|
|
|
145
140
|
function isErrorCacheEntry<T>(entry: CacheEntry<T>): entry is ErrorCacheEntry {
|
|
@@ -215,7 +210,7 @@ function handleBusinessError(request: BusinessErrorRequest): void {
|
|
|
215
210
|
logCacheEvent('cache.error_cache_failed', { ...logContext, phase: 'encode' });
|
|
216
211
|
return;
|
|
217
212
|
}
|
|
218
|
-
|
|
213
|
+
dispatchCacheWrite({ provider, cacheKey, entry, entryType: 'error', ttl: errorPolicy.ttl, logContext });
|
|
219
214
|
}
|
|
220
215
|
|
|
221
216
|
/**
|
|
@@ -274,15 +269,24 @@ export function Cache(
|
|
|
274
269
|
|
|
275
270
|
const promise = (async () => {
|
|
276
271
|
const provider = resolveCacheProvider(options?.providerName, logContext);
|
|
277
|
-
|
|
272
|
+
if (provider === undefined) {
|
|
273
|
+
return (await originalMethod.apply(this, args)) as T;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let lookupResult: CacheLookupResult<T>;
|
|
278
277
|
try {
|
|
279
|
-
|
|
278
|
+
const entry = await provider.get<CacheEntry<T>>(cacheKey);
|
|
279
|
+
lookupResult = entry === undefined ? { type: 'miss', provider } : { type: 'hit', provider, entry };
|
|
280
280
|
} catch (error) {
|
|
281
281
|
logCacheEvent('cache.operation_failed', { ...logContext, operation: 'read', error });
|
|
282
|
-
|
|
282
|
+
lookupResult = { type: 'bypass' };
|
|
283
283
|
}
|
|
284
|
-
if (
|
|
285
|
-
|
|
284
|
+
if (lookupResult.type === 'bypass') {
|
|
285
|
+
return (await originalMethod.apply(this, args)) as T;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (lookupResult.type === 'hit') {
|
|
289
|
+
const cachedResult = resolveCachedEntry(lookupResult.entry, errorPolicy, logContext);
|
|
286
290
|
if (cachedResult.type === 'value') {
|
|
287
291
|
return cachedResult.value;
|
|
288
292
|
}
|
|
@@ -244,7 +244,7 @@ it('cache-operation-logging/E04 重复淘汰每次只执行一次实际删除和
|
|
|
244
244
|
expect(events(mockLogger.debug)).toEqual(['cache.evict_dispatched', 'cache.evict_dispatched']);
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
-
it('cache-operation-logging/F03 allEntries
|
|
247
|
+
it('cache-operation-logging/F03 allEntries 删除失败记录同一错误且保留业务结果', async () => {
|
|
248
248
|
const evictionError = new Error('redis scan failed');
|
|
249
249
|
const provider = createProvider();
|
|
250
250
|
provider.deleteByPattern.mockRejectedValue(evictionError);
|
|
@@ -257,7 +257,7 @@ it('cache-operation-logging/F03 allEntries 删除失败记录同一错误且不
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
await expect(new UserService().clearUsers()).
|
|
260
|
+
await expect(new UserService().clearUsers()).resolves.toBe(true);
|
|
261
261
|
expect(provider.deleteByPattern).toHaveBeenCalledTimes(1);
|
|
262
262
|
expect(events(mockLogger.debug)).not.toContain('cache.evict_completed');
|
|
263
263
|
expect(loggedMetadata(mockLogger.error)).toEqual([
|
|
@@ -272,13 +272,39 @@ it('cache-operation-logging/F03 allEntries 删除失败记录同一错误且不
|
|
|
272
272
|
]);
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
-
it('cache-operation-logging/
|
|
275
|
+
it('cache-operation-logging/F03 allEntries 同步删除失败保留业务结果且不报告 completed', async () => {
|
|
276
|
+
const evictionError = new Error('synchronous scan failed');
|
|
277
|
+
const provider = createProvider();
|
|
278
|
+
provider.deleteByPattern.mockImplementation(() => {
|
|
279
|
+
throw evictionError;
|
|
280
|
+
});
|
|
281
|
+
CacheProviderRegistry.register('sync-failed-all-provider', provider.provider);
|
|
282
|
+
|
|
283
|
+
class UserService {
|
|
284
|
+
@CacheEvict('sync-failed-all-users', { providerName: 'sync-failed-all-provider', allEntries: true })
|
|
285
|
+
clearUsers(): string {
|
|
286
|
+
return 'business-result';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
await expect(new UserService().clearUsers()).resolves.toBe('business-result');
|
|
291
|
+
expect(events(mockLogger.debug)).not.toContain('cache.evict_completed');
|
|
292
|
+
expect(loggedMetadata(mockLogger.error)).toContainEqual({
|
|
293
|
+
event: 'cache.operation_failed',
|
|
294
|
+
cacheName: 'sync-failed-all-users',
|
|
295
|
+
methodName: 'clearUsers',
|
|
296
|
+
providerName: 'sync-failed-all-provider',
|
|
297
|
+
operation: 'evict',
|
|
298
|
+
error: evictionError,
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('cache-operation-logging/F05 异步单 key 删除拒绝被观察但不改变业务结果', async () => {
|
|
276
303
|
let rejectEviction: ((reason?: unknown) => void) | undefined;
|
|
277
304
|
const evictionError = new Error('async delete failed');
|
|
278
305
|
const evictionPromise = new Promise<void>((_resolve, reject) => {
|
|
279
306
|
rejectEviction = reject;
|
|
280
307
|
});
|
|
281
|
-
const observedEvictionFailure = evictionPromise.catch((error: unknown) => error);
|
|
282
308
|
const thenSpy = jest.spyOn(evictionPromise, 'then');
|
|
283
309
|
const provider = createProvider();
|
|
284
310
|
provider.delete.mockReturnValue(evictionPromise);
|
|
@@ -293,7 +319,7 @@ it('cache-operation-logging/F05 异步单 key 删除拒绝不会被等待或消
|
|
|
293
319
|
|
|
294
320
|
await expect(new UserService().deleteUser()).resolves.toBe(15);
|
|
295
321
|
expect(provider.delete).toHaveBeenCalledTimes(1);
|
|
296
|
-
expect(thenSpy).
|
|
322
|
+
expect(thenSpy).toHaveBeenCalledTimes(1);
|
|
297
323
|
expect(events(mockLogger.debug)).toEqual(['cache.evict_dispatched']);
|
|
298
324
|
expect(events(mockLogger.debug)).not.toContain('cache.evict_completed');
|
|
299
325
|
|
|
@@ -301,25 +327,35 @@ it('cache-operation-logging/F05 异步单 key 删除拒绝不会被等待或消
|
|
|
301
327
|
throw new Error('Eviction rejecter was not initialized');
|
|
302
328
|
}
|
|
303
329
|
rejectEviction(evictionError);
|
|
304
|
-
await
|
|
330
|
+
await Promise.resolve();
|
|
331
|
+
expect(loggedMetadata(mockLogger.error)).toContainEqual({
|
|
332
|
+
event: 'cache.operation_failed',
|
|
333
|
+
cacheName: 'async-delete-users',
|
|
334
|
+
methodName: 'deleteUser',
|
|
335
|
+
providerName: 'async-delete-provider',
|
|
336
|
+
operation: 'evict',
|
|
337
|
+
error: evictionError,
|
|
338
|
+
});
|
|
305
339
|
});
|
|
306
340
|
|
|
307
|
-
it('淘汰 Provider 解析失败记录 provider_resolution
|
|
341
|
+
it('淘汰 Provider 解析失败记录 provider_resolution 且保留业务结果', async () => {
|
|
308
342
|
const registryError = new Error('evict provider missing');
|
|
309
343
|
jest.spyOn(CacheProviderRegistry, 'get').mockImplementation(() => {
|
|
310
344
|
throw registryError;
|
|
311
345
|
});
|
|
312
346
|
const businessMethod = jest.fn(() => 'updated');
|
|
347
|
+
const resolver = jest.fn(() => 'unused-key');
|
|
313
348
|
|
|
314
349
|
class UserService {
|
|
315
|
-
@CacheEvict('missing-evict-users', { providerName: 'missing-evict-provider' })
|
|
350
|
+
@CacheEvict('missing-evict-users', { providerName: 'missing-evict-provider', key: resolver })
|
|
316
351
|
updateUser(): string {
|
|
317
352
|
return businessMethod();
|
|
318
353
|
}
|
|
319
354
|
}
|
|
320
355
|
|
|
321
|
-
await expect(new UserService().updateUser()).
|
|
356
|
+
await expect(new UserService().updateUser()).resolves.toBe('updated');
|
|
322
357
|
expect(businessMethod).toHaveBeenCalledTimes(1);
|
|
358
|
+
expect(resolver).not.toHaveBeenCalled();
|
|
323
359
|
expect(loggedMetadata(mockLogger.error)).toEqual([
|
|
324
360
|
{
|
|
325
361
|
event: 'cache.operation_failed',
|
|
@@ -333,7 +369,7 @@ it('淘汰 Provider 解析失败记录 provider_resolution 且不删除', async
|
|
|
333
369
|
expect(mockLogger.debug).not.toHaveBeenCalled();
|
|
334
370
|
});
|
|
335
371
|
|
|
336
|
-
it('同步单 key 删除失败记录 evict operation
|
|
372
|
+
it('同步单 key 删除失败记录 evict operation 且保留业务结果', async () => {
|
|
337
373
|
const evictionError = new Error('synchronous delete failed');
|
|
338
374
|
const provider = createProvider();
|
|
339
375
|
provider.delete.mockImplementation(() => {
|
|
@@ -348,7 +384,7 @@ it('同步单 key 删除失败记录 evict operation 且传播同一错误', asy
|
|
|
348
384
|
}
|
|
349
385
|
}
|
|
350
386
|
|
|
351
|
-
await expect(new UserService().deleteUser(16)).
|
|
387
|
+
await expect(new UserService().deleteUser(16)).resolves.toBe(16);
|
|
352
388
|
expect(provider.delete).toHaveBeenCalledTimes(1);
|
|
353
389
|
expect(events(mockLogger.debug)).not.toContain('cache.evict_dispatched');
|
|
354
390
|
expect(loggedMetadata(mockLogger.error)).toContainEqual({
|