@buoy-gg/network 7.0.12 → 7.0.14

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 (136) hide show
  1. package/lib/commonjs/index.js +52 -0
  2. package/lib/commonjs/network/components/NetworkDetailStepper.js +26 -14
  3. package/lib/commonjs/network/components/NetworkEventDetailView.js +86 -14
  4. package/lib/commonjs/network/components/NetworkEventItemCompact.js +4 -0
  5. package/lib/commonjs/network/components/NetworkHeaderLive.js +32 -7
  6. package/lib/commonjs/network/components/NetworkHeaderMenu.js +183 -0
  7. package/lib/commonjs/network/components/NetworkListScreen.js +134 -8
  8. package/lib/commonjs/network/components/NetworkModal.js +198 -51
  9. package/lib/commonjs/network/components/NetworkNumberPrompt.js +178 -0
  10. package/lib/commonjs/network/components/NetworkOverrideEditor.js +797 -0
  11. package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +102 -0
  12. package/lib/commonjs/network/components/NetworkOverridesButton.js +88 -0
  13. package/lib/commonjs/network/components/NetworkOverridesScreen.js +489 -0
  14. package/lib/commonjs/network/components/response-editor/Explorer.js +973 -0
  15. package/lib/commonjs/network/components/response-editor/deleteNestedDataByPath.js +56 -0
  16. package/lib/commonjs/network/components/response-editor/editorContext.js +33 -0
  17. package/lib/commonjs/network/components/response-editor/updateNestedDataByPath.js +61 -0
  18. package/lib/commonjs/network/hooks/useNetworkSelection.js +53 -0
  19. package/lib/commonjs/network/hooks/useOverrideRules.js +36 -0
  20. package/lib/commonjs/network/overrides/applyToFetch.js +80 -0
  21. package/lib/commonjs/network/overrides/applyToXhr.js +190 -0
  22. package/lib/commonjs/network/overrides/engine.js +70 -0
  23. package/lib/commonjs/network/overrides/matchRule.js +81 -0
  24. package/lib/commonjs/network/overrides/overrideRulesStore.js +550 -0
  25. package/lib/commonjs/network/overrides/presets.js +70 -0
  26. package/lib/commonjs/network/overrides/resolveOverride.js +158 -0
  27. package/lib/commonjs/network/overrides/types.js +85 -0
  28. package/lib/commonjs/network/sync/networkSyncAdapter.js +230 -1
  29. package/lib/commonjs/network/types/index.js +5 -1
  30. package/lib/commonjs/network/utils/networkEventStore.js +52 -6
  31. package/lib/commonjs/network/utils/networkListener.js +90 -10
  32. package/lib/commonjs/network/utils/networkSelectionStore.js +193 -0
  33. package/lib/commonjs/preset.js +11 -0
  34. package/lib/module/index.js +19 -0
  35. package/lib/module/network/components/NetworkDetailStepper.js +27 -15
  36. package/lib/module/network/components/NetworkEventDetailView.js +87 -15
  37. package/lib/module/network/components/NetworkEventItemCompact.js +5 -1
  38. package/lib/module/network/components/NetworkHeaderLive.js +32 -7
  39. package/lib/module/network/components/NetworkHeaderMenu.js +177 -0
  40. package/lib/module/network/components/NetworkListScreen.js +135 -9
  41. package/lib/module/network/components/NetworkModal.js +200 -53
  42. package/lib/module/network/components/NetworkNumberPrompt.js +174 -0
  43. package/lib/module/network/components/NetworkOverrideEditor.js +794 -0
  44. package/lib/module/network/components/NetworkOverrideHeaderButton.js +99 -0
  45. package/lib/module/network/components/NetworkOverridesButton.js +85 -0
  46. package/lib/module/network/components/NetworkOverridesScreen.js +485 -0
  47. package/lib/module/network/components/response-editor/Explorer.js +972 -0
  48. package/lib/module/network/components/response-editor/deleteNestedDataByPath.js +51 -0
  49. package/lib/module/network/components/response-editor/editorContext.js +29 -0
  50. package/lib/module/network/components/response-editor/updateNestedDataByPath.js +56 -0
  51. package/lib/module/network/hooks/useNetworkSelection.js +46 -0
  52. package/lib/module/network/hooks/useOverrideRules.js +32 -0
  53. package/lib/module/network/overrides/applyToFetch.js +72 -0
  54. package/lib/module/network/overrides/applyToXhr.js +185 -0
  55. package/lib/module/network/overrides/engine.js +64 -0
  56. package/lib/module/network/overrides/matchRule.js +75 -0
  57. package/lib/module/network/overrides/overrideRulesStore.js +547 -0
  58. package/lib/module/network/overrides/presets.js +65 -0
  59. package/lib/module/network/overrides/resolveOverride.js +152 -0
  60. package/lib/module/network/overrides/types.js +80 -0
  61. package/lib/module/network/sync/networkSyncAdapter.js +230 -1
  62. package/lib/module/network/types/index.js +3 -1
  63. package/lib/module/network/utils/networkEventStore.js +52 -6
  64. package/lib/module/network/utils/networkListener.js +91 -10
  65. package/lib/module/network/utils/networkSelectionStore.js +188 -0
  66. package/lib/module/preset.js +13 -1
  67. package/lib/typescript/index.d.ts +9 -0
  68. package/lib/typescript/index.d.ts.map +1 -1
  69. package/lib/typescript/network/components/NetworkDetailStepper.d.ts +8 -5
  70. package/lib/typescript/network/components/NetworkDetailStepper.d.ts.map +1 -1
  71. package/lib/typescript/network/components/NetworkEventDetailView.d.ts +36 -1
  72. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  73. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +6 -0
  74. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  75. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +13 -3
  76. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  77. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +46 -0
  78. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -0
  79. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  80. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  81. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts +33 -0
  82. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts.map +1 -0
  83. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts +17 -0
  84. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts.map +1 -0
  85. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts +27 -0
  86. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts.map +1 -0
  87. package/lib/typescript/network/components/NetworkOverridesButton.d.ts +17 -0
  88. package/lib/typescript/network/components/NetworkOverridesButton.d.ts.map +1 -0
  89. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts +29 -0
  90. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -0
  91. package/lib/typescript/network/components/response-editor/Explorer.d.ts +17 -0
  92. package/lib/typescript/network/components/response-editor/Explorer.d.ts.map +1 -0
  93. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts +9 -0
  94. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts.map +1 -0
  95. package/lib/typescript/network/components/response-editor/editorContext.d.ts +29 -0
  96. package/lib/typescript/network/components/response-editor/editorContext.d.ts.map +1 -0
  97. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts +10 -0
  98. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts.map +1 -0
  99. package/lib/typescript/network/hooks/useNetworkSelection.d.ts +26 -0
  100. package/lib/typescript/network/hooks/useNetworkSelection.d.ts.map +1 -0
  101. package/lib/typescript/network/hooks/useOverrideRules.d.ts +29 -0
  102. package/lib/typescript/network/hooks/useOverrideRules.d.ts.map +1 -0
  103. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts +16 -0
  104. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts.map +1 -0
  105. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts +19 -0
  106. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +1 -0
  107. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts +22 -0
  108. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts.map +1 -0
  109. package/lib/typescript/network/overrides/applyToFetch.d.ts +50 -0
  110. package/lib/typescript/network/overrides/applyToFetch.d.ts.map +1 -0
  111. package/lib/typescript/network/overrides/applyToXhr.d.ts +43 -0
  112. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -0
  113. package/lib/typescript/network/overrides/engine.d.ts +47 -0
  114. package/lib/typescript/network/overrides/engine.d.ts.map +1 -0
  115. package/lib/typescript/network/overrides/matchRule.d.ts +22 -0
  116. package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -0
  117. package/lib/typescript/network/overrides/overrideRulesStore.d.ts +205 -0
  118. package/lib/typescript/network/overrides/overrideRulesStore.d.ts.map +1 -0
  119. package/lib/typescript/network/overrides/presets.d.ts +38 -0
  120. package/lib/typescript/network/overrides/presets.d.ts.map +1 -0
  121. package/lib/typescript/network/overrides/resolveOverride.d.ts +46 -0
  122. package/lib/typescript/network/overrides/resolveOverride.d.ts.map +1 -0
  123. package/lib/typescript/network/overrides/types.d.ts +169 -0
  124. package/lib/typescript/network/overrides/types.d.ts.map +1 -0
  125. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +73 -0
  126. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  127. package/lib/typescript/network/types/index.d.ts +10 -0
  128. package/lib/typescript/network/types/index.d.ts.map +1 -1
  129. package/lib/typescript/network/utils/networkEventStore.d.ts +14 -0
  130. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  131. package/lib/typescript/network/utils/networkListener.d.ts +3 -0
  132. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  133. package/lib/typescript/network/utils/networkSelectionStore.d.ts +117 -0
  134. package/lib/typescript/network/utils/networkSelectionStore.d.ts.map +1 -0
  135. package/lib/typescript/preset.d.ts.map +1 -1
  136. package/package.json +2 -2
