@buoy-gg/network 7.0.25 → 7.0.27

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 (49) 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 +24 -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/networkEventStore.js +26 -0
  16. package/lib/commonjs/network/utils/networkListener.js +173 -39
  17. package/lib/module/network/components/NetworkDetailStepper.js +2 -1
  18. package/lib/module/network/components/NetworkEventDetailView.js +75 -73
  19. package/lib/module/network/components/NetworkEventItemCompact.js +25 -21
  20. package/lib/module/network/components/NetworkEventItemCompact.web.js +24 -6
  21. package/lib/module/network/components/NetworkFilterViewV3.js +13 -13
  22. package/lib/module/network/components/NetworkHeaderLive.js +26 -30
  23. package/lib/module/network/components/NetworkHeaderLive.web.js +24 -6
  24. package/lib/module/network/components/NetworkHeaderMenu.js +11 -10
  25. package/lib/module/network/components/NetworkListScreen.js +3 -2
  26. package/lib/module/network/components/NetworkListScreen.web.js +24 -6
  27. package/lib/module/network/components/NetworkModal.js +46 -32
  28. package/lib/module/network/components/NetworkOverrideHeaderButton.js +11 -8
  29. package/lib/module/network/components/NetworkOverridesButton.js +8 -8
  30. package/lib/module/network/sync/networkSyncAdapter.js +37 -17
  31. package/lib/module/network/utils/networkEventStore.js +26 -0
  32. package/lib/module/network/utils/networkListener.js +171 -38
  33. package/lib/typescript/network/components/NetworkDetailStepper.d.ts.map +1 -1
  34. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  35. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  36. package/lib/typescript/network/components/NetworkEventItemCompact.web.d.ts.map +1 -1
  37. package/lib/typescript/network/components/NetworkFilterViewV3.d.ts.map +1 -1
  38. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  39. package/lib/typescript/network/components/NetworkHeaderLive.web.d.ts.map +1 -1
  40. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  41. package/lib/typescript/network/components/NetworkListScreen.web.d.ts.map +1 -1
  42. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  43. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +1 -0
  44. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  45. package/lib/typescript/network/utils/networkEventStore.d.ts +17 -0
  46. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  47. package/lib/typescript/network/utils/networkListener.d.ts +61 -5
  48. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  49. 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
  };
@@ -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,7 +3,7 @@
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");
@@ -31,6 +31,18 @@ let insideOverriddenFetch = false;
31
31
 
32
32
  // Event types for network operations
33
33
 
34
+ /**
35
+ * Markers stamped on the installed interceptor functions so their presence in
36
+ * the live chain stays detectable even after another tool wraps them.
37
+ * Cooperating patchers (@buoy-gg/impersonate) copy every `__buoy*` marker from
38
+ * the function they wrap onto their own wrapper; a global fetch/XHR WITHOUT
39
+ * the marker therefore means our interceptor was clobbered — some runtime
40
+ * re-assigned `globalThis.fetch` over it (Expo's winter runtime, React Native
41
+ * DevTools' network panel, a second dev server's tooling, hot-reload churn all
42
+ * do this in the wild) — and capture must re-install. See startListening.
43
+ */
44
+ const BUOY_FETCH_INTERCEPTOR_FLAG = exports.BUOY_FETCH_INTERCEPTOR_FLAG = "__buoyNetworkFetchIntercepted";
45
+ const BUOY_XHR_INTERCEPTOR_FLAG = exports.BUOY_XHR_INTERCEPTOR_FLAG = "__buoyNetworkXhrIntercepted";
34
46
  /**
35
47
  * Network traffic interceptor for React Native applications
36
48
  *
@@ -71,6 +83,25 @@ class NetworkListener {
71
83
 
72
84
  // Store original methods
73
85
 
86
+ /**
87
+ * The wrappers currently installed by THIS instance, for identity checks:
88
+ * `stopListening` restores a global only when it is still literally ours —
89
+ * assigning our saved original over someone who patched after us would wipe
90
+ * their interceptor exactly the way this class gets wiped (see below).
91
+ */
92
+ patchedFetch = null;
93
+ patchedXHROpen = null;
94
+ patchedXHRSend = null;
95
+ patchedXHRSetRequestHeader = null;
96
+
97
+ /**
98
+ * Bumped on every (re)install and on stop. Each wrapper closes over the
99
+ * generation it was born with and goes silent (pure pass-through — no emit,
100
+ * no counter bump) once a newer generation exists, so a wrapper stranded
101
+ * inside another tool's chain can never double-report a request.
102
+ */
103
+ fetchGeneration = 0;
104
+ xhrGeneration = 0;
74
105
  constructor() {
75
106
  // Store original methods
76
107
  this.originalFetch = globalThis.fetch.bind(globalThis);
@@ -408,30 +439,82 @@ class NetworkListener {
408
439
  }
409
440
  }
