@buoy-gg/events 2.1.1 → 2.1.3

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 (47) hide show
  1. package/lib/commonjs/components/EventsCopySettingsView.js +39 -8
  2. package/lib/commonjs/components/EventsModal.js +4 -7
  3. package/lib/commonjs/components/UnifiedEventDetail.js +32 -1
  4. package/lib/commonjs/components/UnifiedEventFilters.js +50 -21
  5. package/lib/commonjs/components/UnifiedEventItem.js +6 -1
  6. package/lib/commonjs/hooks/useUnifiedEvents.js +137 -28
  7. package/lib/commonjs/index.js +6 -0
  8. package/lib/commonjs/stores/unifiedEventStore.js +61 -16
  9. package/lib/commonjs/types/copySettings.js +29 -0
  10. package/lib/commonjs/utils/autoDiscoverEventSources.js +261 -39
  11. package/lib/commonjs/utils/badgeSelectionStorage.js +32 -0
  12. package/lib/commonjs/utils/eventExportFormatter.js +778 -1
  13. package/lib/module/components/EventsCopySettingsView.js +40 -9
  14. package/lib/module/components/EventsModal.js +5 -8
  15. package/lib/module/components/UnifiedEventDetail.js +32 -1
  16. package/lib/module/components/UnifiedEventFilters.js +50 -21
  17. package/lib/module/components/UnifiedEventItem.js +6 -1
  18. package/lib/module/hooks/useUnifiedEvents.js +140 -31
  19. package/lib/module/index.js +4 -1
  20. package/lib/module/stores/unifiedEventStore.js +58 -16
  21. package/lib/module/types/copySettings.js +29 -0
  22. package/lib/module/utils/autoDiscoverEventSources.js +260 -39
  23. package/lib/module/utils/badgeSelectionStorage.js +30 -0
  24. package/lib/module/utils/eventExportFormatter.js +777 -1
  25. package/lib/typescript/components/UnifiedEventFilters.d.ts +3 -0
  26. package/lib/typescript/hooks/useUnifiedEvents.d.ts +2 -0
  27. package/lib/typescript/index.d.ts +1 -1
  28. package/lib/typescript/stores/unifiedEventStore.d.ts +18 -2
  29. package/lib/typescript/types/copySettings.d.ts +25 -1
  30. package/lib/typescript/types/index.d.ts +3 -1
  31. package/lib/typescript/utils/autoDiscoverEventSources.d.ts +17 -0
  32. package/lib/typescript/utils/badgeSelectionStorage.d.ts +9 -0
  33. package/lib/typescript/utils/eventExportFormatter.d.ts +4 -0
  34. package/package.json +3 -3
  35. package/src/components/EventsCopySettingsView.tsx +41 -5
  36. package/src/components/EventsModal.tsx +6 -17
  37. package/src/components/UnifiedEventDetail.tsx +28 -0
  38. package/src/components/UnifiedEventFilters.tsx +88 -21
  39. package/src/components/UnifiedEventItem.tsx +5 -0
  40. package/src/hooks/useUnifiedEvents.ts +153 -25
  41. package/src/index.tsx +4 -0
  42. package/src/stores/unifiedEventStore.ts +58 -12
  43. package/src/types/copySettings.ts +31 -1
  44. package/src/types/index.ts +4 -1
  45. package/src/utils/autoDiscoverEventSources.ts +268 -44
  46. package/src/utils/badgeSelectionStorage.ts +30 -0
  47. package/src/utils/eventExportFormatter.ts +797 -0
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { useCallback, useEffect, useMemo, useState } from "react";
11
11
  import { Text, StyleSheet, ScrollView, View, TouchableOpacity } from "react-native";
12
- import { DynamicFilterView, macOSColors, FileText, FileCode, Hash, Zap, AlertTriangle, Settings, Eye, ChevronDown, ChevronUp, XCircle, ToolbarCopyButton, useFeatureGate, ProFeatureBanner } from "@buoy-gg/shared-ui";
12
+ import { DynamicFilterView, macOSColors, FileText, FileCode, Hash, Zap, AlertTriangle, Settings, Eye, ChevronDown, ChevronUp, XCircle, GitBranch, ToolbarCopyButton, useFeatureGate, ProFeatureBanner } from "@buoy-gg/shared-ui";
13
13
  import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
