@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.
- package/lib/commonjs/index.js +219 -16
- package/lib/commonjs/storage/components/StorageEventCard.js +112 -0
- package/lib/commonjs/storage/components/StorageModalWithTabs.js +45 -190
- package/lib/commonjs/storage/hooks/useStorageEvents.js +98 -0
- package/lib/commonjs/storage/index.js +111 -2
- package/lib/commonjs/storage/stores/storageEventStore.js +243 -0
- package/lib/module/index.js +74 -3
- package/lib/module/storage/components/StorageEventCard.js +107 -0
- package/lib/module/storage/components/StorageModalWithTabs.js +47 -193
- package/lib/module/storage/hooks/useStorageEvents.js +95 -0
- package/lib/module/storage/index.js +7 -1
- package/lib/module/storage/stores/storageEventStore.js +231 -0
- package/lib/typescript/index.d.ts +36 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageActionButtons.d.ts +0 -2
- package/lib/typescript/storage/components/StorageActionButtons.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageEventCard.d.ts +40 -0
- package/lib/typescript/storage/components/StorageEventCard.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useStorageEvents.d.ts +51 -0
- package/lib/typescript/storage/hooks/useStorageEvents.d.ts.map +1 -0
- package/lib/typescript/storage/index.d.ts +4 -0
- package/lib/typescript/storage/index.d.ts.map +1 -1
- package/lib/typescript/storage/stores/storageEventStore.d.ts +113 -0
- package/lib/typescript/storage/stores/storageEventStore.d.ts.map +1 -0
- 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,
|
|
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 {
|
|
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 {
|
|
15
|
-
|
|
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
|
|
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 && !
|
|
96
|
-
await
|
|
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
|
-
//
|
|
173
|
-
|
|
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
|
-
|
|
221
|
-
|
|
182
|
+
stopCapturing();
|
|
183
|
+
stopListening(); // Also stop the underlying AsyncStorage listener
|
|
222
184
|
} else {
|
|
223
|
-
await
|
|
224
|
-
setIsListening(true);
|
|
185
|
+
await startCapturing();
|
|
225
186
|
}
|
|
226
|
-
}, [isListening]);
|
|
187
|
+
}, [isListening, startCapturing, stopCapturing]);
|
|
227
188
|
const handleClearEvents = useCallback(() => {
|
|
228
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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(
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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;
|