@buoy-gg/storage 7.0.32 → 7.0.34

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.
@@ -3,9 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.createStorageTool = createStorageTool;
7
- exports.storageToolPreset = void 0;
6
+ exports.storageToolPreset = exports.createStorageTool = void 0;
8
7
  var _floatingToolsCore = require("@buoy-gg/floating-tools-core");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
9
9
  var _StorageModalWithTabs = require("./storage/components/StorageModalWithTabs");
10
10
  var _jsxRuntime = require("react/jsx-runtime");
11
11
  /**
@@ -33,7 +33,7 @@ var _jsxRuntime = require("react/jsx-runtime");
33
33
  * - Live storage event monitoring
34
34
  * - Required key validation
35
35
  */
36
- const storageToolPreset = exports.storageToolPreset = {
36
+ const storageToolPreset = exports.storageToolPreset = (0, _sharedUi.defineToolPreset)({
37
37
  id: "storage",
38
38
  name: "STORAGE",
39
39
  description: "AsyncStorage browser",
@@ -48,7 +48,7 @@ const storageToolPreset = exports.storageToolPreset = {
48
48
  requiredStorageKeys: [],
49
49
  enableSharedModalDimensions: false
50
50
  }
51
- };
51
+ });
52
52
 
53
53
  /**
54
54
  * Create a custom AsyncStorage browser tool configuration.
@@ -74,21 +74,7 @@ const storageToolPreset = exports.storageToolPreset = {
74
74
  * });
75
75
  * ```
76
76
  */
77
- function createStorageTool(options) {
78
- return {
79
- id: options?.id || "storage",
80
- name: options?.name || "STORAGE",
81
- description: options?.description || "AsyncStorage browser",
82
- slot: "both",
83
- icon: ({
84
- size
85
- }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_floatingToolsCore.StorageIcon, {
86
- size: size
87
- }),
88
- component: _StorageModalWithTabs.StorageModalWithTabs,
89
- props: {
90
- requiredStorageKeys: options?.requiredStorageKeys || [],
91
- enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
92
- }
93
- };
94
- }
77
+ const createStorageTool = exports.createStorageTool = (0, _sharedUi.createToolFactory)(storageToolPreset, options => ({
78
+ requiredStorageKeys: options.requiredStorageKeys || [],
79
+ enableSharedModalDimensions: options.enableSharedModalDimensions ?? false
80
+ }));
@@ -861,19 +861,60 @@ function GameUIStorageBrowser({
861
861
  children: searchQuery ? "Upgrade to Pro to search your full storage" : "Upgrade to Pro to unlock full storage access"
862
862
  })]
863
863
  }) : null;
