@buoy-gg/storage 1.7.8 → 2.1.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 (26) hide show
  1. package/lib/commonjs/index.js +219 -16
  2. package/lib/commonjs/storage/components/StorageEventCard.js +112 -0
  3. package/lib/commonjs/storage/components/StorageModalWithTabs.js +45 -190
  4. package/lib/commonjs/storage/hooks/useStorageEvents.js +98 -0
  5. package/lib/commonjs/storage/index.js +111 -2
  6. package/lib/commonjs/storage/stores/storageEventStore.js +243 -0
  7. package/lib/module/index.js +74 -3
  8. package/lib/module/storage/components/StorageEventCard.js +107 -0
  9. package/lib/module/storage/components/StorageModalWithTabs.js +47 -193
  10. package/lib/module/storage/hooks/useStorageEvents.js +95 -0
  11. package/lib/module/storage/index.js +7 -1
  12. package/lib/module/storage/stores/storageEventStore.js +231 -0
  13. package/lib/typescript/index.d.ts +36 -1
  14. package/lib/typescript/index.d.ts.map +1 -1
  15. package/lib/typescript/storage/components/StorageActionButtons.d.ts +0 -2
  16. package/lib/typescript/storage/components/StorageActionButtons.d.ts.map +1 -1
  17. package/lib/typescript/storage/components/StorageEventCard.d.ts +40 -0
  18. package/lib/typescript/storage/components/StorageEventCard.d.ts.map +1 -0
  19. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  20. package/lib/typescript/storage/hooks/useStorageEvents.d.ts +51 -0
  21. package/lib/typescript/storage/hooks/useStorageEvents.d.ts.map +1 -0
  22. package/lib/typescript/storage/index.d.ts +4 -0
  23. package/lib/typescript/storage/index.d.ts.map +1 -1
  24. package/lib/typescript/storage/stores/storageEventStore.d.ts +113 -0
  25. package/lib/typescript/storage/stores/storageEventStore.d.ts.map +1 -0
  26. package/package.json +18 -4
@@ -3,25 +3,18 @@
3
3
  import { useState, useCallback, useEffect, useRef, useMemo } from "react";
4
4
  import { Text, View, TouchableOpacity, StyleSheet, FlatList, Alert } from "react-native";
5
5
  import AsyncStorage from "@react-native-async-storage/async-storage";
6
- import { JsModal, ModalHeader, TabSelector, ValueTypeBadge, StorageTypeBadge, formatRelativeTime, parseValue, devToolsStorageKeys, macOSColors, Database, Pause, Play, Trash2, Filter, Search, SearchBar } from "@buoy-gg/shared-ui";
6
+ import { JsModal, ModalHeader, TabSelector, parseValue, devToolsStorageKeys, macOSColors, Database, Trash2, Filter, Search, SearchBar, PowerToggleButton } from "@buoy-gg/shared-ui";
7
7
  import { StorageBrowserMode } from "./StorageBrowserMode";
8
8
  import { clearAllAppStorage, clearAllStorageIncludingDevTools } from "../utils/clearAllStorage";
9
9
  import { ProFeatureBanner } from "@buoy-gg/shared-ui";
10
- import { startListening, stopListening, addListener, isListening as checkIsListening } from "../utils/AsyncStorageListener";
10
+ import { stopListening } from "../utils/AsyncStorageListener";
11
+ import { useStorageEvents } from "../hooks/useStorageEvents";
11
12
  import { StorageEventDetailContent, StorageEventDetailFooter } from "./StorageEventDetailContent";
12
13
  import { StorageFilterViewV2 } from "./StorageFilterViewV2";
13
14
  import { StorageEventFilterView } from "./StorageEventFilterView";
