@buoy-gg/network 1.7.2

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 (67) hide show
  1. package/README.md +381 -0
  2. package/lib/commonjs/index.js +34 -0
  3. package/lib/commonjs/network/components/NetworkCopySettingsView.js +867 -0
  4. package/lib/commonjs/network/components/NetworkEventDetailView.js +837 -0
  5. package/lib/commonjs/network/components/NetworkEventItemCompact.js +323 -0
  6. package/lib/commonjs/network/components/NetworkFilterViewV3.js +297 -0
  7. package/lib/commonjs/network/components/NetworkModal.js +937 -0
  8. package/lib/commonjs/network/hooks/useNetworkEvents.js +320 -0
  9. package/lib/commonjs/network/hooks/useTickEveryMinute.js +34 -0
  10. package/lib/commonjs/network/index.js +102 -0
  11. package/lib/commonjs/network/types/index.js +1 -0
  12. package/lib/commonjs/network/utils/extractOperationName.js +80 -0
  13. package/lib/commonjs/network/utils/formatGraphQLVariables.js +219 -0
  14. package/lib/commonjs/network/utils/formatting.js +30 -0
  15. package/lib/commonjs/network/utils/networkEventStore.js +269 -0
  16. package/lib/commonjs/network/utils/networkListener.js +801 -0
  17. package/lib/commonjs/package.json +1 -0
  18. package/lib/commonjs/preset.js +83 -0
  19. package/lib/module/index.js +7 -0
  20. package/lib/module/network/components/NetworkCopySettingsView.js +862 -0
  21. package/lib/module/network/components/NetworkEventDetailView.js +834 -0
  22. package/lib/module/network/components/NetworkEventItemCompact.js +320 -0
  23. package/lib/module/network/components/NetworkFilterViewV3.js +293 -0
  24. package/lib/module/network/components/NetworkModal.js +933 -0
  25. package/lib/module/network/hooks/useNetworkEvents.js +316 -0
  26. package/lib/module/network/hooks/useTickEveryMinute.js +29 -0
  27. package/lib/module/network/index.js +20 -0
  28. package/lib/module/network/types/index.js +1 -0
  29. package/lib/module/network/utils/extractOperationName.js +76 -0
  30. package/lib/module/network/utils/formatGraphQLVariables.js +213 -0
  31. package/lib/module/network/utils/formatting.js +9 -0
  32. package/lib/module/network/utils/networkEventStore.js +265 -0
  33. package/lib/module/network/utils/networkListener.js +791 -0
  34. package/lib/module/preset.js +79 -0
  35. package/lib/typescript/index.d.ts +3 -0
  36. package/lib/typescript/index.d.ts.map +1 -0
  37. package/lib/typescript/network/components/NetworkCopySettingsView.d.ts +26 -0
  38. package/lib/typescript/network/components/NetworkCopySettingsView.d.ts.map +1 -0
  39. package/lib/typescript/network/components/NetworkEventDetailView.d.ts +13 -0
  40. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -0
  41. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
  42. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -0
  43. package/lib/typescript/network/components/NetworkFilterViewV3.d.ts +22 -0
  44. package/lib/typescript/network/components/NetworkFilterViewV3.d.ts.map +1 -0
  45. package/lib/typescript/network/components/NetworkModal.d.ts +14 -0
  46. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -0
  47. package/lib/typescript/network/hooks/useNetworkEvents.d.ts +72 -0
  48. package/lib/typescript/network/hooks/useNetworkEvents.d.ts.map +1 -0
  49. package/lib/typescript/network/hooks/useTickEveryMinute.d.ts +9 -0
  50. package/lib/typescript/network/hooks/useTickEveryMinute.d.ts.map +1 -0
  51. package/lib/typescript/network/index.d.ts +12 -0
  52. package/lib/typescript/network/index.d.ts.map +1 -0
  53. package/lib/typescript/network/types/index.d.ts +88 -0
  54. package/lib/typescript/network/types/index.d.ts.map +1 -0
  55. package/lib/typescript/network/utils/extractOperationName.d.ts +41 -0
  56. package/lib/typescript/network/utils/extractOperationName.d.ts.map +1 -0
  57. package/lib/typescript/network/utils/formatGraphQLVariables.d.ts +79 -0
  58. package/lib/typescript/network/utils/formatGraphQLVariables.d.ts.map +1 -0
  59. package/lib/typescript/network/utils/formatting.d.ts +6 -0
  60. package/lib/typescript/network/utils/formatting.d.ts.map +1 -0
  61. package/lib/typescript/network/utils/networkEventStore.d.ts +81 -0
  62. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -0
  63. package/lib/typescript/network/utils/networkListener.d.ts +191 -0
  64. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -0
  65. package/lib/typescript/preset.d.ts +76 -0
  66. package/lib/typescript/preset.d.ts.map +1 -0
  67. package/package.json +69 -0
