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