@buoy-gg/events 3.0.1 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/components/EventsCopySettingsView.js.map +1 -0
- package/lib/commonjs/components/EventsModal.js.map +1 -0
- package/lib/commonjs/components/ReactQueryEventDetail.js.map +1 -0
- package/lib/commonjs/components/UnifiedEventDetail.js +34 -10
- package/lib/commonjs/components/UnifiedEventDetail.js.map +1 -0
- package/lib/commonjs/components/UnifiedEventFilters.js.map +1 -0
- package/lib/commonjs/components/UnifiedEventItem.js.map +1 -0
- package/lib/commonjs/components/UnifiedEventList.js.map +1 -0
- package/lib/commonjs/components/UnifiedEventViewer.js.map +1 -0
- package/lib/commonjs/hooks/useUnifiedEvents.js +57 -27
- package/lib/commonjs/hooks/useUnifiedEvents.js.map +1 -0
- package/lib/commonjs/index.js +21 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/preset.js.map +1 -0
- package/lib/commonjs/sources/sourceIds.js +30 -0
- package/lib/commonjs/sources/sourceIds.js.map +1 -0
- package/lib/commonjs/stores/unifiedEventStore.js +185 -14
- package/lib/commonjs/stores/unifiedEventStore.js.map +1 -0
- package/lib/commonjs/sync/eventsSyncAdapter.js +62 -0
- package/lib/commonjs/sync/eventsSyncAdapter.js.map +1 -0
- package/lib/commonjs/types/copySettings.js.map +1 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/commonjs/utils/autoDiscoverEventSources.js.map +1 -0
- package/lib/commonjs/utils/badgeSelectionStorage.js.map +1 -0
- package/lib/commonjs/utils/copySettingsStorage.js.map +1 -0
- package/lib/commonjs/utils/correlationUtils.js.map +1 -0
- package/lib/commonjs/utils/eventExportFormatter.js.map +1 -0
- package/lib/commonjs/utils/eventTransformers.js.map +1 -0
- package/lib/module/components/EventsCopySettingsView.js.map +1 -0
- package/lib/module/components/EventsModal.js.map +1 -0
- package/lib/module/components/ReactQueryEventDetail.js.map +1 -0
- package/lib/module/components/UnifiedEventDetail.js +36 -12
- package/lib/module/components/UnifiedEventDetail.js.map +1 -0
- package/lib/module/components/UnifiedEventFilters.js.map +1 -0
- package/lib/module/components/UnifiedEventItem.js.map +1 -0
- package/lib/module/components/UnifiedEventList.js.map +1 -0
- package/lib/module/components/UnifiedEventViewer.js.map +1 -0
- package/lib/module/hooks/useUnifiedEvents.js +59 -29
- package/lib/module/hooks/useUnifiedEvents.js.map +1 -0
- package/lib/module/index.js +9 -1
- package/lib/module/index.js.map +1 -0
- package/lib/module/preset.js.map +1 -0
- package/lib/module/sources/sourceIds.js +26 -0
- package/lib/module/sources/sourceIds.js.map +1 -0
- package/lib/module/stores/unifiedEventStore.js +185 -14
- package/lib/module/stores/unifiedEventStore.js.map +1 -0
- package/lib/module/sync/eventsSyncAdapter.js +58 -0
- package/lib/module/sync/eventsSyncAdapter.js.map +1 -0
- package/lib/module/types/copySettings.js.map +1 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/utils/autoDiscoverEventSources.js.map +1 -0
- package/lib/module/utils/badgeSelectionStorage.js.map +1 -0
- package/lib/module/utils/copySettingsStorage.js.map +1 -0
- package/lib/module/utils/correlationUtils.js.map +1 -0
- package/lib/module/utils/eventExportFormatter.js.map +1 -0
- package/lib/module/utils/eventTransformers.js.map +1 -0
- package/lib/typescript/components/EventsCopySettingsView.d.ts.map +1 -0
- package/lib/typescript/components/EventsModal.d.ts.map +1 -0
- package/lib/typescript/components/ReactQueryEventDetail.d.ts.map +1 -0
- package/lib/typescript/components/UnifiedEventDetail.d.ts +20 -0
- package/lib/typescript/components/UnifiedEventDetail.d.ts.map +1 -0
- package/lib/typescript/components/UnifiedEventFilters.d.ts.map +1 -0
- package/lib/typescript/components/UnifiedEventItem.d.ts.map +1 -0
- package/lib/typescript/components/UnifiedEventList.d.ts.map +1 -0
- package/lib/typescript/components/UnifiedEventViewer.d.ts.map +1 -0
- package/lib/typescript/hooks/useUnifiedEvents.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +5 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/lib/typescript/sources/sourceIds.d.ts +13 -0
- package/lib/typescript/sources/sourceIds.d.ts.map +1 -0
- package/lib/typescript/stores/unifiedEventStore.d.ts +49 -2
- package/lib/typescript/stores/unifiedEventStore.d.ts.map +1 -0
- package/lib/typescript/sync/eventsSyncAdapter.d.ts +37 -0
- package/lib/typescript/sync/eventsSyncAdapter.d.ts.map +1 -0
- package/lib/typescript/types/copySettings.d.ts.map +1 -0
- package/lib/typescript/types/index.d.ts.map +1 -0
- package/lib/typescript/utils/autoDiscoverEventSources.d.ts.map +1 -0
- package/lib/typescript/utils/badgeSelectionStorage.d.ts.map +1 -0
- package/lib/typescript/utils/copySettingsStorage.d.ts.map +1 -0
- package/lib/typescript/utils/correlationUtils.d.ts.map +1 -0
- package/lib/typescript/utils/eventExportFormatter.d.ts.map +1 -0
- package/lib/typescript/utils/eventTransformers.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/components/UnifiedEventDetail.tsx +51 -7
- package/src/hooks/useUnifiedEvents.ts +74 -28
- package/src/index.tsx +9 -1
- package/src/sources/sourceIds.ts +25 -0
- package/src/stores/unifiedEventStore.ts +197 -16
- package/src/sync/eventsSyncAdapter.ts +56 -0
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { View, Text, StyleSheet, ScrollView } from "react-native";
|
|
10
|
-
import { memo, useMemo, type ComponentType } from "react";
|
|
10
|
+
import { memo, useMemo, type ComponentType, type ReactNode } from "react";
|
|
11
11
|
import {
|
|
12
12
|
buoyColors,
|
|
13
13
|
formatRelativeTime,
|
|
14
14
|
copyToClipboard,
|
|
15
15
|
parseValue,
|
|
16
|
+
useIgnoredPatterns,
|
|
16
17
|
} from "@buoy-gg/shared-ui";
|
|
17
18
|
import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
|
|
18
19
|
import { ReactQueryEventDetail } from "./ReactQueryEventDetail";
|
|
@@ -44,6 +45,8 @@ interface OptionalDetailComponents {
|
|
|
44
45
|
}> | null;
|
|
45
46
|
NetworkEventDetailView: ComponentType<{
|
|
46
47
|
event: unknown;
|
|
48
|
+
ignoredPatterns?: Set<string>;
|
|
49
|
+
onTogglePattern?: (value: string) => void;
|
|
47
50
|
}> | null;
|
|
48
51
|
RenderDetailView: ComponentType<{
|
|
49
52
|
render: unknown;
|
|
@@ -183,17 +186,46 @@ function getValueType(
|
|
|
183
186
|
// Component
|
|
184
187
|
// ============================================================================
|
|
185
188
|
|
|
189
|
+
/** Passthrough used when no network detail wrapper is injected. */
|
|
190
|
+
function PassThrough({ children }: { children: ReactNode }) {
|
|
191
|
+
return <>{children}</>;
|
|
192
|
+
}
|
|
193
|
+
|
|
186
194
|
interface UnifiedEventDetailProps {
|
|
187
195
|
event: UnifiedEvent;
|
|
188
196
|
onBack?: () => void;
|
|
197
|
+
/**
|
|
198
|
+
* Optional override for the network detail component. The desktop dashboard
|
|
199
|
+
* injects the real `@buoy-gg/network` export here because the runtime
|
|
200
|
+
* `require("@buoy-gg/network")` fallback is unreliable in its Vite/ESM build.
|
|
201
|
+
* When omitted, the dynamically-required component is used (works on mobile).
|
|
202
|
+
*/
|
|
203
|
+
NetworkEventDetailViewComponent?: ComponentType<{
|
|
204
|
+
event: unknown;
|
|
205
|
+
ignoredPatterns?: Set<string>;
|
|
206
|
+
onTogglePattern?: (value: string) => void;
|
|
207
|
+
}>;
|
|
208
|
+
/**
|
|
209
|
+
* Optional wrapper rendered around the network detail view — e.g. the desktop
|
|
210
|
+
* passes `NetworkBodyResolverProvider` so large request/response bodies that
|
|
211
|
+
* were stripped from the sync snapshot can be fetched from the device on demand.
|
|
212
|
+
*/
|
|
213
|
+
networkDetailWrapper?: ComponentType<{ children: ReactNode }>;
|
|
189
214
|
}
|
|
190
215
|
|
|
191
216
|
export const UnifiedEventDetail = memo(function UnifiedEventDetail({
|
|
192
217
|
event,
|
|
193
218
|
onBack,
|
|
219
|
+
NetworkEventDetailViewComponent,
|
|
220
|
+
networkDetailWrapper,
|
|
194
221
|
}: UnifiedEventDetailProps) {
|
|
195
222
|
const sourceConfig = SOURCE_CONFIG[event.source];
|
|
196
223
|
const timestamp = new Date(event.timestamp);
|
|
224
|
+
// Shared ignored-domain/URL patterns — drives the "Ignore Domain / Ignore URL
|
|
225
|
+
// Pattern" toggles at the bottom of the network detail page so they actually
|
|
226
|
+
// work here and stay in sync with the Network tool.
|
|
227
|
+
const { values: ignoredPatternValues, toggle: toggleIgnoredPattern } =
|
|
228
|
+
useIgnoredPatterns();
|
|
197
229
|
|
|
198
230
|
// For storage events, create a conversation object to use with StorageEventDetailContent
|
|
199
231
|
const storageConversation = useMemo(() => {
|
|
@@ -249,16 +281,28 @@ export const UnifiedEventDetail = memo(function UnifiedEventDetail({
|
|
|
249
281
|
);
|
|
250
282
|
}
|
|
251
283
|
|
|
252
|
-
// For network events,
|
|
253
|
-
|
|
254
|
-
|
|
284
|
+
// For network events, render the SAME NetworkEventDetailView the Network tool
|
|
285
|
+
// uses. Prefer an injected component (desktop), fall back to the dynamically
|
|
286
|
+
// required one (mobile). Wire the shared ignored-patterns so the bottom
|
|
287
|
+
// "Ignore Domain / Ignore URL Pattern" toggles work, and wrap in the optional
|
|
288
|
+
// body-resolver provider so large bodies can be fetched on demand.
|
|
289
|
+
const NetworkDetailView =
|
|
290
|
+
NetworkEventDetailViewComponent ?? optionalComponents.NetworkEventDetailView;
|
|
291
|
+
if (event.source === "network" && NetworkDetailView) {
|
|
255
292
|
const networkEvent = event.originalEvent as NetworkEvent;
|
|
293
|
+
const NetworkWrapper = networkDetailWrapper ?? PassThrough;
|
|
256
294
|
|
|
257
295
|
return (
|
|
258
296
|
<View style={styles.container}>
|
|
259
|
-
<
|
|
260
|
-
<
|
|
261
|
-
|
|
297
|
+
<NetworkWrapper>
|
|
298
|
+
<ScrollView style={styles.content}>
|
|
299
|
+
<NetworkDetailView
|
|
300
|
+
event={networkEvent}
|
|
301
|
+
ignoredPatterns={ignoredPatternValues}
|
|
302
|
+
onTogglePattern={toggleIgnoredPattern}
|
|
303
|
+
/>
|
|
304
|
+
</ScrollView>
|
|
305
|
+
</NetworkWrapper>
|
|
262
306
|
</View>
|
|
263
307
|
);
|
|
264
308
|
}
|
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
useFeatureGate,
|
|
11
|
+
subscribeToSubscriberCountChanges,
|
|
12
|
+
isDevToolsStorageKey,
|
|
13
|
+
useIgnoredPatterns,
|
|
14
|
+
isUrlIgnored,
|
|
15
|
+
type IgnoredPattern,
|
|
16
|
+
} from "@buoy-gg/shared-ui";
|
|
10
17
|
import type { UnifiedEvent, EventSource, SourceInfo } from "../types";
|
|
11
18
|
import {
|
|
12
19
|
subscribe,
|
|
@@ -26,7 +33,6 @@ import {
|
|
|
26
33
|
unsubscribeFromZustand,
|
|
27
34
|
unsubscribeFromJotai,
|
|
28
35
|
unsubscribeFromRender,
|
|
29
|
-
unsubscribeAll,
|
|
30
36
|
getSourceCounts,
|
|
31
37
|
clearEvents as clearStoreEvents,
|
|
32
38
|
getAvailableEventSources,
|
|
@@ -76,6 +82,20 @@ function isBuoyInternalEvent(event: UnifiedEvent): boolean {
|
|
|
76
82
|
return false;
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Check whether a network event should be hidden because its URL matches one of
|
|
87
|
+
* the user's shared ignored-domain/URL patterns (the same store the Network tool
|
|
88
|
+
* writes to). Non-network events are never affected.
|
|
89
|
+
*/
|
|
90
|
+
function isNetworkEventIgnored(
|
|
91
|
+
event: UnifiedEvent,
|
|
92
|
+
ignoredPatterns: IgnoredPattern[]
|
|
93
|
+
): boolean {
|
|
94
|
+
if (event.source !== "network" || ignoredPatterns.length === 0) return false;
|
|
95
|
+
const url = (event.originalEvent as { url?: string } | undefined)?.url ?? "";
|
|
96
|
+
return isUrlIgnored(url, ignoredPatterns);
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
/**
|
|
80
100
|
* All possible sources for display
|
|
81
101
|
*/
|
|
@@ -117,22 +137,6 @@ const SOURCE_TO_EVENT_SOURCES: Record<EventSource, EventSource[]> = {
|
|
|
117
137
|
render: ["render"],
|
|
118
138
|
};
|
|
119
139
|
|
|
120
|
-
/**
|
|
121
|
-
* Map event sources to their parent discovery ID
|
|
122
|
-
*/
|
|
123
|
-
const EVENT_SOURCE_TO_DISCOVERY_ID: Record<EventSource, string> = {
|
|
124
|
-
"storage-async": "storage",
|
|
125
|
-
"storage-mmkv": "storage",
|
|
126
|
-
redux: "redux",
|
|
127
|
-
network: "network",
|
|
128
|
-
"react-query": "react-query",
|
|
129
|
-
"react-query-query": "react-query",
|
|
130
|
-
"react-query-mutation": "react-query",
|
|
131
|
-
route: "route-events",
|
|
132
|
-
zustand: "zustand",
|
|
133
|
-
jotai: "jotai",
|
|
134
|
-
render: "render",
|
|
135
|
-
};
|
|
136
140
|
|
|
137
141
|
export interface UseUnifiedEventsResult {
|
|
138
142
|
events: UnifiedEvent[];
|
|
@@ -160,6 +164,10 @@ export interface UseUnifiedEventsResult {
|
|
|
160
164
|
|
|
161
165
|
export function useUnifiedEvents(): UseUnifiedEventsResult {
|
|
162
166
|
const { isPro } = useFeatureGate();
|
|
167
|
+
// Shared ignored-domain/URL patterns (same store as the Network tool). Network
|
|
168
|
+
// events matching these are hidden from the events list too, keeping both
|
|
169
|
+
// tools' filtering perfectly in sync.
|
|
170
|
+
const { patterns: ignoredPatterns } = useIgnoredPatterns();
|
|
163
171
|
const [events, setEvents] = useState<UnifiedEvent[]>([]);
|
|
164
172
|
const [enabledSources, setEnabledSources] = useState<Set<EventSource>>(
|
|
165
173
|
() => new Set(DEFAULT_ENABLED_SOURCES)
|
|
@@ -167,10 +175,16 @@ export function useUnifiedEvents(): UseUnifiedEventsResult {
|
|
|
167
175
|
const [isCapturing, setIsCapturing] = useState(true);
|
|
168
176
|
const isStateRestoredRef = useRef(false);
|
|
169
177
|
|
|
170
|
-
// Get discovered sources (which packages are installed)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
// Get discovered sources (which packages are installed).
|
|
179
|
+
// Held in state, not a mount-only memo, because in remote mirror mode
|
|
180
|
+
// (desktop dashboard) the device's available sources arrive asynchronously
|
|
181
|
+
// via setRemoteAvailableSources() — after first paint. We refresh this from
|
|
182
|
+
// the store's notify path below so the filter badge bar appears once the
|
|
183
|
+
// first device snapshot lands. The reference is kept stable while the set is
|
|
184
|
+
// unchanged so dependent memos/effects don't re-run on every event.
|
|
185
|
+
const [discoveredSources, setDiscoveredSources] = useState<Set<EventSource>>(
|
|
186
|
+
() => getAvailableEventSources()
|
|
187
|
+
);
|
|
174
188
|
|
|
175
189
|
// Subscribe to subscriber count changes for instant UI updates (TanStack Query pattern)
|
|
176
190
|
const [subscriberCountVersion, setSubscriberCountVersion] = useState(0);
|
|
@@ -189,10 +203,22 @@ export function useUnifiedEvents(): UseUnifiedEventsResult {
|
|
|
189
203
|
});
|
|
190
204
|
}, [discoveredSources]);
|
|
191
205
|
|
|
192
|
-
// Subscribe to store changes (always subscribe to get events when they come)
|
|
206
|
+
// Subscribe to store changes (always subscribe to get events when they come).
|
|
207
|
+
// Also refresh discoveredSources here: in remote mirror mode the device's
|
|
208
|
+
// availability is set right before replaceEvents() fires this listener, so
|
|
209
|
+
// this is where the badge bar first learns which sources exist. Keep the
|
|
210
|
+
// Set reference stable when membership is unchanged to avoid re-running the
|
|
211
|
+
// availableDisplaySources memo / restore-state effect on every event.
|
|
193
212
|
useEffect(() => {
|
|
194
213
|
const unsubscribe = subscribe((newEvents) => {
|
|
195
214
|
setEvents(newEvents);
|
|
215
|
+
setDiscoveredSources((prev) => {
|
|
216
|
+
const next = getAvailableEventSources();
|
|
217
|
+
if (prev.size === next.size && [...next].every((s) => prev.has(s))) {
|
|
218
|
+
return prev;
|
|
219
|
+
}
|
|
220
|
+
return new Set(next);
|
|
221
|
+
});
|
|
196
222
|
});
|
|
197
223
|
return unsubscribe;
|
|
198
224
|
}, []);
|
|
@@ -279,15 +305,19 @@ export function useUnifiedEvents(): UseUnifiedEventsResult {
|
|
|
279
305
|
return allowed;
|
|
280
306
|
}, [enabledSources]);
|
|
281
307
|
|
|
282
|
-
// Filter events by enabled sources
|
|
308
|
+
// Filter events by enabled sources, exclude Buoy internal events, and hide
|
|
309
|
+
// network events matching the shared ignored-domain/URL patterns.
|
|
283
310
|
const allFilteredEvents = useMemo(() => {
|
|
284
311
|
if (allowedEventSources.size === 0) {
|
|
285
312
|
return [];
|
|
286
313
|
}
|
|
287
314
|
return events.filter(
|
|
288
|
-
(event) =>
|
|
315
|
+
(event) =>
|
|
316
|
+
allowedEventSources.has(event.source) &&
|
|
317
|
+
!isBuoyInternalEvent(event) &&
|
|
318
|
+
!isNetworkEventIgnored(event, ignoredPatterns)
|
|
289
319
|
);
|
|
290
|
-
}, [events, allowedEventSources]);
|
|
320
|
+
}, [events, allowedEventSources, ignoredPatterns]);
|
|
291
321
|
|
|
292
322
|
// Apply free tier limit if not Pro
|
|
293
323
|
const filteredEvents = useMemo(() => {
|
|
@@ -456,9 +486,25 @@ export function useUnifiedEvents(): UseUnifiedEventsResult {
|
|
|
456
486
|
}, [enabledSources]);
|
|
457
487
|
|
|
458
488
|
const stopCapturing = useCallback(() => {
|
|
459
|
-
|
|
489
|
+
// Release only THIS hook's enabled sources (mirrors startCapturing). Source
|
|
490
|
+
// subscriptions are ref-counted in the store, so this won't yank sources a
|
|
491
|
+
// remote dashboard consumer is still watching (vs the old unsubscribeAll
|
|
492
|
+
// hard reset, which tore down everyone's).
|
|
493
|
+
if (enabledSources.has("storage-async")) unsubscribeFromStorage();
|
|
494
|
+
if (enabledSources.has("redux")) unsubscribeFromRedux();
|
|
495
|
+
if (enabledSources.has("network")) unsubscribeFromNetwork();
|
|
496
|
+
if (
|
|
497
|
+
enabledSources.has("react-query-query") ||
|
|
498
|
+
enabledSources.has("react-query-mutation")
|
|
499
|
+
) {
|
|
500
|
+
unsubscribeFromReactQuery();
|
|
501
|
+
}
|
|
502
|
+
if (enabledSources.has("route")) unsubscribeFromRoutes();
|
|
503
|
+
if (enabledSources.has("zustand")) unsubscribeFromZustand();
|
|
504
|
+
if (enabledSources.has("jotai")) unsubscribeFromJotai();
|
|
505
|
+
if (enabledSources.has("render")) unsubscribeFromRender();
|
|
460
506
|
setIsCapturing(false);
|
|
461
|
-
}, []);
|
|
507
|
+
}, [enabledSources]);
|
|
462
508
|
|
|
463
509
|
const toggleCapturing = useCallback(() => {
|
|
464
510
|
if (isCapturing) {
|
package/src/index.tsx
CHANGED
|
@@ -19,6 +19,12 @@ export { eventsToolPreset, createEventsTool } from "./preset";
|
|
|
19
19
|
// MAIN COMPONENT
|
|
20
20
|
// =============================================================================
|
|
21
21
|
export { UnifiedEventViewer } from "./components/UnifiedEventViewer";
|
|
22
|
+
export { EventsModal } from "./components/EventsModal";
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// EXTERNAL SYNC (Adapter for @buoy-gg/external-sync's useExternalSync)
|
|
26
|
+
// =============================================================================
|
|
27
|
+
export { eventsSyncAdapter } from "./sync/eventsSyncAdapter";
|
|
22
28
|
|
|
23
29
|
// =============================================================================
|
|
24
30
|
// HOOKS (For consuming event data)
|
|
@@ -102,8 +108,10 @@ export {
|
|
|
102
108
|
// These are NOT exported to prevent users from bypassing the unified event
|
|
103
109
|
// system. Event subscription should go through the proper tool integration.
|
|
104
110
|
// =============================================================================
|
|
111
|
+
/** @internal - Remote mirror mode only (desktop dashboard): replaceEvents /
|
|
112
|
+
* onClear / setRemoteAvailableSources. Do not use for event subscription. */
|
|
113
|
+
export { unifiedEventStore } from "./stores/unifiedEventStore";
|
|
105
114
|
// NOTE: The following are intentionally NOT exported:
|
|
106
|
-
// - unifiedEventStore (internal store)
|
|
107
115
|
// - subscribeToStorage, unsubscribeFromStorage (internal subscription)
|
|
108
116
|
// - subscribeToRedux, unsubscribeFromRedux (internal subscription)
|
|
109
117
|
// - subscribeToNetwork, unsubscribeFromNetwork (internal subscription)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EventSource } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Map an event source (the granular UI badge identity) to its parent discovery
|
|
5
|
+
* ID (the key used by unifiedEventStore's source subscriptions). Several event
|
|
6
|
+
* sources can share one discovery source — e.g. storage-async + storage-mmkv
|
|
7
|
+
* both come from the "storage" discovery source, and the react-query family all
|
|
8
|
+
* map to "react-query".
|
|
9
|
+
*
|
|
10
|
+
* Shared by useUnifiedEvents (the on-device consumer) and unifiedEventStore's
|
|
11
|
+
* remote-consumer channel so the mapping lives in exactly one place.
|
|
12
|
+
*/
|
|
13
|
+
export const EVENT_SOURCE_TO_DISCOVERY_ID: Record<EventSource, string> = {
|
|
14
|
+
"storage-async": "storage",
|
|
15
|
+
"storage-mmkv": "storage",
|
|
16
|
+
redux: "redux",
|
|
17
|
+
network: "network",
|
|
18
|
+
"react-query": "react-query",
|
|
19
|
+
"react-query-query": "react-query",
|
|
20
|
+
"react-query-mutation": "react-query",
|
|
21
|
+
route: "route-events",
|
|
22
|
+
zustand: "zustand",
|
|
23
|
+
jotai: "jotai",
|
|
24
|
+
render: "render",
|
|
25
|
+
};
|
|
@@ -18,16 +18,42 @@ import {
|
|
|
18
18
|
type DiscoveredEventSource,
|
|
19
19
|
type AggregatedSubscriberCounts,
|
|
20
20
|
} from "../utils/autoDiscoverEventSources";
|
|
21
|
+
import { EVENT_SOURCE_TO_DISCOVERY_ID } from "../sources/sourceIds";
|
|
21
22
|
|
|
22
23
|
const MAX_EVENTS = 200;
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Source IDs that subscribeToAll() must NOT auto-subscribe to.
|
|
27
|
+
*
|
|
28
|
+
* These are high-frequency sources the UI excludes from DEFAULT_ENABLED_SOURCES
|
|
29
|
+
* (see useUnifiedEvents.ts). "render" (highlight-updates) fires on every
|
|
30
|
+
* component render — tens of events per second — which floods the shared store
|
|
31
|
+
* (capped at MAX_EVENTS), shoves real events out, and shows up as hidden noise
|
|
32
|
+
* ("1 of 100"). subscribeToAll() is the dashboard-watch path; it must mirror
|
|
33
|
+
* the on-device default of leaving render OFF unless the user opts in.
|
|
34
|
+
*/
|
|
35
|
+
const SUBSCRIBE_ALL_EXCLUDED_SOURCE_IDS = new Set<string>(["render"]);
|
|
36
|
+
|
|
24
37
|
class UnifiedEventStore {
|
|
25
38
|
private events: UnifiedEvent[] = [];
|
|
26
39
|
private listeners: Set<UnifiedEventListener> = new Set();
|
|
27
40
|
private activeSources: Set<EventSource> = new Set();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
private clearListeners: Set<() => void> = new Set();
|
|
42
|
+
private remoteAvailableSources: Set<EventSource> | null = null;
|
|
43
|
+
|
|
44
|
+
// Track which sources are currently subscribed, ref-counted so independent
|
|
45
|
+
// consumers (the on-device EventsModal and the remote dashboard) can each
|
|
46
|
+
// request a source and the underlying capture only stops when the LAST one
|
|
47
|
+
// drops it. Presence of a key still means "subscribed".
|
|
48
|
+
private sourceUnsubscribers: Map<
|
|
49
|
+
string,
|
|
50
|
+
{ unsubscribe: () => void; refCount: number }
|
|
51
|
+
> = new Map();
|
|
52
|
+
|
|
53
|
+
// Discovery IDs the remote dashboard (sync adapter) currently wants. Tracked
|
|
54
|
+
// separately so the remote consumer can be diffed/cleared independently of
|
|
55
|
+
// the on-device consumer's subscriptions.
|
|
56
|
+
private remoteDiscoveryIds: Set<string> = new Set();
|
|
31
57
|
|
|
32
58
|
// Map network event IDs to unified event IDs for updates
|
|
33
59
|
private networkEventIdMap: Map<string, string> = new Map();
|
|
@@ -40,12 +66,66 @@ class UnifiedEventStore {
|
|
|
40
66
|
}
|
|
41
67
|
|
|
42
68
|
/**
|
|
43
|
-
* Get which event source types are available
|
|
69
|
+
* Get which event source types are available. In remote mirror mode this
|
|
70
|
+
* reflects the synced device's availability instead of local discovery.
|
|
44
71
|
*/
|
|
45
72
|
getAvailableEventSources(): Set<EventSource> {
|
|
73
|
+
if (this.remoteAvailableSources) {
|
|
74
|
+
return this.remoteAvailableSources;
|
|
75
|
+
}
|
|
46
76
|
return getCachedDiscovery().availableEventSources;
|
|
47
77
|
}
|
|
48
78
|
|
|
79
|
+
// ===========================================================================
|
|
80
|
+
// REMOTE MIRROR MODE
|
|
81
|
+
// ===========================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Override source availability with the synced device's (auto-discovery
|
|
85
|
+
* finds nothing on the dashboard — the tool packages live on the device).
|
|
86
|
+
* Pass null to restore local discovery.
|
|
87
|
+
*
|
|
88
|
+
* Notifies listeners when the set actually changes so consumers
|
|
89
|
+
* (useUnifiedEvents) can refresh their available-source list — this is the
|
|
90
|
+
* only signal the dashboard's filter badge bar gets when capture is OFF and
|
|
91
|
+
* no events are flowing to trigger replaceEvents().
|
|
92
|
+
*/
|
|
93
|
+
setRemoteAvailableSources(sources: EventSource[] | null): void {
|
|
94
|
+
const next = sources ? new Set(sources) : null;
|
|
95
|
+
const prev = this.remoteAvailableSources;
|
|
96
|
+
const unchanged =
|
|
97
|
+
(prev === null && next === null) ||
|
|
98
|
+
(!!prev &&
|
|
99
|
+
!!next &&
|
|
100
|
+
prev.size === next.size &&
|
|
101
|
+
[...next].every((s) => prev.has(s)));
|
|
102
|
+
this.remoteAvailableSources = next;
|
|
103
|
+
if (!unchanged) {
|
|
104
|
+
this.notifyListeners();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Replace the entire event list and notify listeners. Used in remote
|
|
110
|
+
* mirror mode where full snapshots arrive from a synced device.
|
|
111
|
+
*/
|
|
112
|
+
replaceEvents(events: UnifiedEvent[]): void {
|
|
113
|
+
this.events = events.slice(0, MAX_EVENTS);
|
|
114
|
+
this.activeSources = new Set(events.map((event) => event.source));
|
|
115
|
+
this.notifyListeners();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Listen for clearEvents() calls. Used in remote mirror mode to forward a
|
|
120
|
+
* clear performed in the dashboard UI to the synced device.
|
|
121
|
+
*/
|
|
122
|
+
onClear(listener: () => void): () => void {
|
|
123
|
+
this.clearListeners.add(listener);
|
|
124
|
+
return () => {
|
|
125
|
+
this.clearListeners.delete(listener);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
49
129
|
/**
|
|
50
130
|
* Subscribe to all available event sources
|
|
51
131
|
*/
|
|
@@ -53,6 +133,12 @@ class UnifiedEventStore {
|
|
|
53
133
|
const { sources } = getCachedDiscovery();
|
|
54
134
|
|
|
55
135
|
for (const source of sources) {
|
|
136
|
+
// Skip high-frequency sources the UI leaves off by default (e.g. render).
|
|
137
|
+
// Subscribing to them here would flood the timeline with events that are
|
|
138
|
+
// then filtered out of the display — see SUBSCRIBE_ALL_EXCLUDED_SOURCE_IDS.
|
|
139
|
+
if (SUBSCRIBE_ALL_EXCLUDED_SOURCE_IDS.has(source.id)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
56
142
|
await this.subscribeToSource(source);
|
|
57
143
|
}
|
|
58
144
|
}
|
|
@@ -61,10 +147,18 @@ class UnifiedEventStore {
|
|
|
61
147
|
* Subscribe to a specific event source by ID
|
|
62
148
|
*/
|
|
63
149
|
async subscribeToSource(source: DiscoveredEventSource): Promise<void> {
|
|
64
|
-
|
|
65
|
-
|
|
150
|
+
const existing = this.sourceUnsubscribers.get(source.id);
|
|
151
|
+
if (existing) {
|
|
152
|
+
existing.refCount++; // Another consumer wants it — just count.
|
|
153
|
+
return;
|
|
66
154
|
}
|
|
67
155
|
|
|
156
|
+
// Reserve the slot synchronously (refCount 1) so concurrent subscribe calls
|
|
157
|
+
// for the same source ref-count instead of double-subscribing during the
|
|
158
|
+
// awaits below.
|
|
159
|
+
const entry = { unsubscribe: () => {}, refCount: 1 };
|
|
160
|
+
this.sourceUnsubscribers.set(source.id, entry);
|
|
161
|
+
|
|
68
162
|
try {
|
|
69
163
|
// Run setup if needed
|
|
70
164
|
if (source.setup) {
|
|
@@ -77,20 +171,27 @@ class UnifiedEventStore {
|
|
|
77
171
|
});
|
|
78
172
|
|
|
79
173
|
if (unsubscribe) {
|
|
80
|
-
|
|
174
|
+
entry.unsubscribe = unsubscribe;
|
|
81
175
|
}
|
|
82
176
|
} catch {
|
|
83
|
-
//
|
|
177
|
+
// Failed - source may not be available. Drop the reservation unless other
|
|
178
|
+
// consumers incremented it while we were awaiting.
|
|
179
|
+
if (this.sourceUnsubscribers.get(source.id) === entry && entry.refCount <= 1) {
|
|
180
|
+
this.sourceUnsubscribers.delete(source.id);
|
|
181
|
+
}
|
|
84
182
|
}
|
|
85
183
|
}
|
|
86
184
|
|
|
87
185
|
/**
|
|
88
|
-
* Unsubscribe from a specific source by ID
|
|
186
|
+
* Unsubscribe from a specific source by ID. Ref-counted: the real teardown
|
|
187
|
+
* only runs when the last consumer drops it.
|
|
89
188
|
*/
|
|
90
189
|
unsubscribeFromSource(sourceId: string): void {
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
190
|
+
const entry = this.sourceUnsubscribers.get(sourceId);
|
|
191
|
+
if (!entry) return;
|
|
192
|
+
entry.refCount--;
|
|
193
|
+
if (entry.refCount <= 0) {
|
|
194
|
+
entry.unsubscribe();
|
|
94
195
|
this.sourceUnsubscribers.delete(sourceId);
|
|
95
196
|
}
|
|
96
197
|
}
|
|
@@ -341,6 +442,13 @@ class UnifiedEventStore {
|
|
|
341
442
|
this.activeSources.clear();
|
|
342
443
|
this.networkEventIdMap.clear();
|
|
343
444
|
this.notifyListeners();
|
|
445
|
+
this.clearListeners.forEach((listener) => {
|
|
446
|
+
try {
|
|
447
|
+
listener();
|
|
448
|
+
} catch {
|
|
449
|
+
// Ignore listener errors
|
|
450
|
+
}
|
|
451
|
+
});
|
|
344
452
|
}
|
|
345
453
|
|
|
346
454
|
/**
|
|
@@ -377,14 +485,87 @@ class UnifiedEventStore {
|
|
|
377
485
|
* Unsubscribe from all sources
|
|
378
486
|
*/
|
|
379
487
|
unsubscribeAll(): void {
|
|
380
|
-
//
|
|
381
|
-
for (const [,
|
|
382
|
-
unsubscribe();
|
|
488
|
+
// Hard reset: tear down every tracked source regardless of ref-count.
|
|
489
|
+
for (const [, entry] of this.sourceUnsubscribers) {
|
|
490
|
+
entry.unsubscribe();
|
|
383
491
|
}
|
|
384
492
|
this.sourceUnsubscribers.clear();
|
|
385
493
|
|
|
386
|
-
// Clear active
|
|
494
|
+
// Clear active + remote source bookkeeping (all subscriptions are gone).
|
|
387
495
|
this.activeSources.clear();
|
|
496
|
+
this.remoteDiscoveryIds.clear();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Remote-consumer channel: the dashboard sync adapter declares which event
|
|
501
|
+
* sources it wants the device to capture. Diffed against the previous remote
|
|
502
|
+
* set so only changed sources are (un)subscribed; ref-counting keeps the
|
|
503
|
+
* on-device EventsModal's own subscriptions intact. Pass "all" to capture
|
|
504
|
+
* every discovered source except the high-frequency excluded ones.
|
|
505
|
+
*/
|
|
506
|
+
async setRemoteEnabledSources(
|
|
507
|
+
sources: EventSource[] | "all"
|
|
508
|
+
): Promise<void> {
|
|
509
|
+
const { sources: discovered } = getCachedDiscovery();
|
|
510
|
+
|
|
511
|
+
let nextIds: Set<string>;
|
|
512
|
+
if (sources === "all") {
|
|
513
|
+
nextIds = new Set(
|
|
514
|
+
discovered
|
|
515
|
+
.map((s) => s.id)
|
|
516
|
+
.filter((id) => !SUBSCRIBE_ALL_EXCLUDED_SOURCE_IDS.has(id))
|
|
517
|
+
);
|
|
518
|
+
} else {
|
|
519
|
+
nextIds = new Set<string>();
|
|
520
|
+
for (const src of sources) {
|
|
521
|
+
const id = EVENT_SOURCE_TO_DISCOVERY_ID[src];
|
|
522
|
+
if (id) nextIds.add(id);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Swap the tracked set SYNCHRONOUSLY (before the awaits) so a watch's
|
|
527
|
+
// ensureRemoteSourcesDefault() that runs in between sees the new selection
|
|
528
|
+
// and won't re-broaden to "all".
|
|
529
|
+
const prevIds = this.remoteDiscoveryIds;
|
|
530
|
+
this.remoteDiscoveryIds = nextIds;
|
|
531
|
+
|
|
532
|
+
// Subscribe newly-requested sources.
|
|
533
|
+
for (const id of nextIds) {
|
|
534
|
+
if (!prevIds.has(id)) {
|
|
535
|
+
const source = discovered.find((s) => s.id === id);
|
|
536
|
+
if (source) await this.subscribeToSource(source);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Release sources the remote no longer wants.
|
|
540
|
+
for (const id of prevIds) {
|
|
541
|
+
if (!nextIds.has(id)) {
|
|
542
|
+
this.unsubscribeFromSource(id);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Called by the sync adapter when a dashboard starts watching: default to all
|
|
549
|
+
* sources, but ONLY if the dashboard hasn't already declared a selection for
|
|
550
|
+
* this watch session (the setEnabledSources action can arrive before the
|
|
551
|
+
* watch). Keeps backward compatibility with dashboards that never narrow.
|
|
552
|
+
*/
|
|
553
|
+
ensureRemoteSourcesDefault(): void {
|
|
554
|
+
if (this.remoteDiscoveryIds.size === 0) {
|
|
555
|
+
void this.setRemoteEnabledSources("all");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Release all of the remote consumer's source subscriptions (e.g. when the
|
|
561
|
+
* dashboard stops watching the events tool). Ref-counting means sources the
|
|
562
|
+
* on-device EventsModal also wants stay subscribed.
|
|
563
|
+
*/
|
|
564
|
+
clearRemoteSources(): void {
|
|
565
|
+
for (const id of this.remoteDiscoveryIds) {
|
|
566
|
+
this.unsubscribeFromSource(id);
|
|
567
|
+
}
|
|
568
|
+
this.remoteDiscoveryIds.clear();
|
|
388
569
|
}
|
|
389
570
|
|
|
390
571
|
/**
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { unifiedEventStore } from "../stores/unifiedEventStore";
|
|
2
|
+
import type { EventSource } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sync adapter for the events tool, consumed by @buoy-gg/external-sync's
|
|
6
|
+
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
7
|
+
* this package doesn't need a dependency on it).
|
|
8
|
+
*
|
|
9
|
+
* This adapter is the "remote consumer" of the unified event store. While a
|
|
10
|
+
* dashboard is watching, it requests a SET of event sources (defaulting to all,
|
|
11
|
+
* narrowed by the dashboard via `setEnabledSources`). On unwatch it releases
|
|
12
|
+
* those requests, so — unlike before — the device stops capturing sources that
|
|
13
|
+
* nothing else is watching. Source requests are ref-counted in the store, so
|
|
14
|
+
* the on-device EventsModal keeps working when open at the same time.
|
|
15
|
+
*
|
|
16
|
+
* The snapshot carries the device's available sources so the dashboard can
|
|
17
|
+
* render the source filter chips (its own auto-discovery finds nothing — the
|
|
18
|
+
* tool packages live on the device).
|
|
19
|
+
*/
|
|
20
|
+
export const eventsSyncAdapter = {
|
|
21
|
+
version: 2,
|
|
22
|
+
getSnapshot: () => ({
|
|
23
|
+
events: unifiedEventStore.getEvents(),
|
|
24
|
+
availableSources: Array.from(unifiedEventStore.getAvailableEventSources()),
|
|
25
|
+
}),
|
|
26
|
+
subscribe: (onChange: () => void) => {
|
|
27
|
+
// Default to all sources until the dashboard narrows the set via
|
|
28
|
+
// setEnabledSources — backward compatible with dashboards that don't send
|
|
29
|
+
// it. Uses ensureRemoteSourcesDefault so a setEnabledSources that raced
|
|
30
|
+
// ahead of this watch isn't clobbered back to "all".
|
|
31
|
+
unifiedEventStore.ensureRemoteSourcesDefault();
|
|
32
|
+
const unsubscribe = unifiedEventStore.subscribe(() => onChange());
|
|
33
|
+
return () => {
|
|
34
|
+
unsubscribe();
|
|
35
|
+
// Release this consumer's source subscriptions so the device stops
|
|
36
|
+
// capturing sources nothing else is watching (fixes the old leak).
|
|
37
|
+
unifiedEventStore.clearRemoteSources();
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
actions: {
|
|
41
|
+
clearEvents: () => {
|
|
42
|
+
unifiedEventStore.clearEvents();
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Narrow which sources the device captures for this dashboard, driven by
|
|
46
|
+
* the dashboard's source badges. Ref-counted in the store, so disabling a
|
|
47
|
+
* source here only stops its capture if no other consumer wants it.
|
|
48
|
+
*/
|
|
49
|
+
setEnabledSources: (params: unknown) => {
|
|
50
|
+
const sources =
|
|
51
|
+
(params as { sources?: EventSource[] } | undefined)?.sources ?? [];
|
|
52
|
+
unifiedEventStore.setRemoteEnabledSources(sources);
|
|
53
|
+
return { enabledSources: sources };
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|