864
- const emptyStateNode = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
865
- style: styles.emptyState,
866
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Database, {
867
- size: 32,
868
- color: _sharedUi.night.color.textTertiary
869
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
870
- style: styles.emptyTitle,
871
- children: searchQuery ? "No results found" : activeFilter === "all" ? "No valid keys found" : activeFilter === "missing" ? "No missing keys" : "No issues found"
872
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
873
- style: styles.emptySubtitle,
874
- children: searchQuery ? `No keys matching "${searchQuery}"` : activeFilter === "all" ? "All keys have issues or are missing" : activeFilter === "missing" ? "All required keys are present" : "All storage keys are correctly configured"
875
- })]
876
- });
864
+ const emptyStateNode = (() => {
865
+ // Short-circuits on the first real key, so this costs nothing.
866
+ const hasAnyKeys = allStorageKeys.some(k => !(0, _sharedUi.isDevToolsStorageKey)(k.key));
867
+
868
+ // Three causes here are unlike any other tool's: an empty MISSING or
869
+ // ISSUES list is a PASS, not a failure, and an empty VALID list means
870
+ // everything is broken. Those skip the shared resolver and say so.
871
+ let content;
872
+ if (searchQuery || !hasAnyKeys) {
873
+ content = (0, _sharedUi.resolveListEmptyState)({
874
+ subject: "keys",
875
+ verb: "registered",
876
+ search: searchQuery,
877
+ total: hasAnyKeys ? 1 : 0,
878
+ waiting: {
879
+ // Previously this fell into the "all" branch and claimed every key
880
+ // had an issue — of the zero keys that exist.
881
+ otto: "empty",
882
+ title: "No storage keys",
883
+ message: "Nothing is registered for this tool to read."
884
+ }
885
+ });
886
+ if (searchQuery && hasAnyKeys) {
887
+ // No count: the pre-search total would mean re-running the whole key
888
+ // pipeline a second time for a number the sentence does not need.
889
+ content = {
890
+ ...content,
891
+ message: "No keys match. Clear the search to see them."
892
+ };
893
+ }
894
+ } else if (activeFilter === "missing") {
895
+ content = {
896
+ otto: "thumbsup",
897
+ title: "No missing keys",
898
+ message: "All required keys are present"
899
+ };
900
+ } else if (activeFilter === "issues") {
901
+ content = {
902
+ otto: "cool",
903
+ title: "No issues found",
904
+ message: "All storage keys are correctly configured"
905
+ };
906
+ } else {
907
+ content = {
908
+ otto: "broken",
909
+ title: "No valid keys found",
910
+ message: "All keys have issues or are missing"
911
+ };
912
+ }
913
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
914
+ ...content,
915
+ testID: "buoy-storage-keys-empty"
916
+ });
917
+ })();
877
918
  const upgradeModalNode = /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ProUpgradeModal, {
878
919
  visible: showUpgradeModal,
879
920
  onClose: () => setShowUpgradeModal(false),
@@ -29,6 +29,41 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
29
29
  * because it is part of the package's public exports.
30
30
  */
31
31
  const FREE_TIER_EVENT_LIMIT = exports.FREE_TIER_EVENT_LIMIT = _sharedUi.TIER_LIMITS["storage.events"].free;
32
+ /**
33
+ * Persistence recipes. These keys were always written straight to
34
+ * AsyncStorage (not the FileSystem-first persistentStorage), so they keep
35
+ * that backend — a user's saved tab, filters and pins survive the upgrade.
36
+ */
37
+ const ASYNC_STORAGE_BACKEND = {
38
+ getItem: key => _asyncStorage.default.getItem(key),
39
+ setItem: (key, value) => _asyncStorage.default.setItem(key, value)
40
+ };
41
+ const TAB_PERSISTENCE = {
42
+ storage: ASYNC_STORAGE_BACKEND,
43
+ // Stored as the bare tab name, not JSON.
44
+ parse: raw => raw === "browser" || raw === "events" ? raw : undefined,
45
+ serialize: tab => tab
46
+ };
47
+ const BOOLEAN_PERSISTENCE = {
48
+ storage: ASYNC_STORAGE_BACKEND,
49
+ // Stored as "true" / "false".
50
+ parse: raw => raw === "true",
51
+ serialize: value => value.toString()
52
+ };
53
+ const STRING_SET_PERSISTENCE = {
54
+ storage: ASYNC_STORAGE_BACKEND,
55
+ parse: raw => new Set(JSON.parse(raw)),
56
+ serialize: set => JSON.stringify(Array.from(set))
57
+ };
58
+ const IGNORED_PATTERNS_PERSISTENCE = {
59
+ ...STRING_SET_PERSISTENCE,
60
+ parse: raw => {
61
+ const filters = JSON.parse(raw);
62
+ // Always ensure @react_buoy is included to hide dev tool keys
63
+ if (!filters.includes("@react_buoy")) filters.push("@react_buoy");
64
+ return new Set(filters);
65
+ }
66
+ };
32
67
  function StorageModalWithTabs({
33
68
  visible,
34
69
  onClose,
@@ -43,7 +78,7 @@ function StorageModalWithTabs({
43
78
  }) {
44
79
  const isPro = (0, _license.useIsPro)();
45
80
  const eventLimit = (0, _sharedUi.useLimit)("storage.events");
46
- const [activeTab, setActiveTab] = (0, _react.useState)("browser");
81
+ const [activeTab, setActiveTab] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.storage.activeTab(), "browser", TAB_PERSISTENCE);
47
82
  (0, _react.useEffect)(() => {
48
83
  onTabChange?.(activeTab);
49
84
  }, [activeTab, onTabChange]);
@@ -59,7 +94,7 @@ function StorageModalWithTabs({
59
94
  const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
60
95
 
61
96
  // Local state to control subscription (true = subscribed, false = unsubscribed)
62
- const [isListeningEnabled, setIsListeningEnabled] = (0, _react.useState)(true);
97
+ const [isListeningEnabled, setIsListeningEnabled] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.storage.isMonitoring(), true, BOOLEAN_PERSISTENCE);
63
98
 
64
99
  // Event Listener state - using centralized store via hook
65
100
  // Pass enabled option to control subscription - this allows events tool to
@@ -76,8 +111,9 @@ function StorageModalWithTabs({
76
111
  const [selectedHistoryEventIndex, setSelectedHistoryEventIndex] = (0, _react.useState)(null);
77
112
  const [selectedEventIndex, setSelectedEventIndex] = (0, _react.useState)(0);
78
113
  const [showFilters, setShowFilters] = (0, _react.useState)(false);
79
- const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set(["@react_buoy", "@RNAsyncStorage", "redux-persist", "persist:"]) // Auto-hide dev tool keys by default
80
- );
114
+ const [ignoredPatterns, setIgnoredPatterns] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.storage.eventFilters(),
115
+ // Auto-hide dev tool keys by default
116
+ new Set(["@react_buoy", "@RNAsyncStorage", "redux-persist", "persist:"]), IGNORED_PATTERNS_PERSISTENCE);
81
117
  /**
82
118
  * Which backend both tabs are looking at.
83
119
  *
@@ -93,12 +129,8 @@ function StorageModalWithTabs({
93
129
  const [storageTypeFilter, setStorageTypeFilter] = (0, _react.useState)("all");
94
130
  const [enabledStorageTypes, setEnabledStorageTypes] = (0, _react.useState)(new Set(['async', 'mmkv', 'secure']) // All enabled by default
95
131
  );
96
- const [pinnedKeys, setPinnedKeys] = (0, _react.useState)(new Set());
132
+ const [pinnedKeys, setPinnedKeys] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.storage.pinnedKeys(), new Set(), STRING_SET_PERSISTENCE);
97
133
  const lastEventRef = (0, _react.useRef)(null);
98
- const hasLoadedFilters = (0, _react.useRef)(false);
99
- const hasLoadedPins = (0, _react.useRef)(false);
100
- const hasLoadedTabState = (0, _react.useRef)(false);
101
- const hasLoadedMonitoringState = (0, _react.useRef)(false);
102
134
 
103
135
  // isListening = enabled and capturing (has subscribers)
104
136
  const isListening = isListeningEnabled && isCapturing;
@@ -113,140 +145,6 @@ function StorageModalWithTabs({
113
145
 
114
146
  // Timer removed - using useTickEveryMinute hook instead
115
147
 
116
- // Load persisted tab state on mount
117
- (0, _react.useEffect)(() => {
118
- if (!visible || hasLoadedTabState.current) return;
119
- const loadTabState = async () => {
120
- try {
121
- const storedTab = await _asyncStorage.default.getItem(_sharedUi.devToolsStorageKeys.storage.activeTab());
122
- if (storedTab && (storedTab === "browser" || storedTab === "events")) {
123
- setActiveTab(storedTab);
124
- }
125
- hasLoadedTabState.current = true;
126
- } catch (error) {
127
- // Failed to load tab state
128
- }
129
- };
130
- loadTabState();
131
- }, [visible]);
132
-
133
- // Load persisted monitoring state on mount
134
- (0, _react.useEffect)(() => {
135
- if (!visible || hasLoadedMonitoringState.current) return;
136
- const loadMonitoringState = async () => {
137
- try {
138
- const storedMonitoring = await _asyncStorage.default.getItem(_sharedUi.devToolsStorageKeys.storage.isMonitoring());
139
- if (storedMonitoring !== null) {
140
- const shouldMonitor = storedMonitoring === "true";
141
- setIsListeningEnabled(shouldMonitor);
142
- }
143
- hasLoadedMonitoringState.current = true;
144
- } catch (error) {
145
- // Failed to load monitoring state
146
- }
147
- };
148
- loadMonitoringState();
149
- }, [visible]);
150
-
151
- // Note: Conversations will appear when storage events are triggered
152
- // Click on any conversation to see the unified view with toggle cards
153
-
154
- // Save tab state when it changes
155
- (0, _react.useEffect)(() => {
156
- if (!hasLoadedTabState.current) return; // Don't save on initial load
157
-
158
- const saveTabState = async () => {
159
- try {
160
- await _asyncStorage.default.setItem(_sharedUi.devToolsStorageKeys.storage.activeTab(), activeTab);
161
- } catch (error) {
162
- // Failed to save tab state
163
- }
164
- };
165
- saveTabState();
166
- }, [activeTab]);
167
-
168
- // Save monitoring state when it changes
169
- (0, _react.useEffect)(() => {
170
- if (!hasLoadedMonitoringState.current) return; // Don't save on initial load
171
-
172
- const saveMonitoringState = async () => {
173
- try {
174
- await _asyncStorage.default.setItem(_sharedUi.devToolsStorageKeys.storage.isMonitoring(), isListening.toString());
175
- } catch (error) {
176
- // Failed to save monitoring state
177
- }
178
- };
179
- saveMonitoringState();
180
- }, [isListening]);
181
-
182
- // Load persisted filters on mount
183
- (0, _react.useEffect)(() => {
184
- if (!visible || hasLoadedFilters.current) return;
185
- const loadFilters = async () => {
186
- try {
187
- const storedFilters = await _asyncStorage.default.getItem(_sharedUi.devToolsStorageKeys.storage.eventFilters());
188
- if (storedFilters) {
189
- const filters = JSON.parse(storedFilters);
190
- // Always ensure @react_buoy is included to hide dev tool keys
191
- if (!filters.includes("@react_buoy")) {
192
- filters.push("@react_buoy");
193
- }
194
- setIgnoredPatterns(new Set(filters));
195
- }
196
- hasLoadedFilters.current = true;
197
- } catch (error) {
198
- // Failed to load filters
199
- }
200
- };
201
- loadFilters();
202
- }, [visible]);
203
-
204
- // Save filters when they change
205
- (0, _react.useEffect)(() => {
206
- if (!hasLoadedFilters.current) return; // Don't save on initial load
207
-
208
- const saveFilters = async () => {
209
- try {
210
- const filters = Array.from(ignoredPatterns);
211
- await _asyncStorage.default.setItem(_sharedUi.devToolsStorageKeys.storage.eventFilters(), JSON.stringify(filters));
212
- } catch (error) {
213
- // Failed to save filters
214
- }
215
- };
216
- saveFilters();
217
- }, [ignoredPatterns]);
218
-
219
- // Load persisted pinned keys on mount
220
- (0, _react.useEffect)(() => {
221
- if (!visible || hasLoadedPins.current) return;
222
- const loadPins = async () => {
223
- try {
224
- const stored = await _asyncStorage.default.getItem(_sharedUi.devToolsStorageKeys.storage.pinnedKeys());
225
- if (stored) {
226
- setPinnedKeys(new Set(JSON.parse(stored)));
227
- }
228
- hasLoadedPins.current = true;
229
- } catch (error) {
230
- // Failed to load pinned keys
231
- }
232
- };
233
- loadPins();
234
- }, [visible]);
235
-
236
- // Save pinned keys when they change
237
- (0, _react.useEffect)(() => {
238
- if (!hasLoadedPins.current) return; // Don't save on initial load
239
-
240
- const savePins = async () => {
241
- try {
242
- await _asyncStorage.default.setItem(_sharedUi.devToolsStorageKeys.storage.pinnedKeys(), JSON.stringify(Array.from(pinnedKeys)));
243
- } catch (error) {
244
- // Failed to save pinned keys
245
- }
246
- };
247
- savePins();
248
- }, [pinnedKeys]);
249
-
250
148
  // Event listener setup - now handled by useStorageEvents hook
251
149
  // The hook subscribes to the centralized storageEventStore
252
150
 
@@ -676,18 +574,26 @@ function StorageModalWithTabs({
676
574
  const filtered = storageTypeFilter !== "all";
677
575
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
678
576
  style: styles.flexFill,
679
- children: [eventTypeFilterBar, /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
680
- style: styles.emptyState,
681
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Database, {
682
- size: 48,
683
- color: _sharedUi.night.color.textTertiary
684
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
685
- style: styles.emptyTitle,
686
- children: !isListening ? "Event listener is paused" : filtered ? `No ${storageTypeFilter} events` : "No storage events yet"
687
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
688
- style: styles.emptySubtitle,
689
- children: !isListening ? "Press play to start monitoring" : filtered ? "Other backends have events — pick All Types to see them" : "Storage operations will appear here"
690
- })]
577
+ children: [eventTypeFilterBar, /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
578
+ testID: "buoy-storage-events-empty"
579
+ // Three literal causes, no search on this tab — so it renders
580
+ // through the shared shell without the shared resolver. The
581
+ // backend pills need their own sentence naming the pill that
582
+ // undoes them, which the generic "Filtered Out" would drop.
583
+ ,
584
+ ...(!isListening ? {
585
+ otto: "paused",
586
+ title: "Event listener is paused",
587
+ message: "Press play to start monitoring"
588
+ } : filtered ? {
589
+ otto: "filtering",
590
+ title: `No ${storageTypeFilter} events`,
591
+ message: "Other backends have events — pick All Types to see them"
592
+ } : {
593
+ otto: "listening",
594
+ title: "No storage events yet",
595
+ message: "Storage operations will appear here"
596
+ })
691
597
  })]
692
598
  });
693
599
  }
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.useStorageEvents = useStorageEvents;
7
7
  var _react = require("react");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
8
9
  var _storageEventStore = require("../stores/storageEventStore");
9
10
  /**
10
11
  * useStorageEvents Hook
@@ -39,60 +40,22 @@ function useStorageEvents(options = {}) {
39
40
  maxEvents = 500,
40
41
  enabled = true
41
42
  } = options;
42
- const [events, setEvents] = (0, _react.useState)([]);
43
- // Track capturing state locally so it updates reactively with the subscription
44
- const [isCapturing, setIsCapturing] = (0, _react.useState)(enabled);
45
-
46
- // Subscribe to store - this automatically starts/stops the storage listener
47
- // based on subscriber count (Subscribable pattern from TanStack Query)
48
- // Only subscribe when enabled is true
49
- (0, _react.useEffect)(() => {
50
- if (!enabled) {
51
- setIsCapturing(false);
52
- return;
53
- }
54
-
55
- // Frame-coalesced (same pattern as network's useNetworkEventList):
56
- // bursts of storage writes land as ONE commit per frame instead of
57
- // one per emission — the dominant live-churn cost measured on the
58
- // network tool (tool-bench 2026-07-06).
59
- let rafId = null;
60
- let pending = null;
61
- const unsubscribe = _storageEventStore.storageEventStore.subscribeToEvents(allEvents => {
62
- pending = allEvents;
63
- if (rafId != null) return;
64
- rafId = requestAnimationFrame(() => {
65
- rafId = null;
66
- if (!pending) return;
67
- // Filter by storage type if specified
68
- let filtered = pending;
69
- pending = null;
70
- if (storageType !== "all") {
71
- filtered = filtered.filter(e => e.storageType === storageType);
72
- }
73
- // Limit events
74
- if (filtered.length > maxEvents) {
75
- filtered = filtered.slice(0, maxEvents);
76
- }
77
- setEvents(filtered);
78
- });
79
- });
80
-
81
- // Mark as capturing after successful subscription
82
- setIsCapturing(true);
83
- return () => {
84
- unsubscribe();
85
- if (rafId != null) cancelAnimationFrame(rafId);
86
- // Note: Don't set isCapturing=false here because we might be
87
- // re-subscribing due to dependency change, not actually stopping
88
- };
89
- }, [storageType, maxEvents, enabled]);
90
- const clearEvents = (0, _react.useCallback)(() => {
91
- _storageEventStore.storageEventStore.clearEvents();
92
- }, []);
43
+ // Stable per storageType so the shared hook doesn't resubscribe every render.
44
+ const select = (0, _react.useMemo)(() => storageType === "all" ? undefined : events => events.filter(e => e.storageType === storageType), [storageType]);
45
+ // Subscribing auto-starts the storage listener (Subscribable pattern), and
46
+ // the shared hook frame-coalesces write bursts into one commit.
47
+ const {
48
+ events,
49
+ clearEvents,
50
+ isActive
51
+ } = (0, _sharedUi.useEventStoreEvents)(_storageEventStore.storageEventStore, {
52
+ maxEvents,
53
+ enabled,
54
+ select
55
+ });
93
56
  return {
94
57
  events,
95
58
  clearEvents,
96
- isCapturing
59
+ isCapturing: isActive
97
60
  };
98
61
  }