@@ -4,11 +4,29 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.getNetworkListenerCount = exports.addNetworkListener = void 0;
7
+ var _engine = require("../overrides/engine");
8
+ var _applyToXhr = require("../overrides/applyToXhr");
9
+ var _applyToFetch = require("../overrides/applyToFetch");
10
+ var _types = require("../overrides/types");
7
11
  /**
8
12
  * Network listener using Reactotron-style event pattern
9
13
  * Simple and reliable network interception for React Native
10
14
  */
11
15
 
16
+ /**
17
+ * True only while `originalFetch` is being CALLED (not awaited).
18
+ *
19
+ * Overrides hook both `fetch` and `XMLHttpRequest`, because Expo's native
20
+ * `expo/fetch` bypasses XHR while bare RN's whatwg-fetch polyfill runs straight
21
+ * through it. On bare RN that means one `fetch()` would otherwise be overridden
22
+ * twice — a doubled delay and a doubled hit count.
23
+ *
24
+ * whatwg-fetch creates and sends its XHR synchronously inside the Promise
25
+ * executor, so a flag raised across the call expression alone is enough to
26
+ * cover the inner `send()` without leaking across the await.
27
+ */
28
+ let insideOverriddenFetch = false;
29
+
12
30
  // Extended XMLHttpRequest interface for monkey-patching
