@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.
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
@@ -4,26 +4,39 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.getActiveStore = getActiveStore;
7
+ Object.defineProperty(exports, "getInstrumentationStatus", {
8
+ enumerable: true,
9
+ get: function () {
10
+ return _activeStoreRegistry.getInstrumentationStatus;
11
+ }
12
+ });
7
13
  exports.instrumentStore = instrumentStore;
8
14
  exports.isAutoInstrumentActive = isAutoInstrumentActive;
9
15
  exports.isStoreInstrumented = isStoreInstrumented;
10
- exports.isTimeTravelEnabled = isTimeTravelEnabled;
11
16
  exports.jumpToState = jumpToState;
17
+ exports.registerReduxStore = registerReduxStore;
12
18
  exports.replayAction = replayAction;
13
19
  exports.uninstrumentStore = uninstrumentStore;
14
20
  var _reduxActionStore = require("./reduxActionStore");
15
21
  var _buoyReduxMiddleware = require("./buoyReduxMiddleware");
22
+ var _activeStoreRegistry = require("./activeStoreRegistry");
16
23
  /**
17
- * Auto-instrumentation for Redux stores
24
+ * Fallback auto-instrumentation for Redux stores.
18
25
  *
19
- * This module provides zero-config Redux DevTools integration by automatically
20
- * patching the store's dispatch method when the DevTools component mounts.
26
+ * The PRIMARY capture path is the store-creation-time enhancer in
27
+ * devtoolsHook.ts (full capture, real time travel). This module is the
28
+ * fallback for stores created before our module loaded: it patches
29
+ * store.dispatch after the fact.
21
30
  *
22
- * Similar to how @buoy-gg/network patches globalThis.fetch, this patches
23
- * store.dispatch to capture all Redux actions without requiring manual
24
- * middleware configuration.
31
+ * Fallback limitation (inherent to patching, verified against redux 5.x):
32
+ * `middlewareAPI.dispatch` is a separate closure from `store.dispatch`, so
33
+ * actions dispatched from INSIDE middleware/thunks (RTK pending/fulfilled/
34
+ * rejected, RTK Query) bypass the patch. That's why this mechanism reports
35
+ * capture as "top-level-only" — see activeStoreRegistry.getInstrumentationStatus.
25
36
  */
26
37
 
38
+ // Re-exported for non-React callers (sync adapter, app code).
39
+
27
40
  // ============================================
28
41
  // Store Tracking
29
42
  // ============================================
@@ -34,15 +47,6 @@ const INSTRUMENTED_SYMBOL = Symbol.for("@@buoy/instrumented");
34
47
  /** Symbol to store original dispatch reference */
35
48
  const ORIGINAL_DISPATCH_SYMBOL = Symbol.for("@@buoy/originalDispatch");
36
49
 
37
- /** Symbol to store original reducer reference */
38
- const ORIGINAL_REDUCER_SYMBOL = Symbol.for("@@buoy/originalReducer");
39
-
40
- /** Reference to the active store for time-travel operations */
41
- let activeStore = null;
42
-
43
- /** Track if store has been enhanced for time-travel */
44
- let timeTravelEnabled = false;
45
-
46
50
  // ============================================
47
51
  // Type Definitions
48
52
  // ============================================
@@ -52,40 +56,44 @@ let timeTravelEnabled = false;
52
56
  // ============================================
53
57
 
54
58
  /**
55
- * Check if a store has already been instrumented
59
+ * Check if a store has already been instrumented (either mechanism)
56
60
  */
57
61
  function isStoreInstrumented(store) {
58
- return store[INSTRUMENTED_SYMBOL] === true;
62
+ const s = store;
63
+ return s[INSTRUMENTED_SYMBOL] === true || s[_activeStoreRegistry.ENHANCER_SYMBOL] === true;
59
64
  }
60
65
 
