@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
package/test/cache-evict.test.ts
CHANGED
|
@@ -4,175 +4,175 @@ import { CacheProviderRegistry } from '../src/core/cache-provider-registry';
|
|
|
4
4
|
import { MemoryCacheProvider } from '../src/core/native-cache';
|
|
5
5
|
|
|
6
6
|
describe('@CacheEvict 装饰器', () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
memoryProvider.clear();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('按入参清除', () => {
|
|
20
|
-
it('应清除对应缓存条目', async () => {
|
|
21
|
-
let getUserCallCount = 0;
|
|
22
|
-
let updateUserCallCount = 0;
|
|
23
|
-
|
|
24
|
-
class UserService {
|
|
25
|
-
@Cache('user-cache')
|
|
26
|
-
async getUser(id: number) {
|
|
27
|
-
getUserCallCount++;
|
|
28
|
-
return { id };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
@CacheEvict('user-cache')
|
|
32
|
-
async updateUser(id: number) {
|
|
33
|
-
updateUserCallCount++;
|
|
34
|
-
return { id, updated: true };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const service = new UserService();
|
|
39
|
-
await service.getUser(1);
|
|
40
|
-
await service.getUser(1);
|
|
41
|
-
expect(getUserCallCount).toBe(1);
|
|
42
|
-
|
|
43
|
-
await service.updateUser(1);
|
|
44
|
-
await service.getUser(1);
|
|
45
|
-
expect(getUserCallCount).toBe(2);
|
|
46
|
-
expect(updateUserCallCount).toBe(1);
|
|
7
|
+
let memoryProvider: MemoryCacheProvider;
|
|
8
|
+
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
memoryProvider = new MemoryCacheProvider();
|
|
11
|
+
CacheProviderRegistry.register('memory', memoryProvider);
|
|
12
|
+
CacheProviderRegistry.setDefault('memory');
|
|
47
13
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
it('应清除该缓存名称下所有条目', async () => {
|
|
52
|
-
let getUserCallCount = 0;
|
|
53
|
-
let getOrderCallCount = 0;
|
|
54
|
-
let clearCallCount = 0;
|
|
55
|
-
|
|
56
|
-
class Service {
|
|
57
|
-
@Cache('user-cache')
|
|
58
|
-
async getUser(id: number) {
|
|
59
|
-
getUserCallCount++;
|
|
60
|
-
return { id };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
@Cache('order-cache')
|
|
64
|
-
async getOrder(id: number) {
|
|
65
|
-
getOrderCallCount++;
|
|
66
|
-
return { id };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
@CacheEvict('user-cache', { allEntries: true })
|
|
70
|
-
async clearAllUsers() {
|
|
71
|
-
clearCallCount++;
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const service = new Service();
|
|
77
|
-
await service.getUser(1);
|
|
78
|
-
await service.getUser(2);
|
|
79
|
-
await service.getOrder(1);
|
|
80
|
-
expect(getUserCallCount).toBe(2);
|
|
81
|
-
expect(getOrderCallCount).toBe(1);
|
|
82
|
-
|
|
83
|
-
await service.clearAllUsers();
|
|
84
|
-
expect(clearCallCount).toBe(1);
|
|
85
|
-
|
|
86
|
-
await service.getUser(1);
|
|
87
|
-
await service.getUser(2);
|
|
88
|
-
await service.getOrder(1);
|
|
89
|
-
expect(getUserCallCount).toBe(4);
|
|
90
|
-
expect(getOrderCallCount).toBe(1);
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
memoryProvider.clear();
|
|
91
17
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
18
|
+
|
|
19
|
+
describe('按入参清除', () => {
|
|
20
|
+
it('应清除对应缓存条目', async () => {
|
|
21
|
+
let getUserCallCount = 0;
|
|
22
|
+
let updateUserCallCount = 0;
|
|
23
|
+
|
|
24
|
+
class UserService {
|
|
25
|
+
@Cache('user-cache')
|
|
26
|
+
async getUser(id: number) {
|
|
27
|
+
getUserCallCount++;
|
|
28
|
+
return { id };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@CacheEvict('user-cache')
|
|
32
|
+
async updateUser(id: number) {
|
|
33
|
+
updateUserCallCount++;
|
|
34
|
+
return { id, updated: true };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const service = new UserService();
|
|
39
|
+
await service.getUser(1);
|
|
40
|
+
await service.getUser(1);
|
|
41
|
+
expect(getUserCallCount).toBe(1);
|
|
42
|
+
|
|
43
|
+
await service.updateUser(1);
|
|
44
|
+
await service.getUser(1);
|
|
45
|
+
expect(getUserCallCount).toBe(2);
|
|
46
|
+
expect(updateUserCallCount).toBe(1);
|
|
47
|
+
});
|
|
119
48
|
});
|
|
120
49
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
50
|
+
describe('清除所有条目', () => {
|
|
51
|
+
it('应清除该缓存名称下所有条目', async () => {
|
|
52
|
+
let getUserCallCount = 0;
|
|
53
|
+
let getOrderCallCount = 0;
|
|
54
|
+
let clearCallCount = 0;
|
|
55
|
+
|
|
56
|
+
class Service {
|
|
57
|
+
@Cache('user-cache')
|
|
58
|
+
async getUser(id: number) {
|
|
59
|
+
getUserCallCount++;
|
|
60
|
+
return { id };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Cache('order-cache')
|
|
64
|
+
async getOrder(id: number) {
|
|
65
|
+
getOrderCallCount++;
|
|
66
|
+
return { id };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@CacheEvict('user-cache', { allEntries: true })
|
|
70
|
+
async clearAllUsers() {
|
|
71
|
+
clearCallCount++;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const service = new Service();
|
|
77
|
+
await service.getUser(1);
|
|
78
|
+
await service.getUser(2);
|
|
79
|
+
await service.getOrder(1);
|
|
80
|
+
expect(getUserCallCount).toBe(2);
|
|
81
|
+
expect(getOrderCallCount).toBe(1);
|
|
82
|
+
|
|
83
|
+
await service.clearAllUsers();
|
|
84
|
+
expect(clearCallCount).toBe(1);
|
|
85
|
+
|
|
86
|
+
await service.getUser(1);
|
|
87
|
+
await service.getUser(2);
|
|
88
|
+
await service.getOrder(1);
|
|
89
|
+
expect(getUserCallCount).toBe(4);
|
|
90
|
+
expect(getOrderCallCount).toBe(1);
|
|
91
|
+
});
|
|
146
92
|
});
|
|
147
93
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
94
|
+
describe('key 选项', () => {
|
|
95
|
+
it('key 为字符串时应删除对应缓存条目', async () => {
|
|
96
|
+
let getUserCallCount = 0;
|
|
97
|
+
|
|
98
|
+
class UserService {
|
|
99
|
+
@Cache('user-cache', { key: 'all-users' })
|
|
100
|
+
async getUsers() {
|
|
101
|
+
getUserCallCount++;
|
|
102
|
+
return [{ id: 1 }, { id: 2 }];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@CacheEvict('user-cache', { key: 'all-users' })
|
|
106
|
+
async clearUsers() {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const service = new UserService();
|
|
112
|
+
await service.getUsers();
|
|
113
|
+
await service.getUsers();
|
|
114
|
+
expect(getUserCallCount).toBe(1);
|
|
115
|
+
|
|
116
|
+
await service.clearUsers();
|
|
117
|
+
await service.getUsers();
|
|
118
|
+
expect(getUserCallCount).toBe(2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('key 为函数时应使用函数返回值删除缓存条目', async () => {
|
|
122
|
+
let getUserCallCount = 0;
|
|
123
|
+
|
|
124
|
+
class UserService {
|
|
125
|
+
@Cache('user-cache', { key: (...args: unknown[]) => String(args[0]) })
|
|
126
|
+
async getUser(id: number) {
|
|
127
|
+
getUserCallCount++;
|
|
128
|
+
return { id };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@CacheEvict('user-cache', { key: (...args: unknown[]) => String(args[0]) })
|
|
132
|
+
async deleteUser(id: number) {
|
|
133
|
+
return { id, deleted: true };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const service = new UserService();
|
|
138
|
+
await service.getUser(1);
|
|
139
|
+
await service.getUser(2);
|
|
140
|
+
expect(getUserCallCount).toBe(2);
|
|
141
|
+
|
|
142
|
+
await service.deleteUser(1);
|
|
143
|
+
await service.getUser(1);
|
|
144
|
+
await service.getUser(2);
|
|
145
|
+
expect(getUserCallCount).toBe(3);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('allEntries 为 true 时忽略 key 选项', async () => {
|
|
149
|
+
let getUserCallCount = 0;
|
|
150
|
+
let clearCallCount = 0;
|
|
151
|
+
|
|
152
|
+
class UserService {
|
|
153
|
+
@Cache('user-cache', { key: 'specific-key' })
|
|
154
|
+
async getUsers() {
|
|
155
|
+
getUserCallCount++;
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@CacheEvict('user-cache', { allEntries: true, key: 'ignored-key' })
|
|
160
|
+
async clearAllUsers() {
|
|
161
|
+
clearCallCount++;
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const service = new UserService();
|
|
167
|
+
await service.getUsers();
|
|
168
|
+
await service.getUsers();
|
|
169
|
+
expect(getUserCallCount).toBe(1);
|
|
170
|
+
|
|
171
|
+
await service.clearAllUsers();
|
|
172
|
+
expect(clearCallCount).toBe(1);
|
|
173
|
+
|
|
174
|
+
await service.getUsers();
|
|
175
|
+
expect(getUserCallCount).toBe(2);
|
|
176
|
+
});
|
|
176
177
|
});
|
|
177
|
-
|
|
178
|
-
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { runCommand } from './helpers/package-consumer';
|
|
5
|
+
|
|
6
|
+
jest.setTimeout(30_000);
|
|
7
|
+
|
|
8
|
+
interface CacheLogMetadata {
|
|
9
|
+
readonly event: string;
|
|
10
|
+
readonly cacheName: string;
|
|
11
|
+
readonly methodName: string;
|
|
12
|
+
readonly providerName: string;
|
|
13
|
+
readonly entryType?: string;
|
|
14
|
+
readonly operation?: string;
|
|
15
|
+
readonly error?: {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly password: string;
|
|
19
|
+
readonly email: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface JsonLogEvent {
|
|
24
|
+
readonly level: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly traceId: string;
|
|
28
|
+
readonly meta: CacheLogMetadata;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ChildResult {
|
|
32
|
+
readonly code: number | null;
|
|
33
|
+
readonly stdout: string;
|
|
34
|
+
readonly stderr: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const packageRoot = resolve(__dirname, '..');
|
|
38
|
+
const loggerRoot = resolve(packageRoot, '../logger');
|
|
39
|
+
const fixture = join(__dirname, 'fixtures', 'cache-logger-child.mjs');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 在隔离进程运行真实 Logger fixture,确保关闭流程完整刷新结构化输出。
|
|
43
|
+
* @param profileLevel cache 命名 profile 的筛选级别。
|
|
44
|
+
* @returns 子进程退出码和完整标准流。
|
|
45
|
+
* @throws 子进程无法启动时拒绝。
|
|
46
|
+
*/
|
|
47
|
+
function runFixture(profileLevel: 'debug' | 'error'): Promise<ChildResult> {
|
|
48
|
+
return new Promise((resolveResult, reject) => {
|
|
49
|
+
const child = spawn(process.execPath, [fixture, profileLevel], {
|
|
50
|
+
cwd: packageRoot,
|
|
51
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
52
|
+
windowsHide: true,
|
|
53
|
+
});
|
|
54
|
+
let stdout = '';
|
|
55
|
+
let stderr = '';
|
|
56
|
+
child.stdout.setEncoding('utf8');
|
|
57
|
+
child.stderr.setEncoding('utf8');
|
|
58
|
+
child.stdout.on('data', (chunk: string) => (stdout += chunk));
|
|
59
|
+
child.stderr.on('data', (chunk: string) => (stderr += chunk));
|
|
60
|
+
child.once('error', reject);
|
|
61
|
+
child.once('exit', (code) => resolveResult({ code, stdout, stderr }));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseEvents(output: string): JsonLogEvent[] {
|
|
66
|
+
return output
|
|
67
|
+
.split(/\r?\n/)
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.map((line) => JSON.parse(line) as JsonLogEvent);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
beforeAll(() => {
|
|
73
|
+
const compiler = require.resolve('typescript/bin/tsc');
|
|
74
|
+
runCommand({ executable: process.execPath, args: [compiler, '-p', loggerRoot], cwd: loggerRoot });
|
|
75
|
+
runCommand({ executable: process.execPath, args: [compiler, '-p', packageRoot], cwd: packageRoot });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('cache-operation-logging/M03 cache-operation-logging/M04 使用真实命名 Logger 脱敏并关联 traceId', async () => {
|
|
79
|
+
const result = await runFixture('debug');
|
|
80
|
+
expect(result.code).toBe(0);
|
|
81
|
+
expect(result.stderr).toBe('');
|
|
82
|
+
expect(result.stdout).not.toContain('provider-password-secret');
|
|
83
|
+
expect(result.stdout).not.toContain('provider@example.com');
|
|
84
|
+
expect(result.stdout).not.toContain('business-value-secret');
|
|
85
|
+
|
|
86
|
+
const output = parseEvents(result.stdout);
|
|
87
|
+
expect(output.map(({ level, meta }) => `${level}:${meta.event}`)).toEqual([
|
|
88
|
+
'debug:cache.miss',
|
|
89
|
+
'debug:cache.write_dispatched',
|
|
90
|
+
'error:cache.operation_failed',
|
|
91
|
+
]);
|
|
92
|
+
expect(output.every(({ name }) => name === '@jintianxiayu/cache-decorator')).toBe(true);
|
|
93
|
+
expect(output.every(({ traceId }) => traceId === 'cache-fixture-trace')).toBe(true);
|
|
94
|
+
expect(output.every(({ meta }) => !('traceId' in meta))).toBe(true);
|
|
95
|
+
expect(output[2]?.meta).toMatchObject({
|
|
96
|
+
event: 'cache.operation_failed',
|
|
97
|
+
cacheName: 'fixture-failure-users',
|
|
98
|
+
methodName: 'getUser',
|
|
99
|
+
providerName: 'failing',
|
|
100
|
+
operation: 'read',
|
|
101
|
+
error: {
|
|
102
|
+
name: 'Error',
|
|
103
|
+
message: 'redis unavailable',
|
|
104
|
+
password: '********',
|
|
105
|
+
email: 'pr***@example.com',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('cache-operation-logging/Logger 配置筛选输出且复用应用提供的唯一 Logger runtime', async () => {
|
|
111
|
+
const result = await runFixture('error');
|
|
112
|
+
expect(result.code).toBe(0);
|
|
113
|
+
expect(result.stderr).toBe('');
|
|
114
|
+
const output = parseEvents(result.stdout);
|
|
115
|
+
expect(output).toHaveLength(1);
|
|
116
|
+
expect(output[0]).toMatchObject({
|
|
117
|
+
level: 'error',
|
|
118
|
+
name: '@jintianxiayu/cache-decorator',
|
|
119
|
+
traceId: 'cache-fixture-trace',
|
|
120
|
+
meta: { event: 'cache.operation_failed', operation: 'read' },
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('cache decorator 源码不读取或写入 LoggerContext', () => {
|
|
125
|
+
const sources = ['src/core/cache-logger.ts', 'src/decorators/cache.ts', 'src/decorators/cache-evict.ts']
|
|
126
|
+
.map((file) => readFileSync(join(packageRoot, file), 'utf8'))
|
|
127
|
+
.join('\n');
|
|
128
|
+
expect(sources).not.toContain('LoggerContext');
|
|
129
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { LoggerInterface } from '@jintianxiayu/logger';
|
|
2
|
+
|
|
3
|
+
type CacheLoggerModule = typeof import('../src/core/cache-logger');
|
|
4
|
+
|
|
5
|
+
interface CacheLoggerHarness {
|
|
6
|
+
readonly cacheLogger: CacheLoggerModule;
|
|
7
|
+
readonly getLogger: jest.Mock<LoggerInterface, [string]>;
|
|
8
|
+
readonly logger: jest.Mocked<LoggerInterface>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const logContext = {
|
|
12
|
+
cacheName: 'users',
|
|
13
|
+
methodName: 'findUser',
|
|
14
|
+
providerName: 'default',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
function createMockLogger(): jest.Mocked<LoggerInterface> {
|
|
18
|
+
return {
|
|
19
|
+
debug: jest.fn(),
|
|
20
|
+
info: jest.fn(),
|
|
21
|
+
warn: jest.fn(),
|
|
22
|
+
error: jest.fn(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 为每个用例重新加载 cache Logger 模块,隔离模块级 Logger 缓存并允许注入获取失败。
|
|
28
|
+
* @param getLoggerImplementation 可选的 LoggerFactory.getLogger 替代实现。
|
|
29
|
+
* @returns 独立模块、Logger 获取 mock 和四个 level 方法 mock。
|
|
30
|
+
* @throws 隔离模块未能同步加载时抛出。
|
|
31
|
+
*/
|
|
32
|
+
function loadCacheLogger(getLoggerImplementation?: (name: string) => LoggerInterface): CacheLoggerHarness {
|
|
33
|
+
jest.resetModules();
|
|
34
|
+
const logger = createMockLogger();
|
|
35
|
+
const getLogger = jest.fn<LoggerInterface, [string]>(getLoggerImplementation ?? (() => logger));
|
|
36
|
+
jest.doMock('@jintianxiayu/logger', () => ({ LoggerFactory: { getLogger } }));
|
|
37
|
+
let cacheLogger: CacheLoggerModule | undefined;
|
|
38
|
+
jest.isolateModules(() => {
|
|
39
|
+
cacheLogger = jest.requireActual<CacheLoggerModule>('../src/core/cache-logger');
|
|
40
|
+
});
|
|
41
|
+
if (!cacheLogger) {
|
|
42
|
+
throw new Error('Cache logger module was not loaded');
|
|
43
|
+
}
|
|
44
|
+
return { cacheLogger, getLogger, logger };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
jest.resetModules();
|
|
49
|
+
jest.dontMock('@jintianxiayu/logger');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('cache-operation-logging/L01 导入和 decorator 求值不初始化 Logger', () => {
|
|
53
|
+
const { getLogger } = loadCacheLogger();
|
|
54
|
+
const { Cache } = jest.requireActual<typeof import('../src/decorators/cache')>('../src/decorators/cache');
|
|
55
|
+
const { CacheEvict } = jest.requireActual<typeof import('../src/decorators/cache-evict')>(
|
|
56
|
+
'../src/decorators/cache-evict'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
Cache('users');
|
|
60
|
+
CacheEvict('users');
|
|
61
|
+
|
|
62
|
+
expect(getLogger).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('cache-operation-logging/L02 首次运行期事件获取并复用应用命名 Logger', () => {
|
|
66
|
+
const { cacheLogger, getLogger, logger } = loadCacheLogger();
|
|
67
|
+
|
|
68
|
+
cacheLogger.logCacheEvent('cache.miss', logContext);
|
|
69
|
+
cacheLogger.logCacheEvent('cache.hit', { ...logContext, entryType: 'value' });
|
|
70
|
+
|
|
71
|
+
expect(getLogger).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(getLogger).toHaveBeenCalledWith('@jintianxiayu/cache-decorator');
|
|
73
|
+
expect(logger.debug).toHaveBeenCalledTimes(2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('cache-operation-logging/正常缓存决策使用 debug', () => {
|
|
77
|
+
const { cacheLogger, logger } = loadCacheLogger();
|
|
78
|
+
const events = [
|
|
79
|
+
'cache.pending_hit',
|
|
80
|
+
'cache.hit',
|
|
81
|
+
'cache.miss',
|
|
82
|
+
'cache.write_dispatched',
|
|
83
|
+
'cache.evict_dispatched',
|
|
84
|
+
'cache.evict_completed',
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
87
|
+
for (const event of events) {
|
|
88
|
+
cacheLogger.logCacheEvent(event, logContext);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
expect(logger.debug).toHaveBeenCalledTimes(events.length);
|
|
92
|
+
expect(logger.debug.mock.calls.map((call) => (call[1] as { event: string }).event)).toEqual(events);
|
|
93
|
+
expect(logger.info).not.toHaveBeenCalled();
|
|
94
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
95
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('cache-operation-logging/可恢复回退与淘汰跳过使用 warn', () => {
|
|
99
|
+
const { cacheLogger, logger } = loadCacheLogger();
|
|
100
|
+
|
|
101
|
+
cacheLogger.logCacheEvent('cache.key_fallback', { ...logContext, reason: 'resolver_error' });
|
|
102
|
+
cacheLogger.logCacheEvent('cache.evict_skipped', { ...logContext, reason: 'business_error' });
|
|
103
|
+
|
|
104
|
+
expect(logger.warn).toHaveBeenCalledTimes(2);
|
|
105
|
+
expect(logger.warn.mock.calls.map((call) => (call[1] as { event: string }).event)).toEqual([
|
|
106
|
+
'cache.key_fallback',
|
|
107
|
+
'cache.evict_skipped',
|
|
108
|
+
]);
|
|
109
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
110
|
+
expect(logger.info).not.toHaveBeenCalled();
|
|
111
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('cache-operation-logging/缓存基础设施失败使用 error', () => {
|
|
115
|
+
const { cacheLogger, logger } = loadCacheLogger();
|
|
116
|
+
const providerError = new Error('redis unavailable');
|
|
117
|
+
|
|
118
|
+
cacheLogger.logCacheEvent('cache.operation_failed', {
|
|
119
|
+
...logContext,
|
|
120
|
+
operation: 'read',
|
|
121
|
+
error: providerError,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
125
|
+
'Cache operation failed',
|
|
126
|
+
expect.objectContaining({ event: 'cache.operation_failed', operation: 'read', error: providerError })
|
|
127
|
+
);
|
|
128
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
129
|
+
expect(logger.info).not.toHaveBeenCalled();
|
|
130
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('Logger 获取或写入同步失败时不向缓存调用方抛出', () => {
|
|
134
|
+
const getLoggerError = new Error('invalid logger config');
|
|
135
|
+
const failedGet = loadCacheLogger(() => {
|
|
136
|
+
throw getLoggerError;
|
|
137
|
+
});
|
|
138
|
+
expect(() => failedGet.cacheLogger.logCacheEvent('cache.miss', logContext)).not.toThrow();
|
|
139
|
+
|
|
140
|
+
const failedWrite = loadCacheLogger();
|
|
141
|
+
failedWrite.logger.debug.mockImplementationOnce(() => {
|
|
142
|
+
throw new Error('transport failed');
|
|
143
|
+
});
|
|
144
|
+
expect(() => failedWrite.cacheLogger.logCacheEvent('cache.miss', logContext)).not.toThrow();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('Provider 日志标签将缺省和空名称稳定映射为 default', () => {
|
|
148
|
+
const { cacheLogger } = loadCacheLogger();
|
|
149
|
+
|
|
150
|
+
expect(cacheLogger.cacheProviderLabel(undefined)).toBe('default');
|
|
151
|
+
expect(cacheLogger.cacheProviderLabel('')).toBe('default');
|
|
152
|
+
expect(cacheLogger.cacheProviderLabel('redis-primary')).toBe('redis-primary');
|
|
153
|
+
});
|