@buoy-gg/network 5.0.0 → 6.0.0
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 +14 -0
- package/lib/commonjs/network/components/NetworkEventItemCompact.js +32 -18
- package/lib/commonjs/network/components/NetworkHeaderLive.js +199 -0
- package/lib/commonjs/network/components/NetworkListScreen.js +203 -0
- package/lib/commonjs/network/components/NetworkModal.js +153 -630
- package/lib/commonjs/network/hooks/useNetworkBadgeCounts.js +83 -0
- package/lib/commonjs/network/hooks/useNetworkEventList.js +93 -0
- package/lib/commonjs/network/hooks/useNetworkHasMatches.js +56 -0
- package/lib/commonjs/network/utils/filterNetworkEvents.js +96 -0
- package/lib/commonjs/network/utils/generateNetworkCopyText.js +139 -0
- package/lib/module/index.js +5 -0
- package/lib/module/network/components/NetworkEventItemCompact.js +32 -18
- package/lib/module/network/components/NetworkHeaderLive.js +193 -0
- package/lib/module/network/components/NetworkListScreen.js +199 -0
- package/lib/module/network/components/NetworkModal.js +157 -634
- package/lib/module/network/hooks/useNetworkBadgeCounts.js +79 -0
- package/lib/module/network/hooks/useNetworkEventList.js +89 -0
- package/lib/module/network/hooks/useNetworkHasMatches.js +52 -0
- package/lib/module/network/utils/filterNetworkEvents.js +92 -0
- package/lib/module/network/utils/generateNetworkCopyText.js +135 -0
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +0 -4
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts +52 -0
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkListScreen.d.ts +27 -0
- package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/hooks/useNetworkBadgeCounts.d.ts +24 -0
- package/lib/typescript/network/hooks/useNetworkBadgeCounts.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkEventList.d.ts +34 -0
- package/lib/typescript/network/hooks/useNetworkEventList.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkHasMatches.d.ts +23 -0
- package/lib/typescript/network/hooks/useNetworkHasMatches.d.ts.map +1 -0
- package/lib/typescript/network/utils/filterNetworkEvents.d.ts +13 -0
- package/lib/typescript/network/utils/filterNetworkEvents.d.ts.map +1 -0
- package/lib/typescript/network/utils/generateNetworkCopyText.d.ts +11 -0
- package/lib/typescript/network/utils/generateNetworkCopyText.d.ts.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useNetworkBadgeCounts = useNetworkBadgeCounts;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _networkEventStore = require("../utils/networkEventStore");
|
|
9
|
+
var _filterNetworkEvents = require("../utils/filterNetworkEvents");
|
|
10
|
+
/**
|
|
11
|
+
* Narrow store subscription for the header's live counters — the ONLY
|
|
12
|
+
* header data that should update per network event (the success/error/
|
|
13
|
+
* pending chips and the clear button's has-events state).
|
|
14
|
+
*
|
|
15
|
+
* Holds derived COUNTS in state, not the events array, with an equality
|
|
16
|
+
* guard — so consumers re-render only when a count actually changes
|
|
17
|
+
* (e.g. an ignored-pattern-matched event arriving doesn't re-render the
|
|
18
|
+
* chips). rule2: external store, narrowest possible subscription.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function computeCounts(events, ignoredPatterns) {
|
|
22
|
+
let successful = 0;
|
|
23
|
+
let failed = 0;
|
|
24
|
+
let pending = 0;
|
|
25
|
+
for (const event of (0, _filterNetworkEvents.applyIgnoredPatterns)(events, ignoredPatterns)) {
|
|
26
|
+
if (event.status && event.status >= 200 && event.status < 300) {
|
|
27
|
+
successful++;
|
|
28
|
+
} else if (event.error || event.status && event.status >= 400) {
|
|
29
|
+
failed++;
|
|
30
|
+
} else if (!event.status && !event.error) {
|
|
31
|
+
pending++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
successful,
|
|
36
|
+
failed,
|
|
37
|
+
pending,
|
|
38
|
+
total: events.length
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function countsEqual(a, b) {
|
|
42
|
+
return a.successful === b.successful && a.failed === b.failed && a.pending === b.pending && a.total === b.total;
|
|
43
|
+
}
|
|
44
|
+
function useNetworkBadgeCounts(options) {
|
|
45
|
+
const {
|
|
46
|
+
ignoredPatterns,
|
|
47
|
+
paused
|
|
48
|
+
} = options;
|
|
49
|
+
const [counts, setCounts] = (0, _react.useState)(() => computeCounts(_networkEventStore.networkEventStore.getEvents(), ignoredPatterns));
|
|
50
|
+
(0, _react.useEffect)(() => {
|
|
51
|
+
if (paused) return undefined;
|
|
52
|
+
// Frame-coalesced like useNetworkEventList: a request burst updates
|
|
53
|
+
// the chips once per frame, not once per emission. subscribeToEvents
|
|
54
|
+
// replays immediately, which also recomputes when ignoredPatterns
|
|
55
|
+
// change (this effect re-runs).
|
|
56
|
+
let rafId = null;
|
|
57
|
+
let pending = null;
|
|
58
|
+
const unsubscribe = _networkEventStore.networkEventStore.subscribeToEvents(events => {
|
|
59
|
+
pending = events;
|
|
60
|
+
if (rafId != null) return;
|
|
61
|
+
rafId = requestAnimationFrame(() => {
|
|
62
|
+
rafId = null;
|
|
63
|
+
if (!pending) return;
|
|
64
|
+
const next = computeCounts(pending, ignoredPatterns);
|
|
65
|
+
pending = null;
|
|
66
|
+
setCounts(prev => countsEqual(prev, next) ? prev : next);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
return () => {
|
|
70
|
+
unsubscribe();
|
|
71
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
72
|
+
};
|
|
73
|
+
}, [paused, ignoredPatterns]);
|
|
74
|
+
(0, _react.useEffect)(() => _networkEventStore.networkEventStore.onClear(() => {
|
|
75
|
+
setCounts({
|
|
76
|
+
successful: 0,
|
|
77
|
+
failed: 0,
|
|
78
|
+
pending: 0,
|
|
79
|
+
total: 0
|
|
80
|
+
});
|
|
81
|
+
}), []);
|
|
82
|
+
return counts;
|
|
83
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useNetworkEventList = useNetworkEventList;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _networkEventStore = require("../utils/networkEventStore");
|
|
9
|
+
var _filterNetworkEvents = require("../utils/filterNetworkEvents");
|
|
10
|
+
var _useNetworkEvents = require("./useNetworkEvents");
|
|
11
|
+
/**
|
|
12
|
+
* Narrow store subscription for components that render the network event
|
|
13
|
+
* LIST (or need the filtered list). Decomposed from `useNetworkEvents`
|
|
14
|
+
* per rule2 ("external store + narrow subscriptions") so the subscription
|
|
15
|
+
* lives in the leaf that consumes it — the modal root no longer
|
|
16
|
+
* re-renders per network event.
|
|
17
|
+
*
|
|
18
|
+
* Profiling evidence (tool-bench, 2026-07-06): with the subscription at
|
|
19
|
+
* the modal root, 15 network events re-rendered the ENTIRE modal 15× —
|
|
20
|
+
* JsModalComponent/header buttons ×15, all visible rows ×105.
|
|
21
|
+
*
|
|
22
|
+
* `paused` gates the subscription (the power toggle's freeze-stream
|
|
23
|
+
* semantic, and screens pass `paused || !visible` so hidden screens don't
|
|
24
|
+
* subscribe at all). Store clears are honored even while paused so a
|
|
25
|
+
* frozen list can't show deleted events (parity with the old hook's
|
|
26
|
+
* clearEvents local reset).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
function useNetworkEventList(options) {
|
|
30
|
+
const {
|
|
31
|
+
filter,
|
|
32
|
+
ignoredPatterns,
|
|
33
|
+
paused,
|
|
34
|
+
isPro
|
|
35
|
+
} = options;
|
|
36
|
+
const [events, setEvents] = (0, _react.useState)(() => _networkEventStore.networkEventStore.getEvents());
|
|
37
|
+
(0, _react.useEffect)(() => {
|
|
38
|
+
if (paused) return undefined;
|
|
39
|
+
// Coalesce store emissions to one commit per frame: a burst of N
|
|
40
|
+
// concurrent requests emits 2N times (add + response each), and
|
|
41
|
+
// uncoalesced each emission is its own commit re-rendering the
|
|
42
|
+
// changed row's whole subtree. Profiling (tool-bench 2026-07-06):
|
|
43
|
+
// a rapid 25-request session produced 70 commits at ~220 renders
|
|
44
|
+
// each. subscribeToEvents replays the current array immediately, so
|
|
45
|
+
// resuming from pause catches up on the next frame.
|
|
46
|
+
let rafId = null;
|
|
47
|
+
let pending = null;
|
|
48
|
+
const unsubscribe = _networkEventStore.networkEventStore.subscribeToEvents(next => {
|
|
49
|
+
pending = next;
|
|
50
|
+
if (rafId != null) return;
|
|
51
|
+
rafId = requestAnimationFrame(() => {
|
|
52
|
+
rafId = null;
|
|
53
|
+
if (pending) setEvents(pending);
|
|
54
|
+
pending = null;
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
unsubscribe();
|
|
59
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
60
|
+
};
|
|
61
|
+
}, [paused]);
|
|
62
|
+
(0, _react.useEffect)(() => _networkEventStore.networkEventStore.onClear(() => {
|
|
63
|
+
setEvents([]);
|
|
64
|
+
}), []);
|
|
65
|
+
|
|
66
|
+
// Derivations are plain useMemo over the subscribed array — linear
|
|
67
|
+
// passes over ≤maxEvents items, recomputed only when inputs change.
|
|
68
|
+
const filteredEvents = (0, _react.useMemo)(() => {
|
|
69
|
+
return (0, _filterNetworkEvents.filterNetworkEvents)((0, _filterNetworkEvents.applyIgnoredPatterns)(events, ignoredPatterns), filter);
|
|
70
|
+
}, [events, ignoredPatterns, filter]);
|
|
71
|
+
|
|
72
|
+
// Free tier: events beyond the limit in the ORIGINAL order are locked,
|
|
73
|
+
// so locked events stay locked even when searching (ported verbatim).
|
|
74
|
+
const lockedEventIds = (0, _react.useMemo)(() => {
|
|
75
|
+
if (isPro) return new Set();
|
|
76
|
+
const lockedIds = new Set();
|
|
77
|
+
events.slice(_useNetworkEvents.FREE_TIER_REQUEST_LIMIT).forEach(event => {
|
|
78
|
+
lockedIds.add(event.id);
|
|
79
|
+
});
|
|
80
|
+
return lockedIds;
|
|
81
|
+
}, [events, isPro]);
|
|
82
|
+
const visibleEvents = (0, _react.useMemo)(() => {
|
|
83
|
+
if (lockedEventIds.size === 0) return filteredEvents;
|
|
84
|
+
return filteredEvents.filter(event => !lockedEventIds.has(event.id));
|
|
85
|
+
}, [filteredEvents, lockedEventIds]);
|
|
86
|
+
const lockedEventCount = filteredEvents.length - visibleEvents.length;
|
|
87
|
+
return {
|
|
88
|
+
events: filteredEvents,
|
|
89
|
+
visibleEvents,
|
|
90
|
+
hasLockedEvents: lockedEventCount > 0,
|
|
91
|
+
lockedEventCount
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useNetworkHasEvents = useNetworkHasEvents;
|
|
7
|
+
exports.useNetworkHasMatches = useNetworkHasMatches;
|
|
8
|
+
var _react = require("react");
|
|
9
|
+
var _networkEventStore = require("../utils/networkEventStore");
|
|
10
|
+
var _filterNetworkEvents = require("../utils/filterNetworkEvents");
|
|
11
|
+
/**
|
|
12
|
+
* Narrowest possible subscription: a BOOLEAN that flips when the store
|
|
13
|
+
* goes between "has events matching this filter" and "empty". Used by
|
|
14
|
+
* header buttons (clear/copy) whose only live dependency is their
|
|
15
|
+
* enabled state — with the full counts hook they re-rendered on every
|
|
16
|
+
* count change (32× in a 25-request run, tool-bench 2026-07-06); with a
|
|
17
|
+
* flip-guarded boolean they render ~twice per session.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** True while the store has ANY events (clear button's enabled state). */
|
|
21
|
+
function useNetworkHasEvents(options) {
|
|
22
|
+
const {
|
|
23
|
+
paused
|
|
24
|
+
} = options;
|
|
25
|
+
const [hasEvents, setHasEvents] = (0, _react.useState)(() => _networkEventStore.networkEventStore.getEvents().length > 0);
|
|
26
|
+
(0, _react.useEffect)(() => {
|
|
27
|
+
if (paused) return undefined;
|
|
28
|
+
return _networkEventStore.networkEventStore.subscribeToEvents(events => {
|
|
29
|
+
const next = events.length > 0;
|
|
30
|
+
setHasEvents(prev => prev === next ? prev : next);
|
|
31
|
+
});
|
|
32
|
+
}, [paused]);
|
|
33
|
+
(0, _react.useEffect)(() => _networkEventStore.networkEventStore.onClear(() => setHasEvents(false)), []);
|
|
34
|
+
return hasEvents;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** True while the store has events matching the current filter (copy
|
|
38
|
+
* button's enabled state). The filter pass runs per emission but a
|
|
39
|
+
* re-render only happens when the boolean flips. */
|
|
40
|
+
function useNetworkHasMatches(options) {
|
|
41
|
+
const {
|
|
42
|
+
filter,
|
|
43
|
+
ignoredPatterns,
|
|
44
|
+
paused
|
|
45
|
+
} = options;
|
|
46
|
+
const [hasMatches, setHasMatches] = (0, _react.useState)(() => (0, _filterNetworkEvents.filterNetworkEvents)((0, _filterNetworkEvents.applyIgnoredPatterns)(_networkEventStore.networkEventStore.getEvents(), ignoredPatterns), filter).length > 0);
|
|
47
|
+
(0, _react.useEffect)(() => {
|
|
48
|
+
if (paused) return undefined;
|
|
49
|
+
return _networkEventStore.networkEventStore.subscribeToEvents(events => {
|
|
50
|
+
const next = (0, _filterNetworkEvents.filterNetworkEvents)((0, _filterNetworkEvents.applyIgnoredPatterns)(events, ignoredPatterns), filter).length > 0;
|
|
51
|
+
setHasMatches(prev => prev === next ? prev : next);
|
|
52
|
+
});
|
|
53
|
+
}, [paused, filter, ignoredPatterns]);
|
|
54
|
+
(0, _react.useEffect)(() => _networkEventStore.networkEventStore.onClear(() => setHasMatches(false)), []);
|
|
55
|
+
return hasMatches;
|
|
56
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.applyIgnoredPatterns = applyIgnoredPatterns;
|
|
7
|
+
exports.filterNetworkEvents = filterNetworkEvents;
|
|
8
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
9
|
+
var _formatGraphQLVariables = require("./formatGraphQLVariables");
|
|
10
|
+
/**
|
|
11
|
+
* Pure network-event filtering shared by the list/copy/filter-screen hooks
|
|
12
|
+
* (rule3: reusable logic extracted to a pure function). Ported verbatim
|
|
13
|
+
* from the filtering previously inlined in `useNetworkEvents` so every
|
|
14
|
+
* subscriber applies identical semantics.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Apply the user's search/method/status/host/content-type filter. */
|
|
18
|
+
function filterNetworkEvents(events, filter) {
|
|
19
|
+
const searchLower = filter.searchText ? filter.searchText.toLowerCase() : null;
|
|
20
|
+
const methodSet = filter.method && filter.method.length > 0 ? new Set(filter.method) : null;
|
|
21
|
+
const contentTypeSet = filter.contentType && filter.contentType.length > 0 ? new Set(filter.contentType) : null;
|
|
22
|
+
let filtered = [...events];
|
|
23
|
+
if (methodSet) {
|
|
24
|
+
filtered = filtered.filter(e => methodSet.has(e.method));
|
|
25
|
+
}
|
|
26
|
+
if (filter.status && filter.status !== "all") {
|
|
27
|
+
switch (filter.status) {
|
|
28
|
+
case "success":
|
|
29
|
+
filtered = filtered.filter(e => e.status && e.status >= 200 && e.status < 300);
|
|
30
|
+
break;
|
|
31
|
+
case "error":
|
|
32
|
+
filtered = filtered.filter(e => e.error || e.status && e.status >= 400);
|
|
33
|
+
break;
|
|
34
|
+
case "pending":
|
|
35
|
+
filtered = filtered.filter(e => !e.status && !e.error);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (searchLower) {
|
|
40
|
+
filtered = filtered.filter(e => e.url.toLowerCase().includes(searchLower) || e.method.toLowerCase().includes(searchLower) || e.path?.toLowerCase().includes(searchLower) || e.host?.toLowerCase().includes(searchLower) || e.error && e.error.toLowerCase().includes(searchLower) ||
|
|
41
|
+
// Search by GraphQL operation name (e.g., "GetUser", "CreatePost")
|
|
42
|
+
e.operationName && e.operationName.toLowerCase().includes(searchLower) ||
|
|
43
|
+
// Search by GraphQL variable values (e.g., "Sandshrew", "123")
|
|
44
|
+
(0, _formatGraphQLVariables.searchGraphQLVariables)(e.graphqlVariables, searchLower));
|
|
45
|
+
}
|
|
46
|
+
if (filter.host) {
|
|
47
|
+
filtered = filtered.filter(e => e.host === filter.host);
|
|
48
|
+
}
|
|
49
|
+
if (contentTypeSet) {
|
|
50
|
+
filtered = filtered.filter(e => {
|
|
51
|
+
const headers = e.responseHeaders || e.requestHeaders;
|
|
52
|
+
const contentType = headers?.["content-type"] || headers?.["Content-Type"] || "";
|
|
53
|
+
for (const type of contentTypeSet) {
|
|
54
|
+
switch (type) {
|
|
55
|
+
case "JSON":
|
|
56
|
+
if (contentType.includes("json")) return true;
|
|
57
|
+
break;
|
|
58
|
+
case "XML":
|
|
59
|
+
if (contentType.includes("xml")) return true;
|
|
60
|
+
break;
|
|
61
|
+
case "HTML":
|
|
62
|
+
if (contentType.includes("html")) return true;
|
|
63
|
+
break;
|
|
64
|
+
case "TEXT":
|
|
65
|
+
if (contentType.includes("text")) return true;
|
|
66
|
+
break;
|
|
67
|
+
case "IMAGE":
|
|
68
|
+
if (contentType.includes("image")) return true;
|
|
69
|
+
break;
|
|
70
|
+
case "VIDEO":
|
|
71
|
+
if (contentType.includes("video")) return true;
|
|
72
|
+
break;
|
|
73
|
+
case "AUDIO":
|
|
74
|
+
if (contentType.includes("audio")) return true;
|
|
75
|
+
break;
|
|
76
|
+
case "FORM":
|
|
77
|
+
if (contentType.includes("form")) return true;
|
|
78
|
+
break;
|
|
79
|
+
case "OTHER":
|
|
80
|
+
if (!contentType || !contentType.includes("json") && !contentType.includes("xml") && !contentType.includes("html") && !contentType.includes("text") && !contentType.includes("image") && !contentType.includes("video") && !contentType.includes("audio") && !contentType.includes("form")) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return filtered;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Drop events matching the shared ignored-domain/URL patterns. */
|
|
93
|
+
function applyIgnoredPatterns(events, ignoredPatterns) {
|
|
94
|
+
if (ignoredPatterns.length === 0) return events;
|
|
95
|
+
return events.filter(event => !ignoredPatterns.some(pattern => (0, _sharedUi.urlMatchesIgnoredPattern)(event.url, pattern)));
|
|
96
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.generateNetworkCopyText = generateNetworkCopyText;
|
|
7
|
+
var _formatting = require("./formatting");
|
|
8
|
+
/**
|
|
9
|
+
* Pure copy-text generation for the network tool (rule3: reusable logic
|
|
10
|
+
* extracted from the modal). Ported verbatim from the `generateCopyText`
|
|
11
|
+
* callback previously inlined in NetworkModal — kept pure so the header
|
|
12
|
+
* CopyButton can resolve it LAZILY at copy time (CopyButton accepts a
|
|
13
|
+
* function value) instead of rebuilding the string on every event.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function shouldIncludePayload(data, bodySizeThresholdKb) {
|
|
17
|
+
if (bodySizeThresholdKb === -1) return true;
|
|
18
|
+
if (!data) return true;
|
|
19
|
+
try {
|
|
20
|
+
const size = JSON.stringify(data).length;
|
|
21
|
+
return size < bodySizeThresholdKb * 1024;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function generateNetworkCopyText(events, copySettings) {
|
|
27
|
+
let eventsToUse = events;
|
|
28
|
+
|
|
29
|
+
// Apply filter mode
|
|
30
|
+
if (copySettings.filterMode === "failed") {
|
|
31
|
+
eventsToUse = eventsToUse.filter(e => e.error || e.status && e.status >= 400);
|
|
32
|
+
} else if (copySettings.filterMode === "success") {
|
|
33
|
+
eventsToUse = eventsToUse.filter(e => e.status && e.status >= 200 && e.status < 300);
|
|
34
|
+
}
|
|
35
|
+
if (eventsToUse.length === 0) {
|
|
36
|
+
return "No requests to copy";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Plain text format (URLs only)
|
|
40
|
+
if (copySettings.format === "plaintext") {
|
|
41
|
+
return eventsToUse.map(event => `${event.method} ${event.url}`).join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// JSON format
|
|
45
|
+
if (copySettings.format === "json") {
|
|
46
|
+
const requests = eventsToUse.map(event => {
|
|
47
|
+
const obj = {};
|
|
48
|
+
if (copySettings.includeMethod) {
|
|
49
|
+
obj.method = event.method;
|
|
50
|
+
obj.url = event.url;
|
|
51
|
+
}
|
|
52
|
+
if (copySettings.includeStatus) {
|
|
53
|
+
obj.status = event.status;
|
|
54
|
+
obj.statusText = event.statusText;
|
|
55
|
+
}
|
|
56
|
+
if (copySettings.includeDuration) obj.duration = event.duration;
|
|
57
|
+
if (copySettings.includeTimestamp) obj.timestamp = event.timestamp;
|
|
58
|
+
if (copySettings.includeClient) obj.requestClient = event.requestClient;
|
|
59
|
+
if (copySettings.includeSizes) {
|
|
60
|
+
obj.requestSize = event.requestSize;
|
|
61
|
+
obj.responseSize = event.responseSize;
|
|
62
|
+
}
|
|
63
|
+
if (copySettings.includeErrors && event.error) obj.error = event.error;
|
|
64
|
+
if (copySettings.includeRequestHeaders) {
|
|
65
|
+
obj.requestHeaders = event.requestHeaders;
|
|
66
|
+
}
|
|
67
|
+
if (copySettings.includeResponseHeaders) {
|
|
68
|
+
obj.responseHeaders = event.responseHeaders;
|
|
69
|
+
}
|
|
70
|
+
if (copySettings.includeRequestBody && shouldIncludePayload(event.requestData, copySettings.bodySizeThreshold)) {
|
|
71
|
+
obj.requestData = event.requestData;
|
|
72
|
+
}
|
|
73
|
+
if (copySettings.includeResponseBody && shouldIncludePayload(event.responseData, copySettings.bodySizeThreshold)) {
|
|
74
|
+
obj.responseData = event.responseData;
|
|
75
|
+
}
|
|
76
|
+
return obj;
|
|
77
|
+
});
|
|
78
|
+
return JSON.stringify(requests, null, 2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Markdown format
|
|
82
|
+
const requests = eventsToUse.map((event, index) => {
|
|
83
|
+
let md = `# Request ${index + 1}\n\n`;
|
|
84
|
+
if (copySettings.includeMethod) {
|
|
85
|
+
md += `**Method:** ${event.method}\n`;
|
|
86
|
+
md += `**URL:** ${event.url}\n`;
|
|
87
|
+
}
|
|
88
|
+
if (copySettings.includeStatus) {
|
|
89
|
+
const status = event.status || "Pending";
|
|
90
|
+
md += `**Status:** ${status}${event.statusText ? ` (${event.statusText})` : ""}\n`;
|
|
91
|
+
}
|
|
92
|
+
if (copySettings.includeClient && event.requestClient) {
|
|
93
|
+
md += `**Client:** ${event.requestClient}\n`;
|
|
94
|
+
}
|
|
95
|
+
if (copySettings.includeDuration && event.duration) {
|
|
96
|
+
md += `**Duration:** ${event.duration}ms\n`;
|
|
97
|
+
}
|
|
98
|
+
if (copySettings.includeTimestamp) {
|
|
99
|
+
md += `**Timestamp:** ${new Date(event.timestamp).toISOString()}\n`;
|
|
100
|
+
}
|
|
101
|
+
if (copySettings.includeSizes) {
|
|
102
|
+
const reqSize = event.requestSize ? (0, _formatting.formatBytes)(event.requestSize) : "N/A";
|
|
103
|
+
const resSize = event.responseSize ? (0, _formatting.formatBytes)(event.responseSize) : "N/A";
|
|
104
|
+
md += `**Request Size:** ${reqSize}\n`;
|
|
105
|
+
md += `**Response Size:** ${resSize}\n`;
|
|
106
|
+
}
|
|
107
|
+
if (copySettings.includeErrors && event.error) {
|
|
108
|
+
md += `**Error:** ${event.error}\n`;
|
|
109
|
+
}
|
|
110
|
+
if (copySettings.includeRequestHeaders && Object.keys(event.requestHeaders).length > 0) {
|
|
111
|
+
md += `\n## Request Headers\n\`\`\`json\n${JSON.stringify(event.requestHeaders, null, 2)}\n\`\`\`\n`;
|
|
112
|
+
}
|
|
113
|
+
if (copySettings.includeRequestBody) {
|
|
114
|
+
md += `\n## Request Data\n`;
|
|
115
|
+
if (shouldIncludePayload(event.requestData, copySettings.bodySizeThreshold)) {
|
|
116
|
+
md += `\`\`\`json\n${JSON.stringify(event.requestData, null, 2)}\n\`\`\`\n`;
|
|
117
|
+
} else {
|
|
118
|
+
const size = event.requestSize ? (0, _formatting.formatBytes)(event.requestSize) : "Unknown size";
|
|
119
|
+
md += `_Payload omitted (${size}). Exceeds ${copySettings.bodySizeThreshold}KB threshold._\n`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (copySettings.includeResponseHeaders && Object.keys(event.responseHeaders).length > 0) {
|
|
123
|
+
md += `\n## Response Headers\n\`\`\`json\n${JSON.stringify(event.responseHeaders, null, 2)}\n\`\`\`\n`;
|
|
124
|
+
}
|
|
125
|
+
if (copySettings.includeResponseBody) {
|
|
126
|
+
md += `\n## Response Data\n`;
|
|
127
|
+
if (shouldIncludePayload(event.responseData, copySettings.bodySizeThreshold)) {
|
|
128
|
+
md += `\`\`\`json\n${JSON.stringify(event.responseData, null, 2)}\n\`\`\`\n`;
|
|
129
|
+
} else {
|
|
130
|
+
const size = event.responseSize ? (0, _formatting.formatBytes)(event.responseSize) : "Unknown size";
|
|
131
|
+
md += `_Payload omitted (${size}). Exceeds ${copySettings.bodySizeThreshold}KB threshold._\n`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
md += `\n---\n`;
|
|
135
|
+
return md;
|
|
136
|
+
}).join("\n");
|
|
137
|
+
const header = `# Network Requests (${eventsToUse.length} total)\n\n`;
|
|
138
|
+
return header + requests;
|
|
139
|
+
}
|
package/lib/module/index.js
CHANGED
|
@@ -26,6 +26,11 @@ export { NetworkEventItemCompact } from "./network/components/NetworkEventItemCo
|
|
|
26
26
|
// HOOKS (For consuming network data in custom components)
|
|
27
27
|
// =============================================================================
|
|
28
28
|
export { useNetworkEvents } from "./network/hooks/useNetworkEvents";
|
|
29
|
+
// Narrow per-consumer subscriptions (what NetworkModal itself uses) — the
|
|
30
|
+
// list and the header counters subscribe independently so a host UI never
|
|
31
|
+
// re-renders wholesale per network event.
|
|
32
|
+
export { useNetworkEventList } from "./network/hooks/useNetworkEventList";
|
|
33
|
+
export { useNetworkBadgeCounts } from "./network/hooks/useNetworkBadgeCounts";
|
|
29
34
|
export { TickProvider, useTickEveryMinute } from "./network/hooks/useTickEveryMinute";
|
|
30
35
|
|
|
31
36
|
// =============================================================================
|
|
@@ -109,11 +109,33 @@ function SizeIndicators({
|
|
|
109
109
|
* Compact list-row representation of a network event. Optimized for large lists with memoization
|
|
110
110
|
* and periodic refresh via `useTickEveryMinute` to keep relative timestamps accurate.
|
|
111
111
|
*/
|
|
112
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Sole tick-context consumer in the row (rule2: composition over memo) —
|
|
114
|
+
* the periodic relative-timestamp tick re-renders ONLY this Text, not the
|
|
115
|
+
* whole card. Profiling (tool-bench 2026-07-06): the tick accounted for
|
|
116
|
+
* ~30 of 65 row renders (plus their SVG-icon churn) in a 12s recording.
|
|
117
|
+
*/
|
|
118
|
+
function BottomRightTime({
|
|
119
|
+
timestamp,
|
|
120
|
+
duration
|
|
121
|
+
}) {
|
|
122
|
+
const tick = useTickEveryMinute();
|
|
123
|
+
const relativeTime = formatRelativeTime(timestamp, tick);
|
|
124
|
+
return duration ? /*#__PURE__*/_jsxs(Text, {
|
|
125
|
+
style: styles.bottomRightText,
|
|
126
|
+
children: [formatDuration(duration), " ", relativeTime]
|
|
127
|
+
}) : /*#__PURE__*/_jsx(Text, {
|
|
128
|
+
style: styles.bottomRightText,
|
|
129
|
+
children: relativeTime
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export const NetworkEventItemCompact = /*#__PURE__*/memo(
|
|
133
|
+
// Named function (not an arrow) so render-capture reports attribute row
|
|
134
|
+
// renders to this component instead of a compiled anonymous name.
|
|
135
|
+
function NetworkEventItemCompact({
|
|
113
136
|
event,
|
|
114
137
|
onPress
|
|
115
|
-
})
|
|
116
|
-
const tick = useTickEveryMinute();
|
|
138
|
+
}) {
|
|
117
139
|
const statusColor = getStatusColor(event.status, event.error);
|
|
118
140
|
const isPending = !event.status && !event.error;
|
|
119
141
|
const contentType = getContentTypeBadge(event.responseHeaders);
|
|
@@ -135,15 +157,6 @@ export const NetworkEventItemCompact = /*#__PURE__*/memo(({
|
|
|
135
157
|
// For non-GraphQL requests with operation names (e.g., gRPC)
|
|
136
158
|
displayUrl = `${displayUrl}\n(${event.operationName})`;
|
|
137
159
|
}
|
|
138
|
-
|
|
139
|
-
// Format time with both absolute and relative
|
|
140
|
-
const timeString = new Date(event.timestamp).toLocaleTimeString("en-US", {
|
|
141
|
-
hour: "numeric",
|
|
142
|
-
minute: "2-digit",
|
|
143
|
-
second: "2-digit",
|
|
144
|
-
hour12: true
|
|
145
|
-
});
|
|
146
|
-
const relativeTime = formatRelativeTime(event.timestamp, tick);
|
|
147
160
|
return /*#__PURE__*/_jsxs(DevToolsCard, {
|
|
148
161
|
onPress: () => onPress(event),
|
|
149
162
|
style: styles.cardWrapper,
|
|
@@ -194,16 +207,17 @@ export const NetworkEventItemCompact = /*#__PURE__*/memo(({
|
|
|
194
207
|
})]
|
|
195
208
|
}), /*#__PURE__*/_jsx(View, {
|
|
196
209
|
style: styles.bottomRightContainer,
|
|
197
|
-
children:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}) : /*#__PURE__*/_jsx(Text, {
|
|
201
|
-
style: styles.bottomRightText,
|
|
202
|
-
children: relativeTime
|
|
210
|
+
children: /*#__PURE__*/_jsx(BottomRightTime, {
|
|
211
|
+
timestamp: event.timestamp,
|
|
212
|
+
duration: event.duration
|
|
203
213
|
})
|
|
204
214
|
})]
|
|
205
215
|
});
|
|
206
216
|
});
|
|
217
|
+
|
|
218
|
+
// Without this, render-capture reports attribute row renders to the
|
|
219
|
+
// anonymous memo wrapper ("Memo"/"_c4") instead of the row.
|
|
220
|
+
NetworkEventItemCompact.displayName = "NetworkEventItemCompact";
|
|
207
221
|
const styles = StyleSheet.create({
|
|
208
222
|
cardWrapper: {
|
|
209
223
|
marginHorizontal: 8
|