@wutiange/log-listener-plugin 2.0.2-alpha.2 → 2.0.2-alpha.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wutiange/log-listener-plugin",
3
- "version": "2.0.2-alpha.2",
3
+ "version": "2.0.2-alpha.4",
4
4
  "description": "log-record 客户端对应的的插件\r\nLog-record client corresponding plugin",
5
5
  "source": "index.ts",
6
6
  "react-native": "index.ts",
@@ -22,10 +22,10 @@
22
22
  "scripts": {
23
23
  "publish-alpha": "npm publish --access public --tag alpha",
24
24
  "prepublishOnly": "npm run build",
25
- "test": "jest",
26
- "build": "yarn clean && yarn test && rollup -c",
27
- "dev": "rollup -c -w",
28
- "clean": "rimraf dist"
25
+ "test": "yarn dlx jest",
26
+ "build": "yarn clean && yarn test && yarn dlx rollup -c",
27
+ "dev": "yarn dlx rollup -c -w",
28
+ "clean": "yarn dlx rimraf dist"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/react-native-zeroconf": "^0.13.1",
@@ -1,7 +1,7 @@
1
1
  import XHRInterceptor from 'react-native/Libraries/Network/XHRInterceptor';
2
2
  import BlobFileReader from 'react-native/Libraries/Blob/FileReader';
3
- import {Blob} from 'buffer';
4
- import { createClassWithErrorHandling, formDataToString } from './utils';
3
+ import { Blob } from 'buffer';
4
+ import { formDataToString } from './utils';
5
5
  import logger from './logger';
6
6
 
7
7
  type StartNetworkLoggingOptions = {
@@ -87,7 +87,7 @@ const getResponseBody = async (responseType: string, response: any) => {
87
87
  }
88
88
  return response ?? null;
89
89
  } catch (error) {
90
- logger.warn("getResponseBody---error---", error)
90
+ logger.warn('getResponseBody---error---', error);
91
91
  return null;
92
92
  }
93
93
  };
@@ -153,7 +153,7 @@ class HTTPInterceptor {
153
153
  await listener(data);
154
154
  }
155
155
  } catch (error: any) {
156
- console.warn(`eventName=${eventName}, error=${error?.message}`)
156
+ console.warn(`eventName=${eventName}, error=${error?.message}`);
157
157
  }
158
158
  });
159
159
  };
