doru 1.0.0
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.md +248 -0
- package/dist/cli.js +940 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.mts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +631 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +605 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ui/app.js +29334 -0
- package/dist/ui/app.js.map +1 -0
- package/dist/ui/favicon-paused.svg +20 -0
- package/dist/ui/favicon.svg +30 -0
- package/dist/ui/index.html +14 -0
- package/dist/ui/styles.css +1293 -0
- package/package.json +52 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { BatchInterceptor } from '@mswjs/interceptors';
|
|
3
|
+
import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest';
|
|
4
|
+
import { FetchInterceptor } from '@mswjs/interceptors/fetch';
|
|
5
|
+
import * as zlib from 'zlib';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
|
|
9
|
+
// src/core/config.ts
|
|
10
|
+
var statusCodesSchema = z.object({
|
|
11
|
+
min: z.number().int().min(100).max(599).nullable().default(null),
|
|
12
|
+
max: z.number().int().min(100).max(599).nullable().default(null)
|
|
13
|
+
}).refine((s) => s.min === null || s.max === null || s.min <= s.max, {
|
|
14
|
+
message: "filters.statusCodes.min must be <= filters.statusCodes.max"
|
|
15
|
+
});
|
|
16
|
+
var filtersSchema = z.object({
|
|
17
|
+
includeHosts: z.array(z.string()).default([]),
|
|
18
|
+
excludeHosts: z.array(z.string()).default([]),
|
|
19
|
+
includePaths: z.array(z.string()).default([]),
|
|
20
|
+
excludePaths: z.array(z.string()).default([]),
|
|
21
|
+
methods: z.array(z.string()).default([]),
|
|
22
|
+
statusCodes: statusCodesSchema.default({ min: null, max: null }),
|
|
23
|
+
minSize: z.number().int().nullable().default(null),
|
|
24
|
+
maxSize: z.number().int().nullable().default(null)
|
|
25
|
+
});
|
|
26
|
+
var performanceSchema = z.object({
|
|
27
|
+
bufferSize: z.number().int().positive().default(1024),
|
|
28
|
+
flushInterval: z.number().int().positive().default(1e3),
|
|
29
|
+
compression: z.boolean().default(false)
|
|
30
|
+
// minimal core: compression disabled by default
|
|
31
|
+
});
|
|
32
|
+
var doruConfigSchema = z.object({
|
|
33
|
+
enabled: z.boolean().default(true),
|
|
34
|
+
outputFile: z.string().default("./network-capture.jsonl"),
|
|
35
|
+
format: z.enum(["json", "jsonl"]).default("jsonl"),
|
|
36
|
+
maxFileSize: z.number().int().positive().default(10 * 1024 * 1024),
|
|
37
|
+
// 10MB
|
|
38
|
+
filters: filtersSchema.default({
|
|
39
|
+
includeHosts: [],
|
|
40
|
+
excludeHosts: [],
|
|
41
|
+
includePaths: [],
|
|
42
|
+
excludePaths: [],
|
|
43
|
+
methods: [],
|
|
44
|
+
statusCodes: { min: null, max: null },
|
|
45
|
+
minSize: null,
|
|
46
|
+
maxSize: null
|
|
47
|
+
}),
|
|
48
|
+
performance: performanceSchema.default({
|
|
49
|
+
bufferSize: 1024,
|
|
50
|
+
flushInterval: 1e3,
|
|
51
|
+
compression: false
|
|
52
|
+
}),
|
|
53
|
+
debug: z.boolean().default(false)
|
|
54
|
+
});
|
|
55
|
+
var deepMerge = (a, b) => {
|
|
56
|
+
const out = { ...a };
|
|
57
|
+
for (const [k, v] of Object.entries(b || {})) {
|
|
58
|
+
if (v === void 0) continue;
|
|
59
|
+
const key = k;
|
|
60
|
+
const av = a[key];
|
|
61
|
+
if (Array.isArray(v)) {
|
|
62
|
+
out[key] = v.slice();
|
|
63
|
+
} else if (v && typeof v === "object" && av && typeof av === "object" && !Array.isArray(av)) {
|
|
64
|
+
out[key] = deepMerge(
|
|
65
|
+
av,
|
|
66
|
+
v
|
|
67
|
+
);
|
|
68
|
+
} else out[key] = v;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
};
|
|
72
|
+
var envDebugDefault = () => {
|
|
73
|
+
const v = process.env.DORU_DEBUG;
|
|
74
|
+
if (v === "true") return true;
|
|
75
|
+
if (v === "false") return false;
|
|
76
|
+
const legacy = process.env.NETPEEK_DEBUG;
|
|
77
|
+
if (legacy === "true") return true;
|
|
78
|
+
if (legacy === "false") return false;
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
var normalizeConfig = (c) => {
|
|
82
|
+
return {
|
|
83
|
+
...c,
|
|
84
|
+
filters: {
|
|
85
|
+
...c.filters,
|
|
86
|
+
methods: c.filters.methods.map((m) => m.toUpperCase())
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
var createDefaultConfig = (user = {}) => {
|
|
91
|
+
const defaults = doruConfigSchema.parse({});
|
|
92
|
+
const merged = deepMerge(defaults, { ...user, debug: user.debug ?? envDebugDefault() });
|
|
93
|
+
const parsed = doruConfigSchema.parse(merged);
|
|
94
|
+
return normalizeConfig(parsed);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/core/filters.ts
|
|
98
|
+
var FilterEngine = class _FilterEngine {
|
|
99
|
+
constructor(cfg) {
|
|
100
|
+
this.cfg = cfg;
|
|
101
|
+
}
|
|
102
|
+
updateConfig(next) {
|
|
103
|
+
this.cfg = next;
|
|
104
|
+
}
|
|
105
|
+
shouldCaptureRequest(req) {
|
|
106
|
+
if (!this.checkHost(req.hostname)) return false;
|
|
107
|
+
if (!this.checkPath(req.path)) return false;
|
|
108
|
+
if (!this.checkMethod(req.method)) return false;
|
|
109
|
+
if (!this.checkSize(req.body)) return false;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
shouldCaptureResponse(res) {
|
|
113
|
+
if (!this.checkStatus(res.statusCode)) return false;
|
|
114
|
+
if (!this.checkSize(res.body)) return false;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
checkHost(host) {
|
|
118
|
+
const { includeHosts, excludeHosts } = this.cfg;
|
|
119
|
+
if (includeHosts.length > 0) {
|
|
120
|
+
const ok = includeHosts.some((p) => _FilterEngine.matchStringOrRegex(host, p));
|
|
121
|
+
if (!ok) return false;
|
|
122
|
+
}
|
|
123
|
+
if (excludeHosts.length > 0) {
|
|
124
|
+
const hit = excludeHosts.some((p) => _FilterEngine.matchStringOrRegex(host, p));
|
|
125
|
+
if (hit) return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
checkPath(path2) {
|
|
130
|
+
const { includePaths, excludePaths } = this.cfg;
|
|
131
|
+
if (includePaths.length > 0) {
|
|
132
|
+
const ok = includePaths.some((p) => _FilterEngine.matchRegexSafe(path2, p));
|
|
133
|
+
if (!ok) return false;
|
|
134
|
+
}
|
|
135
|
+
if (excludePaths.length > 0) {
|
|
136
|
+
const hit = excludePaths.some((p) => _FilterEngine.matchRegexSafe(path2, p));
|
|
137
|
+
if (hit) return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
checkMethod(method) {
|
|
142
|
+
const { methods } = this.cfg;
|
|
143
|
+
if (methods.length === 0) return true;
|
|
144
|
+
return methods.includes(method.toUpperCase());
|
|
145
|
+
}
|
|
146
|
+
checkStatus(code) {
|
|
147
|
+
const { min, max } = this.cfg.statusCodes;
|
|
148
|
+
if (min !== null && code < min) return false;
|
|
149
|
+
if (max !== null && code > max) return false;
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
checkSize(body) {
|
|
153
|
+
if (!body) return true;
|
|
154
|
+
const size = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body);
|
|
155
|
+
const { minSize, maxSize } = this.cfg;
|
|
156
|
+
if (minSize !== null && size < minSize) return false;
|
|
157
|
+
if (maxSize !== null && size > maxSize) return false;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
// accept user-provided regex in /.../ form, otherwise do substring match
|
|
161
|
+
static matchStringOrRegex(value, pattern) {
|
|
162
|
+
if (pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
163
|
+
try {
|
|
164
|
+
return new RegExp(pattern.slice(1, -1)).test(value);
|
|
165
|
+
} catch {
|
|
166
|
+
return value.includes(pattern);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return value.includes(pattern);
|
|
170
|
+
}
|
|
171
|
+
static matchRegexSafe(value, pattern) {
|
|
172
|
+
try {
|
|
173
|
+
return new RegExp(pattern).test(value);
|
|
174
|
+
} catch {
|
|
175
|
+
return value.includes(pattern);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/core/logging.ts
|
|
181
|
+
var DebugLogger = class {
|
|
182
|
+
constructor(cfg) {
|
|
183
|
+
this.cfg = cfg;
|
|
184
|
+
}
|
|
185
|
+
info(msg, data) {
|
|
186
|
+
if (!this.cfg.debug) return;
|
|
187
|
+
if (data === void 0) console.log(msg);
|
|
188
|
+
else console.log(msg, data);
|
|
189
|
+
}
|
|
190
|
+
error(msg, err) {
|
|
191
|
+
if (!this.cfg.debug) return;
|
|
192
|
+
if (err === void 0) console.error(msg);
|
|
193
|
+
else console.error(msg, err);
|
|
194
|
+
}
|
|
195
|
+
tag(tag, msg, data) {
|
|
196
|
+
this.info(`${tag} ${msg}`, data);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var createDebugLogger = (cfg) => new DebugLogger(cfg);
|
|
200
|
+
|
|
201
|
+
// src/core/interceptor.ts
|
|
202
|
+
var isErrorPayload = (p) => {
|
|
203
|
+
return p !== null && typeof p === "object" && "requestId" in p && typeof p.requestId === "string";
|
|
204
|
+
};
|
|
205
|
+
var NetworkInterceptor = class _NetworkInterceptor {
|
|
206
|
+
constructor(cfg, storage) {
|
|
207
|
+
this.started = false;
|
|
208
|
+
this.counter = 0;
|
|
209
|
+
this.pendings = /* @__PURE__ */ new Map();
|
|
210
|
+
this.storage = storage;
|
|
211
|
+
this.filter = new FilterEngine(cfg.filters);
|
|
212
|
+
this.log = createDebugLogger(cfg);
|
|
213
|
+
this.interceptor = new BatchInterceptor({
|
|
214
|
+
name: "doru",
|
|
215
|
+
interceptors: [new ClientRequestInterceptor(), new FetchInterceptor()]
|
|
216
|
+
});
|
|
217
|
+
this.wire();
|
|
218
|
+
}
|
|
219
|
+
wire() {
|
|
220
|
+
this.interceptor.on(
|
|
221
|
+
"request",
|
|
222
|
+
async ({ request, requestId }) => {
|
|
223
|
+
try {
|
|
224
|
+
if (request.url.startsWith("data:") || request.url.startsWith("blob:")) return;
|
|
225
|
+
const captureId = _NetworkInterceptor.newCaptureId();
|
|
226
|
+
const start = Date.now();
|
|
227
|
+
const req = await _NetworkInterceptor.getRequestData(request);
|
|
228
|
+
if (!this.filter.shouldCaptureRequest(req)) {
|
|
229
|
+
this.log.tag("\u{1F4E4}", "Request filtered", req.hostname);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.pendings.set(requestId, {
|
|
233
|
+
captureId,
|
|
234
|
+
start,
|
|
235
|
+
hostname: req.hostname,
|
|
236
|
+
method: req.method
|
|
237
|
+
});
|
|
238
|
+
this.captureRequest(captureId, req);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this.log.error("Request intercept error", error);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
this.interceptor.on(
|
|
245
|
+
"response",
|
|
246
|
+
async ({ response, requestId }) => {
|
|
247
|
+
try {
|
|
248
|
+
const pending = this.pendings.get(requestId);
|
|
249
|
+
if (!pending) return;
|
|
250
|
+
this.pendings.delete(requestId);
|
|
251
|
+
const res = await _NetworkInterceptor.getResponseData(response);
|
|
252
|
+
if (!this.filter.shouldCaptureResponse(res)) return;
|
|
253
|
+
this.captureResponse(pending.captureId, res, pending.start);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
this.log.error("Response intercept error", error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
const handleErrorEvent = (event) => (payload) => {
|
|
260
|
+
if (!isErrorPayload(payload)) return;
|
|
261
|
+
const { error, requestId } = payload;
|
|
262
|
+
try {
|
|
263
|
+
const pending = this.pendings.get(requestId);
|
|
264
|
+
if (!pending) return;
|
|
265
|
+
this.pendings.delete(requestId);
|
|
266
|
+
this.captureError(pending.captureId, error, pending.start);
|
|
267
|
+
} catch (error_) {
|
|
268
|
+
this.log.error(`${event} handler error`, error_);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
this.interceptor.on("unhandledException", handleErrorEvent("unhandledException"));
|
|
272
|
+
}
|
|
273
|
+
start() {
|
|
274
|
+
if (this.started) return;
|
|
275
|
+
this.started = true;
|
|
276
|
+
this.interceptor.apply();
|
|
277
|
+
this.log.tag("\u{1F527}", "Network interception started");
|
|
278
|
+
}
|
|
279
|
+
stop() {
|
|
280
|
+
if (!this.started) return;
|
|
281
|
+
this.started = false;
|
|
282
|
+
this.interceptor.dispose();
|
|
283
|
+
this.pendings.clear();
|
|
284
|
+
this.log.tag("\u{1F527}", "Network interception stopped");
|
|
285
|
+
}
|
|
286
|
+
updateConfig(next) {
|
|
287
|
+
this.filter.updateConfig(next.filters);
|
|
288
|
+
}
|
|
289
|
+
static async getRequestData(request) {
|
|
290
|
+
const url = new URL(request.url);
|
|
291
|
+
const headers = {};
|
|
292
|
+
for (const [k, v] of request.headers.entries()) {
|
|
293
|
+
headers[k.toLowerCase()] = v;
|
|
294
|
+
}
|
|
295
|
+
let body = null;
|
|
296
|
+
if (request.body && ["PATCH", "POST", "PUT"].includes(request.method.toUpperCase())) {
|
|
297
|
+
try {
|
|
298
|
+
const cloned = request.clone();
|
|
299
|
+
const ab = await cloned.arrayBuffer();
|
|
300
|
+
body = ab.byteLength > 0 ? Buffer.from(ab) : null;
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
method: request.method,
|
|
306
|
+
hostname: url.hostname,
|
|
307
|
+
port: url.port ? Number.parseInt(url.port) : void 0,
|
|
308
|
+
path: url.pathname + url.search,
|
|
309
|
+
protocol: url.protocol.replace(/:$/, ""),
|
|
310
|
+
headers,
|
|
311
|
+
body
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
static async getResponseData(response) {
|
|
315
|
+
const headers = {};
|
|
316
|
+
for (const [k, v] of response.headers.entries()) {
|
|
317
|
+
headers[k.toLowerCase()] = v;
|
|
318
|
+
}
|
|
319
|
+
let body = null;
|
|
320
|
+
try {
|
|
321
|
+
const cloned = response.clone();
|
|
322
|
+
const ab = await cloned.arrayBuffer();
|
|
323
|
+
if (ab.byteLength > 0) {
|
|
324
|
+
body = Buffer.from(ab);
|
|
325
|
+
const enc = headers["content-encoding"];
|
|
326
|
+
if (enc && enc.includes("gzip") && body) {
|
|
327
|
+
try {
|
|
328
|
+
const head = body.toString("utf8", 0, Math.min(body.length, 100));
|
|
329
|
+
const looksText = /^[\s\u0020-\u007E]*$/.test(head) && /^\s*[<[{]/.test(head.trim());
|
|
330
|
+
if (!looksText) body = zlib.gunzipSync(body);
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
statusCode: response.status,
|
|
339
|
+
statusMessage: response.statusText,
|
|
340
|
+
headers,
|
|
341
|
+
body
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
captureRequest(captureId, req) {
|
|
345
|
+
const rec = {
|
|
346
|
+
id: this.newRequestId(),
|
|
347
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
348
|
+
type: "request",
|
|
349
|
+
method: req.method,
|
|
350
|
+
hostname: req.hostname,
|
|
351
|
+
port: req.port,
|
|
352
|
+
path: req.path,
|
|
353
|
+
url: _NetworkInterceptor.buildUrl(req),
|
|
354
|
+
headers: req.headers,
|
|
355
|
+
body: req.body ?? null,
|
|
356
|
+
captureId
|
|
357
|
+
};
|
|
358
|
+
this.storage.write(rec);
|
|
359
|
+
}
|
|
360
|
+
captureResponse(captureId, res, start) {
|
|
361
|
+
const rec = {
|
|
362
|
+
id: this.newRequestId(),
|
|
363
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
364
|
+
type: "response",
|
|
365
|
+
statusCode: res.statusCode,
|
|
366
|
+
statusMessage: res.statusMessage,
|
|
367
|
+
headers: res.headers,
|
|
368
|
+
body: res.body ?? null,
|
|
369
|
+
duration: Date.now() - start,
|
|
370
|
+
captureId
|
|
371
|
+
};
|
|
372
|
+
this.storage.write(rec);
|
|
373
|
+
}
|
|
374
|
+
captureError(captureId, error, start) {
|
|
375
|
+
const rec = {
|
|
376
|
+
id: this.newRequestId(),
|
|
377
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
378
|
+
type: "error",
|
|
379
|
+
error: error?.message ?? String(error),
|
|
380
|
+
duration: Date.now() - start,
|
|
381
|
+
captureId
|
|
382
|
+
};
|
|
383
|
+
this.storage.write(rec);
|
|
384
|
+
}
|
|
385
|
+
static buildUrl(req) {
|
|
386
|
+
const proto = req.protocol;
|
|
387
|
+
const port = req.port && (proto === "http" && req.port !== 80 || proto === "https" && req.port !== 443) ? `:${req.port}` : "";
|
|
388
|
+
return `${proto}://${req.hostname}${port}${req.path}`;
|
|
389
|
+
}
|
|
390
|
+
static newCaptureId() {
|
|
391
|
+
return `cap-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
392
|
+
}
|
|
393
|
+
newRequestId() {
|
|
394
|
+
return `req-${++this.counter}-${Date.now()}`;
|
|
395
|
+
}
|
|
396
|
+
updateStorage(storage) {
|
|
397
|
+
this.storage = storage;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// src/core/storage/data-serializer.ts
|
|
402
|
+
var DataSerializer = {
|
|
403
|
+
serialize(data, compact) {
|
|
404
|
+
const { body, ...rest } = data;
|
|
405
|
+
let outBody = null;
|
|
406
|
+
if (typeof body === "string") {
|
|
407
|
+
outBody = body;
|
|
408
|
+
} else if (body && Buffer.isBuffer(body)) {
|
|
409
|
+
if (DataSerializer.isTextContent(body, data.headers)) {
|
|
410
|
+
outBody = body.toString("utf8");
|
|
411
|
+
} else {
|
|
412
|
+
outBody = {
|
|
413
|
+
type: "buffer",
|
|
414
|
+
data: body.toString("base64"),
|
|
415
|
+
encoding: "base64"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const out = { ...rest, body: outBody };
|
|
420
|
+
return JSON.stringify(out, null, compact ? 0 : 2);
|
|
421
|
+
},
|
|
422
|
+
isTextContent(buffer, headers) {
|
|
423
|
+
if (buffer.length === 0) return true;
|
|
424
|
+
const getHeader = (name) => {
|
|
425
|
+
if (!headers) return void 0;
|
|
426
|
+
const v = headers[name] ?? headers[name.toLowerCase()] ?? headers[name.toUpperCase()];
|
|
427
|
+
return Array.isArray(v) ? v[0] : v;
|
|
428
|
+
};
|
|
429
|
+
const ct = getHeader("content-type");
|
|
430
|
+
if (ct) {
|
|
431
|
+
if (/^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded))/.test(ct)) return true;
|
|
432
|
+
if (/^(image\/|video\/|audio\/|application\/(pdf|zip|octet-stream))/.test(ct)) return false;
|
|
433
|
+
}
|
|
434
|
+
const sampleSize = Math.min(buffer.length, 1024);
|
|
435
|
+
const sample = buffer.subarray(0, sampleSize);
|
|
436
|
+
let textish = 0;
|
|
437
|
+
for (const byte of sample) {
|
|
438
|
+
if (byte >= 32 && byte <= 126 || byte === 9 || byte === 10 || byte === 13 || byte >= 128) textish++;
|
|
439
|
+
}
|
|
440
|
+
return textish / sample.length > 0.9;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
var FileManager = class _FileManager {
|
|
444
|
+
constructor(cfg) {
|
|
445
|
+
this.cfg = cfg;
|
|
446
|
+
}
|
|
447
|
+
getExt() {
|
|
448
|
+
return this.cfg.format === "jsonl" ? ".jsonl" : ".json";
|
|
449
|
+
}
|
|
450
|
+
genPath(counter) {
|
|
451
|
+
const ext = this.getExt();
|
|
452
|
+
const base = path.basename(this.cfg.outputFile, path.extname(this.cfg.outputFile));
|
|
453
|
+
const dir = path.dirname(this.cfg.outputFile);
|
|
454
|
+
const file = counter === 0 ? `${base}${ext}` : `${base}-${counter}${ext}`;
|
|
455
|
+
return path.join(dir, file);
|
|
456
|
+
}
|
|
457
|
+
static ensureDir(filePath) {
|
|
458
|
+
const dir = path.dirname(filePath);
|
|
459
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
460
|
+
}
|
|
461
|
+
shouldRotate(currentSize) {
|
|
462
|
+
return currentSize > this.cfg.maxFileSize;
|
|
463
|
+
}
|
|
464
|
+
static createWriteStream(filePath) {
|
|
465
|
+
_FileManager.ensureDir(filePath);
|
|
466
|
+
return fs.createWriteStream(filePath, { flags: "w", encoding: "utf8" });
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/core/storage/formats.ts
|
|
471
|
+
var JsonFormatter = {
|
|
472
|
+
writeHeader(stream) {
|
|
473
|
+
stream.write("[\n");
|
|
474
|
+
},
|
|
475
|
+
formatEntry(json, isFirst) {
|
|
476
|
+
return isFirst ? ` ${json}` : `,
|
|
477
|
+
${json}`;
|
|
478
|
+
},
|
|
479
|
+
getHeaderSize() {
|
|
480
|
+
return 2;
|
|
481
|
+
},
|
|
482
|
+
close(stream, isFirst) {
|
|
483
|
+
if (isFirst) stream.write("]");
|
|
484
|
+
else stream.write("\n]");
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
var StreamFormatter = {
|
|
488
|
+
writeHeader() {
|
|
489
|
+
},
|
|
490
|
+
formatEntry(json) {
|
|
491
|
+
return `${json}
|
|
492
|
+
`;
|
|
493
|
+
},
|
|
494
|
+
getHeaderSize() {
|
|
495
|
+
return 0;
|
|
496
|
+
},
|
|
497
|
+
close() {
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
var createFormatHandler = (fmt) => fmt === "json" ? JsonFormatter : StreamFormatter;
|
|
501
|
+
|
|
502
|
+
// src/core/storage/storage.ts
|
|
503
|
+
var StorageManager = class {
|
|
504
|
+
constructor(config) {
|
|
505
|
+
this.stream = null;
|
|
506
|
+
this.size = 0;
|
|
507
|
+
this.counter = 0;
|
|
508
|
+
this.isFirst = true;
|
|
509
|
+
this.closed = false;
|
|
510
|
+
this.cfg = config;
|
|
511
|
+
this.fm = new FileManager(config);
|
|
512
|
+
this.fmt = createFormatHandler(config.format);
|
|
513
|
+
this.log = createDebugLogger(config);
|
|
514
|
+
this.initFile();
|
|
515
|
+
}
|
|
516
|
+
write(data) {
|
|
517
|
+
if (this.closed || !this.stream) return;
|
|
518
|
+
try {
|
|
519
|
+
if (this.fm.shouldRotate(this.size)) this.rotate();
|
|
520
|
+
const compact = this.cfg.format === "jsonl";
|
|
521
|
+
const json = DataSerializer.serialize(data, compact);
|
|
522
|
+
const chunk = this.fmt.formatEntry(json, this.isFirst);
|
|
523
|
+
this.stream.write(chunk);
|
|
524
|
+
this.size += Buffer.byteLength(chunk);
|
|
525
|
+
if (this.isFirst) this.isFirst = false;
|
|
526
|
+
this.log.tag("\u{1F4BE}", `Wrote ${Buffer.byteLength(chunk)} bytes (${data.type})`);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
this.log.error("Storage write error:", error);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
close() {
|
|
532
|
+
if (this.closed || !this.stream) return;
|
|
533
|
+
try {
|
|
534
|
+
this.fmt.close(this.stream, this.isFirst);
|
|
535
|
+
this.stream.end();
|
|
536
|
+
this.stream = null;
|
|
537
|
+
this.closed = true;
|
|
538
|
+
this.log.tag("\u{1F4BE}", "Storage closed");
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
initFile() {
|
|
543
|
+
try {
|
|
544
|
+
const filePath = this.fm.genPath(this.counter);
|
|
545
|
+
this.stream = FileManager.createWriteStream(filePath);
|
|
546
|
+
this.size = this.fmt.getHeaderSize();
|
|
547
|
+
this.isFirst = true;
|
|
548
|
+
this.fmt.writeHeader(this.stream);
|
|
549
|
+
if (!this.stream.writableNeedDrain) {
|
|
550
|
+
this.stream.cork();
|
|
551
|
+
this.stream.uncork();
|
|
552
|
+
}
|
|
553
|
+
this.log.tag("\u{1F4C1}", `Writing to ${filePath}`);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
this.log.error("Storage init error:", error);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
rotate() {
|
|
559
|
+
if (this.stream) {
|
|
560
|
+
this.fmt.close(this.stream, this.isFirst);
|
|
561
|
+
this.stream.end();
|
|
562
|
+
}
|
|
563
|
+
this.counter++;
|
|
564
|
+
this.initFile();
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// src/index.ts
|
|
569
|
+
var createInterceptor = (userConfig = {}) => {
|
|
570
|
+
let config = createDefaultConfig(userConfig);
|
|
571
|
+
let storage = new StorageManager(config);
|
|
572
|
+
const interceptor = new NetworkInterceptor(config, storage);
|
|
573
|
+
const api = {
|
|
574
|
+
start() {
|
|
575
|
+
interceptor.start();
|
|
576
|
+
},
|
|
577
|
+
stop() {
|
|
578
|
+
try {
|
|
579
|
+
interceptor.stop();
|
|
580
|
+
} finally {
|
|
581
|
+
storage.close();
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
updateConfig(next) {
|
|
585
|
+
const updated = createDefaultConfig({ ...config, ...next });
|
|
586
|
+
const old = config;
|
|
587
|
+
config = updated;
|
|
588
|
+
interceptor.updateConfig(updated);
|
|
589
|
+
const changed = updated.outputFile !== old.outputFile || updated.format !== old.format || updated.maxFileSize !== old.maxFileSize || updated.performance.compression !== old.performance.compression;
|
|
590
|
+
if (changed) {
|
|
591
|
+
storage.close();
|
|
592
|
+
storage = new StorageManager(updated);
|
|
593
|
+
interceptor.updateStorage(storage);
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
getConfig() {
|
|
597
|
+
return config;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
return api;
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
export { NetworkInterceptor, StorageManager, createInterceptor };
|
|
604
|
+
//# sourceMappingURL=index.mjs.map
|
|
605
|
+
//# sourceMappingURL=index.mjs.map
|