@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.
- package/CHANGELOG.md +18 -0
- package/README.md +270 -133
- 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-logger.d.ts +28 -0
- package/dist/core/cache-logger.d.ts.map +1 -0
- package/dist/core/cache-logger.js +70 -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 +1 -1
- package/dist/decorators/cache.d.ts.map +1 -1
- package/dist/decorators/cache.js +85 -23
- 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 +11 -14
- 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-logger.ts +105 -0
- package/src/core/cache-provider-registry.ts +39 -39
- package/src/core/cache-provider.ts +28 -28
- package/src/core/key-builder.ts +33 -31
- package/src/core/native-cache.ts +55 -53
- package/src/core/pending-cache.ts +29 -26
- package/src/core/redis-cache-client.ts +160 -0
- package/src/core/redis-cache.ts +104 -62
- package/src/decorators/cache-evict.ts +129 -73
- package/src/decorators/cache.ts +203 -129
- package/src/index.ts +11 -9
- package/test/cache-evict-logging.test.ts +362 -0
- package/test/cache-evict.test.ts +165 -165
- package/test/cache-logger.integration.test.ts +129 -0
- package/test/cache-logger.test.ts +153 -0
- package/test/cache-logging.test.ts +544 -0
- package/test/cache-provider-registry.test.ts +24 -24
- package/test/cache.test.ts +255 -256
- package/test/fixtures/cache-logger-child.mjs +108 -0
- package/test/helpers/legacy-redis-cache.ts +22 -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/key-builder.test.ts +28 -28
- package/test/native-cache.test.ts +77 -71
- package/test/node-redis-cache-client.test.ts +149 -0
- package/test/pending-cache.test.ts +69 -56
- 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 +327 -0
- package/test/redis-cache-decorator.test.ts +201 -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 +64 -0
- package/test/type-contract/tsconfig.json +12 -0
- package/tsconfig.json +8 -8
|
@@ -1,71 +1,77 @@
|
|
|
1
|
-
import { MemoryCacheProvider } from '../src/core/native-cache';
|
|
2
|
-
|
|
3
|
-
describe('MemoryCacheProvider', () => {
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
import { MemoryCacheProvider } from '../src/core/native-cache';
|
|
2
|
+
|
|
3
|
+
describe('MemoryCacheProvider', () => {
|
|
4
|
+
let provider: MemoryCacheProvider;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
provider = new MemoryCacheProvider();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
jest.useRealTimers();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('get & set', () => {
|
|
15
|
+
it('应返回已缓存的值', () => {
|
|
16
|
+
provider.set('key1', 'value1');
|
|
17
|
+
expect(provider.get('key1')).toBe('value1');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('应返回 undefined 对不存在的键', () => {
|
|
21
|
+
expect(provider.get('nonexistent')).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('TTL', () => {
|
|
26
|
+
it('应在 TTL 后失效', () => {
|
|
27
|
+
jest.useFakeTimers();
|
|
28
|
+
|
|
29
|
+
provider.set('key1', 'value1', 5);
|
|
30
|
+
expect(provider.get('key1')).toBe('value1');
|
|
31
|
+
|
|
32
|
+
jest.advanceTimersByTime(5001);
|
|
33
|
+
expect(provider.get('key1')).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('无 TTL 时不应失效', () => {
|
|
37
|
+
jest.useFakeTimers();
|
|
38
|
+
|
|
39
|
+
provider.set('key1', 'value1');
|
|
40
|
+
jest.advanceTimersByTime(24 * 60 * 60 * 1000);
|
|
41
|
+
|
|
42
|
+
expect(provider.get('key1')).toBe('value1');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('delete', () => {
|
|
47
|
+
it('应删除指定键', () => {
|
|
48
|
+
provider.set('key1', 'value1');
|
|
49
|
+
provider.delete('key1');
|
|
50
|
+
expect(provider.get('key1')).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('clear', () => {
|
|
55
|
+
it('应清除所有缓存', () => {
|
|
56
|
+
provider.set('key1', 'value1');
|
|
57
|
+
provider.set('key2', 'value2');
|
|
58
|
+
provider.clear();
|
|
59
|
+
expect(provider.get('key1')).toBeUndefined();
|
|
60
|
+
expect(provider.get('key2')).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('deleteByPattern', () => {
|
|
65
|
+
it('cache-evict-allentries-prefix/MemoryCacheProvider 实现前缀匹配', () => {
|
|
66
|
+
provider.set('user:1', 'user1');
|
|
67
|
+
provider.set('user:2', 'user2');
|
|
68
|
+
provider.set('order:1', 'order1');
|
|
69
|
+
|
|
70
|
+
provider.deleteByPattern('user:*');
|
|
71
|
+
|
|
72
|
+
expect(provider.get('user:1')).toBeUndefined();
|
|
73
|
+
expect(provider.get('user:2')).toBeUndefined();
|
|
74
|
+
expect(provider.get('order:1')).toBe('order1');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createNodeRedisCacheClient } from '../src/adapters/node-redis-cache-client';
|
|
2
|
+
import { NodeRedisCacheClientSource } from '../src/core/redis-cache-client';
|
|
3
|
+
|
|
4
|
+
function createSource(): jest.Mocked<NodeRedisCacheClientSource> {
|
|
5
|
+
const source = {
|
|
6
|
+
get: jest.fn<Promise<unknown>, Parameters<NodeRedisCacheClientSource['get']>>(),
|
|
7
|
+
set: jest.fn<Promise<unknown>, Parameters<NodeRedisCacheClientSource['set']>>(),
|
|
8
|
+
setEx: jest.fn<Promise<unknown>, Parameters<NodeRedisCacheClientSource['setEx']>>(),
|
|
9
|
+
del: jest.fn<Promise<unknown>, Parameters<NodeRedisCacheClientSource['del']>>(),
|
|
10
|
+
scan: jest.fn<Promise<unknown>, Parameters<NodeRedisCacheClientSource['scan']>>(),
|
|
11
|
+
flushDb: jest.fn<Promise<unknown>, Parameters<NodeRedisCacheClientSource['flushDb']>>(),
|
|
12
|
+
} satisfies jest.Mocked<NodeRedisCacheClientSource>;
|
|
13
|
+
return source;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
it('redis-cache-client/A03 node-redis 无 TTL 写入成功', async () => {
|
|
17
|
+
const source = createSource();
|
|
18
|
+
const client = createNodeRedisCacheClient(source, { keyPrefix: 'cache:' });
|
|
19
|
+
const request = Object.freeze({ key: 'order:1', value: '{"status":"open"}' });
|
|
20
|
+
source.set.mockResolvedValue('OK');
|
|
21
|
+
|
|
22
|
+
await expect(client.set(request)).resolves.toBeUndefined();
|
|
23
|
+
|
|
24
|
+
expect(source.set).toHaveBeenCalledWith('cache:order:1', request.value);
|
|
25
|
+
expect(source.setEx).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('redis-cache-client/A04 node-redis 带 TTL 写入成功', async () => {
|
|
29
|
+
const source = createSource();
|
|
30
|
+
const client = createNodeRedisCacheClient(source, { keyPrefix: 'cache:' });
|
|
31
|
+
const request = Object.freeze({ key: 'order:2', value: 'paid', ttlSeconds: 60 });
|
|
32
|
+
source.setEx.mockResolvedValue('OK');
|
|
33
|
+
|
|
34
|
+
await expect(client.set(request)).resolves.toBeUndefined();
|
|
35
|
+
|
|
36
|
+
expect(source.setEx).toHaveBeenCalledWith('cache:order:2', request.ttlSeconds, request.value);
|
|
37
|
+
expect(source.set).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('redis-cache-client/A05 GET 响应规范化', async () => {
|
|
41
|
+
const source = createSource();
|
|
42
|
+
const client = createNodeRedisCacheClient(source, { keyPrefix: 'cache:' });
|
|
43
|
+
source.get.mockResolvedValueOnce('cached').mockResolvedValueOnce(null);
|
|
44
|
+
|
|
45
|
+
await expect(client.get('present')).resolves.toBe('cached');
|
|
46
|
+
await expect(client.get('missing')).resolves.toBeNull();
|
|
47
|
+
|
|
48
|
+
expect(source.get).toHaveBeenNthCalledWith(1, 'cache:present');
|
|
49
|
+
expect(source.get).toHaveBeenNthCalledWith(2, 'cache:missing');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('redis-cache-client/A06 删除和清库响应规范化', async () => {
|
|
53
|
+
const source = createSource();
|
|
54
|
+
const client = createNodeRedisCacheClient(source, { keyPrefix: 'cache:' });
|
|
55
|
+
source.del.mockResolvedValueOnce(2).mockResolvedValueOnce(0);
|
|
56
|
+
source.flushDb.mockResolvedValue('OK');
|
|
57
|
+
|
|
58
|
+
await expect(client.deleteMany(['first', 'second'])).resolves.toBeUndefined();
|
|
59
|
+
await expect(client.deleteMany(['missing'])).resolves.toBeUndefined();
|
|
60
|
+
await expect(client.deleteMany([])).resolves.toBeUndefined();
|
|
61
|
+
await expect(client.flushDatabase()).resolves.toBeUndefined();
|
|
62
|
+
|
|
63
|
+
expect(source.del).toHaveBeenCalledTimes(2);
|
|
64
|
+
expect(source.del).toHaveBeenNthCalledWith(1, ['cache:first', 'cache:second']);
|
|
65
|
+
expect(source.del).toHaveBeenNthCalledWith(2, ['cache:missing']);
|
|
66
|
+
expect(source.flushDb).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('redis-cache-client/A07 调用上下文与请求保持不变', async () => {
|
|
70
|
+
const source = createSource();
|
|
71
|
+
const options = Object.freeze({ keyPrefix: 'scope:' });
|
|
72
|
+
const client = createNodeRedisCacheClient(source, options);
|
|
73
|
+
source.set.mockImplementation(function (this: NodeRedisCacheClientSource): Promise<unknown> {
|
|
74
|
+
expect(this).toBe(source);
|
|
75
|
+
return Promise.resolve('OK');
|
|
76
|
+
});
|
|
77
|
+
source.del.mockImplementation(function (this: NodeRedisCacheClientSource): Promise<unknown> {
|
|
78
|
+
expect(this).toBe(source);
|
|
79
|
+
return Promise.resolve(2);
|
|
80
|
+
});
|
|
81
|
+
source.scan.mockImplementation(function (this: NodeRedisCacheClientSource): Promise<unknown> {
|
|
82
|
+
expect(this).toBe(source);
|
|
83
|
+
return Promise.resolve({ cursor: '0', keys: ['scope:first', 'scope:second'] });
|
|
84
|
+
});
|
|
85
|
+
const writeRequest = Object.freeze({ key: 'first', value: 'value' });
|
|
86
|
+
const keys = Object.freeze(['first', 'second']);
|
|
87
|
+
const scanRequest = Object.freeze({ cursor: '0', pattern: 'first*', count: 100 });
|
|
88
|
+
|
|
89
|
+
await client.set(writeRequest);
|
|
90
|
+
await client.deleteMany(keys);
|
|
91
|
+
await client.scan(scanRequest);
|
|
92
|
+
|
|
93
|
+
expect(options).toEqual({ keyPrefix: 'scope:' });
|
|
94
|
+
expect(writeRequest).toEqual({ key: 'first', value: 'value' });
|
|
95
|
+
expect(keys).toEqual(['first', 'second']);
|
|
96
|
+
expect(scanRequest).toEqual({ cursor: '0', pattern: 'first*', count: 100 });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('redis-cache-client/A08 非标准响应明确失败', async () => {
|
|
100
|
+
const source = createSource();
|
|
101
|
+
const client = createNodeRedisCacheClient(source);
|
|
102
|
+
source.get.mockResolvedValue(Buffer.from('value'));
|
|
103
|
+
source.set.mockResolvedValue(true);
|
|
104
|
+
source.setEx.mockResolvedValue(undefined);
|
|
105
|
+
source.del.mockResolvedValue(1.5);
|
|
106
|
+
source.scan.mockResolvedValue(['0', []]);
|
|
107
|
+
source.flushDb.mockResolvedValue(null);
|
|
108
|
+
|
|
109
|
+
await expect(client.get('key')).rejects.toThrow(TypeError);
|
|
110
|
+
await expect(client.set({ key: 'key', value: 'value' })).rejects.toThrow(TypeError);
|
|
111
|
+
await expect(client.set({ key: 'key', value: 'value', ttlSeconds: 1 })).rejects.toThrow(TypeError);
|
|
112
|
+
await expect(client.deleteMany(['key'])).rejects.toThrow(TypeError);
|
|
113
|
+
await expect(client.scan({ cursor: '0', pattern: '*', count: 100 })).rejects.toThrow(TypeError);
|
|
114
|
+
await expect(client.flushDatabase()).rejects.toThrow(TypeError);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('cache-evict-allentries-prefix/F07 node-redis 缺省前缀不改写 key', async () => {
|
|
118
|
+
const source = createSource();
|
|
119
|
+
const client = createNodeRedisCacheClient(source);
|
|
120
|
+
source.scan.mockResolvedValue({ cursor: '0', keys: ['name:1'] });
|
|
121
|
+
source.del.mockResolvedValue(1);
|
|
122
|
+
|
|
123
|
+
const page = await client.scan({ cursor: '0', pattern: 'name*', count: 100 });
|
|
124
|
+
await client.deleteMany(page.keys);
|
|
125
|
+
|
|
126
|
+
expect(source.scan).toHaveBeenCalledWith('0', { MATCH: 'name*', COUNT: 100 });
|
|
127
|
+
expect(page).toEqual({ cursor: '0', keys: ['name:1'] });
|
|
128
|
+
expect(source.del).toHaveBeenCalledWith(['name:1']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('cache-evict-allentries-prefix/F08 node-redis 显式前缀访问同一命名空间', async () => {
|
|
132
|
+
const source = createSource();
|
|
133
|
+
const client = createNodeRedisCacheClient(source, { keyPrefix: 'cache:' });
|
|
134
|
+
source.get.mockResolvedValue('value');
|
|
135
|
+
source.set.mockResolvedValue('OK');
|
|
136
|
+
source.scan.mockResolvedValue({ cursor: '0', keys: ['cache:name:1'] });
|
|
137
|
+
source.del.mockResolvedValue(1);
|
|
138
|
+
|
|
139
|
+
await expect(client.get('name:1')).resolves.toBe('value');
|
|
140
|
+
await client.set({ key: 'name:1', value: 'updated' });
|
|
141
|
+
const page = await client.scan({ cursor: '0', pattern: 'name*', count: 100 });
|
|
142
|
+
await client.deleteMany(page.keys);
|
|
143
|
+
|
|
144
|
+
expect(source.get).toHaveBeenCalledWith('cache:name:1');
|
|
145
|
+
expect(source.set).toHaveBeenCalledWith('cache:name:1', 'updated');
|
|
146
|
+
expect(source.scan).toHaveBeenCalledWith('0', { MATCH: 'cache:name*', COUNT: 100 });
|
|
147
|
+
expect(page).toEqual({ cursor: '0', keys: ['name:1'] });
|
|
148
|
+
expect(source.del).toHaveBeenCalledWith(['cache:name:1']);
|
|
149
|
+
});
|
|
@@ -1,56 +1,69 @@
|
|
|
1
|
-
import { PendingCache } from '../src/core/pending-cache';
|
|
2
|
-
|
|
3
|
-
describe('PendingCache', () => {
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
import { PendingCache } from '../src/core/pending-cache';
|
|
2
|
+
|
|
3
|
+
describe('PendingCache', () => {
|
|
4
|
+
let pendingCache: PendingCache;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
pendingCache = new PendingCache();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('get & set', () => {
|
|
11
|
+
it('应返回已存储的 Promise', async () => {
|
|
12
|
+
const promise = Promise.resolve('value');
|
|
13
|
+
pendingCache.set('key1', promise);
|
|
14
|
+
expect(await pendingCache.get('key1')).toBe('value');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('应返回 undefined 对不存在的键', () => {
|
|
18
|
+
expect(pendingCache.get('nonexistent')).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('请求合并', () => {
|
|
23
|
+
it('应在 Promise 完成时自动删除', async () => {
|
|
24
|
+
let resolve: (value: string) => void;
|
|
25
|
+
const promise = new Promise<string>((r) => {
|
|
26
|
+
resolve = r;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
pendingCache.set('key1', promise);
|
|
30
|
+
expect(pendingCache.get('key1')).toBeDefined();
|
|
31
|
+
|
|
32
|
+
resolve!('value');
|
|
33
|
+
await promise;
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
35
|
+
|
|
36
|
+
expect(pendingCache.get('key1')).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('应在 Promise 拒绝时自动删除且保留原始异常', async () => {
|
|
40
|
+
const expectedError = new Error('request failed');
|
|
41
|
+
const promise = Promise.reject(expectedError);
|
|
42
|
+
|
|
43
|
+
pendingCache.set('key1', promise);
|
|
44
|
+
|
|
45
|
+
await expect(promise).rejects.toBe(expectedError);
|
|
46
|
+
await Promise.resolve();
|
|
47
|
+
expect(pendingCache.get('key1')).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('delete', () => {
|
|
52
|
+
it('应删除指定键', async () => {
|
|
53
|
+
const promise = Promise.resolve('value');
|
|
54
|
+
pendingCache.set('key1', promise);
|
|
55
|
+
pendingCache.delete('key1');
|
|
56
|
+
expect(pendingCache.get('key1')).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('clear', () => {
|
|
61
|
+
it('应清除所有 pending 请求', async () => {
|
|
62
|
+
pendingCache.set('key1', Promise.resolve('v1'));
|
|
63
|
+
pendingCache.set('key2', Promise.resolve('v2'));
|
|
64
|
+
pendingCache.clear();
|
|
65
|
+
expect(pendingCache.get('key1')).toBeUndefined();
|
|
66
|
+
expect(pendingCache.get('key2')).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { createIoredisCacheClient } from '../src/adapters/ioredis-cache-client';
|
|
3
|
+
import { createNodeRedisCacheClient } from '../src/adapters/node-redis-cache-client';
|
|
4
|
+
import { CacheProviderRegistry } from '../src/core/cache-provider-registry';
|
|
5
|
+
import { RedisCacheClient } from '../src/core/redis-cache-client';
|
|
6
|
+
import { RedisCacheProvider } from '../src/core/redis-cache';
|
|
7
|
+
|
|
8
|
+
/** 创建由测试持有的连接探针,记录库是否越过连接所有权边界。 */
|
|
9
|
+
function createConnectionProbe() {
|
|
10
|
+
const connection = Object.assign(new EventEmitter(), {
|
|
11
|
+
options: Object.freeze({ connectionName: 'application', keyPrefix: 'tenant:' }),
|
|
12
|
+
get: jest.fn((_key: string): Promise<unknown> => Promise.resolve(null)),
|
|
13
|
+
set: jest.fn((_key: string, _value: string): Promise<unknown> => Promise.resolve('OK')),
|
|
14
|
+
setex: jest.fn((_key: string, _ttlSeconds: number, _value: string): Promise<unknown> => Promise.resolve('OK')),
|
|
15
|
+
setEx: jest.fn((_key: string, _ttlSeconds: number, _value: string): Promise<unknown> => Promise.resolve('OK')),
|
|
16
|
+
del: jest.fn((..._keys: Array<string | string[]>): Promise<unknown> => Promise.resolve(1)),
|
|
17
|
+
scan: jest.fn((...args: unknown[]): Promise<unknown> => {
|
|
18
|
+
return Promise.resolve(typeof args[1] === 'string' ? ['0', []] : { cursor: '0', keys: [] });
|
|
19
|
+
}),
|
|
20
|
+
flushdb: jest.fn((): Promise<unknown> => Promise.resolve('OK')),
|
|
21
|
+
flushDb: jest.fn((): Promise<unknown> => Promise.resolve('OK')),
|
|
22
|
+
connect: jest.fn(),
|
|
23
|
+
quit: jest.fn(),
|
|
24
|
+
disconnect: jest.fn(),
|
|
25
|
+
close: jest.fn(),
|
|
26
|
+
destroy: jest.fn(),
|
|
27
|
+
});
|
|
28
|
+
const errors: Error[] = [];
|
|
29
|
+
connection.on('error', (error: Error) => errors.push(error));
|
|
30
|
+
return connection;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 断言库未更改连接配置、监听器或建立/关闭连接。 */
|
|
34
|
+
function expectCallerOwnership(connection: ReturnType<typeof createConnectionProbe>): void {
|
|
35
|
+
expect(connection.options).toEqual({ connectionName: 'application', keyPrefix: 'tenant:' });
|
|
36
|
+
expect(connection.eventNames()).toEqual(['error']);
|
|
37
|
+
expect(connection.listenerCount('error')).toBe(1);
|
|
38
|
+
for (const operation of [
|
|
39
|
+
connection.connect,
|
|
40
|
+
connection.quit,
|
|
41
|
+
connection.disconnect,
|
|
42
|
+
connection.close,
|
|
43
|
+
connection.destroy,
|
|
44
|
+
]) {
|
|
45
|
+
expect(operation).not.toHaveBeenCalled();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface AdapterCase {
|
|
50
|
+
readonly name: string;
|
|
51
|
+
adapt(connection: ReturnType<typeof createConnectionProbe>): RedisCacheClient;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const adapterCases: readonly AdapterCase[] = [
|
|
55
|
+
{
|
|
56
|
+
name: 'ioredis',
|
|
57
|
+
adapt: (connection) => createIoredisCacheClient(connection),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'node-redis',
|
|
61
|
+
adapt: (connection) => createNodeRedisCacheClient(connection, { keyPrefix: connection.options.keyPrefix }),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
afterEach(() => CacheProviderRegistry.clear());
|
|
66
|
+
|
|
67
|
+
describe.each(adapterCases)('$name 连接所有权', ({ adapt }) => {
|
|
68
|
+
it('redis-cache-client/O01 构造与注册不操作连接', () => {
|
|
69
|
+
const connection = createConnectionProbe();
|
|
70
|
+
const options = connection.options;
|
|
71
|
+
const listeners = connection.listeners('error');
|
|
72
|
+
const provider = new RedisCacheProvider(adapt(connection));
|
|
73
|
+
CacheProviderRegistry.register('shared', provider);
|
|
74
|
+
CacheProviderRegistry.setDefault('shared');
|
|
75
|
+
|
|
76
|
+
expect(CacheProviderRegistry.get()).toBe(provider);
|
|
77
|
+
expect(connection.options).toBe(options);
|
|
78
|
+
expect(connection.listeners('error')).toEqual(listeners);
|
|
79
|
+
expectCallerOwnership(connection);
|
|
80
|
+
for (const command of [
|
|
81
|
+
connection.get,
|
|
82
|
+
connection.set,
|
|
83
|
+
connection.setex,
|
|
84
|
+
connection.setEx,
|
|
85
|
+
connection.del,
|
|
86
|
+
connection.scan,
|
|
87
|
+
connection.flushdb,
|
|
88
|
+
connection.flushDb,
|
|
89
|
+
]) {
|
|
90
|
+
expect(command).not.toHaveBeenCalled();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('redis-cache-client/O02 多个 Provider 共享连接', async () => {
|
|
95
|
+
const connection = createConnectionProbe();
|
|
96
|
+
const first = new RedisCacheProvider(adapt(connection));
|
|
97
|
+
const second = new RedisCacheProvider(adapt(connection));
|
|
98
|
+
|
|
99
|
+
await first.set('first', { value: 1 });
|
|
100
|
+
await second.get('first');
|
|
101
|
+
await first.delete('first');
|
|
102
|
+
await second.deleteByPattern('shared*');
|
|
103
|
+
await first.clear();
|
|
104
|
+
|
|
105
|
+
const error = new Error('connection failure');
|
|
106
|
+
connection.get.mockRejectedValueOnce(error);
|
|
107
|
+
await expect(second.get('failed')).rejects.toBe(error);
|
|
108
|
+
await expect(connection.get('application-key')).resolves.toBeNull();
|
|
109
|
+
await expect(connection.set('application-key', 'application-value')).resolves.toBe('OK');
|
|
110
|
+
expectCallerOwnership(connection);
|
|
111
|
+
});
|
|
112
|
+
});
|