@lark-apaas/client-toolkit 1.0.5 → 1.0.6
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/lib/components/AppContainer/LogInterceptor.js +27 -5
- package/lib/components/AppContainer/index.js +2 -0
- package/lib/components/AppContainer/utils/childApi.js +1 -11
- package/lib/components/ui/button.d.ts +1 -1
- package/lib/integrations/getAppInfo.js +14 -2
- package/lib/logger/__tests__/batch-logger.test.d.ts +1 -0
- package/lib/logger/__tests__/batch-logger.test.js +367 -0
- package/lib/logger/batch-logger.d.ts +78 -0
- package/lib/logger/batch-logger.js +134 -0
- package/lib/types/iframe-events.d.ts +0 -9
- package/package.json +1 -1
- package/lib/components/AppContainer/api-proxy/core.d.ts +0 -182
- package/lib/components/AppContainer/api-proxy/core.js +0 -294
- package/lib/components/AppContainer/utils/api-panel.d.ts +0 -29
- package/lib/components/AppContainer/utils/api-panel.js +0 -66
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { batchLogInfo, destroyBatchLogger, initBatchLogger } from "../../logger/batch-logger.js";
|
|
1
2
|
import { postMessage } from "../../utils/postMessage.js";
|
|
2
3
|
const PROXY_CONSOLE_METHOD = [
|
|
3
4
|
'log',
|
|
@@ -9,6 +10,20 @@ const LOG_FILTER_PREFIX = [
|
|
|
9
10
|
'[Dataloom]',
|
|
10
11
|
'[MiaoDa]'
|
|
11
12
|
];
|
|
13
|
+
function formatArg(arg) {
|
|
14
|
+
if (null === arg) return 'null';
|
|
15
|
+
if (void 0 === arg) return 'undefined';
|
|
16
|
+
if ('object' == typeof arg) {
|
|
17
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack}`;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(arg, this.getCircularReplacer(), 2);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
return Object.prototype.toString.call(arg);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if ('function' == typeof arg) return arg.toString();
|
|
25
|
+
return String(arg);
|
|
26
|
+
}
|
|
12
27
|
const initHandleError = ()=>{
|
|
13
28
|
window.onerror = (message, source, lineno, colno, error)=>{
|
|
14
29
|
const errorList = [];
|
|
@@ -24,16 +39,23 @@ const initHandleError = ()=>{
|
|
|
24
39
|
};
|
|
25
40
|
};
|
|
26
41
|
const initLogInterceptor = ()=>{
|
|
42
|
+
initBatchLogger(console);
|
|
43
|
+
window.addEventListener('beforeunload', ()=>{
|
|
44
|
+
destroyBatchLogger();
|
|
45
|
+
});
|
|
27
46
|
PROXY_CONSOLE_METHOD.forEach((method)=>{
|
|
28
47
|
const originalMethod = window.console[method];
|
|
29
48
|
window.console[method] = (...args)=>{
|
|
30
49
|
originalMethod(...args);
|
|
31
50
|
const log = args[0];
|
|
32
|
-
if ('string' == typeof log && LOG_FILTER_PREFIX.some((prefix)=>log.startsWith(prefix)))
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
if ('string' == typeof log && LOG_FILTER_PREFIX.some((prefix)=>log.startsWith(prefix))) {
|
|
52
|
+
batchLogInfo(method, args.map(formatArg).join(' '));
|
|
53
|
+
postMessage({
|
|
54
|
+
type: 'Console',
|
|
55
|
+
method,
|
|
56
|
+
data: args
|
|
57
|
+
});
|
|
58
|
+
}
|
|
37
59
|
};
|
|
38
60
|
});
|
|
39
61
|
};
|
|
@@ -10,11 +10,13 @@ import { ThemeProvider, findValueByPixel, generateTailwindRadiusToken, themeColo
|
|
|
10
10
|
import { registerDayjsPlugins } from "./dayjsPlugins.js";
|
|
11
11
|
import "../../index.css";
|
|
12
12
|
import { initAxiosConfig } from "../../utils/axiosConfig.js";
|
|
13
|
+
import { useAppInfo } from "../../hooks/index.js";
|
|
13
14
|
registerDayjsPlugins();
|
|
14
15
|
initAxiosConfig();
|
|
15
16
|
const isMiaodaPreview = window.IS_MIAODA_PREVIEW;
|
|
16
17
|
const App = (props)=>{
|
|
17
18
|
const { themeMeta = {} } = props;
|
|
19
|
+
useAppInfo();
|
|
18
20
|
const { rem } = findValueByPixel(themeMetaOptions.themeRadius, themeMeta.borderRadius) || {
|
|
19
21
|
rem: '0.625'
|
|
20
22
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { normalizeBasePath } from "../../../utils/utils.js";
|
|
2
|
-
import { api_delete, api_get, api_head, api_options, api_patch, api_post, api_put } from "./api-panel.js";
|
|
3
2
|
async function getRoutes() {
|
|
4
3
|
let routes = [
|
|
5
4
|
{
|
|
@@ -28,15 +27,6 @@ async function getSourceMap() {
|
|
|
28
27
|
}
|
|
29
28
|
const childApi = {
|
|
30
29
|
getRoutes,
|
|
31
|
-
getSourceMap
|
|
32
|
-
apiProxy: {
|
|
33
|
-
api_get: api_get,
|
|
34
|
-
api_post: api_post,
|
|
35
|
-
api_put: api_put,
|
|
36
|
-
api_delete: api_delete,
|
|
37
|
-
api_patch: api_patch,
|
|
38
|
-
api_head: api_head,
|
|
39
|
-
api_options: api_options
|
|
40
|
-
}
|
|
30
|
+
getSourceMap
|
|
41
31
|
};
|
|
42
32
|
export { childApi };
|
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const buttonVariants: (props?: {
|
|
4
4
|
variant?: "default" | "link" | "secondary" | "destructive" | "outline" | "ghost";
|
|
5
|
-
size?: "default" | "
|
|
5
|
+
size?: "default" | "icon" | "sm" | "lg" | "icon-sm" | "icon-lg";
|
|
6
6
|
} & import("class-variance-authority/dist/types").ClassProp) => string;
|
|
7
7
|
declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
|
|
8
8
|
asChild?: boolean;
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { getInitialInfo } from "../utils/getInitialInfo.js";
|
|
2
2
|
import { isSparkRuntime } from "../utils/utils.js";
|
|
3
3
|
async function getAppInfo() {
|
|
4
|
-
let appInfo = window._appInfo;
|
|
4
|
+
let appInfo = 'undefined' != typeof window ? window._appInfo : void 0;
|
|
5
5
|
if (!appInfo && isSparkRuntime()) {
|
|
6
|
-
const info = (await getInitialInfo())
|
|
6
|
+
const info = (await getInitialInfo())?.app_info;
|
|
7
7
|
appInfo = {
|
|
8
8
|
name: info?.app_name || '',
|
|
9
9
|
avatar: info?.app_avatar || ''
|
|
10
10
|
};
|
|
11
|
+
if (appInfo.name) appInfo.name = appInfo.name.trim();
|
|
12
|
+
if ('undefined' != typeof window) window._appInfo = appInfo;
|
|
13
|
+
if ('undefined' != typeof document && document.head) {
|
|
14
|
+
if (appInfo.name) document.title = appInfo.name;
|
|
15
|
+
let link = document.querySelector("link[rel~='icon']");
|
|
16
|
+
if (!link) {
|
|
17
|
+
link = document.createElement('link');
|
|
18
|
+
link.rel = 'icon';
|
|
19
|
+
document.head.appendChild(link);
|
|
20
|
+
}
|
|
21
|
+
link.href = appInfo.avatar;
|
|
22
|
+
}
|
|
11
23
|
}
|
|
12
24
|
return appInfo ?? {};
|
|
13
25
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { BatchLogger, batchLogInfo, initBatchLogger } from "../batch-logger.js";
|
|
3
|
+
var __webpack_require__ = {};
|
|
4
|
+
(()=>{
|
|
5
|
+
__webpack_require__.g = (()=>{
|
|
6
|
+
if ('object' == typeof globalThis) return globalThis;
|
|
7
|
+
try {
|
|
8
|
+
return this || new Function('return this')();
|
|
9
|
+
} catch (e) {
|
|
10
|
+
if ('object' == typeof window) return window;
|
|
11
|
+
}
|
|
12
|
+
})();
|
|
13
|
+
})();
|
|
14
|
+
vi.mock('node-fetch', ()=>({
|
|
15
|
+
default: vi.fn()
|
|
16
|
+
}));
|
|
17
|
+
const mockConsole = {
|
|
18
|
+
debug: vi.fn(),
|
|
19
|
+
info: vi.fn(),
|
|
20
|
+
warn: vi.fn(),
|
|
21
|
+
error: vi.fn(),
|
|
22
|
+
log: vi.fn()
|
|
23
|
+
};
|
|
24
|
+
describe('BatchLogger', ()=>{
|
|
25
|
+
let batchLogger;
|
|
26
|
+
let mockFetch;
|
|
27
|
+
beforeEach(()=>{
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
mockFetch = vi.fn();
|
|
30
|
+
__webpack_require__.g.fetch = mockFetch;
|
|
31
|
+
const mockResponse = new Response(JSON.stringify({
|
|
32
|
+
success: true
|
|
33
|
+
}), {
|
|
34
|
+
status: 200,
|
|
35
|
+
statusText: 'OK',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json'
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
mockFetch.mockResolvedValue(mockResponse);
|
|
41
|
+
if ('undefined' == typeof window) __webpack_require__.g.window = void 0;
|
|
42
|
+
});
|
|
43
|
+
afterEach(async ()=>{
|
|
44
|
+
if (batchLogger) await batchLogger.destroy();
|
|
45
|
+
});
|
|
46
|
+
afterAll(()=>{
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
describe('Basic Functionality', ()=>{
|
|
50
|
+
it('should create a BatchLogger instance', ()=>{
|
|
51
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
52
|
+
userId: 'user123',
|
|
53
|
+
tenantId: 'tenant456',
|
|
54
|
+
appId: 'app789'
|
|
55
|
+
});
|
|
56
|
+
expect(batchLogger).toBeInstanceOf(BatchLogger);
|
|
57
|
+
});
|
|
58
|
+
it('should add logs to queue', ()=>{
|
|
59
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
60
|
+
userId: 'user123',
|
|
61
|
+
tenantId: 'tenant456',
|
|
62
|
+
appId: 'app789'
|
|
63
|
+
});
|
|
64
|
+
batchLogger.batchLog('info', 'Test message', 'test-source');
|
|
65
|
+
expect(batchLogger.getQueueSize()).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
it('should respect batch size limit', async ()=>{
|
|
68
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
69
|
+
userId: 'user123',
|
|
70
|
+
tenantId: 'tenant456',
|
|
71
|
+
appId: 'app789',
|
|
72
|
+
sizeThreshold: 3,
|
|
73
|
+
flushInterval: 10000
|
|
74
|
+
});
|
|
75
|
+
for(let i = 0; i < 5; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
76
|
+
await new Promise((resolve)=>setTimeout(resolve, 200));
|
|
77
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
78
|
+
expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
79
|
+
expect(batchLogger.getQueueSize()).toBeLessThanOrEqual(2);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('Log Preservation', ()=>{
|
|
83
|
+
it('should preserve all logs without merging', ()=>{
|
|
84
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
85
|
+
userId: 'user123',
|
|
86
|
+
tenantId: 'tenant456',
|
|
87
|
+
appId: 'app789'
|
|
88
|
+
});
|
|
89
|
+
for(let i = 0; i < 5; i++)batchLogger.batchLog('error', 'Same error message');
|
|
90
|
+
expect(batchLogger.getQueueSize()).toBe(5);
|
|
91
|
+
});
|
|
92
|
+
it('should handle different log levels independently', ()=>{
|
|
93
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
94
|
+
userId: 'user123',
|
|
95
|
+
tenantId: 'tenant456',
|
|
96
|
+
appId: 'app789'
|
|
97
|
+
});
|
|
98
|
+
batchLogger.batchLog('info', 'Test message');
|
|
99
|
+
batchLogger.batchLog('info', 'Test message');
|
|
100
|
+
batchLogger.batchLog('warn', 'Test message');
|
|
101
|
+
batchLogger.batchLog('warn', 'Test message');
|
|
102
|
+
expect(batchLogger.getQueueSize()).toBe(4);
|
|
103
|
+
});
|
|
104
|
+
it('should handle large volumes of logs efficiently', ()=>{
|
|
105
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
106
|
+
userId: 'user123',
|
|
107
|
+
tenantId: 'tenant456',
|
|
108
|
+
appId: 'app789',
|
|
109
|
+
sizeThreshold: 1000,
|
|
110
|
+
flushInterval: 10000
|
|
111
|
+
});
|
|
112
|
+
for(let i = 0; i < 200; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
113
|
+
expect(batchLogger.getQueueSize()).toBe(200);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('Retry Mechanism', ()=>{
|
|
117
|
+
it('should retry failed requests', async ()=>{
|
|
118
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
119
|
+
userId: 'user123',
|
|
120
|
+
tenantId: 'tenant456',
|
|
121
|
+
appId: 'app789',
|
|
122
|
+
maxRetries: 2,
|
|
123
|
+
retryDelay: 100,
|
|
124
|
+
flushInterval: 10000
|
|
125
|
+
});
|
|
126
|
+
const successResponse = new Response(JSON.stringify({
|
|
127
|
+
success: true
|
|
128
|
+
}), {
|
|
129
|
+
status: 200,
|
|
130
|
+
statusText: 'OK',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json'
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
mockFetch.mockRejectedValueOnce(new Error('Network error')).mockRejectedValueOnce(new Error('Network error')).mockResolvedValueOnce(successResponse);
|
|
136
|
+
batchLogger.batchLog('info', 'Test message');
|
|
137
|
+
await batchLogger.flush();
|
|
138
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
139
|
+
expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(3);
|
|
140
|
+
});
|
|
141
|
+
it('should handle permanent failures', async ()=>{
|
|
142
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
143
|
+
userId: 'user123',
|
|
144
|
+
tenantId: 'tenant456',
|
|
145
|
+
appId: 'app789',
|
|
146
|
+
maxRetries: 1,
|
|
147
|
+
retryDelay: 100,
|
|
148
|
+
flushInterval: 10000
|
|
149
|
+
});
|
|
150
|
+
mockFetch.mockRejectedValue(new Error('Permanent error'));
|
|
151
|
+
batchLogger.batchLog('info', 'Test message');
|
|
152
|
+
await batchLogger.flush();
|
|
153
|
+
expect(mockConsole.warn).toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('Auto Flush', ()=>{
|
|
157
|
+
it('should auto flush based on interval', async ()=>{
|
|
158
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
159
|
+
userId: 'user123',
|
|
160
|
+
tenantId: 'tenant456',
|
|
161
|
+
appId: 'app789',
|
|
162
|
+
flushInterval: 1000,
|
|
163
|
+
sizeThreshold: 100
|
|
164
|
+
});
|
|
165
|
+
for(let i = 0; i < 3; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
166
|
+
await new Promise((resolve)=>setTimeout(resolve, 1200));
|
|
167
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
168
|
+
expect(batchLogger.getQueueSize()).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe('Request Configuration', ()=>{
|
|
172
|
+
it('should include custom headers', async ()=>{
|
|
173
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
174
|
+
userId: 'user123',
|
|
175
|
+
tenantId: 'tenant456',
|
|
176
|
+
appId: 'app789',
|
|
177
|
+
endpoint: 'https://api.example.com/logs',
|
|
178
|
+
headers: {
|
|
179
|
+
Authorization: 'Bearer token123',
|
|
180
|
+
'X-Custom-Header': 'custom-value'
|
|
181
|
+
},
|
|
182
|
+
flushInterval: 1000
|
|
183
|
+
});
|
|
184
|
+
batchLogger.batchLog('info', 'Test message');
|
|
185
|
+
await batchLogger.flush();
|
|
186
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/logs', expect.objectContaining({
|
|
187
|
+
headers: expect.objectContaining({
|
|
188
|
+
Authorization: 'Bearer token123',
|
|
189
|
+
'X-Custom-Header': 'custom-value'
|
|
190
|
+
})
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
193
|
+
it('should send correct batch format', async ()=>{
|
|
194
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
195
|
+
userId: 'user123',
|
|
196
|
+
tenantId: 'tenant456',
|
|
197
|
+
appId: 'app789',
|
|
198
|
+
flushInterval: 10000
|
|
199
|
+
});
|
|
200
|
+
batchLogger.batchLog('info', 'Test message', 'test-source');
|
|
201
|
+
await batchLogger.flush();
|
|
202
|
+
expect(mockFetch).toHaveBeenCalledWith('/dev/logs/collect-batch', expect.objectContaining({
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: {
|
|
205
|
+
'Content-Type': 'application/json'
|
|
206
|
+
}
|
|
207
|
+
}));
|
|
208
|
+
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
209
|
+
expect(Array.isArray(callBody)).toBe(true);
|
|
210
|
+
expect(callBody).toHaveLength(1);
|
|
211
|
+
expect(callBody[0]).toMatchObject({
|
|
212
|
+
level: 'info',
|
|
213
|
+
message: 'Test message',
|
|
214
|
+
source: 'test-source',
|
|
215
|
+
user_id: 'user123',
|
|
216
|
+
tenant_id: 'tenant456',
|
|
217
|
+
app_id: 'app789'
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('Global Instance Management', ()=>{
|
|
222
|
+
it('should initialize global batch logger instance', ()=>{
|
|
223
|
+
initBatchLogger(mockConsole, {
|
|
224
|
+
userId: 'user123',
|
|
225
|
+
tenantId: 'tenant456',
|
|
226
|
+
appId: 'app789'
|
|
227
|
+
});
|
|
228
|
+
expect(()=>batchLogInfo('info', 'Test message')).not.toThrow();
|
|
229
|
+
});
|
|
230
|
+
it('should handle batchLogInfo when no instance exists', ()=>{
|
|
231
|
+
expect(()=>batchLogInfo('info', 'Test message')).not.toThrow();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('Configuration Updates', ()=>{
|
|
235
|
+
it('should update configuration', ()=>{
|
|
236
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
237
|
+
userId: 'user123',
|
|
238
|
+
tenantId: 'tenant456',
|
|
239
|
+
appId: 'app789',
|
|
240
|
+
sizeThreshold: 10
|
|
241
|
+
});
|
|
242
|
+
batchLogger.updateConfig({
|
|
243
|
+
sizeThreshold: 20,
|
|
244
|
+
maxRetries: 5
|
|
245
|
+
});
|
|
246
|
+
for(let i = 0; i < 21; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
247
|
+
expect(batchLogger.getQueueSize()).toBe(1);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
describe('High Volume Log Processing', ()=>{
|
|
251
|
+
it('should handle large batch of logs without crashing', async ()=>{
|
|
252
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
253
|
+
userId: 'user123',
|
|
254
|
+
tenantId: 'tenant456',
|
|
255
|
+
appId: 'app789',
|
|
256
|
+
sizeThreshold: 50,
|
|
257
|
+
flushInterval: 1000
|
|
258
|
+
});
|
|
259
|
+
const largeBatchSize = 1000;
|
|
260
|
+
const startTime = Date.now();
|
|
261
|
+
for(let i = 0; i < largeBatchSize; i++)batchLogger.batchLog('info', `High volume log message ${i % 100}`, 'stress-test');
|
|
262
|
+
const endTime = Date.now();
|
|
263
|
+
expect(endTime - startTime).toBeLessThan(5000);
|
|
264
|
+
await new Promise((resolve)=>setTimeout(resolve, 2000));
|
|
265
|
+
expect(batchLogger.getQueueSize()).toBe(0);
|
|
266
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
267
|
+
const callCount = mockFetch.mock.calls.length;
|
|
268
|
+
expect(callCount).toBeGreaterThan(0);
|
|
269
|
+
expect(callCount).toBeLessThanOrEqual(25);
|
|
270
|
+
});
|
|
271
|
+
it('should control request frequency under high load', async ()=>{
|
|
272
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
273
|
+
userId: 'user123',
|
|
274
|
+
tenantId: 'tenant456',
|
|
275
|
+
appId: 'app789',
|
|
276
|
+
sizeThreshold: 30,
|
|
277
|
+
flushInterval: 500
|
|
278
|
+
});
|
|
279
|
+
const requestsBefore = mockFetch.mock.calls.length;
|
|
280
|
+
for(let batch = 0; batch < 5; batch++){
|
|
281
|
+
for(let i = 0; i < 25; i++)batchLogger.batchLog('error', `Error message ${batch}`, 'high-frequency-test');
|
|
282
|
+
await new Promise((resolve)=>setTimeout(resolve, 50));
|
|
283
|
+
}
|
|
284
|
+
await new Promise((resolve)=>setTimeout(resolve, 1000));
|
|
285
|
+
const totalRequests = mockFetch.mock.calls.length - requestsBefore;
|
|
286
|
+
expect(totalRequests).toBeGreaterThan(0);
|
|
287
|
+
expect(totalRequests).toBeLessThanOrEqual(10);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
describe('Edge Cases', ()=>{
|
|
291
|
+
it('should handle empty flush', async ()=>{
|
|
292
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
293
|
+
userId: 'user123',
|
|
294
|
+
tenantId: 'tenant456',
|
|
295
|
+
appId: 'app789'
|
|
296
|
+
});
|
|
297
|
+
await batchLogger.flush();
|
|
298
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
it('should handle concurrent flush calls safely', async ()=>{
|
|
301
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
302
|
+
userId: 'user123',
|
|
303
|
+
tenantId: 'tenant456',
|
|
304
|
+
appId: 'app789',
|
|
305
|
+
flushInterval: 10000
|
|
306
|
+
});
|
|
307
|
+
for(let i = 0; i < 20; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
308
|
+
const flushPromises = [];
|
|
309
|
+
for(let i = 0; i < 10; i++)flushPromises.push(batchLogger.flush());
|
|
310
|
+
await Promise.all(flushPromises);
|
|
311
|
+
expect(batchLogger.getQueueSize()).toBe(0);
|
|
312
|
+
expect(mockFetch.mock.calls.length).toBeGreaterThan(0);
|
|
313
|
+
expect(mockFetch.mock.calls.length).toBeLessThanOrEqual(3);
|
|
314
|
+
});
|
|
315
|
+
it('should ensure no logs remain after destroy', async ()=>{
|
|
316
|
+
mockFetch.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
317
|
+
success: true
|
|
318
|
+
}), {
|
|
319
|
+
status: 200,
|
|
320
|
+
statusText: 'OK',
|
|
321
|
+
headers: {
|
|
322
|
+
'Content-Type': 'application/json'
|
|
323
|
+
}
|
|
324
|
+
}));
|
|
325
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
326
|
+
userId: 'user123',
|
|
327
|
+
tenantId: 'tenant456',
|
|
328
|
+
appId: 'app789',
|
|
329
|
+
flushInterval: 10000,
|
|
330
|
+
sizeThreshold: 1000
|
|
331
|
+
});
|
|
332
|
+
for(let i = 0; i < 50; i++){
|
|
333
|
+
batchLogger.batchLog('info', `Test message info ${i}`);
|
|
334
|
+
batchLogger.batchLog('warn', `Warning message warn ${i}`);
|
|
335
|
+
}
|
|
336
|
+
const queueSize = batchLogger.getQueueSize();
|
|
337
|
+
expect(queueSize).toBe(100);
|
|
338
|
+
const fetchCallsBefore = mockFetch.mock.calls.length;
|
|
339
|
+
await batchLogger.destroy();
|
|
340
|
+
const queueSizeAfter = batchLogger.getQueueSize();
|
|
341
|
+
const fetchCallsAfter = mockFetch.mock.calls.length;
|
|
342
|
+
expect(queueSizeAfter).toBe(0);
|
|
343
|
+
expect(fetchCallsAfter).toBeGreaterThan(fetchCallsBefore);
|
|
344
|
+
});
|
|
345
|
+
it('should handle environment detection without browser APIs', ()=>{
|
|
346
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
347
|
+
userId: 'user123',
|
|
348
|
+
tenantId: 'tenant456',
|
|
349
|
+
appId: 'app789'
|
|
350
|
+
});
|
|
351
|
+
batchLogger.batchLog('info', 'Test message');
|
|
352
|
+
expect(batchLogger.getQueueSize()).toBe(1);
|
|
353
|
+
});
|
|
354
|
+
it('should handle destroy method with pending logs', async ()=>{
|
|
355
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
356
|
+
userId: 'user123',
|
|
357
|
+
tenantId: 'tenant456',
|
|
358
|
+
appId: 'app789'
|
|
359
|
+
});
|
|
360
|
+
batchLogger.batchLog('info', 'Test message');
|
|
361
|
+
batchLogger.batchLog('warn', 'Warning message');
|
|
362
|
+
await batchLogger.destroy();
|
|
363
|
+
expect(batchLogger.getQueueSize()).toBe(0);
|
|
364
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
interface BatchLoggerConfig {
|
|
2
|
+
/** 用户ID */
|
|
3
|
+
userId: string;
|
|
4
|
+
/** 租户ID */
|
|
5
|
+
tenantId: string;
|
|
6
|
+
/** 应用ID */
|
|
7
|
+
appId: string;
|
|
8
|
+
/** 后端接收日志的接口地址,默认为/dev/logs/collect */
|
|
9
|
+
endpoint?: string;
|
|
10
|
+
/** 达到此数量时触发刷新 */
|
|
11
|
+
sizeThreshold?: number;
|
|
12
|
+
/** 自动刷新的时间间隔(毫秒) */
|
|
13
|
+
flushInterval?: number;
|
|
14
|
+
/** 最大重试次数 */
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
/** 重试延迟(毫秒) */
|
|
17
|
+
retryDelay?: number;
|
|
18
|
+
/** 自定义请求头 */
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
export declare class BatchLogger {
|
|
22
|
+
private config;
|
|
23
|
+
private logQueue;
|
|
24
|
+
private flushTimer;
|
|
25
|
+
private isProcessing;
|
|
26
|
+
private originConsole;
|
|
27
|
+
constructor(console: Console, config?: BatchLoggerConfig);
|
|
28
|
+
/**
|
|
29
|
+
* 批量记录日志(对外暴露的唯一方法)
|
|
30
|
+
*/
|
|
31
|
+
batchLog(level: string, message: string, source?: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* 刷新日志队列,全部发送
|
|
34
|
+
*/
|
|
35
|
+
flush(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* 发送日志批次到后端
|
|
38
|
+
*/
|
|
39
|
+
private sendBatch;
|
|
40
|
+
/**
|
|
41
|
+
* 执行实际的fetch请求
|
|
42
|
+
*/
|
|
43
|
+
private execFetch;
|
|
44
|
+
/**
|
|
45
|
+
* 启动自动刷新定时器
|
|
46
|
+
*/
|
|
47
|
+
private startFlushTimer;
|
|
48
|
+
/**
|
|
49
|
+
* 设置页面卸载时的处理
|
|
50
|
+
*/
|
|
51
|
+
private setupBeforeUnloadHandler;
|
|
52
|
+
/**
|
|
53
|
+
* 延迟函数
|
|
54
|
+
*/
|
|
55
|
+
private delay;
|
|
56
|
+
/**
|
|
57
|
+
* 生成唯一ID
|
|
58
|
+
*/
|
|
59
|
+
private generateId;
|
|
60
|
+
/**
|
|
61
|
+
* 销毁资源
|
|
62
|
+
*/
|
|
63
|
+
destroy(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* 获取队列大小
|
|
66
|
+
*/
|
|
67
|
+
getQueueSize(): number;
|
|
68
|
+
/**
|
|
69
|
+
* 更新配置
|
|
70
|
+
*/
|
|
71
|
+
updateConfig(newConfig: Partial<BatchLoggerConfig>): void;
|
|
72
|
+
}
|
|
73
|
+
export declare function getBatchLogger(): BatchLogger;
|
|
74
|
+
export declare function initBatchLogger(oldConsole: Console, config?: BatchLoggerConfig): void;
|
|
75
|
+
/** 记录日志进行批量同步 */
|
|
76
|
+
export declare function batchLogInfo(level: string, message: string, source?: string): void;
|
|
77
|
+
export declare function destroyBatchLogger(): void;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
class BatchLogger {
|
|
2
|
+
config;
|
|
3
|
+
logQueue = [];
|
|
4
|
+
flushTimer = null;
|
|
5
|
+
isProcessing = false;
|
|
6
|
+
originConsole;
|
|
7
|
+
constructor(console, config){
|
|
8
|
+
this.originConsole = {
|
|
9
|
+
...console
|
|
10
|
+
};
|
|
11
|
+
const { userId = '', tenantId = '', appId = '' } = window || {};
|
|
12
|
+
this.config = {
|
|
13
|
+
userId,
|
|
14
|
+
tenantId,
|
|
15
|
+
appId,
|
|
16
|
+
endpoint: '/dev/logs/collect-batch',
|
|
17
|
+
sizeThreshold: 20,
|
|
18
|
+
flushInterval: 1000,
|
|
19
|
+
maxRetries: 3,
|
|
20
|
+
retryDelay: 500,
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json'
|
|
23
|
+
},
|
|
24
|
+
...config || {}
|
|
25
|
+
};
|
|
26
|
+
this.startFlushTimer();
|
|
27
|
+
this.setupBeforeUnloadHandler();
|
|
28
|
+
}
|
|
29
|
+
batchLog(level, message, source) {
|
|
30
|
+
const logEntry = {
|
|
31
|
+
id: this.generateId(),
|
|
32
|
+
level,
|
|
33
|
+
message,
|
|
34
|
+
source,
|
|
35
|
+
timestamp: Date.now()
|
|
36
|
+
};
|
|
37
|
+
this.logQueue.push(logEntry);
|
|
38
|
+
if (this.logQueue.length >= this.config.sizeThreshold) this.flush();
|
|
39
|
+
}
|
|
40
|
+
async flush() {
|
|
41
|
+
if (this.isProcessing || 0 === this.logQueue.length) return;
|
|
42
|
+
this.isProcessing = true;
|
|
43
|
+
const logsToSend = this.logQueue.splice(0, this.logQueue.length);
|
|
44
|
+
try {
|
|
45
|
+
await this.sendBatch(logsToSend);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.logQueue.unshift(...logsToSend);
|
|
48
|
+
} finally{
|
|
49
|
+
this.isProcessing = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async sendBatch(logs) {
|
|
53
|
+
const collectLogs = logs.map((log)=>({
|
|
54
|
+
level: log.level,
|
|
55
|
+
message: log.message,
|
|
56
|
+
time: new Date(log.timestamp).toISOString(),
|
|
57
|
+
source: log.source,
|
|
58
|
+
user_id: this.config.userId,
|
|
59
|
+
tenant_id: this.config.tenantId,
|
|
60
|
+
app_id: this.config.appId
|
|
61
|
+
}));
|
|
62
|
+
let retries = 0;
|
|
63
|
+
while(retries <= this.config.maxRetries)try {
|
|
64
|
+
await this.execFetch(this.config.endpoint, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: this.config.headers,
|
|
67
|
+
body: JSON.stringify(collectLogs)
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
retries++;
|
|
72
|
+
if (retries > this.config.maxRetries) this.originConsole.error(`Failed to send logs (attempt ${retries}), retrying in ${this.config.retryDelay}ms...`);
|
|
73
|
+
else this.originConsole.warn(`Failed to send logs (attempt ${retries}), retrying in ${this.config.retryDelay}ms...`);
|
|
74
|
+
await this.delay(this.config.retryDelay * retries);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async execFetch(url, options) {
|
|
78
|
+
return fetch(url, options);
|
|
79
|
+
}
|
|
80
|
+
startFlushTimer() {
|
|
81
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
82
|
+
this.flushTimer = setInterval(()=>{
|
|
83
|
+
if (this.logQueue.length > 0) this.flush();
|
|
84
|
+
}, this.config.flushInterval);
|
|
85
|
+
}
|
|
86
|
+
setupBeforeUnloadHandler() {
|
|
87
|
+
if ('undefined' != typeof window) window.addEventListener('beforeunload', ()=>{
|
|
88
|
+
this.flush().finally(()=>{
|
|
89
|
+
this.destroy();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
delay(ms) {
|
|
94
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
95
|
+
}
|
|
96
|
+
generateId() {
|
|
97
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
98
|
+
}
|
|
99
|
+
async destroy() {
|
|
100
|
+
if (this.flushTimer) {
|
|
101
|
+
clearInterval(this.flushTimer);
|
|
102
|
+
this.flushTimer = null;
|
|
103
|
+
}
|
|
104
|
+
if (this.logQueue.length > 0) await this.flush();
|
|
105
|
+
}
|
|
106
|
+
getQueueSize() {
|
|
107
|
+
return this.logQueue.length;
|
|
108
|
+
}
|
|
109
|
+
updateConfig(newConfig) {
|
|
110
|
+
this.config = {
|
|
111
|
+
...this.config,
|
|
112
|
+
...newConfig
|
|
113
|
+
};
|
|
114
|
+
this.startFlushTimer();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let defaultBatchLogger = null;
|
|
118
|
+
function getBatchLogger() {
|
|
119
|
+
return defaultBatchLogger;
|
|
120
|
+
}
|
|
121
|
+
function initBatchLogger(oldConsole, config) {
|
|
122
|
+
defaultBatchLogger = new BatchLogger(oldConsole, config);
|
|
123
|
+
}
|
|
124
|
+
function batchLogInfo(level, message, source) {
|
|
125
|
+
if (!defaultBatchLogger) return;
|
|
126
|
+
defaultBatchLogger.batchLog(level, message, source);
|
|
127
|
+
}
|
|
128
|
+
function destroyBatchLogger() {
|
|
129
|
+
if (defaultBatchLogger) {
|
|
130
|
+
defaultBatchLogger.destroy();
|
|
131
|
+
defaultBatchLogger = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export { BatchLogger, batchLogInfo, destroyBatchLogger, getBatchLogger, initBatchLogger };
|
|
@@ -48,13 +48,4 @@ export interface ParentApi {
|
|
|
48
48
|
export interface ChildApi {
|
|
49
49
|
getRoutes: () => Promise<any[]>;
|
|
50
50
|
getSourceMap: () => Promise<string>;
|
|
51
|
-
apiProxy: {
|
|
52
|
-
api_get: (url: string, config?: any) => Promise<any>;
|
|
53
|
-
api_post: (url: string, data?: any, config?: any) => Promise<any>;
|
|
54
|
-
api_put: (url: string, data?: any, config?: any) => Promise<any>;
|
|
55
|
-
api_delete: (url: string, config?: any) => Promise<any>;
|
|
56
|
-
api_patch: (url: string, data?: any, config?: any) => Promise<any>;
|
|
57
|
-
api_head: (url: string, config?: any) => Promise<any>;
|
|
58
|
-
api_options: (url: string, config?: any) => Promise<any>;
|
|
59
|
-
};
|
|
60
51
|
}
|
package/package.json
CHANGED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP 请求方法类型
|
|
3
|
-
*/
|
|
4
|
-
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
|
5
|
-
/**
|
|
6
|
-
* 请求配置接口
|
|
7
|
-
*/
|
|
8
|
-
export interface RequestConfig {
|
|
9
|
-
/** 请求 URL */
|
|
10
|
-
url: string;
|
|
11
|
-
/** 请求方法 */
|
|
12
|
-
method?: HttpMethod;
|
|
13
|
-
/** 请求头 */
|
|
14
|
-
headers?: Record<string, string>;
|
|
15
|
-
/** 请求参数(用于 GET 请求的查询参数) */
|
|
16
|
-
params?: Record<string, any>;
|
|
17
|
-
/** 请求体数据 */
|
|
18
|
-
data?: any;
|
|
19
|
-
/** 超时时间(毫秒) */
|
|
20
|
-
timeout?: number;
|
|
21
|
-
/** 响应类型 */
|
|
22
|
-
responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData';
|
|
23
|
-
/** 是否携带凭证 */
|
|
24
|
-
credentials?: RequestCredentials;
|
|
25
|
-
/** AbortSignal 用于取消请求 */
|
|
26
|
-
signal?: AbortSignal;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* 响应数据接口
|
|
30
|
-
*/
|
|
31
|
-
export interface ApiResponse<T = any> {
|
|
32
|
-
/** 响应数据 */
|
|
33
|
-
data: T;
|
|
34
|
-
/** HTTP 状态码 */
|
|
35
|
-
status: number;
|
|
36
|
-
/** 状态文本 */
|
|
37
|
-
statusText: string;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* API 错误类
|
|
41
|
-
*/
|
|
42
|
-
export declare class ApiError extends Error {
|
|
43
|
-
status?: number;
|
|
44
|
-
statusText?: string;
|
|
45
|
-
response?: Response;
|
|
46
|
-
config?: RequestConfig;
|
|
47
|
-
code?: string;
|
|
48
|
-
constructor(message: string, config?: RequestConfig, response?: Response, code?: string);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* 请求拦截器类型
|
|
52
|
-
*/
|
|
53
|
-
export type RequestInterceptor = (config: RequestConfig) => RequestConfig | Promise<RequestConfig>;
|
|
54
|
-
/**
|
|
55
|
-
* 响应拦截器类型
|
|
56
|
-
*/
|
|
57
|
-
export type ResponseInterceptor = <T = any>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>;
|
|
58
|
-
/**
|
|
59
|
-
* 错误拦截器类型
|
|
60
|
-
*/
|
|
61
|
-
export type ErrorInterceptor = (error: ApiError) => any;
|
|
62
|
-
/**
|
|
63
|
-
* 通用接口测试 API 代理类
|
|
64
|
-
*
|
|
65
|
-
* 特性:
|
|
66
|
-
* - 支持所有 RESTful 请求方法 (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
|
|
67
|
-
* - 完善的错误处理和边界检查
|
|
68
|
-
* - 请求/响应拦截器
|
|
69
|
-
* - 超时控制和请求取消
|
|
70
|
-
* - 支持多种响应类型
|
|
71
|
-
*/
|
|
72
|
-
declare class ApiProxy {
|
|
73
|
-
/** 默认配置 */
|
|
74
|
-
private defaultConfig;
|
|
75
|
-
/** 请求拦截器队列 */
|
|
76
|
-
private requestInterceptors;
|
|
77
|
-
/** 响应拦截器队列 */
|
|
78
|
-
private responseInterceptors;
|
|
79
|
-
/** 错误拦截器队列 */
|
|
80
|
-
private errorInterceptors;
|
|
81
|
-
/** 活跃的请求控制器 Map */
|
|
82
|
-
private activeRequests;
|
|
83
|
-
/**
|
|
84
|
-
* 构造函数
|
|
85
|
-
* @param baseConfig 基础配置
|
|
86
|
-
*/
|
|
87
|
-
constructor(baseConfig?: Partial<RequestConfig>);
|
|
88
|
-
/**
|
|
89
|
-
* 添加请求拦截器
|
|
90
|
-
*/
|
|
91
|
-
useRequestInterceptor(interceptor: RequestInterceptor): void;
|
|
92
|
-
/**
|
|
93
|
-
* 添加响应拦截器
|
|
94
|
-
*/
|
|
95
|
-
useResponseInterceptor(interceptor: ResponseInterceptor): void;
|
|
96
|
-
/**
|
|
97
|
-
* 添加错误拦截器
|
|
98
|
-
*/
|
|
99
|
-
useErrorInterceptor(interceptor: ErrorInterceptor): void;
|
|
100
|
-
/**
|
|
101
|
-
* 验证 URL 格式
|
|
102
|
-
*/
|
|
103
|
-
private validateUrl;
|
|
104
|
-
/**
|
|
105
|
-
* 构建完整 URL (包含查询参数)
|
|
106
|
-
*/
|
|
107
|
-
private buildUrl;
|
|
108
|
-
/**
|
|
109
|
-
* 序列化请求体
|
|
110
|
-
*/
|
|
111
|
-
private serializeData;
|
|
112
|
-
/**
|
|
113
|
-
* 解析响应数据
|
|
114
|
-
*/
|
|
115
|
-
private parseResponse;
|
|
116
|
-
/**
|
|
117
|
-
* 执行请求拦截器
|
|
118
|
-
*/
|
|
119
|
-
private runRequestInterceptors;
|
|
120
|
-
/**
|
|
121
|
-
* 执行响应拦截器
|
|
122
|
-
*/
|
|
123
|
-
private runResponseInterceptors;
|
|
124
|
-
/**
|
|
125
|
-
* 执行错误拦截器
|
|
126
|
-
*/
|
|
127
|
-
private runErrorInterceptors;
|
|
128
|
-
/**
|
|
129
|
-
* 生成请求唯一键
|
|
130
|
-
*/
|
|
131
|
-
private generateRequestKey;
|
|
132
|
-
/**
|
|
133
|
-
* 核心请求方法
|
|
134
|
-
*/
|
|
135
|
-
request<T = any>(config: RequestConfig): Promise<ApiResponse<T>>;
|
|
136
|
-
/**
|
|
137
|
-
* 执行实际请求
|
|
138
|
-
*/
|
|
139
|
-
private executeRequest;
|
|
140
|
-
/**
|
|
141
|
-
* GET 请求
|
|
142
|
-
*/
|
|
143
|
-
get<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<ApiResponse<T>>;
|
|
144
|
-
/**
|
|
145
|
-
* POST 请求
|
|
146
|
-
*/
|
|
147
|
-
post<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<ApiResponse<T>>;
|
|
148
|
-
/**
|
|
149
|
-
* PUT 请求
|
|
150
|
-
*/
|
|
151
|
-
put<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<ApiResponse<T>>;
|
|
152
|
-
/**
|
|
153
|
-
* DELETE 请求
|
|
154
|
-
*/
|
|
155
|
-
delete<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<ApiResponse<T>>;
|
|
156
|
-
/**
|
|
157
|
-
* PATCH 请求
|
|
158
|
-
*/
|
|
159
|
-
patch<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<ApiResponse<T>>;
|
|
160
|
-
/**
|
|
161
|
-
* HEAD 请求
|
|
162
|
-
*/
|
|
163
|
-
head<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<ApiResponse<T>>;
|
|
164
|
-
/**
|
|
165
|
-
* OPTIONS 请求
|
|
166
|
-
*/
|
|
167
|
-
options<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method' | 'data'>): Promise<ApiResponse<T>>;
|
|
168
|
-
/**
|
|
169
|
-
* 取消指定的请求
|
|
170
|
-
*/
|
|
171
|
-
cancelRequest(requestKey: string): void;
|
|
172
|
-
/**
|
|
173
|
-
* 取消所有活跃请求
|
|
174
|
-
*/
|
|
175
|
-
cancelAllRequests(): void;
|
|
176
|
-
/**
|
|
177
|
-
* 获取活跃请求数量
|
|
178
|
-
*/
|
|
179
|
-
getActiveRequestCount(): number;
|
|
180
|
-
}
|
|
181
|
-
declare const apiProxy: ApiProxy;
|
|
182
|
-
export default apiProxy;
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
class ApiError extends Error {
|
|
2
|
-
status;
|
|
3
|
-
statusText;
|
|
4
|
-
response;
|
|
5
|
-
config;
|
|
6
|
-
code;
|
|
7
|
-
constructor(message, config, response, code){
|
|
8
|
-
super(message);
|
|
9
|
-
this.name = 'ApiError';
|
|
10
|
-
this.config = config;
|
|
11
|
-
this.response = response;
|
|
12
|
-
this.status = response?.status;
|
|
13
|
-
this.statusText = response?.statusText;
|
|
14
|
-
this.code = code;
|
|
15
|
-
Object.setPrototypeOf(this, ApiError.prototype);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
class ApiProxy {
|
|
19
|
-
defaultConfig = {
|
|
20
|
-
timeout: 30000,
|
|
21
|
-
responseType: 'json',
|
|
22
|
-
credentials: 'same-origin',
|
|
23
|
-
headers: {
|
|
24
|
-
'Content-Type': 'application/json',
|
|
25
|
-
'X-Suda-Csrf-Token': window.csrfToken || '',
|
|
26
|
-
'Cache-Control': 'no-store'
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
requestInterceptors = [];
|
|
30
|
-
responseInterceptors = [];
|
|
31
|
-
errorInterceptors = [];
|
|
32
|
-
activeRequests = new Map();
|
|
33
|
-
constructor(baseConfig){
|
|
34
|
-
if (baseConfig) this.defaultConfig = {
|
|
35
|
-
...this.defaultConfig,
|
|
36
|
-
...baseConfig
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
useRequestInterceptor(interceptor) {
|
|
40
|
-
if ('function' != typeof interceptor) throw new TypeError('Request interceptor must be a function');
|
|
41
|
-
this.requestInterceptors.push(interceptor);
|
|
42
|
-
}
|
|
43
|
-
useResponseInterceptor(interceptor) {
|
|
44
|
-
if ('function' != typeof interceptor) throw new TypeError('Response interceptor must be a function');
|
|
45
|
-
this.responseInterceptors.push(interceptor);
|
|
46
|
-
}
|
|
47
|
-
useErrorInterceptor(interceptor) {
|
|
48
|
-
if ('function' != typeof interceptor) throw new TypeError('Error interceptor must be a function');
|
|
49
|
-
this.errorInterceptors.push(interceptor);
|
|
50
|
-
}
|
|
51
|
-
validateUrl(url) {
|
|
52
|
-
if (!url || 'string' != typeof url) return false;
|
|
53
|
-
if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) return true;
|
|
54
|
-
try {
|
|
55
|
-
new URL(url);
|
|
56
|
-
return true;
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
buildUrl(url, params) {
|
|
62
|
-
if (!params || 0 === Object.keys(params).length) return url;
|
|
63
|
-
try {
|
|
64
|
-
const urlObj = new URL(url, window.location.origin);
|
|
65
|
-
Object.entries(params).forEach(([key, value])=>{
|
|
66
|
-
if (null != value) urlObj.searchParams.append(key, String(value));
|
|
67
|
-
});
|
|
68
|
-
return urlObj.toString();
|
|
69
|
-
} catch (error) {
|
|
70
|
-
console.error('Failed to build URL:', error);
|
|
71
|
-
return url;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
serializeData(data, contentType) {
|
|
75
|
-
if (null == data) return null;
|
|
76
|
-
if (data instanceof FormData) return data;
|
|
77
|
-
if (data instanceof Blob || data instanceof File) return data;
|
|
78
|
-
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data;
|
|
79
|
-
if (data instanceof URLSearchParams) return data;
|
|
80
|
-
const type = contentType?.toLowerCase() || '';
|
|
81
|
-
if (type.includes('application/json')) try {
|
|
82
|
-
return JSON.stringify(data);
|
|
83
|
-
} catch (error) {
|
|
84
|
-
throw new ApiError('Failed to serialize JSON data', void 0, void 0, 'SERIALIZATION_ERROR');
|
|
85
|
-
}
|
|
86
|
-
if (type.includes('application/x-www-form-urlencoded')) {
|
|
87
|
-
if ('object' == typeof data) return new URLSearchParams(data).toString();
|
|
88
|
-
return String(data);
|
|
89
|
-
}
|
|
90
|
-
if ('object' == typeof data) try {
|
|
91
|
-
return JSON.stringify(data);
|
|
92
|
-
} catch (error) {
|
|
93
|
-
throw new ApiError('Failed to serialize data', void 0, void 0, 'SERIALIZATION_ERROR');
|
|
94
|
-
}
|
|
95
|
-
return String(data);
|
|
96
|
-
}
|
|
97
|
-
async parseResponse(response, responseType) {
|
|
98
|
-
try {
|
|
99
|
-
switch(responseType){
|
|
100
|
-
case 'json':
|
|
101
|
-
const text = await response.text();
|
|
102
|
-
return text ? JSON.parse(text) : null;
|
|
103
|
-
case 'text':
|
|
104
|
-
return await response.text();
|
|
105
|
-
case 'blob':
|
|
106
|
-
return await response.blob();
|
|
107
|
-
case 'arrayBuffer':
|
|
108
|
-
return await response.arrayBuffer();
|
|
109
|
-
case 'formData':
|
|
110
|
-
return await response.formData();
|
|
111
|
-
default:
|
|
112
|
-
return await response.text();
|
|
113
|
-
}
|
|
114
|
-
} catch (error) {
|
|
115
|
-
throw new ApiError(`Failed to parse response as ${responseType}`, void 0, response, 'PARSE_ERROR');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
async runRequestInterceptors(config) {
|
|
119
|
-
let modifiedConfig = {
|
|
120
|
-
...config
|
|
121
|
-
};
|
|
122
|
-
for (const interceptor of this.requestInterceptors)try {
|
|
123
|
-
modifiedConfig = await interceptor(modifiedConfig);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
console.error('Request interceptor error:', error);
|
|
126
|
-
throw new ApiError('Request interceptor failed', modifiedConfig, void 0, 'INTERCEPTOR_ERROR');
|
|
127
|
-
}
|
|
128
|
-
return modifiedConfig;
|
|
129
|
-
}
|
|
130
|
-
async runResponseInterceptors(response) {
|
|
131
|
-
let modifiedResponse = response;
|
|
132
|
-
for (const interceptor of this.responseInterceptors)try {
|
|
133
|
-
modifiedResponse = await interceptor(modifiedResponse);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
console.error('Response interceptor error:', error);
|
|
136
|
-
}
|
|
137
|
-
return modifiedResponse;
|
|
138
|
-
}
|
|
139
|
-
async runErrorInterceptors(error) {
|
|
140
|
-
for (const interceptor of this.errorInterceptors)try {
|
|
141
|
-
const result = await interceptor(error);
|
|
142
|
-
if (void 0 !== result) return result;
|
|
143
|
-
} catch (err) {
|
|
144
|
-
console.error('Error interceptor failed:', err);
|
|
145
|
-
}
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
generateRequestKey(config) {
|
|
149
|
-
return `${config.method || 'GET'}_${config.url}_${Date.now()}_${Math.random()}`;
|
|
150
|
-
}
|
|
151
|
-
async request(config) {
|
|
152
|
-
if (!config || 'object' != typeof config) throw new ApiError('Request config is required and must be an object', config, void 0, 'INVALID_CONFIG');
|
|
153
|
-
if (!config.url) throw new ApiError('Request URL is required', config, void 0, 'MISSING_URL');
|
|
154
|
-
if (!this.validateUrl(config.url)) throw new ApiError('Invalid URL format', config, void 0, 'INVALID_URL');
|
|
155
|
-
const mergedConfig = {
|
|
156
|
-
...this.defaultConfig,
|
|
157
|
-
...config,
|
|
158
|
-
headers: {
|
|
159
|
-
...this.defaultConfig.headers,
|
|
160
|
-
...config.headers
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
const finalConfig = await this.runRequestInterceptors(mergedConfig);
|
|
164
|
-
const requestKey = this.generateRequestKey(finalConfig);
|
|
165
|
-
try {
|
|
166
|
-
const response = await this.executeRequest(finalConfig, requestKey);
|
|
167
|
-
return response;
|
|
168
|
-
} catch (error) {
|
|
169
|
-
const apiError = error instanceof ApiError ? error : new ApiError(error instanceof Error ? error.message : 'Unknown error', finalConfig, void 0, 'REQUEST_FAILED');
|
|
170
|
-
return await this.runErrorInterceptors(apiError);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
async executeRequest(config, requestKey) {
|
|
174
|
-
let abortController;
|
|
175
|
-
if (config.signal) {
|
|
176
|
-
abortController = new AbortController();
|
|
177
|
-
config.signal.addEventListener('abort', ()=>abortController.abort());
|
|
178
|
-
} else abortController = new AbortController();
|
|
179
|
-
this.activeRequests.set(requestKey, abortController);
|
|
180
|
-
let timeoutId = null;
|
|
181
|
-
if (config.timeout && config.timeout > 0) timeoutId = setTimeout(()=>{
|
|
182
|
-
abortController.abort();
|
|
183
|
-
}, config.timeout);
|
|
184
|
-
try {
|
|
185
|
-
const fullUrl = this.buildUrl(config.url, config.params);
|
|
186
|
-
const fetchOptions = {
|
|
187
|
-
method: config.method || 'GET',
|
|
188
|
-
headers: config.headers,
|
|
189
|
-
credentials: config.credentials,
|
|
190
|
-
signal: abortController.signal
|
|
191
|
-
};
|
|
192
|
-
const methodsWithoutBody = [
|
|
193
|
-
'GET',
|
|
194
|
-
'HEAD'
|
|
195
|
-
];
|
|
196
|
-
if (config.data && !methodsWithoutBody.includes(config.method || 'GET')) fetchOptions.body = this.serializeData(config.data, config.headers?.['Content-Type']);
|
|
197
|
-
const response = await fetch(fullUrl, fetchOptions);
|
|
198
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
199
|
-
this.activeRequests.delete(requestKey);
|
|
200
|
-
if (!response.ok) throw new ApiError(`HTTP Error: ${response.status} ${response.statusText}`, config, response, 'HTTP_ERROR');
|
|
201
|
-
const data = await this.parseResponse(response, config.responseType || 'json');
|
|
202
|
-
const apiResponse = {
|
|
203
|
-
data,
|
|
204
|
-
status: response.status,
|
|
205
|
-
statusText: response.statusText
|
|
206
|
-
};
|
|
207
|
-
return await this.runResponseInterceptors(apiResponse);
|
|
208
|
-
} catch (error) {
|
|
209
|
-
let errorResponse = null, errorResponseString = '';
|
|
210
|
-
if (error.response && error.config.responseType) errorResponse = await this.parseResponse(error.response, error.config.responseType || 'json');
|
|
211
|
-
try {
|
|
212
|
-
errorResponseString = JSON.stringify(errorResponse);
|
|
213
|
-
} catch (error) {
|
|
214
|
-
throw new ApiError(error instanceof Error ? error.message : 'Response parse error', config, void 0, 'UNKNOWN_ERROR');
|
|
215
|
-
}
|
|
216
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
217
|
-
this.activeRequests.delete(requestKey);
|
|
218
|
-
if (error instanceof Error && 'AbortError' === error.name) throw new ApiError(config.timeout ? 'Request timeout' : 'Request cancelled', config, void 0, 'ABORT_ERROR');
|
|
219
|
-
if (error instanceof TypeError) throw new ApiError('Network error or CORS issue', config, void 0, 'NETWORK_ERROR');
|
|
220
|
-
throw new ApiError(errorResponseString, config, errorResponse, 'UNKNOWN_ERROR');
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
async get(url, config) {
|
|
224
|
-
return this.request({
|
|
225
|
-
...config,
|
|
226
|
-
url,
|
|
227
|
-
method: 'GET'
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
async post(url, data, config) {
|
|
231
|
-
return this.request({
|
|
232
|
-
...config,
|
|
233
|
-
url,
|
|
234
|
-
method: 'POST',
|
|
235
|
-
data
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
async put(url, data, config) {
|
|
239
|
-
return this.request({
|
|
240
|
-
...config,
|
|
241
|
-
url,
|
|
242
|
-
method: 'PUT',
|
|
243
|
-
data
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
async delete(url, config) {
|
|
247
|
-
return this.request({
|
|
248
|
-
...config,
|
|
249
|
-
url,
|
|
250
|
-
method: 'DELETE'
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
async patch(url, data, config) {
|
|
254
|
-
return this.request({
|
|
255
|
-
...config,
|
|
256
|
-
url,
|
|
257
|
-
method: 'PATCH',
|
|
258
|
-
data
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
async head(url, config) {
|
|
262
|
-
return this.request({
|
|
263
|
-
...config,
|
|
264
|
-
url,
|
|
265
|
-
method: 'HEAD'
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
async options(url, config) {
|
|
269
|
-
return this.request({
|
|
270
|
-
...config,
|
|
271
|
-
url,
|
|
272
|
-
method: 'OPTIONS'
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
cancelRequest(requestKey) {
|
|
276
|
-
const controller = this.activeRequests.get(requestKey);
|
|
277
|
-
if (controller) {
|
|
278
|
-
controller.abort();
|
|
279
|
-
this.activeRequests.delete(requestKey);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
cancelAllRequests() {
|
|
283
|
-
this.activeRequests.forEach((controller)=>{
|
|
284
|
-
controller.abort();
|
|
285
|
-
});
|
|
286
|
-
this.activeRequests.clear();
|
|
287
|
-
}
|
|
288
|
-
getActiveRequestCount() {
|
|
289
|
-
return this.activeRequests.size;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
const apiProxy = new ApiProxy();
|
|
293
|
-
const core = apiProxy;
|
|
294
|
-
export { ApiError, core as default };
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GET 请求
|
|
3
|
-
*/
|
|
4
|
-
declare function api_get(url: any, config: any): Promise<import("../api-proxy/core").ApiResponse<any>>;
|
|
5
|
-
/**
|
|
6
|
-
* POST 请求
|
|
7
|
-
*/
|
|
8
|
-
declare function api_post(url: any, data: any, config: any): Promise<import("../api-proxy/core").ApiResponse<any>>;
|
|
9
|
-
/**
|
|
10
|
-
* PUT 请求
|
|
11
|
-
*/
|
|
12
|
-
declare function api_put(url: any, data: any, config: any): Promise<import("../api-proxy/core").ApiResponse<any>>;
|
|
13
|
-
/**
|
|
14
|
-
* DELETE 请求
|
|
15
|
-
*/
|
|
16
|
-
declare function api_delete(url: any, config: any): Promise<import("../api-proxy/core").ApiResponse<any>>;
|
|
17
|
-
/**
|
|
18
|
-
* PATCH 请求
|
|
19
|
-
*/
|
|
20
|
-
declare function api_patch(url: any, data: any, config: any): Promise<import("../api-proxy/core").ApiResponse<any>>;
|
|
21
|
-
/**
|
|
22
|
-
* HEAD 请求
|
|
23
|
-
*/
|
|
24
|
-
declare function api_head(url: any, config: any): Promise<import("../api-proxy/core").ApiResponse<any>>;
|
|
25
|
-
/**
|
|
26
|
-
* OPTIONS 请求
|
|
27
|
-
*/
|
|
28
|
-
declare function api_options(url: any, config: any): Promise<import("../api-proxy/core").ApiResponse<any>>;
|
|
29
|
-
export { api_get, api_post, api_put, api_delete, api_patch, api_head, api_options, };
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { normalizeBasePath } from "../../../utils/utils.js";
|
|
2
|
-
import core from "../api-proxy/core.js";
|
|
3
|
-
async function api_get(url, config) {
|
|
4
|
-
try {
|
|
5
|
-
const basePath = normalizeBasePath(process.env.CLIENT_BASE_PATH);
|
|
6
|
-
const res = await core.get(`${basePath}${url}`, config);
|
|
7
|
-
return res;
|
|
8
|
-
} catch (error) {
|
|
9
|
-
throw new Error(JSON.stringify(error));
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
async function api_post(url, data, config) {
|
|
13
|
-
try {
|
|
14
|
-
const basePath = normalizeBasePath(process.env.CLIENT_BASE_PATH);
|
|
15
|
-
const res = await core.post(`${basePath}${url}`, data, config);
|
|
16
|
-
return res;
|
|
17
|
-
} catch (error) {
|
|
18
|
-
throw new Error(JSON.stringify(error));
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
async function api_put(url, data, config) {
|
|
22
|
-
try {
|
|
23
|
-
const basePath = normalizeBasePath(process.env.CLIENT_BASE_PATH);
|
|
24
|
-
const res = await core.put(`${basePath}${url}`, data, config);
|
|
25
|
-
return res;
|
|
26
|
-
} catch (error) {
|
|
27
|
-
throw new Error(JSON.stringify(error));
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
async function api_delete(url, config) {
|
|
31
|
-
try {
|
|
32
|
-
const basePath = normalizeBasePath(process.env.CLIENT_BASE_PATH);
|
|
33
|
-
const res = await core["delete"](`${basePath}${url}`, config);
|
|
34
|
-
return res;
|
|
35
|
-
} catch (error) {
|
|
36
|
-
throw new Error(JSON.stringify(error));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
async function api_patch(url, data, config) {
|
|
40
|
-
try {
|
|
41
|
-
const basePath = normalizeBasePath(process.env.CLIENT_BASE_PATH);
|
|
42
|
-
const res = await core.patch(`${basePath}${url}`, data, config);
|
|
43
|
-
return res;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
throw new Error(JSON.stringify(error));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
async function api_head(url, config) {
|
|
49
|
-
try {
|
|
50
|
-
const basePath = normalizeBasePath(process.env.CLIENT_BASE_PATH);
|
|
51
|
-
const res = await core.head(`${basePath}${url}`, config);
|
|
52
|
-
return res;
|
|
53
|
-
} catch (error) {
|
|
54
|
-
throw new Error(JSON.stringify(error));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
async function api_options(url, config) {
|
|
58
|
-
try {
|
|
59
|
-
const basePath = normalizeBasePath(process.env.CLIENT_BASE_PATH);
|
|
60
|
-
const res = await core.options(`${basePath}${url}`, config);
|
|
61
|
-
return res;
|
|
62
|
-
} catch (error) {
|
|
63
|
-
throw new Error(JSON.stringify(error));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
export { api_delete, api_get, api_head, api_options, api_patch, api_post, api_put };
|