@@ -0,0 +1,316 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Hook for accessing network events and controls
5
+ * Uses Reactotron-style listener pattern
6
+ */
7
+
8
+ import { useState, useEffect, useCallback, useMemo } from "react";
9
+ import { networkEventStore } from "../utils/networkEventStore";
10
+ import { networkListener, startNetworkListener, stopNetworkListener, addNetworkListener } from "../utils/networkListener";
11
+ import { searchGraphQLVariables } from "../utils/formatGraphQLVariables";
12
+
13
+ /** Free tier limit for network requests */
14
+ export const FREE_TIER_REQUEST_LIMIT = 25;
15
+
16
+ /**
17
+ * Custom hook for accessing network events and controls
18
+ *
19
+ * This hook provides a complete interface for network monitoring, including
20
+ * event filtering, statistics calculation, and interception control. It uses
21
+ * the Reactotron-style listener pattern for network event handling.
22
+ *
23
+ * @returns Object containing filtered events, statistics, controls, and utilities
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * function NetworkMonitor() {
28
+ * const {
29
+ * events,
30
+ * stats,
31
+ * filter,
32
+ * setFilter,
33
+ * clearEvents,
34
+ * toggleInterception,
35
+ * isEnabled
36
+ * } = useNetworkEvents();
37
+ *
38
+ * return (
39
+ * <div>
40
+ * <p>Total requests: {stats.totalRequests}</p>
41
+ * <p>Success rate: {stats.successfulRequests}/{stats.totalRequests}</p>
42
+ * <button onClick={toggleInterception}>
43
+ * {isEnabled ? 'Stop' : 'Start'} Monitoring
44
+ * </button>
45
+ * </div>
46
+ * );
47
+ * }
48
+ * ```
49
+ *
50
+ * @performance Uses memoization for expensive filtering and statistics calculations
51
+ * @performance Optimizes string operations and array processing for large datasets
52
+ * @performance Includes Set-based lookups for O(1) filter matching
53
+ *
54
+ * @param options.isPro - Whether user has Pro access (unlimited events). Defaults to false (limited to FREE_TIER_REQUEST_LIMIT)
55
+ */
56
+ export function useNetworkEvents(options = {}) {
57
+ const {
58
+ isPro = false
59
+ } = options;
60
+ const [events, setEvents] = useState([]);
61
+ const [filter, setFilter] = useState({});
62
+ const [isEnabled, setIsEnabled] = useState(false);
63
+
64
+ // Subscribe to event store changes
65
+ useEffect(() => {
66
+ // Subscribe to store changes
67
+ const unsubscribeStore = networkEventStore.subscribe(setEvents);
68
+
69
+ // Add listener to network events
70
+ const unsubscribeListener = addNetworkListener(event => {
71
+ // Only log in development and for non-ignored URLs
72
+ if (__DEV__ && !event.request.url.includes("symbolicate") && !event.request.url.includes(":8081")) {
73
+ // Network event processed: [event.type] [method] [url] - available for debugging if needed
74
+ }
75
+ networkEventStore.processNetworkEvent(event);
76
+ });
77
+
78
+ // Check if already listening
79
+ setIsEnabled(networkListener().isActive);
80
+
81
+ // Start listening if not already
82
+ if (!networkListener().isActive) {
83
+ startNetworkListener();
84
+ setIsEnabled(true);
85
+ }
86
+
87
+ // Load initial events
88
+ setEvents(networkEventStore.getEvents());
89
+ return () => {
90
+ unsubscribeStore();
91
+ unsubscribeListener();
92
+ };
93
+ }, []);
94
+
95
+ // Clear all events
96
+ const clearEvents = useCallback(() => {
97
+ networkEventStore.clearEvents();
98
+ }, []);
99
+
100
+ // Toggle interception
101
+ const toggleInterception = useCallback(() => {
102
+ if (isEnabled) {
103
+ stopNetworkListener();
104
+ setIsEnabled(false);
105
+ } else {
106
+ startNetworkListener();
107
+ setIsEnabled(true);
108
+ }
109
+ }, [isEnabled]);
110
+
111
+ // Memoize search text processing to avoid repeated toLowerCase calls
112
+ // Performance: Expensive string operations repeated for every event on every filter
113
+ const searchLower = useMemo(() => {
114
+ return filter.searchText ? filter.searchText.toLowerCase() : null;
115
+ }, [filter.searchText]);
116
+
117
+ // Memoize method filter Set for O(1) lookup instead of Array.includes
118
+ // Performance: Converting array.includes to Set.has for faster lookups with large method lists
119
+ const methodSet = useMemo(() => {
120
+ return filter.method && filter.method.length > 0 ? new Set(filter.method) : null;
121
+ }, [filter.method]);
122
+
123
+ // Memoize content type Set for O(1) lookup
124
+ // Performance: Converting array.some to Set.has for faster content type matching
125
+ const contentTypeSet = useMemo(() => {
126
+ return filter.contentType && filter.contentType.length > 0 ? new Set(filter.contentType) : null;
127
+ }, [filter.contentType]);
128
+
129
+ // Filter events with optimized string operations and Set lookups
130
+ // Performance: Complex multi-stage filtering with string operations and content type matching
131
+ const filteredEvents = useMemo(() => {
132
+ let filtered = [...events];
133
+ if (methodSet) {
134
+ filtered = filtered.filter(e => methodSet.has(e.method));
135
+ }
136
+ if (filter.status && filter.status !== "all") {
137
+ switch (filter.status) {
138
+ case "success":
139
+ filtered = filtered.filter(e => e.status && e.status >= 200 && e.status < 300);
140
+ break;
141
+ case "error":
142
+ filtered = filtered.filter(e => e.error || e.status && e.status >= 400);
143
+ break;
144
+ case "pending":
145
+ filtered = filtered.filter(e => !e.status && !e.error);
146
+ break;
147
+ }
148
+ }
149
+ if (searchLower) {
150
+ 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) ||
151
+ // Search by GraphQL operation name (e.g., "GetUser", "CreatePost")
152
+ e.operationName && e.operationName.toLowerCase().includes(searchLower) ||
153
+ // Search by GraphQL variable values (e.g., "Sandshrew", "123", "true")
154
+ // This enables finding specific requests like "GetPokemon › Sandshrew" by typing "Sandshrew"
155
+ searchGraphQLVariables(e.graphqlVariables, searchLower));
156
+ }
157
+ if (filter.host) {
158
+ filtered = filtered.filter(e => e.host === filter.host);
159
+ }
160
+ if (contentTypeSet) {
161
+ filtered = filtered.filter(e => {
162
+ const headers = e.responseHeaders || e.requestHeaders;
163
+ const contentType = headers?.["content-type"] || headers?.["Content-Type"] || "";
164
+ for (const type of contentTypeSet) {
165
+ switch (type) {
166
+ case "JSON":
167
+ if (contentType.includes("json")) return true;
168
+ break;
169
+ case "XML":
170
+ if (contentType.includes("xml")) return true;
171
+ break;
172
+ case "HTML":
173
+ if (contentType.includes("html")) return true;
174
+ break;
175
+ case "TEXT":
176
+ if (contentType.includes("text")) return true;
177
+ break;
178
+ case "IMAGE":
179
+ if (contentType.includes("image")) return true;
180
+ break;
181
+ case "VIDEO":
182
+ if (contentType.includes("video")) return true;
183
+ break;
184
+ case "AUDIO":
185
+ if (contentType.includes("audio")) return true;
186
+ break;
187
+ case "FORM":
188
+ if (contentType.includes("form")) return true;
189
+ break;
190
+ case "OTHER":
191
+ 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")) {
192
+ return true;
193
+ }
194
+ break;
195
+ }
196
+ }
197
+ return false;
198
+ });
199
+ }
200
+ return filtered;
201
+ }, [events, filter, searchLower, methodSet, contentTypeSet]);
202
+
203
+ // For free users, determine which events are "locked" (beyond the limit)
204
+ // Based on original event order, not filtered results - so locked events stay locked even when searching
205
+ const lockedEventIds = useMemo(() => {
206
+ if (isPro) {
207
+ return new Set();
208
+ }
209
+ // Events beyond the limit in the ORIGINAL events array are locked
210
+ const lockedIds = new Set();
211
+ events.slice(FREE_TIER_REQUEST_LIMIT).forEach(event => {
212
+ lockedIds.add(event.id);
213
+ });
214
+ return lockedIds;
215
+ }, [events, isPro]);
216
+
217
+ // Check if there are locked events in the current filtered results
218
+ const lockedEventsInFilter = useMemo(() => {
219
+ return filteredEvents.filter(event => lockedEventIds.has(event.id));
220
+ }, [filteredEvents, lockedEventIds]);
221
+ const hasLockedEvents = lockedEventsInFilter.length > 0;
222
+ const lockedEventCount = lockedEventsInFilter.length;
223
+
224
+ // Helper to check if a specific event is locked
225
+ const isEventLocked = useCallback(eventId => {
226
+ return lockedEventIds.has(eventId);
227
+ }, [lockedEventIds]);
228
+
229
+ // Memoize expensive statistics calculation by categorizing events in single pass
230
+ // Performance: Multiple array.filter operations replaced with single loop for better performance
231
+ const stats = useMemo(() => {
232
+ let successful = 0;
233
+ let failed = 0;
234
+ let pending = 0;
235
+ let totalSent = 0;
236
+ let totalReceived = 0;
237
+ let durationSum = 0;
238
+ let durationCount = 0;
239
+
240
+ // Single pass through events for all statistics
241
+ for (const event of events) {
242
+ // Categorize status
243
+ if (event.status && event.status >= 200 && event.status < 300) {
244
+ successful++;
245
+ } else if (event.error || event.status && event.status >= 400) {
246
+ failed++;
247
+ } else if (!event.status && !event.error) {
248
+ pending++;
249
+ }
250
+
251
+ // Accumulate data sizes
252
+ totalSent += event.requestSize || 0;
253
+ totalReceived += event.responseSize || 0;
254
+
255
+ // Accumulate durations
256
+ if (event.duration) {
257
+ durationSum += event.duration;
258
+ durationCount++;
259
+ }
260
+ }
261
+ const avgDuration = durationCount > 0 ? durationSum / durationCount : 0;
262
+ return {
263
+ totalRequests: events.length,
264
+ successfulRequests: successful,
265
+ failedRequests: failed,
266
+ pendingRequests: pending,
267
+ totalDataSent: totalSent,
268
+ totalDataReceived: totalReceived,
269
+ averageDuration: Math.round(avgDuration)
270
+ };
271
+ }, [events]);
272
+
273
+ // Memoize unique hosts extraction with single pass instead of map + filter + Set
274
+ // Performance: Avoiding array.map().filter() chain, using single loop with Set for deduplication
275
+ const hosts = useMemo(() => {
276
+ const hostSet = new Set();
277
+ for (const event of events) {
278
+ if (event.host) {
279
+ hostSet.add(event.host);
280
+ }
281
+ }
282
+ return Array.from(hostSet);
283
+ }, [events]);
284
+
285
+ // Memoize unique methods extraction with single pass
286
+ // Performance: Avoiding array.map() + Set constructor, using single loop for better performance
287
+ const methods = useMemo(() => {
288
+ const methodSet = new Set();
289
+ for (const event of events) {
290
+ methodSet.add(event.method);
291
+ }
292
+ return Array.from(methodSet);
293
+ }, [events]);
294
+ return {
295
+ /** Filtered events (all shown, but some may be locked for free users) */
296
+ events: filteredEvents,
297
+ /** All events before filtering (for stats) */
298
+ allEvents: events,
299
+ stats,
300
+ filter,
301
+ setFilter,
302
+ clearEvents,
303
+ isEnabled,
304
+ toggleInterception,
305
+ hosts,
306
+ methods,
307
+ /** Whether there are locked events due to free tier limit */
308
+ hasLockedEvents,
309
+ /** Number of events locked due to free tier limit */
310
+ lockedEventCount,
311
+ /** Check if a specific event is locked (by ID) */
312
+ isEventLocked,
313
+ /** Free tier request limit */
314
+ requestLimit: FREE_TIER_REQUEST_LIMIT
315
+ };
316
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
4
+ import { jsx as _jsx } from "react/jsx-runtime";
5
+ const TickContext = /*#__PURE__*/createContext(Date.now());
6
+ export function TickProvider({
7
+ children,
8
+ intervalMs = 60_000
9
+ }) {
10
+ const [tick, setTick] = useState(() => Date.now());
11
+ useEffect(() => {
12
+ const id = setInterval(() => {
13
+ setTick(Date.now());
14
+ }, intervalMs);
15
+ return () => {
16
+ clearInterval(id);
17
+ };
18
+ }, [intervalMs]);
19
+ return /*#__PURE__*/_jsx(TickContext.Provider, {
20
+ value: tick,
21
+ children: children
22
+ });
23
+ }
24
+ export function useTickEveryMinute() {
25
+ const tick = useContext(TickContext);
26
+
27
+ // Expose stable object so consumers can memoize on value changes
28
+ return useMemo(() => tick, [tick]);
29
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Network monitoring section for React Native dev tools
5
+ */
6
+
7
+ // Components
8
+ export { NetworkModal } from "./components/NetworkModal";
9
+ export { NetworkEventDetailView } from "./components/NetworkEventDetailView";
10
+ export { NetworkEventItemCompact } from "./components/NetworkEventItemCompact";
11
+
12
+ // Hooks
13
+ export { useNetworkEvents } from "./hooks/useNetworkEvents";
14
+
15
+ // Utils
16
+ export { networkListener, startNetworkListener, stopNetworkListener, addNetworkListener, removeAllNetworkListeners, isNetworkListening, getNetworkListenerCount } from "./utils/networkListener";
17
+ export { networkEventStore } from "./utils/networkEventStore";
18
+ export { formatBytes, formatDuration, formatHttpStatus } from "./utils/formatting";
19
+
20
+ // Types
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Extract GraphQL operation name from request data
5
+ *
6
+ * This utility extracts the operation name from GraphQL requests to enable
7
+ * searching and filtering by operation name rather than just URL paths.
8
+ *
9
+ * For GraphQL, all requests typically go to the same endpoint (e.g., /graphql),
10
+ * making URL-based search ineffective. This function extracts the actual
11
+ * operation name (e.g., "GetUser", "CreatePost") from the request payload.
12
+ *
13
+ * Extraction Methods (tried in order):
14
+ * 1. Direct operationName field in request data
15
+ * 2. Parse from query string using regex pattern
16
+ *
17
+ * @param requestData - The request data object (typically GraphQL query payload)
18
+ * @returns Operation name string or null if not found
19
+ *
20
+ * @example
21
+ * // Method 1: Explicit operationName field
22
+ * const data1 = {
23
+ * operationName: "GetUser",
24
+ * query: "query GetUser { user { id name } }"
25
+ * };
26
+ * extractOperationName(data1); // Returns "GetUser"
27
+ *
28
+ * @example
29
+ * // Method 2: Parse from query string
30
+ * const data2 = {
31
+ * query: "mutation CreatePost { createPost(title: \"Hello\") { id } }"
32
+ * };
33
+ * extractOperationName(data2); // Returns "CreatePost"
34
+ *
35
+ * @example
36
+ * // No operation name (anonymous query)
37
+ * const data3 = {
38
+ * query: "{ user { id name } }"
39
+ * };
40
+ * extractOperationName(data3); // Returns null
41
+ */
42
+ export function extractOperationName(requestData) {
43
+ // Validate input is an object
44
+ if (!requestData || typeof requestData !== 'object') {
45
+ return null;
46
+ }
47
+
48
+ // Method 1: Check for explicit operationName field
49
+ // This is the standard GraphQL request format
50
+ if ('operationName' in requestData && requestData.operationName) {
51
+ const opName = requestData.operationName;
52
+ // Ensure it's a string and not empty
53
+ if (typeof opName === 'string' && opName.trim().length > 0) {
54
+ return opName.trim();
55
+ }
56
+ }
57
+
58
+ // Method 2: Parse from query string
59
+ // Handles cases where operationName field is missing or null
60
+ if ('query' in requestData && typeof requestData.query === 'string') {
61
+ const query = requestData.query;
62
+
63
+ // Match: query OperationName or mutation OperationName or subscription OperationName
64
+ // Pattern explanation:
65
+ // - (?:query|mutation|subscription) - Match operation type (non-capturing group)
66
+ // - \s+ - One or more whitespace characters
67
+ // - (\w+) - Capture operation name (letters, numbers, underscore)
68
+ const match = query.match(/(?:query|mutation|subscription)\s+(\w+)/);
69
+ if (match && match[1]) {
70
+ return match[1];
71
+ }
72
+ }
73
+
74
+ // No operation name found
75
+ return null;
76
+ }
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Format GraphQL variables for display using arrow notation
5
+ *
6
+ * Mimics React Query key display format to provide visual consistency:
7
+ * - React Query: ["pokemon", "Sandshrew"] → "pokemon › Sandshrew"
8
+ * - GraphQL: GetPokemon(id: "Sandshrew") → "GetPokemon › Sandshrew"
9
+ *
10
+ * Extracts only the values from variables (not keys) and formats them
11
+ * for clean, compact display in the network request list.
12
+ */
13
+
14
+ /**
15
+ * Format a single variable value for display
16
+ *
17
+ * @param value - The variable value to format
18
+ * @param maxLength - Maximum length for string values (default: 30)
19
+ * @returns Formatted string or null if value should be skipped
20
+ */
21
+ function formatValue(value, maxLength = 30) {
22
+ // Null/undefined - skip
23
+ if (value === null || value === undefined) {
24
+ return null;
25
+ }
26
+
27
+ // Boolean
28
+ if (typeof value === 'boolean') {
29
+ return String(value);
30
+ }
31
+
32
+ // Number
33
+ if (typeof value === 'number') {
34
+ return String(value);
35
+ }
36
+
37
+ // String
38
+ if (typeof value === 'string') {
39
+ // Truncate long strings
40
+ if (value.length > maxLength) {
41
+ return value.substring(0, maxLength - 1) + '…';
42
+ }
43
+ return value;
44
+ }
45
+
46
+ // Array - take first value or show count
47
+ if (Array.isArray(value)) {
48
+ if (value.length === 0) return null;
49
+ if (value.length === 1) {
50
+ return formatValue(value[0], maxLength);
51
+ }
52
+ // Multiple items - show count
53
+ return `${value.length} items`;
54
+ }
55
+
56
+ // Object - extract first string/number value
57
+ if (typeof value === 'object') {
58
+ const entries = Object.entries(value);
59
+ if (entries.length === 0) return null;
60
+
61
+ // Find first meaningful value
62
+ for (const [_, v] of entries) {
63
+ const formatted = formatValue(v, maxLength);
64
+ if (formatted) {
65
+ return formatted;
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * Extract variable values from GraphQL variables object
75
+ *
76
+ * Returns an array of formatted values for display with arrow notation.
77
+ *
78
+ * @param variables - GraphQL variables object
79
+ * @param maxValues - Maximum number of values to show (default: 3)
80
+ * @returns Array of formatted variable values
81
+ *
82
+ * @example
83
+ * formatGraphQLVariables({ id: "pikachu" })
84
+ * // Returns: ["pikachu"]
85
+ *
86
+ * @example
87
+ * formatGraphQLVariables({ userId: 123, includeProfile: true })
88
+ * // Returns: ["123", "true"]
89
+ *
90
+ * @example
91
+ * formatGraphQLVariables({ filter: { status: "active" }, limit: 10 })
92
+ * // Returns: ["active", "10"]
93
+ */
94
+ export function formatGraphQLVariables(variables, maxValues = 3) {
95
+ if (!variables || typeof variables !== 'object') {
96
+ return null;
97
+ }
98
+ const values = [];
99
+
100
+ // Extract values from variables object
101
+ for (const [_, value] of Object.entries(variables)) {
102
+ const formatted = formatValue(value);
103
+ if (formatted) {
104
+ values.push(formatted);
105
+ }
106
+
107
+ // Stop if we've reached max values
108
+ if (values.length >= maxValues) {
109
+ break;
110
+ }
111
+ }
112
+ return values.length > 0 ? values : null;
113
+ }
114
+
115
+ /**
116
+ * Combine operation name with variables using arrow notation
117
+ *
118
+ * Matches React Query display pattern: "pokemon › Sandshrew"
119
+ * This provides visual consistency across all dev tools.
120
+ *
121
+ * @param operationName - GraphQL operation name (e.g., "GetPokemon")
122
+ * @param variables - GraphQL variables object
123
+ * @returns Formatted string with arrow notation
124
+ *
125
+ * @example
126
+ * formatGraphQLDisplay("GetPokemon", { id: "Sandshrew" })
127
+ * // Returns: "GetPokemon › Sandshrew"
128
+ *
129
+ * @example
130
+ * formatGraphQLDisplay("GetUser", { userId: 123, includeProfile: true })
131
+ * // Returns: "GetUser › 123 › true"
132
+ *
133
+ * @example
134
+ * formatGraphQLDisplay("GetPosts", { status: "published", limit: 10, offset: 0 })
135
+ * // Returns: "GetPosts › published › 10 › 0" (first 3 values by default)
136
+ *
137
+ * @example
138
+ * formatGraphQLDisplay("GetCurrentUser", {})
139
+ * // Returns: "GetCurrentUser" (no variables)
140
+ */
141
+ export function formatGraphQLDisplay(operationName, variables) {
142
+ const values = formatGraphQLVariables(variables);
143
+ if (!values || values.length === 0) {
144
+ // No variables - just operation name
145
+ return operationName;
146
+ }
147
+
148
+ // Combine: "GetPokemon › Sandshrew" (matches React Query pattern)
149
+ return [operationName, ...values].join(" › ");
150
+ }
151
+
152
+ /**
153
+ * Search GraphQL variables for a given text
154
+ *
155
+ * Recursively searches through all variable values to find matches.
156
+ * Used for filtering GraphQL requests by variable content.
157
+ *
158
+ * @param variables - GraphQL variables object
159
+ * @param searchText - Text to search for (already lowercased)
160
+ * @returns true if search text found in any variable value
161
+ *
162
+ * @example
163
+ * searchGraphQLVariables({ id: "pikachu" }, "pika")
164
+ * // Returns: true
165
+ *
166
+ * @example
167
+ * searchGraphQLVariables({ userId: 123, name: "John" }, "123")
168
+ * // Returns: true
169
+ */
170
+ export function searchGraphQLVariables(variables, searchText) {
171
+ if (!variables || typeof variables !== 'object') {
172
+ return false;
173
+ }
174
+
175
+ // Search through all variable values
176
+ for (const value of Object.values(variables)) {
177
+ if (searchValue(value, searchText)) {
178
+ return true;
179
+ }
180
+ }
181
+ return false;
182
+ }
183
+
184
+ /**
185
+ * Recursively search a value for matching text
186
+ */
187
+ function searchValue(value, searchText) {
188
+ // String - direct match
189
+ if (typeof value === 'string') {
190
+ return value.toLowerCase().includes(searchText);
191
+ }
192
+
193
+ // Number - convert to string and match
194
+ if (typeof value === 'number') {
195
+ return String(value).includes(searchText);
196
+ }
197
+
198
+ // Boolean - convert to string and match
199
+ if (typeof value === 'boolean') {
200
+ return String(value).includes(searchText);
201
+ }
202
+
203
+ // Array - search each item
204
+ if (Array.isArray(value)) {
205
+ return value.some(item => searchValue(item, searchText));
206
+ }
207
+
208
+ // Object - search nested values
209
+ if (typeof value === 'object' && value !== null) {
210
+ return Object.values(value).some(v => searchValue(v, searchText));
211
+ }
212
+ return false;
213
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Formatting utilities for network events
5
+ * Re-exports shared utilities to maintain backward compatibility
6
+ */
7
+
8
+ // Re-export shared formatting utilities
9
+ export { formatBytes, formatDuration, formatHttpStatus, getMethodColor } from "@buoy-gg/shared-ui";