13
31
 
14
32
  // Event types for network operations
@@ -203,7 +221,7 @@ class NetworkListener {
203
221
  * Handle XMLHttpRequest response processing
204
222
  * This method processes the response and emits appropriate events
205
223
  */
206
- handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "xhr") {
224
+ handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "xhr", override) {
207
225
  const duration = startTime ? Date.now() - startTime : 0;
208
226
 
209
227
  // XHR's duration was ALREADY total (measured at readyState 4 / load), so
@@ -232,7 +250,8 @@ class NetworkListener {
232
250
  },
233
251
  error: {
234
252
  message: isError ? "Request failed" : "Network error or request aborted"
235
- }
253
+ },
254
+ override
236
255
  });
237
256
  return;
238
257
  }
@@ -371,7 +390,8 @@ class NetworkListener {
371
390
  headers: responseHeaders,
372
391
  body,
373
392
  size: responseSize
374
- }
393
+ },
394
+ override
375
395
  });
376
396
  } else {
377
397
  this.emit({
@@ -396,7 +416,8 @@ class NetworkListener {
396
416
  },
397
417
  error: {
398
418
  message: `HTTP ${xhr.status}: ${xhr.statusText}`
399
- }
419
+ },
420
+ override
400
421
  });
401
422
  }
402
423
  }
@@ -467,6 +488,18 @@ class NetworkListener {
467
488
  // Determine client type from X-Request-Client header or default to fetch
468
489
  const clientType = requestHeaders["X-Request-Client"] || requestHeaders["x-request-client"] || "fetch";
469
490
 
491
+ /**
492
+ * Response overrides, fetch side.
493
+ *
494
+ * Needed even though `send` below also resolves them: Expo installs a
495
+ * NATIVE `expo/fetch` as `globalThis.fetch`, which never constructs an
496
+ * XMLHttpRequest, so the XHR hook alone would never see these requests.
497
+ * Skipped when `canSynthesizeResponse()` is false so a runtime without a
498
+ * `Response` global degrades to a real request rather than a broken one.
499
+ */
500
+ const outcome = (0, _applyToFetch.canSynthesizeResponse)() ? (0, _engine.overrideForRequest)(url, method) : null;
501
+ const override = outcome ? (0, _types.markForOutcome)(outcome) : undefined;
502
+
470
503
  // Emit request event
471
504
  self.emit({
472
505
  type: "request",
@@ -479,10 +512,28 @@ class NetworkListener {
479
512
  data: requestData,
480
513
  params: params || undefined,
481
514
  client: clientType
482
- }
515
+ },
516
+ override
483
517
  });