410
441
 
442
+ /** Is our fetch interceptor still reachable through the global chain? */
443
+ isFetchInterceptionLive() {
444
+ return globalThis.fetch[BUOY_FETCH_INTERCEPTOR_FLAG] === true;
445
+ }
446
+
447
+ /** Is our XHR interceptor still reachable through the prototype chain? */
448
+ isXhrInterceptionLive() {
449
+ return XMLHttpRequest.prototype.send[BUOY_XHR_INTERCEPTOR_FLAG] === true;
450
+ }
451
+
411
452
  /**
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.
453
+ * Both halves of the interceptor are reachable — requests actually flow
454
+ * through capture. `isListening` alone cannot promise that (see
455
+ * startListening); diagnostics should report THIS.
456
+ */
457
+ isInterceptionLive() {
458
+ return this.isFetchInterceptionLive() && this.isXhrInterceptionLive();
459
+ }
460
+
461
+ /**
462
+ * Start (or repair) interception by swizzling fetch and XMLHttpRequest.
416
463
  *
417
- * @throws Will log warnings if already listening
464
+ * NOT guarded by a plain `isListening` flag: `globalThis.fetch` gets
465
+ * re-assigned in the wild — Expo's winter runtime, React Native DevTools'
466
+ * network panel, a second dev server's tooling, hot-reload churn. When that
467
+ * happens the flag still says "listening" while every request bypasses
468
+ * capture, and a flag-only guard makes the outage PERMANENT (observed as
469
+ * "No network events" forever with `interceptorInstalled: true`). So this
470
+ * verifies each interceptor is actually reachable (marker check) and
471
+ * re-installs whichever half was clobbered, wrapping the CURRENT global so
472
+ * whatever replaced us keeps working underneath. When both halves are live
473
+ * this costs two property reads.
418
474
  *
419
475
  * @performance Uses method swizzling for minimal runtime overhead
420
476
  * @performance Includes request deduplication through ignored URL patterns
421
477
  */
