@buoy-gg/redux 7.0.4 → 7.0.6
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/README.md +21 -2
- package/lib/commonjs/index.js +44 -0
- package/lib/commonjs/redux/components/ReduxAutoInstrument.js +20 -0
- package/lib/commonjs/redux/components/ReduxModal.js +123 -68
- package/lib/commonjs/redux/components/index.js +8 -1
- package/lib/commonjs/redux/hooks/useAutoInstrumentRedux.js +32 -42
- package/lib/commonjs/redux/sync/reduxSyncAdapter.js +27 -8
- package/lib/commonjs/redux/utils/activeStoreRegistry.js +73 -0
- package/lib/commonjs/redux/utils/autoInstrument.js +88 -114
- package/lib/commonjs/redux/utils/buoyReduxMiddleware.js +20 -15
- package/lib/commonjs/redux/utils/devtoolsHook.js +137 -0
- package/lib/commonjs/redux/utils/index.js +32 -1
- package/lib/module/index.js +29 -4
- package/lib/module/redux/components/ReduxAutoInstrument.js +17 -0
- package/lib/module/redux/components/ReduxModal.js +122 -68
- package/lib/module/redux/components/index.js +2 -1
- package/lib/module/redux/hooks/useAutoInstrumentRedux.js +33 -43
- package/lib/module/redux/sync/reduxSyncAdapter.js +28 -9
- package/lib/module/redux/utils/activeStoreRegistry.js +63 -0
- package/lib/module/redux/utils/autoInstrument.js +82 -113
- package/lib/module/redux/utils/buoyReduxMiddleware.js +20 -15
- package/lib/module/redux/utils/devtoolsHook.js +130 -0
- package/lib/module/redux/utils/index.js +3 -1
- package/lib/typescript/index.d.ts +19 -5
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/redux/components/ReduxAutoInstrument.d.ts +11 -0
- package/lib/typescript/redux/components/ReduxAutoInstrument.d.ts.map +1 -0
- package/lib/typescript/redux/components/ReduxModal.d.ts +15 -1
- package/lib/typescript/redux/components/ReduxModal.d.ts.map +1 -1
- package/lib/typescript/redux/components/index.d.ts +1 -0
- package/lib/typescript/redux/components/index.d.ts.map +1 -1
- package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts +16 -28
- package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts.map +1 -1
- package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts +18 -4
- package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/redux/utils/activeStoreRegistry.d.ts +47 -0
- package/lib/typescript/redux/utils/activeStoreRegistry.d.ts.map +1 -0
- package/lib/typescript/redux/utils/autoInstrument.d.ts +46 -23
- package/lib/typescript/redux/utils/autoInstrument.d.ts.map +1 -1
- package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts +5 -1
- package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts.map +1 -1
- package/lib/typescript/redux/utils/devtoolsHook.d.ts +45 -0
- package/lib/typescript/redux/utils/devtoolsHook.d.ts.map +1 -0
- package/lib/typescript/redux/utils/index.d.ts +3 -2
- package/lib/typescript/redux/utils/index.d.ts.map +1 -1
- package/package.json +8 -5
package/lib/module/index.js
CHANGED
|
@@ -29,9 +29,20 @@
|
|
|
29
29
|
*
|
|
30
30
|
* That's it! The Redux tool will auto-detect your store and capture actions.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
32
|
+
* HOW CAPTURE WORKS (in priority order):
|
|
33
|
+
* 1. ENHANCER — importing this package claims the official Redux DevTools
|
|
34
|
+
* global (`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__`), which RTK checks by
|
|
35
|
+
* default. If this package loads before your store module, Buoy is inside
|
|
36
|
+
* the store from creation: FULL capture (thunk/RTK Query actions included)
|
|
37
|
+
* and real time travel. To guarantee this ordering, make
|
|
38
|
+
* `import '@buoy-gg/redux';` the FIRST import of your app entry.
|
|
39
|
+
* 2. MIDDLEWARE — add `buoyReduxMiddleware` for explicit full capture.
|
|
40
|
+
* 3. PATCH — otherwise, `FloatingDevTools` binds the store at first mount by
|
|
41
|
+
* patching store.dispatch. Top-level dispatches only (thunk-internal
|
|
42
|
+
* actions bypass a dispatch patch — that's a Redux architecture fact).
|
|
43
|
+
*
|
|
44
|
+
* @example Advanced: Manual middleware (explicit full capture)
|
|
33
45
|
* ```tsx
|
|
34
|
-
* // For time-travel or custom options, you can optionally use middleware:
|
|
35
46
|
* import { buoyReduxMiddleware, withBuoyDevTools } from '@buoy-gg/redux';
|
|
36
47
|
*
|
|
37
48
|
* const store = configureStore({
|
|
@@ -42,6 +53,11 @@
|
|
|
42
53
|
* ```
|
|
43
54
|
*/
|
|
44
55
|
|
|
56
|
+
// SIDE EFFECT (must stay first): claim the Redux DevTools global so stores
|
|
57
|
+
// created after this module loads get the full-capture enhancer. Dev-only,
|
|
58
|
+
// never overrides an existing claimant. See ./redux/utils/devtoolsHook.ts.
|
|
59
|
+
import "./redux/utils/devtoolsHook";
|
|
60
|
+
|
|
45
61
|
// =============================================================================
|
|
46
62
|
// PRESET (Primary entry point for users)
|
|
47
63
|
// =============================================================================
|
|
@@ -55,7 +71,11 @@ export { buoyReduxMiddleware, createBuoyReduxMiddleware, withBuoyDevTools, jumpT
|
|
|
55
71
|
// =============================================================================
|
|
56
72
|
// AUTO-INSTRUMENTATION (For programmatic store instrumentation)
|
|
57
73
|
// =============================================================================
|
|
58
|
-
export { instrumentStore, uninstrumentStore, isStoreInstrumented, isAutoInstrumentActive, getActiveStore } from "./redux/utils/autoInstrument";
|
|
74
|
+
export { instrumentStore, uninstrumentStore, isStoreInstrumented, isAutoInstrumentActive, getActiveStore, getInstrumentationStatus, registerReduxStore } from "./redux/utils/autoInstrument";
|
|
75
|
+
// Store-creation-time capture (the primary, full-capture path). Claimed as a
|
|
76
|
+
// module side effect above; exported for tests/advanced hosts.
|
|
77
|
+
export { buoyDevtoolsEnhancer, claimDevtoolsHook, isDevtoolsHookClaimed } from "./redux/utils/devtoolsHook";
|
|
78
|
+
|
|
59
79
|
// =============================================================================
|
|
60
80
|
// HISTORY ADAPTER (For universal history devtools)
|
|
61
81
|
// =============================================================================
|
|
@@ -75,11 +95,16 @@ export { useAutoInstrumentRedux, useReduxAvailability } from "./redux/hooks/useA
|
|
|
75
95
|
// =============================================================================
|
|
76
96
|
// COMPONENTS (For custom UI implementations)
|
|
77
97
|
// =============================================================================
|
|
78
|
-
|
|
98
|
+
// `ReduxToolView` is the same tool rendered inline (header bar + body + footer)
|
|
99
|
+
// instead of inside a JsModal — for hosts that show tools in a panel.
|
|
100
|
+
export { ReduxModal, ReduxToolView, FREE_TIER_ACTION_LIMIT } from "./redux/components/ReduxModal";
|
|
79
101
|
export { ReduxActionItem } from "./redux/components/ReduxActionItem";
|
|
80
102
|
export { ReduxActionDetailView } from "./redux/components/ReduxActionDetailView";
|
|
81
103
|
export { ReduxActionDetailContent, ReduxActionDetailFooter } from "./redux/components/ReduxActionDetailContent";
|
|
82
104
|
export { ReduxIcon } from "./redux/components/ReduxIcon";
|
|
105
|
+
// Headless startup instrumentation — auto-rendered by @buoy-gg/core's
|
|
106
|
+
// FloatingDevTools; exported for hosts that mount devtools their own way.
|
|
107
|
+
export { ReduxAutoInstrument } from "./redux/components/ReduxAutoInstrument";
|
|
83
108
|
|
|
84
109
|
// =============================================================================
|
|
85
110
|
// TYPES
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useAutoInstrumentRedux } from "../hooks/useAutoInstrumentRedux";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Headless startup instrumentation. Auto-rendered by FloatingDevTools (same
|
|
7
|
+
* pattern as RouteTracker & the overlay tools) so the Redux store is bound
|
|
8
|
+
* from app mount — action log, MCP getState/dispatch, and desktop sync all
|
|
9
|
+
* work WITHOUT the user ever opening the Redux tool. Renders nothing.
|
|
10
|
+
*
|
|
11
|
+
* Safe anywhere: if react-redux is missing or there's no Provider above, the
|
|
12
|
+
* hook records the diagnostic and fails gracefully.
|
|
13
|
+
*/
|
|
14
|
+
export function ReduxAutoInstrument() {
|
|
15
|
+
useAutoInstrumentRedux();
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
@@ -41,7 +41,8 @@ export function ReduxModal({
|
|
|
41
41
|
onClose,
|
|
42
42
|
onBack,
|
|
43
43
|
onMinimize,
|
|
44
|
-
enableSharedModalDimensions = false
|
|
44
|
+
enableSharedModalDimensions = false,
|
|
45
|
+
inline = false
|
|
45
46
|
}) {
|
|
46
47
|
const isPro = useIsPro();
|
|
47
48
|
const [showProModal, setShowProModal] = useState(false);
|
|
@@ -51,7 +52,8 @@ export function ReduxModal({
|
|
|
51
52
|
isInstrumented,
|
|
52
53
|
isLoading: isInstrumentLoading,
|
|
53
54
|
error: instrumentError,
|
|
54
|
-
isReactReduxAvailable
|
|
55
|
+
isReactReduxAvailable,
|
|
56
|
+
captureMechanism
|
|
55
57
|
} = useAutoInstrumentRedux();
|
|
56
58
|
const {
|
|
57
59
|
filteredActions,
|
|
@@ -280,6 +282,95 @@ export function ReduxModal({
|
|
|
280
282
|
// The action list owns its scrolling so FlatList virtualization works
|
|
281
283
|
// (see ZustandModal — same pattern, same tool-bench evidence).
|
|
282
284
|
const listOwnsScroll = selectedAction == null;
|
|
285
|
+
const body = /*#__PURE__*/_jsx(View, {
|
|
286
|
+
style: styles.container,
|
|
287
|
+
children: selectedAction && selectedActionIndex !== null ? /*#__PURE__*/_jsx(ReduxActionDetailContent, {
|
|
288
|
+
action: selectedAction,
|
|
289
|
+
actions: visibleActions,
|
|
290
|
+
selectedIndex: selectedActionIndex,
|
|
291
|
+
onIndexChange: handleIndexChange,
|
|
292
|
+
disableInternalFooter: true
|
|
293
|
+
}) : /*#__PURE__*/_jsxs(_Fragment, {
|
|
294
|
+
children: [instrumentError && !isInstrumented && !reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/_jsxs(View, {
|
|
295
|
+
style: styles.errorBanner,
|
|
296
|
+
children: [/*#__PURE__*/_jsx(AlertTriangle, {
|
|
297
|
+
size: 14,
|
|
298
|
+
color: macOSColors.semantic.error
|
|
299
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
300
|
+
style: styles.errorText,
|
|
301
|
+
children: isReactReduxAvailable ? "Not inside a Redux Provider. Wrap your app with <Provider store={store}>." : "react-redux not installed. Run: npm install react-redux"
|
|
302
|
+
})]
|
|
303
|
+
}), !isEnabled && isInstrumented && /*#__PURE__*/_jsxs(View, {
|
|
304
|
+
style: styles.disabledBanner,
|
|
305
|
+
children: [/*#__PURE__*/_jsx(Power, {
|
|
306
|
+
size: 14,
|
|
307
|
+
color: macOSColors.semantic.warning
|
|
308
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
309
|
+
style: styles.disabledText,
|
|
310
|
+
children: "Action capture is disabled"
|
|
311
|
+
})]
|
|
312
|
+
}), captureMechanism === "patch" && !reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/_jsxs(View, {
|
|
313
|
+
style: styles.disabledBanner,
|
|
314
|
+
children: [/*#__PURE__*/_jsx(AlertTriangle, {
|
|
315
|
+
size: 14,
|
|
316
|
+
color: macOSColors.semantic.warning
|
|
317
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
318
|
+
style: styles.disabledText,
|
|
319
|
+
children: "Top-level capture only \u2014 thunk/RTK Query internals aren't visible. Put import '@buoy-gg/redux' first in your app entry (or add buoyReduxMiddleware) for full capture."
|
|
320
|
+
})]
|
|
321
|
+
}), hasLockedActions && /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
322
|
+
style: styles.lockedBanner,
|
|
323
|
+
onPress: handleUpgradePress,
|
|
324
|
+
activeOpacity: 0.8,
|
|
325
|
+
children: [/*#__PURE__*/_jsx(Lock, {
|
|
326
|
+
size: 14,
|
|
327
|
+
color: buoyColors.primary
|
|
328
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
329
|
+
style: styles.lockedText,
|
|
330
|
+
children: [lockedActionCount, " older ", lockedActionCount === 1 ? "action" : "actions", " locked"]
|
|
331
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
332
|
+
style: styles.upgradeBadge,
|
|
333
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
334
|
+
style: styles.upgradeBadgeText,
|
|
335
|
+
children: "UPGRADE"
|
|
336
|
+
})
|
|
337
|
+
})]
|
|
338
|
+
}), visibleActions.length > 0 ? /*#__PURE__*/_jsx(FlatList, {
|
|
339
|
+
ref: flatListRef,
|
|
340
|
+
data: visibleActions,
|
|
341
|
+
renderItem: renderItem,
|
|
342
|
+
keyExtractor: keyExtractor,
|
|
343
|
+
contentContainerStyle: styles.listContent,
|
|
344
|
+
showsVerticalScrollIndicator: true,
|
|
345
|
+
removeClippedSubviews: true,
|
|
346
|
+
initialNumToRender: 12,
|
|
347
|
+
maxToRenderPerBatch: 8,
|
|
348
|
+
windowSize: 5,
|
|
349
|
+
updateCellsBatchingPeriod: 50
|
|
350
|
+
}) : /*#__PURE__*/_jsx(EmptyState, {
|
|
351
|
+
isEnabled: isEnabled
|
|
352
|
+
})]
|
|
353
|
+
})
|
|
354
|
+
});
|
|
355
|
+
const upgradeModal = /*#__PURE__*/_jsx(ProUpgradeModal, {
|
|
356
|
+
visible: showProModal,
|
|
357
|
+
onClose: () => setShowProModal(false),
|
|
358
|
+
featureName: "Full Action History"
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// The ONLY difference between the phone and the desktop dashboard is which
|
|
362
|
+
// shell the header, body and footer go in. Everything above this is shared.
|
|
363
|
+
if (inline) {
|
|
364
|
+
return /*#__PURE__*/_jsxs(TickProvider, {
|
|
365
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
366
|
+
style: styles.inlineRoot,
|
|
367
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
368
|
+
style: styles.inlineHeader,
|
|
369
|
+
children: renderHeaderContent()
|
|
370
|
+
}), body, footerNode]
|
|
371
|
+
}), upgradeModal]
|
|
372
|
+
});
|
|
373
|
+
}
|
|
283
374
|
return /*#__PURE__*/_jsxs(TickProvider, {
|
|
284
375
|
children: [/*#__PURE__*/_jsx(JsModal, {
|
|
285
376
|
visible: visible,
|
|
@@ -298,75 +389,38 @@ export function ReduxModal({
|
|
|
298
389
|
styles: {},
|
|
299
390
|
footer: footerNode,
|
|
300
391
|
footerHeight: footerHeight,
|
|
301
|
-
children:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
})]
|
|
319
|
-
}), !isEnabled && isInstrumented && /*#__PURE__*/_jsxs(View, {
|
|
320
|
-
style: styles.disabledBanner,
|
|
321
|
-
children: [/*#__PURE__*/_jsx(Power, {
|
|
322
|
-
size: 14,
|
|
323
|
-
color: macOSColors.semantic.warning
|
|
324
|
-
}), /*#__PURE__*/_jsx(Text, {
|
|
325
|
-
style: styles.disabledText,
|
|
326
|
-
children: "Action capture is disabled"
|
|
327
|
-
})]
|
|
328
|
-
}), hasLockedActions && /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
329
|
-
style: styles.lockedBanner,
|
|
330
|
-
onPress: handleUpgradePress,
|
|
331
|
-
activeOpacity: 0.8,
|
|
332
|
-
children: [/*#__PURE__*/_jsx(Lock, {
|
|
333
|
-
size: 14,
|
|
334
|
-
color: buoyColors.primary
|
|
335
|
-
}), /*#__PURE__*/_jsxs(Text, {
|
|
336
|
-
style: styles.lockedText,
|
|
337
|
-
children: [lockedActionCount, " older ", lockedActionCount === 1 ? "action" : "actions", " locked"]
|
|
338
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
339
|
-
style: styles.upgradeBadge,
|
|
340
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
341
|
-
style: styles.upgradeBadgeText,
|
|
342
|
-
children: "UPGRADE"
|
|
343
|
-
})
|
|
344
|
-
})]
|
|
345
|
-
}), visibleActions.length > 0 ? /*#__PURE__*/_jsx(FlatList, {
|
|
346
|
-
ref: flatListRef,
|
|
347
|
-
data: visibleActions,
|
|
348
|
-
renderItem: renderItem,
|
|
349
|
-
keyExtractor: keyExtractor,
|
|
350
|
-
contentContainerStyle: styles.listContent,
|
|
351
|
-
showsVerticalScrollIndicator: true,
|
|
352
|
-
removeClippedSubviews: true,
|
|
353
|
-
initialNumToRender: 12,
|
|
354
|
-
maxToRenderPerBatch: 8,
|
|
355
|
-
windowSize: 5,
|
|
356
|
-
updateCellsBatchingPeriod: 50
|
|
357
|
-
}) : /*#__PURE__*/_jsx(EmptyState, {
|
|
358
|
-
isEnabled: isEnabled
|
|
359
|
-
})]
|
|
360
|
-
})
|
|
361
|
-
})
|
|
362
|
-
}), /*#__PURE__*/_jsx(ProUpgradeModal, {
|
|
363
|
-
visible: showProModal,
|
|
364
|
-
onClose: () => setShowProModal(false),
|
|
365
|
-
featureName: "Full Action History"
|
|
366
|
-
})]
|
|
392
|
+
children: body
|
|
393
|
+
}), upgradeModal]
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The same Redux tool as `ReduxModal`, rendered INLINE for hosts that place
|
|
399
|
+
* tools in a panel rather than a modal. Exists so there is exactly one
|
|
400
|
+
* implementation — the desktop dashboard used to ship its own copy of this
|
|
401
|
+
* component's header, action list and detail router.
|
|
402
|
+
*/
|
|
403
|
+
export function ReduxToolView(props = {}) {
|
|
404
|
+
return /*#__PURE__*/_jsx(ReduxModal, {
|
|
405
|
+
...props,
|
|
406
|
+
inline: true,
|
|
407
|
+
visible: true,
|
|
408
|
+
onClose: () => {}
|
|
367
409
|
});
|
|
368
410
|
}
|
|
369
411
|
const styles = StyleSheet.create({
|
|
412
|
+
inlineRoot: {
|
|
413
|
+
flex: 1,
|
|
414
|
+
minHeight: 0,
|
|
415
|
+
backgroundColor: macOSColors.background.base
|
|
416
|
+
},
|
|
417
|
+
inlineHeader: {
|
|
418
|
+
paddingHorizontal: 12,
|
|
419
|
+
paddingVertical: 8,
|
|
420
|
+
borderBottomWidth: 1,
|
|
421
|
+
borderBottomColor: macOSColors.border.default,
|
|
422
|
+
backgroundColor: macOSColors.background.card
|
|
423
|
+
},
|
|
370
424
|
container: {
|
|
371
425
|
flex: 1,
|
|
372
426
|
backgroundColor: macOSColors.background.base
|
|
@@ -4,4 +4,5 @@ export { ReduxModal, FREE_TIER_ACTION_LIMIT } from "./ReduxModal";
|
|
|
4
4
|
export { ReduxActionItem } from "./ReduxActionItem";
|
|
5
5
|
export { ReduxActionDetailView } from "./ReduxActionDetailView";
|
|
6
6
|
export { ReduxActionDetailContent, ReduxActionDetailFooter } from "./ReduxActionDetailContent";
|
|
7
|
-
export { ReduxIcon } from "./ReduxIcon";
|
|
7
|
+
export { ReduxIcon } from "./ReduxIcon";
|
|
8
|
+
export { ReduxAutoInstrument } from "./ReduxAutoInstrument";
|
|
@@ -3,30 +3,22 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* React hook for auto-instrumenting Redux stores
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Mounted for the app's lifetime by FloatingDevTools (via ReduxAutoInstrument)
|
|
7
|
+
* and also called by the Redux modal. Binds Buoy to the store from react-redux
|
|
8
|
+
* context:
|
|
9
|
+
* - If the store was created through the devtools-hook enhancer, capture is
|
|
10
|
+
* already live — this just keeps the registry pointed at it.
|
|
11
|
+
* - Otherwise it applies the dispatch-patch fallback (top-level-only capture).
|
|
12
|
+
* - If react-redux is missing or there's no Provider above, it fails
|
|
13
|
+
* gracefully and records WHY (surfaced by the sync adapter / MCP).
|
|
8
14
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - If successful, it instruments the store automatically
|
|
12
|
-
* - If react-redux is not available or not in a Provider, it fails gracefully
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```tsx
|
|
16
|
-
* function ReduxDevTools() {
|
|
17
|
-
* const { isInstrumented, error } = useAutoInstrumentRedux();
|
|
18
|
-
*
|
|
19
|
-
* if (error) {
|
|
20
|
-
* return <Text>Redux not available: {error}</Text>;
|
|
21
|
-
* }
|
|
22
|
-
*
|
|
23
|
-
* return <ReduxActionList />;
|
|
24
|
-
* }
|
|
25
|
-
* ```
|
|
15
|
+
* Re-instruments when the store INSTANCE changes (hosts that rebuild their
|
|
16
|
+
* store at runtime), so the registry never pins a dead store.
|
|
26
17
|
*/
|
|
27
18
|
|
|
28
19
|
import { useEffect, useState, useRef, useCallback } from "react";
|
|
29
|
-
import { instrumentStore, uninstrumentStore,
|
|
20
|
+
import { instrumentStore, uninstrumentStore, getInstrumentationStatus } from "../utils/autoInstrument";
|
|
21
|
+
import { setReduxDiagnostic } from "../utils/activeStoreRegistry";
|
|
30
22
|
|
|
31
23
|
// ============================================
|
|
32
24
|
// Safe Module Loading
|
|
@@ -55,19 +47,16 @@ function getReactReduxUseStore() {
|
|
|
55
47
|
// Main Hook
|
|
56
48
|
// ============================================
|
|
57
49
|
|
|
58
|
-
/**
|
|
59
|
-
* Hook to automatically instrument Redux store for DevTools
|
|
60
|
-
*
|
|
61
|
-
* This provides zero-config Redux action capture by:
|
|
62
|
-
* 1. Using react-redux's useStore() to get the Redux store
|
|
63
|
-
* 2. Automatically patching store.dispatch to capture actions
|
|
64
|
-
* 3. Cleaning up when the component unmounts
|
|
65
|
-
*/
|
|
66
50
|
export function useAutoInstrumentRedux(options = {}) {
|
|
67
51
|
const {
|
|
68
|
-
autoStart = true
|
|
69
|
-
...instrumentOptions
|
|
52
|
+
autoStart = true
|
|
70
53
|
} = options;
|
|
54
|
+
|
|
55
|
+
// Options live in a ref: this hook is app-lifetime-mounted, and a fresh
|
|
56
|
+
// `{...rest}` object every render would churn startInstrumentation's
|
|
57
|
+
// identity and re-run the effect on every render.
|
|
58
|
+
const optionsRef = useRef(options);
|
|
59
|
+
optionsRef.current = options;
|
|
71
60
|
const [isInstrumented, setIsInstrumented] = useState(false);
|
|
72
61
|
const [isLoading, setIsLoading] = useState(true);
|
|
73
62
|
const [error, setError] = useState(null);
|
|
@@ -82,37 +71,35 @@ export function useAutoInstrumentRedux(options = {}) {
|
|
|
82
71
|
// This must be called unconditionally due to hooks rules
|
|
83
72
|
let reduxStore = null;
|
|
84
73
|
let storeError = null;
|
|
74
|
+
let storeDiagnostic = null;
|
|
85
75
|
if (useStoreHook) {
|
|
86
76
|
try {
|
|
87
77
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
88
78
|
reduxStore = useStoreHook();
|
|
89
79
|
} catch (e) {
|
|
90
80
|
// Not inside a Provider or other error
|
|
81
|
+
storeDiagnostic = "no-provider";
|
|
91
82
|
storeError = e instanceof Error ? e.message : "Failed to access Redux store. Make sure your app is wrapped in a Redux Provider.";
|
|
92
83
|
}
|
|
93
84
|
} else {
|
|
85
|
+
storeDiagnostic = "no-react-redux";
|
|
94
86
|
storeError = "react-redux is not installed or not available";
|
|
95
87
|
}
|
|
96
88
|
|
|
97
89
|
// Function to start instrumentation
|
|
98
90
|
const startInstrumentation = useCallback(() => {
|
|
99
91
|
if (!reduxStore) {
|
|
92
|
+
// Record the failure reason for non-React surfaces (sync adapter/MCP).
|
|
93
|
+
setReduxDiagnostic(storeDiagnostic);
|
|
100
94
|
setError(storeError || "No Redux store available");
|
|
101
95
|
setIsLoading(false);
|
|
102
96
|
return;
|
|
103
97
|
}
|
|
104
|
-
|
|
105
|
-
// Check if already instrumented
|
|
106
|
-
if (isStoreInstrumented(reduxStore)) {
|
|
107
|
-
setIsInstrumented(true);
|
|
108
|
-
setStore(reduxStore);
|
|
109
|
-
setIsLoading(false);
|
|
110
|
-
setError(null);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
98
|
try {
|
|
114
|
-
// Instrument the store
|
|
115
|
-
|
|
99
|
+
// Instrument the store. Idempotent per store: enhancer-created and
|
|
100
|
+
// already-patched stores just get (re)registered, so a store SWAP at
|
|
101
|
+
// runtime re-binds the registry to the new instance.
|
|
102
|
+
const cleanup = instrumentStore(reduxStore, optionsRef.current);
|
|
116
103
|
cleanupRef.current = cleanup;
|
|
117
104
|
setIsInstrumented(true);
|
|
118
105
|
setStore(reduxStore);
|
|
@@ -123,7 +110,8 @@ export function useAutoInstrumentRedux(options = {}) {
|
|
|
123
110
|
} finally {
|
|
124
111
|
setIsLoading(false);
|
|
125
112
|
}
|
|
126
|
-
|
|
113
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- storeError/storeDiagnostic derive from reduxStore
|
|
114
|
+
}, [reduxStore]);
|
|
127
115
|
|
|
128
116
|
// Function to stop instrumentation
|
|
129
117
|
const stopInstrumentation = useCallback(() => {
|
|
@@ -138,7 +126,8 @@ export function useAutoInstrumentRedux(options = {}) {
|
|
|
138
126
|
setStore(null);
|
|
139
127
|
}, [store]);
|
|
140
128
|
|
|
141
|
-
// Auto-start instrumentation on mount
|
|
129
|
+
// Auto-start instrumentation on mount, and again whenever the store
|
|
130
|
+
// INSTANCE changes (hosts that rebuild their store re-point the registry).
|
|
142
131
|
useEffect(() => {
|
|
143
132
|
if (autoStart) {
|
|
144
133
|
startInstrumentation();
|
|
@@ -159,6 +148,7 @@ export function useAutoInstrumentRedux(options = {}) {
|
|
|
159
148
|
isLoading,
|
|
160
149
|
error,
|
|
161
150
|
isReactReduxAvailable,
|
|
151
|
+
captureMechanism: getInstrumentationStatus().mechanism,
|
|
162
152
|
startInstrumentation,
|
|
163
153
|
stopInstrumentation,
|
|
164
154
|
store
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { reduxActionStore } from "../utils/reduxActionStore";
|
|
4
|
-
import { getActiveStore } from "../utils/autoInstrument";
|
|
4
|
+
import { getActiveStore, getInstrumentationStatus } from "../utils/autoInstrument";
|
|
5
5
|
/** Top-level slice names of the state tree, for the compact (no-values) view. */
|
|
6
6
|
function topLevelKeys(state) {
|
|
7
7
|
return state && typeof state === "object" && !Array.isArray(state) ? Object.keys(state) : [];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Why there's no bound store, in a shape remote drivers (MCP/desktop) can turn
|
|
12
|
+
* into a SPECIFIC remedy instead of a generic "check your setup".
|
|
13
|
+
*/
|
|
14
|
+
function unavailable() {
|
|
15
|
+
const status = getInstrumentationStatus();
|
|
16
|
+
const reason = status.diagnostic ?? "not-instrumented";
|
|
17
|
+
return {
|
|
18
|
+
available: false,
|
|
19
|
+
slices: [],
|
|
20
|
+
/** 'no-react-redux' | 'no-provider' | 'not-instrumented' */
|
|
21
|
+
reason
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
/**
|
|
11
26
|
* Sync adapter for the redux tool, consumed by @buoy-gg/external-sync's
|
|
12
27
|
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
13
28
|
* this package doesn't need a dependency on it).
|
|
14
29
|
*
|
|
15
|
-
* Actions are captured by the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
30
|
+
* Actions are captured from app startup — by the store-creation enhancer or
|
|
31
|
+
* the explicit middleware (full capture), or by the dispatch-patch fallback
|
|
32
|
+
* mounted with FloatingDevTools (top-level-only) — independent of whether a
|
|
33
|
+
* dashboard is watching. Subscribing here only streams what's already
|
|
34
|
+
* recorded.
|
|
18
35
|
*
|
|
19
36
|
* `getState`/`dispatch` read & drive the live instrumented store (via
|
|
20
37
|
* getActiveStore()) so a remote driver (MCP/LLM) can inspect current state and
|
|
@@ -39,15 +56,16 @@ export const reduxSyncAdapter = {
|
|
|
39
56
|
} = params ?? {};
|
|
40
57
|
const store = getActiveStore();
|
|
41
58
|
if (!store) {
|
|
42
|
-
return
|
|
43
|
-
available: false,
|
|
44
|
-
slices: []
|
|
45
|
-
};
|
|
59
|
+
return unavailable();
|
|
46
60
|
}
|
|
47
61
|
const state = store.getState();
|
|
62
|
+
const status = getInstrumentationStatus();
|
|
48
63
|
return {
|
|
49
64
|
available: true,
|
|
50
65
|
slices: topLevelKeys(state),
|
|
66
|
+
/** 'full' | 'top-level-only' — what the live capture mechanism sees. */
|
|
67
|
+
capture: status.capture,
|
|
68
|
+
mechanism: status.mechanism,
|
|
51
69
|
...(includeValues ? {
|
|
52
70
|
state
|
|
53
71
|
} : {})
|
|
@@ -61,7 +79,8 @@ export const reduxSyncAdapter = {
|
|
|
61
79
|
const store = getActiveStore();
|
|
62
80
|
if (!store) {
|
|
63
81
|
return {
|
|
64
|
-
dispatched: false
|
|
82
|
+
dispatched: false,
|
|
83
|
+
...unavailable()
|
|
65
84
|
};
|
|
66
85
|
}
|
|
67
86
|
store.dispatch(action);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared registry for the one live Redux store Buoy is bound to, plus how it
|
|
5
|
+
* got bound (the capture mechanism) and why binding failed when it did.
|
|
6
|
+
*
|
|
7
|
+
* Lives in its own module so the three capture paths (devtools-hook enhancer,
|
|
8
|
+
* explicit middleware, dispatch-patch fallback) and the sync adapter can all
|
|
9
|
+
* import it without cycles.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The minimum store surface Buoy needs (middleware storeApi satisfies this). */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How the store got instrumented:
|
|
16
|
+
* - `enhancer` — store-creation-time enhancer via the Redux DevTools global.
|
|
17
|
+
* FULL capture (thunk-internal & RTK Query actions included).
|
|
18
|
+
* - `middleware` — user-added buoyReduxMiddleware. FULL capture.
|
|
19
|
+
* - `patch` — store.dispatch patched after creation (auto-instrumentation
|
|
20
|
+
* fallback). TOP-LEVEL-ONLY capture: actions dispatched from
|
|
21
|
+
* inside middleware/thunks bypass the patch.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Why there is no bound store, when there isn't one. */
|
|
25
|
+
|
|
26
|
+
/** Marks stores created through the Buoy devtools-hook enhancer. */
|
|
27
|
+
export const ENHANCER_SYMBOL = Symbol.for("@@buoy/enhancerInstrumented");
|
|
28
|
+
let activeStore = null;
|
|
29
|
+
let mechanism = null;
|
|
30
|
+
let diagnostic = null;
|
|
31
|
+
export function registerActiveStore(store, mech) {
|
|
32
|
+
activeStore = store;
|
|
33
|
+
mechanism = mech;
|
|
34
|
+
diagnostic = null;
|
|
35
|
+
}
|
|
36
|
+
export function unregisterActiveStore(store) {
|
|
37
|
+
if (activeStore === store) {
|
|
38
|
+
activeStore = null;
|
|
39
|
+
mechanism = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function getActiveStore() {
|
|
43
|
+
return activeStore;
|
|
44
|
+
}
|
|
45
|
+
export function getCaptureMechanism() {
|
|
46
|
+
return mechanism;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Recorded by the auto-instrument hook so non-React surfaces (sync adapter,
|
|
51
|
+
* MCP) can report WHY there's no store instead of a generic failure.
|
|
52
|
+
*/
|
|
53
|
+
export function setReduxDiagnostic(d) {
|
|
54
|
+
diagnostic = d;
|
|
55
|
+
}
|
|
56
|
+
export function getInstrumentationStatus() {
|
|
57
|
+
return {
|
|
58
|
+
mechanism,
|
|
59
|
+
capture: mechanism === "enhancer" || mechanism === "middleware" ? "full" : mechanism === "patch" ? "top-level-only" : "none",
|
|
60
|
+
hasStore: activeStore !== null,
|
|
61
|
+
diagnostic
|
|
62
|
+
};
|
|
63
|
+
}
|