484
518
  try {
485
- const response = await self.originalFetch(input, init);
519
+ if (outcome) await (0, _applyToFetch.sleep)(outcome.delayMs);
520
+ if (outcome?.kind === "fail") throw (0, _applyToFetch.synthesizeFetchError)(outcome);
521
+ let response;
522
+ if (outcome?.kind === "respond") {
523
+ response = (0, _applyToFetch.synthesizeResponse)(url, outcome);
524
+ } else {
525
+ // The flag spans the CALL only — see `insideOverriddenFetch`. It has
526
+ // to be cleared before the await, or it would still be raised for
527
+ // every unrelated request that starts while this one is in flight.
528
+ insideOverriddenFetch = outcome != null;
529
+ let pending;
530
+ try {
531
+ pending = self.originalFetch(input, init);
532
+ } finally {
533
+ insideOverriddenFetch = false;
534
+ }
535
+ response = await pending;
536
+ }
486
537
  // A fetch promise resolves when the HEADERS arrive, not the body — so
487
538
  // this is time-to-first-byte, and it is where `duration` used to stop.
488
539
  // That silently excluded download time, making an identical request
@@ -551,7 +602,8 @@ class NetworkListener {
551
602
  headers: responseHeaders,
552
603
  body,
553
604
  size: responseSize
554
- }
605
+ },
606
+ override
555
607
  });
556
608
  return response;
557
609
  } catch (error) {
@@ -574,7 +626,8 @@ class NetworkListener {
574
626
  error: {
575
627
  message: error instanceof Error ? error.message : "Network error",
576
628
  stack: error instanceof Error ? error.stack : undefined
577
- }
629
+ },
630
+ override
578
631
  });
579
632
  throw error;
580
633
  }
@@ -641,6 +694,22 @@ class NetworkListener {
641
694
  const looksLikeAxios = acceptHeader.includes("application/json") && acceptHeader.includes("text/plain") && acceptHeader.includes("*/*");
642
695
  const clientType = explicitClient || (looksLikeAxios ? "axios" : "xhr");
643
696
 
697
+ /**
698
+ * Response overrides.
699
+ *
700
+ * This is the layer that covers `axios` and raw XMLHttpRequest. It also
701
+ * covers `fetch` on BARE React Native, where the whatwg-fetch polyfill
702
+ * runs through XHR — which is why the fetch wrapper raises
703
+ * `insideOverriddenFetch` around its pass-through, so one request can't
704
+ * be overridden twice.
705
+ *
706
+ * Matching uses the raw `url` (query string included), which is what the
707
+ * developer sees in the list and writes into a rule. No-ops in production
708
+ * builds; see `overrideForRequest`.
709
+ */
710
+ const outcome = insideOverriddenFetch ? null : (0, _engine.overrideForRequest)(url, method);
711
+ const override = outcome ? (0, _types.markForOutcome)(outcome) : undefined;
712
+
644
713
  // Emit request event
645
714
  self.emit({
646
715
  type: "request",
@@ -653,7 +722,8 @@ class NetworkListener {
653
722
  data: requestData,
654
723
  params: params || undefined,
655
724
  client: clientType
656
- }
725
+ },
726
+ override
657
727
  });
658
728
 
659
729
  // Track if we've already processed this request to avoid duplicate events
