@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
package/dist/decorators/cache.js
CHANGED
|
@@ -3,29 +3,150 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CacheProviderRegistry = void 0;
|
|
4
4
|
exports.Cache = Cache;
|
|
5
5
|
require("reflect-metadata");
|
|
6
|
+
const cache_error_1 = require("../core/cache-error");
|
|
7
|
+
const cache_logger_1 = require("../core/cache-logger");
|
|
6
8
|
const cache_provider_registry_1 = require("../core/cache-provider-registry");
|
|
7
9
|
const key_builder_1 = require("../core/key-builder");
|
|
8
10
|
const pending_cache_1 = require("../core/pending-cache");
|
|
9
11
|
const pendingCache = new pending_cache_1.PendingCache();
|
|
12
|
+
/**
|
|
13
|
+
* 获取配置指向的缓存 Provider,并在解析失败时记录原始错误。
|
|
14
|
+
* @param providerName decorator 配置中的 Provider 名称。
|
|
15
|
+
* @param logContext 当前方法的稳定日志上下文。
|
|
16
|
+
* @returns 已注册的缓存 Provider。
|
|
17
|
+
* @throws Provider 注册表抛出的原始错误。
|
|
18
|
+
*/
|
|
19
|
+
function resolveCacheProvider(providerName, logContext) {
|
|
20
|
+
try {
|
|
21
|
+
return cache_provider_registry_1.CacheProviderRegistry.get(providerName);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...logContext, operation: 'provider_resolution', error });
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 保持 fire-and-forget 语义提交缓存写入,仅捕获调用当下可观察的同步失败。
|
|
30
|
+
* @param request 写入所需 Provider、entry 和无业务数据日志上下文。
|
|
31
|
+
* @returns 无返回值;异步写入 Promise 不会被等待或消费。
|
|
32
|
+
* @throws Provider set 同步抛出的原始错误。
|
|
33
|
+
*/
|
|
34
|
+
function dispatchCacheWrite(request) {
|
|
35
|
+
try {
|
|
36
|
+
request.provider.set(request.cacheKey, request.entry, request.ttl);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...request.logContext, operation: 'write', error });
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
(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) {
|
|
54
|
+
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...request.logContext, operation: 'write', error });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
(0, cache_logger_1.logCacheEvent)('cache.write_dispatched', { ...request.logContext, entryType: 'error' });
|
|
58
|
+
}
|
|
59
|
+
function isErrorCacheEntry(entry) {
|
|
60
|
+
return typeof entry === 'object' && entry !== null && 'error' in entry;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 按当前异常策略分类 Provider 条目,并只把可成功解码的当前版本异常视为命中。
|
|
64
|
+
* @param entry Provider 返回的缓存联合条目。
|
|
65
|
+
* @param errorPolicy 当前装饰器归一化后的异常策略。
|
|
66
|
+
* @param logContext 当前方法的稳定日志上下文。
|
|
67
|
+
* @returns value/error 命中结果,或需要继续业务流程的 miss。
|
|
68
|
+
*/
|
|
69
|
+
function resolveCachedEntry(entry, errorPolicy, logContext) {
|
|
70
|
+
if (!isErrorCacheEntry(entry)) {
|
|
71
|
+
(0, cache_logger_1.logCacheEvent)('cache.hit', { ...logContext, entryType: 'value' });
|
|
72
|
+
return { type: 'value', value: entry.value };
|
|
73
|
+
}
|
|
74
|
+
if (!(0, cache_error_1.isCurrentCacheErrorPayload)(entry.error)) {
|
|
75
|
+
(0, cache_logger_1.logCacheEvent)('cache.error_cache_skipped', { ...logContext, reason: 'legacy_entry' });
|
|
76
|
+
return { type: 'miss' };
|
|
77
|
+
}
|
|
78
|
+
if (errorPolicy === undefined) {
|
|
79
|
+
(0, cache_logger_1.logCacheEvent)('cache.error_cache_skipped', { ...logContext, reason: 'disabled_entry' });
|
|
80
|
+
return { type: 'miss' };
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const error = (0, cache_error_1.decodeCacheError)(entry.error, errorPolicy);
|
|
84
|
+
(0, cache_logger_1.logCacheEvent)('cache.hit', { ...logContext, entryType: 'error' });
|
|
85
|
+
return { type: 'error', error };
|
|
86
|
+
}
|
|
87
|
+
catch (_error) {
|
|
88
|
+
(0, cache_logger_1.logCacheEvent)('cache.error_cache_failed', { ...logContext, phase: 'decode' });
|
|
89
|
+
return { type: 'miss' };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 按固定顺序评估筛选器、编码异常并发起独立 TTL 写入,任何策略失败都保留原业务异常。
|
|
94
|
+
* @param request 本次业务异常、当前策略、Provider、完整 key 与稳定日志上下文。
|
|
95
|
+
* @returns 无返回值。
|
|
96
|
+
*/
|
|
97
|
+
function handleBusinessError(request) {
|
|
98
|
+
const { error, errorPolicy, provider, cacheKey, logContext } = request;
|
|
99
|
+
if (errorPolicy === undefined) {
|
|
100
|
+
(0, cache_logger_1.logCacheEvent)('cache.error_cache_skipped', { ...logContext, reason: 'disabled' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (errorPolicy.shouldCache !== undefined) {
|
|
104
|
+
let accepted;
|
|
105
|
+
try {
|
|
106
|
+
accepted = errorPolicy.shouldCache(error);
|
|
107
|
+
if (typeof accepted !== 'boolean') {
|
|
108
|
+
throw new TypeError('Cache error shouldCache must return a boolean');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (_error) {
|
|
112
|
+
(0, cache_logger_1.logCacheEvent)('cache.error_cache_failed', { ...logContext, phase: 'predicate' });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!accepted) {
|
|
116
|
+
(0, cache_logger_1.logCacheEvent)('cache.error_cache_skipped', { ...logContext, reason: 'predicate_rejected' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let entry;
|
|
121
|
+
try {
|
|
122
|
+
entry = { error: (0, cache_error_1.encodeCacheError)(error, errorPolicy) };
|
|
123
|
+
}
|
|
124
|
+
catch (_error) {
|
|
125
|
+
(0, cache_logger_1.logCacheEvent)('cache.error_cache_failed', { ...logContext, phase: 'encode' });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
dispatchCacheErrorWrite({ provider, cacheKey, entry, entryType: 'error', ttl: errorPolicy.ttl, logContext });
|
|
129
|
+
}
|
|
10
130
|
/**
|
|
11
131
|
* 解析缓存 key
|
|
12
|
-
* @param cacheName 缓存名称
|
|
13
132
|
* @param keyResolver key 解析器
|
|
14
133
|
* @param args 方法参数数组
|
|
134
|
+
* @param logContext 不包含业务参数和值的日志上下文
|
|
15
135
|
* @returns 解析后的缓存 key
|
|
16
136
|
*/
|
|
17
|
-
function resolveCacheKey(
|
|
137
|
+
function resolveCacheKey(keyResolver, args, logContext) {
|
|
18
138
|
if (keyResolver === undefined || keyResolver === null) {
|
|
19
|
-
return key_builder_1.KeyBuilder.build(cacheName, args);
|
|
139
|
+
return key_builder_1.KeyBuilder.build(logContext.cacheName, args);
|
|
20
140
|
}
|
|
21
141
|
if (typeof keyResolver === 'string') {
|
|
22
|
-
return key_builder_1.KeyBuilder.build(cacheName, [keyResolver]);
|
|
142
|
+
return key_builder_1.KeyBuilder.build(logContext.cacheName, [keyResolver]);
|
|
23
143
|
}
|
|
24
144
|
try {
|
|
25
|
-
return key_builder_1.KeyBuilder.build(cacheName, [keyResolver(...args)]);
|
|
145
|
+
return key_builder_1.KeyBuilder.build(logContext.cacheName, [keyResolver(...args)]);
|
|
26
146
|
}
|
|
27
|
-
catch {
|
|
28
|
-
|
|
147
|
+
catch (_error) {
|
|
148
|
+
(0, cache_logger_1.logCacheEvent)('cache.key_fallback', { ...logContext, reason: 'resolver_error' });
|
|
149
|
+
return key_builder_1.KeyBuilder.build(logContext.cacheName, args);
|
|
29
150
|
}
|
|
30
151
|
}
|
|
31
152
|
/**
|
|
@@ -35,34 +156,58 @@ function resolveCacheKey(cacheName, keyResolver, args) {
|
|
|
35
156
|
* @param options 配置项
|
|
36
157
|
*/
|
|
37
158
|
function Cache(cacheName, options) {
|
|
38
|
-
|
|
159
|
+
const errorPolicy = (0, cache_error_1.normalizeCacheErrorPolicy)(options?.errorCache);
|
|
160
|
+
return function (_target, propertyKey, descriptor) {
|
|
39
161
|
const originalMethod = descriptor.value;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if ('error' in entry) {
|
|
48
|
-
throw entry.error;
|
|
49
|
-
}
|
|
50
|
-
return entry.value;
|
|
51
|
-
}
|
|
162
|
+
const logContext = {
|
|
163
|
+
cacheName,
|
|
164
|
+
methodName: propertyKey,
|
|
165
|
+
providerName: (0, cache_logger_1.cacheProviderLabel)(options?.providerName),
|
|
166
|
+
};
|
|
167
|
+
descriptor.value = function (...args) {
|
|
168
|
+
const cacheKey = resolveCacheKey(options?.key, args, logContext);
|
|
52
169
|
const pending = pendingCache.get(cacheKey);
|
|
53
170
|
if (pending) {
|
|
171
|
+
(0, cache_logger_1.logCacheEvent)('cache.pending_hit', logContext);
|
|
54
172
|
return pending;
|
|
55
173
|
}
|
|
56
174
|
const promise = (async () => {
|
|
175
|
+
const provider = resolveCacheProvider(options?.providerName, logContext);
|
|
176
|
+
let cached;
|
|
177
|
+
try {
|
|
178
|
+
cached = await provider.get(cacheKey);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
(0, cache_logger_1.logCacheEvent)('cache.operation_failed', { ...logContext, operation: 'read', error });
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
if (cached !== undefined) {
|
|
185
|
+
const cachedResult = resolveCachedEntry(cached, errorPolicy, logContext);
|
|
186
|
+
if (cachedResult.type === 'value') {
|
|
187
|
+
return cachedResult.value;
|
|
188
|
+
}
|
|
189
|
+
if (cachedResult.type === 'error') {
|
|
190
|
+
throw cachedResult.error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
(0, cache_logger_1.logCacheEvent)('cache.miss', logContext);
|
|
194
|
+
let result;
|
|
57
195
|
try {
|
|
58
|
-
|
|
59
|
-
provider.set(cacheKey, { value: result }, options?.ttl);
|
|
60
|
-
return result;
|
|
196
|
+
result = (await originalMethod.apply(this, args));
|
|
61
197
|
}
|
|
62
198
|
catch (error) {
|
|
63
|
-
|
|
199
|
+
handleBusinessError({ error, errorPolicy, provider, cacheKey, logContext });
|
|
64
200
|
throw error;
|
|
65
201
|
}
|
|
202
|
+
dispatchCacheWrite({
|
|
203
|
+
provider,
|
|
204
|
+
cacheKey,
|
|
205
|
+
entry: { value: result },
|
|
206
|
+
entryType: 'value',
|
|
207
|
+
ttl: options?.ttl,
|
|
208
|
+
logContext,
|
|
209
|
+
});
|
|
210
|
+
return result;
|
|
66
211
|
})();
|
|
67
212
|
pendingCache.set(cacheKey, promise);
|
|
68
213
|
return promise;
|
|
@@ -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/dist/index.d.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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAChF,cAAc,uBAAuB,CAAC;AACtC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -14,11 +14,17 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.createNodeRedisCacheClient = exports.createIoredisCacheClient = void 0;
|
|
18
|
+
var ioredis_cache_client_1 = require("./adapters/ioredis-cache-client");
|
|
19
|
+
Object.defineProperty(exports, "createIoredisCacheClient", { enumerable: true, get: function () { return ioredis_cache_client_1.createIoredisCacheClient; } });
|
|
20
|
+
var node_redis_cache_client_1 = require("./adapters/node-redis-cache-client");
|
|
21
|
+
Object.defineProperty(exports, "createNodeRedisCacheClient", { enumerable: true, get: function () { return node_redis_cache_client_1.createNodeRedisCacheClient; } });
|
|
17
22
|
__exportStar(require("./core/cache-provider"), exports);
|
|
18
23
|
__exportStar(require("./core/cache-provider-registry"), exports);
|
|
19
24
|
__exportStar(require("./core/key-builder"), exports);
|
|
20
25
|
__exportStar(require("./core/native-cache"), exports);
|
|
21
26
|
__exportStar(require("./core/pending-cache"), exports);
|
|
27
|
+
__exportStar(require("./core/redis-cache-client"), exports);
|
|
22
28
|
__exportStar(require("./core/redis-cache"), exports);
|
|
23
29
|
__exportStar(require("./decorators/cache"), exports);
|
|
24
30
|
__exportStar(require("./decorators/cache-evict"), exports);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,wEAA2E;AAAlE,gIAAA,wBAAwB,OAAA;AACjC,8EAAgF;AAAvE,qIAAA,0BAA0B,OAAA;AACnC,wDAAsC;AACtC,iEAA+C;AAC/C,qDAAmC;AACnC,sDAAoC;AACpC,uDAAqC;AACrC,4DAA0C;AAC1C,qDAAmC;AACnC,qDAAmC;AACnC,2DAAyC"}
|
package/jest.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,30 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
"gitHead": "0518a4e3649799cc6c176dabfebeb86bd0891085"
|
|
30
|
-
}
|
|
2
|
+
"name": "@jintianxiayu/cache-decorator",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Method caching decorators for TypeScript",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"reflect-metadata": "^0.2.2"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"@jintianxiayu/logger": "^1.0.0"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"ioredis": "^5.10.1",
|
|
18
|
+
"redis": "^5.12.1",
|
|
19
|
+
"@jintianxiayu/logger": "^1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"test": "jest",
|
|
24
|
+
"clean": "rimraf dist",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { IoredisCacheClientSource, RedisCacheClient } from '../core/redis-cache-client';
|
|
2
|
+
import { createRedisScanPattern, stripRedisKeyPrefix } from './redis-key-prefix';
|
|
3
|
+
|
|
4
|
+
function normalizeGetResponse(response: unknown): string | null {
|
|
5
|
+
if (typeof response === 'string' || response === null) {
|
|
6
|
+
return response;
|
|
7
|
+
}
|
|
8
|
+
throw new TypeError('Expected Redis GET to return a string or null');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function validateOkResponse(command: string, response: unknown): void {
|
|
12
|
+
if (response !== 'OK') {
|
|
13
|
+
throw new TypeError(`Expected Redis ${command} to return "OK"`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validateDeleteResponse(response: unknown): void {
|
|
18
|
+
if (typeof response !== 'number' || !Number.isInteger(response) || response < 0) {
|
|
19
|
+
throw new TypeError('Expected Redis DEL to return a non-negative integer');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeScanResponse(
|
|
24
|
+
response: unknown,
|
|
25
|
+
keyPrefix: string
|
|
26
|
+
): { readonly cursor: string; readonly keys: readonly string[] } {
|
|
27
|
+
if (
|
|
28
|
+
!Array.isArray(response) ||
|
|
29
|
+
response.length !== 2 ||
|
|
30
|
+
typeof response[0] !== 'string' ||
|
|
31
|
+
!Array.isArray(response[1]) ||
|
|
32
|
+
!response[1].every((key: unknown) => typeof key === 'string')
|
|
33
|
+
) {
|
|
34
|
+
throw new TypeError('Expected ioredis SCAN to return a [cursor, keys] tuple');
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
cursor: response[0],
|
|
38
|
+
keys: response[1].map((key: string) => stripRedisKeyPrefix(keyPrefix, key)),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 将调用方的 ioredis 连接适配为客户端无关的缓存命令,不创建或关闭连接。
|
|
44
|
+
* @param client 使用默认字符串响应映射的 ioredis 普通客户端。
|
|
45
|
+
* @returns 复用该连接并恢复逻辑 key 的 Redis 缓存客户端。
|
|
46
|
+
* @throws 返回对象的命令方法传播客户端错误;非标准响应拒绝为 TypeError。
|
|
47
|
+
*/
|
|
48
|
+
export function createIoredisCacheClient(client: IoredisCacheClientSource): RedisCacheClient {
|
|
49
|
+
const keyPrefix = client.options.keyPrefix ?? '';
|
|
50
|
+
return {
|
|
51
|
+
/** @inheritdoc */
|
|
52
|
+
async get(key: string): Promise<string | null> {
|
|
53
|
+
return normalizeGetResponse(await client.get(key));
|
|
54
|
+
},
|
|
55
|
+
/** @inheritdoc */
|
|
56
|
+
async set({ key, value, ttlSeconds }): Promise<void> {
|
|
57
|
+
if (ttlSeconds === undefined) {
|
|
58
|
+
validateOkResponse('SET', await client.set(key, value));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
validateOkResponse('SETEX', await client.setex(key, ttlSeconds, value));
|
|
62
|
+
},
|
|
63
|
+
/** @inheritdoc */
|
|
64
|
+
async deleteMany(keys: readonly string[]): Promise<void> {
|
|
65
|
+
if (keys.length === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
validateDeleteResponse(await client.del(...keys));
|
|
69
|
+
},
|
|
70
|
+
/** @inheritdoc */
|
|
71
|
+
async scan({ cursor, pattern, count }): Promise<{
|
|
72
|
+
readonly cursor: string;
|
|
73
|
+
readonly keys: readonly string[];
|
|
74
|
+
}> {
|
|
75
|
+
const response = await client.scan(
|
|
76
|
+
cursor,
|
|
77
|
+
'MATCH',
|
|
78
|
+
createRedisScanPattern(keyPrefix, pattern),
|
|
79
|
+
'COUNT',
|
|
80
|
+
count
|
|
81
|
+
);
|
|
82
|
+
return normalizeScanResponse(response, keyPrefix);
|
|
83
|
+
},
|
|
84
|
+
/** @inheritdoc */
|
|
85
|
+
async flushDatabase(): Promise<void> {
|
|
86
|
+
validateOkResponse('FLUSHDB', await client.flushdb());
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { NodeRedisCacheClientOptions, NodeRedisCacheClientSource, RedisCacheClient } from '../core/redis-cache-client';
|
|
2
|
+
import { createRedisScanPattern, stripRedisKeyPrefix } from './redis-key-prefix';
|
|
3
|
+
|
|
4
|
+
function normalizeGetResponse(response: unknown): string | null {
|
|
5
|
+
if (typeof response === 'string' || response === null) {
|
|
6
|
+
return response;
|
|
7
|
+
}
|
|
8
|
+
throw new TypeError('Expected Redis GET to return a string or null');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function validateOkResponse(command: string, response: unknown): void {
|
|
12
|
+
if (response !== 'OK') {
|
|
13
|
+
throw new TypeError(`Expected Redis ${command} to return "OK"`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validateDeleteResponse(response: unknown): void {
|
|
18
|
+
if (typeof response !== 'number' || !Number.isInteger(response) || response < 0) {
|
|
19
|
+
throw new TypeError('Expected Redis DEL to return a non-negative integer');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeScanResponse(
|
|
28
|
+
response: unknown,
|
|
29
|
+
keyPrefix: string
|
|
30
|
+
): { readonly cursor: string; readonly keys: readonly string[] } {
|
|
31
|
+
if (
|
|
32
|
+
!isRecord(response) ||
|
|
33
|
+
typeof response.cursor !== 'string' ||
|
|
34
|
+
!Array.isArray(response.keys) ||
|
|
35
|
+
!response.keys.every((key: unknown) => typeof key === 'string')
|
|
36
|
+
) {
|
|
37
|
+
throw new TypeError('Expected node-redis SCAN to return a { cursor, keys } object');
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
cursor: response.cursor,
|
|
41
|
+
keys: response.keys.map((key: string) => stripRedisKeyPrefix(keyPrefix, key)),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 将调用方的 node-redis 连接适配为客户端无关的缓存命令,不创建或关闭连接。
|
|
47
|
+
* @param client 使用默认字符串响应映射的 node-redis 普通客户端。
|
|
48
|
+
* @param options 可选的字面 key 前缀;缺省时不改写逻辑 key。
|
|
49
|
+
* @returns 复用该连接并统一物理 key 前缀的 Redis 缓存客户端。
|
|
50
|
+
* @throws 返回对象的命令方法传播客户端错误;非标准响应拒绝为 TypeError。
|
|
51
|
+
*/
|
|
52
|
+
export function createNodeRedisCacheClient(
|
|
53
|
+
client: NodeRedisCacheClientSource,
|
|
54
|
+
options?: NodeRedisCacheClientOptions
|
|
55
|
+
): RedisCacheClient {
|
|
56
|
+
const keyPrefix = options?.keyPrefix ?? '';
|
|
57
|
+
const createPhysicalKey = (key: string): string => `${keyPrefix}${key}`;
|
|
58
|
+
return {
|
|
59
|
+
/** @inheritdoc */
|
|
60
|
+
async get(key: string): Promise<string | null> {
|
|
61
|
+
return normalizeGetResponse(await client.get(createPhysicalKey(key)));
|
|
62
|
+
},
|
|
63
|
+
/** @inheritdoc */
|
|
64
|
+
async set({ key, value, ttlSeconds }): Promise<void> {
|
|
65
|
+
const physicalKey = createPhysicalKey(key);
|
|
66
|
+
if (ttlSeconds === undefined) {
|
|
67
|
+
validateOkResponse('SET', await client.set(physicalKey, value));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
validateOkResponse('SETEX', await client.setEx(physicalKey, ttlSeconds, value));
|
|
71
|
+
},
|
|
72
|
+
/** @inheritdoc */
|
|
73
|
+
async deleteMany(keys: readonly string[]): Promise<void> {
|
|
74
|
+
if (keys.length === 0) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
validateDeleteResponse(await client.del(keys.map(createPhysicalKey)));
|
|
78
|
+
},
|
|
79
|
+
/** @inheritdoc */
|
|
80
|
+
async scan({ cursor, pattern, count }): Promise<{
|
|
81
|
+
readonly cursor: string;
|
|
82
|
+
readonly keys: readonly string[];
|
|
83
|
+
}> {
|
|
84
|
+
const response = await client.scan(cursor, {
|
|
85
|
+
MATCH: createRedisScanPattern(keyPrefix, pattern),
|
|
86
|
+
COUNT: count,
|
|
87
|
+
});
|
|
88
|
+
return normalizeScanResponse(response, keyPrefix);
|
|
89
|
+
},
|
|
90
|
+
/** @inheritdoc */
|
|
91
|
+
async flushDatabase(): Promise<void> {
|
|
92
|
+
validateOkResponse('FLUSHDB', await client.flushDb());
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const REDIS_GLOB_META_CHARACTERS = new Set(['\\', '*', '?', '[', ']']);
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 将普通字符串转换为 Redis glob pattern 中的字面量片段。
|
|
5
|
+
* @param value 仅作为字面前缀使用的字符串。
|
|
6
|
+
* @returns 对反斜杠、星号、问号和方括号增加反斜杠转义后的片段。
|
|
7
|
+
*/
|
|
8
|
+
export function escapeRedisGlobLiteral(value: string): string {
|
|
9
|
+
let escaped = '';
|
|
10
|
+
for (const character of value) {
|
|
11
|
+
escaped += REDIS_GLOB_META_CHARACTERS.has(character) ? `\\${character}` : character;
|
|
12
|
+
}
|
|
13
|
+
return escaped;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 为逻辑 glob pattern 增加按字面量解释的 Redis key 前缀。
|
|
18
|
+
* @param keyPrefix 调用方配置的物理 key 前缀。
|
|
19
|
+
* @param pattern Provider 传入且需要保留 glob 语义的逻辑 pattern。
|
|
20
|
+
* @returns 可直接交给 Redis SCAN MATCH 的物理 pattern。
|
|
21
|
+
*/
|
|
22
|
+
export function createRedisScanPattern(keyPrefix: string, pattern: string): string {
|
|
23
|
+
return `${escapeRedisGlobLiteral(keyPrefix)}${pattern}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 验证扫描结果属于指定物理命名空间并恢复逻辑 key。
|
|
28
|
+
* @param keyPrefix 调用方配置的物理 key 前缀。
|
|
29
|
+
* @param physicalKey Redis SCAN 返回的物理 key。
|
|
30
|
+
* @returns 只剥离一次字面前缀后的逻辑 key。
|
|
31
|
+
* @throws 扫描结果不属于指定前缀时抛出 TypeError,避免扩大后续删除范围。
|
|
32
|
+
*/
|
|
33
|
+
export function stripRedisKeyPrefix(keyPrefix: string, physicalKey: string): string {
|
|
34
|
+
if (!physicalKey.startsWith(keyPrefix)) {
|
|
35
|
+
throw new TypeError('Redis SCAN returned a key outside the configured key prefix');
|
|
36
|
+
}
|
|
37
|
+
return physicalKey.slice(keyPrefix.length);
|
|
38
|
+
}
|