422
478
  startListening() {
423
- if (this.isListening) {
479
+ const fetchLive = this.isListening && this.isFetchInterceptionLive();
480
+ const xhrLive = this.isListening && this.isXhrInterceptionLive();
481
+ if (fetchLive && xhrLive) {
424
482
  return;
425
483
  }
426
- const self = this;
484
+ if (!fetchLive) {
485
+ this.installFetchInterceptor();
486
+ }
487
+ if (!xhrLive) {
488
+ this.installXhrInterceptor();
489
+ }
490
+ this.isListening = true;
491
+ if (__DEV__) {
492
+ // Network listener has started monitoring fetch and XMLHttpRequest operations
493
+ }
494
+ }
427
495
 
428
- // Swizzle fetch
429
- globalThis.fetch = async (input, init) => {
496
+ /** Swizzle `globalThis.fetch`, wrapping whatever is installed right now. */
497
+ installFetchInterceptor() {
498
+ const self = this;
499
+ // The pass-through is the CURRENT global (not the construction-time one):
500
+ // re-installing after a clobber must call through the replacement, and a
501
+ // stale generation must replay the chain exactly as it stood beneath it.
502
+ const passthroughFetch = globalThis.fetch.bind(globalThis);
503
+ this.originalFetch = passthroughFetch;
504
+ const generation = ++this.fetchGeneration;
505
+ const wrappedFetch = async (input, init) => {
430
506
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
431
507
 
508
+ // A newer install supersedes this wrapper. If it is still stranded in
509
+ // another tool's chain it must pass through silently, or one request
510
+ // would be reported once per stranded generation.
511
+ if (generation !== self.fetchGeneration) {
512
+ return passthroughFetch(input, init);
513
+ }
514
+
432
515
  // Skip ignored URLs
433
516
  if (self.shouldIgnoreUrl(url)) {
434
- return self.originalFetch(input, init);
517
+ return passthroughFetch(input, init);
435
518
  }
436
519
  const startTime = Date.now();
437
520
  const requestId = `fetch_${++self.requestCounter}`;
@@ -514,7 +597,7 @@ class NetworkListener {
514
597
  insideOverriddenFetch = outcome != null;
515
598
  let pending;
516
599
  try {
517
- pending = self.originalFetch(input, init);
600
+ pending = passthroughFetch(input, init);
518
601
  } finally {
519
602
  insideOverriddenFetch = false;
520
603
  }
@@ -617,30 +700,51 @@ class NetworkListener {
617
700
  throw error;
618
701
  }
619
702
  };
703
+ wrappedFetch[BUOY_FETCH_INTERCEPTOR_FLAG] = true;
704
+ this.patchedFetch = wrappedFetch;
705
+ globalThis.fetch = this.patchedFetch;
706
+ }
620
707
 
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);
708
+ /** Swizzle the XHR prototype methods, wrapping whatever is installed now. */
709
+ installXhrInterceptor() {
710
+ const self = this;
711
+ // Same contract as the fetch half: pass through to the CURRENT methods.
712
+ const passthroughOpen = XMLHttpRequest.prototype.open;
713
+ const passthroughSend = XMLHttpRequest.prototype.send;
714
+ const passthroughSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
715
+ this.originalXHROpen = passthroughOpen;
716
+ this.originalXHRSend = passthroughSend;
717
+ this.originalXHRSetRequestHeader = passthroughSetRequestHeader;
718
+ const generation = ++this.xhrGeneration;
719
+ const patchedOpen = function (method, url, async, user, password) {
720
+ // Superseded generations must not re-stamp (a double stamp burns two
721
+ // request ids per request) — just pass through.
722
+ if (generation === self.xhrGeneration) {
723
+ const xhr = this;
724
+ xhr._requestId = `xhr_${++self.requestCounter}`;
725
+ xhr._method = method;
726
+ xhr._url = url;
727
+ xhr._startTime = Date.now();
728
+ xhr._requestHeaders = {};
729
+ }
730
+ return passthroughOpen.call(this, method, url, async ?? true, user, password);
631
731
  };
632
732
 
633
733
  // Track request headers
634
- XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
734
+ const patchedSetRequestHeader = function (header, value) {
635
735
  const xhr = this;
636
- if (xhr._requestHeaders) {
736
+ if (generation === self.xhrGeneration && xhr._requestHeaders) {
637
737
  xhr._requestHeaders[header] = value;
638
738
  }
639
- return self.originalXHRSetRequestHeader.call(this, header, value);
739
+ return passthroughSetRequestHeader.call(this, header, value);
640
740
  };
641
- XMLHttpRequest.prototype.send = function (
741
+ const patchedSend = function (
642
742
  // @ts-ignore - this does exist on native
643
743
  data) {
744
+ // Superseded generation: silent pass-through (see the fetch wrapper).
745
+ if (generation !== self.xhrGeneration) {
746
+ return passthroughSend.call(this, data);
747
+ }
644
748
  const xhr = this;
645
749
  const requestId = xhr._requestId;
646
750
  const method = xhr._method || "GET";
@@ -650,7 +754,7 @@ class NetworkListener {
650
754
 
651
755
  // Skip ignored URLs
652
756
  if (self.shouldIgnoreUrl(url)) {
653
- return self.originalXHRSend.call(this, data);
757
+ return passthroughSend.call(this, data);
654
758
  }
655
759
  const {
656
760
  url: cleanUrl,
@@ -767,18 +871,21 @@ class NetworkListener {
767
871
  // Listeners are attached above FIRST: `runOverride` drives the same
768
872
  // readyState transitions a real response would, and the interceptor has
769
873
  // to see them (readyState 2 is where TTFB is stamped).
770
- const handled = (0, _applyToXhr.runOverride)(this, url, outcome, () => self.originalXHRSend.call(this, data));
874
+ const handled = (0, _applyToXhr.runOverride)(this, url, outcome, () => passthroughSend.call(this, data));
771
875
  // `handled` is false only where this isn't React Native's own
772
876
  // XMLHttpRequest and can't be driven by hand. Fall through and send for
773
877
  // real rather than dropping the request on the floor.
774
878
  if (handled) return;
775
879
  }
776
- return self.originalXHRSend.call(this, data);
880
+ return passthroughSend.call(this, data);
777
881
  };
778
- this.isListening = true;
779
- if (__DEV__) {
780
- // Network listener has started monitoring fetch and XMLHttpRequest operations
781
- }
882
+ patchedSend[BUOY_XHR_INTERCEPTOR_FLAG] = true;
883
+ this.patchedXHROpen = patchedOpen;
884
+ this.patchedXHRSend = patchedSend;
885
+ this.patchedXHRSetRequestHeader = patchedSetRequestHeader;
886
+ XMLHttpRequest.prototype.open = this.patchedXHROpen;
887
+ XMLHttpRequest.prototype.send = this.patchedXHRSend;
888
+ XMLHttpRequest.prototype.setRequestHeader = this.patchedXHRSetRequestHeader;
782
889
  }
783
890
 
784
891
  /**
@@ -792,11 +899,30 @@ class NetworkListener {
792
899
  return;
793
900
  }
794
901
 
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;
902
+ // Silence any of our wrappers stranded inside another tool's chain — they
903
+ // check the generation on every call and now pass through without emitting.
904
+ this.fetchGeneration++;
905
+ this.xhrGeneration++;
906
+
907
+ // Restore ONLY what is still literally ours. If another tool patched after
908
+ // us (impersonate, a debugger), assigning our saved originals would wipe
909
+ // ITS interceptor — the exact injury startListening defends against.
910
+ if (this.patchedFetch && globalThis.fetch === this.patchedFetch) {
911
+ globalThis.fetch = this.originalFetch;
912
+ }
913
+ if (this.patchedXHROpen && XMLHttpRequest.prototype.open === this.patchedXHROpen) {
914
+ XMLHttpRequest.prototype.open = this.originalXHROpen;
915
+ }
916
+ if (this.patchedXHRSend && XMLHttpRequest.prototype.send === this.patchedXHRSend) {
917
+ XMLHttpRequest.prototype.send = this.originalXHRSend;
918
+ }
919
+ if (this.patchedXHRSetRequestHeader && XMLHttpRequest.prototype.setRequestHeader === this.patchedXHRSetRequestHeader) {
920
+ XMLHttpRequest.prototype.setRequestHeader = this.originalXHRSetRequestHeader;
921
+ }
922
+ this.patchedFetch = null;
923
+ this.patchedXHROpen = null;
924
+ this.patchedXHRSend = null;
925
+ this.patchedXHRSetRequestHeader = null;
800
926
  this.isListening = false;
801
927
  if (__DEV__) {
802
928
  // Network listener has stopped monitoring and restored original methods
@@ -921,4 +1047,12 @@ const isNetworkListening = () => getNetworkListener().isActive;
921
1047
  */
922
1048
  exports.isNetworkListening = isNetworkListening;
923
1049
  const getNetworkListenerCount = () => getNetworkListener().listenerCount;
924
- exports.getNetworkListenerCount = getNetworkListenerCount;
1050
+
1051
+ /**
1052
+ * Whether both interceptors are actually reachable through the live global
1053
+ * chain — the honest version of `isNetworkListening` (whose flag stays true
1054
+ * even after another tool re-assigns `globalThis.fetch` over the wrapper).
1055
+ */
1056
+ exports.getNetworkListenerCount = getNetworkListenerCount;
1057
+ const isNetworkInterceptionLive = () => getNetworkListener().isInterceptionLive();
1058
+ 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
  });