@jintianxiayu/cache-decorator 0.1.2 → 1.0.0

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 (91) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +270 -133
  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-logger.d.ts +28 -0
  16. package/dist/core/cache-logger.d.ts.map +1 -0
  17. package/dist/core/cache-logger.js +70 -0
  18. package/dist/core/cache-logger.js.map +1 -0
  19. package/dist/core/cache-provider-registry.d.ts.map +1 -1
  20. package/dist/core/cache-provider-registry.js.map +1 -1
  21. package/dist/core/cache-provider.d.ts.map +1 -1
  22. package/dist/core/key-builder.d.ts.map +1 -1
  23. package/dist/core/key-builder.js +3 -2
  24. package/dist/core/key-builder.js.map +1 -1
  25. package/dist/core/native-cache.d.ts.map +1 -1
  26. package/dist/core/native-cache.js +2 -1
  27. package/dist/core/native-cache.js.map +1 -1
  28. package/dist/core/pending-cache.d.ts.map +1 -1
  29. package/dist/core/pending-cache.js +6 -3
  30. package/dist/core/pending-cache.js.map +1 -1
  31. package/dist/core/redis-cache-client.d.ts +156 -0
  32. package/dist/core/redis-cache-client.d.ts.map +1 -0
  33. package/dist/core/redis-cache-client.js +3 -0
  34. package/dist/core/redis-cache-client.js.map +1 -0
  35. package/dist/core/redis-cache.d.ts +39 -7
  36. package/dist/core/redis-cache.d.ts.map +1 -1
  37. package/dist/core/redis-cache.js +63 -22
  38. package/dist/core/redis-cache.js.map +1 -1
  39. package/dist/decorators/cache-evict.d.ts +1 -1
  40. package/dist/decorators/cache-evict.d.ts.map +1 -1
  41. package/dist/decorators/cache-evict.js +68 -14
  42. package/dist/decorators/cache-evict.js.map +1 -1
  43. package/dist/decorators/cache.d.ts +1 -1
  44. package/dist/decorators/cache.d.ts.map +1 -1
  45. package/dist/decorators/cache.js +85 -23
  46. package/dist/decorators/cache.js.map +1 -1
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +6 -0
  50. package/dist/index.js.map +1 -1
  51. package/jest.config.js +11 -14
  52. package/package.json +26 -29
  53. package/src/adapters/ioredis-cache-client.ts +89 -0
  54. package/src/adapters/node-redis-cache-client.ts +95 -0
  55. package/src/adapters/redis-key-prefix.ts +38 -0
  56. package/src/core/cache-logger.ts +105 -0
  57. package/src/core/cache-provider-registry.ts +39 -39
  58. package/src/core/cache-provider.ts +28 -28
  59. package/src/core/key-builder.ts +33 -31
  60. package/src/core/native-cache.ts +55 -53
  61. package/src/core/pending-cache.ts +29 -26
  62. package/src/core/redis-cache-client.ts +160 -0
  63. package/src/core/redis-cache.ts +104 -62
  64. package/src/decorators/cache-evict.ts +129 -73
  65. package/src/decorators/cache.ts +203 -129
  66. package/src/index.ts +11 -9
  67. package/test/cache-evict-logging.test.ts +362 -0
  68. package/test/cache-evict.test.ts +165 -165
  69. package/test/cache-logger.integration.test.ts +129 -0
  70. package/test/cache-logger.test.ts +153 -0
  71. package/test/cache-logging.test.ts +544 -0
  72. package/test/cache-provider-registry.test.ts +24 -24
  73. package/test/cache.test.ts +255 -256
  74. package/test/fixtures/cache-logger-child.mjs +108 -0
  75. package/test/helpers/legacy-redis-cache.ts +22 -0
  76. package/test/helpers/package-consumer.ts +231 -0
  77. package/test/helpers/redis-fixture.ts +142 -0
  78. package/test/ioredis-cache-client.test.ts +143 -0
  79. package/test/key-builder.test.ts +28 -28
  80. package/test/native-cache.test.ts +77 -71
  81. package/test/node-redis-cache-client.test.ts +149 -0
  82. package/test/pending-cache.test.ts +69 -56
  83. package/test/redis-cache-client-lifecycle.test.ts +112 -0
  84. package/test/redis-cache-client-types.test.ts +184 -0
  85. package/test/redis-cache-client.integration.test.ts +327 -0
  86. package/test/redis-cache-decorator.test.ts +201 -0
  87. package/test/redis-cache-package.integration.test.ts +328 -0
  88. package/test/redis-cache-provider.test.ts +269 -0
  89. package/test/type-contract/contract.ts +64 -0
  90. package/test/type-contract/tsconfig.json +12 -0
  91. package/tsconfig.json +8 -8