14
14
  import { DEFAULT_COPY_SETTINGS, COPY_PRESETS, PRESET_METADATA, detectActivePreset } from "../types/copySettings";
15
15
  import { generateExport, estimateExportSize, getExportSummary } from "../utils/eventExportFormatter";
@@ -263,14 +263,19 @@ export function EventsCopySettingsView({
263
263
  data: JSON.parse(exportText),
264
264
  title: "",
265
265
  showTypeFilter: false
266
- }) : /*#__PURE__*/_jsx(ScrollView, {
267
- style: styles.previewScroll,
268
- nestedScrollEnabled: true,
269
- children: /*#__PURE__*/_jsx(Text, {
270
- style: styles.previewText,
271
- selectable: isPro,
272
- children: exportText
273
- })
266
+ }) : /*#__PURE__*/_jsxs(View, {
267
+ children: [settings.format === "mermaid" && /*#__PURE__*/_jsx(Text, {
268
+ style: styles.mermaidHint,
269
+ children: "\uD83D\uDCA1 Paste into mermaid.live to visualize"
270
+ }), /*#__PURE__*/_jsx(ScrollView, {
271
+ style: styles.previewScroll,
272
+ nestedScrollEnabled: true,
273
+ children: /*#__PURE__*/_jsx(Text, {
274
+ style: styles.previewText,
275
+ selectable: isPro,
276
+ children: exportText
277
+ })
278
+ })]
274
279
  })]
275
280
  });
276
281
  }, [isPreviewExpanded, previewEvents, settings, estimatedSize, sizeWarningLevel, hasLiveData, generateCopyText, isPro]);
