@jintianxiayu/cache-decorator 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -11
- package/README.md +364 -270
- package/dist/core/cache-error.d.ts +75 -0
- package/dist/core/cache-error.d.ts.map +1 -0
- package/dist/core/cache-error.js +168 -0
- package/dist/core/cache-error.js.map +1 -0
- package/dist/core/cache-logger.d.ts +3 -2
- package/dist/core/cache-logger.d.ts.map +1 -1
- package/dist/core/cache-logger.js +2 -0
- package/dist/core/cache-logger.js.map +1 -1
- package/dist/decorators/cache-evict.d.ts.map +1 -1
- package/dist/decorators/cache-evict.js +15 -10
- package/dist/decorators/cache-evict.js.map +1 -1
- package/dist/decorators/cache.d.ts +6 -0
- package/dist/decorators/cache.d.ts.map +1 -1
- package/dist/decorators/cache.js +113 -36
- package/dist/decorators/cache.js.map +1 -1
- package/jest.config.js +11 -11
- package/package.json +1 -1
- package/src/adapters/ioredis-cache-client.ts +89 -89
- package/src/adapters/node-redis-cache-client.ts +95 -95
- package/src/adapters/redis-key-prefix.ts +38 -38
- package/src/core/cache-error.ts +233 -0
- package/src/core/cache-logger.ts +116 -105
- package/src/core/key-builder.ts +33 -33
- package/src/core/native-cache.ts +55 -55
- package/src/core/pending-cache.ts +29 -29
- package/src/core/redis-cache-client.ts +160 -160
- package/src/core/redis-cache.ts +104 -104
- package/src/decorators/cache-evict.ts +137 -129
- package/src/decorators/cache.ts +323 -203
- package/src/index.ts +11 -11
- package/test/cache-evict-logging.test.ts +398 -362
- package/test/cache-logger.integration.test.ts +159 -129
- package/test/cache-logger.test.ts +156 -153
- package/test/cache-logging.test.ts +929 -544
- package/test/cache.test.ts +1017 -255
- package/test/fixtures/cache-logger-child.mjs +108 -108
- package/test/fixtures/cache-provider-failure-child.mjs +70 -0
- package/test/helpers/legacy-redis-cache.ts +41 -22
- package/test/helpers/package-consumer.ts +231 -231
- package/test/helpers/redis-fixture.ts +142 -142
- package/test/ioredis-cache-client.test.ts +143 -143
- package/test/legacy-redis-cache.test.ts +51 -0
- package/test/native-cache.test.ts +77 -77
- package/test/node-redis-cache-client.test.ts +149 -149
- package/test/pending-cache.test.ts +69 -69
- package/test/redis-cache-client-lifecycle.test.ts +112 -112
- package/test/redis-cache-client-types.test.ts +184 -184
- package/test/redis-cache-client.integration.test.ts +355 -327
- package/test/redis-cache-decorator.test.ts +601 -201
- package/test/redis-cache-provider.test.ts +269 -269
- package/test/type-contract/contract.ts +119 -64
- package/test/type-contract/tsconfig.json +12 -12
- package/tsconfig.json +8 -8
|
@@ -1,112 +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
|
-
});
|
|
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
|
+
});
|
|
@@ -1,184 +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
|
-
});
|
|
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
|
+
});
|