@mandujs/core 0.18.22 → 0.19.2
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/README.ko.md +0 -14
- package/package.json +4 -1
- package/src/brain/architecture/analyzer.ts +4 -4
- package/src/brain/doctor/analyzer.ts +18 -14
- package/src/bundler/build.test.ts +127 -0
- package/src/bundler/build.ts +291 -113
- package/src/bundler/css.ts +20 -5
- package/src/bundler/dev.ts +55 -2
- package/src/bundler/prerender.ts +195 -0
- package/src/change/snapshot.ts +4 -23
- package/src/change/types.ts +2 -3
- package/src/client/Form.tsx +105 -0
- package/src/client/__tests__/use-sse.test.ts +153 -0
- package/src/client/hooks.ts +105 -6
- package/src/client/index.ts +35 -6
- package/src/client/router.ts +670 -433
- package/src/client/rpc.ts +140 -0
- package/src/client/runtime.ts +24 -21
- package/src/client/use-fetch.ts +239 -0
- package/src/client/use-head.ts +197 -0
- package/src/client/use-sse.ts +378 -0
- package/src/components/Image.tsx +162 -0
- package/src/config/mandu.ts +5 -0
- package/src/config/validate.ts +34 -0
- package/src/content/index.ts +5 -1
- package/src/devtools/client/catchers/error-catcher.ts +17 -0
- package/src/devtools/client/catchers/network-proxy.ts +390 -367
- package/src/devtools/client/components/kitchen-root.tsx +479 -467
- package/src/devtools/client/components/panel/diff-viewer.tsx +219 -0
- package/src/devtools/client/components/panel/guard-panel.tsx +374 -244
- package/src/devtools/client/components/panel/index.ts +45 -32
- package/src/devtools/client/components/panel/panel-container.tsx +332 -312
- package/src/devtools/client/components/panel/preview-panel.tsx +188 -0
- package/src/devtools/client/state-manager.ts +535 -478
- package/src/devtools/design-tokens.ts +265 -264
- package/src/devtools/types.ts +345 -319
- package/src/filling/context.ts +65 -0
- package/src/filling/filling.ts +336 -14
- package/src/filling/index.ts +5 -1
- package/src/filling/session.ts +216 -0
- package/src/filling/ws.ts +78 -0
- package/src/generator/generate.ts +2 -2
- package/src/guard/auto-correct.ts +0 -29
- package/src/guard/check.ts +14 -31
- package/src/guard/presets/index.ts +296 -294
- package/src/guard/rules.ts +15 -19
- package/src/guard/validator.ts +834 -834
- package/src/index.ts +5 -1
- package/src/island/index.ts +373 -304
- package/src/kitchen/api/contract-api.ts +225 -0
- package/src/kitchen/api/diff-parser.ts +108 -0
- package/src/kitchen/api/file-api.ts +273 -0
- package/src/kitchen/api/guard-api.ts +83 -0
- package/src/kitchen/api/guard-decisions.ts +100 -0
- package/src/kitchen/api/routes-api.ts +50 -0
- package/src/kitchen/index.ts +21 -0
- package/src/kitchen/kitchen-handler.ts +256 -0
- package/src/kitchen/kitchen-ui.ts +1732 -0
- package/src/kitchen/stream/activity-sse.ts +145 -0
- package/src/kitchen/stream/file-tailer.ts +99 -0
- package/src/middleware/compress.ts +62 -0
- package/src/middleware/cors.ts +47 -0
- package/src/middleware/index.ts +10 -0
- package/src/middleware/jwt.ts +134 -0
- package/src/middleware/logger.ts +58 -0
- package/src/middleware/timeout.ts +55 -0
- package/src/paths.ts +0 -4
- package/src/plugins/hooks.ts +64 -0
- package/src/plugins/index.ts +3 -0
- package/src/plugins/types.ts +5 -0
- package/src/report/build.ts +0 -6
- package/src/resource/__tests__/backward-compat.test.ts +0 -1
- package/src/router/fs-patterns.ts +11 -1
- package/src/router/fs-routes.ts +78 -14
- package/src/router/fs-scanner.ts +2 -2
- package/src/router/fs-types.ts +2 -1
- package/src/runtime/adapter-bun.ts +62 -0
- package/src/runtime/adapter.ts +47 -0
- package/src/runtime/cache.ts +310 -0
- package/src/runtime/handler.ts +65 -0
- package/src/runtime/image-handler.ts +195 -0
- package/src/runtime/index.ts +12 -0
- package/src/runtime/middleware.ts +263 -0
- package/src/runtime/server.ts +686 -92
- package/src/runtime/ssr.ts +55 -29
- package/src/runtime/streaming-ssr.ts +106 -82
- package/src/spec/index.ts +0 -1
- package/src/spec/schema.ts +1 -0
- package/src/testing/index.ts +144 -0
- package/src/watcher/watcher.ts +27 -1
- package/src/spec/lock.ts +0 -56
|
@@ -1,367 +1,390 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Kitchen DevTools - Network Proxy
|
|
3
|
-
* @version 1.0.
|
|
4
|
-
*
|
|
5
|
-
* Fetch/XHR 요청을 인터셉트하여 DevTools로 전달
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
this.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Kitchen DevTools - Network Proxy
|
|
3
|
+
* @version 1.0.4
|
|
4
|
+
*
|
|
5
|
+
* Fetch/XHR 요청을 인터셉트하여 DevTools로 전달
|
|
6
|
+
*
|
|
7
|
+
* FIXED (v1.0.4): Streaming response handling no longer clones the response.
|
|
8
|
+
* The previous approach used response.clone() + a parallel reader loop, which
|
|
9
|
+
* caused two critical issues:
|
|
10
|
+
* 1. Tee'd ReadableStream backpressure: clone() creates a tee — if one
|
|
11
|
+
* branch is read faster than the other, the browser buffers data
|
|
12
|
+
* internally, eventually stalling both streams.
|
|
13
|
+
* 2. Microtask starvation: the tight `while(true) { await reader.read() }`
|
|
14
|
+
* loop resolved as microtasks. With fast SSE token streams, the loop
|
|
15
|
+
* never yielded to the macrotask queue, blocking page.evaluate(),
|
|
16
|
+
* user interactions, and all macrotask-scheduled work — effectively
|
|
17
|
+
* freezing the main thread.
|
|
18
|
+
*
|
|
19
|
+
* New approach: wrap the original response body with a TransformStream that
|
|
20
|
+
* passthrough-observes each chunk without consuming or buffering it separately.
|
|
21
|
+
* The consumer (island component) drives the read pace; the proxy merely
|
|
22
|
+
* piggybacks on each chunk as it flows through.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { NetworkRequest, NetworkBodyPolicy } from '../../types';
|
|
26
|
+
import { getOrCreateHook } from '../../hook';
|
|
27
|
+
import { createNetworkRequestEvent, createNetworkResponseEvent, ALLOWED_HEADERS, BLOCKED_HEADERS } from '../../protocol';
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Types
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
interface NetworkProxyOptions {
|
|
34
|
+
/** Network body 수집 정책 */
|
|
35
|
+
bodyPolicy?: NetworkBodyPolicy;
|
|
36
|
+
/** 무시할 URL 패턴 */
|
|
37
|
+
ignorePatterns?: (string | RegExp)[];
|
|
38
|
+
/** 최대 추적 요청 수 */
|
|
39
|
+
maxTrackedRequests?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Constants
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
const DEFAULT_OPTIONS: Required<NetworkProxyOptions> = {
|
|
47
|
+
bodyPolicy: {
|
|
48
|
+
collectBody: false,
|
|
49
|
+
optInPolicy: {
|
|
50
|
+
maxBytes: 10_000,
|
|
51
|
+
applyPIIFilter: true,
|
|
52
|
+
applySecretFilter: true,
|
|
53
|
+
allowedContentTypes: ['application/json', 'text/plain', 'text/event-stream'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
ignorePatterns: [
|
|
57
|
+
// DevTools 자체 요청
|
|
58
|
+
/__mandu/,
|
|
59
|
+
// HMR
|
|
60
|
+
/__vite/,
|
|
61
|
+
/\.hot-update\./,
|
|
62
|
+
// Source maps
|
|
63
|
+
/\.map$/,
|
|
64
|
+
// Chrome extensions
|
|
65
|
+
/^chrome-extension:/,
|
|
66
|
+
],
|
|
67
|
+
maxTrackedRequests: 200,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Helper Functions
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
let requestIdCounter = 0;
|
|
75
|
+
|
|
76
|
+
function generateRequestId(): string {
|
|
77
|
+
return `req-${Date.now()}-${++requestIdCounter}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function shouldIgnore(url: string, patterns: (string | RegExp)[]): boolean {
|
|
81
|
+
for (const pattern of patterns) {
|
|
82
|
+
if (typeof pattern === 'string') {
|
|
83
|
+
if (url.includes(pattern)) return true;
|
|
84
|
+
} else {
|
|
85
|
+
if (pattern.test(url)) return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function extractSafeHeaders(headers: Headers | Record<string, string>): {
|
|
92
|
+
safeHeaders: Record<string, string>;
|
|
93
|
+
redactedHeaders: string[];
|
|
94
|
+
} {
|
|
95
|
+
const safeHeaders: Record<string, string> = {};
|
|
96
|
+
const redactedHeaders: string[] = [];
|
|
97
|
+
|
|
98
|
+
const entries = headers instanceof Headers
|
|
99
|
+
? Array.from(headers.entries())
|
|
100
|
+
: Object.entries(headers);
|
|
101
|
+
|
|
102
|
+
for (const [key, value] of entries) {
|
|
103
|
+
const lowerKey = key.toLowerCase();
|
|
104
|
+
|
|
105
|
+
if (BLOCKED_HEADERS.has(lowerKey)) {
|
|
106
|
+
redactedHeaders.push(key);
|
|
107
|
+
} else if (ALLOWED_HEADERS.has(lowerKey)) {
|
|
108
|
+
safeHeaders[key] = value;
|
|
109
|
+
} else {
|
|
110
|
+
// 커스텀 헤더: 키만 표시
|
|
111
|
+
safeHeaders[key] = '[...]';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { safeHeaders, redactedHeaders };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isStreamingResponse(contentType: string | null): boolean {
|
|
119
|
+
if (!contentType) return false;
|
|
120
|
+
return (
|
|
121
|
+
contentType.includes('text/event-stream') ||
|
|
122
|
+
contentType.includes('application/x-ndjson')
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Network Proxy Class
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
export class NetworkProxy {
|
|
131
|
+
private options: Required<NetworkProxyOptions>;
|
|
132
|
+
private isAttached = false;
|
|
133
|
+
private originalFetch?: typeof fetch;
|
|
134
|
+
private trackedRequests = new Map<string, NetworkRequest>();
|
|
135
|
+
|
|
136
|
+
constructor(options?: NetworkProxyOptions) {
|
|
137
|
+
this.options = {
|
|
138
|
+
...DEFAULT_OPTIONS,
|
|
139
|
+
...options,
|
|
140
|
+
bodyPolicy: {
|
|
141
|
+
...DEFAULT_OPTIONS.bodyPolicy,
|
|
142
|
+
...options?.bodyPolicy,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --------------------------------------------------------------------------
|
|
148
|
+
// Lifecycle
|
|
149
|
+
// --------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
attach(): void {
|
|
152
|
+
if (this.isAttached || typeof window === 'undefined') return;
|
|
153
|
+
|
|
154
|
+
this.attachFetch();
|
|
155
|
+
this.isAttached = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
detach(): void {
|
|
159
|
+
if (!this.isAttached || typeof window === 'undefined') return;
|
|
160
|
+
|
|
161
|
+
if (this.originalFetch) {
|
|
162
|
+
window.fetch = this.originalFetch;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.trackedRequests.clear();
|
|
166
|
+
this.isAttached = false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --------------------------------------------------------------------------
|
|
170
|
+
// Fetch Interceptor
|
|
171
|
+
// --------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
private attachFetch(): void {
|
|
174
|
+
this.originalFetch = window.fetch.bind(window);
|
|
175
|
+
const self = this;
|
|
176
|
+
const originalFetch = this.originalFetch;
|
|
177
|
+
|
|
178
|
+
const interceptedFetch = async function(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
179
|
+
const url = typeof input === 'string'
|
|
180
|
+
? input
|
|
181
|
+
: input instanceof URL
|
|
182
|
+
? input.href
|
|
183
|
+
: input.url;
|
|
184
|
+
|
|
185
|
+
// 무시 패턴 체크
|
|
186
|
+
if (shouldIgnore(url, self.options.ignorePatterns)) {
|
|
187
|
+
return originalFetch(input, init);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const requestId = generateRequestId();
|
|
191
|
+
const method = init?.method ?? 'GET';
|
|
192
|
+
const headers = new Headers(init?.headers);
|
|
193
|
+
const { safeHeaders, redactedHeaders } = extractSafeHeaders(headers);
|
|
194
|
+
|
|
195
|
+
// 요청 시작 이벤트
|
|
196
|
+
const request: Omit<NetworkRequest, 'id' | 'startTime'> = {
|
|
197
|
+
method: method.toUpperCase(),
|
|
198
|
+
url,
|
|
199
|
+
safeHeaders,
|
|
200
|
+
redactedHeaders,
|
|
201
|
+
isStreaming: false,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const event = createNetworkRequestEvent(request);
|
|
205
|
+
const trackedRequest = event.data as NetworkRequest;
|
|
206
|
+
self.trackedRequests.set(requestId, trackedRequest);
|
|
207
|
+
|
|
208
|
+
// 최대 추적 수 제한
|
|
209
|
+
if (self.trackedRequests.size > self.options.maxTrackedRequests) {
|
|
210
|
+
const firstKey = self.trackedRequests.keys().next().value;
|
|
211
|
+
if (firstKey) self.trackedRequests.delete(firstKey);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getOrCreateHook().emit(event);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const response = await originalFetch(input, init);
|
|
218
|
+
|
|
219
|
+
// 스트리밍 여부 확인
|
|
220
|
+
const contentType = response.headers.get('content-type');
|
|
221
|
+
const isStreaming = isStreamingResponse(contentType);
|
|
222
|
+
|
|
223
|
+
if (isStreaming) {
|
|
224
|
+
// SSE/스트리밍 응답 처리
|
|
225
|
+
return self.handleStreamingResponse(requestId, response);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 일반 응답 완료 이벤트
|
|
229
|
+
const responseEvent = createNetworkResponseEvent(requestId, response.status);
|
|
230
|
+
getOrCreateHook().emit(responseEvent);
|
|
231
|
+
|
|
232
|
+
return response;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
// 네트워크 에러
|
|
235
|
+
getOrCreateHook().emit({
|
|
236
|
+
type: 'network:error',
|
|
237
|
+
timestamp: Date.now(),
|
|
238
|
+
data: {
|
|
239
|
+
id: requestId,
|
|
240
|
+
error: error instanceof Error ? error.message : String(error),
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Bun's fetch type includes `preconnect`, use Object.assign to preserve it
|
|
249
|
+
Object.assign(interceptedFetch, { preconnect: window.fetch.preconnect });
|
|
250
|
+
window.fetch = interceptedFetch as typeof fetch;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --------------------------------------------------------------------------
|
|
254
|
+
// Streaming Response Handler
|
|
255
|
+
// --------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Handle streaming (SSE/NDJSON) responses by wrapping the body with a
|
|
259
|
+
* passthrough TransformStream that observes chunks without cloning.
|
|
260
|
+
*
|
|
261
|
+
* This avoids:
|
|
262
|
+
* - response.clone() tee backpressure that stalls both streams
|
|
263
|
+
* - Separate reader loop that causes microtask starvation
|
|
264
|
+
*
|
|
265
|
+
* The consumer (island component) remains in full control of read pacing.
|
|
266
|
+
*/
|
|
267
|
+
private handleStreamingResponse(requestId: string, response: Response): Response {
|
|
268
|
+
const trackedRequest = this.trackedRequests.get(requestId);
|
|
269
|
+
if (trackedRequest) {
|
|
270
|
+
trackedRequest.isStreaming = true;
|
|
271
|
+
trackedRequest.chunkCount = 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// If there is no body (e.g., 204), just emit the response event and return
|
|
275
|
+
if (!response.body) {
|
|
276
|
+
const responseEvent = createNetworkResponseEvent(requestId, response.status);
|
|
277
|
+
getOrCreateHook().emit(responseEvent);
|
|
278
|
+
return response;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let chunkIndex = 0;
|
|
282
|
+
const self = this;
|
|
283
|
+
|
|
284
|
+
// Create a passthrough TransformStream that observes each chunk as it
|
|
285
|
+
// flows from the network to the consumer. No cloning, no buffering.
|
|
286
|
+
const observerTransform = new TransformStream<Uint8Array, Uint8Array>({
|
|
287
|
+
transform(chunk, controller) {
|
|
288
|
+
// Pass the chunk through immediately — zero copy
|
|
289
|
+
controller.enqueue(chunk);
|
|
290
|
+
|
|
291
|
+
// Emit tracking event (lightweight, non-blocking)
|
|
292
|
+
if (trackedRequest) {
|
|
293
|
+
trackedRequest.chunkCount = (trackedRequest.chunkCount ?? 0) + 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
getOrCreateHook().emit({
|
|
297
|
+
type: 'network:chunk',
|
|
298
|
+
timestamp: Date.now(),
|
|
299
|
+
data: {
|
|
300
|
+
id: requestId,
|
|
301
|
+
chunkIndex: chunkIndex++,
|
|
302
|
+
size: chunk?.length ?? 0,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
flush() {
|
|
308
|
+
// Stream completed — emit response event
|
|
309
|
+
const responseEvent = createNetworkResponseEvent(requestId, response.status);
|
|
310
|
+
getOrCreateHook().emit(responseEvent);
|
|
311
|
+
self.trackedRequests.delete(requestId);
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Pipe the original body through the observer
|
|
316
|
+
const observedBody = response.body.pipeThrough(observerTransform);
|
|
317
|
+
|
|
318
|
+
// Construct a new Response with the observed body but identical headers/status
|
|
319
|
+
return new Response(observedBody, {
|
|
320
|
+
status: response.status,
|
|
321
|
+
statusText: response.statusText,
|
|
322
|
+
headers: response.headers,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// --------------------------------------------------------------------------
|
|
327
|
+
// Manual Tracking
|
|
328
|
+
// --------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 수동으로 요청 추적 (외부 라이브러리 등)
|
|
332
|
+
*/
|
|
333
|
+
trackRequest(
|
|
334
|
+
method: string,
|
|
335
|
+
url: string,
|
|
336
|
+
headers?: Record<string, string>
|
|
337
|
+
): string {
|
|
338
|
+
const requestId = generateRequestId();
|
|
339
|
+
const { safeHeaders, redactedHeaders } = extractSafeHeaders(headers ?? {});
|
|
340
|
+
|
|
341
|
+
const event = createNetworkRequestEvent({
|
|
342
|
+
method: method.toUpperCase(),
|
|
343
|
+
url,
|
|
344
|
+
safeHeaders,
|
|
345
|
+
redactedHeaders,
|
|
346
|
+
isStreaming: false,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const trackedRequest = event.data as NetworkRequest;
|
|
350
|
+
this.trackedRequests.set(requestId, trackedRequest);
|
|
351
|
+
getOrCreateHook().emit(event);
|
|
352
|
+
|
|
353
|
+
return requestId;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 수동으로 응답 완료 알림
|
|
358
|
+
*/
|
|
359
|
+
completeRequest(requestId: string, status: number): void {
|
|
360
|
+
const event = createNetworkResponseEvent(requestId, status);
|
|
361
|
+
getOrCreateHook().emit(event);
|
|
362
|
+
this.trackedRequests.delete(requestId);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// Singleton Instance
|
|
368
|
+
// ============================================================================
|
|
369
|
+
|
|
370
|
+
let globalNetworkProxy: NetworkProxy | null = null;
|
|
371
|
+
|
|
372
|
+
export function getNetworkProxy(options?: NetworkProxyOptions): NetworkProxy {
|
|
373
|
+
if (!globalNetworkProxy) {
|
|
374
|
+
globalNetworkProxy = new NetworkProxy(options);
|
|
375
|
+
}
|
|
376
|
+
return globalNetworkProxy;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function initializeNetworkProxy(options?: NetworkProxyOptions): NetworkProxy {
|
|
380
|
+
const proxy = getNetworkProxy(options);
|
|
381
|
+
proxy.attach();
|
|
382
|
+
return proxy;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function destroyNetworkProxy(): void {
|
|
386
|
+
if (globalNetworkProxy) {
|
|
387
|
+
globalNetworkProxy.detach();
|
|
388
|
+
globalNetworkProxy = null;
|
|
389
|
+
}
|
|
390
|
+
}
|