@@ -171,7 +171,7 @@ class HTTPInterceptor {
171
171
 
172
172
  if (this.ignoredPatterns) {
173
173
  if (
174
- this.ignoredPatterns.some(pattern => pattern.test(`${method} ${url}`))
174
+ this.ignoredPatterns.some((pattern) => pattern.test(`${method} ${url}`))
175
175
  ) {
176
176
  return;
177
177
  }
@@ -257,19 +257,17 @@ class HTTPInterceptor {
257
257
  };
258
258
 
259
259
  setIgnoredUrls = (ignoredUrls: string[]) => {
260
- if (ignoredUrls?.length) {
261
- if (
262
- !Array.isArray(ignoredUrls) ||
263
- typeof ignoredUrls[0] !== 'string'
264
- ) {
265
- console.warn(
266
- 'ignoredUrls must be an array of strings. The logger has not been started.',
267
- );
268
- return;
269
- }
270
- this.ignoredUrls = new Set(ignoredUrls);
260
+ if (
261
+ !Array.isArray(ignoredUrls) ||
262
+ (ignoredUrls[0] && typeof ignoredUrls[0] !== 'string')
263
+ ) {
264
+ console.warn(
265
+ 'ignoredUrls must be an array of strings. The logger has not been started.',
266
+ );
267
+ return;
271
268
  }
272
- }
269
+ this.ignoredUrls = new Set(ignoredUrls);
270
+ };
273
271
 
274
272
  enable = (options?: StartNetworkLoggingOptions) => {
275
273
  try {
@@ -301,7 +299,7 @@ class HTTPInterceptor {
301
299
  if (options?.ignoredPatterns) {
302
300
  this.ignoredPatterns = options.ignoredPatterns;
303
301
  }
304
- this.setIgnoredUrls(options?.ignoredUrls ?? [])
302
+ this.setIgnoredUrls(options?.ignoredUrls ?? []);
305
303
  XHRInterceptor.setOpenCallback(this.openHandle);
306
304
  XHRInterceptor.setRequestHeaderCallback(this.requestHeaderHandle);
307
305
  XHRInterceptor.setHeaderReceivedCallback(this.headerReceivedHandle);
@@ -330,8 +328,7 @@ class HTTPInterceptor {
330
328
  };
331
329
  }
332
330
 
333
- const SafeHTTPInterceptor = createClassWithErrorHandling(HTTPInterceptor)
334
- const httpInterceptor = new SafeHTTPInterceptor();
331
+ const httpInterceptor = new HTTPInterceptor();
335
332
  export {
336
333
  type StartNetworkLoggingOptions,
337
334
  httpInterceptor,
package/src/Server.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { hasPort, sleep, typeReplacer } from "./utils";
2
- import { getBaseData, LOG_KEY } from "./common";
3
- import logger from "./logger";
1
+ import { hasPort, sleep, typeReplacer } from './utils';
2
+ import { getBaseData, LOG_KEY } from './common';
3
+ import logger from './logger';
4
4
 
5
5
  const DEFAULT_PORT = 27751;
6
6
  class Server {
@@ -12,7 +12,7 @@ class Server {
12
12
  private innerBaseData: Record<string, string> = {};
13
13
 
14
14
  constructor(url?: string | Set<string>, timeout: number = 30000) {
15
- if (typeof url === "string") {
15
+ if (typeof url === 'string') {
16
16
  this.updateUrl(url);
17
17
  } else {
18
18
  this.setBaseUrlArr(url ?? new Set());
@@ -22,21 +22,21 @@ class Server {
22
22
  this.handleZeroConf();
23
23
  }
24
24
 
25
- addUrlsListener = (
26
- onNewUrlCallback: (urls: Set<string>) => void
27
- ) => {
25
+ addUrlsListener = (onNewUrlCallback: (urls: Set<string>) => void) => {
28
26
  this.urlsListener = onNewUrlCallback;
29
27
  };
30
28
 
31
29
  private requestJoin = async (url: string, token: string) => {
32
30
  const response = await fetch(url, {
33
- method: "POST",
31
+ method: 'POST',
34
32
  headers: {
35
- "Content-Type": "application/json;charset=utf-8",
33
+ 'Content-Type': 'application/json;charset=utf-8',
36
34
  },
37
35
  body: JSON.stringify({
38
36
  token,
39
- model: this.innerBaseData.Model ?? `${this.innerBaseData.systemName}v${this.innerBaseData.osVersion}`,
37
+ model:
38
+ this.innerBaseData.Model ??
39
+ `${this.innerBaseData.systemName}v${this.innerBaseData.osVersion}`,
40
40
  }),
41
41
  });
42
42
  if (response.status !== 200) {
@@ -46,17 +46,17 @@ class Server {
46
46
  if (json.code !== 0) {
47
47
  return false;
48
48
  }
49
- return true
50
- }
49
+ return true;
50
+ };
51
51
 
52
52
  private async handleZeroConf() {
53
53
  try {
54
- const ZeroConf: any = require("react-native-zeroconf")?.default
54
+ const ZeroConf: any = require('react-native-zeroconf')?.default;
55
55
  if (!ZeroConf) {
56
56
  return;
57
57
  }
58
- const zeroConf: import("react-native-zeroconf").default = new ZeroConf();
59
- zeroConf.on("resolved", async (service) => {
58
+ const zeroConf: import('react-native-zeroconf').default = new ZeroConf();
59
+ zeroConf.on('resolved', async (service) => {
60
60
  try {
61
61
  const { path, token } = service.txt ?? {};
62
62
  const url = `http://${service.host}:${service.port}`;
@@ -67,31 +67,35 @@ class Server {
67
67
  return;
68
68
  }
69
69
  this.baseUrlArr.add(url);
70
- this.urlsObj.set(service.name, url)
70
+ this.urlsObj.set(service.name, url);
71
71
  if (this.urlsListener) {
72
72
  this.urlsListener(this.baseUrlArr);
73
73
  }
74
74
  } catch (error) {
75
- logger.warn(LOG_KEY, "加入日志系统失败---", error);
75
+ logger.warn(LOG_KEY, '加入日志系统失败---', error);
76
76
  }
77
77
  });
78
- zeroConf.on("remove", (name: string) => {
78
+ zeroConf.on('remove', (name: string) => {
79
79
  const currentUrl = this.urlsObj.get(name);
80
80
  if (currentUrl === undefined) {
81
81
  return;
82
82
  }
83
- this.baseUrlArr.delete(currentUrl)
84
- this.urlsObj.delete(name)
83
+ this.baseUrlArr.delete(currentUrl);
84
+ this.urlsObj.delete(name);
85
85
  if (this.urlsListener) {
86
86
  this.urlsListener(this.baseUrlArr);
87
87
  }
88
88
  });
89
- zeroConf.on("error", (err: any) => {
90
- logger.warn(LOG_KEY, "zeroconf出现错误", err);
91
- })
92
- zeroConf.scan("http", "tcp");
89
+ zeroConf.on('error', (err: any) => {
90
+ logger.warn(LOG_KEY, 'zeroconf出现错误', err);
91
+ });
92
+ zeroConf.scan('http', 'tcp');
93
93
  } catch (error: any) {
94
- logger.warn(LOG_KEY, "zeroconf扫描或处理相关逻辑失败或者您根本就没有安装 react-native-zeroconf ,如果您没有安装,那么您将无法使用发现功能", error);
94
+ logger.warn(
95
+ LOG_KEY,
96
+ 'zeroconf扫描或处理相关逻辑失败或者您根本就没有安装 react-native-zeroconf ,如果您没有安装,那么您将无法使用发现功能',
97
+ error,
98
+ );
95
99
  }
96
100
  }
97
101
 
@@ -101,18 +105,18 @@ class Server {
101
105
 
102
106
  private send = async (
103
107
  path: string,
104
- data: Record<string, any>
108
+ data: Record<string, any>,
105
109
  ): Promise<void> => {
106
110
  const request = async (url: string, _data: Record<string, any>) => {
107
111
  await Promise.race([
108
112
  fetch(`${url}/${path}`, {
109
- method: "POST",
113
+ method: 'POST',
110
114
  headers: {
111
- "Content-Type": "application/json;charset=utf-8",
115
+ 'Content-Type': 'application/json;charset=utf-8',
112
116
  },
113
117
  body: JSON.stringify(
114
118
  { ...this.innerBaseData, ...this.baseData, ..._data },
115
- typeReplacer
119
+ typeReplacer,
116
120
  ),
117
121
  }),
118
122
  sleep(this.timeout, true),
@@ -123,30 +127,37 @@ class Server {
123
127
  }
124
128
  this.baseUrlArr.forEach(async (e) => {
125
129
  try {
126
- await request(e, data)
130
+ await request(e, data);
127
131
  } catch (error: any) {
128
- if (error?.message?.includes("Network request failed") || error?.message?.includes("Timeout")) {
129
- return
132
+ if (
133
+ error?.message?.includes('Network request failed') ||
134
+ error?.message?.includes('Timeout')
135
+ ) {
136
+ return;
130
137
  }
131
- logger.warn(LOG_KEY, "上报日志失败", error)
138
+ logger.warn(LOG_KEY, '上报日志失败', error);
132
139
  }
133
- })
140
+ });
134
141
  };
135
142
 
136
143
  updateUrl(url: string = '') {
137
- const tempUrl = url.includes("http") ? url : `http://${url}`;
144
+ const tempUrl = url.includes('http') ? url : `http://${url}`;
138
145
  if (!url) {
139
- const currentUrl = this.urlsObj.get("Default");
146
+ const currentUrl = this.urlsObj.get('Default');
140
147
  if (!currentUrl) {
141
148
  return;
142
149
  }
143
150
  this.baseUrlArr.delete(currentUrl);
144
- this.urlsObj.delete("Default");
151
+ this.urlsObj.delete('Default');
145
152
  } else if (!hasPort(tempUrl)) {
146
153
  this.updateUrl(`${tempUrl}:${DEFAULT_PORT}`);
147
154
  } else {
155
+ const defaultUrl = this.urlsObj.get('Default');
156
+ if (defaultUrl) {
157
+ this.baseUrlArr.delete(defaultUrl);
158
+ }
148
159
  this.baseUrlArr.add(tempUrl);
149
- this.urlsObj.set("Default", tempUrl);
160
+ this.urlsObj.set('Default', tempUrl);
150
161
  }
151
162
  }
152
163
 
@@ -163,11 +174,11 @@ class Server {
163
174
  }
164
175
 
165
176
  log = async (data: Record<string, any>) => {
166
- return this.send("log", data);
177
+ return this.send('log', data);
167
178
  };
168
179
 
169
180
  network = async (data: Record<string, any>) => {
170
- return this.send("network", data);
181
+ return this.send('network', data);
171
182
  };
172
183
  }
173
184
 
@@ -7,7 +7,6 @@ jest.mock('buffer', () => ({
7
7
  Blob: jest.fn(),
8
8
  }));
9
9
  jest.mock('../utils', () => ({
10
- createClassWithErrorHandling: jest.fn(Class => Class),
11
10
  formDataToString: jest.fn(),
12
11
  }));
13
12
 
@@ -28,7 +27,7 @@ class MockFormData {
28
27
 
29
28
  class MockBlob {
30
29
  private content: string;
31
- type: any
30
+ type: any;
32
31
  constructor(parts: any, options: any = {}) {
33
32
  this.content = parts ? parts.join('') : '';
34
33
  this.type = options.type || '';
@@ -165,127 +164,166 @@ describe('HTTPInterceptor', () => {
165
164
 
166
165
  beforeEach(() => {
167
166
  httpInterceptor.enable();
168
- openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock.calls[0][0];
169
- requestHeaderCallback = (XHRInterceptor.setRequestHeaderCallback as jest.Mock).mock.calls[0][0];
170
- headerReceivedCallback = (XHRInterceptor.setHeaderReceivedCallback as jest.Mock).mock.calls[0][0];
171
- sendCallback = (XHRInterceptor.setSendCallback as jest.Mock).mock.calls[0][0];
172
- responseCallback = (XHRInterceptor.setResponseCallback as jest.Mock).mock.calls[0][0];
167
+ openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
168
+ .calls[0][0];
169
+ requestHeaderCallback = (
170
+ XHRInterceptor.setRequestHeaderCallback as jest.Mock
171
+ ).mock.calls[0][0];
172
+ headerReceivedCallback = (
173
+ XHRInterceptor.setHeaderReceivedCallback as jest.Mock
174
+ ).mock.calls[0][0];
175
+ sendCallback = (XHRInterceptor.setSendCallback as jest.Mock).mock
176
+ .calls[0][0];
177
+ responseCallback = (XHRInterceptor.setResponseCallback as jest.Mock).mock
178
+ .calls[0][0];
173
179
  });
174
180
 
175
181
  it('should handle open event', () => {
176
182
  const listener = jest.fn();
177
183
  httpInterceptor.addListener('open', listener);
178
- const xhr = {}
184
+ const xhr = {};
179
185
  openCallback('GET', 'https://example.com', xhr);
180
- expect(listener).toHaveBeenCalledWith(expect.objectContaining({
181
- method: 'GET',
182
- url: 'https://example.com',
183
- }));
186
+ expect(listener).toHaveBeenCalledWith(
187
+ expect.objectContaining({
188
+ method: 'GET',
189
+ url: 'https://example.com',
190
+ }),
191
+ );
184
192
  });
185
193
 
186
194
  it('should handle request header event', () => {
187
195
  const listener = jest.fn();
188
196
  httpInterceptor.addListener('requestHeader', listener);
189
- const xhr = {}
197
+ const xhr = {};
190
198
  openCallback('GET', 'https://example.com', xhr);
191
199
  requestHeaderCallback('Content-Type', 'application/json', xhr);
192
- expect(listener).toHaveBeenCalledWith(expect.objectContaining({
193
- requestHeaders: { 'Content-Type': 'application/json' },
194
- }));
200
+ expect(listener).toHaveBeenCalledWith(
201
+ expect.objectContaining({
202
+ requestHeaders: { 'Content-Type': 'application/json' },
203
+ }),
204
+ );
195
205
  });
196
206
 
197
207
  it('should handle header received event', () => {
198
208
  const listener = jest.fn();
199
209
  httpInterceptor.addListener('headerReceived', listener);
200
- const xhr: {[key in string]: any} = {}
210
+ const xhr: { [key in string]: any } = {};
201
211
  openCallback('GET', 'https://example.com', xhr);
202
- xhr.responseHeaders = { 'Content-Type': 'application/json' }
212
+ xhr.responseHeaders = { 'Content-Type': 'application/json' };
203
213
  headerReceivedCallback('application/json', 100, {}, xhr);
204
- expect(listener).toHaveBeenCalledWith(expect.objectContaining({
205
- responseContentType: 'application/json',
206
- responseSize: 100,
207
- responseHeaders: { 'Content-Type': 'application/json' },
208
- }));
214
+ expect(listener).toHaveBeenCalledWith(
215
+ expect.objectContaining({
216
+ responseContentType: 'application/json',
217
+ responseSize: 100,
218
+ responseHeaders: { 'Content-Type': 'application/json' },
219
+ }),
220
+ );
209
221
  });
210
222
 
211
223
  it('should handle send event with JSON data', () => {
212
224
  const listener = jest.fn();
213
225
  httpInterceptor.addListener('send', listener);
214
- const xhr = {}
226
+ const xhr = {};
215
227
  openCallback('POST', 'https://example.com', xhr);
216
228
  sendCallback(JSON.stringify({ key: 'value' }), xhr);
217
- expect(listener).toHaveBeenCalledWith(expect.objectContaining({
218
- requestData: { key: 'value' },
219
- }));
229
+ expect(listener).toHaveBeenCalledWith(
230
+ expect.objectContaining({
231
+ requestData: { key: 'value' },
232
+ }),
233
+ );
220
234
  });
221
235
 
222
236
  it('should handle send event with FormData', () => {
223
237
  const listener = jest.fn();
224
238
  httpInterceptor.addListener('send', listener);
225
- const xhr = {}
239
+ const xhr = {};
226
240
  openCallback('POST', 'https://example.com', xhr);
227
241
  const formData = new FormData();
228
242
  formData.append('key', 'value');
229
243
  (formDataToString as jest.Mock).mockReturnValue('key=value');
230
244
  sendCallback(formData, xhr);
231
- expect(listener).toHaveBeenCalledWith(expect.objectContaining({
232
- requestData: 'key=value',
233
- }));
245
+ expect(listener).toHaveBeenCalledWith(
246
+ expect.objectContaining({
247
+ requestData: 'key=value',
248
+ }),
249
+ );
234
250
  });
235
251
 
236
252
  it('should handle response event', async () => {
237
253
  const listener = jest.fn();
238
254
  httpInterceptor.addListener('response', listener);
239
- const xhr = {}
255
+ const xhr = {};
240
256
  openCallback('GET', 'https://example.com', xhr);
241
- await responseCallback(200, 1000, { data: 'response' }, 'https://example.com', 'json', xhr);
242
- expect(listener).toHaveBeenCalledWith(expect.objectContaining({
243
- status: 200,
244
- timeout: 1000,
245
- responseData: { data: 'response' },
246
- responseURL: 'https://example.com',
247
- responseType: 'json',
248
- }));
257
+ await responseCallback(
258
+ 200,
259
+ 1000,
260
+ { data: 'response' },
261
+ 'https://example.com',
262
+ 'json',
263
+ xhr,
264
+ );
265
+ expect(listener).toHaveBeenCalledWith(
266
+ expect.objectContaining({
267
+ status: 200,
268
+ timeout: 1000,
269
+ responseData: { data: 'response' },
270
+ responseURL: 'https://example.com',
271
+ responseType: 'json',
272
+ }),
273
+ );
249
274
  });
250
275
 
251
276
  it('should handle response event with blob data', async () => {
252
277
  const listener = jest.fn();
253
278
  httpInterceptor.addListener('response', listener);
254
- const xhr = {}
279
+ const xhr = {};
255
280
  openCallback('GET', 'https://example.com', xhr);
256
281
  const mockBlob = new MockBlob(['blob content']);
257
- await responseCallback(200, 1000, mockBlob, 'https://example.com', 'blob', xhr);
258
- expect(listener).toHaveBeenCalledWith(expect.objectContaining({
259
- responseData: 'blob content',
260
- }));
282
+ await responseCallback(
283
+ 200,
284
+ 1000,
285
+ mockBlob,
286
+ 'https://example.com',
287
+ 'blob',
288
+ xhr,
289
+ );
290
+ expect(listener).toHaveBeenCalledWith(
291
+ expect.objectContaining({
292
+ responseData: 'blob content',
293
+ }),
294
+ );
261
295
  });
262
296
  });
263
297
 
264
298
  describe('error handling', () => {
265
299
  beforeEach(() => {
266
300
  httpInterceptor.enable();
267
- })
301
+ });
268
302
  it('should handle errors in listeners', async () => {
269
303
  const errorListener = jest.fn(() => {
270
304
  throw new Error('Listener error');
271
305
  });
272
306
  httpInterceptor.addListener('open', errorListener);
273
307
  console.warn = jest.fn();
274
- const xhr = {}
275
- const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock.calls[0][0];
308
+ const xhr = {};
309
+ const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
310
+ .calls[0][0];
276
311
  openCallback('GET', 'https://example.com', xhr);
277
-
278
- expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Listener error'));
312
+
313
+ expect(console.warn).toHaveBeenCalledWith(
314
+ expect.stringContaining('Listener error'),
315
+ );
279
316
  });
280
317
  });
281
318
 
282
319
  describe('ignored requests', () => {
283
320
  it('should ignore requests to ignored hosts', () => {
284
321
  httpInterceptor.enable({ ignoredHosts: ['ignored.com'] });
285
- const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock.calls[0][0];
322
+ const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
323
+ .calls[0][0];
286
324
  const listener = jest.fn();
287
325
  httpInterceptor.addListener('open', listener);
288
-
326
+
289
327
  openCallback('GET', 'https://ignored.com', { uniqueId: '123' });
290
328
  expect(listener).not.toHaveBeenCalled();
291
329
 
@@ -295,10 +333,11 @@ describe('HTTPInterceptor', () => {
295
333
 
296
334
  it('should ignore requests to ignored URLs', () => {
297
335
  httpInterceptor.enable({ ignoredUrls: ['https://example.com/ignored'] });
298
- const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock.calls[0][0];
336
+ const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
337
+ .calls[0][0];
299
338
  const listener = jest.fn();
300
339
  httpInterceptor.addListener('open', listener);
301
-
340
+
302
341
  openCallback('GET', 'https://example.com/ignored', { uniqueId: '123' });
303
342
  expect(listener).not.toHaveBeenCalled();
304
343
 
@@ -308,10 +347,11 @@ describe('HTTPInterceptor', () => {
308
347
 
309
348
  it('should ignore requests matching ignored patterns', () => {
310
349
  httpInterceptor.enable({ ignoredPatterns: [/^GET https:\/\/test\.com/] });
311
- const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock.calls[0][0];
350
+ const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock
351
+ .calls[0][0];
312
352
  const listener = jest.fn();
313
353
  httpInterceptor.addListener('open', listener);
314
-
354
+
315
355
  openCallback('GET', 'https://test.com/api', { uniqueId: '123' });
316
356
  expect(listener).not.toHaveBeenCalled();
317
357
 
@@ -1,114 +1,236 @@
1
- import logger from '../logger';
2
- import { createClassWithErrorHandling, hasPort } from '../utils';
1
+ import { hasPort, formDataToString, sleep, typeReplacer } from '../utils';
3
2
 
4
- describe('createClassWithErrorHandling', () => {
5
- class TestClass {
6
- normalMethod(): string {
7
- return 'normal';
8
- }
3
+ describe('hasPort function', () => {
4
+ it('should return true for URLs with explicit ports', () => {
5
+ expect(hasPort('http://example.com:8080')).toBe(true);
6
+ expect(hasPort('ftp://example.com:210')).toBe(true);
7
+ });
8
+
9
+ it('should return false for URLs without explicit ports', () => {
10
+ expect(hasPort('http://example.com')).toBe(false);
11
+ expect(hasPort('https://example.com')).toBe(false);
12
+ expect(hasPort('ftp://example.com')).toBe(false);
13
+ });
14
+
15
+ it('should return false for invalid URLs', () => {
16
+ expect(hasPort('not a url')).toBe(false);
17
+ expect(hasPort('http:/example.com')).toBe(false);
18
+ expect(hasPort('example.com:8080')).toBe(false);
19
+ });
20
+
21
+ it('should return false for empty input', () => {
22
+ expect(hasPort('')).toBe(false);
23
+ });
9
24
 
10
- errorMethod(): void {
11
- throw new Error('Test error');
12
- }
25
+ it('should return false for non-string input', () => {
26
+ expect(hasPort(null as any)).toBe(false);
27
+ expect(hasPort(undefined as any)).toBe(false);
28
+ expect(hasPort(123 as any)).toBe(false);
29
+ expect(hasPort({} as any)).toBe(false);
30
+ });
13
31
 
14
- async asyncMethod(): Promise<string> {
15
- return 'async';
16
- }
32
+ it('should handle URLs with default ports correctly', () => {
33
+ expect(hasPort('http://example.com:80')).toBe(false);
34
+ expect(hasPort('https://example.com:443')).toBe(false);
35
+ });
17
36
 
18
- async asyncErrorMethod(): Promise<void> {
19
- throw new Error('Async test error');
20
- }
21
- }
37
+ it('should handle URLs with IPv6 addresses', () => {
38
+ expect(hasPort('http://[2001:db8::1]:8080')).toBe(true);
39
+ expect(hasPort('https://[2001:db8::1]')).toBe(false);
40
+ });
41
+
42
+ it('should handle URLs with userinfo', () => {
43
+ expect(hasPort('http://user:pass@example.com:8080')).toBe(true);
44
+ expect(hasPort('http://user:pass@example.com')).toBe(false);
45
+ });
46
+ });
22
47
 
23
- let consoleErrorSpy: jest.SpyInstance;
48
+ describe('formDataToString', () => {
49
+ let mockFormData: FormData;
24
50
 
25
51
  beforeEach(() => {
26
- consoleErrorSpy = jest.spyOn(logger, 'error').mockImplementation(() => {});
52
+ // 创建一个模拟的 FormData 对象
53
+ mockFormData = new FormData();
54
+ // 模拟 getParts 方法
55
+ (mockFormData as any).getParts = jest.fn();
27
56
  });
28
57
 
29
- afterEach(() => {
30
- consoleErrorSpy.mockRestore();
58
+ it('should convert form data with text fields to string', () => {
59
+ // 模拟 getParts 返回包含文本字段的数据
60
+ (mockFormData as any).getParts.mockReturnValue([
61
+ {
62
+ headers: {
63
+ 'content-disposition': 'form-data; name="field1"',
64
+ },
65
+ string: 'value1',
66
+ },
67
+ ]);
68
+
69
+ const result = formDataToString(mockFormData);
70
+
71
+ // 验证基本结构
72
+ expect(result).toMatch(/^------WebKitFormBoundary.*\r\n/);
73
+ expect(result).toMatch(/Content-Disposition: form-data; name="field1"\r\n/);
74
+ expect(result).toMatch(/Content-Length: 6\r\n/);
75
+ expect(result).toMatch(/value1\r\n/);
76
+ expect(result).toMatch(/----WebKitFormBoundary.*--\r\n$/);
31
77
  });
32
78
 
33
- test('should not interfere with normal methods', () => {
34
- const EnhancedClass = createClassWithErrorHandling(TestClass);
35
- const instance = new EnhancedClass();
36
- expect(instance.normalMethod()).toBe('normal');
79
+ it('should handle form data with content-type header', () => {
80
+ // 模拟 getParts 返回包含 content-type 的数据
81
+ (mockFormData as any).getParts.mockReturnValue([
82
+ {
83
+ headers: {
84
+ 'content-disposition': 'form-data; name="file"; filename="test.txt"',
85
+ 'content-type': 'text/plain',
86
+ },
87
+ string: 'file content',
88
+ },
89
+ ]);
90
+
91
+ const result = formDataToString(mockFormData);
92
+
93
+ expect(result).toMatch(
94
+ /Content-Disposition: form-data; name="file"; filename="test.txt"\r\n/,
95
+ );
96
+ expect(result).toMatch(/Content-Type: text\/plain\r\n/);
97
+ expect(result).toMatch(/Content-Length: 12\r\n/);
98
+ expect(result).toMatch(/file content\r\n/);
37
99
  });
38
100
 
39
- test('should catch and log errors from methods', () => {
40
- const EnhancedClass = createClassWithErrorHandling(TestClass);
41
- const instance = new EnhancedClass();
42
- expect(() => instance.errorMethod()).toThrow('Test error');
43
- expect(consoleErrorSpy).toHaveBeenCalledWith('Error in errorMethod:', expect.any(Error));
101
+ it('should handle multiple form fields', () => {
102
+ // 模拟 getParts 返回多个字段
103
+ (mockFormData as any).getParts.mockReturnValue([
104
+ {
105
+ headers: {
106
+ 'content-disposition': 'form-data; name="field1"',
107
+ },
108
+ string: 'value1',
109
+ },
110
+ {
111
+ headers: {
112
+ 'content-disposition': 'form-data; name="field2"',
113
+ },
114
+ string: 'value2',
115
+ },
116
+ ]);
117
+
118
+ const result = formDataToString(mockFormData);
119
+
120
+ expect(result).toMatch(/field1.*value1.*field2.*value2/s);
121
+ expect((result.match(/----WebKitFormBoundary/g) || []).length).toBe(3); // 开始、中间、结束
44
122
  });
45
123
 
46
- test('should not interfere with async methods that resolve', async () => {
47
- const EnhancedClass = createClassWithErrorHandling(TestClass);
48
- const instance = new EnhancedClass();
49
- await expect(instance.asyncMethod()).resolves.toBe('async');
124
+ it('should handle URI parts', () => {
125
+ // 模拟 getParts 返回包含 URI 的数据
126
+ (mockFormData as any).getParts.mockReturnValue([
127
+ {
128
+ headers: {
129
+ 'content-disposition': 'form-data; name="file"',
130
+ 'content-type': 'image/jpeg',
131
+ },
132
+ uri: 'file:///path/to/image.jpg',
133
+ },
134
+ ]);
135
+
136
+ const result = formDataToString(mockFormData);
137
+
138
+ expect(result).toMatch(/Content-Type: image\/jpeg\r\n/);
139
+ expect(result).toMatch(/file:\/\/\/path\/to\/image.jpg\r\n/);
50
140
  });
141
+ });
51
142
 
52
- test('should catch and log errors from async methods that reject', async () => {
53
- const EnhancedClass = createClassWithErrorHandling(TestClass);
54
- const instance = new EnhancedClass();
55
- await expect(instance.asyncErrorMethod()).rejects.toThrow('Async test error');
56
- expect(consoleErrorSpy).toHaveBeenCalledWith('Error in asyncErrorMethod:', expect.any(Error));
143
+ describe('sleep function', () => {
144
+ // 测试正常延迟情况
145
+ it('should resolve after specified delay', async () => {
146
+ const startTime = Date.now();
147
+ const delay = 100;
148
+
149
+ await sleep(delay);
150
+ const endTime = Date.now();
151
+ const actualDelay = endTime - startTime;
152
+
153
+ // 由于 JavaScript 定时器的不精确性,我们允许一个小的误差范围
154
+ expect(actualDelay).toBeGreaterThanOrEqual(delay);
155
+ expect(actualDelay).toBeLessThan(delay + 50); // 允许 50ms 的误差
57
156
  });
58
157
 
59
- test('should handle methods added after instantiation', () => {
60
- const EnhancedClass = createClassWithErrorHandling(TestClass);
61
- const instance = new EnhancedClass();
62
- (instance as any).dynamicMethod = function(): void {
63
- throw new Error('Dynamic method error');
64
- };
65
- expect(() => (instance as any).dynamicMethod()).toThrow('Dynamic method error');
66
- expect(consoleErrorSpy).toHaveBeenCalledWith('Error in dynamicMethod:', expect.any(Error));
158
+ // 测试超时拒绝情况
159
+ it('should reject with timeout error when isReject is true', async () => {
160
+ const delay = 100;
161
+
162
+ await expect(sleep(delay, true)).rejects.toEqual({
163
+ code: 11001,
164
+ key: '@wutiange/log-listener-plugin%%timeout',
165
+ message: 'Timeout',
166
+ });
67
167
  });
68
168
  });
69
169
 
170
+ describe('typeReplacer', () => {
171
+ // 测试 Error 类型转换
172
+ it('should convert Error to string', () => {
173
+ const error = new Error('test error');
174
+ expect(typeReplacer('error', error)).toBe('Error: test error');
175
+ });
70
176
 
71
- describe('hasPort function', () => {
72
- test('should return true for URLs with explicit ports', () => {
73
- expect(hasPort('http://example.com:8080')).toBe(true);
74
- expect(hasPort('ftp://example.com:210')).toBe(true);
177
+ // 测试 Function 类型转换
178
+ it('should convert Function to string', () => {
179
+ const fn = function test() {
180
+ return 'hello';
181
+ };
182
+ const result = typeReplacer('fn', fn);
183
+ expect(result).toContain('function test()');
75
184
  });
76
185
 
77
- test('should return false for URLs without explicit ports', () => {
78
- expect(hasPort('http://example.com')).toBe(false);
79
- expect(hasPort('https://example.com')).toBe(false);
80
- expect(hasPort('ftp://example.com')).toBe(false);
186
+ // 测试 Symbol 类型转换
187
+ it('should convert Symbol to string', () => {
188
+ const sym = Symbol('test');
189
+ expect(typeReplacer('symbol', sym)).toBe('Symbol(test)');
81
190
  });
82
191
 
83
- test('should return false for invalid URLs', () => {
84
- expect(hasPort('not a url')).toBe(false);
85
- expect(hasPort('http:/example.com')).toBe(false);
86
- expect(hasPort('example.com:8080')).toBe(false);
192
+ // 测试 BigInt 类型转换
193
+ it('should convert BigInt to string', () => {
194
+ const big = BigInt(9007199254740991);
195
+ expect(typeReplacer('bigint', big)).toBe('9007199254740991');
87
196
  });
88
197
 
89
- test('should return false for empty input', () => {
90
- expect(hasPort('')).toBe(false);
198
+ // 测试 RegExp 类型转换
199
+ it('should convert RegExp to string', () => {
200
+ const regex = /test/g;
201
+ expect(typeReplacer('regex', regex)).toBe('/test/g');
91
202
  });
92
203
 
93
- test('should return false for non-string input', () => {
94
- expect(hasPort(null as any)).toBe(false);
95
- expect(hasPort(undefined as any)).toBe(false);
96
- expect(hasPort(123 as any)).toBe(false);
97
- expect(hasPort({} as any)).toBe(false);
204
+ // 测试 Set 类型转换
205
+ it('should convert Set to array', () => {
206
+ const set = new Set([1, 2, 3]);
207
+ expect(typeReplacer('set', set)).toEqual([1, 2, 3]);
98
208
  });
99
209
 
100
- test('should handle URLs with default ports correctly', () => {
101
- expect(hasPort('http://example.com:80')).toBe(false);
102
- expect(hasPort('https://example.com:443')).toBe(false);
210
+ // 测试 Map 类型转换
211
+ it('should convert Map to object', () => {
212
+ const map = new Map([
213
+ ['key1', 'value1'],
214
+ ['key2', 'value2'],
215
+ ]);
216
+ expect(typeReplacer('map', map)).toEqual({
217
+ key1: 'value1',
218
+ key2: 'value2',
219
+ });
103
220
  });
104
221
 
105
- test('should handle URLs with IPv6 addresses', () => {
106
- expect(hasPort('http://[2001:db8::1]:8080')).toBe(true);
107
- expect(hasPort('https://[2001:db8::1]')).toBe(false);
222
+ // 测试普通值不变
223
+ it('should return primitive values as is', () => {
224
+ expect(typeReplacer('string', 'test')).toBe('test');
225
+ expect(typeReplacer('number', 42)).toBe(42);
226
+ expect(typeReplacer('boolean', true)).toBe(true);
227
+ expect(typeReplacer('null', null)).toBe(null);
228
+ expect(typeReplacer('undefined', undefined)).toBe(undefined);
108
229
  });
109
230
 
110
- test('should handle URLs with userinfo', () => {
111
- expect(hasPort('http://user:pass@example.com:8080')).toBe(true);
112
- expect(hasPort('http://user:pass@example.com')).toBe(false);
231
+ // 测试普通对象不变
232
+ it('should return objects as is', () => {
233
+ const obj = { name: 'test', age: 25 };
234
+ expect(typeReplacer('object', obj)).toEqual(obj);
113
235
  });
114
236
  });
package/src/logPlugin.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import Server from './Server';
2
- import { createClassWithErrorHandling } from './utils';
3
2
  import { httpInterceptor } from './HTTPInterceptor';
4
3
  import {
5
4
  DEFAULT_TIMEOUT,
@@ -232,7 +231,5 @@ class LogPlugin {
232
231
  this._log(Level.ERROR, Tag.DEFAULT, ...data);
233
232
  };
234
233
  }
235
- const SafeLogPlugin = createClassWithErrorHandling(LogPlugin);
236
- const logPlugin = new SafeLogPlugin();
237
- export { SafeLogPlugin };
234
+ const logPlugin = new LogPlugin();
238
235
  export default logPlugin;
package/src/utils.ts CHANGED
@@ -24,75 +24,12 @@ export function hasPort(url: string) {
24
24
  return false;
25
25
  }
26
26
 
27
- try {
28
- // 使用 URL 构造函数解析 URL
29
- const parsedUrl = new URL(url);
27
+ // 使用 URL 构造函数解析 URL
28
+ const parsedUrl = new URL(url);
30
29
 
31
- // 检查 port 属性是否为空
32
- // 注意:如果使用默认端口(如 HTTP 的 80 或 HTTPS 的 443),port 会是空字符串
33
- return parsedUrl.port !== '';
34
- } catch (error) {
35
- logger.error(error);
36
- // 如果 URL 无效,捕获错误并返回 false
37
- return false;
38
- }
39
- }
40
-
41
- type Constructor<T = {}> = new (...args: any[]) => T;
42
-
43
- export function createClassWithErrorHandling<T extends Constructor>(
44
- BaseClass: T,
45
- ): T {
46
- return new Proxy(BaseClass, {
47
- construct(target: T, args: any[]): object {
48
- const instance = new target(...args);
49
- return new Proxy(instance, {
50
- get(target: any, prop: string | symbol): any {
51
- const value = target[prop];
52
- if (typeof value === 'function') {
53
- return function (this: any, ...args: any[]): any {
54
- try {
55
- const result = value.apply(this, args);
56
- if (result instanceof Promise) {
57
- return result.catch((error: Error) => {
58
- logger.error(`Error in ${String(prop)}:`, error);
59
- throw error; // 重新抛出错误,以便调用者可以捕获它
60
- });
61
- }
62
- return result;
63
- } catch (error) {
64
- logger.error(`Error in ${String(prop)}:`, error);
65
- throw error; // 重新抛出错误,以便调用者可以捕获它
66
- }
67
- };
68
- }
69
- return value;
70
- },
71
- set(target: any, prop: string | symbol, value: any): boolean {
72
- if (typeof value === 'function') {
73
- target[prop] = function (this: any, ...args: any[]): any {
74
- try {
75
- const result = value.apply(this, args);
76
- if (result instanceof Promise) {
77
- return result.catch((error: Error) => {
78
- logger.error(`Error in ${String(prop)}:`, error);
79
- throw error;
80
- });
81
- }
82
- return result;
83
- } catch (error) {
84
- logger.error(`Error in ${String(prop)}:`, error);
85
- throw error;
86
- }
87
- };
88
- } else {
89
- target[prop] = value;
90
- }
91
- return true;
92
- },
93
- });
94
- },
95
- });
30
+ // 检查 port 属性是否为空
31
+ // 注意:如果使用默认端口(如 HTTP 的 80 或 HTTPS 的 443),port 会是空字符串
32
+ return parsedUrl.port !== '';
96
33
  }
97
34
 
98
35
  export function formDataToString(formData: FormData): string {
@@ -120,7 +57,7 @@ export function typeReplacer(key: string, val: any) {
120
57
  return val.toString();
121
58
  } else if (val instanceof Function) {
122
59
  return Function.prototype.toString.call(val);
123
- } else if (val instanceof Symbol) {
60
+ } else if (typeof val === 'symbol') {
124
61
  return val.toString();
125
62
  } else if (typeof val === 'bigint') {
126
63
  return val.toString();