@buoy-gg/storage 7.0.7 → 7.0.8

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 (42) hide show
  1. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +1 -1
  2. package/package.json +5 -5
  3. package/lib/commonjs/storage/components/GameUIStorageStats.js +0 -746
  4. package/lib/commonjs/storage/components/StorageActions.js +0 -178
  5. package/lib/commonjs/storage/components/StorageEventListener.js +0 -357
  6. package/lib/commonjs/storage/components/StorageFilterViewV2.js +0 -42
  7. package/lib/commonjs/storage/hooks/useMeasuredWidth.js +0 -74
  8. package/lib/commonjs/storage/hooks/useTickEverySecond.js +0 -21
  9. package/lib/commonjs/storage/index.js +0 -258
  10. package/lib/commonjs/storage/utils/index.js +0 -230
  11. package/lib/commonjs/storage/utils/objectDiff.js +0 -157
  12. package/lib/commonjs/storage/utils/safeAsyncStorage.js +0 -140
  13. package/lib/module/storage/components/GameUIStorageStats.js +0 -743
  14. package/lib/module/storage/components/StorageActions.js +0 -174
  15. package/lib/module/storage/components/StorageEventListener.js +0 -354
  16. package/lib/module/storage/components/StorageFilterViewV2.js +0 -38
  17. package/lib/module/storage/hooks/useMeasuredWidth.js +0 -71
  18. package/lib/module/storage/hooks/useTickEverySecond.js +0 -18
  19. package/lib/module/storage/index.js +0 -34
  20. package/lib/module/storage/utils/index.js +0 -28
  21. package/lib/module/storage/utils/objectDiff.js +0 -153
  22. package/lib/module/storage/utils/safeAsyncStorage.js +0 -134
  23. package/lib/typescript/storage/components/GameUIStorageStats.d.ts +0 -7
  24. package/lib/typescript/storage/components/GameUIStorageStats.d.ts.map +0 -1
  25. package/lib/typescript/storage/components/StorageActions.d.ts +0 -10
  26. package/lib/typescript/storage/components/StorageActions.d.ts.map +0 -1
  27. package/lib/typescript/storage/components/StorageEventListener.d.ts +0 -6
  28. package/lib/typescript/storage/components/StorageEventListener.d.ts.map +0 -1
  29. package/lib/typescript/storage/components/StorageFilterViewV2.d.ts +0 -9
  30. package/lib/typescript/storage/components/StorageFilterViewV2.d.ts.map +0 -1
  31. package/lib/typescript/storage/hooks/useMeasuredWidth.d.ts +0 -37
  32. package/lib/typescript/storage/hooks/useMeasuredWidth.d.ts.map +0 -1
  33. package/lib/typescript/storage/hooks/useTickEverySecond.d.ts +0 -6
  34. package/lib/typescript/storage/hooks/useTickEverySecond.d.ts.map +0 -1
  35. package/lib/typescript/storage/index.d.ts +0 -21
  36. package/lib/typescript/storage/index.d.ts.map +0 -1
  37. package/lib/typescript/storage/utils/index.d.ts +0 -10
  38. package/lib/typescript/storage/utils/index.d.ts.map +0 -1
  39. package/lib/typescript/storage/utils/objectDiff.d.ts +0 -35
  40. package/lib/typescript/storage/utils/objectDiff.d.ts.map +0 -1
  41. package/lib/typescript/storage/utils/safeAsyncStorage.d.ts +0 -56
  42. package/lib/typescript/storage/utils/safeAsyncStorage.d.ts.map +0 -1
