@buoy-gg/redux 7.0.5 → 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.
Files changed (45) hide show
  1. package/README.md +21 -2
  2. package/lib/commonjs/index.js +38 -0
  3. package/lib/commonjs/redux/components/ReduxAutoInstrument.js +20 -0
  4. package/lib/commonjs/redux/components/ReduxModal.js +11 -1
  5. package/lib/commonjs/redux/components/index.js +8 -1
  6. package/lib/commonjs/redux/hooks/useAutoInstrumentRedux.js +32 -42
  7. package/lib/commonjs/redux/sync/reduxSyncAdapter.js +27 -8
  8. package/lib/commonjs/redux/utils/activeStoreRegistry.js +73 -0
  9. package/lib/commonjs/redux/utils/autoInstrument.js +88 -114
  10. package/lib/commonjs/redux/utils/buoyReduxMiddleware.js +20 -15
  11. package/lib/commonjs/redux/utils/devtoolsHook.js +137 -0
  12. package/lib/commonjs/redux/utils/index.js +32 -1
  13. package/lib/module/index.js +26 -3
  14. package/lib/module/redux/components/ReduxAutoInstrument.js +17 -0
  15. package/lib/module/redux/components/ReduxModal.js +11 -1
  16. package/lib/module/redux/components/index.js +2 -1
  17. package/lib/module/redux/hooks/useAutoInstrumentRedux.js +33 -43
  18. package/lib/module/redux/sync/reduxSyncAdapter.js +28 -9
  19. package/lib/module/redux/utils/activeStoreRegistry.js +63 -0
  20. package/lib/module/redux/utils/autoInstrument.js +82 -113
  21. package/lib/module/redux/utils/buoyReduxMiddleware.js +20 -15
  22. package/lib/module/redux/utils/devtoolsHook.js +130 -0
  23. package/lib/module/redux/utils/index.js +3 -1
  24. package/lib/typescript/index.d.ts +18 -4
  25. package/lib/typescript/index.d.ts.map +1 -1
  26. package/lib/typescript/redux/components/ReduxAutoInstrument.d.ts +11 -0
  27. package/lib/typescript/redux/components/ReduxAutoInstrument.d.ts.map +1 -0
  28. package/lib/typescript/redux/components/ReduxModal.d.ts.map +1 -1
  29. package/lib/typescript/redux/components/index.d.ts +1 -0
  30. package/lib/typescript/redux/components/index.d.ts.map +1 -1
  31. package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts +16 -28
  32. package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts.map +1 -1
  33. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts +18 -4
  34. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts.map +1 -1
  35. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts +47 -0
  36. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts.map +1 -0
  37. package/lib/typescript/redux/utils/autoInstrument.d.ts +46 -23
  38. package/lib/typescript/redux/utils/autoInstrument.d.ts.map +1 -1
  39. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts +5 -1
  40. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts.map +1 -1
  41. package/lib/typescript/redux/utils/devtoolsHook.d.ts +45 -0
  42. package/lib/typescript/redux/utils/devtoolsHook.d.ts.map +1 -0
  43. package/lib/typescript/redux/utils/index.d.ts +3 -2
  44. package/lib/typescript/redux/utils/index.d.ts.map +1 -1
  45. 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
- * This hook automatically patches the Redux store when the component mounts,
7
- * providing zero-config action capture for the Redux DevTools.
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
- * Usage:
10
- * - The hook attempts to use react-redux's useStore() to get the store
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, isStoreInstrumented } from "../utils/autoInstrument";
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
- const cleanup = instrumentStore(reduxStore, instrumentOptions);
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
- }, [reduxStore, storeError, instrumentOptions]);
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 auto-instrumentation middleware (installed when
16
- * FloatingDevTools mounts), independent of whether a dashboard is watching —
17
- * subscribing here only streams what the store already records.
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
- * Auto-instrumentation for Redux stores
4
+ * Fallback auto-instrumentation for Redux stores.
5
5
  *
6
- * This module provides zero-config Redux DevTools integration by automatically
7
- * patching the store's dispatch method when the DevTools component mounts.
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
- * Similar to how @buoy-gg/network patches globalThis.fetch, this patches
10
- * store.dispatch to capture all Redux actions without requiring manual
11
- * middleware configuration.
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
- return store[INSTRUMENTED_SYMBOL] === true;
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
- * This function patches store.dispatch to capture all actions without
55
- * requiring the user to configure middleware manually.
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
- activeStore = store;
72
+ registerActiveStore(store, "middleware");
68
73
  return () => {};
69
74
  }
70
75
 
71
- // Skip if already instrumented
76
+ // Skip if already patched — update the registry even so (store swap case)
72
77
  if (instrumentedStore[INSTRUMENTED_SYMBOL]) {
73
- // Update active store reference even if already instrumented
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
- activeStore = store;
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 (!instrumentedStore[INSTRUMENTED_SYMBOL]) {
148
- return;
149
- }
150
-
151
- // Restore original dispatch
152
- const originalDispatch = instrumentedStore[ORIGINAL_DISPATCH_SYMBOL];
153
- if (originalDispatch) {
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
- * Enable time-travel support by wrapping the store's reducer
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
- * This uses store.replaceReducer to inject our time-travel wrapper
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 enableTimeTravelSupport(store) {
186
- const instrumentedStore = store;
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
- // Get the result from the original dispatch flow
201
- // Since we've already wrapped dispatch, we need to be careful here
202
- // The state will be updated by the store's internal reducer
203
- return state;
204
- };
171
+ // ============================================
172
+ // Time-Travel / Introspection
173
+ // ============================================
205
174
 
206
- // Store reference (we can't actually get the original reducer directly)
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
- * For full time-travel support, users should use withBuoyDevTools.
217
- * This limited version attempts to work with auto-instrumentation.
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
- if (activeStore) {
221
- activeStore.dispatch({
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
- if (activeStore) {
235
- activeStore.dispatch(action);
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 activeStore;
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 activeStore !== null && isStoreInstrumented(activeStore);
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
  }