@buoy-gg/network 7.0.11 → 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/README.md +1 -0
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/network/components/NetworkDetailStepper.js +28 -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 +121 -26
- package/lib/commonjs/network/components/NetworkSavedScreen.js +12 -15
- 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 +29 -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 +121 -26
- package/lib/module/network/components/NetworkSavedScreen.js +13 -16
- 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 +10 -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/components/NetworkSavedScreen.d.ts +12 -1
- package/lib/typescript/network/components/NetworkSavedScreen.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/README.md
CHANGED
|
@@ -40,6 +40,7 @@ Want a different label or color? Build the tool with `createNetworkTool({ name,
|
|
|
40
40
|
- **Full request detail** — request/response headers, payloads, HTTP status, and duration for every call, with status-colored rows.
|
|
41
41
|
- **Filter and search** — narrow by status code, HTTP method, or URL pattern; search captured events; pause/resume capture; clear history.
|
|
42
42
|
- **Pin the one that failed** — pin a request to hold it at the top of the list (ignoring your filters), or save it to a separate favorites list. Both keep a full snapshot, so it survives Clear, the 500-request cap, and an app restart. Long-press a row, or use the buttons in the request detail header.
|
|
43
|
+
- **Step between requests** — the detail view has a Previous/Next footer that walks exactly what the list was showing (same pins, filters, and search — or your Saved list), so comparing two calls doesn't mean going back and finding your place.
|
|
43
44
|
- **Ignore the noise** — a shared ignored-domains/URL filter (`useIgnoredPatterns`) hides endpoints you never want to see, shared with the Events tool.
|
|
44
45
|
- **Hooks for custom UIs** — `useNetworkEvents`, `useNetworkEventList`, `useNetworkBadgeCounts`, and `useNetworkSaved` expose the capture stream with narrow per-consumer subscriptions.
|
|
45
46
|
|
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,25 @@ 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
|
-
|
|
52
|
+
applySafeAreaInset
|
|
43
53
|
}) {
|
|
44
54
|
const {
|
|
45
55
|
visibleEvents
|
|
@@ -66,7 +76,16 @@ const NetworkDetailStepper = exports.NetworkDetailStepper = /*#__PURE__*/(0, _re
|
|
|
66
76
|
} = (0, _withPinnedFirst.splitPinned)(visibleEvents, pinnedEvents, pinnedLiveIds);
|
|
67
77
|
return pinned.length === 0 ? rest : [...pinned, ...rest];
|
|
68
78
|
}, [fromSaved, savedRecords, savedSearch, isPro, visibleEvents, pinnedEvents, pinnedLiveIds]);
|
|
69
|
-
|
|
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)();
|
|
70
89
|
|
|
71
90
|
// Not in the list at all: the filter changed under it while it was open, it
|
|
72
91
|
// aged past the 500-request cap, or it is a saved snapshot with no live twin.
|
|
@@ -78,15 +97,10 @@ const NetworkDetailStepper = exports.NetworkDetailStepper = /*#__PURE__*/(0, _re
|
|
|
78
97
|
// the same direction as scrolling up, which is what the arrow means here.
|
|
79
98
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.EventStepperFooter, {
|
|
80
99
|
currentIndex: index,
|
|
81
|
-
totalItems:
|
|
100
|
+
totalItems: total,
|
|
82
101
|
itemLabel: "Request",
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
},
|
|
87
|
-
onNext: () => {
|
|
88
|
-
const target = ordered[index + 1];
|
|
89
|
-
if (target) onSelect(target.id);
|
|
90
|
-
}
|
|
102
|
+
applySafeAreaInset: applySafeAreaInset,
|
|
103
|
+
onPrevious: stepPrevious,
|
|
104
|
+
onNext: stepNext
|
|
91
105
|
});
|
|
92
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
|
|
@@ -15,8 +15,11 @@ var _NetworkCopySettingsView = require("./NetworkCopySettingsView");
|
|
|
15
15
|
var _NetworkHeaderLive = require("./NetworkHeaderLive");
|
|
16
16
|
var _NetworkListScreen = require("./NetworkListScreen");
|
|
17
17
|
var _NetworkSavedScreen = require("./NetworkSavedScreen");
|
|
18
|
+
var _NetworkDetailStepper = require("./NetworkDetailStepper");
|
|
18
19
|
var _useNetworkEventList = require("../hooks/useNetworkEventList");
|
|
19
20
|
var _useLiveNetworkEvent = require("../hooks/useLiveNetworkEvent");
|
|
21
|
+
var _useNetworkSelection = require("../hooks/useNetworkSelection");
|
|
22
|
+
var _networkSelectionStore = require("../utils/networkSelectionStore");
|
|
20
23
|
var _networkSavedStore = require("../utils/networkSavedStore");
|
|
21
24
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
22
25
|
// ============================================================================
|
|
@@ -28,22 +31,76 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
28
31
|
|
|
29
32
|
function NetworkDetailScreen({
|
|
30
33
|
visible,
|
|
31
|
-
eventId,
|
|
32
34
|
paused,
|
|
33
35
|
ignoredPatternValues,
|
|
34
|
-
onTogglePattern
|
|
36
|
+
onTogglePattern,
|
|
37
|
+
isPro,
|
|
38
|
+
filter,
|
|
39
|
+
ignoredPatterns,
|
|
40
|
+
fromSaved,
|
|
41
|
+
savedSearch,
|
|
42
|
+
applySafeAreaInset
|
|
35
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;
|
|
36
64
|
// Resolve the ID to the live event so a request opened while pending picks
|
|
37
65
|
// up its response/error in place instead of freezing on the snapshot taken
|
|
38
66
|
// when the row was tapped.
|
|
39
67
|
const event = (0, _useLiveNetworkEvent.useLiveNetworkEvent)(eventId, {
|
|
40
68
|
paused: paused || !visible
|
|
41
69
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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;
|
|
81
|
+
// Column: the detail body scrolls, the stepper stays pinned under it. The
|
|
82
|
+
// body owns its scroll here (see `bodyOwnsScroll`) — inside JsModal's
|
|
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).
|
|
86
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
87
|
+
style: styles.detailRoot,
|
|
88
|
+
children: [event ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventDetailView.NetworkEventDetailView, {
|
|
89
|
+
event: event,
|
|
90
|
+
ignoredPatterns: ignoredPatternValues,
|
|
91
|
+
onTogglePattern: onTogglePattern,
|
|
92
|
+
showBodies: heavyReady
|
|
93
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
94
|
+
style: styles.detailBodyFallback
|
|
95
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkDetailStepper.NetworkDetailStepper, {
|
|
96
|
+
paused: paused,
|
|
97
|
+
filter: filter,
|
|
98
|
+
ignoredPatterns: ignoredPatterns,
|
|
99
|
+
isPro: isPro,
|
|
100
|
+
fromSaved: fromSaved,
|
|
101
|
+
savedSearch: savedSearch,
|
|
102
|
+
applySafeAreaInset: applySafeAreaInset
|
|
103
|
+
})]
|
|
47
104
|
});
|
|
48
105
|
}
|
|
49
106
|
function NetworkFilterScreen({
|
|
@@ -136,14 +193,24 @@ function NetworkModalInner({
|
|
|
136
193
|
if (controlledPause) onPausedChange(value);else setPausedState(value);
|
|
137
194
|
};
|
|
138
195
|
const [filter, setFilter] = (0, _react.useState)({});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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)();
|
|
142
205
|
const [showFilterView, setShowFilterView] = (0, _react.useState)(false);
|
|
143
206
|
const [showSavedView, setShowSavedView] = (0, _react.useState)(false);
|
|
144
207
|
const [activeTab, setActiveTab] = (0, _react.useState)("filters");
|
|
145
208
|
const [searchText, setSearchText] = (0, _react.useState)("");
|
|
146
209
|
const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
|
|
210
|
+
// The Saved screen's search box lives HERE, not in that screen, because the
|
|
211
|
+
// detail footer steps through the same list the user was just looking at —
|
|
212
|
+
// and the Saved screen is unmounted while a detail is open.
|
|
213
|
+
const [savedSearch, setSavedSearch] = (0, _react.useState)("");
|
|
147
214
|
const searchInputRef = (0, _react.useRef)(null);
|
|
148
215
|
// Ignored domains/URL patterns live in a shared, persisted singleton store
|
|
149
216
|
// (@buoy-gg/shared-ui) so the Network tool and the Events tool stay in sync.
|
|
@@ -208,12 +275,12 @@ function NetworkModalInner({
|
|
|
208
275
|
|
|
209
276
|
// Simple handlers - no useCallback needed per rule2
|
|
210
277
|
const handleBack = () => {
|
|
211
|
-
|
|
278
|
+
_networkSelectionStore.networkSelectionStore.clear();
|
|
212
279
|
};
|
|
213
280
|
|
|
214
281
|
// Stable identity: this is passed down to the memoized list rows.
|
|
215
282
|
const handleEventPress = (0, _react.useCallback)(event => {
|
|
216
|
-
|
|
283
|
+
_networkSelectionStore.networkSelectionStore.select(event.id);
|
|
217
284
|
}, []);
|
|
218
285
|
const handleSearch = text => {
|
|
219
286
|
setSearchText(text);
|
|
@@ -228,8 +295,13 @@ function NetworkModalInner({
|
|
|
228
295
|
status: prev.status === status ? undefined : status
|
|
229
296
|
}));
|
|
230
297
|
};
|
|
231
|
-
|
|
232
|
-
|
|
298
|
+
|
|
299
|
+
// JsModal owns the mode; the only thing this modal needs it for is the
|
|
300
|
+
// detail footer's bottom inset — a bottom sheet sits on the home indicator,
|
|
301
|
+
// a floating window does not (same call StorageModalWithTabs makes).
|
|
302
|
+
const [modalMode, setModalMode] = (0, _react.useState)("bottomSheet");
|
|
303
|
+
const handleModeChange = mode => {
|
|
304
|
+
setModalMode(mode);
|
|
233
305
|
};
|
|
234
306
|
(0, _react.useEffect)(() => {
|
|
235
307
|
if (isSearchActive) {
|
|
@@ -266,7 +338,7 @@ function NetworkModalInner({
|
|
|
266
338
|
}
|
|
267
339
|
|
|
268
340
|
// Event detail view header
|
|
269
|
-
if (
|
|
341
|
+
if (hasSelection) {
|
|
270
342
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
|
|
271
343
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
272
344
|
onBack: handleBack
|
|
@@ -275,7 +347,6 @@ function NetworkModalInner({
|
|
|
275
347
|
centered: true
|
|
276
348
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions, {
|
|
277
349
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkDetailPinActions, {
|
|
278
|
-
eventId: selectedEventId,
|
|
279
350
|
isPro: isPro,
|
|
280
351
|
onUpgradePress: () => showUpgrade("saved")
|
|
281
352
|
})
|
|
@@ -386,19 +457,31 @@ function NetworkModalInner({
|
|
|
386
457
|
// it — inside JsModal's ScrollView wrapper every row mounts, which at
|
|
387
458
|
// the 500-event cap measured 17 JS FPS / 133% CPU / 1.4GB; see
|
|
388
459
|
// NetworkListScreen). The Saved screen is a FlatList too, so it takes the
|
|
389
|
-
// same deal.
|
|
390
|
-
|
|
460
|
+
// same deal. The DETAIL screen owns it as well, so its prev/next footer
|
|
461
|
+
// stays pinned instead of scrolling away with the body. Filter/copy still
|
|
462
|
+
// use the wrapper.
|
|
463
|
+
const bodyOwnsScroll = hasSelection || !showFilterView;
|
|
391
464
|
const body = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
392
465
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
393
466
|
style: styles.container,
|
|
394
467
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkDetailScreen, {
|
|
395
|
-
visible:
|
|
396
|
-
eventId: selectedEventId,
|
|
468
|
+
visible: hasSelection,
|
|
397
469
|
paused: paused,
|
|
398
470
|
ignoredPatternValues: ignoredPatternValues,
|
|
399
|
-
onTogglePattern: toggleIgnoredPattern
|
|
471
|
+
onTogglePattern: toggleIgnoredPattern,
|
|
472
|
+
isPro: isPro,
|
|
473
|
+
filter: filter,
|
|
474
|
+
ignoredPatterns: ignoredPatterns
|
|
475
|
+
// `showSavedView` is what Back returns to, so it is also which list
|
|
476
|
+
// the user came from — and therefore which one to step through.
|
|
477
|
+
,
|
|
478
|
+
fromSaved: showSavedView,
|
|
479
|
+
savedSearch: savedSearch
|
|
480
|
+
// Inline (desktop panel) has no home indicator to clear.
|
|
481
|
+
,
|
|
482
|
+
applySafeAreaInset: !inline && modalMode === "bottomSheet"
|
|
400
483
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkFilterScreen, {
|
|
401
|
-
visible: showFilterView && activeTab === "filters" && !
|
|
484
|
+
visible: showFilterView && activeTab === "filters" && !hasSelection,
|
|
402
485
|
paused: paused,
|
|
403
486
|
isPro: isPro,
|
|
404
487
|
filter: filter,
|
|
@@ -408,7 +491,7 @@ function NetworkModalInner({
|
|
|
408
491
|
onAddPattern: addIgnoredPattern,
|
|
409
492
|
onTogglePatternMode: toggleIgnoredPatternMode
|
|
410
493
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkCopyScreen, {
|
|
411
|
-
visible: showFilterView && activeTab === "copy" && !
|
|
494
|
+
visible: showFilterView && activeTab === "copy" && !hasSelection,
|
|
412
495
|
paused: paused,
|
|
413
496
|
isPro: isPro,
|
|
414
497
|
filter: filter,
|
|
@@ -416,13 +499,15 @@ function NetworkModalInner({
|
|
|
416
499
|
settings: copySettings,
|
|
417
500
|
onSettingsChange: setCopySettings
|
|
418
501
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkSavedScreen.NetworkSavedScreen, {
|
|
419
|
-
visible: showSavedView &&
|
|
502
|
+
visible: showSavedView && !hasSelection && !showFilterView,
|
|
420
503
|
isPro: isPro,
|
|
421
504
|
copySettings: copySettings,
|
|
505
|
+
search: savedSearch,
|
|
506
|
+
onSearchChange: setSavedSearch,
|
|
422
507
|
onEventPress: handleEventPress,
|
|
423
508
|
onUpgradePress: () => showUpgrade("saved")
|
|
424
509
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkListScreen.NetworkListScreen, {
|
|
425
|
-
visible:
|
|
510
|
+
visible: !hasSelection && !showFilterView && !showSavedView,
|
|
426
511
|
paused: paused,
|
|
427
512
|
filter: filter,
|
|
428
513
|
ignoredPatterns: ignoredPatterns,
|
|
@@ -464,7 +549,7 @@ function NetworkModalInner({
|
|
|
464
549
|
enablePersistence: true,
|
|
465
550
|
initialMode: "bottomSheet",
|
|
466
551
|
enableGlitchEffects: true,
|
|
467
|
-
disableScrollWrapper:
|
|
552
|
+
disableScrollWrapper: bodyOwnsScroll,
|
|
468
553
|
styles: {},
|
|
469
554
|
children: body
|
|
470
555
|
});
|
|
@@ -474,6 +559,16 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
474
559
|
flex: 1,
|
|
475
560
|
backgroundColor: _sharedUi.macOSColors.background.base
|
|
476
561
|
},
|
|
562
|
+
/** Detail = scrolling body + pinned stepper footer. `minHeight: 0` so the
|
|
563
|
+
* body's ScrollView scrolls instead of stretching the column. */
|
|
564
|
+
detailRoot: {
|
|
565
|
+
flex: 1,
|
|
566
|
+
minHeight: 0
|
|
567
|
+
},
|
|
568
|
+
/** Holds the footer down when the selected ID resolves to no event. */
|
|
569
|
+
detailBodyFallback: {
|
|
570
|
+
flex: 1
|
|
571
|
+
},
|
|
477
572
|
/** Inline shell — fills the host panel; `minHeight: 0` so the list's own
|
|
478
573
|
* FlatList can scroll instead of stretching the column. */
|
|
479
574
|
inlineRoot: {
|
|
@@ -10,7 +10,7 @@ var _sharedUi = require("@buoy-gg/shared-ui");
|
|
|
10
10
|
var _NetworkEventItemCompact = require("./NetworkEventItemCompact");
|
|
11
11
|
var _NetworkUpgradeBanner = require("./NetworkUpgradeBanner");
|
|
12
12
|
var _generateNetworkCopyText = require("../utils/generateNetworkCopyText");
|
|
13
|
-
var
|
|
13
|
+
var _selectSavedEvents = require("../utils/selectSavedEvents");
|
|
14
14
|
var _useNetworkSaved = require("../hooks/useNetworkSaved");
|
|
15
15
|
var _networkSavedStore = require("../utils/networkSavedStore");
|
|
16
16
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
@@ -51,6 +51,8 @@ function NetworkSavedScreen({
|
|
|
51
51
|
visible,
|
|
52
52
|
isPro,
|
|
53
53
|
copySettings,
|
|
54
|
+
search: searchProp,
|
|
55
|
+
onSearchChange,
|
|
54
56
|
onEventPress,
|
|
55
57
|
onUpgradePress
|
|
56
58
|
}) {
|
|
@@ -60,22 +62,17 @@ function NetworkSavedScreen({
|
|
|
60
62
|
toggleSave,
|
|
61
63
|
clearSaved
|
|
62
64
|
} = (0, _useNetworkSaved.useNetworkSaved)();
|
|
63
|
-
const [
|
|
65
|
+
const [searchState, setSearchState] = (0, _react.useState)("");
|
|
66
|
+
const controlled = searchProp != null && onSearchChange != null;
|
|
67
|
+
const search = controlled ? searchProp : searchState;
|
|
68
|
+
const setSearch = controlled ? onSearchChange : setSearchState;
|
|
64
69
|
if (!visible) return null;
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*/
|
|
72
|
-
const limit = isPro ? _networkSavedStore.MAX_SAVED : _networkSavedStore.FREE_TIER_SAVED_LIMIT;
|
|
73
|
-
const unlocked = savedRecords.slice(0, limit);
|
|
74
|
-
const lockedCount = savedRecords.length - unlocked.length;
|
|
75
|
-
const events = unlocked.map(record => record.event);
|
|
76
|
-
const filtered = search ? (0, _filterNetworkEvents.filterNetworkEvents)(events, {
|
|
77
|
-
searchText: search
|
|
78
|
-
}) : events;
|
|
71
|
+
// Shared with the detail footer, so "Next" can't disagree with the row below.
|
|
72
|
+
const {
|
|
73
|
+
events: filtered,
|
|
74
|
+
lockedCount
|
|
75
|
+
} = (0, _selectSavedEvents.selectSavedEvents)(savedRecords, isPro, search);
|
|
79
76
|
const handleLongPress = event => {
|
|
80
77
|
toggleSave(event);
|
|
81
78
|
};
|