@taicode/common-base 4.0.0-beta.2 → 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;IACnB,OAAO,CAAC,gBAAgB,CAAwB;IAGhD,GAAG,CAAC,SAAS,EAAE,eAAe,GAAG,IAAI;IAK/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAY/B"}
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
- for (const cleanupFn of cleanupFunctions) {
16
+ // 后创建的东西依赖先创建的,必须**后**销毁:逆注册序执行
17
+ for (const cleanupFn of [...cleanupFunctions].reverse()) {
13
18
  try {
14
19
  await cleanupFn();
15
20
  }
16
21
  catch (error) {
17
- logger.error('Error during cleanup:', error);
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
  }
@@ -54,15 +54,16 @@ describe('Disposer', () => {
54
54
  expect(cleanupFn2).toHaveBeenCalledOnce();
55
55
  expect(cleanupFn3).toHaveBeenCalledOnce();
56
56
  });
57
- it('应该按添加顺序执行清理函数', async () => {
57
+ it('应该按添加顺序的**逆序**(LIFO)执行清理函数', async () => {
58
58
  const executionOrder = [];
59
59
  disposer.add(() => executionOrder.push(1));
60
60
  disposer.add(() => executionOrder.push(2));
61
61
  disposer.add(() => executionOrder.push(3));
62
62
  await disposer.dispose();
63
- expect(executionOrder).toEqual([1, 2, 3]);
63
+ // 后创建的东西依赖先创建的,必须后销毁
64
+ expect(executionOrder).toEqual([3, 2, 1]);
64
65
  });
65
- it('应该按添加顺序执行异步清理函数', async () => {
66
+ it('应该按添加顺序的**逆序**(LIFO)执行异步清理函数', async () => {
66
67
  const executionOrder = [];
67
68
  disposer.add(async () => {
68
69
  await Promise.resolve();
@@ -77,7 +78,7 @@ describe('Disposer', () => {
77
78
  executionOrder.push(3);
78
79
  });
79
80
  await disposer.dispose();
80
- expect(executionOrder).toEqual([1, 2, 3]);
81
+ expect(executionOrder).toEqual([3, 2, 1]);
81
82
  });
82
83
  it('应该处理清理函数中的错误', async () => {
83
84
  const workingFn1 = vi.fn();
@@ -97,7 +98,7 @@ describe('Disposer', () => {
97
98
  expect(errorFn).toHaveBeenCalled();
98
99
  expect(workingFn2).toHaveBeenCalled();
99
100
  // 错误应该被记录
100
- expect(loggerErrorSpy).toHaveBeenCalledWith('Error during cleanup:', expect.any(Error));
101
+ expect(loggerErrorSpy).toHaveBeenCalledWith({ error: expect.any(Error) }, '[disposer] cleanup failed');
101
102
  loggerErrorSpy.mockRestore();
102
103
  });
103
104
  it('应该在执行后清空清理函数列表', async () => {
@@ -131,6 +132,54 @@ describe('Disposer', () => {
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();
@@ -184,7 +233,7 @@ describe('Disposer', () => {
184
233
  throw error;
185
234
  });
186
235
  await disposer.dispose();
187
- expect(loggerErrorSpy).toHaveBeenCalledWith('Error during cleanup:', error);
236
+ expect(loggerErrorSpy).toHaveBeenCalledWith({ error }, '[disposer] cleanup failed');
188
237
  loggerErrorSpy.mockRestore();
189
238
  });
190
239
  it('应该处理不同类型的错误', async () => {
@@ -209,7 +258,7 @@ describe('Disposer', () => {
209
258
  throw asyncError;
210
259
  });
211
260
  await disposer.dispose();
212
- expect(loggerErrorSpy).toHaveBeenCalledWith('Error during cleanup:', asyncError);
261
+ expect(loggerErrorSpy).toHaveBeenCalledWith({ error: asyncError }, '[disposer] cleanup failed');
213
262
  loggerErrorSpy.mockRestore();
214
263
  });
215
264
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taicode/common-base",
3
- "version": "4.0.0-beta.2",
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.