14
- import { translateStorageAction } from "../utils/storageActionHelpers";
15
- import { isMMKVAvailable } from "../utils/mmkvAvailability";
16
-
17
- // Conditionally import MMKV listener
18
- let addMMKVListener;
19
- if (isMMKVAvailable()) {
20
- const mmkvListener = require("../utils/MMKVListener");
21
- addMMKVListener = mmkvListener.addMMKVListener;
22
- }
23
-
24
- // Unified storage event type
15
+ import { StorageEventCard } from "./StorageEventCard";
16
+ // Note: StorageEvent type is now imported from useStorageEvents hook
17
+ // which handles both AsyncStorage and MMKV events via the centralized store
25
18
 
26
19
  import { useIsPro } from "@buoy-gg/license";
27
20
 
@@ -47,8 +40,16 @@ export function StorageModalWithTabs({
47
40
  const [isSearchActive, setIsSearchActive] = useState(false);
48
41
  const hasLoadedStorageFilters = useRef(false);
49
42
 
50
- // Event Listener state
51
- const [events, setEvents] = useState([]);
43
+ // Event Listener state - using centralized store via hook
44
+ const {
45
+ events,
46
+ clearEvents: clearStorageEvents,
47
+ isCapturing,
48
+ startCapturing,
49
+ stopCapturing
50
+ } = useStorageEvents({
51
+ autoStart: false
52
+ }); // Don't auto-start, we manage it
52
53
  const [isListening, setIsListening] = useState(false);
53
54
  const [selectedConversationKey, setSelectedConversationKey] = useState(null);
54
55
  const [selectedEventIndex, setSelectedEventIndex] = useState(0);
@@ -61,6 +62,11 @@ export function StorageModalWithTabs({
61
62
  const hasLoadedFilters = useRef(false);
62
63
  const hasLoadedTabState = useRef(false);
63
64
  const hasLoadedMonitoringState = useRef(false);
65
+
66
+ // Sync isListening state with hook's isCapturing
67
+ useEffect(() => {
68
+ setIsListening(isCapturing);
69
+ }, [isCapturing]);
64
70
  const handleModeChange = useCallback(_mode => {
65
71
  // Mode changes handled by JsModal
66
72
  }, []);
@@ -92,9 +98,8 @@ export function StorageModalWithTabs({
92
98
  const storedMonitoring = await AsyncStorage.getItem(devToolsStorageKeys.storage.isMonitoring());
93
99
  if (storedMonitoring !== null) {
94
100
  const shouldMonitor = storedMonitoring === "true";
95
- if (shouldMonitor && !checkIsListening()) {
96
- await startListening();
97
- setIsListening(true);
101
+ if (shouldMonitor && !isCapturing) {
102
+ await startCapturing();
98
103
  }
99
104
  }
100
105
  hasLoadedMonitoringState.current = true;
@@ -103,7 +108,7 @@ export function StorageModalWithTabs({
103
108
  }
104
109
  };
105
110
  loadMonitoringState();
106
- }, [visible]);
111
+ }, [visible, isCapturing, startCapturing]);
107
112
 
108
113
  // Note: Conversations will appear when storage events are triggered
109
114
  // Click on any conversation to see the unified view with toggle cards
@@ -169,65 +174,21 @@ export function StorageModalWithTabs({
169
174
  saveFilters();
170
175
  }, [ignoredPatterns]);
171
176
 
172
- // Sync isListening state with actual listener state on mount
173
- useEffect(() => {
174
- if (!visible) return;
175
-
176
- // Check if already listening and sync state
177
- const listening = checkIsListening();
178
- setIsListening(listening);
179
- }, [visible]);
177
+ // Event listener setup - now handled by useStorageEvents hook
178
+ // The hook subscribes to the centralized storageEventStore
180
179
 
181
- // Event listener setup - only collect events when isListening is true
182
- useEffect(() => {
183
- if (!visible || !isListening) return;
184
-
185
- // Set up AsyncStorage event listener
186
- const unsubscribeAsync = addListener(event => {
187
- const storageEvent = {
188
- ...event,
189
- storageType: 'async'
190
- };
191
- lastEventRef.current = storageEvent;
192
- setEvents(prev => {
193
- const updated = [storageEvent, ...prev];
194
- return updated.slice(0, 500);
195
- });
196
- });
197
-
198
- // Set up MMKV event listener (if available)
199
- let unsubscribeMMKV = () => {};
200
- if (isMMKVAvailable() && addMMKVListener) {
201
- unsubscribeMMKV = addMMKVListener(event => {
202
- const storageEvent = {
203
- ...event,
204
- storageType: 'mmkv'
205
- };
206
- lastEventRef.current = storageEvent;
207
- setEvents(prev => {
208
- const updated = [storageEvent, ...prev];
209
- return updated.slice(0, 500);
210
- });
211
- });
212
- }
213
- return () => {
214
- unsubscribeAsync();
215
- unsubscribeMMKV();
216
- };
217
- }, [visible, isListening]);
218
180
  const handleToggleListening = useCallback(async () => {
219
181
  if (isListening) {
220
- stopListening();
221
- setIsListening(false);
182
+ stopCapturing();
183
+ stopListening(); // Also stop the underlying AsyncStorage listener
222
184
  } else {
223
- await startListening();
224
- setIsListening(true);
185
+ await startCapturing();
225
186
  }
226
- }, [isListening]);
187
+ }, [isListening, startCapturing, stopCapturing]);
227
188
  const handleClearEvents = useCallback(() => {
228
- setEvents([]);
189
+ clearStorageEvents();
229
190
  setSelectedConversationKey(null);
230
- }, []);
191
+ }, [clearStorageEvents]);
231
192
  const handleConversationPress = useCallback(conversation => {
232
193
  setSelectedConversationKey(conversation.key);
233
194
  setSelectedEventIndex(0);
@@ -404,44 +365,6 @@ export function StorageModalWithTabs({
404
365
  if (isPro) return 0;
405
366
  return Math.max(0, conversations.length - FREE_TIER_EVENT_LIMIT);
406
367
  }, [conversations.length, isPro]);
407
- const getActionColor = action => {
408
- switch (action) {
409
- // AsyncStorage - Set operations
410
- case "setItem":
411
- case "multiSet":
412
- // MMKV - Set operations
413
- // falls through
414
- case "set.string":
415
- case "set.number":
416
- case "set.boolean":
417
- case "set.buffer":
418
- return macOSColors.semantic.success;
419
-
420
- // AsyncStorage - Remove operations
421
- case "removeItem":
422
- case "multiRemove":
423
- case "clear":
424
- // MMKV - Delete operations
425
- // falls through
426
- case "delete":
427
- case "clearAll":
428
- return macOSColors.semantic.error;
429
-
430
- // AsyncStorage - Merge operations
431
- case "mergeItem":
432
- case "multiMerge":
433
- return macOSColors.semantic.info;
434
-
435
- // MMKV - Get operations
436
- case "get.string":
437
- case "get.number":
438
- case "get.boolean":
439
- case "get.buffer":
440
- return macOSColors.semantic.warning;
441
- default:
442
- return macOSColors.text.muted;
443
- }
444
- };
445
368
 
446
369
  // FlatList optimization constants
447
370
  const END_REACHED_THRESHOLD = 0.8;
@@ -461,35 +384,17 @@ export function StorageModalWithTabs({
461
384
  const renderConversationItem = useCallback(({
462
385
  item
463
386
  }) => {
464
- return /*#__PURE__*/_jsxs(TouchableOpacity, {
465
- onPress: () => selectConversationRef.current?.(item),
466
- style: styles.conversationItem,
467
- children: [/*#__PURE__*/_jsxs(View, {
468
- style: styles.conversationHeader,
469
- children: [/*#__PURE__*/_jsx(Text, {
470
- style: styles.keyText,
471
- numberOfLines: 1,
472
- children: item.key
473
- }), /*#__PURE__*/_jsx(Text, {
474
- style: [styles.actionText, {
475
- color: getActionColor(item.lastEvent.action)
476
- }],
477
- children: translateStorageAction(item.lastEvent.action)
478
- })]
479
- }), /*#__PURE__*/_jsxs(View, {
480
- style: styles.conversationDetails,
481
- children: [item.storageTypes && Array.from(item.storageTypes).map(storageType => /*#__PURE__*/_jsx(StorageTypeBadge, {
482
- type: storageType
483
- }, storageType)), /*#__PURE__*/_jsx(ValueTypeBadge, {
484
- type: item.valueType
485
- }), /*#__PURE__*/_jsxs(Text, {
486
- style: styles.operationCount,
487
- children: [item.totalOperations, " operation", item.totalOperations !== 1 ? "s" : ""]
488
- }), /*#__PURE__*/_jsx(Text, {
489
- style: styles.timestamp,
490
- children: formatRelativeTime(item.lastEvent.timestamp)
491
- })]
492
- })]
387
+ const cardData = {
388
+ key: item.key,
389
+ lastAction: item.lastEvent.action,
390
+ totalOperations: item.totalOperations,
391
+ lastEventTimestamp: item.lastEvent.timestamp,
392
+ storageTypes: item.storageTypes,
393
+ valueType: item.valueType
394
+ };
395
+ return /*#__PURE__*/_jsx(StorageEventCard, {
396
+ data: cardData,
397
+ onPress: () => selectConversationRef.current?.(item)
493
398
  });
494
399
  }, []);
495
400
  if (!visible) return null;
@@ -668,16 +573,10 @@ export function StorageModalWithTabs({
668
573
  size: 14,
669
574
  color: ignoredPatterns.size > 0 ? macOSColors.semantic.debug : macOSColors.text.secondary
670
575
  })
671
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
672
- onPress: handleToggleListening,
673
- style: [styles.iconButton, isListening && styles.activeButton],
674
- children: isListening ? /*#__PURE__*/_jsx(Pause, {
675
- size: 14,
676
- color: macOSColors.semantic.success
677
- }) : /*#__PURE__*/_jsx(Play, {
678
- size: 14,
679
- color: macOSColors.semantic.success
680
- })
576
+ }), /*#__PURE__*/_jsx(PowerToggleButton, {
577
+ isEnabled: isListening,
578
+ onToggle: handleToggleListening,
579
+ accessibilityLabel: "Toggle storage event monitoring"
681
580
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
682
581
  onPress: handleClearEvents,
683
582
  style: styles.iconButton,
@@ -714,54 +613,9 @@ const styles = StyleSheet.create({
714
613
  alignItems: "center",
715
614
  justifyContent: "center"
716
615
  },
717
- activeButton: {
718
- backgroundColor: macOSColors.semantic.successBackground
719
- },
720
616
  activeFilterButton: {
721
617
  backgroundColor: macOSColors.semantic.infoBackground
722
618
  },
723
- conversationItem: {
724
- padding: 12,
725
- backgroundColor: macOSColors.background.card,
726
- borderRadius: 8,
727
- marginHorizontal: 16
728
- },
729
- conversationHeader: {
730
- flexDirection: "row",
731
- justifyContent: "space-between",
732
- alignItems: "center",
733
- marginBottom: 8
734
- },
735
- keyText: {
736
- color: macOSColors.text.primary,
737
- fontSize: 14,
738
- fontWeight: "600",
739
- flex: 1,
740
- marginRight: 8,
741
- fontFamily: "monospace"
742
- },
743
- actionText: {
744
- fontSize: 11,
745
- fontWeight: "600",
746
- fontFamily: "monospace",
747
- textTransform: "uppercase"
748
- },
749
- conversationDetails: {
750
- flexDirection: "row",
751
- alignItems: "center",
752
- gap: 8
753
- },
754
- operationCount: {
755
- color: macOSColors.text.secondary,
756
- fontSize: 11,
757
- flex: 1,
758
- fontFamily: "monospace"
759
- },
760
- timestamp: {
761
- color: macOSColors.text.muted,
762
- fontSize: 11,
763
- fontFamily: "monospace"
764
- },
765
619
  separator: {
766
620
  height: 8
767
621
  },
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * useStorageEvents Hook
5
+ *
6
+ * React hook for subscribing to storage events from the centralized store.
7
+ * Provides a clean interface for components to receive storage events.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * function MyComponent() {
12
+ * const { events, clearEvents, isCapturing } = useStorageEvents();
13
+ *
14
+ * return (
15
+ * <View>
16
+ * {events.map((event) => (
17
+ * <Text key={event.timestamp.toString()}>
18
+ * {event.action}: {event.data?.key}
19
+ * </Text>
20
+ * ))}
21
+ * </View>
22
+ * );
23
+ * }
24
+ * ```
25
+ */
26
+
27
+ import { useState, useEffect, useCallback } from "react";
28
+ import { storageEventStore } from "../stores/storageEventStore";
29
+
30
+ // Re-export StorageEvent for convenience
31
+
32
+ export function useStorageEvents(options = {}) {
33
+ const {
34
+ storageType = "all",
35
+ maxEvents = 500,
36
+ autoStart = true
37
+ } = options;
38
+ const [events, setEvents] = useState([]);
39
+ const [isCapturing, setIsCapturing] = useState(storageEventStore.capturing);
40
+
41
+ // Subscribe to store changes
42
+ useEffect(() => {
43
+ const unsubscribe = storageEventStore.subscribe(allEvents => {
44
+ // Filter by storage type if specified
45
+ let filtered = allEvents;
46
+ if (storageType !== "all") {
47
+ filtered = allEvents.filter(e => e.storageType === storageType);
48
+ }
49
+
50
+ // Limit events
51
+ if (filtered.length > maxEvents) {
52
+ filtered = filtered.slice(0, maxEvents);
53
+ }
54
+ setEvents(filtered);
55
+ setIsCapturing(storageEventStore.capturing);
56
+ });
57
+ return () => {
58
+ unsubscribe();
59
+ };
60
+ }, [storageType, maxEvents]);
61
+
62
+ // Auto-start capturing on mount
63
+ useEffect(() => {
64
+ if (autoStart && !storageEventStore.capturing) {
65
+ storageEventStore.startCapturing();
66
+ setIsCapturing(true);
67
+ }
68
+ }, [autoStart]);
69
+ const clearEvents = useCallback(() => {
70
+ storageEventStore.clearEvents();
71
+ }, []);
72
+ const startCapturing = useCallback(async () => {
73
+ await storageEventStore.startCapturing();
74
+ setIsCapturing(true);
75
+ }, []);
76
+ const stopCapturing = useCallback(() => {
77
+ storageEventStore.stopCapturing();
78
+ setIsCapturing(false);
79
+ }, []);
80
+ const pauseCapturing = useCallback(() => {
81
+ storageEventStore.pauseCapture();
82
+ }, []);
83
+ const resumeCapturing = useCallback(() => {
84
+ storageEventStore.resumeCapture();
85
+ }, []);
86
+ return {
87
+ events,
88
+ clearEvents,
89
+ isCapturing,
90
+ startCapturing,
91
+ stopCapturing,
92
+ pauseCapturing,
93
+ resumeCapturing
94
+ };
95
+ }
@@ -8,11 +8,14 @@ export { StorageKeyStatsSection } from "./components/StorageKeyStats";
8
8
  export { StorageKeySection } from "./components/StorageKeySection";
9
9
  export { StorageBrowserMode } from "./components/StorageBrowserMode";
10
10
  export { StorageEventsSection } from "./components/StorageEventsSection";
11
+ export { StorageEventCard, getValueType } from "./components/StorageEventCard";
12
+ export { StorageEventDetailContent, StorageEventDetailFooter } from "./components/StorageEventDetailContent";
11
13
 
12
14
  // Storage hooks
13
15
  export { useAsyncStorageKeys } from "./hooks/useAsyncStorageKeys";
14
16
  export { useMMKVKeys, useMultiMMKVKeys } from "./hooks/useMMKVKeys";
15
17
  export { useMMKVInstances, useMMKVInstance, useMMKVInstanceExists } from "./hooks/useMMKVInstances";
18
+ export { useStorageEvents } from "./hooks/useStorageEvents";
16
19
 
17
20
  // MMKV Components
18
21
  export { MMKVInstanceSelector } from "./components/MMKVInstanceSelector";
@@ -22,4 +25,7 @@ export { MMKVInstanceInfoPanel } from "./components/MMKVInstanceInfoPanel";
22
25
  export * from "./types";
23
26
 
24
27
  // Storage utilities
25
- export * from "./utils";
28
+ export * from "./utils";
29
+
30
+ // Storage Event Store
31
+ export { storageEventStore, startStorageCapture, stopStorageCapture, pauseStorageCapture, resumeStorageCapture, subscribeToStorageEvents, onStorageEvent, getStorageEvents, clearStorageEvents, isStorageCapturing } from "./stores/storageEventStore";
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Storage Event Store
5
+ *
6
+ * Centralized store that aggregates storage events from AsyncStorage and MMKV
7
+ * into a single event stream. This provides a single source of truth for all
8
+ * storage operations, eliminating duplicate subscriptions across components.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { storageEventStore } from '@buoy-gg/storage';
13
+ *
14
+ * // Subscribe to storage events
15
+ * const unsubscribe = storageEventStore.subscribe((events) => {
16
+ * console.log('Storage events:', events);
17
+ * });
18
+ *
19
+ * // Start capturing (must be called once)
20
+ * await storageEventStore.startCapturing();
21
+ *
22
+ * // Later, clean up
23
+ * unsubscribe();
24
+ * ```
25
+ */
26
+
27
+ import { startListening as startAsyncStorageListening, addListener as addAsyncStorageListener, isListening as isAsyncStorageListening, pauseCapture as pauseAsyncStorageCapture, resumeCapture as resumeAsyncStorageCapture } from "../utils/AsyncStorageListener";
28
+ import { addMMKVListener } from "../utils/MMKVListener";
29
+
30
+ /**
31
+ * Unified storage event type combining AsyncStorage and MMKV events
32
+ */
33
+
34
+ /**
35
+ * Listener callback type for storage events
36
+ */
37
+
38
+ /**
39
+ * Listener callback for individual new events
40
+ */
41
+
42
+ const MAX_EVENTS = 500;
43
+ class StorageEventStore {
44
+ events = [];
45
+ listeners = new Set();
46
+ eventCallbacks = new Set();
47
+ isCapturing = false;
48
+
49
+ // Unsubscribe functions
50
+ asyncStorageUnsubscribe = null;
51
+ mmkvUnsubscribe = null;
52
+
53
+ /**
54
+ * Start capturing storage events from both AsyncStorage and MMKV
55
+ */
56
+ async startCapturing() {
57
+ if (this.isCapturing) {
58
+ return;
59
+ }
60
+
61
+ // Start AsyncStorage listening if not already active
62
+ if (!isAsyncStorageListening()) {
63
+ await startAsyncStorageListening();
64
+ }
65
+
66
+ // Subscribe to AsyncStorage events
67
+ this.asyncStorageUnsubscribe = addAsyncStorageListener(event => {
68
+ const storageEvent = {
69
+ ...event,
70
+ storageType: "async"
71
+ };
72
+ this.addEvent(storageEvent);
73
+ });
74
+
75
+ // Subscribe to MMKV events
76
+ this.mmkvUnsubscribe = addMMKVListener(event => {
77
+ const storageEvent = {
78
+ ...event,
79
+ storageType: "mmkv"
80
+ };
81
+ this.addEvent(storageEvent);
82
+ });
83
+ this.isCapturing = true;
84
+ }
85
+
86
+ /**
87
+ * Stop capturing storage events
88
+ */
89
+ stopCapturing() {
90
+ if (!this.isCapturing) {
91
+ return;
92
+ }
93
+
94
+ // Unsubscribe from AsyncStorage
95
+ if (this.asyncStorageUnsubscribe) {
96
+ this.asyncStorageUnsubscribe();
97
+ this.asyncStorageUnsubscribe = null;
98
+ }
99
+
100
+ // Unsubscribe from MMKV
101
+ if (this.mmkvUnsubscribe) {
102
+ this.mmkvUnsubscribe();
103
+ this.mmkvUnsubscribe = null;
104
+ }
105
+ this.isCapturing = false;
106
+ }
107
+
108
+ /**
109
+ * Pause event capture (used during time-travel operations)
110
+ */
111
+ pauseCapture() {
112
+ pauseAsyncStorageCapture();
113
+ // MMKV listener doesn't have pause, but it's less common for time-travel
114
+ }
115
+
116
+ /**
117
+ * Resume event capture after pausing
118
+ */
119
+ resumeCapture() {
120
+ resumeAsyncStorageCapture();
121
+ }
122
+
123
+ /**
124
+ * Add an event to the store
125
+ */
126
+ addEvent(event) {
127
+ // Add to beginning (newest first)
128
+ this.events = [event, ...this.events].slice(0, MAX_EVENTS);
129
+
130
+ // Notify event callbacks (for individual events)
131
+ this.eventCallbacks.forEach(callback => {
132
+ try {
133
+ callback(event);
134
+ } catch {
135
+ // Ignore callback errors
136
+ }
137
+ });
138
+
139
+ // Notify listeners (for full event list)
140
+ this.notifyListeners();
141
+ }
142
+
143
+ /**
144
+ * Subscribe to all storage events (receives full event array on each change)
145
+ */
146
+ subscribe(listener) {
147
+ this.listeners.add(listener);
148
+
149
+ // Immediately call with current events
150
+ listener(this.events);
151
+
152
+ // Return unsubscribe function
153
+ return () => {
154
+ this.listeners.delete(listener);
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Subscribe to new events only (receives individual events as they occur)
160
+ */
161
+ onEvent(callback) {
162
+ this.eventCallbacks.add(callback);
163
+ return () => {
164
+ this.eventCallbacks.delete(callback);
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Notify all listeners of changes
170
+ */
171
+ notifyListeners() {
172
+ const events = this.events;
173
+ this.listeners.forEach(listener => {
174
+ try {
175
+ listener(events);
176
+ } catch {
177
+ // Ignore listener errors
178
+ }
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Get all events
184
+ */
185
+ getEvents() {
186
+ return this.events;
187
+ }
188
+
189
+ /**
190
+ * Get events filtered by storage type
191
+ */
192
+ getEventsByType(storageType) {
193
+ return this.events.filter(event => event.storageType === storageType);
194
+ }
195
+
196
+ /**
197
+ * Get event count
198
+ */
199
+ getEventCount() {
200
+ return this.events.length;
201
+ }
202
+
203
+ /**
204
+ * Clear all events
205
+ */
206
+ clearEvents() {
207
+ this.events = [];
208
+ this.notifyListeners();
209
+ }
210
+
211
+ /**
212
+ * Check if currently capturing events
213
+ */
214
+ get capturing() {
215
+ return this.isCapturing;
216
+ }
217
+ }
218
+
219
+ // Singleton instance
220
+ export const storageEventStore = new StorageEventStore();
221
+
222
+ // Convenience exports
223
+ export const startStorageCapture = () => storageEventStore.startCapturing();
224
+ export const stopStorageCapture = () => storageEventStore.stopCapturing();
225
+ export const pauseStorageCapture = () => storageEventStore.pauseCapture();
226
+ export const resumeStorageCapture = () => storageEventStore.resumeCapture();
227
+ export const subscribeToStorageEvents = listener => storageEventStore.subscribe(listener);
228
+ export const onStorageEvent = callback => storageEventStore.onEvent(callback);
229
+ export const getStorageEvents = () => storageEventStore.getEvents();
230
+ export const clearStorageEvents = () => storageEventStore.clearEvents();
231
+ export const isStorageCapturing = () => storageEventStore.capturing;