@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.
- package/CHANGELOG.md +25 -11
- package/README.md +364 -270
- 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 +3 -2
- package/dist/core/cache-logger.d.ts.map +1 -1
- package/dist/core/cache-logger.js +2 -0
- package/dist/core/cache-logger.js.map +1 -1
- 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 +6 -0
- package/dist/decorators/cache.d.ts.map +1 -1
- package/dist/decorators/cache.js +113 -36
- package/dist/decorators/cache.js.map +1 -1
- package/jest.config.js +11 -11
- package/package.json +1 -1
- package/src/adapters/ioredis-cache-client.ts +89 -89
- package/src/adapters/node-redis-cache-client.ts +95 -95
- package/src/adapters/redis-key-prefix.ts +38 -38
- package/src/core/cache-error.ts +233 -0
- package/src/core/cache-logger.ts +116 -105
- package/src/core/key-builder.ts +33 -33
- package/src/core/native-cache.ts +55 -55
- package/src/core/pending-cache.ts +29 -29
- package/src/core/redis-cache-client.ts +160 -160
- package/src/core/redis-cache.ts +104 -104
- package/src/decorators/cache-evict.ts +137 -129
- package/src/decorators/cache.ts +323 -203
- package/src/index.ts +11 -11
- package/test/cache-evict-logging.test.ts +398 -362
- package/test/cache-logger.integration.test.ts +159 -129
- package/test/cache-logger.test.ts +156 -153
- package/test/cache-logging.test.ts +929 -544
- package/test/cache.test.ts +1017 -255
- package/test/fixtures/cache-logger-child.mjs +108 -108
- package/test/fixtures/cache-provider-failure-child.mjs +70 -0
- package/test/helpers/legacy-redis-cache.ts +41 -22
- package/test/helpers/package-consumer.ts +231 -231
- package/test/helpers/redis-fixture.ts +142 -142
- package/test/ioredis-cache-client.test.ts +143 -143
- package/test/legacy-redis-cache.test.ts +51 -0
- package/test/native-cache.test.ts +77 -77
- package/test/node-redis-cache-client.test.ts +149 -149
- package/test/pending-cache.test.ts +69 -69
- package/test/redis-cache-client-lifecycle.test.ts +112 -112
- package/test/redis-cache-client-types.test.ts +184 -184
- package/test/redis-cache-client.integration.test.ts +355 -327
- package/test/redis-cache-decorator.test.ts +601 -201
- package/test/redis-cache-provider.test.ts +269 -269
- package/test/type-contract/contract.ts +119 -64
- package/test/type-contract/tsconfig.json +12 -12
- package/tsconfig.json +8 -8
|
@@ -1,327 +1,355 @@
|
|
|
1
|
-
import { once } from 'node:events';
|
|
2
|
-
import Redis from 'ioredis';
|
|
3
|
-
import { createIoredisCacheClient } from '../src/adapters/ioredis-cache-client';
|
|
4
|
-
import { createNodeRedisCacheClient } from '../src/adapters/node-redis-cache-client';
|
|
5
|
-
import { RedisCacheProvider } from '../src/core/redis-cache';
|
|
6
|
-
import { writeLegacyRedisCache } from './helpers/legacy-redis-cache';
|
|
7
|
-
import { RedisFixture, createIsolatedDatabaseRedisFixture, createRedisFixture } from './helpers/redis-fixture';
|
|
8
|
-
|
|
9
|
-
const redisUrl = process.env.CACHE_DECORATOR_TEST_REDIS_URL;
|
|
10
|
-
const redisTests = redisUrl ? describe : describe.skip;
|
|
11
|
-
|
|
12
|
-
interface TestKeyNamespace {
|
|
13
|
-
readonly keyPrefix: string;
|
|
14
|
-
physicalKey(logicalKey: string): string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function createKeyNamespace(fixture: RedisFixture): TestKeyNamespace {
|
|
18
|
-
const keyPrefix = `${fixture.key()}:`;
|
|
19
|
-
return {
|
|
20
|
-
keyPrefix,
|
|
21
|
-
physicalKey(logicalKey: string): string {
|
|
22
|
-
return fixture.track(`${keyPrefix}${logicalKey}`);
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
redisTests('真实 Redis 夹具(需 CACHE_DECORATOR_TEST_REDIS_URL)', () => {
|
|
28
|
-
let fixture: RedisFixture | undefined;
|
|
29
|
-
|
|
30
|
-
beforeEach(async () => {
|
|
31
|
-
if (!redisUrl) {
|
|
32
|
-
throw new Error('CACHE_DECORATOR_TEST_REDIS_URL is required');
|
|
33
|
-
}
|
|
34
|
-
fixture = await createRedisFixture(redisUrl);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
afterEach(async () => fixture?.close());
|
|
38
|
-
|
|
39
|
-
it('双客户端连接及夹具清理只影响自有唯一 key', async () => {
|
|
40
|
-
if (!redisUrl || !fixture) {
|
|
41
|
-
throw new Error('CACHE_DECORATOR_TEST_REDIS_URL is required');
|
|
42
|
-
}
|
|
43
|
-
const observer = await createRedisFixture(redisUrl);
|
|
44
|
-
const ownKey = fixture.key();
|
|
45
|
-
const otherKey = observer.key();
|
|
46
|
-
try {
|
|
47
|
-
await expect(fixture.io.ping()).resolves.toBe('PONG');
|
|
48
|
-
await expect(fixture.node.ping()).resolves.toBe('PONG');
|
|
49
|
-
await fixture.io.set(ownKey, 'owned');
|
|
50
|
-
await observer.node.set(otherKey, 'untouched');
|
|
51
|
-
await fixture.close();
|
|
52
|
-
await expect(observer.node.get(ownKey)).resolves.toBeNull();
|
|
53
|
-
await expect(observer.node.get(otherKey)).resolves.toBe('untouched');
|
|
54
|
-
expect(fixture.errors).toEqual([]);
|
|
55
|
-
} finally {
|
|
56
|
-
await observer.close();
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
redisTests('Redis 客户端数据与互操作协议', () => {
|
|
62
|
-
let fixture: RedisFixture | undefined;
|
|
63
|
-
let ioProvider: RedisCacheProvider;
|
|
64
|
-
let nodeProvider: RedisCacheProvider;
|
|
65
|
-
|
|
66
|
-
beforeEach(async () => {
|
|
67
|
-
if (!redisUrl) {
|
|
68
|
-
throw new Error('CACHE_DECORATOR_TEST_REDIS_URL is required');
|
|
69
|
-
}
|
|
70
|
-
fixture = await createRedisFixture(redisUrl);
|
|
71
|
-
ioProvider = new RedisCacheProvider(createIoredisCacheClient(fixture.io));
|
|
72
|
-
nodeProvider = new RedisCacheProvider(createNodeRedisCacheClient(fixture.node));
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
afterEach(async () => fixture?.close());
|
|
76
|
-
|
|
77
|
-
it('redis-cache-client/V02 JSON 值跨客户端读取', async () => {
|
|
78
|
-
if (!fixture) {
|
|
79
|
-
throw new Error('Redis fixture is required');
|
|
80
|
-
}
|
|
81
|
-
const values: readonly unknown[] = [{ status: 'open' }, [1, 'two'], true, 42, null];
|
|
82
|
-
for (const value of values) {
|
|
83
|
-
const ioKey = fixture.key();
|
|
84
|
-
const nodeKey = fixture.key();
|
|
85
|
-
await ioProvider.set(ioKey, value);
|
|
86
|
-
await expect(nodeProvider.get(ioKey)).resolves.toEqual(value);
|
|
87
|
-
await nodeProvider.set(nodeKey, value);
|
|
88
|
-
await expect(ioProvider.get(nodeKey)).resolves.toEqual(value);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('redis-cache-client/V05 正整数 TTL 按秒传递', async () => {
|
|
93
|
-
if (!fixture) {
|
|
94
|
-
throw new Error('Redis fixture is required');
|
|
95
|
-
}
|
|
96
|
-
const ioKey = fixture.key();
|
|
97
|
-
const nodeKey = fixture.key();
|
|
98
|
-
await ioProvider.set(ioKey, 'io', 30);
|
|
99
|
-
await nodeProvider.set(nodeKey, 'node', 30);
|
|
100
|
-
|
|
101
|
-
const [ioTtl, nodeTtl] = await Promise.all([fixture.node.ttl(ioKey), fixture.io.ttl(nodeKey)]);
|
|
102
|
-
expect(ioTtl).toBeGreaterThan(20);
|
|
103
|
-
expect(ioTtl).toBeLessThanOrEqual(30);
|
|
104
|
-
expect(nodeTtl).toBeGreaterThan(20);
|
|
105
|
-
expect(nodeTtl).toBeLessThanOrEqual(30);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('redis-cache-client/V06 缺省 TTL 与零 TTL 永不过期', async () => {
|
|
109
|
-
if (!fixture) {
|
|
110
|
-
throw new Error('Redis fixture is required');
|
|
111
|
-
}
|
|
112
|
-
const ioKey = fixture.key();
|
|
113
|
-
const nodeKey = fixture.key();
|
|
114
|
-
await ioProvider.set(ioKey, 'io');
|
|
115
|
-
await nodeProvider.set(nodeKey, 'node', 0);
|
|
116
|
-
|
|
117
|
-
await expect(fixture.node.ttl(ioKey)).resolves.toBe(-1);
|
|
118
|
-
await expect(fixture.io.ttl(nodeKey)).resolves.toBe(-1);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('redis-cache-client/R03 并发写入不提供额外竞争控制', async () => {
|
|
122
|
-
if (!fixture) {
|
|
123
|
-
throw new Error('Redis fixture is required');
|
|
124
|
-
}
|
|
125
|
-
const key = fixture.key();
|
|
126
|
-
const ioSet = jest.spyOn(fixture.io, 'set');
|
|
127
|
-
const nodeSet = jest.spyOn(fixture.node, 'set');
|
|
128
|
-
|
|
129
|
-
await Promise.all([
|
|
130
|
-
ioProvider.set(key, { writer: 'ioredis' }),
|
|
131
|
-
nodeProvider.set(key, { writer: 'node-redis' }),
|
|
132
|
-
]);
|
|
133
|
-
|
|
134
|
-
expect([{ writer: 'ioredis' }, { writer: 'node-redis' }]).toContainEqual(await ioProvider.get(key));
|
|
135
|
-
expect(ioSet).toHaveBeenCalledTimes(1);
|
|
136
|
-
expect(nodeSet).toHaveBeenCalledTimes(1);
|
|
137
|
-
ioSet.mockRestore();
|
|
138
|
-
nodeSet.mockRestore();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('redis-cache-client/E05 真实连接不可用时不创建替代连接', async () => {
|
|
142
|
-
if (!redisUrl) {
|
|
143
|
-
throw new Error('CACHE_DECORATOR_TEST_REDIS_URL is required');
|
|
144
|
-
}
|
|
145
|
-
for (const kind of ['ioredis', 'node-redis'] as const) {
|
|
146
|
-
const unavailable = await createRedisFixture(redisUrl);
|
|
147
|
-
const provider = new RedisCacheProvider(
|
|
148
|
-
kind === 'ioredis'
|
|
149
|
-
? createIoredisCacheClient(unavailable.io)
|
|
150
|
-
: createNodeRedisCacheClient(unavailable.node)
|
|
151
|
-
);
|
|
152
|
-
const ioConnect = jest.spyOn(unavailable.io, 'connect');
|
|
153
|
-
const nodeConnect = jest.spyOn(unavailable.node, 'connect');
|
|
154
|
-
try {
|
|
155
|
-
if (kind === 'ioredis') {
|
|
156
|
-
const ended = once(unavailable.io, 'end');
|
|
157
|
-
unavailable.io.disconnect();
|
|
158
|
-
await ended;
|
|
159
|
-
} else {
|
|
160
|
-
unavailable.node.destroy();
|
|
161
|
-
}
|
|
162
|
-
const started = Date.now();
|
|
163
|
-
await expect(provider.get(unavailable.key())).rejects.toBeInstanceOf(Error);
|
|
164
|
-
expect(Date.now() - started).toBeLessThan(1500);
|
|
165
|
-
expect(ioConnect).not.toHaveBeenCalled();
|
|
166
|
-
expect(nodeConnect).not.toHaveBeenCalled();
|
|
167
|
-
} finally {
|
|
168
|
-
ioConnect.mockRestore();
|
|
169
|
-
nodeConnect.mockRestore();
|
|
170
|
-
await unavailable.close();
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('redis-cache-client/I01 ioredis 写入后由 node-redis 读取', async () => {
|
|
176
|
-
if (!fixture) {
|
|
177
|
-
throw new Error('Redis fixture is required');
|
|
178
|
-
}
|
|
179
|
-
const key = fixture.key();
|
|
180
|
-
await ioProvider.set(key, { source: 'ioredis' }, 30);
|
|
181
|
-
|
|
182
|
-
await expect(nodeProvider.get(key)).resolves.toEqual({ source: 'ioredis' });
|
|
183
|
-
expect(await fixture.node.ttl(key)).toBeGreaterThan(20);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('redis-cache-client/I02 node-redis 写入后由 ioredis 读取', async () => {
|
|
187
|
-
if (!fixture) {
|
|
188
|
-
throw new Error('Redis fixture is required');
|
|
189
|
-
}
|
|
190
|
-
const key = fixture.key();
|
|
191
|
-
await nodeProvider.set(key, { source: 'node-redis' }, 30);
|
|
192
|
-
|
|
193
|
-
await expect(ioProvider.get(key)).resolves.toEqual({ source: 'node-redis' });
|
|
194
|
-
expect(await fixture.io.ttl(key)).toBeGreaterThan(20);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('cache-operation-logging/A03 redis-cache-client/I03 新版本读取旧版本缓存', async () => {
|
|
198
|
-
if (!fixture) {
|
|
199
|
-
throw new Error('Redis fixture is required');
|
|
200
|
-
}
|
|
201
|
-
const stringKey = fixture.key();
|
|
202
|
-
const objectKey = fixture.key();
|
|
203
|
-
await writeLegacyRedisCache(fixture.io, stringKey, 'legacy-string');
|
|
204
|
-
await writeLegacyRedisCache(fixture.io, objectKey, { legacy: true }, 30);
|
|
205
|
-
|
|
206
|
-
await expect(nodeProvider.get(stringKey)).resolves.toBe('legacy-string');
|
|
207
|
-
await expect(ioProvider.get(objectKey)).resolves.toEqual({ legacy: true });
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('
|
|
211
|
-
if (!fixture) {
|
|
212
|
-
throw new Error('Redis fixture is required');
|
|
213
|
-
}
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
expect(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('cache-evict-allentries-prefix/
|
|
239
|
-
if (!fixture) {
|
|
240
|
-
throw new Error('Redis fixture is required');
|
|
241
|
-
}
|
|
242
|
-
const namespace = createKeyNamespace(fixture);
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('cache-evict-allentries-prefix/
|
|
267
|
-
if (!fixture) {
|
|
268
|
-
throw new Error('Redis fixture is required');
|
|
269
|
-
}
|
|
270
|
-
const namespace = createKeyNamespace(fixture);
|
|
271
|
-
const first = namespace.physicalKey('name:1');
|
|
272
|
-
const second = namespace.physicalKey('name:2');
|
|
273
|
-
const other = namespace.physicalKey('order:1');
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
await expect(
|
|
322
|
-
await expect(provider.
|
|
323
|
-
|
|
324
|
-
await expect(fixture.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
1
|
+
import { once } from 'node:events';
|
|
2
|
+
import Redis from 'ioredis';
|
|
3
|
+
import { createIoredisCacheClient } from '../src/adapters/ioredis-cache-client';
|
|
4
|
+
import { createNodeRedisCacheClient } from '../src/adapters/node-redis-cache-client';
|
|
5
|
+
import { RedisCacheProvider } from '../src/core/redis-cache';
|
|
6
|
+
import { readLegacyRedisCache, writeLegacyRedisCache } from './helpers/legacy-redis-cache';
|
|
7
|
+
import { RedisFixture, createIsolatedDatabaseRedisFixture, createRedisFixture } from './helpers/redis-fixture';
|
|
8
|
+
|
|
9
|
+
const redisUrl = process.env.CACHE_DECORATOR_TEST_REDIS_URL;
|
|
10
|
+
const redisTests = redisUrl ? describe : describe.skip;
|
|
11
|
+
|
|
12
|
+
interface TestKeyNamespace {
|
|
13
|
+
readonly keyPrefix: string;
|
|
14
|
+
physicalKey(logicalKey: string): string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createKeyNamespace(fixture: RedisFixture): TestKeyNamespace {
|
|
18
|
+
const keyPrefix = `${fixture.key()}:`;
|
|
19
|
+
return {
|
|
20
|
+
keyPrefix,
|
|
21
|
+
physicalKey(logicalKey: string): string {
|
|
22
|
+
return fixture.track(`${keyPrefix}${logicalKey}`);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
redisTests('真实 Redis 夹具(需 CACHE_DECORATOR_TEST_REDIS_URL)', () => {
|
|
28
|
+
let fixture: RedisFixture | undefined;
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
if (!redisUrl) {
|
|
32
|
+
throw new Error('CACHE_DECORATOR_TEST_REDIS_URL is required');
|
|
33
|
+
}
|
|
34
|
+
fixture = await createRedisFixture(redisUrl);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(async () => fixture?.close());
|
|
38
|
+
|
|
39
|
+
it('双客户端连接及夹具清理只影响自有唯一 key', async () => {
|
|
40
|
+
if (!redisUrl || !fixture) {
|
|
41
|
+
throw new Error('CACHE_DECORATOR_TEST_REDIS_URL is required');
|
|
42
|
+
}
|
|
43
|
+
const observer = await createRedisFixture(redisUrl);
|
|
44
|
+
const ownKey = fixture.key();
|
|
45
|
+
const otherKey = observer.key();
|
|
46
|
+
try {
|
|
47
|
+
await expect(fixture.io.ping()).resolves.toBe('PONG');
|
|
48
|
+
await expect(fixture.node.ping()).resolves.toBe('PONG');
|
|
49
|
+
await fixture.io.set(ownKey, 'owned');
|
|
50
|
+
await observer.node.set(otherKey, 'untouched');
|
|
51
|
+
await fixture.close();
|
|
52
|
+
await expect(observer.node.get(ownKey)).resolves.toBeNull();
|
|
53
|
+
await expect(observer.node.get(otherKey)).resolves.toBe('untouched');
|
|
54
|
+
expect(fixture.errors).toEqual([]);
|
|
55
|
+
} finally {
|
|
56
|
+
await observer.close();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
redisTests('Redis 客户端数据与互操作协议', () => {
|
|
62
|
+
let fixture: RedisFixture | undefined;
|
|
63
|
+
let ioProvider: RedisCacheProvider;
|
|
64
|
+
let nodeProvider: RedisCacheProvider;
|
|
65
|
+
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
if (!redisUrl) {
|
|
68
|
+
throw new Error('CACHE_DECORATOR_TEST_REDIS_URL is required');
|
|
69
|
+
}
|
|
70
|
+
fixture = await createRedisFixture(redisUrl);
|
|
71
|
+
ioProvider = new RedisCacheProvider(createIoredisCacheClient(fixture.io));
|
|
72
|
+
nodeProvider = new RedisCacheProvider(createNodeRedisCacheClient(fixture.node));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(async () => fixture?.close());
|
|
76
|
+
|
|
77
|
+
it('redis-cache-client/V02 JSON 值跨客户端读取', async () => {
|
|
78
|
+
if (!fixture) {
|
|
79
|
+
throw new Error('Redis fixture is required');
|
|
80
|
+
}
|
|
81
|
+
const values: readonly unknown[] = [{ status: 'open' }, [1, 'two'], true, 42, null];
|
|
82
|
+
for (const value of values) {
|
|
83
|
+
const ioKey = fixture.key();
|
|
84
|
+
const nodeKey = fixture.key();
|
|
85
|
+
await ioProvider.set(ioKey, value);
|
|
86
|
+
await expect(nodeProvider.get(ioKey)).resolves.toEqual(value);
|
|
87
|
+
await nodeProvider.set(nodeKey, value);
|
|
88
|
+
await expect(ioProvider.get(nodeKey)).resolves.toEqual(value);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('redis-cache-client/V05 正整数 TTL 按秒传递', async () => {
|
|
93
|
+
if (!fixture) {
|
|
94
|
+
throw new Error('Redis fixture is required');
|
|
95
|
+
}
|
|
96
|
+
const ioKey = fixture.key();
|
|
97
|
+
const nodeKey = fixture.key();
|
|
98
|
+
await ioProvider.set(ioKey, 'io', 30);
|
|
99
|
+
await nodeProvider.set(nodeKey, 'node', 30);
|
|
100
|
+
|
|
101
|
+
const [ioTtl, nodeTtl] = await Promise.all([fixture.node.ttl(ioKey), fixture.io.ttl(nodeKey)]);
|
|
102
|
+
expect(ioTtl).toBeGreaterThan(20);
|
|
103
|
+
expect(ioTtl).toBeLessThanOrEqual(30);
|
|
104
|
+
expect(nodeTtl).toBeGreaterThan(20);
|
|
105
|
+
expect(nodeTtl).toBeLessThanOrEqual(30);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('redis-cache-client/V06 缺省 TTL 与零 TTL 永不过期', async () => {
|
|
109
|
+
if (!fixture) {
|
|
110
|
+
throw new Error('Redis fixture is required');
|
|
111
|
+
}
|
|
112
|
+
const ioKey = fixture.key();
|
|
113
|
+
const nodeKey = fixture.key();
|
|
114
|
+
await ioProvider.set(ioKey, 'io');
|
|
115
|
+
await nodeProvider.set(nodeKey, 'node', 0);
|
|
116
|
+
|
|
117
|
+
await expect(fixture.node.ttl(ioKey)).resolves.toBe(-1);
|
|
118
|
+
await expect(fixture.io.ttl(nodeKey)).resolves.toBe(-1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('redis-cache-client/R03 并发写入不提供额外竞争控制', async () => {
|
|
122
|
+
if (!fixture) {
|
|
123
|
+
throw new Error('Redis fixture is required');
|
|
124
|
+
}
|
|
125
|
+
const key = fixture.key();
|
|
126
|
+
const ioSet = jest.spyOn(fixture.io, 'set');
|
|
127
|
+
const nodeSet = jest.spyOn(fixture.node, 'set');
|
|
128
|
+
|
|
129
|
+
await Promise.all([
|
|
130
|
+
ioProvider.set(key, { writer: 'ioredis' }),
|
|
131
|
+
nodeProvider.set(key, { writer: 'node-redis' }),
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
expect([{ writer: 'ioredis' }, { writer: 'node-redis' }]).toContainEqual(await ioProvider.get(key));
|
|
135
|
+
expect(ioSet).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(nodeSet).toHaveBeenCalledTimes(1);
|
|
137
|
+
ioSet.mockRestore();
|
|
138
|
+
nodeSet.mockRestore();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('redis-cache-client/E05 真实连接不可用时不创建替代连接', async () => {
|
|
142
|
+
if (!redisUrl) {
|
|
143
|
+
throw new Error('CACHE_DECORATOR_TEST_REDIS_URL is required');
|
|
144
|
+
}
|
|
145
|
+
for (const kind of ['ioredis', 'node-redis'] as const) {
|
|
146
|
+
const unavailable = await createRedisFixture(redisUrl);
|
|
147
|
+
const provider = new RedisCacheProvider(
|
|
148
|
+
kind === 'ioredis'
|
|
149
|
+
? createIoredisCacheClient(unavailable.io)
|
|
150
|
+
: createNodeRedisCacheClient(unavailable.node)
|
|
151
|
+
);
|
|
152
|
+
const ioConnect = jest.spyOn(unavailable.io, 'connect');
|
|
153
|
+
const nodeConnect = jest.spyOn(unavailable.node, 'connect');
|
|
154
|
+
try {
|
|
155
|
+
if (kind === 'ioredis') {
|
|
156
|
+
const ended = once(unavailable.io, 'end');
|
|
157
|
+
unavailable.io.disconnect();
|
|
158
|
+
await ended;
|
|
159
|
+
} else {
|
|
160
|
+
unavailable.node.destroy();
|
|
161
|
+
}
|
|
162
|
+
const started = Date.now();
|
|
163
|
+
await expect(provider.get(unavailable.key())).rejects.toBeInstanceOf(Error);
|
|
164
|
+
expect(Date.now() - started).toBeLessThan(1500);
|
|
165
|
+
expect(ioConnect).not.toHaveBeenCalled();
|
|
166
|
+
expect(nodeConnect).not.toHaveBeenCalled();
|
|
167
|
+
} finally {
|
|
168
|
+
ioConnect.mockRestore();
|
|
169
|
+
nodeConnect.mockRestore();
|
|
170
|
+
await unavailable.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('redis-cache-client/I01 ioredis 写入后由 node-redis 读取', async () => {
|
|
176
|
+
if (!fixture) {
|
|
177
|
+
throw new Error('Redis fixture is required');
|
|
178
|
+
}
|
|
179
|
+
const key = fixture.key();
|
|
180
|
+
await ioProvider.set(key, { source: 'ioredis' }, 30);
|
|
181
|
+
|
|
182
|
+
await expect(nodeProvider.get(key)).resolves.toEqual({ source: 'ioredis' });
|
|
183
|
+
expect(await fixture.node.ttl(key)).toBeGreaterThan(20);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('redis-cache-client/I02 node-redis 写入后由 ioredis 读取', async () => {
|
|
187
|
+
if (!fixture) {
|
|
188
|
+
throw new Error('Redis fixture is required');
|
|
189
|
+
}
|
|
190
|
+
const key = fixture.key();
|
|
191
|
+
await nodeProvider.set(key, { source: 'node-redis' }, 30);
|
|
192
|
+
|
|
193
|
+
await expect(ioProvider.get(key)).resolves.toEqual({ source: 'node-redis' });
|
|
194
|
+
expect(await fixture.io.ttl(key)).toBeGreaterThan(20);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('cache-operation-logging/A03 redis-cache-client/I03 新版本读取旧版本缓存', async () => {
|
|
198
|
+
if (!fixture) {
|
|
199
|
+
throw new Error('Redis fixture is required');
|
|
200
|
+
}
|
|
201
|
+
const stringKey = fixture.key();
|
|
202
|
+
const objectKey = fixture.key();
|
|
203
|
+
await writeLegacyRedisCache(fixture.io, stringKey, 'legacy-string');
|
|
204
|
+
await writeLegacyRedisCache(fixture.io, objectKey, { legacy: true }, 30);
|
|
205
|
+
|
|
206
|
+
await expect(nodeProvider.get(stringKey)).resolves.toBe('legacy-string');
|
|
207
|
+
await expect(ioProvider.get(objectKey)).resolves.toEqual({ legacy: true });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('旧读取方可解析新异常 envelope,但按旧逻辑抛出未解码对象', async () => {
|
|
211
|
+
if (!fixture) {
|
|
212
|
+
throw new Error('Redis fixture is required');
|
|
213
|
+
}
|
|
214
|
+
const key = fixture.key();
|
|
215
|
+
const envelope = {
|
|
216
|
+
kind: '@jintianxiayu/cache-decorator/error',
|
|
217
|
+
version: 1,
|
|
218
|
+
payload: { type: 'error', name: 'Error', message: 'not found' },
|
|
219
|
+
};
|
|
220
|
+
await nodeProvider.set(key, { error: envelope }, 30);
|
|
221
|
+
|
|
222
|
+
const legacyEntry = await readLegacyRedisCache(fixture.io, key);
|
|
223
|
+
expect(legacyEntry).toEqual({ error: envelope });
|
|
224
|
+
let legacyThrown: unknown;
|
|
225
|
+
try {
|
|
226
|
+
if (typeof legacyEntry === 'object' && legacyEntry !== null && 'error' in legacyEntry) {
|
|
227
|
+
throw legacyEntry.error;
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
legacyThrown = error;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
expect(legacyThrown).toEqual(envelope);
|
|
234
|
+
expect(legacyThrown).not.toBeInstanceOf(Error);
|
|
235
|
+
expect(await fixture.io.ttl(key)).toBeGreaterThan(20);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('cache-evict-allentries-prefix/RedisCacheProvider 使用 SCAN 迭代删除', async () => {
|
|
239
|
+
if (!fixture) {
|
|
240
|
+
throw new Error('Redis fixture is required');
|
|
241
|
+
}
|
|
242
|
+
const namespace = createKeyNamespace(fixture);
|
|
243
|
+
const provider = new RedisCacheProvider(
|
|
244
|
+
createNodeRedisCacheClient(fixture.node, { keyPrefix: namespace.keyPrefix })
|
|
245
|
+
);
|
|
246
|
+
const matchingKeys = Array.from({ length: 150 }, (_, index) => `name:${index}`);
|
|
247
|
+
const physicalMatchingKeys = matchingKeys.map((key) => namespace.physicalKey(key));
|
|
248
|
+
const otherKey = namespace.physicalKey('order:1');
|
|
249
|
+
await Promise.all([
|
|
250
|
+
...physicalMatchingKeys.map((key) => fixture.node.set(key, 'value')),
|
|
251
|
+
fixture.node.set(otherKey, 'other'),
|
|
252
|
+
]);
|
|
253
|
+
const scan = jest.spyOn(fixture.node, 'scan');
|
|
254
|
+
|
|
255
|
+
await provider.deleteByPattern('name*');
|
|
256
|
+
|
|
257
|
+
expect(scan).toHaveBeenCalled();
|
|
258
|
+
for (const call of scan.mock.calls) {
|
|
259
|
+
expect(call[1]).toEqual({ MATCH: `${namespace.keyPrefix}name*`, COUNT: 100 });
|
|
260
|
+
}
|
|
261
|
+
expect((await fixture.node.mGet(physicalMatchingKeys)).every((value) => value === null)).toBe(true);
|
|
262
|
+
await expect(fixture.node.get(otherKey)).resolves.toBe('other');
|
|
263
|
+
scan.mockRestore();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('cache-evict-allentries-prefix/F05 ioredis keyPrefix 下删除逻辑 pattern', async () => {
|
|
267
|
+
if (!fixture) {
|
|
268
|
+
throw new Error('Redis fixture is required');
|
|
269
|
+
}
|
|
270
|
+
const namespace = createKeyNamespace(fixture);
|
|
271
|
+
const first = namespace.physicalKey('name:1');
|
|
272
|
+
const second = namespace.physicalKey('name:2');
|
|
273
|
+
const other = namespace.physicalKey('order:1');
|
|
274
|
+
const prefixed = new Redis({ ...fixture.io.options, keyPrefix: namespace.keyPrefix, lazyConnect: true });
|
|
275
|
+
prefixed.on('error', (error: Error) => fixture.errors.push(error));
|
|
276
|
+
try {
|
|
277
|
+
await prefixed.connect();
|
|
278
|
+
await fixture.node.mSet({ [first]: 'one', [second]: 'two', [other]: 'other' });
|
|
279
|
+
const provider = new RedisCacheProvider(createIoredisCacheClient(prefixed));
|
|
280
|
+
|
|
281
|
+
await provider.deleteByPattern('name*');
|
|
282
|
+
|
|
283
|
+
await expect(fixture.node.mGet([first, second])).resolves.toEqual([null, null]);
|
|
284
|
+
await expect(fixture.node.get(other)).resolves.toBe('other');
|
|
285
|
+
} finally {
|
|
286
|
+
if (prefixed.status !== 'end') {
|
|
287
|
+
const ended = once(prefixed, 'end');
|
|
288
|
+
prefixed.disconnect();
|
|
289
|
+
await ended;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('cache-evict-allentries-prefix/F07 node-redis 缺省前缀不改写 key', async () => {
|
|
295
|
+
if (!fixture) {
|
|
296
|
+
throw new Error('Redis fixture is required');
|
|
297
|
+
}
|
|
298
|
+
const namespace = createKeyNamespace(fixture);
|
|
299
|
+
const first = namespace.physicalKey('name:1');
|
|
300
|
+
const second = namespace.physicalKey('name:2');
|
|
301
|
+
const other = namespace.physicalKey('order:1');
|
|
302
|
+
await fixture.node.mSet({ [first]: 'one', [second]: 'two', [other]: 'other' });
|
|
303
|
+
|
|
304
|
+
await nodeProvider.deleteByPattern(`${namespace.keyPrefix}name*`);
|
|
305
|
+
|
|
306
|
+
await expect(fixture.node.mGet([first, second])).resolves.toEqual([null, null]);
|
|
307
|
+
await expect(fixture.node.get(other)).resolves.toBe('other');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('cache-evict-allentries-prefix/F08 node-redis 显式前缀访问同一命名空间', async () => {
|
|
311
|
+
if (!fixture) {
|
|
312
|
+
throw new Error('Redis fixture is required');
|
|
313
|
+
}
|
|
314
|
+
const namespace = createKeyNamespace(fixture);
|
|
315
|
+
const physicalKey = namespace.physicalKey('name:1');
|
|
316
|
+
const provider = new RedisCacheProvider(
|
|
317
|
+
createNodeRedisCacheClient(fixture.node, { keyPrefix: namespace.keyPrefix })
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
await provider.set('name:1', { prefixed: true });
|
|
321
|
+
await expect(fixture.io.get(physicalKey)).resolves.toBe('{"prefixed":true}');
|
|
322
|
+
await expect(provider.get('name:1')).resolves.toEqual({ prefixed: true });
|
|
323
|
+
await provider.deleteByPattern('name*');
|
|
324
|
+
await expect(fixture.io.get(physicalKey)).resolves.toBeNull();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
redisTests('Redis 数据库级清理(要求 URL 显式选择非零数据库)', () => {
|
|
329
|
+
let fixture: RedisFixture | undefined;
|
|
330
|
+
|
|
331
|
+
beforeEach(async () => {
|
|
332
|
+
if (!redisUrl) {
|
|
333
|
+
throw new Error('CACHE_DECORATOR_TEST_REDIS_URL is required');
|
|
334
|
+
}
|
|
335
|
+
fixture = await createIsolatedDatabaseRedisFixture(redisUrl);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
afterEach(async () => fixture?.close());
|
|
339
|
+
|
|
340
|
+
it('redis-cache-client/R04 重复清库保持数据库级语义', async () => {
|
|
341
|
+
if (!fixture) {
|
|
342
|
+
throw new Error('Redis fixture is required');
|
|
343
|
+
}
|
|
344
|
+
const cacheKey = fixture.key();
|
|
345
|
+
const businessKey = fixture.key();
|
|
346
|
+
const provider = new RedisCacheProvider(createNodeRedisCacheClient(fixture.node, { keyPrefix: 'ignored:' }));
|
|
347
|
+
await fixture.node.mSet({ [cacheKey]: 'cache', [businessKey]: 'business' });
|
|
348
|
+
|
|
349
|
+
await expect(provider.clear()).resolves.toBeUndefined();
|
|
350
|
+
await expect(provider.clear()).resolves.toBeUndefined();
|
|
351
|
+
|
|
352
|
+
await expect(fixture.node.mGet([cacheKey, businessKey])).resolves.toEqual([null, null]);
|
|
353
|
+
await expect(fixture.node.dbSize()).resolves.toBe(0);
|
|
354
|
+
});
|
|
355
|
+
});
|