@buoy-gg/network 4.0.2 → 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,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Narrow store subscription for the header's live counters — the ONLY
|
|
5
|
+
* header data that should update per network event (the success/error/
|
|
6
|
+
* pending chips and the clear button's has-events state).
|
|
7
|
+
*
|
|
8
|
+
* Holds derived COUNTS in state, not the events array, with an equality
|
|
9
|
+
* guard — so consumers re-render only when a count actually changes
|
|
10
|
+
* (e.g. an ignored-pattern-matched event arriving doesn't re-render the
|
|
11
|
+
* chips). rule2: external store, narrowest possible subscription.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { networkEventStore } from "../utils/networkEventStore";
|
|
16
|
+
import { applyIgnoredPatterns } from "../utils/filterNetworkEvents";
|
|
17
|
+
function computeCounts(events, ignoredPatterns) {
|
|
18
|
+
let successful = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
let pending = 0;
|
|
21
|
+
for (const event of applyIgnoredPatterns(events, ignoredPatterns)) {
|
|
22
|
+
if (event.status && event.status >= 200 && event.status < 300) {
|
|
23
|
+
successful++;
|
|
24
|
+
} else if (event.error || event.status && event.status >= 400) {
|
|
25
|
+
failed++;
|
|
26
|
+
} else if (!event.status && !event.error) {
|
|
27
|
+
pending++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
successful,
|
|
32
|
+
failed,
|
|
33
|
+
pending,
|
|
34
|
+
total: events.length
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function countsEqual(a, b) {
|
|
38
|
+
return a.successful === b.successful && a.failed === b.failed && a.pending === b.pending && a.total === b.total;
|
|
39
|
+
}
|
|
40
|
+
export function useNetworkBadgeCounts(options) {
|
|
41
|
+
const {
|
|
42
|
+
ignoredPatterns,
|
|
43
|
+
paused
|
|
44
|
+
} = options;
|
|
45
|
+
const [counts, setCounts] = useState(() => computeCounts(networkEventStore.getEvents(), ignoredPatterns));
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (paused) return undefined;
|
|
48
|
+
// Frame-coalesced like useNetworkEventList: a request burst updates
|
|
49
|
+
// the chips once per frame, not once per emission. subscribeToEvents
|
|
50
|
+
// replays immediately, which also recomputes when ignoredPatterns
|
|
51
|
+
// change (this effect re-runs).
|
|
52
|
+
let rafId = null;
|
|
53
|
+
let pending = null;
|
|
54
|
+
const unsubscribe = networkEventStore.subscribeToEvents(events => {
|
|
55
|
+
pending = events;
|
|
56
|
+
if (rafId != null) return;
|
|
57
|
+
rafId = requestAnimationFrame(() => {
|
|
58
|
+
rafId = null;
|
|
59
|
+
if (!pending) return;
|
|
60
|
+
const next = computeCounts(pending, ignoredPatterns);
|
|
61
|
+
pending = null;
|
|
62
|
+
setCounts(prev => countsEqual(prev, next) ? prev : next);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return () => {
|
|
66
|
+
unsubscribe();
|
|
67
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
68
|
+
};
|
|
69
|
+
}, [paused, ignoredPatterns]);
|
|
70
|
+
useEffect(() => networkEventStore.onClear(() => {
|
|
71
|
+
setCounts({
|
|
72
|
+
successful: 0,
|
|
73
|
+
failed: 0,
|
|
74
|
+
pending: 0,
|
|
75
|
+
total: 0
|
|
76
|
+
});
|
|
77
|
+
}), []);
|
|
78
|
+
return counts;
|
|
79
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Narrow store subscription for components that render the network event
|
|
5
|
+
* LIST (or need the filtered list). Decomposed from `useNetworkEvents`
|
|
6
|
+
* per rule2 ("external store + narrow subscriptions") so the subscription
|
|
7
|
+
* lives in the leaf that consumes it — the modal root no longer
|
|
8
|
+
* re-renders per network event.
|
|
9
|
+
*
|
|
10
|
+
* Profiling evidence (tool-bench, 2026-07-06): with the subscription at
|
|
11
|
+
* the modal root, 15 network events re-rendered the ENTIRE modal 15× —
|
|
12
|
+
* JsModalComponent/header buttons ×15, all visible rows ×105.
|
|
13
|
+
*
|
|
14
|
+
* `paused` gates the subscription (the power toggle's freeze-stream
|
|
15
|
+
* semantic, and screens pass `paused || !visible` so hidden screens don't
|
|
16
|
+
* subscribe at all). Store clears are honored even while paused so a
|
|
17
|
+
* frozen list can't show deleted events (parity with the old hook's
|
|
18
|
+
* clearEvents local reset).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useEffect, useMemo, useState } from "react";
|
|
22
|
+
import { networkEventStore } from "../utils/networkEventStore";
|
|
23
|
+
import { applyIgnoredPatterns, filterNetworkEvents } from "../utils/filterNetworkEvents";
|
|
24
|
+
import { FREE_TIER_REQUEST_LIMIT } from "./useNetworkEvents";
|
|
25
|
+
export function useNetworkEventList(options) {
|
|
26
|
+
const {
|
|
27
|
+
filter,
|
|
28
|
+
ignoredPatterns,
|
|
29
|
+
paused,
|
|
30
|
+
isPro
|
|
31
|
+
} = options;
|
|
32
|
+
const [events, setEvents] = useState(() => networkEventStore.getEvents());
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (paused) return undefined;
|
|
35
|
+
// Coalesce store emissions to one commit per frame: a burst of N
|
|
36
|
+
// concurrent requests emits 2N times (add + response each), and
|
|
37
|
+
// uncoalesced each emission is its own commit re-rendering the
|
|
38
|
+
// changed row's whole subtree. Profiling (tool-bench 2026-07-06):
|
|
39
|
+
// a rapid 25-request session produced 70 commits at ~220 renders
|
|
40
|
+
// each. subscribeToEvents replays the current array immediately, so
|
|
41
|
+
// resuming from pause catches up on the next frame.
|
|
42
|
+
let rafId = null;
|
|
43
|
+
let pending = null;
|
|
44
|
+
const unsubscribe = networkEventStore.subscribeToEvents(next => {
|
|
45
|
+
pending = next;
|
|
46
|
+
if (rafId != null) return;
|
|
47
|
+
rafId = requestAnimationFrame(() => {
|
|
48
|
+
rafId = null;
|
|
49
|
+
if (pending) setEvents(pending);
|
|
50
|
+
pending = null;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return () => {
|
|
54
|
+
unsubscribe();
|
|
55
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
56
|
+
};
|
|
57
|
+
}, [paused]);
|
|
58
|
+
useEffect(() => networkEventStore.onClear(() => {
|
|
59
|
+
setEvents([]);
|
|
60
|
+
}), []);
|
|
61
|
+
|
|
62
|
+
// Derivations are plain useMemo over the subscribed array — linear
|
|
63
|
+
// passes over ≤maxEvents items, recomputed only when inputs change.
|
|
64
|
+
const filteredEvents = useMemo(() => {
|
|
65
|
+
return filterNetworkEvents(applyIgnoredPatterns(events, ignoredPatterns), filter);
|
|
66
|
+
}, [events, ignoredPatterns, filter]);
|
|
67
|
+
|
|
68
|
+
// Free tier: events beyond the limit in the ORIGINAL order are locked,
|
|
69
|
+
// so locked events stay locked even when searching (ported verbatim).
|
|
70
|
+
const lockedEventIds = useMemo(() => {
|
|
71
|
+
if (isPro) return new Set();
|
|
72
|
+
const lockedIds = new Set();
|
|
73
|
+
events.slice(FREE_TIER_REQUEST_LIMIT).forEach(event => {
|
|
74
|
+
lockedIds.add(event.id);
|
|
75
|
+
});
|
|
76
|
+
return lockedIds;
|
|
77
|
+
}, [events, isPro]);
|
|
78
|
+
const visibleEvents = useMemo(() => {
|
|
79
|
+
if (lockedEventIds.size === 0) return filteredEvents;
|
|
80
|
+
return filteredEvents.filter(event => !lockedEventIds.has(event.id));
|
|
81
|
+
}, [filteredEvents, lockedEventIds]);
|
|
82
|
+
const lockedEventCount = filteredEvents.length - visibleEvents.length;
|
|
83
|
+
return {
|
|
84
|
+
events: filteredEvents,
|
|
85
|
+
visibleEvents,
|
|
86
|
+
hasLockedEvents: lockedEventCount > 0,
|
|
87
|
+
lockedEventCount
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Narrowest possible subscription: a BOOLEAN that flips when the store
|
|
5
|
+
* goes between "has events matching this filter" and "empty". Used by
|
|
6
|
+
* header buttons (clear/copy) whose only live dependency is their
|
|
7
|
+
* enabled state — with the full counts hook they re-rendered on every
|
|
8
|
+
* count change (32× in a 25-request run, tool-bench 2026-07-06); with a
|
|
9
|
+
* flip-guarded boolean they render ~twice per session.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
13
|
+
import { networkEventStore } from "../utils/networkEventStore";
|
|
14
|
+
import { applyIgnoredPatterns, filterNetworkEvents } from "../utils/filterNetworkEvents";
|
|
15
|
+
|
|
16
|
+
/** True while the store has ANY events (clear button's enabled state). */
|
|
17
|
+
export function useNetworkHasEvents(options) {
|
|
18
|
+
const {
|
|
19
|
+
paused
|
|
20
|
+
} = options;
|
|
21
|
+
const [hasEvents, setHasEvents] = useState(() => networkEventStore.getEvents().length > 0);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (paused) return undefined;
|
|
24
|
+
return networkEventStore.subscribeToEvents(events => {
|
|
25
|
+
const next = events.length > 0;
|
|
26
|
+
setHasEvents(prev => prev === next ? prev : next);
|
|
27
|
+
});
|
|
28
|
+
}, [paused]);
|
|
29
|
+
useEffect(() => networkEventStore.onClear(() => setHasEvents(false)), []);
|
|
30
|
+
return hasEvents;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True while the store has events matching the current filter (copy
|
|
34
|
+
* button's enabled state). The filter pass runs per emission but a
|
|
35
|
+
* re-render only happens when the boolean flips. */
|
|
36
|
+
export function useNetworkHasMatches(options) {
|
|
37
|
+
const {
|
|
38
|
+
filter,
|
|
39
|
+
ignoredPatterns,
|
|
40
|
+
paused
|
|
41
|
+
} = options;
|
|
42
|
+
const [hasMatches, setHasMatches] = useState(() => filterNetworkEvents(applyIgnoredPatterns(networkEventStore.getEvents(), ignoredPatterns), filter).length > 0);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (paused) return undefined;
|
|
45
|
+
return networkEventStore.subscribeToEvents(events => {
|
|
46
|
+
const next = filterNetworkEvents(applyIgnoredPatterns(events, ignoredPatterns), filter).length > 0;
|
|
47
|
+
setHasMatches(prev => prev === next ? prev : next);
|
|
48
|
+
});
|
|
49
|
+
}, [paused, filter, ignoredPatterns]);
|
|
50
|
+
useEffect(() => networkEventStore.onClear(() => setHasMatches(false)), []);
|
|
51
|
+
return hasMatches;
|
|
52
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure network-event filtering shared by the list/copy/filter-screen hooks
|
|
5
|
+
* (rule3: reusable logic extracted to a pure function). Ported verbatim
|
|
6
|
+
* from the filtering previously inlined in `useNetworkEvents` so every
|
|
7
|
+
* subscriber applies identical semantics.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { urlMatchesIgnoredPattern } from "@buoy-gg/shared-ui";
|
|
11
|
+
import { searchGraphQLVariables } from "./formatGraphQLVariables";
|
|
12
|
+
|
|
13
|
+
/** Apply the user's search/method/status/host/content-type filter. */
|
|
14
|
+
export function filterNetworkEvents(events, filter) {
|
|
15
|
+
const searchLower = filter.searchText ? filter.searchText.toLowerCase() : null;
|
|
16
|
+
const methodSet = filter.method && filter.method.length > 0 ? new Set(filter.method) : null;
|
|
17
|
+
const contentTypeSet = filter.contentType && filter.contentType.length > 0 ? new Set(filter.contentType) : null;
|
|
18
|
+
let filtered = [...events];
|
|
19
|
+
if (methodSet) {
|
|
20
|
+
filtered = filtered.filter(e => methodSet.has(e.method));
|
|
21
|
+
}
|
|
22
|
+
if (filter.status && filter.status !== "all") {
|
|
23
|
+
switch (filter.status) {
|
|
24
|
+
case "success":
|
|
25
|
+
filtered = filtered.filter(e => e.status && e.status >= 200 && e.status < 300);
|
|
26
|
+
break;
|
|
27
|
+
case "error":
|
|
28
|
+
filtered = filtered.filter(e => e.error || e.status && e.status >= 400);
|
|
29
|
+
break;
|
|
30
|
+
case "pending":
|
|
31
|
+
filtered = filtered.filter(e => !e.status && !e.error);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (searchLower) {
|
|
36
|
+
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) ||
|
|
37
|
+
// Search by GraphQL operation name (e.g., "GetUser", "CreatePost")
|
|
38
|
+
e.operationName && e.operationName.toLowerCase().includes(searchLower) ||
|
|
39
|
+
// Search by GraphQL variable values (e.g., "Sandshrew", "123")
|
|
40
|
+
searchGraphQLVariables(e.graphqlVariables, searchLower));
|
|
41
|
+
}
|
|
42
|
+
if (filter.host) {
|
|
43
|
+
filtered = filtered.filter(e => e.host === filter.host);
|
|
44
|
+
}
|
|
45
|
+
if (contentTypeSet) {
|
|
46
|
+
filtered = filtered.filter(e => {
|
|
47
|
+
const headers = e.responseHeaders || e.requestHeaders;
|
|
48
|
+
const contentType = headers?.["content-type"] || headers?.["Content-Type"] || "";
|
|
49
|
+
for (const type of contentTypeSet) {
|
|
50
|
+
switch (type) {
|
|
51
|
+
case "JSON":
|
|
52
|
+
if (contentType.includes("json")) return true;
|
|
53
|
+
break;
|
|
54
|
+
case "XML":
|
|
55
|
+
if (contentType.includes("xml")) return true;
|
|
56
|
+
break;
|
|
57
|
+
case "HTML":
|
|
58
|
+
if (contentType.includes("html")) return true;
|
|
59
|
+
break;
|
|
60
|
+
case "TEXT":
|
|
61
|
+
if (contentType.includes("text")) return true;
|
|
62
|
+
break;
|
|
63
|
+
case "IMAGE":
|
|
64
|
+
if (contentType.includes("image")) return true;
|
|
65
|
+
break;
|
|
66
|
+
case "VIDEO":
|
|
67
|
+
if (contentType.includes("video")) return true;
|
|
68
|
+
break;
|
|
69
|
+
case "AUDIO":
|
|
70
|
+
if (contentType.includes("audio")) return true;
|
|
71
|
+
break;
|
|
72
|
+
case "FORM":
|
|
73
|
+
if (contentType.includes("form")) return true;
|
|
74
|
+
break;
|
|
75
|
+
case "OTHER":
|
|
76
|
+
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")) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return filtered;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Drop events matching the shared ignored-domain/URL patterns. */
|
|
89
|
+
export function applyIgnoredPatterns(events, ignoredPatterns) {
|
|
90
|
+
if (ignoredPatterns.length === 0) return events;
|
|
91
|
+
return events.filter(event => !ignoredPatterns.some(pattern => urlMatchesIgnoredPattern(event.url, pattern)));
|
|
92
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure copy-text generation for the network tool (rule3: reusable logic
|
|
5
|
+
* extracted from the modal). Ported verbatim from the `generateCopyText`
|
|
6
|
+
* callback previously inlined in NetworkModal — kept pure so the header
|
|
7
|
+
* CopyButton can resolve it LAZILY at copy time (CopyButton accepts a
|
|
8
|
+
* function value) instead of rebuilding the string on every event.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { formatBytes } from "./formatting";
|
|
12
|
+
function shouldIncludePayload(data, bodySizeThresholdKb) {
|
|
13
|
+
if (bodySizeThresholdKb === -1) return true;
|
|
14
|
+
if (!data) return true;
|
|
15
|
+
try {
|
|
16
|
+
const size = JSON.stringify(data).length;
|
|
17
|
+
return size < bodySizeThresholdKb * 1024;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function generateNetworkCopyText(events, copySettings) {
|
|
23
|
+
let eventsToUse = events;
|
|
24
|
+
|
|
25
|
+
// Apply filter mode
|
|
26
|
+
if (copySettings.filterMode === "failed") {
|
|
27
|
+
eventsToUse = eventsToUse.filter(e => e.error || e.status && e.status >= 400);
|
|
28
|
+
} else if (copySettings.filterMode === "success") {
|
|
29
|
+
eventsToUse = eventsToUse.filter(e => e.status && e.status >= 200 && e.status < 300);
|
|
30
|
+
}
|
|
31
|
+
if (eventsToUse.length === 0) {
|
|
32
|
+
return "No requests to copy";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Plain text format (URLs only)
|
|
36
|
+
if (copySettings.format === "plaintext") {
|
|
37
|
+
return eventsToUse.map(event => `${event.method} ${event.url}`).join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// JSON format
|
|
41
|
+
if (copySettings.format === "json") {
|
|
42
|
+
const requests = eventsToUse.map(event => {
|
|
43
|
+
const obj = {};
|
|
44
|
+
if (copySettings.includeMethod) {
|
|
45
|
+
obj.method = event.method;
|
|
46
|
+
obj.url = event.url;
|
|
47
|
+
}
|
|
48
|
+
if (copySettings.includeStatus) {
|
|
49
|
+
obj.status = event.status;
|
|
50
|
+
obj.statusText = event.statusText;
|
|
51
|
+
}
|
|
52
|
+
if (copySettings.includeDuration) obj.duration = event.duration;
|
|
53
|
+
if (copySettings.includeTimestamp) obj.timestamp = event.timestamp;
|
|
54
|
+
if (copySettings.includeClient) obj.requestClient = event.requestClient;
|
|
55
|
+
if (copySettings.includeSizes) {
|
|
56
|
+
obj.requestSize = event.requestSize;
|
|
57
|
+
obj.responseSize = event.responseSize;
|
|
58
|
+
}
|
|
59
|
+
if (copySettings.includeErrors && event.error) obj.error = event.error;
|
|
60
|
+
if (copySettings.includeRequestHeaders) {
|
|
61
|
+
obj.requestHeaders = event.requestHeaders;
|
|
62
|
+
}
|
|
63
|
+
if (copySettings.includeResponseHeaders) {
|
|
64
|
+
obj.responseHeaders = event.responseHeaders;
|
|
65
|
+
}
|
|
66
|
+
if (copySettings.includeRequestBody && shouldIncludePayload(event.requestData, copySettings.bodySizeThreshold)) {
|
|
67
|
+
obj.requestData = event.requestData;
|
|
68
|
+
}
|
|
69
|
+
if (copySettings.includeResponseBody && shouldIncludePayload(event.responseData, copySettings.bodySizeThreshold)) {
|
|
70
|
+
obj.responseData = event.responseData;
|
|
71
|
+
}
|
|
72
|
+
return obj;
|
|
73
|
+
});
|
|
74
|
+
return JSON.stringify(requests, null, 2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Markdown format
|
|
78
|
+
const requests = eventsToUse.map((event, index) => {
|
|
79
|
+
let md = `# Request ${index + 1}\n\n`;
|
|
80
|
+
if (copySettings.includeMethod) {
|
|
81
|
+
md += `**Method:** ${event.method}\n`;
|
|
82
|
+
md += `**URL:** ${event.url}\n`;
|
|
83
|
+
}
|
|
84
|
+
if (copySettings.includeStatus) {
|
|
85
|
+
const status = event.status || "Pending";
|
|
86
|
+
md += `**Status:** ${status}${event.statusText ? ` (${event.statusText})` : ""}\n`;
|
|
87
|
+
}
|
|
88
|
+
if (copySettings.includeClient && event.requestClient) {
|
|
89
|
+
md += `**Client:** ${event.requestClient}\n`;
|
|
90
|
+
}
|
|
91
|
+
if (copySettings.includeDuration && event.duration) {
|
|
92
|
+
md += `**Duration:** ${event.duration}ms\n`;
|
|
93
|
+
}
|
|
94
|
+
if (copySettings.includeTimestamp) {
|
|
95
|
+
md += `**Timestamp:** ${new Date(event.timestamp).toISOString()}\n`;
|
|
96
|
+
}
|
|
97
|
+
if (copySettings.includeSizes) {
|
|
98
|
+
const reqSize = event.requestSize ? formatBytes(event.requestSize) : "N/A";
|
|
99
|
+
const resSize = event.responseSize ? formatBytes(event.responseSize) : "N/A";
|
|
100
|
+
md += `**Request Size:** ${reqSize}\n`;
|
|
101
|
+
md += `**Response Size:** ${resSize}\n`;
|
|
102
|
+
}
|
|
103
|
+
if (copySettings.includeErrors && event.error) {
|
|
104
|
+
md += `**Error:** ${event.error}\n`;
|
|
105
|
+
}
|
|
106
|
+
if (copySettings.includeRequestHeaders && Object.keys(event.requestHeaders).length > 0) {
|
|
107
|
+
md += `\n## Request Headers\n\`\`\`json\n${JSON.stringify(event.requestHeaders, null, 2)}\n\`\`\`\n`;
|
|
108
|
+
}
|
|
109
|
+
if (copySettings.includeRequestBody) {
|
|
110
|
+
md += `\n## Request Data\n`;
|
|
111
|
+
if (shouldIncludePayload(event.requestData, copySettings.bodySizeThreshold)) {
|
|
112
|
+
md += `\`\`\`json\n${JSON.stringify(event.requestData, null, 2)}\n\`\`\`\n`;
|
|
113
|
+
} else {
|
|
114
|
+
const size = event.requestSize ? formatBytes(event.requestSize) : "Unknown size";
|
|
115
|
+
md += `_Payload omitted (${size}). Exceeds ${copySettings.bodySizeThreshold}KB threshold._\n`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (copySettings.includeResponseHeaders && Object.keys(event.responseHeaders).length > 0) {
|
|
119
|
+
md += `\n## Response Headers\n\`\`\`json\n${JSON.stringify(event.responseHeaders, null, 2)}\n\`\`\`\n`;
|
|
120
|
+
}
|
|
121
|
+
if (copySettings.includeResponseBody) {
|
|
122
|
+
md += `\n## Response Data\n`;
|
|
123
|
+
if (shouldIncludePayload(event.responseData, copySettings.bodySizeThreshold)) {
|
|
124
|
+
md += `\`\`\`json\n${JSON.stringify(event.responseData, null, 2)}\n\`\`\`\n`;
|
|
125
|
+
} else {
|
|
126
|
+
const size = event.responseSize ? formatBytes(event.responseSize) : "Unknown size";
|
|
127
|
+
md += `_Payload omitted (${size}). Exceeds ${copySettings.bodySizeThreshold}KB threshold._\n`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
md += `\n---\n`;
|
|
131
|
+
return md;
|
|
132
|
+
}).join("\n");
|
|
133
|
+
const header = `# Network Requests (${eventsToUse.length} total)\n\n`;
|
|
134
|
+
return header + requests;
|
|
135
|
+
}
|
|
@@ -12,6 +12,8 @@ export { NetworkModal } from "./network/components/NetworkModal";
|
|
|
12
12
|
export { NetworkEventDetailView } from "./network/components/NetworkEventDetailView";
|
|
13
13
|
export { NetworkEventItemCompact } from "./network/components/NetworkEventItemCompact";
|
|
14
14
|
export { useNetworkEvents } from "./network/hooks/useNetworkEvents";
|
|
15
|
+
export { useNetworkEventList } from "./network/hooks/useNetworkEventList";
|
|
16
|
+
export { useNetworkBadgeCounts } from "./network/hooks/useNetworkBadgeCounts";
|
|
15
17
|
export { TickProvider, useTickEveryMinute } from "./network/hooks/useTickEveryMinute";
|
|
16
18
|
export { formatBytes, formatDuration, formatHttpStatus, } from "./network/utils/formatting";
|
|
17
19
|
export type { NetworkEvent, NetworkStats, NetworkFilter, NetworkEventStatus, NetworkInsight, IgnoredPattern, IgnoredPatternMatchMode, } from "./network/types";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKhE,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AAKvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKhE,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AAKvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAIpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAKtF,OAAO,EACL,WAAW,EACX,cAAc,EACd,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AAKpC,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,uBAAuB,GACxB,MAAM,iBAAiB,CAAC;AAQzB,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,wBAAwB,EACxB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EACL,2BAA2B,EAC3B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GACzB,MAAM,oCAAoC,CAAC;AAO5C,gBAAgB;AAChB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,gBAAgB;AAChB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,iCAAiC,CAAC"}
|
|
@@ -3,10 +3,6 @@ interface NetworkEventItemCompactProps {
|
|
|
3
3
|
event: NetworkEvent;
|
|
4
4
|
onPress: (event: NetworkEvent) => void;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
* Compact list-row representation of a network event. Optimized for large lists with memoization
|
|
8
|
-
* and periodic refresh via `useTickEveryMinute` to keep relative timestamps accurate.
|
|
9
|
-
*/
|
|
10
6
|
export declare const NetworkEventItemCompact: import("react").NamedExoticComponent<NetworkEventItemCompactProps>;
|
|
11
7
|
export {};
|
|
12
8
|
//# sourceMappingURL=NetworkEventItemCompact.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NetworkEventItemCompact.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkEventItemCompact.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAM7C,UAAU,4BAA4B;IACpC,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CACxC;
|
|
1
|
+
{"version":3,"file":"NetworkEventItemCompact.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkEventItemCompact.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAM7C,UAAU,4BAA4B;IACpC,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CACxC;AA0HD,eAAO,MAAM,uBAAuB,oEA4HnC,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The header pieces that consume LIVE event data, decomposed out of
|
|
3
|
+
* NetworkModal per rule3 (decompose by responsibility) so each one is its
|
|
4
|
+
* own narrow store subscriber and the modal root/header never re-render
|
|
5
|
+
* per network event:
|
|
6
|
+
*
|
|
7
|
+
* - `NetworkHeaderBadges` — success/error/pending count chips
|
|
8
|
+
* - `NetworkClearButton` — trash button (enabled = store has events;
|
|
9
|
+
* clear handler co-located here per rule2)
|
|
10
|
+
* - `NetworkHeaderCopyButton` — copy-all button; the copy TEXT is
|
|
11
|
+
* resolved lazily at press time (CopyButton accepts a function value —
|
|
12
|
+
* rule2's latest-ref pattern), so no string is built per event
|
|
13
|
+
*
|
|
14
|
+
* Profiling evidence for this decomposition: tool-bench 2026-07-06 —
|
|
15
|
+
* Trash2/CopyButton re-rendered 15× for 15 network events when the
|
|
16
|
+
* subscription lived at the modal root.
|
|
17
|
+
*/
|
|
18
|
+
import { type IgnoredPattern } from "@buoy-gg/shared-ui";
|
|
19
|
+
import type { NetworkFilter } from "../types";
|
|
20
|
+
import type { CopySettings } from "./NetworkCopySettingsView";
|
|
21
|
+
type StatusFilter = "success" | "error" | "pending";
|
|
22
|
+
interface NetworkHeaderBadgesProps {
|
|
23
|
+
activeStatus: NetworkFilter["status"];
|
|
24
|
+
onToggleStatus: (status: StatusFilter) => void;
|
|
25
|
+
ignoredPatterns: readonly IgnoredPattern[];
|
|
26
|
+
paused: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Success / error / pending chips — tap to toggle the status filter. */
|
|
29
|
+
export declare function NetworkHeaderBadges({ activeStatus, onToggleStatus, ignoredPatterns, paused, }: NetworkHeaderBadgesProps): import("react").JSX.Element;
|
|
30
|
+
interface NetworkClearButtonProps {
|
|
31
|
+
paused: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Trash button — handler co-located; enabled while the store has events.
|
|
34
|
+
* Subscribes to a flip-guarded boolean, so it renders when the store goes
|
|
35
|
+
* empty↔non-empty, not on every count change. */
|
|
36
|
+
export declare function NetworkClearButton({ paused }: NetworkClearButtonProps): import("react").JSX.Element;
|
|
37
|
+
interface NetworkHeaderCopyButtonProps {
|
|
38
|
+
isPro: boolean;
|
|
39
|
+
filter: NetworkFilter;
|
|
40
|
+
ignoredPatterns: readonly IgnoredPattern[];
|
|
41
|
+
copySettings: CopySettings;
|
|
42
|
+
paused: boolean;
|
|
43
|
+
onUpgradePress: () => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Copy-all-requests button. Subscribes to a flip-guarded boolean (its
|
|
47
|
+
* enabled state); the copy text is generated at PRESS time from a live
|
|
48
|
+
* store read, so there's no per-event work and no stale closure.
|
|
49
|
+
*/
|
|
50
|
+
export declare function NetworkHeaderCopyButton({ isPro, filter, ignoredPatterns, copySettings, paused, onUpgradePress, }: NetworkHeaderCopyButtonProps): import("react").JSX.Element;
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=NetworkHeaderLive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NetworkHeaderLive.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkHeaderLive.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAQL,KAAK,cAAc,EACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAa9D,KAAK,YAAY,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAEpD,UAAU,wBAAwB;IAChC,YAAY,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACtC,cAAc,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAC/C,eAAe,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,EAClC,YAAY,EACZ,cAAc,EACd,eAAe,EACf,MAAM,GACP,EAAE,wBAAwB,+BA4D1B;AAED,UAAU,uBAAuB;IAC/B,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;iDAEiD;AACjD,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE,uBAAuB,+BAqBrE;AAED,UAAU,4BAA4B;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,aAAa,CAAC;IACtB,eAAe,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,YAAY,EAAE,YAAY,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,KAAK,EACL,MAAM,EACN,eAAe,EACf,YAAY,EACZ,MAAM,EACN,cAAc,GACf,EAAE,4BAA4B,+BAiD9B"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The network tool's LIST screen — owns the event-store subscription, the
|
|
3
|
+
* FlatList, empty state, disabled banner, and the free-tier locked-history
|
|
4
|
+
* footer. Decomposed from NetworkModal per rule3 so per-event re-renders
|
|
5
|
+
* are confined to this subtree (plus the header's live leaves) instead of
|
|
6
|
+
* the whole modal.
|
|
7
|
+
*
|
|
8
|
+
* Stable-tree contract (rule4): hooks run unconditionally, then the
|
|
9
|
+
* component returns null when not visible. The subscription is gated with
|
|
10
|
+
* `paused || !visible` so a hidden list doesn't subscribe at all.
|
|
11
|
+
*/
|
|
12
|
+
import { type IgnoredPattern } from "@buoy-gg/shared-ui";
|
|
13
|
+
import type { NetworkEvent, NetworkFilter } from "../types";
|
|
14
|
+
interface NetworkListScreenProps {
|
|
15
|
+
visible: boolean;
|
|
16
|
+
paused: boolean;
|
|
17
|
+
filter: NetworkFilter;
|
|
18
|
+
ignoredPatterns: readonly IgnoredPattern[];
|
|
19
|
+
isPro: boolean;
|
|
20
|
+
searchText: string;
|
|
21
|
+
/** Row tap — the root's setSelectedEvent (inherently stable setter). */
|
|
22
|
+
onEventPress: (event: NetworkEvent) => void;
|
|
23
|
+
onUpgradePress: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function NetworkListScreen({ visible, paused, filter, ignoredPatterns, isPro, searchText, onEventPress, onUpgradePress, }: NetworkListScreenProps): import("react").JSX.Element | null;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=NetworkListScreen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NetworkListScreen.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkListScreen.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAML,KAAK,cAAc,EACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAoB5D,UAAU,sBAAsB;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;IACtB,eAAe,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,YAAY,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,MAAM,EACN,MAAM,EACN,eAAe,EACf,KAAK,EACL,UAAU,EACV,YAAY,EACZ,cAAc,GACf,EAAE,sBAAsB,sCAuFxB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NetworkModal.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkModal.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"NetworkModal.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkModal.tsx"],"names":[],"mappings":"AAyCA,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAoeD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BAMpD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow store subscription for the header's live counters — the ONLY
|
|
3
|
+
* header data that should update per network event (the success/error/
|
|
4
|
+
* pending chips and the clear button's has-events state).
|
|
5
|
+
*
|
|
6
|
+
* Holds derived COUNTS in state, not the events array, with an equality
|
|
7
|
+
* guard — so consumers re-render only when a count actually changes
|
|
8
|
+
* (e.g. an ignored-pattern-matched event arriving doesn't re-render the
|
|
9
|
+
* chips). rule2: external store, narrowest possible subscription.
|
|
10
|
+
*/
|
|
11
|
+
import type { IgnoredPattern } from "@buoy-gg/shared-ui";
|
|
12
|
+
export interface NetworkBadgeCounts {
|
|
13
|
+
/** Ignored-patterns applied — what the chips display. */
|
|
14
|
+
successful: number;
|
|
15
|
+
failed: number;
|
|
16
|
+
pending: number;
|
|
17
|
+
/** Raw store count (pre-ignore) — drives clear-button enablement. */
|
|
18
|
+
total: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function useNetworkBadgeCounts(options: {
|
|
21
|
+
ignoredPatterns: readonly IgnoredPattern[];
|
|
22
|
+
paused: boolean;
|
|
23
|
+
}): NetworkBadgeCounts;
|
|
24
|
+
//# sourceMappingURL=useNetworkBadgeCounts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNetworkBadgeCounts.d.ts","sourceRoot":"","sources":["../../../../src/network/hooks/useNetworkBadgeCounts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAKzD,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;CACf;AA8BD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC7C,eAAe,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,kBAAkB,CAwCrB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow store subscription for components that render the network event
|
|
3
|
+
* LIST (or need the filtered list). Decomposed from `useNetworkEvents`
|
|
4
|
+
* per rule2 ("external store + narrow subscriptions") so the subscription
|
|
5
|
+
* lives in the leaf that consumes it — the modal root no longer
|
|
6
|
+
* re-renders per network event.
|
|
7
|
+
*
|
|
8
|
+
* Profiling evidence (tool-bench, 2026-07-06): with the subscription at
|
|
9
|
+
* the modal root, 15 network events re-rendered the ENTIRE modal 15× —
|
|
10
|
+
* JsModalComponent/header buttons ×15, all visible rows ×105.
|
|
11
|
+
*
|
|
12
|
+
* `paused` gates the subscription (the power toggle's freeze-stream
|
|
13
|
+
* semantic, and screens pass `paused || !visible` so hidden screens don't
|
|
14
|
+
* subscribe at all). Store clears are honored even while paused so a
|
|
15
|
+
* frozen list can't show deleted events (parity with the old hook's
|
|
16
|
+
* clearEvents local reset).
|
|
17
|
+
*/
|
|
18
|
+
import type { IgnoredPattern } from "@buoy-gg/shared-ui";
|
|
19
|
+
import type { NetworkEvent, NetworkFilter } from "../types";
|
|
20
|
+
export interface NetworkEventListResult {
|
|
21
|
+
/** Filtered events (may include free-tier locked events). */
|
|
22
|
+
events: NetworkEvent[];
|
|
23
|
+
/** Filtered events minus locked ones — what the list renders. */
|
|
24
|
+
visibleEvents: NetworkEvent[];
|
|
25
|
+
hasLockedEvents: boolean;
|
|
26
|
+
lockedEventCount: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function useNetworkEventList(options: {
|
|
29
|
+
filter: NetworkFilter;
|
|
30
|
+
ignoredPatterns: readonly IgnoredPattern[];
|
|
31
|
+
paused: boolean;
|
|
32
|
+
isPro: boolean;
|
|
33
|
+
}): NetworkEventListResult;
|
|
34
|
+
//# sourceMappingURL=useNetworkEventList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNetworkEventList.d.ts","sourceRoot":"","sources":["../../../../src/network/hooks/useNetworkEventList.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAO5D,MAAM,WAAW,sBAAsB;IACrC,6DAA6D;IAC7D,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,iEAAiE;IACjE,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,eAAe,EAAE,OAAO,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAC3C,MAAM,EAAE,aAAa,CAAC;IACtB,eAAe,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,sBAAsB,CAyEzB"}
|