@jintianxiayu/cache-decorator 0.1.3 → 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 (86) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +270 -131
  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 -11
  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/key-builder.ts +33 -31
  58. package/src/core/native-cache.ts +55 -53
  59. package/src/core/pending-cache.ts +29 -26
  60. package/src/core/redis-cache-client.ts +160 -0
  61. package/src/core/redis-cache.ts +104 -56
  62. package/src/decorators/cache-evict.ts +129 -69
  63. package/src/decorators/cache.ts +203 -125
  64. package/src/index.ts +11 -8
  65. package/test/cache-evict-logging.test.ts +362 -0
  66. package/test/cache-logger.integration.test.ts +129 -0
  67. package/test/cache-logger.test.ts +153 -0
  68. package/test/cache-logging.test.ts +544 -0
  69. package/test/cache.test.ts +255 -256
  70. package/test/fixtures/cache-logger-child.mjs +108 -0
  71. package/test/helpers/legacy-redis-cache.ts +22 -0
  72. package/test/helpers/package-consumer.ts +231 -0
  73. package/test/helpers/redis-fixture.ts +142 -0
  74. package/test/ioredis-cache-client.test.ts +143 -0
  75. package/test/native-cache.test.ts +77 -71
  76. package/test/node-redis-cache-client.test.ts +149 -0
  77. package/test/pending-cache.test.ts +69 -58
  78. package/test/redis-cache-client-lifecycle.test.ts +112 -0
  79. package/test/redis-cache-client-types.test.ts +184 -0
  80. package/test/redis-cache-client.integration.test.ts +327 -0
  81. package/test/redis-cache-decorator.test.ts +201 -0
  82. package/test/redis-cache-package.integration.test.ts +328 -0
  83. package/test/redis-cache-provider.test.ts +269 -0
  84. package/test/type-contract/contract.ts +64 -0
  85. package/test/type-contract/tsconfig.json +12 -0
  86. package/tsconfig.json +8 -8
@@ -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,58 +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) => {
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
-
40
- describe('delete', () => {
41
- it('应删除指定键', async () => {
42
- const promise = Promise.resolve('value');
43
- pendingCache.set('key1', promise);
44
- pendingCache.delete('key1');
45
- expect(pendingCache.get('key1')).toBeUndefined();
46
- });
47
- });
48
-
49
- describe('clear', () => {
50
- it('应清除所有 pending 请求', async () => {
51
- pendingCache.set('key1', Promise.resolve('v1'));
52
- pendingCache.set('key2', Promise.resolve('v2'));
53
- pendingCache.clear();
54
- expect(pendingCache.get('key1')).toBeUndefined();
55
- expect(pendingCache.get('key2')).toBeUndefined();
56
- });
57
- });
58
- });
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
+ });
@@ -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
+ });