@buoy-gg/network 1.7.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.
Files changed (67) hide show
  1. package/README.md +381 -0
  2. package/lib/commonjs/index.js +34 -0
  3. package/lib/commonjs/network/components/NetworkCopySettingsView.js +867 -0
  4. package/lib/commonjs/network/components/NetworkEventDetailView.js +837 -0
  5. package/lib/commonjs/network/components/NetworkEventItemCompact.js +323 -0
  6. package/lib/commonjs/network/components/NetworkFilterViewV3.js +297 -0
  7. package/lib/commonjs/network/components/NetworkModal.js +937 -0
  8. package/lib/commonjs/network/hooks/useNetworkEvents.js +320 -0
  9. package/lib/commonjs/network/hooks/useTickEveryMinute.js +34 -0
  10. package/lib/commonjs/network/index.js +102 -0
  11. package/lib/commonjs/network/types/index.js +1 -0
  12. package/lib/commonjs/network/utils/extractOperationName.js +80 -0
  13. package/lib/commonjs/network/utils/formatGraphQLVariables.js +219 -0
  14. package/lib/commonjs/network/utils/formatting.js +30 -0
  15. package/lib/commonjs/network/utils/networkEventStore.js +269 -0
  16. package/lib/commonjs/network/utils/networkListener.js +801 -0
  17. package/lib/commonjs/package.json +1 -0
  18. package/lib/commonjs/preset.js +83 -0
  19. package/lib/module/index.js +7 -0
  20. package/lib/module/network/components/NetworkCopySettingsView.js +862 -0
  21. package/lib/module/network/components/NetworkEventDetailView.js +834 -0
  22. package/lib/module/network/components/NetworkEventItemCompact.js +320 -0
  23. package/lib/module/network/components/NetworkFilterViewV3.js +293 -0
  24. package/lib/module/network/components/NetworkModal.js +933 -0
  25. package/lib/module/network/hooks/useNetworkEvents.js +316 -0
  26. package/lib/module/network/hooks/useTickEveryMinute.js +29 -0
  27. package/lib/module/network/index.js +20 -0
  28. package/lib/module/network/types/index.js +1 -0
  29. package/lib/module/network/utils/extractOperationName.js +76 -0
  30. package/lib/module/network/utils/formatGraphQLVariables.js +213 -0
  31. package/lib/module/network/utils/formatting.js +9 -0
  32. package/lib/module/network/utils/networkEventStore.js +265 -0
  33. package/lib/module/network/utils/networkListener.js +791 -0
  34. package/lib/module/preset.js +79 -0
  35. package/lib/typescript/index.d.ts +3 -0
  36. package/lib/typescript/index.d.ts.map +1 -0
  37. package/lib/typescript/network/components/NetworkCopySettingsView.d.ts +26 -0
  38. package/lib/typescript/network/components/NetworkCopySettingsView.d.ts.map +1 -0
  39. package/lib/typescript/network/components/NetworkEventDetailView.d.ts +13 -0
  40. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -0
  41. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
  42. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -0
  43. package/lib/typescript/network/components/NetworkFilterViewV3.d.ts +22 -0
  44. package/lib/typescript/network/components/NetworkFilterViewV3.d.ts.map +1 -0
  45. package/lib/typescript/network/components/NetworkModal.d.ts +14 -0
  46. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -0
  47. package/lib/typescript/network/hooks/useNetworkEvents.d.ts +72 -0
  48. package/lib/typescript/network/hooks/useNetworkEvents.d.ts.map +1 -0
  49. package/lib/typescript/network/hooks/useTickEveryMinute.d.ts +9 -0
  50. package/lib/typescript/network/hooks/useTickEveryMinute.d.ts.map +1 -0
  51. package/lib/typescript/network/index.d.ts +12 -0
  52. package/lib/typescript/network/index.d.ts.map +1 -0
  53. package/lib/typescript/network/types/index.d.ts +88 -0
  54. package/lib/typescript/network/types/index.d.ts.map +1 -0
  55. package/lib/typescript/network/utils/extractOperationName.d.ts +41 -0
  56. package/lib/typescript/network/utils/extractOperationName.d.ts.map +1 -0
  57. package/lib/typescript/network/utils/formatGraphQLVariables.d.ts +79 -0
  58. package/lib/typescript/network/utils/formatGraphQLVariables.d.ts.map +1 -0
  59. package/lib/typescript/network/utils/formatting.d.ts +6 -0
  60. package/lib/typescript/network/utils/formatting.d.ts.map +1 -0
  61. package/lib/typescript/network/utils/networkEventStore.d.ts +81 -0
  62. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -0
  63. package/lib/typescript/network/utils/networkListener.d.ts +191 -0
  64. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -0
  65. package/lib/typescript/preset.d.ts +76 -0
  66. package/lib/typescript/preset.d.ts.map +1 -0
  67. package/package.json +69 -0
