@buoy-gg/network 7.0.26 → 7.0.28

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 (53) hide show
  1. package/lib/commonjs/network/components/NetworkDetailStepper.js +2 -1
  2. package/lib/commonjs/network/components/NetworkEventDetailView.js +74 -72
  3. package/lib/commonjs/network/components/NetworkEventItemCompact.js +26 -20
  4. package/lib/commonjs/network/components/NetworkEventItemCompact.web.js +24 -6
  5. package/lib/commonjs/network/components/NetworkFilterViewV3.js +12 -12
  6. package/lib/commonjs/network/components/NetworkHeaderLive.js +25 -29
  7. package/lib/commonjs/network/components/NetworkHeaderLive.web.js +24 -6
  8. package/lib/commonjs/network/components/NetworkHeaderMenu.js +10 -9
  9. package/lib/commonjs/network/components/NetworkListScreen.js +2 -1
  10. package/lib/commonjs/network/components/NetworkListScreen.web.js +24 -6
  11. package/lib/commonjs/network/components/NetworkModal.js +45 -31
  12. package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +10 -7
  13. package/lib/commonjs/network/components/NetworkOverridesButton.js +7 -7
  14. package/lib/commonjs/network/sync/networkSyncAdapter.js +36 -16
  15. package/lib/commonjs/network/utils/httpCompletion.js +37 -0
  16. package/lib/commonjs/network/utils/networkEventStore.js +26 -0
  17. package/lib/commonjs/network/utils/networkListener.js +180 -43
  18. package/lib/module/network/components/NetworkDetailStepper.js +2 -1
  19. package/lib/module/network/components/NetworkEventDetailView.js +75 -73
  20. package/lib/module/network/components/NetworkEventItemCompact.js +27 -21
  21. package/lib/module/network/components/NetworkEventItemCompact.web.js +24 -6
  22. package/lib/module/network/components/NetworkFilterViewV3.js +13 -13
  23. package/lib/module/network/components/NetworkHeaderLive.js +26 -30
  24. package/lib/module/network/components/NetworkHeaderLive.web.js +24 -6
  25. package/lib/module/network/components/NetworkHeaderMenu.js +11 -10
  26. package/lib/module/network/components/NetworkListScreen.js +3 -2
  27. package/lib/module/network/components/NetworkListScreen.web.js +24 -6
  28. package/lib/module/network/components/NetworkModal.js +46 -32
  29. package/lib/module/network/components/NetworkOverrideHeaderButton.js +11 -8
  30. package/lib/module/network/components/NetworkOverridesButton.js +8 -8
  31. package/lib/module/network/sync/networkSyncAdapter.js +37 -17
  32. package/lib/module/network/utils/httpCompletion.js +32 -0
  33. package/lib/module/network/utils/networkEventStore.js +26 -0
  34. package/lib/module/network/utils/networkListener.js +178 -42
  35. package/lib/typescript/network/components/NetworkDetailStepper.d.ts.map +1 -1
  36. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  37. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  38. package/lib/typescript/network/components/NetworkEventItemCompact.web.d.ts.map +1 -1
  39. package/lib/typescript/network/components/NetworkFilterViewV3.d.ts.map +1 -1
  40. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  41. package/lib/typescript/network/components/NetworkHeaderLive.web.d.ts.map +1 -1
  42. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  43. package/lib/typescript/network/components/NetworkListScreen.web.d.ts.map +1 -1
  44. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  45. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +1 -0
  46. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  47. package/lib/typescript/network/utils/httpCompletion.d.ts +11 -0
  48. package/lib/typescript/network/utils/httpCompletion.d.ts.map +1 -0
  49. package/lib/typescript/network/utils/networkEventStore.d.ts +17 -0
  50. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  51. package/lib/typescript/network/utils/networkListener.d.ts +61 -5
  52. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  53. package/package.json +2 -2
@@ -79,24 +79,27 @@ function NetworkOverrideHeaderButton({
79
79
  });
80
80
  }