@@ -661,7 +731,7 @@ class NetworkListener {
661
731
  const processResponse = (isError = false) => {
662
732
  if (processed) return;
663
733
  processed = true;
664
- self.handleXHRResponse(this, requestId || "unknown", cleanUrl, method, requestHeaders, requestData, params, startTime || 0, isError, clientType);
734
+ self.handleXHRResponse(this, requestId || "unknown", cleanUrl, method, requestHeaders, requestData, params, startTime || 0, isError, clientType, override);
665
735
  // Clean up event listeners after processing to prevent memory leaks
666
736
  cleanup();
667
737
  };
@@ -708,6 +778,16 @@ class NetworkListener {
708
778
  this.addEventListener("error", errorListener);
709
779
  this.addEventListener("abort", abortListener);
710
780
  this.addEventListener("readystatechange", readyStateListener);
781
+ if (outcome) {
782
+ // Listeners are attached above FIRST: `runOverride` drives the same
783
+ // readyState transitions a real response would, and the interceptor has
784
+ // to see them (readyState 2 is where TTFB is stamped).
785
+ const handled = (0, _applyToXhr.runOverride)(this, url, outcome, () => self.originalXHRSend.call(this, data));
786
+ // `handled` is false only where this isn't React Native's own
787
+ // XMLHttpRequest and can't be driven by hand. Fall through and send for
788
+ // real rather than dropping the request on the floor.
789
+ if (handled) return;
790
+ }
711
791
  return self.originalXHRSend.call(this, data);
712
792
  };
713
793
  this.isListening = true;
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.networkSelectionStore = exports.DEFAULT_SETTLE_MS = void 0;
7
+ exports.toSelectionIds = toSelectionIds;
8
+ /**
9
+ * Which request the detail view is showing, and where it sits in the list —
10
+ * the prev/next CURSOR.
11
+ *
12
+ * ## Why this is a store and not `useState` in the modal
13
+ *
14
+ * It used to be `useState` in `NetworkModalInner`. Every tap on Previous/Next
15
+ * therefore dirtied the modal ROOT, which re-rendered `JsModal` (1833 lines,
16
+ * nine useStates, animations/gestures/persistence — and it can never bail out,
17
+ * because its `header` prop is a fresh object literal every render), the whole
18
+ * header, and all five screens. None of them care which request is selected;
19
+ * they only care THAT one is.
20
+ *
21
+ * This is the same argument `NetworkModal`'s own header comment already makes
22
+ * about network events ("with the subscription here, 15 events re-rendered
23
+ * JsModal + every header button 15×"). Stepping is that same class of
24
+ * high-frequency churn, and it was sitting in the same wrong place. Now the
25
+ * root subscribes to `hasSelection` — a boolean that flips twice a session —
26
+ * while the footer, the body and the header's pin actions subscribe to the
27
+ * parts they actually render.
28
+ *
29
+ * ## The three cursors
30
+ *
31
+ * `selectedId` is where the USER is. It moves on the tap, synchronously.
32
+ *
33
+ * `bodyId` is what the detail body is allowed to render, and it deliberately
34
+ * LAGS: drawing a request means handing its payload to the virtualized data
35
+ * viewer to flatten, and that has no business blocking the next tap. It
36
+ * catches up only once the selection has held still for `settleMs`, so a
37
+ * burst of taps renders the request you land on and none of the ones you flew
38
+ * past. (`useDeferredValue` was tried here first and is the wrong tool — it
39
+ * lowers the priority of that work without preventing it, so every settled
40
+ * step still queued a flatten and each one delayed the touch behind it.)
41
+ *
42
+ * `renderedId` is what actually made it to the screen, reported by the detail
43
+ * screen after commit. Nothing in the product reads it; it exists so the
44
+ * benchmark harness can measure "time until request N was really painted"
45
+ * separately from "time until the cursor reached N".
46
+ */
47
+
48
+ /**
49
+ * How long the selection must hold still before the body renders it. Short
50
+ * enough that a single step reads as immediate (the footer and header have
51
+ * already moved), long enough that a burst collapses to one render.
52
+ */
53
+ const DEFAULT_SETTLE_MS = exports.DEFAULT_SETTLE_MS = 120;
54
+ class NetworkSelectionStore {
55
+ ids = [];
56
+ index = -1;
57
+ selectedId = null;
58
+ bodyId = null;
59
+ renderedId = null;
60
+ settleMs = DEFAULT_SETTLE_MS;
61
+ settleTimer = null;
62
+ subscribers = new Set();
63
+ snapshot = this.buildSnapshot();
64
+ buildSnapshot() {
65
+ return {
66
+ ids: this.ids,
67
+ index: this.index,
68
+ selectedId: this.selectedId,
69
+ bodyId: this.bodyId,
70
+ renderedId: this.renderedId
71
+ };
72
+ }
73
+
74
+ /**
75
+ * One immutable snapshot per mutation, so subscribers can identity-compare
76
+ * and `useSyncExternalStore` stays happy.
77
+ */
78
+ emit() {
79
+ this.snapshot = this.buildSnapshot();
80
+ for (const subscriber of this.subscribers) subscriber(this.snapshot);
81
+ }
82
+ getState() {
83
+ return this.snapshot;
84
+ }
85
+ subscribe(subscriber) {
86
+ this.subscribers.add(subscriber);
87
+ return () => {
88
+ this.subscribers.delete(subscriber);
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Publish the navigable order. Called by the stepper whenever its derived
94
+ * list changes (filter, search, pins, new requests arriving).
95
+ *
96
+ * The cursor is re-anchored BY ID, not by position: the live list is
97
+ * newest-first and new requests are prepended, so holding an index would
98
+ * silently slide the selection onto a different request every time one
99
+ * landed.
100
+ */
101
+ setIds(ids) {
102
+ const sameIds = ids.length === this.ids.length && ids.every((id, i) => id === this.ids[i]);
103
+ if (sameIds) return;
104
+ this.ids = ids;
105
+ this.index = this.selectedId == null ? -1 : ids.indexOf(this.selectedId);
106
+ this.emit();
107
+ }
108
+ select(id) {
109
+ if (id === this.selectedId) return;
110
+ this.selectedId = id;
111
+ this.index = id == null ? -1 : this.ids.indexOf(id);
112
+ this.scheduleBody(id);
113
+ this.emit();
114
+ }
115
+ clear() {
116
+ this.select(null);
117
+ }
118
+
119
+ /**
120
+ * Move the cursor. O(1) — the whole reason `index` is stored rather than
121
+ * re-derived with a `findIndex` on every render.
122
+ *
123
+ * Returns the id it landed on, or null if the move was out of bounds (the
124
+ * footer disables its arrows at the ends, but the benchmark driver calls
125
+ * this directly and needs to know when it has run off the end).
126
+ */
127
+ step(delta) {
128
+ if (this.index < 0) return null;
129
+ const next = this.index + delta;
130
+ if (next < 0 || next >= this.ids.length) return null;
131
+ const id = this.ids[next];
132
+ this.selectedId = id;
133
+ this.index = next;
134
+ this.scheduleBody(id);
135
+ this.emit();
136
+ return id;
137
+ }
138
+
139
+ /**
140
+ * Opening and closing skip the settle delay: there is no in-flight render to
141
+ * protect on a null↔id transition, and waiting there just reads as a hang.
142
+ * Only id → id steps debounce.
143
+ */
144
+ scheduleBody(id) {
145
+ if (this.settleTimer != null) {
146
+ clearTimeout(this.settleTimer);
147
+ this.settleTimer = null;
148
+ }
149
+ if (id == null || this.bodyId == null || this.settleMs <= 0) {
150
+ this.bodyId = id;
151
+ return;
152
+ }
153
+ this.settleTimer = setTimeout(() => {
154
+ this.settleTimer = null;
155
+ if (this.bodyId === this.selectedId) return;
156
+ this.bodyId = this.selectedId;
157
+ this.emit();
158
+ }, this.settleMs);
159
+ }
160
+
161
+ /** Benchmark toggle — `?settle=0` measures the gate's contribution. */
162
+ setSettleMs(ms) {
163
+ this.settleMs = Math.max(0, ms);
164
+ }
165
+ getSettleMs() {
166
+ return this.settleMs;
167
+ }
168
+
169
+ /** Reported by the detail screen after commit. Instrumentation only. */
170
+ reportRendered(id) {
171
+ if (id === this.renderedId) return;
172
+ this.renderedId = id;
173
+ this.emit();
174
+ }
175
+
176
+ /** Test/bench helper — drop all cursor state without touching the events. */
177
+ reset() {
178
+ if (this.settleTimer != null) clearTimeout(this.settleTimer);
179
+ this.settleTimer = null;
180
+ this.ids = [];
181
+ this.index = -1;
182
+ this.selectedId = null;
183
+ this.bodyId = null;
184
+ this.renderedId = null;
185
+ this.emit();
186
+ }
187
+ }
188
+ const networkSelectionStore = exports.networkSelectionStore = new NetworkSelectionStore();
189
+
190
+ /** Convenience for publishing an event list as the navigable order. */
191
+ function toSelectionIds(events) {
192
+ return events.map(event => event.id);
193
+ }
@@ -7,6 +7,7 @@ exports.createNetworkTool = createNetworkTool;
7
7
  exports.networkToolPreset = void 0;
8
8
  var _sharedUi = require("@buoy-gg/shared-ui");
9
9
  var _NetworkModal = require("./network/components/NetworkModal");
10
+ var _overrideRulesStore = require("./network/overrides/overrideRulesStore");
10
11
  var _jsxRuntime = require("react/jsx-runtime");
11
12
  /**
12
13
  * Pre-configured network monitoring tool for FloatingDevTools
@@ -25,6 +26,16 @@ var _jsxRuntime = require("react/jsx-runtime");
25
26
  * ```
26
27
  */
27
28
 
29
+ /**
30
+ * Load persisted response-override rules as soon as the tool is registered.
31
+ *
32
+ * Deliberately at import time rather than when the Network modal mounts: an
33
+ * override has to fire from app boot, with the tool closed and nothing
34
+ * capturing — that is the whole point of persisting rules ("force this endpoint
35
+ * to 500, then restart and watch the boot path"). Hydrating also re-installs
36
+ * the interceptor if any rule is still active. One storage read, dev-only.
37
+ */void _overrideRulesStore.overrideRulesStore.ensureLoaded();
38
+
28
39
  /**
29
40
  * Pre-configured network monitoring tool for FloatingDevTools.
30
41
  * Includes:
@@ -41,6 +41,10 @@ export { useNetworkBadgeCounts } from "./network/hooks/useNetworkBadgeCounts";
41
41
  // implementations can't disagree about where a pin goes.
42
42
  export { useNetworkSaved } from "./network/hooks/useNetworkSaved";
43
43
  export { splitPinned } from "./network/utils/withPinnedFirst";
44
+ // The prev/next CURSOR. Exported because the tool-bench harness drives
45
+ // `step(+1)` directly to time stepping without going through the UI, and
46
+ // because `setSettleMs` is the toggle its benchmark cases flip.
47
+ export { networkSelectionStore, DEFAULT_SETTLE_MS } from "./network/utils/networkSelectionStore";
44
48
  export { TickProvider, useTickEveryMinute } from "./network/hooks/useTickEveryMinute";
45
49
 
46
50
  // =============================================================================
@@ -52,6 +56,21 @@ export { formatBytes, formatDuration, formatHttpStatus } from "./network/utils/f
52
56
  // TYPES (For TypeScript users)
53
57
  // =============================================================================
54
58
 
59
+ // =============================================================================
60
+ // RESPONSE OVERRIDES
61
+ // Force a status, body, failure or delay on matching requests — Buoy's answer
62
+ // to Chrome DevTools' Local Overrides, plus the status/latency/failure control
63
+ // Chrome doesn't have.
64
+ // =============================================================================
65
+ export { overrideRulesStore
66
+
67
+ /**
68
+ * @internal — the desktop dashboard puts the rule store in remote-mirror mode
69
+ * so edits made there are forwarded to the device instead of stored locally.
70
+ */ } from "./network/overrides/overrideRulesStore";
71
+ export { draftFromEvent } from "./network/overrides/presets";
72
+ export { MAX_RULES as MAX_OVERRIDE_RULES, FREE_TIER_RULE_LIMIT as FREE_TIER_OVERRIDE_LIMIT, STATUS_MIN as OVERRIDE_STATUS_MIN, STATUS_MAX as OVERRIDE_STATUS_MAX } from "./network/overrides/types";
73
+
55
74
  // =============================================================================
56
75
  // SHARED IGNORED-PATTERN FILTERING
57
76
  // Re-exported from @buoy-gg/shared-ui so consumers wiring the Network detail
@@ -7,8 +7,8 @@
7
7
  * It is its OWN leaf (rule3) because it is the only part of the detail screen
8
8
  * that needs the event LIST. Subscribing here keeps the request stream out of
9
9
  * the detail body's render path, which is the expensive subtree (headers,
10
- * bodies, JSON viewers, diff views): a request arriving re-renders this
11
- * ~30-node footer and nothing else.
10
+ * bodies, JSON viewers): a request arriving re-renders this ~30-node footer
11
+ * and nothing else.
12
12
  *