61
66
  /**
62
- * Instrument a Redux store for action capture
67
+ * Instrument a Redux store for action capture (fallback path).
63
68
  *
64
- * This function patches store.dispatch to capture all actions without
65
- * requiring the user to configure middleware manually.
69
+ * Patches store.dispatch to capture top-level dispatches. No-ops (but still
70
+ * registers the store for getState/dispatch) when the enhancer or the
71
+ * explicit middleware is already capturing.
66
72
  *
67
- * @param store - The Redux store to instrument
68
- * @param options - Configuration options
69
73
  * @returns Cleanup function to restore original dispatch
70
74
  */
71
75
  function instrumentStore(store, options = {}) {
72
76
  const instrumentedStore = store;
73
77
 
78
+ // Store was created through the devtools-hook enhancer — full capture is
79
+ // already live; just (re)point the registry at it.
80
+ if (instrumentedStore[_activeStoreRegistry.ENHANCER_SYMBOL]) {
81
+ (0, _activeStoreRegistry.registerActiveStore)(store, "enhancer");
82
+ return () => {};
83
+ }
84
+
74
85
  // Skip if middleware is already handling action capture
75
- // This prevents duplicate recording when both mechanisms are active
76
86
  if ((0, _buoyReduxMiddleware.isMiddlewareActive)()) {
77
- activeStore = store;
87
+ (0, _activeStoreRegistry.registerActiveStore)(store, "middleware");
78
88
  return () => {};
79
89
  }
80
90
 
81
- // Skip if already instrumented
91
+ // Skip if already patched — update the registry even so (store swap case)
82
92
  if (instrumentedStore[INSTRUMENTED_SYMBOL]) {
83
- // Update active store reference even if already instrumented
84
- activeStore = store;
93
+ (0, _activeStoreRegistry.registerActiveStore)(store, "patch");
85
94
  return () => {};
86
95
  }
87
96
  const {
88
- enableTimeTravel = false,
89
97
  maxActions = 200,
90
98
  ignoreActions = []
91
99
  } = options;
@@ -136,12 +144,7 @@ function instrumentStore(store, options = {}) {
136
144
  // Replace dispatch
137
145
  store.dispatch = instrumentedDispatch;
138
146
  instrumentedStore[INSTRUMENTED_SYMBOL] = true;
139
- activeStore = store;
140
-
141
- // Enable time-travel if requested and store supports replaceReducer
142
- if (enableTimeTravel && typeof store.replaceReducer === "function") {
143
- enableTimeTravelSupport(store);
144
- }
147
+ (0, _activeStoreRegistry.registerActiveStore)(store, "patch");
145
148
 
146
149
  // Return cleanup function
147
150
  return () => {
@@ -150,90 +153,63 @@ function instrumentStore(store, options = {}) {
150
153
  }
151
154
 
152
155
  /**
153
- * Remove instrumentation from a store
156
+ * Remove dispatch-patch instrumentation from a store.
157
+ *
158
+ * Only restores the patched dispatch. (This used to also replaceReducer with a
159
+ * synthetic reducer that froze the host store permanently — that path is gone.)
160
+ * Enhancer-created stores are left untouched apart from the registry.
154
161
  */
155
162
  function uninstrumentStore(store) {
156
163
  const instrumentedStore = store;
157
- if (!instrumentedStore[INSTRUMENTED_SYMBOL]) {
158
- return;
159
- }
160
-
161
- // Restore original dispatch
162
- const originalDispatch = instrumentedStore[ORIGINAL_DISPATCH_SYMBOL];
163
- if (originalDispatch) {
164
- store.dispatch = originalDispatch;
165
- }
166
-
167
- // Restore original reducer if time-travel was enabled
168
- const originalReducer = instrumentedStore[ORIGINAL_REDUCER_SYMBOL];
169
- if (originalReducer && typeof store.replaceReducer === "function") {
170
- store.replaceReducer(originalReducer);
171
- timeTravelEnabled = false;
172
- }
173
-
174
- // Clean up symbols
175
- delete instrumentedStore[INSTRUMENTED_SYMBOL];
176
- delete instrumentedStore[ORIGINAL_DISPATCH_SYMBOL];
177
- delete instrumentedStore[ORIGINAL_REDUCER_SYMBOL];
178
-
179
- // Clear active store reference
180
- if (activeStore === store) {
181
- activeStore = null;
164
+ if (instrumentedStore[INSTRUMENTED_SYMBOL]) {
165
+ const originalDispatch = instrumentedStore[ORIGINAL_DISPATCH_SYMBOL];
166
+ if (originalDispatch) {
167
+ store.dispatch = originalDispatch;
168
+ }
169
+ delete instrumentedStore[INSTRUMENTED_SYMBOL];
170
+ delete instrumentedStore[ORIGINAL_DISPATCH_SYMBOL];
182
171
  }
172
+ (0, _activeStoreRegistry.unregisterActiveStore)(store);
183
173
  }
184
174
 
185
- // ============================================
186
- // Time-Travel Support
187
- // ============================================
188
-
189
175
  /**
190
- * Enable time-travel support by wrapping the store's reducer
176
+ * Manually register a store with Buoy — the escape hatch for apps that can't
177
+ * render FloatingDevTools inside a react-redux Provider (or don't use
178
+ * react-redux at all). Instruments via the fallback dispatch patch.
191
179
  *
192
- * This uses store.replaceReducer to inject our time-travel wrapper
193
- * dynamically, without requiring users to manually wrap their reducer.
180
+ * Prefer the zero-config paths; this exists so no app is ever stuck.
194
181
  */
195
- function enableTimeTravelSupport(store) {
196
- const instrumentedStore = store;
197
-
198
- // Get current reducer by dispatching a probe action
199
- // Note: This is a workaround since there's no store.getReducer()
200
- // We'll wrap whatever reducer is currently active
201
- const currentState = store.getState();
202
-
203
- // Create a time-travel enabled reducer wrapper
204
- const timeTravelReducer = (state = currentState, action) => {
205
- // Handle jump to state action
206
- if (action.type === _buoyReduxMiddleware.BUOY_JUMP_TO_STATE) {
207
- return action.payload;
208
- }
182
+ function registerReduxStore(store, options = {}) {
183
+ return instrumentStore(store, options);
184
+ }
209
185
 
210
- // Get the result from the original dispatch flow
211
- // Since we've already wrapped dispatch, we need to be careful here
212
- // The state will be updated by the store's internal reducer
213
- return state;
214
- };
186
+ // ============================================
187
+ // Time-Travel / Introspection
188
+ // ============================================
215
189
 
216
- // Store reference (we can't actually get the original reducer directly)
217
- // This approach has limitations - full time-travel works better with
218
- // the explicit withBuoyDevTools wrapper
219
- instrumentedStore[ORIGINAL_REDUCER_SYMBOL] = timeTravelReducer;
220
- timeTravelEnabled = true;
221
- }
190
+ let warnedJumpUnsupported = false;
222
191
 
223
192
  /**
224
- * Jump to a specific state (time-travel)
193
+ * Jump to a specific state (time-travel).
225
194
  *
226
- * For full time-travel support, users should use withBuoyDevTools.
227
- * This limited version attempts to work with auto-instrumentation.
195
+ * Real on the enhancer path (reducer is wrapped from creation) and on the
196
+ * middleware path when the app used `withBuoyDevTools`. On the bare patch
197
+ * path there's no reducer wrapper, so the jump action is ignored — we detect
198
+ * that and warn once instead of pretending it worked.
228
199
  */
229
200
  function jumpToState(state) {
230
- if (activeStore) {
231
- activeStore.dispatch({
232
- type: _buoyReduxMiddleware.BUOY_JUMP_TO_STATE,
233
- payload: state
234
- });
235
- } else {
201
+ const store = (0, _activeStoreRegistry.getActiveStore)();
202
+ if (!store) {
236
203
  console.warn("[BuoyRedux] Cannot jump to state - no store instrumented");
204
+ return;
205
+ }
206
+ store.dispatch({
207
+ type: _buoyReduxMiddleware.BUOY_JUMP_TO_STATE,
208
+ payload: state
209
+ });
210
+ if (typeof __DEV__ !== "undefined" && __DEV__ && !warnedJumpUnsupported && store.getState() !== state) {
211
+ warnedJumpUnsupported = true;
212
+ 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.");
237
213
  }
238
214
  }
239
215
 
@@ -241,30 +217,28 @@ function jumpToState(state) {
241
217
  * Replay an action (dispatch it again)
242
218
  */
243
219
  function replayAction(action) {
244
- if (activeStore) {
245
- activeStore.dispatch(action);
220
+ const store = (0, _activeStoreRegistry.getActiveStore)();
221
+ if (store) {
222
+ store.dispatch(action);
246
223
  } else {
247
224
  console.warn("[BuoyRedux] Cannot replay action - no store instrumented");
248
225
  }
249
226
  }
250
227
 
251
228
  /**
252
- * Get the currently active (instrumented) store
229
+ * Get the currently active (instrumented) store.
230
+ *
231
+ * NOTE: on the middleware path this is the middleware `storeApi`
232
+ * ({getState, dispatch}) rather than the full Store object — every consumer
233
+ * in this package only needs those two.
253
234
  */
254
235
  function getActiveStore() {
255
- return activeStore;
236
+ return (0, _activeStoreRegistry.getActiveStore)();
256
237
  }
257
238
 
258
239
  /**
259
240
  * Check if auto-instrumentation is active
260
241
  */
261
242
  function isAutoInstrumentActive() {
262
- return activeStore !== null && isStoreInstrumented(activeStore);
263
- }
264
-
265
- /**
266
- * Check if time-travel support is enabled
267
- */
268
- function isTimeTravelEnabled() {
269
- return timeTravelEnabled;
243
+ return (0, _activeStoreRegistry.getActiveStore)() !== null;
270
244
  }
@@ -10,6 +10,7 @@ exports.jumpToState = jumpToState;
10
10
  exports.replayAction = replayAction;
11
11
  exports.withBuoyDevTools = withBuoyDevTools;
12
12
  var _reduxActionStore = require("./reduxActionStore");
13
+ var _activeStoreRegistry = require("./activeStoreRegistry");
13
14
  /**
14
15
  * Buoy Redux Middleware for Redux Toolkit
15
16
  *
@@ -47,20 +48,22 @@ function isMiddlewareActive() {
47
48
 
48
49
  const BUOY_JUMP_TO_STATE = exports.BUOY_JUMP_TO_STATE = "@@buoy/JUMP_TO_STATE";
49
50
  const BUOY_REPLAY_ACTION = exports.BUOY_REPLAY_ACTION = "@@buoy/REPLAY_ACTION";
50
- // Store reference to dispatch for time-travel
51
- let storeDispatch = null;
52
-
53
51
  /**
54
- * Jump to a specific state (time-travel)
52
+ * Jump to a specific state (time-travel).
53
+ *
54
+ * Registry-backed, so it works on EVERY capture path (enhancer, middleware,
55
+ * patch) — previously this only worked when the middleware was installed,
56
+ * which silently broke the modal's Jump button on the zero-config path.
55
57
  */
56
58
  function jumpToState(state) {
57
- if (storeDispatch) {
58
- storeDispatch({
59
+ const store = (0, _activeStoreRegistry.getActiveStore)();
60
+ if (store) {
61
+ store.dispatch({
59
62
  type: BUOY_JUMP_TO_STATE,
60
63
  payload: state
61
64
  });
62
65
  } else {
63
- console.warn("[BuoyRedux] Cannot jump to state - middleware not initialized");
66
+ console.warn("[BuoyRedux] Cannot jump to state - no store instrumented");
64
67
  }
65
68
  }
66
69
 
@@ -68,11 +71,11 @@ function jumpToState(state) {
68
71
  * Replay an action (dispatch it again)
69
72
  */
70
73
  function replayAction(action) {
71
- if (storeDispatch) {
72
- // Dispatch the original action directly
73
- storeDispatch(action);
74
+ const store = (0, _activeStoreRegistry.getActiveStore)();
75
+ if (store) {
76
+ store.dispatch(action);
74
77
  } else {
75
- console.warn("[BuoyRedux] Cannot replay action - middleware not initialized");
78
+ console.warn("[BuoyRedux] Cannot replay action - no store instrumented");
76
79
  }
77
80
  }
78
81
 
@@ -107,8 +110,10 @@ const buoyReduxMiddleware = storeApi => {
107
110
  // Mark middleware as active so auto-instrument knows to skip
108
111
  middlewareActive = true;
109
112
 
110
- // Store dispatch reference for time-travel
111
- storeDispatch = storeApi.dispatch;
113
+ // Register as the active store so getState/dispatch (MCP, desktop, time
114
+ // travel) work on the middleware path too — storeApi has both getState and
115
+ // dispatch, which is all the registry needs.
116
+ (0, _activeStoreRegistry.registerActiveStore)(storeApi, "middleware");
112
117
  return next => action => {
113
118
  const actionObj = action;
114
119
  const actionType = actionObj.type;
@@ -141,8 +146,8 @@ function createBuoyReduxMiddleware(options = {}) {
141
146
  // Mark middleware as active so auto-instrument knows to skip
142
147
  middlewareActive = true;
143
148
 
144
- // Store dispatch reference for time-travel
145
- storeDispatch = storeApi.dispatch;
149
+ // Register as the active store (see buoyReduxMiddleware above).
150
+ (0, _activeStoreRegistry.registerActiveStore)(storeApi, "middleware");
146
151
  return next => action => {
147
152
  const actionObj = action;
148
153
  const actionType = actionObj.type || "";
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.buoyDevtoolsEnhancer = void 0;
7
+ exports.claimDevtoolsHook = claimDevtoolsHook;
8
+ exports.isDevtoolsHookClaimed = isDevtoolsHookClaimed;
9
+ var _reduxActionStore = require("./reduxActionStore");
10
+ var _buoyReduxMiddleware = require("./buoyReduxMiddleware");
11
+ var _activeStoreRegistry = require("./activeStoreRegistry");
12
+ /**
13
+ * Store-creation-time capture via the OFFICIAL Redux DevTools hook.
14
+ *
15
+ * `configureStore` defaults `devTools: true` and, at RTK module-evaluation
16
+ * time, snapshots `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` if defined.
17
+ * That global doesn't exist in React Native — so Buoy claims it (dev builds
18
+ * only, and only when nothing else has). This makes Buoy a real store
19
+ * ENHANCER, present from the store's birth:
20
+ *
21
+ * - FULL capture: actions dispatched from inside middleware/thunks (RTK
22
+ * `pending`/`fulfilled`/`rejected`, RTK Query) hit our dispatch because the
23
+ * enhancer sits BELOW applyMiddleware in the compose chain.
24
+ * - Real time travel: we wrap the reducer, so BUOY_JUMP_TO_STATE actually
25
+ * jumps — and replaceReducer is re-wrapped, so hosts that inject reducers
26
+ * dynamically keep working.
27
+ * - Nothing is monkey-patched: this is the integration point Redux itself
28
+ * ships for devtools (same one react-native-debugger used).
29
+ *
30
+ * Race note: RTK snapshots the global when ITS module first evaluates, so this
31
+ * module must load before `@reduxjs/toolkit` does. `@buoy-gg/core` requires us
32
+ * at module scope, which usually wins; when it doesn't, the dispatch-patch
33
+ * fallback in autoInstrument.ts still binds at first mount. Apps that want a
34
+ * guarantee put `import '@buoy-gg/redux';` as the FIRST import of their entry.
35
+ */
36
+
37
+ /** Standard redux compose. */
38
+ function composeFns(...funcs) {
39
+ if (funcs.length === 0) return arg => arg;
40
+ if (funcs.length === 1) return funcs[0];
41
+ return funcs.reduce((a, b) => (...args) => a(b(...args)));
42
+ }
43
+
44
+ /** Wrap a reducer so BUOY_JUMP_TO_STATE performs a real state jump. */
45
+ function wrapReducer(reducer) {
46
+ return (state, action) => {
47
+ if (action && action.type === _buoyReduxMiddleware.BUOY_JUMP_TO_STATE) {
48
+ return action.payload;
49
+ }
50
+ return reducer(state, action);
51
+ };
52
+ }
53
+
54
+ /** Should this action be recorded? (Mirrors autoInstrument's filters.) */
55
+ function isRecordable(action) {
56
+ if (typeof action !== "object" || action === null) return false;
57
+ const type = action.type;
58
+ if (typeof type !== "string") return false;
59
+ return !type.startsWith("@@buoy/") && !type.startsWith("@@redux/");
60
+ }
61
+
62
+ /**
63
+ * The Buoy store enhancer. Innermost in the compose chain (below
64
+ * applyMiddleware), so every action that reaches the reducer — including ones
65
+ * dispatched from inside middleware and thunks — passes through here.
66
+ */
67
+ const buoyDevtoolsEnhancer = createStore => (reducer, preloadedState) => {
68
+ const store = createStore(wrapReducer(reducer), preloadedState);
69
+ const dispatch = action => {
70
+ // If the explicit middleware is installed it already records — the
71
+ // enhancer stays passive to avoid double capture.
72
+ if (!isRecordable(action) || (0, _buoyReduxMiddleware.isMiddlewareActive)()) {
73
+ return store.dispatch(action);
74
+ }
75
+ const startTime = performance.now();
76
+ const prevState = store.getState();
77
+ const result = store.dispatch(action);
78
+ _reduxActionStore.reduxActionStore.addAction(action, prevState, store.getState(), performance.now() - startTime);
79
+ return result;
80
+ };
81
+ const enhanced = {
82
+ ...store,
83
+ dispatch,
84
+ // Keep time travel alive across dynamic reducer injection.
85
+ replaceReducer: next => store.replaceReducer(wrapReducer(next)),
86
+ [_activeStoreRegistry.ENHANCER_SYMBOL]: true
87
+ };
88
+ (0, _activeStoreRegistry.registerActiveStore)(enhanced, "enhancer");
89
+ return enhanced;
90
+ };
91
+
92
+ /**
93
+ * Implements the `__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` calling conventions:
94
+ * - `compose(options)(...enhancers)` — what RTK's configureStore does
95
+ * - `compose(...enhancers)` — classic createStore usage from the Redux docs
96
+ */
97
+ exports.buoyDevtoolsEnhancer = buoyDevtoolsEnhancer;
98
+ function buoyComposeWithDevTools(...args) {
99
+ if (args.length === 0) {
100
+ return composeFns(buoyDevtoolsEnhancer);
101
+ }
102
+ if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) {
103
+ // Options form → return a compose function.
104
+ return (...enhancers) => composeFns(...enhancers, buoyDevtoolsEnhancer);
105
+ }
106
+ return composeFns(...args, buoyDevtoolsEnhancer);
107
+ }
108
+ let claimed = false;
109
+
110
+ /**
111
+ * Claim the DevTools globals — dev builds only, and never over an existing
112
+ * claimant (e.g. the real browser extension on web).
113
+ *
114
+ * Idempotent; runs at module scope so simply importing `@buoy-gg/redux` (which
115
+ * `@buoy-gg/core` does at module scope) performs the claim as early as
116
+ * possible.
117
+ */
118
+ function claimDevtoolsHook() {
119
+ const isDev = typeof __DEV__ !== "undefined" && __DEV__;
120
+ if (!isDev) return false;
121
+ const g = globalThis;
122
+ if (!g.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
123
+ g.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = buoyComposeWithDevTools;
124
+ claimed = true;
125
+ }
126
+ if (!g.__REDUX_DEVTOOLS_EXTENSION__) {
127
+ // Legacy single-enhancer form: `createStore(reducer, __REDUX_DEVTOOLS_EXTENSION__())`
128
+ g.__REDUX_DEVTOOLS_EXTENSION__ = () => buoyDevtoolsEnhancer;
129
+ }
130
+ return claimed;
131
+ }
132
+
133
+ /** Whether Buoy owns the DevTools compose global (i.e. won the load race). */
134
+ function isDevtoolsHookClaimed() {
135
+ return claimed;
136
+ }
137
+ claimDevtoolsHook();
@@ -27,12 +27,24 @@ Object.defineProperty(exports, "autoReplayAction", {
27
27
  return _autoInstrument.replayAction;
28
28
  }
29
29
  });
30
+ Object.defineProperty(exports, "buoyDevtoolsEnhancer", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _devtoolsHook.buoyDevtoolsEnhancer;
34
+ }
35
+ });
30
36
  Object.defineProperty(exports, "buoyReduxMiddleware", {
31
37
  enumerable: true,
32
38
  get: function () {
33
39
  return _buoyReduxMiddleware.buoyReduxMiddleware;
34
40
  }
35
41
  });
42
+ Object.defineProperty(exports, "claimDevtoolsHook", {
43
+ enumerable: true,
44
+ get: function () {
45
+ return _devtoolsHook.claimDevtoolsHook;
46
+ }
47
+ });
36
48
  Object.defineProperty(exports, "createBuoyReduxMiddleware", {
37
49
  enumerable: true,
38
50
  get: function () {
@@ -51,6 +63,12 @@ Object.defineProperty(exports, "getActiveStore", {
51
63
  return _autoInstrument.getActiveStore;
52
64
  }
53
65
  });
66
+ Object.defineProperty(exports, "getInstrumentationStatus", {
67
+ enumerable: true,
68
+ get: function () {
69
+ return _autoInstrument.getInstrumentationStatus;
70
+ }
71
+ });
54
72
  Object.defineProperty(exports, "instrumentStore", {
55
73
  enumerable: true,
56
74
  get: function () {
@@ -63,6 +81,12 @@ Object.defineProperty(exports, "isAutoInstrumentActive", {
63
81
  return _autoInstrument.isAutoInstrumentActive;
64
82
  }
65
83
  });
84
+ Object.defineProperty(exports, "isDevtoolsHookClaimed", {
85
+ enumerable: true,
86
+ get: function () {
87
+ return _devtoolsHook.isDevtoolsHookClaimed;
88
+ }
89
+ });
66
90
  Object.defineProperty(exports, "isStoreInstrumented", {
67
91
  enumerable: true,
68
92
  get: function () {
@@ -87,6 +111,12 @@ Object.defineProperty(exports, "reduxHistoryAdapter", {
87
111
  return _createReduxHistoryAdapter.reduxHistoryAdapter;
88
112
  }
89
113
  });
114
+ Object.defineProperty(exports, "registerReduxStore", {
115
+ enumerable: true,
116
+ get: function () {
117
+ return _autoInstrument.registerReduxStore;
118
+ }
119
+ });
90
120
  Object.defineProperty(exports, "replayAction", {
91
121
  enumerable: true,
92
122
  get: function () {
@@ -108,4 +138,5 @@ Object.defineProperty(exports, "withBuoyDevTools", {
108
138
  var _reduxActionStore = require("./reduxActionStore");
109
139
  var _buoyReduxMiddleware = require("./buoyReduxMiddleware");
110
140
  var _createReduxHistoryAdapter = require("./createReduxHistoryAdapter");
111
- var _autoInstrument = require("./autoInstrument");
141
+ var _autoInstrument = require("./autoInstrument");
142
+ var _devtoolsHook = require("./devtoolsHook");
@@ -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
- * @example Advanced: Manual middleware (only if you need more control)
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
  // =============================================================================
@@ -82,6 +102,9 @@ export { ReduxActionItem } from "./redux/components/ReduxActionItem";
82
102
  export { ReduxActionDetailView } from "./redux/components/ReduxActionDetailView";
83
103
  export { ReduxActionDetailContent, ReduxActionDetailFooter } from "./redux/components/ReduxActionDetailContent";
84
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";
85
108
 
86
109
  // =============================================================================
87
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
+ }