@taicode/common-base 4.0.0-beta.3 → 4.0.0-beta.4
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.
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
type CleanupFunction = () => unknown | Promise<unknown>;
|
|
2
|
+
/**
|
|
3
|
+
* 清理失败时的日志出口(结构性类型)。
|
|
4
|
+
*
|
|
5
|
+
* 既兼容 base 全局 logger 的可变参形式 `(...args: unknown[])`,
|
|
6
|
+
* 也兼容 pino 的 `log.error(obj, msg)`;调用方传入应用自己的 logger 后,
|
|
7
|
+
* 失败日志就不会落进 console,而是进应用日志流。
|
|
8
|
+
*/
|
|
9
|
+
export interface DisposerLogSink {
|
|
10
|
+
error(payload: unknown, message?: string): void;
|
|
11
|
+
}
|
|
12
|
+
export interface DisposerOptions {
|
|
13
|
+
/** 失败日志出口;不传则回退到 base 全局 logger */
|
|
14
|
+
log?: DisposerLogSink;
|
|
15
|
+
/** 日志标签,用于区分是哪个阶段的 disposer(缺省 `disposer`) */
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
2
18
|
export declare class Disposer {
|
|
19
|
+
private readonly opts;
|
|
3
20
|
private cleanupFunctions;
|
|
21
|
+
constructor(opts?: DisposerOptions);
|
|
4
22
|
add(cleanupFn: CleanupFunction): void;
|
|
5
23
|
dispose(): Promise<void>;
|
|
6
24
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"disposer.d.ts","sourceRoot":"","sources":["../../source/disposer/disposer.ts"],"names":[],"mappings":"AAEA,KAAK,eAAe,GAAG,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;AAEvD,qBAAa,QAAQ;
|
|
1
|
+
{"version":3,"file":"disposer.d.ts","sourceRoot":"","sources":["../../source/disposer/disposer.ts"],"names":[],"mappings":"AAEA,KAAK,eAAe,GAAG,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;AAEvD;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD;AAED,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,GAAG,CAAC,EAAE,eAAe,CAAA;IACrB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,qBAAa,QAAQ;IAGP,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,gBAAgB,CAAwB;gBAEnB,IAAI,GAAE,eAAoB;IAGvD,GAAG,CAAC,SAAS,EAAE,eAAe,GAAG,IAAI;IAK/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAgB/B"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { logger } from '../logger/index.js';
|
|
2
2
|
export class Disposer {
|
|
3
|
+
opts;
|
|
3
4
|
cleanupFunctions = [];
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
this.opts = opts;
|
|
7
|
+
}
|
|
4
8
|
// 添加清理函数
|
|
5
9
|
add(cleanupFn) {
|
|
6
10
|
this.cleanupFunctions.push(cleanupFn);
|
|
@@ -15,7 +19,10 @@ export class Disposer {
|
|
|
15
19
|
await cleanupFn();
|
|
16
20
|
}
|
|
17
21
|
catch (error) {
|
|
18
|
-
|
|
22
|
+
// 单个清理失败不阻断后续,也不向调用方抛出
|
|
23
|
+
const label = this.opts.label ?? 'disposer';
|
|
24
|
+
const sink = this.opts.log ?? logger;
|
|
25
|
+
sink.error({ error }, `[${label}] cleanup failed`);
|
|
19
26
|
}
|
|
20
27
|
}
|
|
21
28
|
}
|
|
@@ -98,7 +98,7 @@ describe('Disposer', () => {
|
|
|
98
98
|
expect(errorFn).toHaveBeenCalled();
|
|
99
99
|
expect(workingFn2).toHaveBeenCalled();
|
|
100
100
|
// 错误应该被记录
|
|
101
|
-
expect(loggerErrorSpy).toHaveBeenCalledWith(
|
|
101
|
+
expect(loggerErrorSpy).toHaveBeenCalledWith({ error: expect.any(Error) }, '[disposer] cleanup failed');
|
|
102
102
|
loggerErrorSpy.mockRestore();
|
|
103
103
|
});
|
|
104
104
|
it('应该在执行后清空清理函数列表', async () => {
|
|
@@ -132,6 +132,54 @@ describe('Disposer', () => {
|
|
|
132
132
|
expect(cleanupFn).toHaveBeenCalledOnce();
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
|
+
describe('日志注入', () => {
|
|
136
|
+
it('应该在传入 log 时使用注入的日志出口,不再走 base logger', async () => {
|
|
137
|
+
const loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => { });
|
|
138
|
+
const logError = vi.fn();
|
|
139
|
+
const error = new Error('Injected log error');
|
|
140
|
+
const injected = new Disposer({ log: { error: logError }, label: 'resources' });
|
|
141
|
+
injected.add(() => {
|
|
142
|
+
throw error;
|
|
143
|
+
});
|
|
144
|
+
await expect(injected.dispose()).resolves.toBeUndefined();
|
|
145
|
+
expect(logError).toHaveBeenCalledOnce();
|
|
146
|
+
expect(logError).toHaveBeenCalledWith({ error }, '[resources] cleanup failed');
|
|
147
|
+
expect(loggerErrorSpy).not.toHaveBeenCalled();
|
|
148
|
+
loggerErrorSpy.mockRestore();
|
|
149
|
+
});
|
|
150
|
+
it('标签应该出现在消息里(注入 log 且自定义 label)', async () => {
|
|
151
|
+
const logError = vi.fn();
|
|
152
|
+
const injected = new Disposer({ log: { error: logError }, label: 'middlewares' });
|
|
153
|
+
injected.add(() => {
|
|
154
|
+
throw new Error('boom');
|
|
155
|
+
});
|
|
156
|
+
await injected.dispose();
|
|
157
|
+
expect(logError).toHaveBeenCalledWith({ error: expect.any(Error) }, '[middlewares] cleanup failed');
|
|
158
|
+
});
|
|
159
|
+
it('未传 log 时应该回退到 base logger', async () => {
|
|
160
|
+
const loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => { });
|
|
161
|
+
const error = new Error('Fallback error');
|
|
162
|
+
const noLog = new Disposer({ label: 'routers' });
|
|
163
|
+
noLog.add(() => {
|
|
164
|
+
throw error;
|
|
165
|
+
});
|
|
166
|
+
await noLog.dispose();
|
|
167
|
+
expect(loggerErrorSpy).toHaveBeenCalledWith({ error }, '[routers] cleanup failed');
|
|
168
|
+
loggerErrorSpy.mockRestore();
|
|
169
|
+
});
|
|
170
|
+
it('传入 log 时单个失败仍不阻断后续,且全部走注入出口', async () => {
|
|
171
|
+
const logError = vi.fn();
|
|
172
|
+
const first = vi.fn();
|
|
173
|
+
const injected = new Disposer({ log: { error: logError }, label: 'services' });
|
|
174
|
+
injected.add(first);
|
|
175
|
+
injected.add(() => {
|
|
176
|
+
throw new Error('boom');
|
|
177
|
+
});
|
|
178
|
+
await injected.dispose();
|
|
179
|
+
expect(first).toHaveBeenCalledOnce();
|
|
180
|
+
expect(logError).toHaveBeenCalledOnce();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
135
183
|
describe('实际使用场景', () => {
|
|
136
184
|
it('应该能够清理事件监听器', async () => {
|
|
137
185
|
const removeEventListener = vi.fn();
|
|
@@ -185,7 +233,7 @@ describe('Disposer', () => {
|
|
|
185
233
|
throw error;
|
|
186
234
|
});
|
|
187
235
|
await disposer.dispose();
|
|
188
|
-
expect(loggerErrorSpy).toHaveBeenCalledWith('
|
|
236
|
+
expect(loggerErrorSpy).toHaveBeenCalledWith({ error }, '[disposer] cleanup failed');
|
|
189
237
|
loggerErrorSpy.mockRestore();
|
|
190
238
|
});
|
|
191
239
|
it('应该处理不同类型的错误', async () => {
|
|
@@ -210,7 +258,7 @@ describe('Disposer', () => {
|
|
|
210
258
|
throw asyncError;
|
|
211
259
|
});
|
|
212
260
|
await disposer.dispose();
|
|
213
|
-
expect(loggerErrorSpy).toHaveBeenCalledWith('
|
|
261
|
+
expect(loggerErrorSpy).toHaveBeenCalledWith({ error: asyncError }, '[disposer] cleanup failed');
|
|
214
262
|
loggerErrorSpy.mockRestore();
|
|
215
263
|
});
|
|
216
264
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taicode/common-base",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.4",
|
|
4
4
|
"author": "Alain",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"description": "",
|
|
@@ -20,6 +20,15 @@
|
|
|
20
20
|
"default": "./output/*/index.js"
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:ci": "vitest run",
|
|
26
|
+
"lint": "eslint && tsc --noEmit",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"build": "rm -rf output && tsc -p tsconfig.json",
|
|
29
|
+
"prepublishOnly": "npm run build",
|
|
30
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
31
|
+
},
|
|
23
32
|
"files": [
|
|
24
33
|
"output"
|
|
25
34
|
],
|
|
@@ -33,13 +42,5 @@
|
|
|
33
42
|
"mobx": "^6.15.0",
|
|
34
43
|
"typescript-eslint": "^8.56.1",
|
|
35
44
|
"vitest": "^3.2.4"
|
|
36
|
-
},
|
|
37
|
-
"scripts": {
|
|
38
|
-
"test": "vitest run",
|
|
39
|
-
"test:ci": "vitest run",
|
|
40
|
-
"lint": "eslint && tsc --noEmit",
|
|
41
|
-
"typecheck": "tsc --noEmit",
|
|
42
|
-
"build": "tsc -p tsconfig.json",
|
|
43
|
-
"dev": "tsc -p tsconfig.json --watch"
|
|
44
45
|
}
|
|
45
|
-
}
|
|
46
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 TaicodeLabs
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|