81
81
  const styles = _reactNative.StyleSheet.create({
82
+ // Night: the header's dark-glass chip, with the accent label the night
83
+ // language gives labelled actions (plate + accent ink). Active keeps the
84
+ // warning tint — same "you are not seeing the truth" signal as the flask.
82
85
  button: {
83
86
  paddingHorizontal: 10,
84
87
  paddingVertical: 6,
85
- borderRadius: 6,
88
+ borderRadius: 8,
86
89
  borderWidth: 1,
87
- borderColor: _sharedUi.macOSColors.border.input,
88
- backgroundColor: _sharedUi.macOSColors.background.input
90
+ borderColor: _sharedUi.night.color.border,
91
+ backgroundColor: _sharedUi.night.color.buttonSurface
89
92
  },
90
93
  buttonActive: {
91
- borderColor: _sharedUi.macOSColors.semantic.warning + "77",
92
- backgroundColor: _sharedUi.macOSColors.semantic.warningBackground
94
+ borderColor: _sharedUi.night.color.warning + "77",
95
+ backgroundColor: _sharedUi.night.color.warningSoft
93
96
  },
94
97
  label: {
95
98
  fontSize: 11,
96
99
  fontWeight: "700",
97
- color: _sharedUi.macOSColors.text.primary
100
+ color: _sharedUi.night.color.accent
98
101
  },
99
102
  labelActive: {
100
- color: _sharedUi.macOSColors.semantic.warning
103
+ color: _sharedUi.night.color.warning
101
104
  }
102
105
  });