@@ -0,0 +1,801 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.getNetworkListenerCount = exports.addNetworkListener = void 0;
7
+ /**
8
+ * Network listener using Reactotron-style event pattern
9
+ * Simple and reliable network interception for React Native
10
+ */
11
+
12
+ // Extended XMLHttpRequest interface for monkey-patching
13
+
14
+ // Event types for network operations
15
+
16
+ /**
17
+ * Network traffic interceptor for React Native applications
18
+ *
19
+ * This class intercepts both fetch and XMLHttpRequest operations to provide
20
+ * comprehensive network monitoring capabilities. It uses method swizzling to
21
+ * wrap native networking APIs while preserving their original functionality.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // Start monitoring network traffic
26
+ * startNetworkListener();
27
+ *
28
+ * // Add a listener for network events
29
+ * const unsubscribe = addNetworkListener((event) => {
30
+ * if (event.type === 'response') {
31
+ * console.log(`${event.request.method} ${event.request.url}: ${event.response?.status}`);
32
+ * }
33
+ * });
34
+ *
35
+ * // Stop monitoring and cleanup
36
+ * unsubscribe();
37
+ * stopNetworkListener();
38
+ * ```
39
+ *
40
+ * @performance Uses lazy singleton pattern to minimize memory footprint
41
+ * @performance Includes URL filtering to ignore development traffic
42
+ */
43
+ class NetworkListener {
44
+ listeners = [];
45
+ isListening = false;
46
+ requestCounter = 1000;
47
+
48
+ // URLs to ignore (Metro bundler, symbolicate, etc.)
49
+ ignoredUrls = [/\/symbolicate$/, /\/logs$/, /\/debugger-proxy/, /\/reload$/, /\/launch-js-devtools/, /localhost:8081/, /100\.64\.\d+\.\d+:8081/,
50
+ // iOS simulator
51
+ /10\.0\.\d+\.\d+:8081/ // Android emulator
52
+ ];
53
+
54
+ // Store original methods
55
+
56
+ constructor() {
57
+ // Store original methods
58
+ this.originalFetch = globalThis.fetch.bind(globalThis);
59
+ this.originalXHROpen = XMLHttpRequest.prototype.open;
60
+ this.originalXHRSend = XMLHttpRequest.prototype.send;
61
+ this.originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
62
+ }
63
+
64
+ /**
65
+ * Check if URL should be ignored from network monitoring
66
+ *
67
+ * Filters out development-related URLs like Metro bundler, debugger proxy,
68
+ * and symbolication requests to reduce noise in the network logs.
69
+ *
70
+ * @param url - The URL to check
71
+ * @returns True if the URL should be ignored
72
+ */
73
+ shouldIgnoreUrl(url) {
74
+ return this.ignoredUrls.some(pattern => pattern.test(url));
75
+ }
76
+
77
+ // Emit event to all listeners
78
+ emit(event) {
79
+ this.listeners.forEach(listener => {
80
+ try {
81
+ listener(event);
82
+ } catch (error) {
83
+ // Error in event listener - continuing with others
84
+ }
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Parse URL to extract query parameters and clean URL
90
+ *
91
+ * @param url - The URL to parse
92
+ * @returns Object containing cleaned URL and parsed query parameters
93
+ *
94
+ * @performance Uses manual parsing instead of URL constructor for better performance
95
+ */
96
+ parseUrl(url) {
97
+ let params = null;
98
+ const queryParamIdx = url.indexOf("?");
99
+ if (queryParamIdx > -1) {
100
+ params = {};
101
+ url.substring(queryParamIdx + 1).split("&").forEach(pair => {
102
+ const [key, value] = pair.split("=");
103
+ if (key && value !== undefined) {
104
+ params[key] = decodeURIComponent(value.replace(/\+/g, " "));
105
+ }
106
+ });
107
+ }
108
+ return {
109
+ url: queryParamIdx > -1 ? url.substring(0, queryParamIdx) : url,
110
+ params
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Process response body with size limits to prevent memory issues
116
+ * @param response - The Response object to process
117
+ * @param maxSize - Maximum body size in bytes (default: 1MB)
118
+ * @returns Object containing body, size, and truncation status
119
+ */
120
+ async processResponseBody(response, maxSize = 1024 * 1024 // 1MB default
121
+ ) {
122
+ try {
123
+ // Check Content-Length header first
124
+ const contentLength = response.headers.get("content-length");
125
+ if (contentLength) {
126
+ const size = parseInt(contentLength, 10);
127
+ if (!isNaN(size) && size > maxSize) {
128
+ return {
129
+ body: `[Response too large: ${this.formatBytes(size)}]`,
130
+ size,
131
+ truncated: true
132
+ };
133
+ }
134
+ }
135
+
136
+ // Read the response text
137
+ const text = await response.text();
138
+ const size = text.length;
139
+
140
+ // Check if text exceeds max size
141
+ if (size > maxSize) {
142
+ const preview = text.substring(0, maxSize);
143
+ const omitted = size - maxSize;
144
+ return {
145
+ body: `${preview}\n\n... [truncated, ${this.formatBytes(omitted)} omitted]`,
146
+ size,
147
+ truncated: true
148
+ };
149
+ }
150
+
151
+ // Try to parse as JSON
152
+ try {
153
+ return {
154
+ body: JSON.parse(text),
155
+ size,
156
+ truncated: false
157
+ };
158
+ } catch {
159
+ // Return as text if not JSON
160
+ return {
161
+ body: text,
162
+ size,
163
+ truncated: false
164
+ };
165
+ }
166
+ } catch (error) {
167
+ return {
168
+ body: "~~~ unable to read body ~~~",
169
+ size: 0,
170
+ truncated: false
171
+ };
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Format bytes into human-readable format
177
+ * @param bytes - Number of bytes
178
+ * @returns Formatted string (e.g., "1.5 MB")
179
+ */
180
+ formatBytes(bytes) {
181
+ if (bytes === 0) return "0 B";
182
+ const k = 1024;
183
+ const sizes = ["B", "KB", "MB", "GB"];
184
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
185
+ return `${Math.round(bytes / Math.pow(k, i) * 100) / 100} ${sizes[i]}`;
186
+ }
187
+
188
+ // Get response body size
189
+ getResponseSize(body) {
190
+ if (!body) return 0;
191
+ if (typeof body === "string") return body.length;
192
+ if (typeof body === "object") {
193
+ try {
194
+ return JSON.stringify(body).length;
195
+ } catch {
196
+ return 0;
197
+ }
198
+ }
199
+ return 0;
200
+ }
201
+
202
+ /**
203
+ * Handle XMLHttpRequest response processing
204
+ * This method processes the response and emits appropriate events
205
+ */
206
+ handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "axios") {
207
+ const duration = startTime ? Date.now() - startTime : 0;
208
+ if (isError || xhr.status === 0) {
209
+ // Network error or aborted request
210
+ this.emit({
211
+ type: "error",
212
+ timestamp: new Date(),
213
+ duration,
214
+ request: {
215
+ id: requestId || "unknown",
216
+ url: cleanUrl,
217
+ method,
218
+ headers: requestHeaders,
219
+ data: requestData,
220
+ params: params || undefined,
221
+ client: clientType
222
+ },
223
+ error: {
224
+ message: isError ? "Request failed" : "Network error or request aborted"
225
+ }
226
+ });
227
+ return;
228
+ }
229
+
230
+ // Parse response
231
+ let body;
232
+ let responseSize = 0;
233
+ try {
234
+ const response = xhr.response;
235
+
236
+ // Debug logging to help diagnose responseType issues
237
+
238
+ // Try different ways to get response based on responseType
239
+ if (xhr.responseType === "json" && response) {
240
+ // Response is already parsed as JSON
241
+ body = response;
242
+ responseSize = JSON.stringify(response).length;
243
+ } else if (xhr.responseType === "" || xhr.responseType === "text") {
244
+ // Only access responseText when responseType allows it
245
+ if (xhr.responseText) {
246
+ responseSize = xhr.responseText.length;
247
+ try {
248
+ body = JSON.parse(xhr.responseText);
249
+ } catch {
250
+ body = xhr.responseText;
251
+ }
252
+ }
253
+ } else if (xhr.responseType === "arraybuffer") {
254
+ // For arraybuffer, try to decode as text and parse as JSON
255
+ // This is common for Axios requests that return JSON
256
+ if (response) {
257
+ try {
258
+ const text = new TextDecoder("utf-8").decode(response);
259
+ responseSize = text.length;
260
+ try {
261
+ body = JSON.parse(text);
262
+ } catch {
263
+ // Not JSON, return as text
264
+ body = text;
265
+ }
266
+ } catch (decodeError) {
267
+ // Failed to decode, show placeholder
268
+ body = `[arraybuffer response - ${response.byteLength || 0} bytes]`;
269
+ responseSize = response.byteLength || 0;
270
+ }
271
+ } else {
272
+ body = `[arraybuffer response - no data]`;
273
+ responseSize = 0;
274
+ }
275
+ } else if (xhr.responseType === "blob") {
276
+ // For blob responses, we can't synchronously read the content
277
+ // Note: In React Native, most JSON responses shouldn't be blobs
278
+ // but if they are, we show metadata
279
+ if (response instanceof Blob) {
280
+ body = `[blob response - ${response.size} bytes, type: ${response.type || "unknown"}]`;
281
+ responseSize = response.size;
282
+ } else if (response) {
283
+ // Sometimes response might not be a Blob object but still have data
284
+ // Try to handle it as an object
285
+ try {
286
+ body = typeof response === "string" ? JSON.parse(response) : response;
287
+ responseSize = JSON.stringify(body).length;
288
+ } catch {
289
+ body = `[blob response - unable to parse]`;
290
+ responseSize = 0;
291
+ }
292
+ } else {
293
+ body = `[blob response - no data]`;
294
+ responseSize = 0;
295
+ }
296
+ } else if (response) {
297
+ // Fallback: try to handle the response regardless of responseType
298
+ // This catches cases where Axios sets an unexpected responseType
299
+ if (typeof response === "string") {
300
+ body = response;
301
+ responseSize = response.length;
302
+ // Try to parse as JSON if it's a string
303
+ try {
304
+ body = JSON.parse(response);
305
+ } catch {
306
+ // Not JSON, keep as string
307
+ }
308
+ } else if (typeof response === "object") {
309
+ // Already an object, use it directly
310
+ body = response;
311
+ responseSize = JSON.stringify(response).length;
312
+ } else {
313
+ body = String(response);
314
+ responseSize = String(response).length;
315
+ }
316
+ }
317
+ } catch (error) {
318
+ // Failed to parse response
319
+ body = "~~~ unable to read body ~~~";
320
+ }
321
+
322
+ // Parse response headers
323
+ const responseHeaders = {};
324
+ try {
325
+ const headerString = xhr.getAllResponseHeaders();
326
+ if (headerString) {
327
+ headerString.split("\r\n").forEach(line => {
328
+ if (line) {
329
+ const colonIndex = line.indexOf(": ");
330
+ if (colonIndex > 0) {
331
+ const key = line.substring(0, colonIndex).toLowerCase();
332
+ const value = line.substring(colonIndex + 2);
333
+ responseHeaders[key] = value;
334
+ }
335
+ }
336
+ });
337
+ }
338
+ } catch {
339
+ // Ignore header parsing errors
340
+ }
341
+
342
+ // Emit response or error based on status
343
+ if (xhr.status >= 200 && xhr.status < 400) {
344
+ this.emit({
345
+ type: "response",
346
+ timestamp: new Date(),
347
+ duration,
348
+ request: {
349
+ id: requestId || "unknown",
350
+ url: cleanUrl,
351
+ method,
352
+ headers: requestHeaders,
353
+ data: requestData,
354
+ params: params || undefined,
355
+ client: clientType
356
+ },
357
+ response: {
358
+ status: xhr.status,
359
+ statusText: xhr.statusText,
360
+ headers: responseHeaders,
361
+ body,
362
+ size: responseSize
363
+ }
364
+ });
365
+ } else {
366
+ this.emit({
367
+ type: "error",
368
+ timestamp: new Date(),
369
+ duration,
370
+ request: {
371
+ id: requestId || "unknown",
372
+ url: cleanUrl,
373
+ method,
374
+ headers: requestHeaders,
375
+ data: requestData,
376
+ params: params || undefined,
377
+ client: clientType
378
+ },
379
+ response: {
380
+ status: xhr.status,
381
+ statusText: xhr.statusText,
382
+ headers: responseHeaders,
383
+ body,
384
+ size: responseSize
385
+ },
386
+ error: {
387
+ message: `HTTP ${xhr.status}: ${xhr.statusText}`
388
+ }
389
+ });
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Start intercepting network operations by swizzling fetch and XMLHttpRequest
395
+ *
396
+ * This method replaces the global fetch function and XMLHttpRequest methods
397
+ * with instrumented versions that emit events while preserving original functionality.
398
+ *
399
+ * @throws Will log warnings if already listening
400
+ *
401
+ * @performance Uses method swizzling for minimal runtime overhead
402
+ * @performance Includes request deduplication through ignored URL patterns
403
+ */
404
+ startListening() {
405
+ if (this.isListening) {
406
+ return;
407
+ }
408
+ const self = this;
409
+
410
+ // Swizzle fetch
411
+ globalThis.fetch = async (input, init) => {
412
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
413
+
414
+ // Skip ignored URLs
415
+ if (self.shouldIgnoreUrl(url)) {
416
+ return self.originalFetch(input, init);
417
+ }
418
+ const startTime = Date.now();
419
+ const requestId = `fetch_${++self.requestCounter}`;
420
+ const method = init?.method || "GET";
421
+ const {
422
+ url: cleanUrl,
423
+ params
424
+ } = self.parseUrl(url);
425
+
426
+ // Parse request headers
427
+ let requestHeaders = {};
428
+ if (init?.headers) {
429
+ if (init.headers instanceof Headers) {
430
+ init.headers.forEach((value, key) => {
431
+ requestHeaders[key] = value;
432
+ });
433
+ } else if (Array.isArray(init.headers)) {
434
+ init.headers.forEach(([key, value]) => {
435
+ requestHeaders[key] = value;
436
+ });
437
+ } else {
438
+ requestHeaders = init.headers;
439
+ }
440
+ }
441
+
442
+ // Parse request body
443
+ let requestData;
444
+ if (init?.body) {
445
+ if (typeof init.body === "string") {
446
+ try {
447
+ requestData = JSON.parse(init.body);
448
+ } catch {
449
+ requestData = init.body;
450
+ }
451
+ } else {
452
+ requestData = init.body;
453
+ }
454
+ }
455
+
456
+ // Determine client type from X-Request-Client header or default to fetch
457
+ const clientType = requestHeaders["X-Request-Client"] || requestHeaders["x-request-client"] || "fetch";
458
+
459
+ // Emit request event
460
+ self.emit({
461
+ type: "request",
462
+ timestamp: new Date(),
463
+ request: {
464
+ id: requestId || "unknown",
465
+ url: cleanUrl,
466
+ method,
467
+ headers: requestHeaders,
468
+ data: requestData,
469
+ params: params || undefined,
470
+ client: clientType
471
+ }
472
+ });
473
+ try {
474
+ const response = await self.originalFetch(input, init);
475
+ const duration = Date.now() - startTime;
476
+
477
+ // Clone response to read body with size limits
478
+ const responseClone = response.clone();
479
+ const {
480
+ body,
481
+ size: responseSize,
482
+ truncated
483
+ } = await self.processResponseBody(responseClone);
484
+
485
+ // Parse response headers
486
+ const responseHeaders = {};
487
+ response.headers.forEach((value, key) => {
488
+ responseHeaders[key.toLowerCase()] = value;
489
+ });
490
+
491
+ // Emit response event
492
+ self.emit({
493
+ type: "response",
494
+ timestamp: new Date(),
495
+ duration,
496
+ request: {
497
+ id: requestId || "unknown",
498
+ url: cleanUrl,
499
+ method,
500
+ headers: requestHeaders,
501
+ data: requestData,
502
+ params: params || undefined,
503
+ client: clientType
504
+ },
505
+ response: {
506
+ status: response.status,
507
+ statusText: response.statusText,
508
+ headers: responseHeaders,
509
+ body,
510
+ size: responseSize
511
+ }
512
+ });
513
+ return response;
514
+ } catch (error) {
515
+ const duration = Date.now() - startTime;
516
+
517
+ // Emit error event
518
+ self.emit({
519
+ type: "error",
520
+ timestamp: new Date(),
521
+ duration,
522
+ request: {
523
+ id: requestId || "unknown",
524
+ url: cleanUrl,
525
+ method,
526
+ headers: requestHeaders,
527
+ data: requestData,
528
+ params: params || undefined,
529
+ client: clientType
530
+ },
531
+ error: {
532
+ message: error instanceof Error ? error.message : "Network error",
533
+ stack: error instanceof Error ? error.stack : undefined
534
+ }
535
+ });
536
+ throw error;
537
+ }
538
+ };
539
+
540
+ // Swizzle XMLHttpRequest
541
+ XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
542
+ // Store request info on the xhr instance
543
+ const xhr = this;
544
+ xhr._requestId = `xhr_${++self.requestCounter}`;
545
+ xhr._method = method;
546
+ xhr._url = url;
547
+ xhr._startTime = Date.now();
548
+ xhr._requestHeaders = {};
549
+ return self.originalXHROpen.call(this, method, url, async, user, password);
550
+ };
551
+
552
+ // Track request headers
553
+ XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
554
+ const xhr = this;
555
+ if (xhr._requestHeaders) {
556
+ xhr._requestHeaders[header] = value;
557
+ }
558
+ return self.originalXHRSetRequestHeader.call(this, header, value);
559
+ };
560
+ XMLHttpRequest.prototype.send = function (
561
+ // @ts-ignore - this does exist on native
562
+ data) {
563
+ const xhr = this;
564
+ const requestId = xhr._requestId;
565
+ const method = xhr._method || "GET";
566
+ const url = xhr._url || "";
567
+ const startTime = xhr._startTime;
568
+ const requestHeaders = xhr._requestHeaders || {};
569
+
570
+ // Skip ignored URLs
571
+ if (self.shouldIgnoreUrl(url)) {
572
+ return self.originalXHRSend.call(this, data);
573
+ }
574
+ const {
575
+ url: cleanUrl,
576
+ params
577
+ } = self.parseUrl(url);
578
+
579
+ // Parse request data
580
+ let requestData;
581
+ if (data) {
582
+ if (typeof data === "string") {
583
+ try {
584
+ requestData = JSON.parse(data);
585
+ } catch {
586
+ requestData = data;
587
+ }
588
+ } else {
589
+ requestData = data;
590
+ }
591
+ }
592
+
593
+ // Determine client type from X-Request-Client header
594
+ const clientType = requestHeaders["X-Request-Client"] || requestHeaders["x-request-client"] || "axios";
595
+
596
+ // Emit request event
597
+ self.emit({
598
+ type: "request",
599
+ timestamp: new Date(),
600
+ request: {
601
+ id: requestId || "unknown",
602
+ url: cleanUrl,
603
+ method,
604
+ headers: requestHeaders,
605
+ data: requestData,
606
+ params: params || undefined,
607
+ client: clientType
608
+ }
609
+ });
610
+
611
+ // Track if we've already processed this request to avoid duplicate events
612
+ let processed = false;
613
+ const processResponse = (isError = false) => {
614
+ if (processed) return;
615
+ processed = true;
616
+ self.handleXHRResponse(this, requestId || "unknown", cleanUrl, method, requestHeaders, requestData, params, startTime || 0, isError, clientType);
617
+ // Clean up event listeners after processing to prevent memory leaks
618
+ cleanup();
619
+ };
620
+
621
+ // Cleanup function to remove event listeners
622
+ const cleanup = () => {
623
+ this.removeEventListener("load", loadListener);
624
+ this.removeEventListener("error", errorListener);
625
+ this.removeEventListener("abort", abortListener);
626
+ this.removeEventListener("readystatechange", readyStateListener);
627
+ };
628
+
629
+ // Use addEventListener to listen to events WITHOUT replacing user handlers
630
+ // This is critical because React Native's XMLHttpRequest uses EventTarget
631
+ // with getters/setters that shouldn't be overridden
632
+
633
+ const loadListener = () => {
634
+ processResponse(false);
635
+ };
636
+ const errorListener = () => {
637
+ processResponse(true);
638
+ };
639
+ const abortListener = () => {
640
+ processResponse(true);
641
+ };
642
+ const readyStateListener = () => {
643
+ if (this.readyState === 4 && !processed) {
644
+ processResponse(false);
645
+ }
646
+ };
647
+
648
+ // Add event listeners that will fire alongside user handlers
649
+ this.addEventListener("load", loadListener);
650
+ this.addEventListener("error", errorListener);
651
+ this.addEventListener("abort", abortListener);
652
+ this.addEventListener("readystatechange", readyStateListener);
653
+ return self.originalXHRSend.call(this, data);
654
+ };
655
+ this.isListening = true;
656
+ if (__DEV__) {
657
+ // Network listener has started monitoring fetch and XMLHttpRequest operations
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Stop listening and restore original networking methods
663
+ *
664
+ * This method restores the original fetch and XMLHttpRequest implementations,
665
+ * effectively disabling network monitoring.
666
+ */
667
+ stopListening() {
668
+ if (!this.isListening) {
669
+ return;
670
+ }
671
+
672
+ // Restore original methods
673
+ globalThis.fetch = this.originalFetch;
674
+ XMLHttpRequest.prototype.open = this.originalXHROpen;
675
+ XMLHttpRequest.prototype.send = this.originalXHRSend;
676
+ XMLHttpRequest.prototype.setRequestHeader = this.originalXHRSetRequestHeader;
677
+ this.isListening = false;
678
+ if (__DEV__) {
679
+ // Network listener has stopped monitoring and restored original methods
680
+ }
681
+ }
682
+
683
+ /**
684
+ * Add a listener for network events
685
+ *
686
+ * @param listener - Callback function to handle network events
687
+ * @returns Unsubscribe function to remove the listener
688
+ */
689
+ addListener(listener) {
690
+ this.listeners.push(listener);
691
+
692
+ // Return unsubscribe function
693
+ return () => {
694
+ const index = this.listeners.indexOf(listener);
695
+ if (index > -1) {
696
+ this.listeners.splice(index, 1);
697
+ }
698
+ };
699
+ }
700
+
701
+ // Remove all listeners
702
+ removeAllListeners() {
703
+ this.listeners = [];
704
+ }
705
+
706
+ // Check if currently listening
707
+ get isActive() {
708
+ return this.isListening;
709
+ }
710
+
711
+ // Get number of active listeners
712
+ get listenerCount() {
713
+ return this.listeners.length;
714
+ }
715
+ }
716
+
717
+ /**
718
+ * Lazy singleton instance holder for NetworkListener
719
+ *
720
+ * This pattern ensures only one NetworkListener instance exists throughout
721
+ * the application lifecycle while deferring instantiation until first use.
722
+ */
723
+ let _networkListener = null;
724
+
725
+ /**
726
+ * Get or create the singleton NetworkListener instance
727
+ *
728
+ * @returns The singleton NetworkListener instance
729
+ */
730
+ const getNetworkListener = () => {
731
+ if (!_networkListener) {
732
+ _networkListener = new NetworkListener();
733
+ }
734
+ return _networkListener;
735
+ };
736
+
737
+ /**
738
+ * Access function for the singleton NetworkListener instance
739
+ *
740
+ * @returns Function that returns the NetworkListener instance
741
+ */
742
+ const networkListener = exports.networkListener = getNetworkListener;
743
+
744
+ /**
745
+ * Start network traffic monitoring
746
+ *
747
+ * @example
748
+ * ```typescript
749
+ * startNetworkListener();
750
+ * console.log('Network monitoring started');
751
+ * ```
752
+ */
753
+ const startNetworkListener = () => getNetworkListener().startListening();
754
+
755
+ /**
756
+ * Stop network traffic monitoring
757
+ */
758
+ exports.startNetworkListener = startNetworkListener;
759
+ const stopNetworkListener = () => getNetworkListener().stopListening();
760
+
761
+ /**
762
+ * Add a listener for network events
763
+ *
764
+ * @param listener - Callback function to handle network events
765
+ * @returns Unsubscribe function to remove the listener
766
+ *
767
+ * @example
768
+ * ```typescript
769
+ * const unsubscribe = addNetworkListener((event) => {
770
+ * console.log(`Network ${event.type}:`, event.request.url);
771
+ * });
772
+ *
773
+ * // Later...
774
+ * unsubscribe();
775
+ * ```
776
+ */
777
+ exports.stopNetworkListener = stopNetworkListener;
778
+ const addNetworkListener = listener => getNetworkListener().addListener(listener);
779
+
780
+ /**
781
+ * Remove all registered network event listeners
782
+ */
783
+ exports.addNetworkListener = addNetworkListener;
784
+ const removeAllNetworkListeners = () => getNetworkListener().removeAllListeners();
785
+
786
+ /**
787
+ * Check if network monitoring is currently active
788
+ *
789
+ * @returns True if currently intercepting network traffic
790
+ */
791
+ exports.removeAllNetworkListeners = removeAllNetworkListeners;
792
+ const isNetworkListening = () => getNetworkListener().isActive;
793
+
794
+ /**
795
+ * Get the number of registered network event listeners
796
+ *
797
+ * @returns Number of active listeners
798
+ */
799
+ exports.isNetworkListening = isNetworkListening;
800
+ const getNetworkListenerCount = () => getNetworkListener().listenerCount;
801
+ exports.getNetworkListenerCount = getNetworkListenerCount;