@taicode/common-base 3.8.0 → 4.0.0-beta.10
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/output/disposer/disposer.d.ts +19 -1
- package/output/disposer/disposer.d.ts.map +1 -1
- package/output/disposer/disposer.js +12 -4
- package/output/disposer/disposer.test.js +95 -46
- package/output/object/object.d.ts +0 -2
- package/output/object/object.d.ts.map +1 -1
- package/output/object/object.js +0 -6
- package/package.json +2 -2
|
@@ -1,7 +1,25 @@
|
|
|
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;
|
|
4
|
-
|
|
21
|
+
constructor(opts?: DisposerOptions);
|
|
22
|
+
add(cleanupFn: CleanupFunction): void;
|
|
5
23
|
dispose(): Promise<void>;
|
|
6
24
|
}
|
|
7
25
|
export {};
|
|
@@ -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,20 +1,28 @@
|
|
|
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);
|
|
7
11
|
}
|
|
8
|
-
//
|
|
12
|
+
// 执行所有清理函数(LIFO:后注册的先执行)
|
|
9
13
|
async dispose() {
|
|
10
14
|
const cleanupFunctions = this.cleanupFunctions;
|
|
11
15
|
this.cleanupFunctions = [];
|
|
12
|
-
|
|
16
|
+
// 后创建的东西依赖先创建的,必须**后**销毁:逆注册序执行
|
|
17
|
+
for (const cleanupFn of [...cleanupFunctions].reverse()) {
|
|
13
18
|
try {
|
|
14
19
|
await cleanupFn();
|
|
15
20
|
}
|
|
16
21
|
catch (error) {
|
|
17
|
-
|
|
22
|
+
// 单个清理失败不阻断后续,也不向调用方抛出
|
|
23
|
+
const label = this.opts.label ?? 'disposer';
|
|
24
|
+
const sink = this.opts.log ?? logger;
|
|
25
|
+
sink.error({ error }, `[${label}] cleanup failed`);
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
}
|
|
@@ -6,20 +6,20 @@ describe('Disposer', () => {
|
|
|
6
6
|
beforeEach(() => {
|
|
7
7
|
disposer = new Disposer();
|
|
8
8
|
});
|
|
9
|
-
describe('
|
|
9
|
+
describe('add', () => {
|
|
10
10
|
it('应该能够添加清理函数', () => {
|
|
11
11
|
const cleanupFn = vi.fn();
|
|
12
12
|
expect(() => {
|
|
13
|
-
disposer.
|
|
13
|
+
disposer.add(cleanupFn);
|
|
14
14
|
}).not.toThrow();
|
|
15
15
|
});
|
|
16
16
|
it('应该能够添加多个清理函数', async () => {
|
|
17
17
|
const cleanupFn1 = vi.fn();
|
|
18
18
|
const cleanupFn2 = vi.fn();
|
|
19
19
|
const cleanupFn3 = vi.fn();
|
|
20
|
-
disposer.
|
|
21
|
-
disposer.
|
|
22
|
-
disposer.
|
|
20
|
+
disposer.add(cleanupFn1);
|
|
21
|
+
disposer.add(cleanupFn2);
|
|
22
|
+
disposer.add(cleanupFn3);
|
|
23
23
|
await disposer.dispose();
|
|
24
24
|
expect(cleanupFn1).toHaveBeenCalledOnce();
|
|
25
25
|
expect(cleanupFn2).toHaveBeenCalledOnce();
|
|
@@ -30,10 +30,10 @@ describe('Disposer', () => {
|
|
|
30
30
|
const stringFn = vi.fn(() => 'cleanup result');
|
|
31
31
|
const numberFn = vi.fn(() => 42);
|
|
32
32
|
const promiseFn = vi.fn(() => Promise.resolve('async cleanup'));
|
|
33
|
-
disposer.
|
|
34
|
-
disposer.
|
|
35
|
-
disposer.
|
|
36
|
-
disposer.
|
|
33
|
+
disposer.add(voidFn);
|
|
34
|
+
disposer.add(stringFn);
|
|
35
|
+
disposer.add(numberFn);
|
|
36
|
+
disposer.add(promiseFn);
|
|
37
37
|
await disposer.dispose();
|
|
38
38
|
expect(voidFn).toHaveBeenCalled();
|
|
39
39
|
expect(stringFn).toHaveBeenCalled();
|
|
@@ -46,38 +46,39 @@ describe('Disposer', () => {
|
|
|
46
46
|
const cleanupFn1 = vi.fn();
|
|
47
47
|
const cleanupFn2 = vi.fn();
|
|
48
48
|
const cleanupFn3 = vi.fn();
|
|
49
|
-
disposer.
|
|
50
|
-
disposer.
|
|
51
|
-
disposer.
|
|
49
|
+
disposer.add(cleanupFn1);
|
|
50
|
+
disposer.add(cleanupFn2);
|
|
51
|
+
disposer.add(cleanupFn3);
|
|
52
52
|
await disposer.dispose();
|
|
53
53
|
expect(cleanupFn1).toHaveBeenCalledOnce();
|
|
54
54
|
expect(cleanupFn2).toHaveBeenCalledOnce();
|
|
55
55
|
expect(cleanupFn3).toHaveBeenCalledOnce();
|
|
56
56
|
});
|
|
57
|
-
it('
|
|
57
|
+
it('应该按添加顺序的**逆序**(LIFO)执行清理函数', async () => {
|
|
58
58
|
const executionOrder = [];
|
|
59
|
-
disposer.
|
|
60
|
-
disposer.
|
|
61
|
-
disposer.
|
|
59
|
+
disposer.add(() => executionOrder.push(1));
|
|
60
|
+
disposer.add(() => executionOrder.push(2));
|
|
61
|
+
disposer.add(() => executionOrder.push(3));
|
|
62
62
|
await disposer.dispose();
|
|
63
|
-
|
|
63
|
+
// 后创建的东西依赖先创建的,必须后销毁
|
|
64
|
+
expect(executionOrder).toEqual([3, 2, 1]);
|
|
64
65
|
});
|
|
65
|
-
it('
|
|
66
|
+
it('应该按添加顺序的**逆序**(LIFO)执行异步清理函数', async () => {
|
|
66
67
|
const executionOrder = [];
|
|
67
|
-
disposer.
|
|
68
|
+
disposer.add(async () => {
|
|
68
69
|
await Promise.resolve();
|
|
69
70
|
executionOrder.push(1);
|
|
70
71
|
});
|
|
71
|
-
disposer.
|
|
72
|
+
disposer.add(async () => {
|
|
72
73
|
await Promise.resolve();
|
|
73
74
|
executionOrder.push(2);
|
|
74
75
|
});
|
|
75
|
-
disposer.
|
|
76
|
+
disposer.add(async () => {
|
|
76
77
|
await Promise.resolve();
|
|
77
78
|
executionOrder.push(3);
|
|
78
79
|
});
|
|
79
80
|
await disposer.dispose();
|
|
80
|
-
expect(executionOrder).toEqual([
|
|
81
|
+
expect(executionOrder).toEqual([3, 2, 1]);
|
|
81
82
|
});
|
|
82
83
|
it('应该处理清理函数中的错误', async () => {
|
|
83
84
|
const workingFn1 = vi.fn();
|
|
@@ -87,9 +88,9 @@ describe('Disposer', () => {
|
|
|
87
88
|
const workingFn2 = vi.fn();
|
|
88
89
|
// 模拟 logger.error
|
|
89
90
|
const loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => { });
|
|
90
|
-
disposer.
|
|
91
|
-
disposer.
|
|
92
|
-
disposer.
|
|
91
|
+
disposer.add(workingFn1);
|
|
92
|
+
disposer.add(errorFn);
|
|
93
|
+
disposer.add(workingFn2);
|
|
93
94
|
// dispose 不应该抛出错误
|
|
94
95
|
await expect(disposer.dispose()).resolves.toBeUndefined();
|
|
95
96
|
// 所有函数都应该被调用
|
|
@@ -97,12 +98,12 @@ describe('Disposer', () => {
|
|
|
97
98
|
expect(errorFn).toHaveBeenCalled();
|
|
98
99
|
expect(workingFn2).toHaveBeenCalled();
|
|
99
100
|
// 错误应该被记录
|
|
100
|
-
expect(loggerErrorSpy).toHaveBeenCalledWith(
|
|
101
|
+
expect(loggerErrorSpy).toHaveBeenCalledWith({ error: expect.any(Error) }, '[disposer] cleanup failed');
|
|
101
102
|
loggerErrorSpy.mockRestore();
|
|
102
103
|
});
|
|
103
104
|
it('应该在执行后清空清理函数列表', async () => {
|
|
104
105
|
const cleanupFn = vi.fn();
|
|
105
|
-
disposer.
|
|
106
|
+
disposer.add(cleanupFn);
|
|
106
107
|
await disposer.dispose();
|
|
107
108
|
expect(cleanupFn).toHaveBeenCalledOnce();
|
|
108
109
|
// 再次调用 dispose 不应该执行任何函数
|
|
@@ -112,9 +113,9 @@ describe('Disposer', () => {
|
|
|
112
113
|
it('应该能够在清空后重新添加清理函数', async () => {
|
|
113
114
|
const firstCleanup = vi.fn();
|
|
114
115
|
const secondCleanup = vi.fn();
|
|
115
|
-
disposer.
|
|
116
|
+
disposer.add(firstCleanup);
|
|
116
117
|
await disposer.dispose();
|
|
117
|
-
disposer.
|
|
118
|
+
disposer.add(secondCleanup);
|
|
118
119
|
await disposer.dispose();
|
|
119
120
|
expect(firstCleanup).toHaveBeenCalledOnce();
|
|
120
121
|
expect(secondCleanup).toHaveBeenCalledOnce();
|
|
@@ -124,19 +125,67 @@ describe('Disposer', () => {
|
|
|
124
125
|
});
|
|
125
126
|
it('应该能够多次调用 dispose', async () => {
|
|
126
127
|
const cleanupFn = vi.fn();
|
|
127
|
-
disposer.
|
|
128
|
+
disposer.add(cleanupFn);
|
|
128
129
|
await disposer.dispose();
|
|
129
130
|
await disposer.dispose();
|
|
130
131
|
await disposer.dispose();
|
|
131
132
|
expect(cleanupFn).toHaveBeenCalledOnce();
|
|
132
133
|
});
|
|
133
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
|
+
});
|
|
134
183
|
describe('实际使用场景', () => {
|
|
135
184
|
it('应该能够清理事件监听器', async () => {
|
|
136
185
|
const removeEventListener = vi.fn();
|
|
137
186
|
const unsubscribe = vi.fn();
|
|
138
|
-
disposer.
|
|
139
|
-
disposer.
|
|
187
|
+
disposer.add(removeEventListener);
|
|
188
|
+
disposer.add(unsubscribe);
|
|
140
189
|
await disposer.dispose();
|
|
141
190
|
expect(removeEventListener).toHaveBeenCalled();
|
|
142
191
|
expect(unsubscribe).toHaveBeenCalled();
|
|
@@ -144,8 +193,8 @@ describe('Disposer', () => {
|
|
|
144
193
|
it('应该能够清理定时器', async () => {
|
|
145
194
|
const clearTimeout = vi.fn();
|
|
146
195
|
const clearInterval = vi.fn();
|
|
147
|
-
disposer.
|
|
148
|
-
disposer.
|
|
196
|
+
disposer.add(() => clearTimeout(123));
|
|
197
|
+
disposer.add(() => clearInterval(456));
|
|
149
198
|
await disposer.dispose();
|
|
150
199
|
expect(clearTimeout).toHaveBeenCalledWith(123);
|
|
151
200
|
expect(clearInterval).toHaveBeenCalledWith(456);
|
|
@@ -153,8 +202,8 @@ describe('Disposer', () => {
|
|
|
153
202
|
it('应该能够清理资源连接', async () => {
|
|
154
203
|
const closeConnection = vi.fn();
|
|
155
204
|
const releaseResource = vi.fn();
|
|
156
|
-
disposer.
|
|
157
|
-
disposer.
|
|
205
|
+
disposer.add(() => closeConnection());
|
|
206
|
+
disposer.add(() => releaseResource());
|
|
158
207
|
await disposer.dispose();
|
|
159
208
|
expect(closeConnection).toHaveBeenCalled();
|
|
160
209
|
expect(releaseResource).toHaveBeenCalled();
|
|
@@ -167,9 +216,9 @@ describe('Disposer', () => {
|
|
|
167
216
|
};
|
|
168
217
|
const offFunction1 = mockEventEmitter.on('event1', vi.fn());
|
|
169
218
|
const offFunction2 = mockEventEmitter.on('event2', vi.fn());
|
|
170
|
-
disposer.
|
|
171
|
-
disposer.
|
|
172
|
-
disposer.
|
|
219
|
+
disposer.add(offFunction1);
|
|
220
|
+
disposer.add(offFunction2);
|
|
221
|
+
disposer.add(() => mockEventEmitter.cleanup());
|
|
173
222
|
await disposer.dispose();
|
|
174
223
|
expect(offFunction1).toHaveBeenCalled();
|
|
175
224
|
expect(offFunction2).toHaveBeenCalled();
|
|
@@ -180,22 +229,22 @@ describe('Disposer', () => {
|
|
|
180
229
|
it('应该捕获并记录同步错误', async () => {
|
|
181
230
|
const loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => { });
|
|
182
231
|
const error = new Error('Sync error');
|
|
183
|
-
disposer.
|
|
232
|
+
disposer.add(() => {
|
|
184
233
|
throw error;
|
|
185
234
|
});
|
|
186
235
|
await disposer.dispose();
|
|
187
|
-
expect(loggerErrorSpy).toHaveBeenCalledWith('
|
|
236
|
+
expect(loggerErrorSpy).toHaveBeenCalledWith({ error }, '[disposer] cleanup failed');
|
|
188
237
|
loggerErrorSpy.mockRestore();
|
|
189
238
|
});
|
|
190
239
|
it('应该处理不同类型的错误', async () => {
|
|
191
240
|
const loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => { });
|
|
192
|
-
disposer.
|
|
241
|
+
disposer.add(() => {
|
|
193
242
|
throw new TypeError('Type error');
|
|
194
243
|
});
|
|
195
|
-
disposer.
|
|
244
|
+
disposer.add(() => {
|
|
196
245
|
throw 'String error';
|
|
197
246
|
});
|
|
198
|
-
disposer.
|
|
247
|
+
disposer.add(() => {
|
|
199
248
|
throw { message: 'Object error' };
|
|
200
249
|
});
|
|
201
250
|
await disposer.dispose();
|
|
@@ -205,11 +254,11 @@ describe('Disposer', () => {
|
|
|
205
254
|
it('应该捕获并记录异步错误', async () => {
|
|
206
255
|
const loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => { });
|
|
207
256
|
const asyncError = new Error('Async error');
|
|
208
|
-
disposer.
|
|
257
|
+
disposer.add(async () => {
|
|
209
258
|
throw asyncError;
|
|
210
259
|
});
|
|
211
260
|
await disposer.dispose();
|
|
212
|
-
expect(loggerErrorSpy).toHaveBeenCalledWith('
|
|
261
|
+
expect(loggerErrorSpy).toHaveBeenCalledWith({ error: asyncError }, '[disposer] cleanup failed');
|
|
213
262
|
loggerErrorSpy.mockRestore();
|
|
214
263
|
});
|
|
215
264
|
});
|
|
@@ -2,6 +2,4 @@
|
|
|
2
2
|
* 递归合并对象,只针对对象类型的属性进行合并,其他类型后者覆盖前者
|
|
3
3
|
*/
|
|
4
4
|
export declare function objectMerge<T extends object>(target: T, ...rest: Partial<T>[]): T;
|
|
5
|
-
export declare function ObjectKeys<T extends object>(obj: T): (keyof T)[];
|
|
6
|
-
export declare function ObjectValues<T extends object>(obj: T): T[keyof T][];
|
|
7
5
|
//# sourceMappingURL=object.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../../source/object/object.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAqBjF
|
|
1
|
+
{"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../../source/object/object.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAqBjF"}
|
package/output/object/object.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taicode/common-base",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-beta.10",
|
|
4
4
|
"author": "Alain",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"description": "",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"test:ci": "vitest run",
|
|
40
40
|
"lint": "eslint && tsc --noEmit",
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
|
-
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"build": "rm -rf output && tsc -p tsconfig.json",
|
|
43
43
|
"dev": "tsc -p tsconfig.json --watch"
|
|
44
44
|
}
|
|
45
45
|
}
|