@buoy-gg/network 7.0.12 → 7.0.13
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.
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/network/components/NetworkDetailStepper.js +26 -14
- package/lib/commonjs/network/components/NetworkEventDetailView.js +56 -11
- package/lib/commonjs/network/components/NetworkHeaderLive.js +6 -1
- package/lib/commonjs/network/components/NetworkListScreen.js +9 -4
- package/lib/commonjs/network/components/NetworkModal.js +64 -24
- package/lib/commonjs/network/hooks/useNetworkSelection.js +53 -0
- package/lib/commonjs/network/utils/networkSelectionStore.js +193 -0
- package/lib/module/index.js +4 -0
- package/lib/module/network/components/NetworkDetailStepper.js +27 -15
- package/lib/module/network/components/NetworkEventDetailView.js +56 -11
- package/lib/module/network/components/NetworkHeaderLive.js +6 -1
- package/lib/module/network/components/NetworkListScreen.js +9 -4
- package/lib/module/network/components/NetworkModal.js +64 -24
- package/lib/module/network/hooks/useNetworkSelection.js +46 -0
- package/lib/module/network/utils/networkSelectionStore.js +188 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkDetailStepper.d.ts +8 -5
- package/lib/typescript/network/components/NetworkDetailStepper.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts +36 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts +5 -2
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/hooks/useNetworkSelection.d.ts +26 -0
- package/lib/typescript/network/hooks/useNetworkSelection.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkSelectionStore.d.ts +117 -0
- package/lib/typescript/network/utils/networkSelectionStore.d.ts.map +1 -0
- package/package.json +2 -2
package/lib/commonjs/index.js
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
Object.defineProperty(exports, "DEFAULT_SETTLE_MS", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _networkSelectionStore.DEFAULT_SETTLE_MS;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
6
12
|
Object.defineProperty(exports, "FREE_TIER_SAVED_LIMIT", {
|
|
7
13
|
enumerable: true,
|
|
8
14
|
get: function () {
|
|
@@ -147,6 +153,12 @@ Object.defineProperty(exports, "networkSavedStore", {
|
|
|
147
153
|
return _networkSavedStore.networkSavedStore;
|
|
148
154
|
}
|
|
149
155
|
});
|
|
156
|
+
Object.defineProperty(exports, "networkSelectionStore", {
|
|
157
|
+
enumerable: true,
|
|
158
|
+
get: function () {
|
|
159
|
+
return _networkSelectionStore.networkSelectionStore;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
150
162
|
Object.defineProperty(exports, "networkSyncAdapter", {
|
|
151
163
|
enumerable: true,
|
|
152
164
|
get: function () {
|
|
@@ -231,6 +243,7 @@ var _useLiveNetworkEvent = require("./network/hooks/useLiveNetworkEvent");
|
|
|
231
243
|
var _useNetworkBadgeCounts = require("./network/hooks/useNetworkBadgeCounts");
|
|
232
244
|
var _useNetworkSaved = require("./network/hooks/useNetworkSaved");
|
|
233
245
|
var _withPinnedFirst = require("./network/utils/withPinnedFirst");
|
|
246
|
+
var _networkSelectionStore = require("./network/utils/networkSelectionStore");
|
|
234
247
|
var _useTickEveryMinute = require("./network/hooks/useTickEveryMinute");
|
|
235
248
|
var _formatting = require("./network/utils/formatting");
|
|
236
249
|
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
@@ -8,6 +8,8 @@ var _react = require("react");
|
|
|
8
8
|
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
9
9
|
var _useNetworkEventList = require("../hooks/useNetworkEventList");
|
|
10
10
|
var _useNetworkSaved = require("../hooks/useNetworkSaved");
|
|
11
|
+
var _useNetworkSelection = require("../hooks/useNetworkSelection");
|
|
12
|
+
var _networkSelectionStore = require("../utils/networkSelectionStore");
|
|
11
13
|
var _withPinnedFirst = require("../utils/withPinnedFirst");
|
|
12
14
|
var _selectSavedEvents = require("../utils/selectSavedEvents");
|
|
13
15
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
@@ -18,8 +20,8 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
18
20
|
* It is its OWN leaf (rule3) because it is the only part of the detail screen
|
|
19
21
|
* that needs the event LIST. Subscribing here keeps the request stream out of
|
|
20
22
|
* the detail body's render path, which is the expensive subtree (headers,
|
|
21
|
-
* bodies, JSON viewers
|
|
22
|
-
*
|
|
23
|
+
* bodies, JSON viewers): a request arriving re-renders this ~30-node footer
|
|
24
|
+
* and nothing else.
|
|
23
25
|
*
|
|
24
26
|
* The order it steps through is EXACTLY what the list screen renders —
|
|
25
27
|
* `[...pinned, ...rest]` off the same `useNetworkEventList` + `splitPinned`
|
|
@@ -29,17 +31,24 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
29
31
|
* "request N of <what the list is showing>", never "N of everything ever
|
|
30
32
|
* captured". Detail opened from the Saved screen steps through the SAVED
|
|
31
33
|
* list instead (with its own search applied), for the same reason.
|
|
34
|
+
*
|
|
35
|
+
* This component OWNS the navigable order but not the cursor: it publishes
|
|
36
|
+
* the ordered ids into `networkSelectionStore` and reads the cursor back.
|
|
37
|
+
* That split is what makes stepping O(1) (the store keeps the index, so
|
|
38
|
+
* there is no `findIndex` per render) and what lets the benchmark harness
|
|
39
|
+
* drive `step(+1)` directly without going through the UI.
|
|
32
40
|
*/
|
|
33
41
|
|
|
42
|
+
/** Stable identities — the store is a singleton, so these never need to change. */
|
|
43
|
+
const stepPrevious = () => _networkSelectionStore.networkSelectionStore.step(-1);
|
|
44
|
+
const stepNext = () => _networkSelectionStore.networkSelectionStore.step(1);
|
|
34
45
|
const NetworkDetailStepper = exports.NetworkDetailStepper = /*#__PURE__*/(0, _react.memo)(function NetworkDetailStepper({
|
|
35
|
-
eventId,
|
|
36
46
|
paused,
|
|
37
47
|
filter,
|
|
38
48
|
ignoredPatterns,
|
|
39
49
|
isPro,
|
|
40
50
|
fromSaved,
|
|
41
51
|
savedSearch,
|
|
42
|
-
onSelect,
|
|
43
52
|
applySafeAreaInset
|
|
44
53
|
}) {
|
|
45
54
|
const {
|
|
@@ -67,7 +76,16 @@ const NetworkDetailStepper = exports.NetworkDetailStepper = /*#__PURE__*/(0, _re
|
|
|
67
76
|
} = (0, _withPinnedFirst.splitPinned)(visibleEvents, pinnedEvents, pinnedLiveIds);
|
|
68
77
|
return pinned.length === 0 ? rest : [...pinned, ...rest];
|
|
69
78
|
}, [fromSaved, savedRecords, savedSearch, isPro, visibleEvents, pinnedEvents, pinnedLiveIds]);
|
|
70
|
-
|
|
79
|
+
|
|
80
|
+
// Publish the navigable order. `setIds` re-anchors the cursor BY ID and
|
|
81
|
+
// no-ops when the ids are unchanged, so this is safe to run every render.
|
|
82
|
+
(0, _react.useEffect)(() => {
|
|
83
|
+
_networkSelectionStore.networkSelectionStore.setIds((0, _networkSelectionStore.toSelectionIds)(ordered));
|
|
84
|
+
}, [ordered]);
|
|
85
|
+
const {
|
|
86
|
+
index,
|
|
87
|
+
total
|
|
88
|
+
} = (0, _useNetworkSelection.useNetworkCursor)();
|
|
71
89
|
|
|
72
90
|
// Not in the list at all: the filter changed under it while it was open, it
|
|
73
91
|
// aged past the 500-request cap, or it is a saved snapshot with no live twin.
|
|
@@ -79,16 +97,10 @@ const NetworkDetailStepper = exports.NetworkDetailStepper = /*#__PURE__*/(0, _re
|
|
|
79
97
|
// the same direction as scrolling up, which is what the arrow means here.
|
|
80
98
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.EventStepperFooter, {
|
|
81
99
|
currentIndex: index,
|
|
82
|
-
totalItems:
|
|
100
|
+
totalItems: total,
|
|
83
101
|
itemLabel: "Request",
|
|
84
102
|
applySafeAreaInset: applySafeAreaInset,
|
|
85
|
-
onPrevious:
|
|
86
|
-
|
|
87
|
-
if (target) onSelect(target.id);
|
|
88
|
-
},
|
|
89
|
-
onNext: () => {
|
|
90
|
-
const target = ordered[index + 1];
|
|
91
|
-
if (target) onSelect(target.id);
|
|
92
|
-
}
|
|
103
|
+
onPrevious: stepPrevious,
|
|
104
|
+
onNext: stepNext
|
|
93
105
|
});
|
|
94
106
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.NetworkEventDetailView =
|
|
6
|
+
exports.NetworkEventDetailView = void 0;
|
|
7
7
|
var _react = require("react");
|
|
8
8
|
var _reactNative = require("react-native");
|
|
9
9
|
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
@@ -17,6 +17,10 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
17
17
|
* body glyphs). The Lock/Unlock pair keeps its colour because
|
|
18
18
|
* secure-vs-insecure is a genuine signal, as does the status pill.
|
|
19
19
|
*/const NEUTRAL_ICON = _sharedUi.macOSColors.semantic.success;
|
|
20
|
+
|
|
21
|
+
/** Hoisted so the defaults don't mint a new Set/function per render. */
|
|
22
|
+
const EMPTY_PATTERNS = new Set();
|
|
23
|
+
const noop = () => {};
|
|
20
24
|
/**
|
|
21
25
|
* The measurable slice of a request's lifetime, as proportional bars.
|
|
22
26
|
*
|
|
@@ -82,6 +86,17 @@ const TimingPhases = ({
|
|
|
82
86
|
});
|
|
83
87
|
};
|
|
84
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Stand-in for a body tree while the prev/next cursor is still moving.
|
|
91
|
+
*
|
|
92
|
+
* Deliberately a fixed-height nothing: it must cost ~0 to render (that is the
|
|
93
|
+
* entire point) and it must not resize the scroll view as the real tree swaps
|
|
94
|
+
* in, or fast stepping would make the page jump under the reader.
|
|
95
|
+
*/
|
|
96
|
+
const BodyDeferred = () => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
97
|
+
style: styles.bodyDeferred
|
|
98
|
+
});
|
|
99
|
+
|
|
85
100
|
// Component for collapsible sections matching Sentry style
|
|
86
101
|
const CollapsibleSection = ({
|
|
87
102
|
title,
|
|
@@ -261,11 +276,30 @@ const UrlBreakdown = ({
|
|
|
261
276
|
/**
|
|
262
277
|
* Detailed inspector rendering request/response metadata for a single network event. Designed to
|
|
263
278
|
* mimic Sentry’s layout with collapsible sections, diff views, and quick copy affordances.
|
|
279
|
+
*
|
|
280
|
+
* MEMOIZED with profiling evidence, not by reflex. Two render paths hit this
|
|
281
|
+
* subtree with unchanged props:
|
|
282
|
+
*
|
|
283
|
+
* 1. `useLiveNetworkEvent` re-subscribes when the selected ID changes, and
|
|
284
|
+
* `subscribeToEvents` replays immediately — so every step through the
|
|
285
|
+
* prev/next footer committed the SAME event object twice, rendering this
|
|
286
|
+
* whole tree (expanded DataViewer included) a second time for nothing.
|
|
287
|
+
* 2. Any modal-root state change (header search, mode toggle) re-renders the
|
|
288
|
+
* shell, and the detail body has nothing to do with it.
|
|
289
|
+
*
|
|
290
|
+
* Both props besides `event` are stable by construction — `values` is memoized
|
|
291
|
+
* in `useIgnoredPatterns` and `toggle` is module-scope there — so the bail-out
|
|
292
|
+
* actually fires.
|
|
293
|
+
*
|
|
294
|
+
* Measured baseline (js-top, example-dev-build, 5 steps over 109–324KB
|
|
295
|
+
* pokeapi responses): 307ms of React work across 5 steps ≈ 61ms of JS per
|
|
296
|
+
* step. Case 1 alone is half of that, since it re-ran the whole body render.
|
|
264
297
|
*/
|
|
265
|
-
function NetworkEventDetailView({
|
|
298
|
+
const NetworkEventDetailView = exports.NetworkEventDetailView = /*#__PURE__*/(0, _react.memo)(function NetworkEventDetailView({
|
|
266
299
|
event,
|
|
267
|
-
ignoredPatterns =
|
|
268
|
-
onTogglePattern =
|
|
300
|
+
ignoredPatterns = EMPTY_PATTERNS,
|
|
301
|
+
onTogglePattern = noop,
|
|
302
|
+
showBodies = true
|
|
269
303
|
}) {
|
|
270
304
|
const status = event.status ? (0, _formatting.formatHttpStatus)(event.status) : null;
|
|
271
305
|
const isPending = !event.status && !event.error;
|
|
@@ -409,8 +443,15 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
409
443
|
style: styles.httpDurationText,
|
|
410
444
|
children: (0, _formatting.formatDuration)(event.duration)
|
|
411
445
|
})]
|
|
412
|
-
}) : null, isPro ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InlineCopyButton
|
|
413
|
-
|
|
446
|
+
}) : null, isPro ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InlineCopyButton
|
|
447
|
+
// LAZY, not `getFullRequestDetails()`. CopyButton resolves a
|
|
448
|
+
// function value at press time; calling it here instead
|
|
449
|
+
// pretty-printed the headers AND both bodies on every render —
|
|
450
|
+
// four JSON.stringify passes over the full payload just to have
|
|
451
|
+
// a string nobody asked for yet. Same shape NetworkSavedScreen
|
|
452
|
+
// already uses for its copy button.
|
|
453
|
+
, {
|
|
454
|
+
value: getFullRequestDetails,
|
|
414
455
|
buttonStyle: styles.copyFullButton
|
|
415
456
|
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
416
457
|
style: styles.copyFullButton,
|
|
@@ -544,14 +585,14 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
544
585
|
defaultOpen: false,
|
|
545
586
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
546
587
|
style: styles.dataViewerContainer,
|
|
547
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.DataViewer, {
|
|
588
|
+
children: showBodies ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.DataViewer, {
|
|
548
589
|
title: "",
|
|
549
590
|
data: requestData,
|
|
550
591
|
showTypeFilter: true,
|
|
551
592
|
rawMode: true,
|
|
552
593
|
initialExpanded: true,
|
|
553
594
|
disableCopy: !isPro
|
|
554
|
-
})
|
|
595
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(BodyDeferred, {})
|
|
555
596
|
})
|
|
556
597
|
}) : requestBodyOmitted ? /*#__PURE__*/(0, _jsxRuntime.jsx)(CollapsibleSection, {
|
|
557
598
|
title: "Request Body",
|
|
@@ -576,14 +617,14 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
576
617
|
defaultOpen: true,
|
|
577
618
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
578
619
|
style: styles.dataViewerContainer,
|
|
579
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.DataViewer, {
|
|
620
|
+
children: showBodies ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.DataViewer, {
|
|
580
621
|
title: "",
|
|
581
622
|
data: responseData,
|
|
582
623
|
showTypeFilter: true,
|
|
583
624
|
rawMode: true,
|
|
584
625
|
initialExpanded: true,
|
|
585
626
|
disableCopy: !isPro
|
|
586
|
-
})
|
|
627
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(BodyDeferred, {})
|
|
587
628
|
})
|
|
588
629
|
}) : responseBodyOmitted ? /*#__PURE__*/(0, _jsxRuntime.jsx)(CollapsibleSection, {
|
|
589
630
|
title: "Response Body",
|
|
@@ -688,7 +729,7 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
688
729
|
benefits: ["Copy full request details including headers and body", "Export to JSON, Markdown, or plain text", "Share request data with teammates", "Feed data to AI assistants for debugging"]
|
|
689
730
|
})]
|
|
690
731
|
});
|
|
691
|
-
}
|
|
732
|
+
});
|
|
692
733
|
const styles = _reactNative.StyleSheet.create({
|
|
693
734
|
container: {
|
|
694
735
|
flex: 1,
|
|
@@ -964,6 +1005,10 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
964
1005
|
marginHorizontal: -12,
|
|
965
1006
|
marginBottom: -12
|
|
966
1007
|
},
|
|
1008
|
+
/** Placeholder for a not-yet-mounted body tree. See BodyDeferred. */
|
|
1009
|
+
bodyDeferred: {
|
|
1010
|
+
height: 120
|
|
1011
|
+
},
|
|
967
1012
|
// Error box
|
|
968
1013
|
errorBox: {
|
|
969
1014
|
flexDirection: "row",
|
|
@@ -13,6 +13,7 @@ var _sharedUi = require("@buoy-gg/shared-ui");
|
|
|
13
13
|
var _networkEventStore = require("../utils/networkEventStore");
|
|
14
14
|
var _networkSavedStore = require("../utils/networkSavedStore");
|
|
15
15
|
var _useNetworkSaved = require("../hooks/useNetworkSaved");
|
|
16
|
+
var _useNetworkSelection = require("../hooks/useNetworkSelection");
|
|
16
17
|
var _useNetworkBadgeCounts = require("../hooks/useNetworkBadgeCounts");
|
|
17
18
|
var _useNetworkHasMatches = require("../hooks/useNetworkHasMatches");
|
|
18
19
|
var _generateNetworkCopyText = require("../utils/generateNetworkCopyText");
|
|
@@ -149,12 +150,16 @@ function NetworkSavedButton({
|
|
|
149
150
|
* The event object is resolved at PRESS time rather than held as a prop: the
|
|
150
151
|
* live store has the fresh copy while the request is in flight, and the saved
|
|
151
152
|
* store is the only copy once it has been cleared or aged out.
|
|
153
|
+
*
|
|
154
|
+
* The selected id is SUBSCRIBED here rather than passed down, so stepping
|
|
155
|
+
* through requests doesn't have to re-render the modal root just to rebuild
|
|
156
|
+
* the header around a new prop.
|
|
152
157
|
*/
|
|
153
158
|
function NetworkDetailPinActions({
|
|
154
|
-
eventId,
|
|
155
159
|
isPro,
|
|
156
160
|
onUpgradePress
|
|
157
161
|
}) {
|
|
162
|
+
const eventId = (0, _useNetworkSelection.useSelectedNetworkEventId)();
|
|
158
163
|
const state = (0, _useNetworkSaved.useNetworkSaved)();
|
|
159
164
|
const {
|
|
160
165
|
pinned,
|
|
@@ -121,16 +121,21 @@ function NetworkListScreen({
|
|
|
121
121
|
togglePin,
|
|
122
122
|
clearPinned
|
|
123
123
|
} = (0, _useNetworkSaved.useNetworkSaved)();
|
|
124
|
-
const {
|
|
125
|
-
pinned,
|
|
126
|
-
rest
|
|
127
|
-
} = (0, _withPinnedFirst.splitPinned)(visibleEvents, pinnedEvents, pinnedLiveIds);
|
|
128
124
|
|
|
129
125
|
// Stable identity: handed to memoized rows.
|
|
130
126
|
const handleLongPress = (0, _react.useCallback)(event => {
|
|
131
127
|
togglePin(event);
|
|
132
128
|
}, [togglePin]);
|
|
129
|
+
|
|
130
|
+
// Everything above is a hook and must run unconditionally; everything below
|
|
131
|
+
// is work. `splitPinned` is an O(n) pass over the whole list, and this screen
|
|
132
|
+
// re-renders on every modal-root state change — including every prev/next
|
|
133
|
+
// step, where it is hidden and the result is thrown away.
|
|
133
134
|
if (!visible) return null;
|
|
135
|
+
const {
|
|
136
|
+
pinned,
|
|
137
|
+
rest
|
|
138
|
+
} = (0, _withPinnedFirst.splitPinned)(visibleEvents, pinnedEvents, pinnedLiveIds);
|
|
134
139
|
const isEnabled = !paused;
|
|
135
140
|
const renderItem = ({
|
|
136
141
|
item
|
|
@@ -18,6 +18,8 @@ var _NetworkSavedScreen = require("./NetworkSavedScreen");
|
|
|
18
18
|
var _NetworkDetailStepper = require("./NetworkDetailStepper");
|
|
19
19
|
var _useNetworkEventList = require("../hooks/useNetworkEventList");
|
|
20
20
|
var _useLiveNetworkEvent = require("../hooks/useLiveNetworkEvent");
|
|
21
|
+
var _useNetworkSelection = require("../hooks/useNetworkSelection");
|
|
22
|
+
var _networkSelectionStore = require("../utils/networkSelectionStore");
|
|
21
23
|
var _networkSavedStore = require("../utils/networkSavedStore");
|
|
22
24
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
23
25
|
// ============================================================================
|
|
@@ -29,7 +31,6 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
29
31
|
|
|
30
32
|
function NetworkDetailScreen({
|
|
31
33
|
visible,
|
|
32
|
-
eventId,
|
|
33
34
|
paused,
|
|
34
35
|
ignoredPatternValues,
|
|
35
36
|
onTogglePattern,
|
|
@@ -38,34 +39,66 @@ function NetworkDetailScreen({
|
|
|
38
39
|
ignoredPatterns,
|
|
39
40
|
fromSaved,
|
|
40
41
|
savedSearch,
|
|
41
|
-
onSelectEvent,
|
|
42
42
|
applySafeAreaInset
|
|
43
43
|
}) {
|
|
44
|
+
/**
|
|
45
|
+
* The detail renders at the IMMEDIATE selection — every step, no delay.
|
|
46
|
+
* Measured, 100 steps at a 150ms tap interval over 64KB payloads:
|
|
47
|
+
*
|
|
48
|
+
* whole detail, bodies included ... 647ms/step
|
|
49
|
+
* detail with no bodies at all .... 43ms/step (over the 150ms tap gap)
|
|
50
|
+
*
|
|
51
|
+
* So ~93% of a step is the request/response BODY trees, and everything else
|
|
52
|
+
* — status, URL breakdown, timing, header counts — is affordable at any tap
|
|
53
|
+
* rate. Deferring the whole detail (which is what this used to do) therefore
|
|
54
|
+
* bought nothing a user could feel: it made the cheap 43ms wait alongside
|
|
55
|
+
* the expensive 450ms instead of splitting them.
|
|
56
|
+
*
|
|
57
|
+
* So only the expensive part waits. `bodyId` trails `selectedId` by the
|
|
58
|
+
* settle window, so `heavyReady` is false exactly while the cursor is still
|
|
59
|
+
* moving — the JSON trees are skipped during a burst and mount once you land.
|
|
60
|
+
*/
|
|
61
|
+
const eventId = (0, _useNetworkSelection.useSelectedNetworkEventId)();
|
|
62
|
+
const settledId = (0, _useNetworkSelection.useNetworkBodyEventId)();
|
|
63
|
+
const heavyReady = eventId != null && eventId === settledId;
|
|
44
64
|
// Resolve the ID to the live event so a request opened while pending picks
|
|
45
65
|
// up its response/error in place instead of freezing on the snapshot taken
|
|
46
66
|
// when the row was tapped.
|
|
47
67
|
const event = (0, _useLiveNetworkEvent.useLiveNetworkEvent)(eventId, {
|
|
48
68
|
paused: paused || !visible
|
|
49
69
|
});
|
|
50
|
-
|
|
70
|
+
|
|
71
|
+
// Instrumentation for the benchmark harness. Gated on `heavyReady` on
|
|
72
|
+
// purpose: the cheap chrome now renders on every step, so reporting here
|
|
73
|
+
// unconditionally would make `bodyMs` a duplicate of `cursorMs`. What the
|
|
74
|
+
// harness needs to know is when the expensive TREES landed.
|
|
75
|
+
(0, _react.useEffect)(() => {
|
|
76
|
+
if (visible && event && heavyReady) {
|
|
77
|
+
_networkSelectionStore.networkSelectionStore.reportRendered(event.id);
|
|
78
|
+
}
|
|
79
|
+
}, [visible, event, heavyReady]);
|
|
80
|
+
if (!visible) return null;
|
|
51
81
|
// Column: the detail body scrolls, the stepper stays pinned under it. The
|
|
52
82
|
// body owns its scroll here (see `bodyOwnsScroll`) — inside JsModal's
|
|
53
|
-
// wrapper the footer would scroll away with the content.
|
|
83
|
+
// wrapper the footer would scroll away with the content. The spacer keeps
|
|
84
|
+
// the footer at the bottom on the rare frame where the ID resolves to
|
|
85
|
+
// nothing (cleared, aged out, and not saved).
|
|
54
86
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
55
87
|
style: styles.detailRoot,
|
|
56
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventDetailView.NetworkEventDetailView, {
|
|
88
|
+
children: [event ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventDetailView.NetworkEventDetailView, {
|
|
57
89
|
event: event,
|
|
58
90
|
ignoredPatterns: ignoredPatternValues,
|
|
59
|
-
onTogglePattern: onTogglePattern
|
|
91
|
+
onTogglePattern: onTogglePattern,
|
|
92
|
+
showBodies: heavyReady
|
|
93
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
94
|
+
style: styles.detailBodyFallback
|
|
60
95
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkDetailStepper.NetworkDetailStepper, {
|
|
61
|
-
eventId: event.id,
|
|
62
96
|
paused: paused,
|
|
63
97
|
filter: filter,
|
|
64
98
|
ignoredPatterns: ignoredPatterns,
|
|
65
99
|
isPro: isPro,
|
|
66
100
|
fromSaved: fromSaved,
|
|
67
101
|
savedSearch: savedSearch,
|
|
68
|
-
onSelect: onSelectEvent,
|
|
69
102
|
applySafeAreaInset: applySafeAreaInset
|
|
70
103
|
})]
|
|
71
104
|
});
|
|
@@ -160,9 +193,15 @@ function NetworkModalInner({
|
|
|
160
193
|
if (controlledPause) onPausedChange(value);else setPausedState(value);
|
|
161
194
|
};
|
|
162
195
|
const [filter, setFilter] = (0, _react.useState)({});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
/**
|
|
197
|
+
* The cursor lives in `networkSelectionStore`, and the root subscribes to
|
|
198
|
+
* the BOOLEAN only. That is the whole point: this component re-renders
|
|
199
|
+
* `JsModal` (which can never bail — its `header` prop is a fresh object
|
|
200
|
+
* every render), the entire header, and all five screens. None of them care
|
|
201
|
+
* WHICH request is selected, only THAT one is, and this flips twice a
|
|
202
|
+
* session instead of once per tap.
|
|
203
|
+
*/
|
|
204
|
+
const hasSelection = (0, _useNetworkSelection.useHasNetworkSelection)();
|
|
166
205
|
const [showFilterView, setShowFilterView] = (0, _react.useState)(false);
|
|
167
206
|
const [showSavedView, setShowSavedView] = (0, _react.useState)(false);
|
|
168
207
|
const [activeTab, setActiveTab] = (0, _react.useState)("filters");
|
|
@@ -236,12 +275,12 @@ function NetworkModalInner({
|
|
|
236
275
|
|
|
237
276
|
// Simple handlers - no useCallback needed per rule2
|
|
238
277
|
const handleBack = () => {
|
|
239
|
-
|
|
278
|
+
_networkSelectionStore.networkSelectionStore.clear();
|
|
240
279
|
};
|
|
241
280
|
|
|
242
281
|
// Stable identity: this is passed down to the memoized list rows.
|
|
243
282
|
const handleEventPress = (0, _react.useCallback)(event => {
|
|
244
|
-
|
|
283
|
+
_networkSelectionStore.networkSelectionStore.select(event.id);
|
|
245
284
|
}, []);
|
|
246
285
|
const handleSearch = text => {
|
|
247
286
|
setSearchText(text);
|
|
@@ -299,7 +338,7 @@ function NetworkModalInner({
|
|
|
299
338
|
}
|
|
300
339
|
|
|
301
340
|
// Event detail view header
|
|
302
|
-
if (
|
|
341
|
+
if (hasSelection) {
|
|
303
342
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
|
|
304
343
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
305
344
|
onBack: handleBack
|
|
@@ -308,7 +347,6 @@ function NetworkModalInner({
|
|
|
308
347
|
centered: true
|
|
309
348
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions, {
|
|
310
349
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkDetailPinActions, {
|
|
311
|
-
eventId: selectedEventId,
|
|
312
350
|
isPro: isPro,
|
|
313
351
|
onUpgradePress: () => showUpgrade("saved")
|
|
314
352
|
})
|
|
@@ -422,13 +460,12 @@ function NetworkModalInner({
|
|
|
422
460
|
// same deal. The DETAIL screen owns it as well, so its prev/next footer
|
|
423
461
|
// stays pinned instead of scrolling away with the body. Filter/copy still
|
|
424
462
|
// use the wrapper.
|
|
425
|
-
const bodyOwnsScroll =
|
|
463
|
+
const bodyOwnsScroll = hasSelection || !showFilterView;
|
|
426
464
|
const body = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
427
465
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
428
466
|
style: styles.container,
|
|
429
467
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkDetailScreen, {
|
|
430
|
-
visible:
|
|
431
|
-
eventId: selectedEventId,
|
|
468
|
+
visible: hasSelection,
|
|
432
469
|
paused: paused,
|
|
433
470
|
ignoredPatternValues: ignoredPatternValues,
|
|
434
471
|
onTogglePattern: toggleIgnoredPattern,
|
|
@@ -439,13 +476,12 @@ function NetworkModalInner({
|
|
|
439
476
|
// the user came from — and therefore which one to step through.
|
|
440
477
|
,
|
|
441
478
|
fromSaved: showSavedView,
|
|
442
|
-
savedSearch: savedSearch
|
|
443
|
-
onSelectEvent: setSelectedEventId
|
|
479
|
+
savedSearch: savedSearch
|
|
444
480
|
// Inline (desktop panel) has no home indicator to clear.
|
|
445
481
|
,
|
|
446
482
|
applySafeAreaInset: !inline && modalMode === "bottomSheet"
|
|
447
483
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkFilterScreen, {
|
|
448
|
-
visible: showFilterView && activeTab === "filters" && !
|
|
484
|
+
visible: showFilterView && activeTab === "filters" && !hasSelection,
|
|
449
485
|
paused: paused,
|
|
450
486
|
isPro: isPro,
|
|
451
487
|
filter: filter,
|
|
@@ -455,7 +491,7 @@ function NetworkModalInner({
|
|
|
455
491
|
onAddPattern: addIgnoredPattern,
|
|
456
492
|
onTogglePatternMode: toggleIgnoredPatternMode
|
|
457
493
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkCopyScreen, {
|
|
458
|
-
visible: showFilterView && activeTab === "copy" && !
|
|
494
|
+
visible: showFilterView && activeTab === "copy" && !hasSelection,
|
|
459
495
|
paused: paused,
|
|
460
496
|
isPro: isPro,
|
|
461
497
|
filter: filter,
|
|
@@ -463,7 +499,7 @@ function NetworkModalInner({
|
|
|
463
499
|
settings: copySettings,
|
|
464
500
|
onSettingsChange: setCopySettings
|
|
465
501
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkSavedScreen.NetworkSavedScreen, {
|
|
466
|
-
visible: showSavedView &&
|
|
502
|
+
visible: showSavedView && !hasSelection && !showFilterView,
|
|
467
503
|
isPro: isPro,
|
|
468
504
|
copySettings: copySettings,
|
|
469
505
|
search: savedSearch,
|
|
@@ -471,7 +507,7 @@ function NetworkModalInner({
|
|
|
471
507
|
onEventPress: handleEventPress,
|
|
472
508
|
onUpgradePress: () => showUpgrade("saved")
|
|
473
509
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkListScreen.NetworkListScreen, {
|
|
474
|
-
visible:
|
|
510
|
+
visible: !hasSelection && !showFilterView && !showSavedView,
|
|
475
511
|
paused: paused,
|
|
476
512
|
filter: filter,
|
|
477
513
|
ignoredPatterns: ignoredPatterns,
|
|
@@ -529,6 +565,10 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
529
565
|
flex: 1,
|
|
530
566
|
minHeight: 0
|
|
531
567
|
},
|
|
568
|
+
/** Holds the footer down when the selected ID resolves to no event. */
|
|
569
|
+
detailBodyFallback: {
|
|
570
|
+
flex: 1
|
|
571
|
+
},
|
|
532
572
|
/** Inline shell — fills the host panel; `minHeight: 0` so the list's own
|
|
533
573
|
* FlatList can scroll instead of stretching the column. */
|
|
534
574
|
inlineRoot: {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useHasNetworkSelection = useHasNetworkSelection;
|
|
7
|
+
exports.useNetworkBodyEventId = useNetworkBodyEventId;
|
|
8
|
+
exports.useNetworkCursor = useNetworkCursor;
|
|
9
|
+
exports.useSelectedNetworkEventId = useSelectedNetworkEventId;
|
|
10
|
+
var _react = require("react");
|
|
11
|
+
var _networkSelectionStore = require("../utils/networkSelectionStore");
|
|
12
|
+
/**
|
|
13
|
+
* Narrow subscriptions to the prev/next cursor (`networkSelectionStore`).
|
|
14
|
+
*
|
|
15
|
+
* One hook per consumer, each returning the SMALLEST value that consumer
|
|
16
|
+
* renders — that is the whole point of moving the cursor into a store. The
|
|
17
|
+
* modal root takes `useHasNetworkSelection()` (a boolean that flips twice a
|
|
18
|
+
* session), so a step no longer re-renders `JsModal`, the header and all five
|
|
19
|
+
* screens; the footer and the body take the parts that actually change.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const subscribe = onChange => _networkSelectionStore.networkSelectionStore.subscribe(onChange);
|
|
23
|
+
const getState = () => _networkSelectionStore.networkSelectionStore.getState();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Is ANYTHING selected — i.e. is the detail screen up?
|
|
27
|
+
*
|
|
28
|
+
* `useSyncExternalStore` compares the selector's result, so the root only
|
|
29
|
+
* re-renders when this boolean flips, not on every step.
|
|
30
|
+
*/
|
|
31
|
+
function useHasNetworkSelection() {
|
|
32
|
+
return (0, _react.useSyncExternalStore)(subscribe, () => getState().selectedId != null, () => getState().selectedId != null);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Where the USER is. Moves synchronously on tap — footer, header pin actions. */
|
|
36
|
+
function useSelectedNetworkEventId() {
|
|
37
|
+
return (0, _react.useSyncExternalStore)(subscribe, () => getState().selectedId, () => getState().selectedId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What the BODY may render. Lags `selectedId` — see networkSelectionStore. */
|
|
41
|
+
function useNetworkBodyEventId() {
|
|
42
|
+
return (0, _react.useSyncExternalStore)(subscribe, () => getState().bodyId, () => getState().bodyId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Cursor position + list length, for the "REQUEST n OF m" counter. */
|
|
46
|
+
function useNetworkCursor() {
|
|
47
|
+
const index = (0, _react.useSyncExternalStore)(subscribe, () => getState().index, () => getState().index);
|
|
48
|
+
const total = (0, _react.useSyncExternalStore)(subscribe, () => getState().ids.length, () => getState().ids.length);
|
|
49
|
+
return {
|
|
50
|
+
index,
|
|
51
|
+
total
|
|
52
|
+
};
|
|
53
|
+
}
|