@@ -35,7 +35,7 @@ function NetworkOverridesButton({
35
35
  accessibilityLabel: active ? `Response overrides — ${activeCount} active` : "Response overrides",
36
36
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.FlaskConical, {
37
37
  size: 14,
38
- color: active ? _sharedUi.macOSColors.semantic.warning : _sharedUi.macOSColors.text.secondary
38
+ color: active ? _sharedUi.night.color.warning : _sharedUi.night.color.textSecondary
39
39
  }), active ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
40
40
  style: styles.badge,
41
41
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -56,15 +56,15 @@ const styles = _reactNative.StyleSheet.create({
56
56
  borderRadius: 8,
57
57
  alignItems: "center",
58
58
  justifyContent: "center",
59
- backgroundColor: _sharedUi.macOSColors.background.hover,
59
+ backgroundColor: _sharedUi.night.color.buttonSurface,
60
60
  borderWidth: 1,
61
- borderColor: _sharedUi.macOSColors.border.default
61
+ borderColor: _sharedUi.night.color.border
62
62
  },
63
63
  // Active fills the whole box, the way the power toggle goes green — the
64
64
  // warning now reads at a glance instead of as a tinted glyph.
65
65
  buttonActive: {
66
- backgroundColor: _sharedUi.macOSColors.semantic.warningBackground,
67
- borderColor: _sharedUi.macOSColors.semantic.warning + "77"
66
+ backgroundColor: _sharedUi.night.color.warningSoft,
67
+ borderColor: _sharedUi.night.color.warning + "77"
68
68
  },
69
69
  badge: {
70
70
  position: "absolute",
@@ -78,11 +78,11 @@ const styles = _reactNative.StyleSheet.create({
78
78
  paddingHorizontal: 3,
79
79
  alignItems: "center",
80
80
  justifyContent: "center",
81
- backgroundColor: _sharedUi.macOSColors.semantic.warning
81
+ backgroundColor: _sharedUi.night.color.warning
82
82
  },
83
83
  badgeText: {
84
84
  fontSize: 8,
85
85
  fontWeight: "800",
86
- color: _sharedUi.macOSColors.background.base
86
+ color: "#000000"
87
87
  }
88
88
  });
@@ -364,22 +364,30 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
364
364
  // flags, and a total snapshot budget so the list degrades instead of the
365
365
  // whole panel being dropped at the emit layer.
366
366
  version: 5,
367
- getSnapshot: () => ({
368
- events: fitEventsToBudget(_networkEventStore.networkEventStore.getEvents()),
369
- /**
370
- * Response-override rules live on the DEVICE — that's the only place they
371
- * can run, since a rule is applied inside the app's own XMLHttpRequest.
372
- * Remote surfaces mirror this and forward their edits back as actions.
373
- */
374
- overrides: stripLargeRuleBodies(_overrideRulesStore.overrideRulesStore.getState()),
375
- // Saved snapshots go through the same body strip. They are few (capped in
376
- // the store) but they ride on EVERY snapshot, so a fat one would land in
377
- // the same 200ms loop that the body-stripping exists to protect.
378
- saved: _networkSavedStore.networkSavedStore.getRecords().map(record => ({
379
- ...record,
380
- event: toWireEvent(record.event).event
381
- }))
382
- }),
367
+ getSnapshot: () => {
368
+ // Piggyback the interception health check on the watch loop: while a
369
+ // dashboard/MCP watch is open this runs continuously, so a clobbered
370
+ // fetch/XHR patch (see networkListener.startListening) heals within one
371
+ // snapshot tick instead of staying dead for the session. Two property
372
+ // reads when healthy.
373
+ _networkEventStore.networkEventStore.ensureInterception();
374
+ return {
375
+ events: fitEventsToBudget(_networkEventStore.networkEventStore.getEvents()),
376
+ /**
377
+ * Response-override rules live on the DEVICE — that's the only place they
378
+ * can run, since a rule is applied inside the app's own XMLHttpRequest.
379
+ * Remote surfaces mirror this and forward their edits back as actions.
380
+ */
381
+ overrides: stripLargeRuleBodies(_overrideRulesStore.overrideRulesStore.getState()),
382
+ // Saved snapshots go through the same body strip. They are few (capped in
383
+ // the store) but they ride on EVERY snapshot, so a fat one would land in
384
+ // the same 200ms loop that the body-stripping exists to protect.
385
+ saved: _networkSavedStore.networkSavedStore.getRecords().map(record => ({
386
+ ...record,
387
+ event: toWireEvent(record.event).event
388
+ }))
389
+ };
390
+ },
383
391
  subscribe: onChange => {
384
392
  // Pinning with no traffic in flight still has to reach the dashboard, so
385
393
  // both stores drive the snapshot.
@@ -590,10 +598,22 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
590
598
  } catch {
591
599
  /* no interceptable runtime here */
592
600
  }
601
+ // `interceptorInstalled` is the listener's own flag; `interceptorLive`
602
+ // is whether the interceptors are actually reachable through the global
603
+ // chain. installed:true + live:false = something re-assigned fetch/XHR
604
+ // over the interceptor (it self-heals on the next subscribe/snapshot —
605
+ // see ensureInterception).
606
+ let interceptorLive = null;
607
+ try {
608
+ interceptorLive = (0, _networkListener.isNetworkInterceptionLive)();
609
+ } catch {
610
+ /* no interceptable runtime here */
611
+ }
593
612
  return {
594
613
  capturing: subscribers > 0,
595
614
  subscribers,
596
615
  interceptorInstalled,
616
+ interceptorLive,
597
617
  listenerCount,
598
618
  eventCount: _networkEventStore.networkEventStore.getEvents().length
599
619
  };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.httpStatusText = httpStatusText;
7
+ exports.xhrCompletionKind = xhrCompletionKind;
8
+ /**
9
+ * Classify a completed XHR/fetch HTTP status.
10
+ *
11
+ * A 4xx/5xx is a *response*, not a transport error. RN's XHR leaves
12
+ * `statusText` empty, which used to become `event.error = "HTTP 500: "` and
13
+ * then MCP `ERR HTTP 500: no error`.
14
+ */
15
+
16
+ const STATUS_TEXT = {
17
+ 400: "Bad Request",
18
+ 401: "Unauthorized",
19
+ 403: "Forbidden",
20
+ 404: "Not Found",
21
+ 408: "Request Timeout",
22
+ 429: "Too Many Requests",
23
+ 500: "Internal Server Error",
24
+ 502: "Bad Gateway",
25
+ 503: "Service Unavailable",
26
+ 504: "Gateway Timeout"
27
+ };
28
+
29
+ /** status 0 (or missing) means the request never got an HTTP response. */
30
+ function xhrCompletionKind(status) {
31
+ return typeof status === "number" && status > 0 ? "response" : "error";
32
+ }
33
+ function httpStatusText(status, statusText) {
34
+ const trimmed = (statusText ?? "").trim();
35
+ if (trimmed) return trimmed;
36
+ return STATUS_TEXT[status] ?? "";
37
+ }
@@ -175,6 +175,32 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
175
175
  return this.rawListenerUnsubscribe !== null;
176
176
  }
177
177
 
178
+ /**
179
+ * Re-assert the interceptor if something re-assigned fetch/XHR since capture
180
+ * started (Expo's runtime, RN DevTools, a second dev server's tooling — see
181
+ * networkListener.startListening). A no-op costing two property reads when
182
+ * everything is healthy, and a no-op entirely unless this store is actively
183
+ * capturing — a mirror-mode dashboard must never install an interceptor in
184
+ * its own process.
185
+ */
186
+ ensureInterception() {
187
+ if (!this.isCapturing()) return;
188
+ (0, _networkListener.networkListener)().startListening();
189
+ }
190
+
191
+ /**
192
+ * Every new consumer — the Network modal opening, a badge mounting, a
193
+ * dashboard watch attaching — is a chance to notice and repair a clobbered
194
+ * interceptor, which matters because a long-lived watch can hold the
195
+ * subscriber count above zero for the whole session (so the 0→1
196
+ * startCapturing path would never run again).
197
+ */
198
+ subscribeToEvents(listener) {
199
+ const unsubscribe = super.subscribeToEvents(listener);
200
+ this.ensureInterception();
201
+ return unsubscribe;
202
+ }
203
+
178
204
  /**
179
205
  * Process a network listener event
180
206
  */
@@ -3,11 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.getNetworkListenerCount = exports.addNetworkListener = void 0;
6
+ exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.isNetworkInterceptionLive = exports.getNetworkListenerCount = exports.addNetworkListener = exports.BUOY_XHR_INTERCEPTOR_FLAG = exports.BUOY_FETCH_INTERCEPTOR_FLAG = void 0;
7
7
  var _engine = require("../overrides/engine");
8
8
  var _applyToXhr = require("../overrides/applyToXhr");
9
9
  var _applyToFetch = require("../overrides/applyToFetch");
10
10
  var _types = require("../overrides/types");
11
+ var _httpCompletion = require("./httpCompletion");
11
12
  /**
12
13
  * Network listener using Reactotron-style event pattern
13
14
  * Simple and reliable network interception for React Native
@@ -31,6 +32,18 @@ let insideOverriddenFetch = false;
31
32
 
32
33
  // Event types for network operations
33
34
 
35
+ /**
36
+ * Markers stamped on the installed interceptor functions so their presence in
37
+ * the live chain stays detectable even after another tool wraps them.
38
+ * Cooperating patchers (@buoy-gg/impersonate) copy every `__buoy*` marker from
39
+ * the function they wrap onto their own wrapper; a global fetch/XHR WITHOUT
40
+ * the marker therefore means our interceptor was clobbered — some runtime
41
+ * re-assigned `globalThis.fetch` over it (Expo's winter runtime, React Native
42
+ * DevTools' network panel, a second dev server's tooling, hot-reload churn all
43
+ * do this in the wild) — and capture must re-install. See startListening.
44
+ */
45
+ const BUOY_FETCH_INTERCEPTOR_FLAG = exports.BUOY_FETCH_INTERCEPTOR_FLAG = "__buoyNetworkFetchIntercepted";
46
+ const BUOY_XHR_INTERCEPTOR_FLAG = exports.BUOY_XHR_INTERCEPTOR_FLAG = "__buoyNetworkXhrIntercepted";
34
47
  /**
35
48
  * Network traffic interceptor for React Native applications
36
49
  *
@@ -71,6 +84,25 @@ class NetworkListener {
71
84
 
72
85
  // Store original methods
73
86
 
87
+ /**
88
+ * The wrappers currently installed by THIS instance, for identity checks:
89
+ * `stopListening` restores a global only when it is still literally ours —
90
+ * assigning our saved original over someone who patched after us would wipe
91
+ * their interceptor exactly the way this class gets wiped (see below).
92
+ */
93
+ patchedFetch = null;
94
+ patchedXHROpen = null;
95
+ patchedXHRSend = null;
96
+ patchedXHRSetRequestHeader = null;
97
+
98
+ /**
99
+ * Bumped on every (re)install and on stop. Each wrapper closes over the
100
+ * generation it was born with and goes silent (pure pass-through — no emit,
101
+ * no counter bump) once a newer generation exists, so a wrapper stranded
102
+ * inside another tool's chain can never double-report a request.
103
+ */
104
+ fetchGeneration = 0;
105
+ xhrGeneration = 0;
74
106
  constructor() {
75
107
  // Store original methods
76
108
  this.originalFetch = globalThis.fetch.bind(globalThis);
@@ -354,8 +386,10 @@ class NetworkListener {
354
386
  // Ignore header parsing errors
355
387
  }
356
388
 
357
- // Emit response or error based on status
358
- if (xhr.status >= 200 && xhr.status < 400) {
389
+ // Emit response or error based on status. A 4xx/5xx is a completed HTTP
390
+ // response, not a transport error — RN XHR leaves statusText empty, which
391
+ // used to become `event.error = "HTTP 500: "` and MCP `ERR HTTP 500: no error`.
392
+ if ((0, _httpCompletion.xhrCompletionKind)(xhr.status) === "response") {
359
393
  this.emit({
360
394
  type: "response",
361
395
  timestamp: new Date(),
@@ -372,7 +406,7 @@ class NetworkListener {
372
406
  },
373
407
  response: {
374
408
  status: xhr.status,
375
- statusText: xhr.statusText,
409
+ statusText: (0, _httpCompletion.httpStatusText)(xhr.status, xhr.statusText),
376
410
  headers: responseHeaders,
377
411
  body,
378
412
  size: responseSize
@@ -401,37 +435,89 @@ class NetworkListener {
401
435
  size: responseSize
402
436
  },
403
437
  error: {
404
- message: `HTTP ${xhr.status}: ${xhr.statusText}`
438
+ message: "Network Error"
405
439
  },
406
440
  override
407
441
  });
408
442
  }
409
443
  }
410
444
 
445
+ /** Is our fetch interceptor still reachable through the global chain? */
446
+ isFetchInterceptionLive() {
447
+ return globalThis.fetch[BUOY_FETCH_INTERCEPTOR_FLAG] === true;
448
+ }
449
+
450
+ /** Is our XHR interceptor still reachable through the prototype chain? */
451
+ isXhrInterceptionLive() {
452
+ return XMLHttpRequest.prototype.send[BUOY_XHR_INTERCEPTOR_FLAG] === true;
453
+ }
454
+
411
455
  /**
412
- * Start intercepting network operations by swizzling fetch and XMLHttpRequest
413
- *
414
- * This method replaces the global fetch function and XMLHttpRequest methods
415
- * with instrumented versions that emit events while preserving original functionality.
456
+ * Both halves of the interceptor are reachable — requests actually flow
457
+ * through capture. `isListening` alone cannot promise that (see
458
+ * startListening); diagnostics should report THIS.
459
+ */
460
+ isInterceptionLive() {
461
+ return this.isFetchInterceptionLive() && this.isXhrInterceptionLive();
462
+ }
463
+
464
+ /**
465
+ * Start (or repair) interception by swizzling fetch and XMLHttpRequest.
416
466
  *
417
- * @throws Will log warnings if already listening
467
+ * NOT guarded by a plain `isListening` flag: `globalThis.fetch` gets
468
+ * re-assigned in the wild — Expo's winter runtime, React Native DevTools'
469
+ * network panel, a second dev server's tooling, hot-reload churn. When that
470
+ * happens the flag still says "listening" while every request bypasses
471
+ * capture, and a flag-only guard makes the outage PERMANENT (observed as
472
+ * "No network events" forever with `interceptorInstalled: true`). So this
473
+ * verifies each interceptor is actually reachable (marker check) and
474
+ * re-installs whichever half was clobbered, wrapping the CURRENT global so
475
+ * whatever replaced us keeps working underneath. When both halves are live
476
+ * this costs two property reads.
418
477
  *
419
478
  * @performance Uses method swizzling for minimal runtime overhead
420
479
  * @performance Includes request deduplication through ignored URL patterns
421
480
  */
422
481
  startListening() {
423
- if (this.isListening) {
482
+ const fetchLive = this.isListening && this.isFetchInterceptionLive();
483
+ const xhrLive = this.isListening && this.isXhrInterceptionLive();
484
+ if (fetchLive && xhrLive) {
424
485
  return;
425
486
  }
426
- const self = this;
487
+ if (!fetchLive) {
488
+ this.installFetchInterceptor();
489
+ }
490
+ if (!xhrLive) {
491
+ this.installXhrInterceptor();
492
+ }
493
+ this.isListening = true;
494
+ if (__DEV__) {
495
+ // Network listener has started monitoring fetch and XMLHttpRequest operations
496
+ }
497
+ }
427
498
 
428
- // Swizzle fetch
429
- globalThis.fetch = async (input, init) => {
499
+ /** Swizzle `globalThis.fetch`, wrapping whatever is installed right now. */
500
+ installFetchInterceptor() {
501
+ const self = this;
502
+ // The pass-through is the CURRENT global (not the construction-time one):
503
+ // re-installing after a clobber must call through the replacement, and a
504
+ // stale generation must replay the chain exactly as it stood beneath it.
505
+ const passthroughFetch = globalThis.fetch.bind(globalThis);
506
+ this.originalFetch = passthroughFetch;
507
+ const generation = ++this.fetchGeneration;
508
+ const wrappedFetch = async (input, init) => {
430
509
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
431
510
 
511
+ // A newer install supersedes this wrapper. If it is still stranded in
512
+ // another tool's chain it must pass through silently, or one request
513
+ // would be reported once per stranded generation.
514
+ if (generation !== self.fetchGeneration) {
515
+ return passthroughFetch(input, init);
516
+ }
517
+
432
518
  // Skip ignored URLs
433
519
  if (self.shouldIgnoreUrl(url)) {
434
- return self.originalFetch(input, init);
520
+ return passthroughFetch(input, init);
435
521
  }
436
522
  const startTime = Date.now();
437
523
  const requestId = `fetch_${++self.requestCounter}`;
@@ -514,7 +600,7 @@ class NetworkListener {
514
600
  insideOverriddenFetch = outcome != null;
515
601
  let pending;
516
602
  try {
517
- pending = self.originalFetch(input, init);
603
+ pending = passthroughFetch(input, init);
518
604
  } finally {
519
605
  insideOverriddenFetch = false;
520
606
  }
@@ -617,30 +703,51 @@ class NetworkListener {
617
703
  throw error;
618
704
  }
619
705
  };
706
+ wrappedFetch[BUOY_FETCH_INTERCEPTOR_FLAG] = true;
707
+ this.patchedFetch = wrappedFetch;
708
+ globalThis.fetch = this.patchedFetch;
709
+ }
620
710
 
621
- // Swizzle XMLHttpRequest
622
- XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
623
- // Store request info on the xhr instance
624
- const xhr = this;
625
- xhr._requestId = `xhr_${++self.requestCounter}`;
626
- xhr._method = method;
627
- xhr._url = url;
628
- xhr._startTime = Date.now();
629
- xhr._requestHeaders = {};
630
- return self.originalXHROpen.call(this, method, url, async, user, password);
711
+ /** Swizzle the XHR prototype methods, wrapping whatever is installed now. */
712
+ installXhrInterceptor() {
713
+ const self = this;
714
+ // Same contract as the fetch half: pass through to the CURRENT methods.
715
+ const passthroughOpen = XMLHttpRequest.prototype.open;
716
+ const passthroughSend = XMLHttpRequest.prototype.send;
717
+ const passthroughSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
718
+ this.originalXHROpen = passthroughOpen;
719
+ this.originalXHRSend = passthroughSend;
720
+ this.originalXHRSetRequestHeader = passthroughSetRequestHeader;
721
+ const generation = ++this.xhrGeneration;
722
+ const patchedOpen = function (method, url, async, user, password) {
723
+ // Superseded generations must not re-stamp (a double stamp burns two
724
+ // request ids per request) — just pass through.
725
+ if (generation === self.xhrGeneration) {
726
+ const xhr = this;
727
+ xhr._requestId = `xhr_${++self.requestCounter}`;
728
+ xhr._method = method;
729
+ xhr._url = url;
730
+ xhr._startTime = Date.now();
731
+ xhr._requestHeaders = {};
732
+ }
733
+ return passthroughOpen.call(this, method, url, async ?? true, user, password);
631
734
  };
632
735
 
633
736
  // Track request headers
634
- XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
737
+ const patchedSetRequestHeader = function (header, value) {
635
738
  const xhr = this;
636
- if (xhr._requestHeaders) {
739
+ if (generation === self.xhrGeneration && xhr._requestHeaders) {
637
740
  xhr._requestHeaders[header] = value;
638
741
  }
639
- return self.originalXHRSetRequestHeader.call(this, header, value);
742
+ return passthroughSetRequestHeader.call(this, header, value);
640
743
  };
641
- XMLHttpRequest.prototype.send = function (
744
+ const patchedSend = function (
642
745
  // @ts-ignore - this does exist on native
643
746
  data) {
747
+ // Superseded generation: silent pass-through (see the fetch wrapper).
748
+ if (generation !== self.xhrGeneration) {
749
+ return passthroughSend.call(this, data);
750
+ }
644
751
  const xhr = this;
645
752
  const requestId = xhr._requestId;
646
753
  const method = xhr._method || "GET";
@@ -650,7 +757,7 @@ class NetworkListener {
650
757
 
651
758
  // Skip ignored URLs
652
759
  if (self.shouldIgnoreUrl(url)) {
653
- return self.originalXHRSend.call(this, data);
760
+ return passthroughSend.call(this, data);
654
761
  }
655
762
  const {
656
763
  url: cleanUrl,
@@ -767,18 +874,21 @@ class NetworkListener {
767
874
  // Listeners are attached above FIRST: `runOverride` drives the same
768
875
  // readyState transitions a real response would, and the interceptor has
769
876
  // to see them (readyState 2 is where TTFB is stamped).
770
- const handled = (0, _applyToXhr.runOverride)(this, url, outcome, () => self.originalXHRSend.call(this, data));
877
+ const handled = (0, _applyToXhr.runOverride)(this, url, outcome, () => passthroughSend.call(this, data));
771
878
  // `handled` is false only where this isn't React Native's own
772
879
  // XMLHttpRequest and can't be driven by hand. Fall through and send for
773
880
  // real rather than dropping the request on the floor.
774
881
  if (handled) return;
775
882
  }
776
- return self.originalXHRSend.call(this, data);
883
+ return passthroughSend.call(this, data);
777
884
  };
778
- this.isListening = true;
779
- if (__DEV__) {
780
- // Network listener has started monitoring fetch and XMLHttpRequest operations
781
- }
885
+ patchedSend[BUOY_XHR_INTERCEPTOR_FLAG] = true;
886
+ this.patchedXHROpen = patchedOpen;
887
+ this.patchedXHRSend = patchedSend;
888
+ this.patchedXHRSetRequestHeader = patchedSetRequestHeader;
889
+ XMLHttpRequest.prototype.open = this.patchedXHROpen;
890
+ XMLHttpRequest.prototype.send = this.patchedXHRSend;
891
+ XMLHttpRequest.prototype.setRequestHeader = this.patchedXHRSetRequestHeader;
782
892
  }
783
893
 
784
894
  /**
@@ -792,11 +902,30 @@ class NetworkListener {
792
902
  return;
793
903
  }
794
904
 
795
- // Restore original methods
796
- globalThis.fetch = this.originalFetch;
797
- XMLHttpRequest.prototype.open = this.originalXHROpen;
798
- XMLHttpRequest.prototype.send = this.originalXHRSend;
799
- XMLHttpRequest.prototype.setRequestHeader = this.originalXHRSetRequestHeader;
905
+ // Silence any of our wrappers stranded inside another tool's chain — they
906
+ // check the generation on every call and now pass through without emitting.
907
+ this.fetchGeneration++;
908
+ this.xhrGeneration++;
909
+
910
+ // Restore ONLY what is still literally ours. If another tool patched after
911
+ // us (impersonate, a debugger), assigning our saved originals would wipe
912
+ // ITS interceptor — the exact injury startListening defends against.
913
+ if (this.patchedFetch && globalThis.fetch === this.patchedFetch) {
914
+ globalThis.fetch = this.originalFetch;
915
+ }
916
+ if (this.patchedXHROpen && XMLHttpRequest.prototype.open === this.patchedXHROpen) {
917
+ XMLHttpRequest.prototype.open = this.originalXHROpen;
918
+ }
919
+ if (this.patchedXHRSend && XMLHttpRequest.prototype.send === this.patchedXHRSend) {
920
+ XMLHttpRequest.prototype.send = this.originalXHRSend;
921
+ }
922
+ if (this.patchedXHRSetRequestHeader && XMLHttpRequest.prototype.setRequestHeader === this.patchedXHRSetRequestHeader) {
923
+ XMLHttpRequest.prototype.setRequestHeader = this.originalXHRSetRequestHeader;
924
+ }
925
+ this.patchedFetch = null;
926
+ this.patchedXHROpen = null;
927
+ this.patchedXHRSend = null;
928
+ this.patchedXHRSetRequestHeader = null;
800
929
  this.isListening = false;
801
930
  if (__DEV__) {
802
931
  // Network listener has stopped monitoring and restored original methods
@@ -921,4 +1050,12 @@ const isNetworkListening = () => getNetworkListener().isActive;
921
1050
  */
922
1051
  exports.isNetworkListening = isNetworkListening;
923
1052
  const getNetworkListenerCount = () => getNetworkListener().listenerCount;
924
- exports.getNetworkListenerCount = getNetworkListenerCount;
1053
+
1054
+ /**
1055
+ * Whether both interceptors are actually reachable through the live global
1056
+ * chain — the honest version of `isNetworkListening` (whose flag stays true
1057
+ * even after another tool re-assigns `globalThis.fetch` over the wrapper).
1058
+ */
1059
+ exports.getNetworkListenerCount = getNetworkListenerCount;
1060
+ const isNetworkInterceptionLive = () => getNetworkListener().isInterceptionLive();
1061
+ exports.isNetworkInterceptionLive = isNetworkInterceptionLive;
@@ -98,6 +98,7 @@ export const NetworkDetailStepper = /*#__PURE__*/memo(function NetworkDetailStep
98
98
  itemLabel: "Request",
99
99
  applySafeAreaInset: applySafeAreaInset,
100
100
  onPrevious: stepPrevious,
101
- onNext: stepNext
101
+ onNext: stepNext,
102
+ variant: "night"
102
103
  });
103
104
  });