13
13
  * The order it steps through is EXACTLY what the list screen renders —
14
14
  * `[...pinned, ...rest]` off the same `useNetworkEventList` + `splitPinned`
@@ -18,24 +18,33 @@
18
18
  * "request N of <what the list is showing>", never "N of everything ever
19
19
  * captured". Detail opened from the Saved screen steps through the SAVED
20
20
  * list instead (with its own search applied), for the same reason.
21
+ *
22
+ * This component OWNS the navigable order but not the cursor: it publishes
23
+ * the ordered ids into `networkSelectionStore` and reads the cursor back.
24
+ * That split is what makes stepping O(1) (the store keeps the index, so
25
+ * there is no `findIndex` per render) and what lets the benchmark harness
26
+ * drive `step(+1)` directly without going through the UI.
21
27
  */
22
28
 
23
- import { memo, useMemo } from "react";
29
+ import { memo, useEffect, useMemo } from "react";
24
30
  import { EventStepperFooter } from "@buoy-gg/shared-ui";
25
31
  import { useNetworkEventList } from "../hooks/useNetworkEventList";
26
32
  import { useNetworkSaved } from "../hooks/useNetworkSaved";
33
+ import { useNetworkCursor } from "../hooks/useNetworkSelection";
34
+ import { networkSelectionStore, toSelectionIds } from "../utils/networkSelectionStore";
27
35
  import { splitPinned } from "../utils/withPinnedFirst";