@@ -337,6 +342,14 @@ export function EventsCopySettingsView({
337
342
  value: "minimal",
338
343
  isActive: activePreset === "minimal",
339
344
  description: PRESET_METADATA.minimal.description
345
+ }, {
346
+ id: "preset::mermaid",
347
+ label: PRESET_METADATA.mermaid.label,
348
+ icon: GitBranch,
349
+ color: PRESET_METADATA.mermaid.color,
350
+ value: "mermaid",
351
+ isActive: activePreset === "mermaid",
352
+ description: PRESET_METADATA.mermaid.description
340
353
  }, {
341
354
  id: "preset::custom",
342
355
  label: "Custom",
@@ -420,6 +433,14 @@ export function EventsCopySettingsView({
420
433
  color: macOSColors.text.secondary,
421
434
  value: "plaintext",
422
435
  isActive: settings.format === "plaintext"
436
+ }, {
437
+ id: "format::mermaid",
438
+ label: "Diagram",
439
+ icon: GitBranch,
440
+ color: "#A855F7",
441
+ value: "mermaid",
442
+ isActive: settings.format === "mermaid",
443
+ description: "Mermaid sequence diagram"
423
444
  }]
424
445
  },
425
446
  // Timestamp Section
@@ -621,6 +642,16 @@ const styles = StyleSheet.create({
621
642
  color: macOSColors.text.primary,
622
643
  lineHeight: 18
623
644
  },
645
+ mermaidHint: {
646
+ fontSize: 11,
647
+ color: macOSColors.text.secondary,
648
+ marginBottom: 8,
649
+ paddingHorizontal: 8,
650
+ paddingVertical: 6,
651
+ backgroundColor: "#A855F7" + "15",
652
+ borderRadius: 6,
653
+ overflow: "hidden"
654
+ },
624
655
  statusLabel: {
625
656
  fontSize: 11,
626
657
  fontWeight: "600",
@@ -10,7 +10,7 @@
10
10
  import { View, StyleSheet, TouchableOpacity, Text, Alert } from "react-native";
11
11
  import { useState, useCallback } from "react";
12
12
  import { useRouter } from "expo-router";
13
- import { JsModal, ModalHeader, buoyColors, Trash2, Power, Copy, devToolsStorageKeys, ToolbarCopyButton, UpgradeModal } from "@buoy-gg/shared-ui";
13
+ import { JsModal, ModalHeader, buoyColors, Trash2, Copy, devToolsStorageKeys, ToolbarCopyButton, UpgradeModal, PowerToggleButton } from "@buoy-gg/shared-ui";
14
14
  import { useUnifiedEvents } from "../hooks/useUnifiedEvents";
15
15
  import { UnifiedEventFilters } from "./UnifiedEventFilters";
16
16
  import { UnifiedEventList } from "./UnifiedEventList";
@@ -143,13 +143,10 @@ export function EventsModal({
143
143
  size: 14,
144
144
  color: totalCount > 0 ? buoyColors.textMuted : buoyColors.textMuted + "50"
145
145
  })
146
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
147
- onPress: toggleCapturing,
148
- style: [styles.headerActionButton, isCapturing ? styles.startButton : styles.stopButton],
149
- children: /*#__PURE__*/_jsx(Power, {
150
- size: 14,
151
- color: isCapturing ? buoyColors.success : buoyColors.error
152
- })
146
+ }), /*#__PURE__*/_jsx(PowerToggleButton, {
147
+ isEnabled: isCapturing,
148
+ onToggle: toggleCapturing,
149
+ accessibilityLabel: "Toggle event capture"
153
150
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
154
151
  onPress: clearEvents,
155
152
  style: [styles.headerActionButton, totalCount === 0 && styles.headerActionButtonDisabled],
@@ -22,7 +22,8 @@ function tryLoadOptionalDetailComponents() {
22
22
  const components = {
23
23
  StorageEventDetailContent: null,
24
24
  ReduxActionDetailContent: null,
25
- NetworkEventDetailView: null
25
+ NetworkEventDetailView: null,
26
+ RenderDetailView: null
26
27
  };
27
28
 
28
29
  // Try to load storage detail component
@@ -51,6 +52,15 @@ function tryLoadOptionalDetailComponents() {
51
52
  } catch {
52
53
  // Optional dependency not installed
53
54
  }
55
+
56
+ // Try to load render detail component
57
+ try {
58
+ // @ts-ignore - Dynamic import that may not exist
59
+ const highlightUpdates = require("@buoy-gg/highlight-updates");
60
+ components.RenderDetailView = highlightUpdates.RenderDetailView;
61
+ } catch {
62
+ // Optional dependency not installed
63
+ }
54
64
  return components;
55
65
  }
56
66
 
@@ -96,6 +106,10 @@ const SOURCE_CONFIG = {
96
106
  route: {
97
107
  label: "Route",
98
108
  color: "#06B6D4"
109
+ },
110
+ render: {
111
+ label: "Render",
112
+ color: "#F472B6"
99
113
  }
100
114
  };
101
115
 
@@ -211,6 +225,23 @@ export const UnifiedEventDetail = /*#__PURE__*/memo(function UnifiedEventDetail(
211
225
  });
212
226
  }
213
227
 
228
+ // For render events, use the RenderDetailView if available
229
+ if (event.source === "render" && optionalComponents.RenderDetailView) {
230
+ const {
231
+ RenderDetailView
232
+ } = optionalComponents;
233
+ const renderData = event.originalEvent;
234
+ return /*#__PURE__*/_jsx(View, {
235
+ style: styles.container,
236
+ children: /*#__PURE__*/_jsx(ScrollView, {
237
+ style: styles.content,
238
+ children: /*#__PURE__*/_jsx(RenderDetailView, {
239
+ render: renderData.render
240
+ })
241
+ })
242
+ });
243
+ }
244
+
214
245
  // Route events expand in-place in the list, so they shouldn't reach this detail view
215
246
  // If they do somehow, fall through to the generic view
216
247
 
@@ -6,6 +6,9 @@
6
6
  * Filter bar showing toggleable badges for each available event source.
7
7
  * Badges are sorted: enabled first, disabled last.
8
8
  * Toggling a badge enables/disables event listening for that source.
9
+ *
10
+ * Badge layout (inspired by React Query DevTools):
11
+ * [subscriber_count] • Label [event_count]
9
12
  */
10
13
 
11
14
  import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from "react-native";
@@ -29,27 +32,45 @@ export const UnifiedEventFilters = /*#__PURE__*/memo(function UnifiedEventFilter
29
32
  contentContainerStyle: styles.badgesRow,
30
33
  children: availableSources.map(source => {
31
34
  const isEnabled = source.enabled !== false;
35
+ const subscriberCount = source.subscriberCount;
36
+ const hasSubscriberTracking = subscriberCount !== undefined;
37
+ const eventCount = source.count || 0;
32
38
  return /*#__PURE__*/_jsxs(TouchableOpacity, {
33
39
  style: [styles.badge, isEnabled ? {
34
- backgroundColor: source.color + "20",
35
- borderColor: source.color + "60"
40
+ backgroundColor: buoyColors.card,
41
+ borderColor: source.color + "50"
36
42
  } : styles.badgeDisabled],
37
43
  onPress: () => onToggleSource(source.source),
38
44
  activeOpacity: 0.7,
39
- children: [/*#__PURE__*/_jsx(View, {
45
+ children: [hasSubscriberTracking && /*#__PURE__*/_jsx(View, {
46
+ style: [styles.countBadge, {
47
+ backgroundColor: isEnabled ? subscriberCount > 0 ? source.color + "25" : buoyColors.textMuted + "20" : buoyColors.textMuted + "15"
48
+ }],
49
+ children: /*#__PURE__*/_jsx(Text, {
50
+ style: [styles.countText, {
51
+ color: isEnabled ? subscriberCount > 0 ? source.color : buoyColors.textMuted : buoyColors.textMuted + "60"
52
+ }],
53
+ children: subscriberCount
54
+ })
55
+ }), /*#__PURE__*/_jsx(View, {
40
56
  style: [styles.dot, {
41
- backgroundColor: isEnabled ? source.color : buoyColors.textMuted + "60"
57
+ backgroundColor: isEnabled ? source.color : buoyColors.textMuted + "40"
42
58
  }]
43
59
  }), /*#__PURE__*/_jsx(Text, {
44
60
  style: [styles.badgeLabel, {
45
- color: isEnabled ? source.color : buoyColors.textMuted + "80"
61
+ color: isEnabled ? buoyColors.text : buoyColors.textMuted + "70"
46
62
  }],
47
63
  children: source.label
48
- }), isEnabled && source.count > 0 && /*#__PURE__*/_jsx(Text, {
49
- style: [styles.badgeCount, {
50
- color: source.color
64
+ }), /*#__PURE__*/_jsx(View, {
65
+ style: [styles.countBadge, {
66
+ backgroundColor: isEnabled ? eventCount > 0 ? source.color + "20" : buoyColors.textMuted + "15" : buoyColors.textMuted + "10"
51
67
  }],
52
- children: source.count
68
+ children: /*#__PURE__*/_jsx(Text, {
69
+ style: [styles.countText, {
70
+ color: isEnabled ? eventCount > 0 ? source.color : buoyColors.textMuted + "80" : buoyColors.textMuted + "50"
71
+ }],
72
+ children: eventCount
73
+ })
53
74
  })]
54
75
  }, source.source);
55
76
  })
@@ -74,30 +95,38 @@ const styles = StyleSheet.create({
74
95
  badge: {
75
96
  flexDirection: "row",
76
97
  alignItems: "center",
77
- paddingHorizontal: 10,
78
- paddingVertical: 6,
79
- borderRadius: 16,
98
+ paddingVertical: 5,
99
+ paddingHorizontal: 6,
100
+ borderRadius: 20,
80
101
  borderWidth: 1,
81
102
  gap: 6
82
103
  },
83
104
  badgeDisabled: {
84
105
  backgroundColor: buoyColors.card,
85
106
  borderColor: buoyColors.border,
86
- opacity: 0.6
107
+ opacity: 0.5
87
108
  },
88
109
  dot: {
89
- width: 6,
90
- height: 6,
91
- borderRadius: 3
110
+ width: 7,
111
+ height: 7,
112
+ borderRadius: 4
92
113
  },
93
114
  badgeLabel: {
94
115
  fontSize: 12,
95
- fontWeight: "600"
116
+ fontWeight: "500"
96
117
  },
97
- badgeCount: {
98
- fontSize: 11,
99
- fontWeight: "500",
100
- opacity: 0.8
118
+ countBadge: {
119
+ minWidth: 18,
120
+ height: 18,
121
+ borderRadius: 9,
122
+ alignItems: "center",
123
+ justifyContent: "center",
124
+ paddingHorizontal: 5
125
+ },
126
+ countText: {
127
+ fontSize: 10,
128
+ fontWeight: "600",
129
+ fontVariant: ["tabular-nums"]
101
130
  },
102
131
  filterInfo: {
103
132
  marginTop: 6,
@@ -106,6 +106,10 @@ const SOURCE_CONFIG = {
106
106
  route: {
107
107
  label: "Route",
108
108
  color: "#06B6D4" // Cyan
109
+ },
110
+ render: {
111
+ label: "Render",
112
+ color: "#F472B6" // Pink
109
113
  }
110
114
  };
111
115
 
@@ -173,7 +177,8 @@ const SOURCE_BADGE_LABELS = {
173
177
  "react-query": "QUERY",
174
178
  "react-query-query": "QUERY",
175
179
  "react-query-mutation": "MUTATION",
176
- route: "ROUTE"
180
+ route: "ROUTE",
181
+ render: "RENDER"
177
182
  };
178
183
 
179
184
  /**
@@ -8,9 +8,9 @@
8
8
  */
9
9
 
10
10
  import { useState, useEffect, useCallback, useMemo, useRef } from "react";
11
- import { useFeatureGate } from "@buoy-gg/shared-ui";
12
- import { subscribe, subscribeToStorage, subscribeToRedux, subscribeToNetwork, subscribeToReactQuery, subscribeToRoutes, unsubscribeAll, getSourceCounts, clearEvents as clearStoreEvents, getAvailableEventSources } from "../stores/unifiedEventStore";
13
- import { saveEnabledSources, loadEnabledSources } from "../utils/badgeSelectionStorage";
11
+ import { useFeatureGate, subscribeToSubscriberCountChanges } from "@buoy-gg/shared-ui";
12
+ import { subscribe, subscribeToStorage, subscribeToRedux, subscribeToNetwork, subscribeToReactQuery, subscribeToRoutes, subscribeToRender, unsubscribeFromStorage, unsubscribeFromRedux, unsubscribeFromNetwork, unsubscribeFromReactQuery, unsubscribeFromRoutes, unsubscribeFromRender, unsubscribeAll, getSourceCounts, clearEvents as clearStoreEvents, getAvailableEventSources, getSubscriberCounts } from "../stores/unifiedEventStore";
13
+ import { saveEnabledSources, loadEnabledSources, saveCapturingState, loadCapturingState } from "../utils/badgeSelectionStorage";
14
14
  import { getSourceDisplayConfig } from "../utils/autoDiscoverEventSources";
15
15
 
16
16
  /** Max events for non-Pro users */
@@ -19,7 +19,7 @@ const FREE_TIER_MAX_EVENTS = 25;
19
19
  /**
20
20
  * All possible sources for display
21
21
  */
22
- const ALL_DISPLAY_SOURCES = ["storage-async", "redux", "network", "react-query-query", "react-query-mutation", "route"];
22
+ const ALL_DISPLAY_SOURCES = ["storage-async", "redux", "network", "react-query-query", "react-query-mutation", "route", "render"];
23
23
 
24
24
  /**
25
25
  * Map display sources to actual event sources they should match
@@ -32,7 +32,8 @@ const SOURCE_TO_EVENT_SOURCES = {
32
32
  "react-query": ["react-query", "react-query-query"],
33
33
  "react-query-query": ["react-query", "react-query-query"],
34
34
  "react-query-mutation": ["react-query-mutation"],
35
- route: ["route"]
35
+ route: ["route"],
36
+ render: ["render"]
36
37
  };
37
38
 
38
39
  /**
@@ -46,7 +47,8 @@ const EVENT_SOURCE_TO_DISCOVERY_ID = {
46
47
  "react-query": "react-query",
47
48
  "react-query-query": "react-query",
48
49
  "react-query-mutation": "react-query",
49
- route: "route-events"
50
+ route: "route-events",
51
+ render: "render"
50
52
  };
51
53
  export function useUnifiedEvents() {
52
54
  const {
@@ -62,6 +64,14 @@ export function useUnifiedEvents() {
62
64
  return getAvailableEventSources();
63
65
  }, []);
64
66
 
67
+ // Subscribe to subscriber count changes for instant UI updates (TanStack Query pattern)
68
+ const [subscriberCountVersion, setSubscriberCountVersion] = useState(0);
69
+ useEffect(() => {
70
+ return subscribeToSubscriberCountChanges(() => {
71
+ setSubscriberCountVersion(v => v + 1);
72
+ });
73
+ }, []);
74
+
65
75
  // Get available display sources (only show sources for installed packages)
66
76
  const availableDisplaySources = useMemo(() => {
67
77
  return ALL_DISPLAY_SOURCES.filter(displaySource => {
@@ -71,39 +81,50 @@ export function useUnifiedEvents() {
71
81
  });
72
82
  }, [discoveredSources]);
73
83
 
74
- // Subscribe to store changes and start capturing on mount
84
+ // Subscribe to store changes (always subscribe to get events when they come)
75
85
  useEffect(() => {
76
- // Subscribe to store updates
77
86
  const unsubscribe = subscribe(newEvents => {
78
87
  setEvents(newEvents);
79
88
  });
80
-
81
- // Start capturing from all available sources
82
- // These functions are now safe - they check if the source is available
83
- subscribeToStorage();
84
- subscribeToRedux();
85
- subscribeToNetwork();
86
- subscribeToReactQuery();
87
- subscribeToRoutes();
88
89
  return unsubscribe;
89
90
  }, []);
90
91
 
91
- // Restore saved badge selection state on mount
92
+ // Restore saved state on mount (badge selection + capturing state)
92
93
  useEffect(() => {
93
94
  const restoreState = async () => {
95
+ // Load badge selection
94
96
  const savedSources = await loadEnabledSources();
97
+ let sourcesToEnable;
95
98
  if (savedSources && savedSources.length > 0) {
96
99
  // Filter to only include valid sources that still exist and are available
97
100
  const validSources = savedSources.filter(s => availableDisplaySources.includes(s));
98
101
  if (validSources.length > 0) {
99
- setEnabledSources(new Set(validSources));
102
+ sourcesToEnable = new Set(validSources);
100
103
  } else {
101
104
  // If no saved sources are valid, enable all available sources
102
- setEnabledSources(new Set(availableDisplaySources));
105
+ sourcesToEnable = new Set(availableDisplaySources);
103
106
  }
104
107
  } else {
105
108
  // No saved state - enable all available sources
106
- setEnabledSources(new Set(availableDisplaySources));
109
+ sourcesToEnable = new Set(availableDisplaySources);
110
+ }
111
+ setEnabledSources(sourcesToEnable);
112
+
113
+ // Load capturing state (default to true if not saved)
114
+ const savedCapturing = await loadCapturingState();
115
+ const shouldCapture = savedCapturing !== null ? savedCapturing : true;
116
+ setIsCapturing(shouldCapture);
117
+
118
+ // Start capturing if enabled - only subscribe to enabled sources
119
+ if (shouldCapture) {
120
+ if (sourcesToEnable.has("storage-async")) subscribeToStorage();
121
+ if (sourcesToEnable.has("redux")) subscribeToRedux();
122
+ if (sourcesToEnable.has("network")) subscribeToNetwork();
123
+ if (sourcesToEnable.has("react-query-query") || sourcesToEnable.has("react-query-mutation")) {
124
+ subscribeToReactQuery();
125
+ }
126
+ if (sourcesToEnable.has("route")) subscribeToRoutes();
127
+ if (sourcesToEnable.has("render")) subscribeToRender();
107
128
  }
108
129
  isStateRestoredRef.current = true;
109
130
  };
@@ -118,6 +139,14 @@ export function useUnifiedEvents() {
118
139
  saveEnabledSources(Array.from(enabledSources));
119
140
  }, [enabledSources]);
120
141
 
142
+ // Persist capturing state whenever it changes (after initial restoration)
143
+ useEffect(() => {
144
+ if (!isStateRestoredRef.current) {
145
+ return;
146
+ }
147
+ saveCapturingState(isCapturing);
148
+ }, [isCapturing]);
149
+
121
150
  // Build set of all event sources that should be shown
122
151
  const allowedEventSources = useMemo(() => {
123
152
  const allowed = new Set();
@@ -157,15 +186,40 @@ export function useUnifiedEvents() {
157
186
  // Get all available sources with counts, sorted: enabled first, disabled last
158
187
  const availableSources = useMemo(() => {
159
188
  const counts = getSourceCounts();
189
+ const subscriberCounts = getSubscriberCounts();
190
+
191
+ // Build a map from source ID to subscriber count
192
+ const subscriberCountBySourceId = {};
193
+ for (const source of subscriberCounts.sources) {
194
+ subscriberCountBySourceId[source.sourceId] = source.counts.total;
195
+ }
196
+
197
+ // Map display source to store source ID
198
+ const displaySourceToStoreId = {
199
+ "storage-async": "storage",
200
+ "storage-mmkv": "storage",
201
+ redux: "redux",
202
+ network: "network",
203
+ "react-query": "react-query",
204
+ "react-query-query": "react-query",
205
+ "react-query-mutation": "react-query",
206
+ route: "route-events",
207
+ render: "render"
208
+ };
160
209
  const sources = availableDisplaySources.map(source => {
161
210
  const eventSources = SOURCE_TO_EVENT_SOURCES[source] || [source];
162
211
  const totalCount = eventSources.reduce((sum, s) => sum + (counts[s] || 0), 0);
163
212
  const displayConfig = getSourceDisplayConfig(source);
213
+ const storeId = displaySourceToStoreId[source];
214
+ // Only set subscriberCount if the source has subscriber tracking
215
+ // (undefined means no tracking, vs 0 which means tracked but no subscribers)
216
+ const subscriberCount = storeId && storeId in subscriberCountBySourceId ? subscriberCountBySourceId[storeId] : undefined;
164
217
  return {
165
218
  source,
166
219
  ...displayConfig,
167
220
  count: totalCount,
168
- enabled: enabledSources.has(source)
221
+ enabled: enabledSources.has(source),
222
+ subscriberCount
169
223
  };
170
224
  });
171
225
 
@@ -175,18 +229,65 @@ export function useUnifiedEvents() {
175
229
  if (!a.enabled && b.enabled) return 1;
176
230
  return 0;
177
231
  });
178
- }, [events, enabledSources, availableDisplaySources]);
232
+ }, [events, enabledSources, availableDisplaySources, subscriberCountVersion]);
179
233
  const toggleSource = useCallback(source => {
180
234
  setEnabledSources(prev => {
181
235
  const next = new Set(prev);
182
- if (next.has(source)) {
236
+ const wasEnabled = next.has(source);
237
+ if (wasEnabled) {
183
238
  next.delete(source);
239
+ // Unsubscribe from the source
240
+ switch (source) {
241
+ case "storage-async":
242
+ unsubscribeFromStorage();
243
+ break;
244
+ case "redux":
245
+ unsubscribeFromRedux();
246
+ break;
247
+ case "network":
248
+ unsubscribeFromNetwork();
249
+ break;
250
+ case "react-query-query":
251
+ case "react-query-mutation":
252
+ unsubscribeFromReactQuery();
253
+ break;
254
+ case "route":
255
+ unsubscribeFromRoutes();
256
+ break;
257
+ case "render":
258
+ unsubscribeFromRender();
259
+ break;
260
+ }
184
261
  } else {
185
262
  next.add(source);
263
+ // Subscribe to the source (only if capturing is active)
264
+ if (isCapturing) {
265
+ switch (source) {
266
+ case "storage-async":
267
+ subscribeToStorage();
268
+ break;
269
+ case "redux":
270
+ subscribeToRedux();
271
+ break;
272
+ case "network":
273
+ subscribeToNetwork();
274
+ break;
275
+ case "react-query-query":
276
+ case "react-query-mutation":
277
+ subscribeToReactQuery();
278
+ break;
279
+ case "route":
280
+ subscribeToRoutes();
281
+ break;
282
+ case "render":
283
+ subscribeToRender();
284
+ break;
285
+ }
286
+ }
186
287
  }
187
288
  return next;
188
289
  });
189
- }, []);
290
+ }, [isCapturing]);
190
291
  const enableAllSources = useCallback(() => {
191
292
  setEnabledSources(new Set(availableDisplaySources));
192
293
  }, [availableDisplaySources]);
@@ -194,13 +295,17 @@ export function useUnifiedEvents() {
194
295
  clearStoreEvents();
195
296
  }, []);
196
297
  const startCapturing = useCallback(() => {
197
- subscribeToStorage();
198
- subscribeToRedux();
199
- subscribeToNetwork();
200
- subscribeToReactQuery();
201
- subscribeToRoutes();
298
+ // Only subscribe to sources that are enabled
299
+ if (enabledSources.has("storage-async")) subscribeToStorage();
300
+ if (enabledSources.has("redux")) subscribeToRedux();
301
+ if (enabledSources.has("network")) subscribeToNetwork();
302
+ if (enabledSources.has("react-query-query") || enabledSources.has("react-query-mutation")) {
303
+ subscribeToReactQuery();
304
+ }
305
+ if (enabledSources.has("route")) subscribeToRoutes();
306
+ if (enabledSources.has("render")) subscribeToRender();
202
307
  setIsCapturing(true);
203
- }, []);
308
+ }, [enabledSources]);
204
309
  const stopCapturing = useCallback(() => {
205
310
  unsubscribeAll();
206
311
  setIsCapturing(false);
@@ -212,6 +317,9 @@ export function useUnifiedEvents() {
212
317
  startCapturing();
213
318
  }
214
319
  }, [isCapturing, startCapturing, stopCapturing]);
320
+
321
+ // Get subscriber counts (recalculates on every render, but it's a cheap operation)
322
+ const totalSubscriberCount = getSubscriberCounts().totalSubscribers;
215
323
  return {
216
324
  events,
217
325
  filteredEvents,
@@ -228,7 +336,8 @@ export function useUnifiedEvents() {
228
336
  toggleCapturing,
229
337
  discoveredSources,
230
338
  hiddenEventsCount,
231
- isPro
339
+ isPro,
340
+ totalSubscriberCount
232
341
  };
233
342
  }
234
343
  //# sourceMappingURL=useUnifiedEvents.js.map
@@ -54,7 +54,7 @@ export { findRelatedEvents, hasRelatedEvents, getRelatedEventsCount, buildCorrel
54
54
  // =============================================================================
55
55
  // EXPORT UTILITIES (For generating formatted exports)
56
56
  // =============================================================================
57
- export { generateExport, generateMarkdownExport, generateJsonExport, generatePlaintextExport, estimateExportSize, getExportSummary, filterEvents } from "./utils/eventExportFormatter";
57
+ export { generateExport, generateMarkdownExport, generateJsonExport, generatePlaintextExport, generateMermaidExport, estimateExportSize, getExportSummary, filterEvents } from "./utils/eventExportFormatter";
58
58
  // =============================================================================
59
59
  // COPY SETTINGS STORAGE (For persisting export preferences)
60
60
  // =============================================================================
@@ -74,4 +74,7 @@ export { loadCopySettings, saveCopySettings, clearCopySettings } from "./utils/c
74
74
  // - subscribeToRoutes, unsubscribeFromRoutes (internal subscription)
75
75
  // - getEvents, getActiveSources, getSourceCounts (internal store operations)
76
76
  // - clearEvents, subscribe, getEventCount (internal store operations)
77
+
78
+ // Note: For subscriber count notifications, use @buoy-gg/shared-ui:
79
+ // import { notifySubscriberCountChange, subscribeToSubscriberCountChanges } from "@buoy-gg/shared-ui";
77
80
  //# sourceMappingURL=index.js.map