@@ -1,174 +0,0 @@
1
- "use strict";
2
-
3
- import { View, Text, TouchableOpacity, StyleSheet, Alert } from "react-native";
4
- import { useState, useCallback, useEffect } from "react";
5
- import { clearAllStorageIncludingDevTools } from "../utils/clearAllStorage";
6
- import { RefreshCw, Trash2, CopyButton, ProUpgradeModal, buoyColors } from "@buoy-gg/shared-ui";
7
- import { useIsPro } from "@buoy-gg/license";
8
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
9
- export function StorageActions({
10
- storageKeys,
11
- onClearAll,
12
- onRefresh,
13
- totalCount
14
- }) {
15
- const [isRefreshing, setIsRefreshing] = useState(false);
16
- const [showUpgradeModal, setShowUpgradeModal] = useState(false);
17
- const isPro = useIsPro();
18
-
19
- // Auto-close upgrade modal when user becomes Pro
20
- useEffect(() => {
21
- if (showUpgradeModal && isPro) {
22
- setShowUpgradeModal(false);
23
- }
24
- }, [showUpgradeModal, isPro]);
25
- const handleRefresh = useCallback(async () => {
26
- setIsRefreshing(true);
27
- try {
28
- await onRefresh();
29
- } finally {
30
- setTimeout(() => setIsRefreshing(false), 500);
31
- }
32
- }, [onRefresh]);
33
- const handleClear = () => {
34
- // Gate clear behind Pro
35
- if (!isPro) {
36
- setShowUpgradeModal(true);
37
- return;
38
- }
39
- Alert.alert("Clear Storage", "Choose what to clear:", [{
40
- text: "Cancel",
41
- style: "cancel"
42
- }, {
43
- text: "Clear App Data",
44
- onPress: handleClearAppData
45
- }, {
46
- text: "Clear Everything",
47
- style: "destructive",
48
- onPress: handleClearEverything
49
- }]);
50
- };
51
- const handleClearAppData = async () => {
52
- try {
53
- await onClearAll();
54
- await onRefresh(); // Auto-refresh after clearing
55
- } catch (error) {
56
- Alert.alert("Error", `Failed to clear storage: ${error}`);
57
- }
58
- };
59
- const handleClearEverything = async () => {
60
- // Clear everything directly without extra confirmation
61
- try {
62
- await clearAllStorageIncludingDevTools();
63
- await onRefresh(); // Auto-refresh after clearing
64
-
65
- // Show success message briefly
66
- Alert.alert("Success", "All storage cleared including dev tools settings.", [{
67
- text: "OK"
68
- }], {
69
- cancelable: true
70
- });
71
- } catch (error) {
72
- Alert.alert("Error", `Failed to clear all storage: ${error}`);
73
- }
74
- };
75
- return /*#__PURE__*/_jsxs(View, {
76
- style: styles.headerContainer,
77
- children: [/*#__PURE__*/_jsx(View, {
78
- style: styles.leftSection,
79
- children: /*#__PURE__*/_jsxs(Text, {
80
- style: styles.keyCount,
81
- children: [totalCount, " ", totalCount === 1 ? "key" : "keys", " found"]
82
- })
83
- }), /*#__PURE__*/_jsxs(View, {
84
- style: styles.headerActions,
85
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
86
- "sentry-label": "ignore storage refresh button",
87
- onPress: handleRefresh,
88
- style: [styles.iconButton, isRefreshing && styles.activeButton],
89
- accessibilityLabel: "Refresh storage",
90
- children: /*#__PURE__*/_jsx(RefreshCw, {
91
- size: 16,
92
- color: isRefreshing ? "#10B981" : buoyColors.textSecondary
93
- })
94
- }), /*#__PURE__*/_jsx(CopyButton, {
95
- value: storageKeys,
96
- size: 16,
97
- buttonStyle: styles.iconButton,
98
- colors: {
99
- idle: "#3B82F6",
100
- success: "#10B981",
101
- error: "#F87171"
102
- }
103
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
104
- "sentry-label": "ignore storage clear button",
105
- onPress: handleClear,
106
- style: styles.iconButton,
107
- accessibilityLabel: "Clear storage",
108
- children: /*#__PURE__*/_jsx(Trash2, {
109
- size: 16,
110
- color: "#F87171"
111
- })
112
- })]
113
- }), /*#__PURE__*/_jsx(ProUpgradeModal, {
114
- visible: showUpgradeModal,
115
- onClose: () => setShowUpgradeModal(false),
116
- featureName: "Storage Editing"
117
- })]
118
- });
119
- }
120
- const styles = StyleSheet.create({
121
- // Header styles matching Sentry pattern
122
- headerContainer: {
123
- flexDirection: "row",
124
- alignItems: "center",
125
- justifyContent: "space-between",
126
- paddingHorizontal: 12,
127
- paddingVertical: 8,
128
- backgroundColor: "rgba(128, 128, 128, 0.06)",
129
- borderRadius: 8,
130
- marginBottom: 12,
131
- borderWidth: 1,
132
- borderColor: "rgba(128, 128, 128, 0.16)"
133
- },
134
- leftSection: {
135
- flexDirection: "row",
136
- alignItems: "center",
137
- gap: 8
138
- },
139
- keyCount: {
140
- color: buoyColors.textSecondary,
141
- fontSize: 12,
142
- fontWeight: "500"
143
- },
144
- copiedBadge: {
145
- backgroundColor: "#10B981",
146
- paddingHorizontal: 8,
147
- paddingVertical: 3,
148
- borderRadius: 10
149
- },
150
- copiedText: {
151
- color: "#FFFFFF",
152
- fontSize: 10,
153
- fontWeight: "600"
154
- },
155
- headerActions: {
156
- flexDirection: "row",
157
- alignItems: "center",
158
- gap: 8
159
- },
160
- iconButton: {
161
- width: 32,
162
- height: 32,
163
- borderRadius: 6,
164
- backgroundColor: "rgba(128, 128, 128, 0.06)",
165
- borderWidth: 1,
166
- borderColor: "rgba(128, 128, 128, 0.16)",
167
- alignItems: "center",
168
- justifyContent: "center"
169
- },
170
- activeButton: {
171
- backgroundColor: "rgba(16, 185, 129, 0.1)",
172
- borderColor: "rgba(16, 185, 129, 0.2)"
173
- }
174
- });
@@ -1,354 +0,0 @@
1
- "use strict";
2
-
3
- import { useEffect, useState, useCallback } from "react";
4
- import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from "react-native";
5
- import { Play, Pause, Trash2, macOSColors, buoyColors } from "@buoy-gg/shared-ui";
6
- import { startListening, stopListening, addListener, isListening as checkIsListening } from "../utils/AsyncStorageListener";
7
- import { translateStorageAction } from "../utils/storageActionHelpers";
8
- import { isMMKVAvailable } from "../utils/mmkvAvailability";
9
-
10
- // Conditionally import MMKV listener
11
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
- let addMMKVListener;
13
-
14
- // Define type for MMKV events (imported conditionally at runtime)
15
-
16
- if (isMMKVAvailable()) {
17
- const mmkvListener = require("../utils/MMKVListener");
18
- addMMKVListener = mmkvListener.addMMKVListener;
19
- }
20
-
21
- // Unified event type that covers both AsyncStorage and MMKV
22
-
23
- /**
24
- * Storage event listener component for monitoring AsyncStorage and MMKV operations
25
- * Follows the Sentry component pattern for consistency
26
- */
27
- export function StorageEventListener() {
28
- const [events, setEvents] = useState([]);
29
- const [isListening, setIsListening] = useState(false);
30
- const [isAsyncStorageAvailable, setIsAsyncStorageAvailable] = useState(false);
31
- useEffect(() => {
32
- // Component mounted, AsyncStorage is available
33
- setIsAsyncStorageAvailable(true);
34
-
35
- // Add listener for AsyncStorage events
36
- const unsubscribeAsync = addListener(event => {
37
- setEvents(prev => {
38
- const newEvents = [{
39
- ...event,
40
- storageType: 'async'
41
- }, ...prev.slice(0, 99)];
42
- return newEvents;
43
- });
44
- });
45
-
46
- // Add listener for MMKV events (only if available)
47
- let unsubscribeMMKV = () => {};
48
- if (isMMKVAvailable() && addMMKVListener) {
49
- unsubscribeMMKV = addMMKVListener(event => {
50
- setEvents(prev => {
51
- const newEvents = [{
52
- ...event,
53
- storageType: 'mmkv'
54
- }, ...prev.slice(0, 99)];
55
- return newEvents;
56
- });
57
- });
58
- }
59
-
60
- // Check initial listening state
61
- const initialState = checkIsListening();
62
- setIsListening(initialState);
63
- return () => {
64
- if (checkIsListening()) {
65
- stopListening();
66
- }
67
- unsubscribeAsync();
68
- unsubscribeMMKV();
69
- };
70
- }, []);
71
- const handleToggleListening = useCallback(async () => {
72
- if (!isAsyncStorageAvailable) {
73
- return;
74
- }
75
- if (isListening) {
76
- // Stopping listener
77
- stopListening();
78
- setIsListening(false);
79
- } else {
80
- // Starting listener
81
- await startListening();
82
- setIsListening(true);
83
- }
84
- }, [isListening, isAsyncStorageAvailable]);
85
- const handleClearEvents = useCallback(() => {
86
- // Clearing all events
87
- setEvents([]);
88
- }, []);
89
- const formatEventData = event => {
90
- if (!event.data) return "";
91
-
92
- // MMKV events
93
- if (event.storageType === 'mmkv') {
94
- const mmkvEvent = event;
95
-
96
- // Show instance ID for MMKV events
97
- const instancePrefix = `[${mmkvEvent.instanceId}] `;
98
- if (mmkvEvent.action.startsWith('set.') || mmkvEvent.action.startsWith('get.') || mmkvEvent.action === 'delete') {
99
- const keyInfo = mmkvEvent.data?.key ? `${mmkvEvent.data.key}` : "";
100
- const typeInfo = mmkvEvent.data?.valueType ? ` (${mmkvEvent.data.valueType})` : "";
101
- return instancePrefix + keyInfo + typeInfo;
102
- }
103
- if (mmkvEvent.action === 'clearAll') {
104
- return instancePrefix + "All keys";
105
- }
106
- return instancePrefix;
107
- }
108
-
109
- // AsyncStorage events
110
- if (event.action === "setItem" || event.action === "removeItem" || event.action === "mergeItem") {
111
- return event.data.key || "";
112
- }
113
- if (event.action === "multiSet" || event.action === "multiMerge") {
114
- return `${event.data.pairs?.length || 0} pairs`;
115
- }
116
- if (event.action === "multiRemove") {
117
- return `${event.data.keys?.length || 0} keys`;
118
- }
119
- if (event.action === "clear") {
120
- return "All storage";
121
- }
122
- return "";
123
- };
124
- const getActionColor = action => {
125
- // MMKV actions
126
- if (action.startsWith('set.')) {
127
- return "#10B981"; // Green for writes
128
- }
129
- if (action.startsWith('get.')) {
130
- return "#F59E0B"; // Orange for reads
131
- }
132
- if (action === 'delete' || action === 'clearAll') {
133
- return "#EF4444"; // Red for deletes
134
- }
135
-
136
- // AsyncStorage actions
137
- switch (action) {
138
- case "setItem":
139
- case "multiSet":
140
- return "#10B981";
141
- // Green for write
142
- case "removeItem":
143
- case "multiRemove":
144
- case "clear":
145
- return "#EF4444";
146
- // Red for delete
147
- case "mergeItem":
148
- case "multiMerge":
149
- return "#3B82F6";
150
- // Blue for merge
151
- default:
152
- return macOSColors.text.icon;
153
- }
154
- };
155
- if (!isAsyncStorageAvailable) {
156
- return null; // Don't show component if AsyncStorage isn't available
157
- }
158
- return /*#__PURE__*/_jsxs(View, {
159
- style: styles.container,
160
- children: [/*#__PURE__*/_jsxs(View, {
161
- style: styles.header,
162
- children: [/*#__PURE__*/_jsxs(View, {
163
- style: styles.headerLeft,
164
- children: [/*#__PURE__*/_jsx(Text, {
165
- style: styles.title,
166
- children: "Storage Events"
167
- }), /*#__PURE__*/_jsxs(View, {
168
- style: styles.statsContainer,
169
- children: [/*#__PURE__*/_jsx(Text, {
170
- style: styles.statsText,
171
- children: events.length
172
- }), isListening && /*#__PURE__*/_jsx(View, {
173
- style: styles.listeningIndicator
174
- })]
175
- })]
176
- }), /*#__PURE__*/_jsxs(View, {
177
- style: styles.headerActions,
178
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
179
- "sentry-label": "ignore toggle listening",
180
- onPress: handleToggleListening,
181
- style: [styles.actionButton, isListening ? styles.stopButton : styles.startButton],
182
- accessibilityLabel: isListening ? "Stop listening" : "Start listening",
183
- children: isListening ? /*#__PURE__*/_jsx(Pause, {
184
- size: 14,
185
- color: "#EF4444"
186
- }) : /*#__PURE__*/_jsx(Play, {
187
- size: 14,
188
- color: "#10B981"
189
- })
190
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
191
- "sentry-label": "ignore clear events",
192
- onPress: handleClearEvents,
193
- style: styles.actionButton,
194
- accessibilityLabel: "Clear events",
195
- disabled: events.length === 0,
196
- children: /*#__PURE__*/_jsx(Trash2, {
197
- size: 14,
198
- color: events.length > 0 ? macOSColors.text.icon : macOSColors.border.toggle
199
- })
200
- })]
201
- })]
202
- }), /*#__PURE__*/_jsx(ScrollView, {
203
- style: styles.eventsList,
204
- nestedScrollEnabled: true,
205
- showsVerticalScrollIndicator: false,
206
- "sentry-label": "ignore event list scroll",
207
- children: events.length === 0 ? /*#__PURE__*/_jsx(View, {
208
- style: styles.emptyState,
209
- children: /*#__PURE__*/_jsx(Text, {
210
- style: styles.emptyText,
211
- children: isListening ? "Waiting for storage operations..." : "Start listening to capture events"
212
- })
213
- }) : events.map((event, index) => /*#__PURE__*/_jsxs(View, {
214
- style: styles.eventItem,
215
- children: [/*#__PURE__*/_jsxs(View, {
216
- style: styles.eventLeft,
217
- children: [/*#__PURE__*/_jsx(Text, {
218
- style: [styles.eventAction, {
219
- color: getActionColor(event.action)
220
- }],
221
- children: translateStorageAction(event.action)
222
- }), /*#__PURE__*/_jsx(Text, {
223
- style: styles.eventData,
224
- numberOfLines: 1,
225
- children: formatEventData(event)
226
- })]
227
- }), /*#__PURE__*/_jsx(Text, {
228
- style: styles.eventTime,
229
- children: event.timestamp.toLocaleTimeString([], {
230
- hour: "2-digit",
231
- minute: "2-digit",
232
- second: "2-digit"
233
- })
234
- })]
235
- }, `${event.timestamp.getTime()}-${index}`))
236
- })]
237
- });
238
- }
239
- const styles = StyleSheet.create({
240
- container: {
241
- backgroundColor: "rgba(128, 128, 128, 0.06)",
242
- borderRadius: 8,
243
- borderWidth: 1,
244
- borderColor: "rgba(128, 128, 128, 0.16)",
245
- marginBottom: 12,
246
- maxHeight: 200
247
- },
248
- header: {
249
- flexDirection: "row",
250
- alignItems: "center",
251
- justifyContent: "space-between",
252
- paddingHorizontal: 12,
253
- paddingVertical: 8,
254
- borderBottomWidth: 1,
255
- borderBottomColor: "rgba(128, 128, 128, 0.12)"
256
- },
257
- headerLeft: {
258
- flexDirection: "row",
259
- alignItems: "center",
260
- gap: 8
261
- },
262
- title: {
263
- fontSize: 12,
264
- fontWeight: "500",
265
- color: buoyColors.textSecondary,
266
- textTransform: "uppercase",
267
- letterSpacing: 0.5
268
- },
269
- statsContainer: {
270
- flexDirection: "row",
271
- alignItems: "center",
272
- gap: 6,
273
- backgroundColor: "rgba(0, 0, 0, 0.2)",
274
- paddingHorizontal: 8,
275
- paddingVertical: 2,
276
- borderRadius: 10
277
- },
278
- statsText: {
279
- fontSize: 11,
280
- color: macOSColors.text.icon,
281
- fontWeight: "500"
282
- },
283
- listeningIndicator: {
284
- width: 6,
285
- height: 6,
286
- borderRadius: 3,
287
- backgroundColor: "#10B981"
288
- },
289
- headerActions: {
290
- flexDirection: "row",
291
- alignItems: "center",
292
- gap: 6
293
- },
294
- actionButton: {
295
- width: 28,
296
- height: 28,
297
- borderRadius: 6,
298
- backgroundColor: "rgba(128, 128, 128, 0.06)",
299
- borderWidth: 1,
300
- borderColor: "rgba(128, 128, 128, 0.16)",
301
- alignItems: "center",
302
- justifyContent: "center"
303
- },
304
- startButton: {
305
- backgroundColor: "rgba(16, 185, 129, 0.1)",
306
- borderColor: "rgba(16, 185, 129, 0.2)"
307
- },
308
- stopButton: {
309
- backgroundColor: "rgba(239, 68, 68, 0.1)",
310
- borderColor: "rgba(239, 68, 68, 0.2)"
311
- },
312
- eventsList: {
313
- maxHeight: 150
314
- },
315
- emptyState: {
316
- padding: 20,
317
- alignItems: "center"
318
- },
319
- emptyText: {
320
- fontSize: 11,
321
- color: macOSColors.text.icon,
322
- fontStyle: "italic"
323
- },
324
- eventItem: {
325
- flexDirection: "row",
326
- alignItems: "center",
327
- justifyContent: "space-between",
328
- paddingHorizontal: 12,
329
- paddingVertical: 6,
330
- borderBottomWidth: 1,
331
- borderBottomColor: "rgba(128, 128, 128, 0.06)"
332
- },
333
- eventLeft: {
334
- flex: 1,
335
- flexDirection: "row",
336
- alignItems: "center",
337
- gap: 8,
338
- marginRight: 8
339
- },
340
- eventAction: {
341
- fontSize: 11,
342
- fontWeight: "600",
343
- minWidth: 60
344
- },
345
- eventData: {
346
- fontSize: 11,
347
- color: buoyColors.textSecondary,
348
- flex: 1
349
- },
350
- eventTime: {
351
- fontSize: 10,
352
- color: macOSColors.text.icon
353
- }
354
- });
@@ -1,38 +0,0 @@
1
- "use strict";
2
-
3
- import { Filter, DynamicFilterView } from "@buoy-gg/shared-ui";
4
- import { jsx as _jsx } from "react/jsx-runtime";
5
- export function StorageFilterViewV2({
6
- ignoredPatterns,
7
- onTogglePattern,
8
- onAddPattern,
9
- availableKeys = []
10
- }) {
11
- const filterConfig = {
12
- addFilterSection: {
13
- enabled: true,
14
- placeholder: "Enter key pattern to hide...",
15
- title: "ACTIVE FILTERS",
16
- icon: Filter
17
- },
18
- availableItemsSection: {
19
- enabled: true,
20
- title: "AVAILABLE STORAGE KEYS",
21
- emptyMessage: "No storage keys available. Keys will appear here once loaded.",
22
- items: availableKeys
23
- },
24
- howItWorksSection: {
25
- enabled: true,
26
- title: "HOW STORAGE FILTERS WORK",
27
- description: "Patterns hide matching storage keys from the list. Filters match if the storage key contains the provided text.",
28
- examples: ["• react_buoy → hides keys containing react_buoy", "• @temp → hides @temp_user, @temp_data", "• redux → hides redux-persist:root", "• : → hides all keys with colons"],
29
- icon: Filter
30
- },
31
- onPatternToggle: onTogglePattern,
32
- onPatternAdd: onAddPattern,
33
- activePatterns: ignoredPatterns
34
- };
35
- return /*#__PURE__*/_jsx(DynamicFilterView, {
36
- ...filterConfig
37
- });
38
- }
@@ -1,71 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * Measure a container's width, for the layouts that switch between a card list
5
- * and a wide table.
6
- *
7
- * Three things went wrong before this landed on a callback ref; all three are
8
- * worth knowing, because the same trap is waiting in any other tool that wants
9
- * a width-driven layout.
10
- *
11
- * 1. `onLayout` ALONE IS NOT ENOUGH. On react-native-web it never delivered its
12
- * first callback for these subtrees. The node carries RNW's
13
- * `__reactLayoutHandler`, and a ResizeObserver attached by hand fires on that
14
- * very same node — so the handler is wired and the observer works; RNW's
15
- * shared observer just never calls it. `width` stayed 0 and the table never
16
- * appeared, silently. `onLayout` is still wired here because it works on
17
- * native.
18
- *
19
- * 2. RE-MEASURING EVERY RENDER DEADLOCKS THE RENDERER. Filtering a list changes
20
- * whether its scrollbar is present, which changes the container width, which
21
- * re-renders, which re-measures — with no fixed point. An observer only fires
22
- * when the size genuinely changes, so layout settles.
23
- *
24
- * 3. A MOUNT-ONLY EFFECT MISSES THE NODE. These components early-return a
25
- * loading state before the measured container exists, so `ref.current` is
26
- * null when a `useEffect(…, [])` runs, and it never runs again. A callback
27
- * ref fires whenever the node actually attaches — including the second time.
28
- */
29
-
30
- import { useCallback, useRef, useState } from "react";
31
-
32
- /** The bits of a DOM node we need. This package has no `dom` lib. */
33
-
34
- function getResizeObserver() {
35
- return globalThis.ResizeObserver;
36
- }
37
- function isMeasurable(node) {
38
- return typeof node?.getBoundingClientRect === "function";
39
- }
40
- export function useMeasuredWidth() {
41
- const [width, setWidth] = useState(0);
42
- const observerRef = useRef(null);
43
-
44
- // A sub-pixel wobble is not a layout change; anything larger is.
45
- const apply = useCallback(measured => {
46
- setWidth(prev => Math.abs(prev - measured) > 1 ? measured : prev);
47
- }, []);
48
- const ref = useCallback(node => {
49
- observerRef.current?.disconnect();
50
- observerRef.current = null;
51
-
52
- // Detach (node === null) or a platform without DOM measurement: leave the
53
- // last width in place so a remount doesn't flash the wrong layout.
54
- if (!isMeasurable(node)) return;
55
- apply(node.getBoundingClientRect().width);
56
- const Observer = getResizeObserver();
57
- if (!Observer) return;
58
- const observer = new Observer(entries => {
59
- const measured = entries[0]?.contentRect?.width;
60
- if (typeof measured === "number") apply(measured);
61
- });
62
- observer.observe(node);
63
- observerRef.current = observer;
64
- }, [apply]);
65
- const onLayout = useCallback(event => apply(event.nativeEvent.layout.width), [apply]);
66
- return {
67
- ref,
68
- width,
69
- onLayout
70
- };
71
- }
@@ -1,18 +0,0 @@
1
- "use strict";
2
-
3
- import { useEffect, useState } from "react";
4
-
5
- /**
6
- * Hook that forces a re-render every second
7
- * Used to update relative timestamps (e.g., "2s ago", "1m ago")
8
- */
9
- export function useTickEverySecond(enabled = true) {
10
- const [, setTick] = useState(0);
11
- useEffect(() => {
12
- if (!enabled) return;
13
- const interval = setInterval(() => {
14
- setTick(prev => prev + 1);
15
- }, 1000);
16
- return () => clearInterval(interval);
17
- }, [enabled]);
18
- }
@@ -1,34 +0,0 @@
1
- "use strict";
2
-
3
- // Storage section components
4
- export { StorageSection } from "./components/StorageSection";
5
- export { StorageModalWithTabs } from "./components/StorageModalWithTabs";
6
- export { StorageKeyCard } from "./components/StorageKeyCard";
7
- export { StorageKeyStatsSection } from "./components/StorageKeyStats";
8
- export { StorageKeySection } from "./components/StorageKeySection";
9
- export { StorageBrowserMode } from "./components/StorageBrowserMode";
10
- export { StorageEventsSection } from "./components/StorageEventsSection";
11
- // The five display operations a storage event collapses onto — semantics, not
12
- // presentation, so hosts can filter or group by the same vocabulary the UI uses.
13
- export { STORAGE_OPS, storageOpOf, storageOpDot, isInitialScan } from "./utils/storageOps";
14
- export { StorageEventCard, getValueType } from "./components/StorageEventCard";
15
- export { StorageEventDetailContent, StorageEventDetailFooter } from "./components/StorageEventDetailContent";
16
-
17
- // Storage hooks
18
- export { useAsyncStorageKeys } from "./hooks/useAsyncStorageKeys";
19
- export { useMMKVKeys, useMultiMMKVKeys } from "./hooks/useMMKVKeys";
20
- export { useMMKVInstances, useMMKVInstance, useMMKVInstanceExists } from "./hooks/useMMKVInstances";
21
- export { useStorageEvents } from "./hooks/useStorageEvents";
22
-
23
- // MMKV Components
24
- export { MMKVInstanceSelector } from "./components/MMKVInstanceSelector";
25
- export { MMKVInstanceInfoPanel } from "./components/MMKVInstanceInfoPanel";
26
-
27
- // Storage types
28
- export * from "./types";
29
-
30
- // Storage utilities
31
- export * from "./utils";
32
-
33
- // Storage Event Store
34
- export { storageEventStore, subscribeToStorageEvents, onStorageEvent, getStorageEvents, clearStorageEvents, isStorageCapturing } from "./stores/storageEventStore";