28
36
  import { selectSavedEvents } from "../utils/selectSavedEvents";
29
37
  import { jsx as _jsx } from "react/jsx-runtime";
38
+ /** Stable identities — the store is a singleton, so these never need to change. */
39
+ const stepPrevious = () => networkSelectionStore.step(-1);
40
+ const stepNext = () => networkSelectionStore.step(1);
30
41
  export const NetworkDetailStepper = /*#__PURE__*/memo(function NetworkDetailStepper({
31
- eventId,
32
42
  paused,
33
43
  filter,
34
44
  ignoredPatterns,
35
45
  isPro,
36
46
  fromSaved,
37
47
  savedSearch,
38
- onSelect,
39
48
  applySafeAreaInset
40
49
  }) {
41
50
  const {
@@ -63,7 +72,16 @@ export const NetworkDetailStepper = /*#__PURE__*/memo(function NetworkDetailStep
63
72
  } = splitPinned(visibleEvents, pinnedEvents, pinnedLiveIds);
64
73
  return pinned.length === 0 ? rest : [...pinned, ...rest];
65
74
  }, [fromSaved, savedRecords, savedSearch, isPro, visibleEvents, pinnedEvents, pinnedLiveIds]);
66
- const index = ordered.findIndex(event => event.id === eventId);
75
+
76
+ // Publish the navigable order. `setIds` re-anchors the cursor BY ID and
77
+ // no-ops when the ids are unchanged, so this is safe to run every render.
78
+ useEffect(() => {
79
+ networkSelectionStore.setIds(toSelectionIds(ordered));
80
+ }, [ordered]);
81
+ const {
82
+ index,
83
+ total
84
+ } = useNetworkCursor();
67
85
 
68
86
  // Not in the list at all: the filter changed under it while it was open, it
69
87
  // aged past the 500-request cap, or it is a saved snapshot with no live twin.
@@ -75,16 +93,10 @@ export const NetworkDetailStepper = /*#__PURE__*/memo(function NetworkDetailStep
75
93
  // the same direction as scrolling up, which is what the arrow means here.
76
94
  return /*#__PURE__*/_jsx(EventStepperFooter, {
77
95
  currentIndex: index,
78
- totalItems: ordered.length,
96
+ totalItems: total,
79
97
  itemLabel: "Request",
80
98
  applySafeAreaInset: applySafeAreaInset,
81
- onPrevious: () => {
82
- const target = ordered[index - 1];
83
- if (target) onSelect(target.id);
84
- },
85
- onNext: () => {
86
- const target = ordered[index + 1];
87
- if (target) onSelect(target.id);
88
- }
99
+ onPrevious: stepPrevious,
100
+ onNext: stepNext
89
101
  });
90
102
  });