@buoy-gg/redux 7.0.5 → 7.0.7
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 +38 -0
- package/lib/commonjs/redux/components/ReduxAutoInstrument.js +20 -0
- package/lib/commonjs/redux/components/ReduxModal.js +11 -1
- 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 +26 -3
- package/lib/module/redux/components/ReduxAutoInstrument.js +17 -0
- package/lib/module/redux/components/ReduxModal.js +11 -1
- 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 +18 -4
- 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.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
|
@@ -52,7 +52,8 @@ export function ReduxModal({
|
|
|
52
52
|
isInstrumented,
|
|
53
53
|
isLoading: isInstrumentLoading,
|
|
54
54
|
error: instrumentError,
|
|
55
|
-
isReactReduxAvailable
|
|
55
|
+
isReactReduxAvailable,
|
|
56
|
+
captureMechanism
|
|
56
57
|
} = useAutoInstrumentRedux();
|
|
57
58
|
const {
|
|
58
59
|
filteredActions,
|
|
@@ -308,6 +309,15 @@ export function ReduxModal({
|
|
|
308
309
|
style: styles.disabledText,
|
|
309
310
|
children: "Action capture is disabled"
|
|
310
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
|
+
})]
|
|
311
321
|
}), hasLockedActions && /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
312
322
|
style: styles.lockedBanner,
|
|
313
323
|
onPress: handleUpgradePress,
|
|
@@ -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
|
+
}
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Fallback auto-instrumentation for Redux stores.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* The PRIMARY capture path is the store-creation-time enhancer in
|
|
7
|
+
* devtoolsHook.ts (full capture, real time travel). This module is the
|
|
8
|
+
* fallback for stores created before our module loaded: it patches
|
|
9
|
+
* store.dispatch after the fact.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* middleware
|
|
11
|
+
* Fallback limitation (inherent to patching, verified against redux 5.x):
|
|
12
|
+
* `middlewareAPI.dispatch` is a separate closure from `store.dispatch`, so
|
|
13
|
+
* actions dispatched from INSIDE middleware/thunks (RTK pending/fulfilled/
|
|
14
|
+
* rejected, RTK Query) bypass the patch. That's why this mechanism reports
|
|
15
|
+
* capture as "top-level-only" — see activeStoreRegistry.getInstrumentationStatus.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import { reduxActionStore } from "./reduxActionStore";
|
|
15
19
|
import { BUOY_JUMP_TO_STATE, isMiddlewareActive } from "./buoyReduxMiddleware";
|
|
20
|
+
import { registerActiveStore, unregisterActiveStore, getActiveStore as getRegisteredStore, ENHANCER_SYMBOL } from "./activeStoreRegistry";
|
|
21
|
+
|
|
22
|
+
// Re-exported for non-React callers (sync adapter, app code).
|
|
23
|
+
export { getInstrumentationStatus } from "./activeStoreRegistry";
|
|
16
24
|
|
|
17
25
|
// ============================================
|
|
18
26
|
// Store Tracking
|
|
@@ -24,15 +32,6 @@ const INSTRUMENTED_SYMBOL = Symbol.for("@@buoy/instrumented");
|
|
|
24
32
|
/** Symbol to store original dispatch reference */
|
|
25
33
|
const ORIGINAL_DISPATCH_SYMBOL = Symbol.for("@@buoy/originalDispatch");
|
|
26
34
|
|
|
27
|
-
/** Symbol to store original reducer reference */
|
|
28
|
-
const ORIGINAL_REDUCER_SYMBOL = Symbol.for("@@buoy/originalReducer");
|
|
29
|
-
|
|
30
|
-
/** Reference to the active store for time-travel operations */
|
|
31
|
-
let activeStore = null;
|
|
32
|
-
|
|
33
|
-
/** Track if store has been enhanced for time-travel */
|
|
34
|
-
let timeTravelEnabled = false;
|
|
35
|
-
|
|
36
35
|
// ============================================
|
|
37
36
|
// Type Definitions
|
|
38
37
|
// ============================================
|
|
@@ -42,40 +41,44 @@ let timeTravelEnabled = false;
|
|
|
42
41
|
// ============================================
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
|
-
* Check if a store has already been instrumented
|
|
44
|
+
* Check if a store has already been instrumented (either mechanism)
|
|
46
45
|
*/
|
|
47
46
|
export function isStoreInstrumented(store) {
|
|
48
|
-
|
|
47
|
+
const s = store;
|
|
48
|
+
return s[INSTRUMENTED_SYMBOL] === true || s[ENHANCER_SYMBOL] === true;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Instrument a Redux store for action capture
|
|
52
|
+
* Instrument a Redux store for action capture (fallback path).
|
|
53
53
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* Patches store.dispatch to capture top-level dispatches. No-ops (but still
|
|
55
|
+
* registers the store for getState/dispatch) when the enhancer or the
|
|
56
|
+
* explicit middleware is already capturing.
|
|
56
57
|
*
|
|
57
|
-
* @param store - The Redux store to instrument
|
|
58
|
-
* @param options - Configuration options
|
|
59
58
|
* @returns Cleanup function to restore original dispatch
|
|
60
59
|
*/
|
|
61
60
|
export function instrumentStore(store, options = {}) {
|
|
62
61
|
const instrumentedStore = store;
|
|
63
62
|
|
|
63
|
+
// Store was created through the devtools-hook enhancer — full capture is
|
|
64
|
+
// already live; just (re)point the registry at it.
|
|
65
|
+
if (instrumentedStore[ENHANCER_SYMBOL]) {
|
|
66
|
+
registerActiveStore(store, "enhancer");
|
|
67
|
+
return () => {};
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
// Skip if middleware is already handling action capture
|
|
65
|
-
// This prevents duplicate recording when both mechanisms are active
|
|
66
71
|
if (isMiddlewareActive()) {
|
|
67
|
-
|
|
72
|
+
registerActiveStore(store, "middleware");
|
|
68
73
|
return () => {};
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
// Skip if already
|
|
76
|
+
// Skip if already patched — update the registry even so (store swap case)
|
|
72
77
|
if (instrumentedStore[INSTRUMENTED_SYMBOL]) {
|
|
73
|
-
|
|
74
|
-
activeStore = store;
|
|
78
|
+
registerActiveStore(store, "patch");
|
|
75
79
|
return () => {};
|
|
76
80
|
}
|
|
77
81
|
const {
|
|
78
|
-
enableTimeTravel = false,
|
|
79
82
|
maxActions = 200,
|
|
80
83
|
ignoreActions = []
|
|
81
84
|
} = options;
|
|
@@ -126,12 +129,7 @@ export function instrumentStore(store, options = {}) {
|
|
|
126
129
|
// Replace dispatch
|
|
127
130
|
store.dispatch = instrumentedDispatch;
|
|
128
131
|
instrumentedStore[INSTRUMENTED_SYMBOL] = true;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Enable time-travel if requested and store supports replaceReducer
|
|
132
|
-
if (enableTimeTravel && typeof store.replaceReducer === "function") {
|
|
133
|
-
enableTimeTravelSupport(store);
|
|
134
|
-
}
|
|
132
|
+
registerActiveStore(store, "patch");
|
|
135
133
|
|
|
136
134
|
// Return cleanup function
|
|
137
135
|
return () => {
|
|
@@ -140,90 +138,63 @@ export function instrumentStore(store, options = {}) {
|
|
|
140
138
|
}
|
|
141
139
|
|
|
142
140
|
/**
|
|
143
|
-
* Remove instrumentation from a store
|
|
141
|
+
* Remove dispatch-patch instrumentation from a store.
|
|
142
|
+
*
|
|
143
|
+
* Only restores the patched dispatch. (This used to also replaceReducer with a
|
|
144
|
+
* synthetic reducer that froze the host store permanently — that path is gone.)
|
|
145
|
+
* Enhancer-created stores are left untouched apart from the registry.
|
|
144
146
|
*/
|
|
145
147
|
export function uninstrumentStore(store) {
|
|
146
148
|
const instrumentedStore = store;
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
store.dispatch = originalDispatch;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Restore original reducer if time-travel was enabled
|
|
158
|
-
const originalReducer = instrumentedStore[ORIGINAL_REDUCER_SYMBOL];
|
|
159
|
-
if (originalReducer && typeof store.replaceReducer === "function") {
|
|
160
|
-
store.replaceReducer(originalReducer);
|
|
161
|
-
timeTravelEnabled = false;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Clean up symbols
|
|
165
|
-
delete instrumentedStore[INSTRUMENTED_SYMBOL];
|
|
166
|
-
delete instrumentedStore[ORIGINAL_DISPATCH_SYMBOL];
|
|
167
|
-
delete instrumentedStore[ORIGINAL_REDUCER_SYMBOL];
|
|
168
|
-
|
|
169
|
-
// Clear active store reference
|
|
170
|
-
if (activeStore === store) {
|
|
171
|
-
activeStore = null;
|
|
149
|
+
if (instrumentedStore[INSTRUMENTED_SYMBOL]) {
|
|
150
|
+
const originalDispatch = instrumentedStore[ORIGINAL_DISPATCH_SYMBOL];
|
|
151
|
+
if (originalDispatch) {
|
|
152
|
+
store.dispatch = originalDispatch;
|
|
153
|
+
}
|
|
154
|
+
delete instrumentedStore[INSTRUMENTED_SYMBOL];
|
|
155
|
+
delete instrumentedStore[ORIGINAL_DISPATCH_SYMBOL];
|
|
172
156
|
}
|
|
157
|
+
unregisterActiveStore(store);
|
|
173
158
|
}
|
|
174
159
|
|
|
175
|
-
// ============================================
|
|
176
|
-
// Time-Travel Support
|
|
177
|
-
// ============================================
|
|
178
|
-
|
|
179
160
|
/**
|
|
180
|
-
*
|
|
161
|
+
* Manually register a store with Buoy — the escape hatch for apps that can't
|
|
162
|
+
* render FloatingDevTools inside a react-redux Provider (or don't use
|
|
163
|
+
* react-redux at all). Instruments via the fallback dispatch patch.
|
|
181
164
|
*
|
|
182
|
-
*
|
|
183
|
-
* dynamically, without requiring users to manually wrap their reducer.
|
|
165
|
+
* Prefer the zero-config paths; this exists so no app is ever stuck.
|
|
184
166
|
*/
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// Get current reducer by dispatching a probe action
|
|
189
|
-
// Note: This is a workaround since there's no store.getReducer()
|
|
190
|
-
// We'll wrap whatever reducer is currently active
|
|
191
|
-
const currentState = store.getState();
|
|
192
|
-
|
|
193
|
-
// Create a time-travel enabled reducer wrapper
|
|
194
|
-
const timeTravelReducer = (state = currentState, action) => {
|
|
195
|
-
// Handle jump to state action
|
|
196
|
-
if (action.type === BUOY_JUMP_TO_STATE) {
|
|
197
|
-
return action.payload;
|
|
198
|
-
}
|
|
167
|
+
export function registerReduxStore(store, options = {}) {
|
|
168
|
+
return instrumentStore(store, options);
|
|
169
|
+
}
|
|
199
170
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return state;
|
|
204
|
-
};
|
|
171
|
+
// ============================================
|
|
172
|
+
// Time-Travel / Introspection
|
|
173
|
+
// ============================================
|
|
205
174
|
|
|
206
|
-
|
|
207
|
-
// This approach has limitations - full time-travel works better with
|
|
208
|
-
// the explicit withBuoyDevTools wrapper
|
|
209
|
-
instrumentedStore[ORIGINAL_REDUCER_SYMBOL] = timeTravelReducer;
|
|
210
|
-
timeTravelEnabled = true;
|
|
211
|
-
}
|
|
175
|
+
let warnedJumpUnsupported = false;
|
|
212
176
|
|
|
213
177
|
/**
|
|
214
|
-
* Jump to a specific state (time-travel)
|
|
178
|
+
* Jump to a specific state (time-travel).
|
|
215
179
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
180
|
+
* Real on the enhancer path (reducer is wrapped from creation) and on the
|
|
181
|
+
* middleware path when the app used `withBuoyDevTools`. On the bare patch
|
|
182
|
+
* path there's no reducer wrapper, so the jump action is ignored — we detect
|
|
183
|
+
* that and warn once instead of pretending it worked.
|
|
218
184
|
*/
|
|
219
185
|
export function jumpToState(state) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
type: BUOY_JUMP_TO_STATE,
|
|
223
|
-
payload: state
|
|
224
|
-
});
|
|
225
|
-
} else {
|
|
186
|
+
const store = getRegisteredStore();
|
|
187
|
+
if (!store) {
|
|
226
188
|
console.warn("[BuoyRedux] Cannot jump to state - no store instrumented");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
store.dispatch({
|
|
192
|
+
type: BUOY_JUMP_TO_STATE,
|
|
193
|
+
payload: state
|
|
194
|
+
});
|
|
195
|
+
if (typeof __DEV__ !== "undefined" && __DEV__ && !warnedJumpUnsupported && store.getState() !== state) {
|
|
196
|
+
warnedJumpUnsupported = true;
|
|
197
|
+
console.warn("[BuoyRedux] Time-travel jump was ignored by the store. Wrap your root " + "reducer with withBuoyDevTools(), or ensure @buoy-gg/redux is imported " + "before your store is created (enhancer path) for real time travel.");
|
|
227
198
|
}
|
|
228
199
|
}
|
|
229
200
|
|
|
@@ -231,30 +202,28 @@ export function jumpToState(state) {
|
|
|
231
202
|
* Replay an action (dispatch it again)
|
|
232
203
|
*/
|
|
233
204
|
export function replayAction(action) {
|
|
234
|
-
|
|
235
|
-
|
|
205
|
+
const store = getRegisteredStore();
|
|
206
|
+
if (store) {
|
|
207
|
+
store.dispatch(action);
|
|
236
208
|
} else {
|
|
237
209
|
console.warn("[BuoyRedux] Cannot replay action - no store instrumented");
|
|
238
210
|
}
|
|
239
211
|
}
|
|
240
212
|
|
|
241
213
|
/**
|
|
242
|
-
* Get the currently active (instrumented) store
|
|
214
|
+
* Get the currently active (instrumented) store.
|
|
215
|
+
*
|
|
216
|
+
* NOTE: on the middleware path this is the middleware `storeApi`
|
|
217
|
+
* ({getState, dispatch}) rather than the full Store object — every consumer
|
|
218
|
+
* in this package only needs those two.
|
|
243
219
|
*/
|
|
244
220
|
export function getActiveStore() {
|
|
245
|
-
return
|
|
221
|
+
return getRegisteredStore();
|
|
246
222
|
}
|
|
247
223
|
|
|
248
224
|
/**
|
|
249
225
|
* Check if auto-instrumentation is active
|
|
250
226
|
*/
|
|
251
227
|
export function isAutoInstrumentActive() {
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Check if time-travel support is enabled
|
|
257
|
-
*/
|
|
258
|
-
export function isTimeTravelEnabled() {
|
|
259
|
-
return timeTravelEnabled;
|
|
228
|
+
return getRegisteredStore() !== null;
|
|
260
229
|
}
|