@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.
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,193 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The header pieces that consume LIVE event data, decomposed out of
5
+ * NetworkModal per rule3 (decompose by responsibility) so each one is its
6
+ * own narrow store subscriber and the modal root/header never re-render
7
+ * per network event:
8
+ *
9
+ * - `NetworkHeaderBadges` — success/error/pending count chips
10
+ * - `NetworkClearButton` — trash button (enabled = store has events;
11
+ * clear handler co-located here per rule2)
12
+ * - `NetworkHeaderCopyButton` — copy-all button; the copy TEXT is
13
+ * resolved lazily at press time (CopyButton accepts a function value —
14
+ * rule2's latest-ref pattern), so no string is built per event
15
+ *
16
+ * Profiling evidence for this decomposition: tool-bench 2026-07-06 —
17
+ * Trash2/CopyButton re-rendered 15× for 15 network events when the
18
+ * subscription lived at the modal root.
19
+ */
20
+
21
+ import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
22
+ import { CheckCircle, XCircle, Clock, Copy, Trash2, CopyButton, macOSColors } from "@buoy-gg/shared-ui";
23
+ import { networkEventStore } from "../utils/networkEventStore";
24
+ import { useNetworkBadgeCounts } from "../hooks/useNetworkBadgeCounts";
25
+ import { useNetworkHasEvents, useNetworkHasMatches } from "../hooks/useNetworkHasMatches";
26
+ import { generateNetworkCopyText } from "../utils/generateNetworkCopyText";
27
+ import { applyIgnoredPatterns, filterNetworkEvents } from "../utils/filterNetworkEvents";
28
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
29
+ /** Success / error / pending chips — tap to toggle the status filter. */
30
+ export function NetworkHeaderBadges({
31
+ activeStatus,
32
+ onToggleStatus,
33
+ ignoredPatterns,
34
+ paused
35
+ }) {
36
+ const counts = useNetworkBadgeCounts({
37
+ ignoredPatterns,
38
+ paused
39
+ });
40
+ return /*#__PURE__*/_jsxs(View, {
41
+ style: styles.headerChipRow,
42
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
43
+ style: [styles.headerChip, activeStatus === "success" && styles.headerChipActive],
44
+ onPress: () => onToggleStatus("success"),
45
+ children: [/*#__PURE__*/_jsx(CheckCircle, {
46
+ size: 12,
47
+ color: macOSColors.semantic.success
48
+ }), /*#__PURE__*/_jsx(Text, {
49
+ style: [styles.headerChipValue, {
50
+ color: macOSColors.semantic.success
51
+ }],
52
+ children: counts.successful
53
+ })]
54
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
55
+ style: [styles.headerChip, activeStatus === "error" && styles.headerChipActive],
56
+ onPress: () => onToggleStatus("error"),
57
+ children: [/*#__PURE__*/_jsx(XCircle, {
58
+ size: 12,
59
+ color: macOSColors.semantic.error
60
+ }), /*#__PURE__*/_jsx(Text, {
61
+ style: [styles.headerChipValue, {
62
+ color: macOSColors.semantic.error
63
+ }],
64
+ children: counts.failed
65
+ })]
66
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
67
+ style: [styles.headerChip, activeStatus === "pending" && styles.headerChipActive],
68
+ onPress: () => onToggleStatus("pending"),
69
+ children: [/*#__PURE__*/_jsx(Clock, {
70
+ size: 12,
71
+ color: macOSColors.semantic.warning
72
+ }), /*#__PURE__*/_jsx(Text, {
73
+ style: [styles.headerChipValue, {
74
+ color: macOSColors.semantic.warning
75
+ }],
76
+ children: counts.pending
77
+ })]
78
+ })]
79
+ });
80
+ }
81
+ /** Trash button — handler co-located; enabled while the store has events.
82
+ * Subscribes to a flip-guarded boolean, so it renders when the store goes
83
+ * empty↔non-empty, not on every count change. */
84
+ export function NetworkClearButton({
85
+ paused
86
+ }) {
87
+ const hasEvents = useNetworkHasEvents({
88
+ paused
89
+ });
90
+ return /*#__PURE__*/_jsx(TouchableOpacity, {
91
+ "sentry-label": "ignore clear events",
92
+ onPress: () => networkEventStore.clearEvents(),
93
+ style: [styles.headerActionButton, !hasEvents && styles.headerActionButtonDisabled],
94
+ disabled: !hasEvents,
95
+ children: /*#__PURE__*/_jsx(Trash2, {
96
+ size: 14,
97
+ color: hasEvents ? macOSColors.text.muted : macOSColors.text.disabled
98
+ })
99
+ });
100
+ }
101
+ /**
102
+ * Copy-all-requests button. Subscribes to a flip-guarded boolean (its
103
+ * enabled state); the copy text is generated at PRESS time from a live
104
+ * store read, so there's no per-event work and no stale closure.
105
+ */
106
+ export function NetworkHeaderCopyButton({
107
+ isPro,
108
+ filter,
109
+ ignoredPatterns,
110
+ copySettings,
111
+ paused,
112
+ onUpgradePress
113
+ }) {
114
+ const hasEvents = useNetworkHasMatches({
115
+ filter,
116
+ ignoredPatterns,
117
+ paused
118
+ });
119
+ if (!isPro) {
120
+ return /*#__PURE__*/_jsx(TouchableOpacity, {
121
+ "sentry-label": "ignore copy pro",
122
+ onPress: onUpgradePress,
123
+ style: [styles.headerActionButton, !hasEvents && styles.headerActionButtonDisabled],
124
+ disabled: !hasEvents,
125
+ children: /*#__PURE__*/_jsx(Copy, {
126
+ size: 14,
127
+ color: hasEvents ? macOSColors.text.secondary : macOSColors.text.disabled
128
+ })
129
+ });
130
+ }
131
+ return /*#__PURE__*/_jsx(CopyButton
132
+ // Lazy value: resolved at copy time by CopyButton's latest-ref
133
+ // pattern, reading the LIVE store — no per-event work, no staleness.
134
+ , {
135
+ value: () => generateNetworkCopyText(filterNetworkEvents(applyIgnoredPatterns(networkEventStore.getEvents(), ignoredPatterns), filter), copySettings),
136
+ size: 14,
137
+ buttonStyle: styles.headerActionButton,
138
+ colors: {
139
+ idle: hasEvents ? macOSColors.text.secondary : macOSColors.text.disabled,
140
+ success: macOSColors.semantic.success,
141
+ error: macOSColors.semantic.error
142
+ },
143
+ disabled: !hasEvents
144
+ });
145
+ }
146
+ const styles = StyleSheet.create({
147
+ headerChipRow: {
148
+ flexDirection: "row",
149
+ alignItems: "center",
150
+ gap: 8
151
+ },
152
+ headerChip: {
153
+ flexDirection: "row",
154
+ alignItems: "center",
155
+ gap: 4,
156
+ backgroundColor: macOSColors.background.hover,
157
+ paddingHorizontal: 10,
158
+ paddingVertical: 5,
159
+ borderRadius: 12,
160
+ borderWidth: 1,
161
+ borderColor: macOSColors.border.default
162
+ },
163
+ headerChipActive: {
164
+ backgroundColor: macOSColors.semantic.infoBackground,
165
+ borderColor: macOSColors.semantic.info + "50",
166
+ shadowColor: macOSColors.semantic.info,
167
+ shadowOffset: {
168
+ width: 0,
169
+ height: 1
170
+ },
171
+ shadowOpacity: 0.08,
172
+ shadowRadius: 2,
173
+ elevation: 1
174
+ },
175
+ headerChipValue: {
176
+ fontSize: 12,
177
+ fontWeight: "600",
178
+ fontFamily: "monospace"
179
+ },
180
+ headerActionButton: {
181
+ width: 32,
182
+ height: 32,
183
+ borderRadius: 8,
184
+ backgroundColor: macOSColors.background.hover,
185
+ borderWidth: 1,
186
+ borderColor: macOSColors.border.default,
187
+ alignItems: "center",
188
+ justifyContent: "center"
189
+ },
190
+ headerActionButtonDisabled: {
191
+ opacity: 0.55
192
+ }
193
+ });
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The network tool's LIST screen — owns the event-store subscription, the
5
+ * FlatList, empty state, disabled banner, and the free-tier locked-history
6
+ * footer. Decomposed from NetworkModal per rule3 so per-event re-renders
7
+ * are confined to this subtree (plus the header's live leaves) instead of
8
+ * the whole modal.
9
+ *
10
+ * Stable-tree contract (rule4): hooks run unconditionally, then the
11
+ * component returns null when not visible. The subscription is gated with
12
+ * `paused || !visible` so a hidden list doesn't subscribe at all.
13
+ */
14
+
15
+ import { FlatList, StyleSheet, Text, TouchableOpacity, View } from "react-native";
16
+ import { Globe, Power, ProBadge, Zap, macOSColors } from "@buoy-gg/shared-ui";
17
+ import { NetworkEventItemCompact } from "./NetworkEventItemCompact";
18
+ import { useNetworkEventList } from "../hooks/useNetworkEventList";
19
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
20
+ function EmptyState({
21
+ isEnabled
22
+ }) {
23
+ return /*#__PURE__*/_jsxs(View, {
24
+ style: styles.emptyState,
25
+ children: [/*#__PURE__*/_jsx(Globe, {
26
+ size: 32,
27
+ color: macOSColors.text.muted
28
+ }), /*#__PURE__*/_jsx(Text, {
29
+ style: styles.emptyTitle,
30
+ children: "No network events"
31
+ }), /*#__PURE__*/_jsx(Text, {
32
+ style: styles.emptyText,
33
+ children: isEnabled ? "Network requests will appear here" : "Enable interception to start capturing"
34
+ })]
35
+ });
36
+ }
37
+ const keyExtractor = item => item.id;
38
+ export function NetworkListScreen({
39
+ visible,
40
+ paused,
41
+ filter,
42
+ ignoredPatterns,
43
+ isPro,
44
+ searchText,
45
+ onEventPress,
46
+ onUpgradePress
47
+ }) {
48
+ const {
49
+ visibleEvents,
50
+ hasLockedEvents,
51
+ lockedEventCount
52
+ } = useNetworkEventList({
53
+ filter,
54
+ ignoredPatterns,
55
+ paused: paused || !visible,
56
+ isPro
57
+ });
58
+ if (!visible) return null;
59
+ const isEnabled = !paused;
60
+ const renderItem = ({
61
+ item
62
+ }) => /*#__PURE__*/_jsx(NetworkEventItemCompact, {
63
+ event: item,
64
+ onPress: onEventPress
65
+ });
66
+ const renderListFooter = () => {
67
+ if (!hasLockedEvents) return null;
68
+ const isSearching = searchText.length > 0;
69
+ const bannerText = isSearching ? `${lockedEventCount} matching ${lockedEventCount === 1 ? "request" : "requests"} in locked history` : `${lockedEventCount} requests locked`;
70
+ const subtextMessage = isSearching ? "Upgrade to Pro to search your full request history" : "Upgrade to Pro to unlock full request history";
71
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
72
+ style: styles.limitBanner,
73
+ onPress: onUpgradePress,
74
+ activeOpacity: 0.8,
75
+ children: [/*#__PURE__*/_jsxs(View, {
76
+ style: styles.limitBannerContent,
77
+ children: [/*#__PURE__*/_jsxs(View, {
78
+ style: styles.limitBannerLeft,
79
+ children: [/*#__PURE__*/_jsx(Zap, {
80
+ size: 16,
81
+ color: "#20C997"
82
+ }), /*#__PURE__*/_jsx(Text, {
83
+ style: styles.limitBannerText,
84
+ children: bannerText
85
+ })]
86
+ }), /*#__PURE__*/_jsx(ProBadge, {})]
87
+ }), /*#__PURE__*/_jsx(Text, {
88
+ style: styles.limitBannerSubtext,
89
+ children: subtextMessage
90
+ })]
91
+ });
92
+ };
93
+ return /*#__PURE__*/_jsxs(_Fragment, {
94
+ children: [!isEnabled ? /*#__PURE__*/_jsxs(View, {
95
+ style: styles.disabledBanner,
96
+ children: [/*#__PURE__*/_jsx(Power, {
97
+ size: 14,
98
+ color: macOSColors.semantic.warning
99
+ }), /*#__PURE__*/_jsx(Text, {
100
+ style: styles.disabledText,
101
+ children: "Network interception is disabled"
102
+ })]
103
+ }) : null, visibleEvents.length > 0 || hasLockedEvents ? /*#__PURE__*/_jsx(FlatList, {
104
+ data: visibleEvents,
105
+ renderItem: renderItem,
106
+ keyExtractor: keyExtractor,
107
+ contentContainerStyle: styles.listContent,
108
+ showsVerticalScrollIndicator: true,
109
+ removeClippedSubviews: true,
110
+ onEndReachedThreshold: 0.8
111
+ // The list OWNS scrolling (NetworkModal passes
112
+ // disableScrollWrapper while this screen is visible) so FlatList
113
+ // virtualization actually works. The previous shipped config —
114
+ // scrollEnabled=false inside JsModal's ScrollView — mounted ALL
115
+ // rows: at the store's 500-event cap that measured 17 JS FPS /
116
+ // 133% CPU / 1.4GB vs ~53 JS FPS / 52% / 360MB virtualized
117
+ // (buoy-optimize batch 2026-07-06, batch-1783396983529-6l7w).
118
+ // Window/batch values are the batch's "tuned" winner.
119
+ ,
120
+ scrollEnabled: true,
121
+ initialNumToRender: 12,
122
+ maxToRenderPerBatch: 8,
123
+ windowSize: 5,
124
+ updateCellsBatchingPeriod: 50,
125
+ "sentry-label": "ignore network events list",
126
+ ListFooterComponent: renderListFooter
127
+ }) : /*#__PURE__*/_jsx(EmptyState, {
128
+ isEnabled: isEnabled
129
+ })]
130
+ });
131
+ }
132
+ const styles = StyleSheet.create({
133
+ disabledBanner: {
134
+ flexDirection: "row",
135
+ alignItems: "center",
136
+ gap: 8,
137
+ padding: 10,
138
+ marginHorizontal: 12,
139
+ marginTop: 8,
140
+ backgroundColor: macOSColors.semantic.warningBackground,
141
+ borderRadius: 8,
142
+ borderWidth: 1,
143
+ borderColor: macOSColors.semantic.warning + "20"
144
+ },
145
+ disabledText: {
146
+ color: macOSColors.semantic.warning,
147
+ fontSize: 11,
148
+ flex: 1
149
+ },
150
+ listContent: {
151
+ paddingTop: 8
152
+ },
153
+ emptyState: {
154
+ alignItems: "center",
155
+ paddingVertical: 40
156
+ },
157
+ emptyTitle: {
158
+ color: macOSColors.text.primary,
159
+ fontSize: 14,
160
+ fontWeight: "600",
161
+ marginTop: 12,
162
+ marginBottom: 6
163
+ },
164
+ emptyText: {
165
+ color: macOSColors.text.muted,
166
+ fontSize: 12,
167
+ textAlign: "center"
168
+ },
169
+ limitBanner: {
170
+ backgroundColor: "#1A1A1A",
171
+ borderRadius: 10,
172
+ borderWidth: 1,
173
+ borderColor: "#20C997" + "40",
174
+ padding: 12,
175
+ marginHorizontal: 12,
176
+ marginTop: 12,
177
+ marginBottom: 34
178
+ },
179
+ limitBannerContent: {
180
+ flexDirection: "row",
181
+ alignItems: "center",
182
+ justifyContent: "space-between",
183
+ marginBottom: 4
184
+ },
185
+ limitBannerLeft: {
186
+ flexDirection: "row",
187
+ alignItems: "center",
188
+ gap: 8
189
+ },
190
+ limitBannerText: {
191
+ color: "#E0E0E0",
192
+ fontSize: 13,
193
+ fontWeight: "600"
194
+ },
195
+ limitBannerSubtext: {
196
+ color: "#888888",
197
+ fontSize: 11
198
+ }
199
+ });