@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.
Files changed (40) hide show
  1. package/lib/commonjs/index.js +14 -0
  2. package/lib/commonjs/network/components/NetworkEventItemCompact.js +32 -18
  3. package/lib/commonjs/network/components/NetworkHeaderLive.js +199 -0
  4. package/lib/commonjs/network/components/NetworkListScreen.js +203 -0
  5. package/lib/commonjs/network/components/NetworkModal.js +153 -630
  6. package/lib/commonjs/network/hooks/useNetworkBadgeCounts.js +83 -0
  7. package/lib/commonjs/network/hooks/useNetworkEventList.js +93 -0
  8. package/lib/commonjs/network/hooks/useNetworkHasMatches.js +56 -0
  9. package/lib/commonjs/network/utils/filterNetworkEvents.js +96 -0
  10. package/lib/commonjs/network/utils/generateNetworkCopyText.js +139 -0
  11. package/lib/module/index.js +5 -0
  12. package/lib/module/network/components/NetworkEventItemCompact.js +32 -18
  13. package/lib/module/network/components/NetworkHeaderLive.js +193 -0
  14. package/lib/module/network/components/NetworkListScreen.js +199 -0
  15. package/lib/module/network/components/NetworkModal.js +157 -634
  16. package/lib/module/network/hooks/useNetworkBadgeCounts.js +79 -0
  17. package/lib/module/network/hooks/useNetworkEventList.js +89 -0
  18. package/lib/module/network/hooks/useNetworkHasMatches.js +52 -0
  19. package/lib/module/network/utils/filterNetworkEvents.js +92 -0
  20. package/lib/module/network/utils/generateNetworkCopyText.js +135 -0
  21. package/lib/typescript/index.d.ts +2 -0
  22. package/lib/typescript/index.d.ts.map +1 -1
  23. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +0 -4
  24. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  25. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +52 -0
  26. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -0
  27. package/lib/typescript/network/components/NetworkListScreen.d.ts +27 -0
  28. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -0
  29. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  30. package/lib/typescript/network/hooks/useNetworkBadgeCounts.d.ts +24 -0
  31. package/lib/typescript/network/hooks/useNetworkBadgeCounts.d.ts.map +1 -0
  32. package/lib/typescript/network/hooks/useNetworkEventList.d.ts +34 -0
  33. package/lib/typescript/network/hooks/useNetworkEventList.d.ts.map +1 -0
  34. package/lib/typescript/network/hooks/useNetworkHasMatches.d.ts +23 -0
  35. package/lib/typescript/network/hooks/useNetworkHasMatches.d.ts.map +1 -0
  36. package/lib/typescript/network/utils/filterNetworkEvents.d.ts +13 -0
  37. package/lib/typescript/network/utils/filterNetworkEvents.d.ts.map +1 -0
  38. package/lib/typescript/network/utils/generateNetworkCopyText.d.ts +11 -0
  39. package/lib/typescript/network/utils/generateNetworkCopyText.d.ts.map +1 -0
  40. 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;AACpE,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"}
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;AA8FD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,oEAuInC,CAAC"}
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":"AA8CA,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;AAu/BD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BAMpD"}
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"}