@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
@@ -1,71 +1,77 @@
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
- describe('get & set', () => {
11
- it('应返回已缓存的值', () => {
12
- provider.set('key1', 'value1');
13
- expect(provider.get('key1')).toBe('value1');
14
- });
15
-
16
- it('应返回 undefined 对不存在的键', () => {
17
- expect(provider.get('nonexistent')).toBeUndefined();
18
- });
19
- });
20
-
21
- describe('TTL', () => {
22
- it('应在 TTL 后失效', () => {
23
- jest.useFakeTimers();
24
-
25
- provider.set('key1', 'value1', 5);
26
- expect(provider.get('key1')).toBe('value1');
27
-
28
- jest.advanceTimersByTime(5001);
29
- expect(provider.get('key1')).toBeUndefined();
30
-
31
- jest.useRealTimers();
32
- });
33
-
34
- it('无 TTL 时不应失效', () => {
35
- provider.set('key1', 'value1');
36
- expect(provider.get('key1')).toBe('value1');
37
- });
38
- });
39
-
40
- describe('delete', () => {
41
- it('应删除指定键', () => {
42
- provider.set('key1', 'value1');
43
- provider.delete('key1');
44
- expect(provider.get('key1')).toBeUndefined();
45
- });
46
- });
47
-
48
- describe('clear', () => {
49
- it('应清除所有缓存', () => {
50
- provider.set('key1', 'value1');
51
- provider.set('key2', 'value2');
52
- provider.clear();
53
- expect(provider.get('key1')).toBeUndefined();
54
- expect(provider.get('key2')).toBeUndefined();
55
- });
56
- });
57
-
58
- describe('deleteByPattern', () => {
59
- it('应删除匹配模式的键', () => {
60
- provider.set('user:1', 'user1');
61
- provider.set('user:2', 'user2');
62
- provider.set('order:1', 'order1');
63
-
64
- provider.deleteByPattern('user:*');
65
-
66
- expect(provider.get('user:1')).toBeUndefined();
67
- expect(provider.get('user:2')).toBeUndefined();
68
- expect(provider.get('order:1')).toBe('order1');
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
- 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 => { resolve = r; });
26
-
27
- pendingCache.set('key1', promise);
28
- expect(pendingCache.get('key1')).toBeDefined();
29
-
30
- resolve!('value');
31
- await promise;
32
- await new Promise(resolve => setTimeout(resolve, 0));
33
-
34
- expect(pendingCache.get('key1')).toBeUndefined();
35
- });
36
- });
37
-
38
- describe('delete', () => {
39
- it('应删除指定键', async () => {
40
- const promise = Promise.resolve('value');
41
- pendingCache.set('key1', promise);
42
- pendingCache.delete('key1');
43
- expect(pendingCache.get('key1')).toBeUndefined();
44
- });
45
- });
46
-
47
- describe('clear', () => {
48
- it('应清除所有 pending 请求', async () => {
49
- pendingCache.set('key1', Promise.resolve('v1'));
50
- pendingCache.set('key2', Promise.resolve('v2'));
51
- pendingCache.clear();
52
- expect(pendingCache.get('key1')).toBeUndefined();
53
- expect(pendingCache.get('key2')).toBeUndefined();
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
+ });