@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
|
@@ -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
|
+
}
|
package/lib/module/index.js
CHANGED
|
@@ -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
|
// =============================================================================
|
|
@@ -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
|
|
11
|
-
*
|
|
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
|
-
|
|
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:
|
|
96
|
+
totalItems: total,
|
|
79
97
|
itemLabel: "Request",
|
|
80
98
|
applySafeAreaInset: applySafeAreaInset,
|
|
81
|
-
onPrevious:
|
|
82
|
-
|
|
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
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useMemo } from "react";
|
|
3
|
+
import { useState, useEffect, useMemo, memo } from "react";
|
|
4
4
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from "react-native";
|
|
5
5
|
import { Clock, Upload, Download, AlertCircle, ChevronDown, ChevronUp, Lock, Unlock, FileJson, Filter, Globe, Link, Copy, InlineCopyButton, macOSColors, useFeatureGate, UpgradeModal } from "@buoy-gg/shared-ui";
|
|
6
6
|
import { formatBytes, formatDuration, formatHttpStatus } from "../utils/formatting";
|
|
@@ -16,6 +16,10 @@ import { useNetworkBodyResolver } from "../sync/networkBodyResolver";
|
|
|
16
16
|
*/
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
18
|
const NEUTRAL_ICON = macOSColors.semantic.success;
|
|
19
|
+
|
|
20
|
+
/** Hoisted so the defaults don't mint a new Set/function per render. */
|
|
21
|
+
const EMPTY_PATTERNS = new Set();
|
|
22
|
+
const noop = () => {};
|
|
19
23
|
/**
|
|
20
24
|
* The measurable slice of a request's lifetime, as proportional bars.
|
|
21
25
|
*
|
|
@@ -81,6 +85,17 @@ const TimingPhases = ({
|
|
|
81
85
|
});
|
|
82
86
|
};
|
|
83
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Stand-in for a body tree while the prev/next cursor is still moving.
|
|
90
|
+
*
|
|
91
|
+
* Deliberately a fixed-height nothing: it must cost ~0 to render (that is the
|
|
92
|
+
* entire point) and it must not resize the scroll view as the real tree swaps
|
|
93
|
+
* in, or fast stepping would make the page jump under the reader.
|
|
94
|
+
*/
|
|
95
|
+
const BodyDeferred = () => /*#__PURE__*/_jsx(View, {
|
|
96
|
+
style: styles.bodyDeferred
|
|
97
|
+
});
|
|
98
|
+
|
|
84
99
|
// Component for collapsible sections matching Sentry style
|
|
85
100
|
const CollapsibleSection = ({
|
|
86
101
|
title,
|
|
@@ -260,11 +275,30 @@ const UrlBreakdown = ({
|
|
|
260
275
|
/**
|
|
261
276
|
* Detailed inspector rendering request/response metadata for a single network event. Designed to
|
|
262
277
|
* mimic Sentry’s layout with collapsible sections, diff views, and quick copy affordances.
|
|
278
|
+
*
|
|
279
|
+
* MEMOIZED with profiling evidence, not by reflex. Two render paths hit this
|
|
280
|
+
* subtree with unchanged props:
|
|
281
|
+
*
|
|
282
|
+
* 1. `useLiveNetworkEvent` re-subscribes when the selected ID changes, and
|
|
283
|
+
* `subscribeToEvents` replays immediately — so every step through the
|
|
284
|
+
* prev/next footer committed the SAME event object twice, rendering this
|
|
285
|
+
* whole tree (expanded DataViewer included) a second time for nothing.
|
|
286
|
+
* 2. Any modal-root state change (header search, mode toggle) re-renders the
|
|
287
|
+
* shell, and the detail body has nothing to do with it.
|
|
288
|
+
*
|
|
289
|
+
* Both props besides `event` are stable by construction — `values` is memoized
|
|
290
|
+
* in `useIgnoredPatterns` and `toggle` is module-scope there — so the bail-out
|
|
291
|
+
* actually fires.
|
|
292
|
+
*
|
|
293
|
+
* Measured baseline (js-top, example-dev-build, 5 steps over 109–324KB
|
|
294
|
+
* pokeapi responses): 307ms of React work across 5 steps ≈ 61ms of JS per
|
|
295
|
+
* step. Case 1 alone is half of that, since it re-ran the whole body render.
|
|
263
296
|
*/
|
|
264
|
-
export function NetworkEventDetailView({
|
|
297
|
+
export const NetworkEventDetailView = /*#__PURE__*/memo(function NetworkEventDetailView({
|
|
265
298
|
event,
|
|
266
|
-
ignoredPatterns =
|
|
267
|
-
onTogglePattern =
|
|
299
|
+
ignoredPatterns = EMPTY_PATTERNS,
|
|
300
|
+
onTogglePattern = noop,
|
|
301
|
+
showBodies = true
|
|
268
302
|
}) {
|
|
269
303
|
const status = event.status ? formatHttpStatus(event.status) : null;
|
|
270
304
|
const isPending = !event.status && !event.error;
|
|
@@ -408,8 +442,15 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
408
442
|
style: styles.httpDurationText,
|
|
409
443
|
children: formatDuration(event.duration)
|
|
410
444
|
})]
|
|
411
|
-
}) : null, isPro ? /*#__PURE__*/_jsx(InlineCopyButton
|
|
412
|
-
|
|
445
|
+
}) : null, isPro ? /*#__PURE__*/_jsx(InlineCopyButton
|
|
446
|
+
// LAZY, not `getFullRequestDetails()`. CopyButton resolves a
|
|
447
|
+
// function value at press time; calling it here instead
|
|
448
|
+
// pretty-printed the headers AND both bodies on every render —
|
|
449
|
+
// four JSON.stringify passes over the full payload just to have
|
|
450
|
+
// a string nobody asked for yet. Same shape NetworkSavedScreen
|
|
451
|
+
// already uses for its copy button.
|
|
452
|
+
, {
|
|
453
|
+
value: getFullRequestDetails,
|
|
413
454
|
buttonStyle: styles.copyFullButton
|
|
414
455
|
}) : /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
415
456
|
style: styles.copyFullButton,
|
|
@@ -543,14 +584,14 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
543
584
|
defaultOpen: false,
|
|
544
585
|
children: /*#__PURE__*/_jsx(View, {
|
|
545
586
|
style: styles.dataViewerContainer,
|
|
546
|
-
children: /*#__PURE__*/_jsx(DataViewer, {
|
|
587
|
+
children: showBodies ? /*#__PURE__*/_jsx(DataViewer, {
|
|
547
588
|
title: "",
|
|
548
589
|
data: requestData,
|
|
549
590
|
showTypeFilter: true,
|
|
550
591
|
rawMode: true,
|
|
551
592
|
initialExpanded: true,
|
|
552
593
|
disableCopy: !isPro
|
|
553
|
-
})
|
|
594
|
+
}) : /*#__PURE__*/_jsx(BodyDeferred, {})
|
|
554
595
|
})
|
|
555
596
|
}) : requestBodyOmitted ? /*#__PURE__*/_jsx(CollapsibleSection, {
|
|
556
597
|
title: "Request Body",
|
|
@@ -575,14 +616,14 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
575
616
|
defaultOpen: true,
|
|
576
617
|
children: /*#__PURE__*/_jsx(View, {
|
|
577
618
|
style: styles.dataViewerContainer,
|
|
578
|
-
children: /*#__PURE__*/_jsx(DataViewer, {
|
|
619
|
+
children: showBodies ? /*#__PURE__*/_jsx(DataViewer, {
|
|
579
620
|
title: "",
|
|
580
621
|
data: responseData,
|
|
581
622
|
showTypeFilter: true,
|
|
582
623
|
rawMode: true,
|
|
583
624
|
initialExpanded: true,
|
|
584
625
|
disableCopy: !isPro
|
|
585
|
-
})
|
|
626
|
+
}) : /*#__PURE__*/_jsx(BodyDeferred, {})
|
|
586
627
|
})
|
|
587
628
|
}) : responseBodyOmitted ? /*#__PURE__*/_jsx(CollapsibleSection, {
|
|
588
629
|
title: "Response Body",
|
|
@@ -687,7 +728,7 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
687
728
|
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"]
|
|
688
729
|
})]
|
|
689
730
|
});
|
|
690
|
-
}
|
|
731
|
+
});
|
|
691
732
|
const styles = StyleSheet.create({
|
|
692
733
|
container: {
|
|
693
734
|
flex: 1,
|
|
@@ -963,6 +1004,10 @@ const styles = StyleSheet.create({
|
|
|
963
1004
|
marginHorizontal: -12,
|
|
964
1005
|
marginBottom: -12
|
|
965
1006
|
},
|
|
1007
|
+
/** Placeholder for a not-yet-mounted body tree. See BodyDeferred. */
|
|
1008
|
+
bodyDeferred: {
|
|
1009
|
+
height: 120
|
|
1010
|
+
},
|
|
966
1011
|
// Error box
|
|
967
1012
|
errorBox: {
|
|
968
1013
|
flexDirection: "row",
|
|
@@ -23,6 +23,7 @@ import { Bookmark, CheckCircle, XCircle, Clock, Copy, Pin, Trash2, CopyButton, m
|
|
|
23
23
|
import { networkEventStore } from "../utils/networkEventStore";
|
|
24
24
|
import { flagsForEventId, networkSavedStore, FREE_TIER_SAVED_LIMIT, MAX_PINNED, MAX_SAVED } from "../utils/networkSavedStore";
|
|
25
25
|
import { useNetworkSaved } from "../hooks/useNetworkSaved";
|
|
26
|
+
import { useSelectedNetworkEventId } from "../hooks/useNetworkSelection";
|
|
26
27
|
import { useNetworkBadgeCounts } from "../hooks/useNetworkBadgeCounts";
|
|
27
28
|
import { useNetworkHasEvents, useNetworkHasMatches } from "../hooks/useNetworkHasMatches";
|
|
28
29
|
import { generateNetworkCopyText } from "../utils/generateNetworkCopyText";
|
|
@@ -141,12 +142,16 @@ export function NetworkSavedButton({
|
|
|
141
142
|
* The event object is resolved at PRESS time rather than held as a prop: the
|
|
142
143
|
* live store has the fresh copy while the request is in flight, and the saved
|
|
143
144
|
* store is the only copy once it has been cleared or aged out.
|
|
145
|
+
*
|
|
146
|
+
* The selected id is SUBSCRIBED here rather than passed down, so stepping
|
|
147
|
+
* through requests doesn't have to re-render the modal root just to rebuild
|
|
148
|
+
* the header around a new prop.
|
|
144
149
|
*/
|
|
145
150
|
export function NetworkDetailPinActions({
|
|
146
|
-
eventId,
|
|
147
151
|
isPro,
|
|
148
152
|
onUpgradePress
|
|
149
153
|
}) {
|
|
154
|
+
const eventId = useSelectedNetworkEventId();
|
|
150
155
|
const state = useNetworkSaved();
|
|
151
156
|
const {
|
|
152
157
|
pinned,
|
|
@@ -117,16 +117,21 @@ export function NetworkListScreen({
|
|
|
117
117
|
togglePin,
|
|
118
118
|
clearPinned
|
|
119
119
|
} = useNetworkSaved();
|
|
120
|
-
const {
|
|
121
|
-
pinned,
|
|
122
|
-
rest
|
|
123
|
-
} = splitPinned(visibleEvents, pinnedEvents, pinnedLiveIds);
|
|
124
120
|
|
|
125
121
|
// Stable identity: handed to memoized rows.
|
|
126
122
|
const handleLongPress = useCallback(event => {
|
|
127
123
|
togglePin(event);
|
|
128
124
|
}, [togglePin]);
|
|
125
|
+
|
|
126
|
+
// Everything above is a hook and must run unconditionally; everything below
|
|
127
|
+
// is work. `splitPinned` is an O(n) pass over the whole list, and this screen
|
|
128
|
+
// re-renders on every modal-root state change — including every prev/next
|
|
129
|
+
// step, where it is hidden and the result is thrown away.
|
|
129
130
|
if (!visible) return null;
|
|
131
|
+
const {
|
|
132
|
+
pinned,
|
|
133
|
+
rest
|
|
134
|
+
} = splitPinned(visibleEvents, pinnedEvents, pinnedLiveIds);
|
|
130
135
|
const isEnabled = !paused;
|
|
131
136
|
const renderItem = ({
|
|
132
137
|
item
|