@@ -0,0 +1,184 @@
1
+ import { resolve } from 'node:path';
2
+ import {
3
+ createCompilerHost,
4
+ createProgram,
5
+ createSourceFile,
6
+ getPreEmitDiagnostics,
7
+ ModuleKind,
8
+ ModuleResolutionKind,
9
+ ScriptTarget,
10
+ } from 'typescript';
11
+
12
+ /**
13
+ * 在包内虚拟文件位置严格检查消费方源码,不构造客户端或建立连接。
14
+ * @param source 消费方 TypeScript 源码。
15
+ * @returns 编译器产生的全部预发射诊断;空数组表示类型检查通过。
16
+ * @throws 编译器或文件系统读取失败时传播原始错误。
17
+ */
18
+ function checkTypes(source: string): readonly unknown[] {
19
+ const file = resolve(__dirname, 'type-consumer.ts');
20
+ const options = {
21
+ strict: true,
22
+ noEmit: true,
23
+ skipLibCheck: false,
24
+ target: ScriptTarget.ES2021,
25
+ module: ModuleKind.NodeNext,
26
+ moduleResolution: ModuleResolutionKind.NodeNext,
27
+ types: ['node'],
28
+ experimentalDecorators: true,
29
+ emitDecoratorMetadata: true,
30
+ };
31
+ const host = createCompilerHost(options);
32
+ const originalGetSourceFile = host.getSourceFile.bind(host);
33
+ host.getSourceFile = (name, languageVersion, onError, shouldCreateNewSourceFile) => {
34
+ if (resolve(name) === file) {
35
+ return createSourceFile(name, source, languageVersion, true);
36
+ }
37
+ return originalGetSourceFile(name, languageVersion, onError, shouldCreateNewSourceFile);
38
+ };
39
+ return getPreEmitDiagnostics(createProgram([file], options, host)).map((diagnostic) => diagnostic.messageText);
40
+ }
41
+
42
+ it('redis-cache-client/C01 自定义客户端接入统一 Provider', () => {
43
+ expect(
44
+ checkTypes(`
45
+ import { RedisCacheClient, RedisCacheProvider } from '../src';
46
+
47
+ const client: RedisCacheClient = {
48
+ get(_key: string): Promise<string | null> {
49
+ return Promise.resolve(null);
50
+ },
51
+ set(_request): Promise<void> {
52
+ return Promise.resolve();
53
+ },
54
+ deleteMany(_keys): Promise<void> {
55
+ return Promise.resolve();
56
+ },
57
+ scan(_request): Promise<{ readonly cursor: string; readonly keys: readonly string[] }> {
58
+ return Promise.resolve({ cursor: '0', keys: [] });
59
+ },
60
+ flushDatabase(): Promise<void> {
61
+ return Promise.resolve();
62
+ },
63
+ };
64
+
65
+ export const provider = new RedisCacheProvider(client);
66
+ `)
67
+ ).toEqual([]);
68
+ });
69
+
70
+ it('redis-cache-client/C02 ioredis 实例通过真实类型检查', () => {
71
+ expect(
72
+ checkTypes(`
73
+ import type Redis from 'ioredis';
74
+ import {
75
+ createIoredisCacheClient,
76
+ IoredisCacheClientSource,
77
+ RedisCacheProvider,
78
+ } from '../src';
79
+
80
+ declare const client: Redis;
81
+ export const accepted: IoredisCacheClientSource = client;
82
+ export const provider = new RedisCacheProvider(createIoredisCacheClient(client));
83
+ `)
84
+ ).toEqual([]);
85
+ });
86
+
87
+ it('redis-cache-client/C03 node-redis 实例通过真实类型检查', () => {
88
+ expect(
89
+ checkTypes(`
90
+ import { createClient } from 'redis';
91
+ import {
92
+ createNodeRedisCacheClient,
93
+ NodeRedisCacheClientSource,
94
+ RedisCacheProvider,
95
+ } from '../src';
96
+
97
+ declare const client: ReturnType<typeof createClient>;
98
+ export const accepted: NodeRedisCacheClientSource = client;
99
+ export const provider = new RedisCacheProvider(createNodeRedisCacheClient(client));
100
+ `)
101
+ ).toEqual([]);
102
+ });
103
+
104
+ const invalidCalls = [
105
+ 'new RedisCacheProvider();',
106
+ 'new RedisCacheProvider(ioredis);',
107
+ 'new RedisCacheProvider(nodeRedis);',
108
+ 'new RedisCacheProvider(null);',
109
+ 'new RedisCacheProvider(undefined);',
110
+ "cacheClient.set({ value: 'value' });",
111
+ "cacheClient.set({ key: 'key' });",
112
+ "cacheClient.scan({ pattern: '*', count: 100 });",
113
+ "cacheClient.scan({ cursor: '0', count: 100 });",
114
+ "cacheClient.scan({ cursor: '0', pattern: '*' });",
115
+ 'cacheClient.set(null);',
116
+ 'cacheClient.scan(undefined);',
117
+ 'createIoredisCacheClient();',
118
+ 'createIoredisCacheClient(null);',
119
+ 'createIoredisCacheClient(undefined);',
120
+ 'createNodeRedisCacheClient();',
121
+ 'createNodeRedisCacheClient(null);',
122
+ 'createNodeRedisCacheClient(undefined);',
123
+ 'createIoredisCacheClient(nodeRedis);',
124
+ 'createNodeRedisCacheClient(ioredis);',
125
+ ];
126
+
127
+ it('redis-cache-client/C04 不完整或错误配对的类型被拒绝', () => {
128
+ const negativeFixture = invalidCalls.map((call) => `// @ts-expect-error 必须拒绝此无效调用\n${call}`).join('\n');
129
+ expect(
130
+ checkTypes(`
131
+ import type Redis from 'ioredis';
132
+ import { createClient } from 'redis';
133
+ import {
134
+ createIoredisCacheClient,
135
+ createNodeRedisCacheClient,
136
+ RedisCacheClient,
137
+ RedisCacheProvider,
138
+ } from '../src';
139
+
140
+ declare const cacheClient: RedisCacheClient;
141
+ declare const ioredis: Redis;
142
+ declare const nodeRedis: ReturnType<typeof createClient>;
143
+ ${negativeFixture}
144
+ `)
145
+ ).toEqual([]);
146
+ });
147
+
148
+ it('cache-operation-logging/A01 既有 decorator 与 Provider 公共类型不需要日志选项', () => {
149
+ expect(
150
+ checkTypes(`
151
+ import {
152
+ Cache,
153
+ CacheEvict,
154
+ type CacheEvictOptions,
155
+ type CacheOptions,
156
+ type CacheProvider,
157
+ } from '../src';
158
+
159
+ const provider: CacheProvider = {
160
+ get: <Value>(_key: string): Value | undefined => undefined,
161
+ set: <Value>(_key: string, _value: Value, _ttl?: number): void => undefined,
162
+ delete: (_key: string): void => undefined,
163
+ clear: (): void => undefined,
164
+ deleteByPattern: (_pattern: string): void => undefined,
165
+ };
166
+ const cache = Cache('contract-cache', { ttl: 60, providerName: 'memory', key: null });
167
+ const evict = CacheEvict('contract-cache', { providerName: 'memory', allEntries: true });
168
+ const invalidCacheOptions: CacheOptions = {
169
+ // @ts-expect-error CacheOptions 不接受第二套日志配置。
170
+ logging: true,
171
+ };
172
+ const invalidEvictOptions: CacheEvictOptions = {
173
+ // @ts-expect-error CacheEvictOptions 不接受 Logger 实例。
174
+ logger: undefined,
175
+ };
176
+
177
+ void provider;
178
+ void cache;
179
+ void evict;
180
+ void invalidCacheOptions;
181
+ void invalidEvictOptions;
182
+ `)
183
+ ).toEqual([]);
184
+ });
@@ -0,0 +1,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 { 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('cache-evict-allentries-prefix/RedisCacheProvider 使用 SCAN 迭代删除', async () => {
211
+ if (!fixture) {
212
+ throw new Error('Redis fixture is required');
213
+ }
214
+ const namespace = createKeyNamespace(fixture);
215
+ const provider = new RedisCacheProvider(
216
+ createNodeRedisCacheClient(fixture.node, { keyPrefix: namespace.keyPrefix })
217
+ );
218
+ const matchingKeys = Array.from({ length: 150 }, (_, index) => `name:${index}`);
219
+ const physicalMatchingKeys = matchingKeys.map((key) => namespace.physicalKey(key));
220
+ const otherKey = namespace.physicalKey('order:1');
221
+ await Promise.all([
222
+ ...physicalMatchingKeys.map((key) => fixture.node.set(key, 'value')),
223
+ fixture.node.set(otherKey, 'other'),
224
+ ]);
225
+ const scan = jest.spyOn(fixture.node, 'scan');
226
+
227
+ await provider.deleteByPattern('name*');
228
+
229
+ expect(scan).toHaveBeenCalled();
230
+ for (const call of scan.mock.calls) {
231
+ expect(call[1]).toEqual({ MATCH: `${namespace.keyPrefix}name*`, COUNT: 100 });
232
+ }
233
+ expect((await fixture.node.mGet(physicalMatchingKeys)).every((value) => value === null)).toBe(true);
234
+ await expect(fixture.node.get(otherKey)).resolves.toBe('other');
235
+ scan.mockRestore();
236
+ });
237
+
238
+ it('cache-evict-allentries-prefix/F05 ioredis keyPrefix 下删除逻辑 pattern', async () => {
239
+ if (!fixture) {
240
+ throw new Error('Redis fixture is required');
241
+ }
242
+ const namespace = createKeyNamespace(fixture);
243
+ const first = namespace.physicalKey('name:1');
244
+ const second = namespace.physicalKey('name:2');
245
+ const other = namespace.physicalKey('order:1');
246
+ const prefixed = new Redis({ ...fixture.io.options, keyPrefix: namespace.keyPrefix, lazyConnect: true });
247
+ prefixed.on('error', (error: Error) => fixture.errors.push(error));
248
+ try {
249
+ await prefixed.connect();
250
+ await fixture.node.mSet({ [first]: 'one', [second]: 'two', [other]: 'other' });
251
+ const provider = new RedisCacheProvider(createIoredisCacheClient(prefixed));
252
+
253
+ await provider.deleteByPattern('name*');
254
+
255
+ await expect(fixture.node.mGet([first, second])).resolves.toEqual([null, null]);
256
+ await expect(fixture.node.get(other)).resolves.toBe('other');
257
+ } finally {
258
+ if (prefixed.status !== 'end') {
259
+ const ended = once(prefixed, 'end');
260
+ prefixed.disconnect();
261
+ await ended;
262
+ }
263
+ }
264
+ });
265
+
266
+ it('cache-evict-allentries-prefix/F07 node-redis 缺省前缀不改写 key', 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
+ await fixture.node.mSet({ [first]: 'one', [second]: 'two', [other]: 'other' });
275
+
276
+ await nodeProvider.deleteByPattern(`${namespace.keyPrefix}name*`);
277
+
278
+ await expect(fixture.node.mGet([first, second])).resolves.toEqual([null, null]);
279
+ await expect(fixture.node.get(other)).resolves.toBe('other');
280
+ });
281
+
282
+ it('cache-evict-allentries-prefix/F08 node-redis 显式前缀访问同一命名空间', async () => {
283
+ if (!fixture) {
284
+ throw new Error('Redis fixture is required');
285
+ }
286
+ const namespace = createKeyNamespace(fixture);
287
+ const physicalKey = namespace.physicalKey('name:1');
288
+ const provider = new RedisCacheProvider(
289
+ createNodeRedisCacheClient(fixture.node, { keyPrefix: namespace.keyPrefix })
290
+ );
291
+
292
+ await provider.set('name:1', { prefixed: true });
293
+ await expect(fixture.io.get(physicalKey)).resolves.toBe('{"prefixed":true}');
294
+ await expect(provider.get('name:1')).resolves.toEqual({ prefixed: true });
295
+ await provider.deleteByPattern('name*');
296
+ await expect(fixture.io.get(physicalKey)).resolves.toBeNull();
297
+ });
298
+ });
299
+
300
+ redisTests('Redis 数据库级清理(要求 URL 显式选择非零数据库)', () => {
301
+ let fixture: RedisFixture | undefined;
302
+
303
+ beforeEach(async () => {
304
+ if (!redisUrl) {
305
+ throw new Error('CACHE_DECORATOR_TEST_REDIS_URL is required');
306
+ }
307
+ fixture = await createIsolatedDatabaseRedisFixture(redisUrl);
308
+ });
309
+
310
+ afterEach(async () => fixture?.close());
311
+
312
+ it('redis-cache-client/R04 重复清库保持数据库级语义', async () => {
313
+ if (!fixture) {
314
+ throw new Error('Redis fixture is required');
315
+ }
316
+ const cacheKey = fixture.key();
317
+ const businessKey = fixture.key();
318
+ const provider = new RedisCacheProvider(createNodeRedisCacheClient(fixture.node, { keyPrefix: 'ignored:' }));
319
+ await fixture.node.mSet({ [cacheKey]: 'cache', [businessKey]: 'business' });
320
+
321
+ await expect(provider.clear()).resolves.toBeUndefined();
322
+ await expect(provider.clear()).resolves.toBeUndefined();
323
+
324
+ await expect(fixture.node.mGet([cacheKey, businessKey])).resolves.toEqual([null, null]);
325
+ await expect(fixture.node.dbSize()).resolves.toBe(0);
326
+ });
327
+ });