@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.
Files changed (46) hide show
  1. package/README.md +21 -2
  2. package/lib/commonjs/index.js +44 -0
  3. package/lib/commonjs/redux/components/ReduxAutoInstrument.js +20 -0
  4. package/lib/commonjs/redux/components/ReduxModal.js +123 -68
  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 +29 -4
  14. package/lib/module/redux/components/ReduxAutoInstrument.js +17 -0
  15. package/lib/module/redux/components/ReduxModal.js +122 -68
  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 +19 -5
  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 +15 -1
  29. package/lib/typescript/redux/components/ReduxModal.d.ts.map +1 -1
  30. package/lib/typescript/redux/components/index.d.ts +1 -0
  31. package/lib/typescript/redux/components/index.d.ts.map +1 -1
  32. package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts +16 -28
  33. package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts.map +1 -1
  34. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts +18 -4
  35. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts.map +1 -1
  36. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts +47 -0
  37. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts.map +1 -0
  38. package/lib/typescript/redux/utils/autoInstrument.d.ts +46 -23
  39. package/lib/typescript/redux/utils/autoInstrument.d.ts.map +1 -1
  40. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts +5 -1
  41. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts.map +1 -1
  42. package/lib/typescript/redux/utils/devtoolsHook.d.ts +45 -0
  43. package/lib/typescript/redux/utils/devtoolsHook.d.ts.map +1 -0
  44. package/lib/typescript/redux/utils/index.d.ts +3 -2
  45. package/lib/typescript/redux/utils/index.d.ts.map +1 -1
  46. package/package.json +8 -5
@@ -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
  }
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { reduxActionStore } from "./reduxActionStore";
20
+ import { registerActiveStore, getActiveStore } from "./activeStoreRegistry";
20
21
 
21
22
  // ============================================
22
23
  // Middleware Active Tracking
@@ -39,20 +40,22 @@ export function isMiddlewareActive() {
39
40
 
40
41
  export const BUOY_JUMP_TO_STATE = "@@buoy/JUMP_TO_STATE";
41
42
  export const BUOY_REPLAY_ACTION = "@@buoy/REPLAY_ACTION";
42
- // Store reference to dispatch for time-travel
43
- let storeDispatch = null;
44
-
45
43
  /**
46
- * Jump to a specific state (time-travel)
44
+ * Jump to a specific state (time-travel).
45
+ *
46
+ * Registry-backed, so it works on EVERY capture path (enhancer, middleware,
47
+ * patch) — previously this only worked when the middleware was installed,
48
+ * which silently broke the modal's Jump button on the zero-config path.
47
49
  */
48
50
  export function jumpToState(state) {
49
- if (storeDispatch) {
50
- storeDispatch({
51
+ const store = getActiveStore();
52
+ if (store) {
53
+ store.dispatch({
51
54
  type: BUOY_JUMP_TO_STATE,
52
55
  payload: state
53
56
  });
54
57
  } else {
55
- console.warn("[BuoyRedux] Cannot jump to state - middleware not initialized");
58
+ console.warn("[BuoyRedux] Cannot jump to state - no store instrumented");
56
59
  }
57
60
  }
58
61
 
@@ -60,11 +63,11 @@ export function jumpToState(state) {
60
63
  * Replay an action (dispatch it again)
61
64
  */
62
65
  export function replayAction(action) {
63
- if (storeDispatch) {
64
- // Dispatch the original action directly
65
- storeDispatch(action);
66
+ const store = getActiveStore();
67
+ if (store) {
68
+ store.dispatch(action);
66
69
  } else {
67
- console.warn("[BuoyRedux] Cannot replay action - middleware not initialized");
70
+ console.warn("[BuoyRedux] Cannot replay action - no store instrumented");
68
71
  }
69
72
  }
70
73
 
@@ -99,8 +102,10 @@ export const buoyReduxMiddleware = storeApi => {
99
102
  // Mark middleware as active so auto-instrument knows to skip
100
103
  middlewareActive = true;
101
104
 
102
- // Store dispatch reference for time-travel
103
- storeDispatch = storeApi.dispatch;
105
+ // Register as the active store so getState/dispatch (MCP, desktop, time
106
+ // travel) work on the middleware path too — storeApi has both getState and
107
+ // dispatch, which is all the registry needs.
108
+ registerActiveStore(storeApi, "middleware");
104
109
  return next => action => {
105
110
  const actionObj = action;
106
111
  const actionType = actionObj.type;
@@ -132,8 +137,8 @@ export function createBuoyReduxMiddleware(options = {}) {
132
137
  // Mark middleware as active so auto-instrument knows to skip
133
138
  middlewareActive = true;
134
139
 
135
- // Store dispatch reference for time-travel
136
- storeDispatch = storeApi.dispatch;
140
+ // Register as the active store (see buoyReduxMiddleware above).
141
+ registerActiveStore(storeApi, "middleware");
137
142
  return next => action => {
138
143
  const actionObj = action;
139
144
  const actionType = actionObj.type || "";
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Store-creation-time capture via the OFFICIAL Redux DevTools hook.
5
+ *
6
+ * `configureStore` defaults `devTools: true` and, at RTK module-evaluation
7
+ * time, snapshots `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` if defined.
8
+ * That global doesn't exist in React Native — so Buoy claims it (dev builds
9
+ * only, and only when nothing else has). This makes Buoy a real store
10
+ * ENHANCER, present from the store's birth:
11
+ *
12
+ * - FULL capture: actions dispatched from inside middleware/thunks (RTK
13
+ * `pending`/`fulfilled`/`rejected`, RTK Query) hit our dispatch because the
14
+ * enhancer sits BELOW applyMiddleware in the compose chain.
15
+ * - Real time travel: we wrap the reducer, so BUOY_JUMP_TO_STATE actually
16
+ * jumps — and replaceReducer is re-wrapped, so hosts that inject reducers
17
+ * dynamically keep working.
18
+ * - Nothing is monkey-patched: this is the integration point Redux itself
19
+ * ships for devtools (same one react-native-debugger used).
20
+ *
21
+ * Race note: RTK snapshots the global when ITS module first evaluates, so this
22
+ * module must load before `@reduxjs/toolkit` does. `@buoy-gg/core` requires us
23
+ * at module scope, which usually wins; when it doesn't, the dispatch-patch
24
+ * fallback in autoInstrument.ts still binds at first mount. Apps that want a
25
+ * guarantee put `import '@buoy-gg/redux';` as the FIRST import of their entry.
26
+ */
27
+
28
+ import { reduxActionStore } from "./reduxActionStore";
29
+ import { BUOY_JUMP_TO_STATE, isMiddlewareActive } from "./buoyReduxMiddleware";
30
+ import { registerActiveStore, ENHANCER_SYMBOL } from "./activeStoreRegistry";
31
+ /** Standard redux compose. */
32
+ function composeFns(...funcs) {
33
+ if (funcs.length === 0) return arg => arg;
34
+ if (funcs.length === 1) return funcs[0];
35
+ return funcs.reduce((a, b) => (...args) => a(b(...args)));
36
+ }
37
+
38
+ /** Wrap a reducer so BUOY_JUMP_TO_STATE performs a real state jump. */
39
+ function wrapReducer(reducer) {
40
+ return (state, action) => {
41
+ if (action && action.type === BUOY_JUMP_TO_STATE) {
42
+ return action.payload;
43
+ }
44
+ return reducer(state, action);
45
+ };
46
+ }
47
+
48
+ /** Should this action be recorded? (Mirrors autoInstrument's filters.) */
49
+ function isRecordable(action) {
50
+ if (typeof action !== "object" || action === null) return false;
51
+ const type = action.type;
52
+ if (typeof type !== "string") return false;
53
+ return !type.startsWith("@@buoy/") && !type.startsWith("@@redux/");
54
+ }
55
+
56
+ /**
57
+ * The Buoy store enhancer. Innermost in the compose chain (below
58
+ * applyMiddleware), so every action that reaches the reducer — including ones
59
+ * dispatched from inside middleware and thunks — passes through here.
60
+ */
61
+ export const buoyDevtoolsEnhancer = createStore => (reducer, preloadedState) => {
62
+ const store = createStore(wrapReducer(reducer), preloadedState);
63
+ const dispatch = action => {
64
+ // If the explicit middleware is installed it already records — the
65
+ // enhancer stays passive to avoid double capture.
66
+ if (!isRecordable(action) || isMiddlewareActive()) {
67
+ return store.dispatch(action);
68
+ }
69
+ const startTime = performance.now();
70
+ const prevState = store.getState();
71
+ const result = store.dispatch(action);
72
+ reduxActionStore.addAction(action, prevState, store.getState(), performance.now() - startTime);
73
+ return result;
74
+ };
75
+ const enhanced = {
76
+ ...store,
77
+ dispatch,
78
+ // Keep time travel alive across dynamic reducer injection.
79
+ replaceReducer: next => store.replaceReducer(wrapReducer(next)),
80
+ [ENHANCER_SYMBOL]: true
81
+ };
82
+ registerActiveStore(enhanced, "enhancer");
83
+ return enhanced;
84
+ };
85
+
86
+ /**
87
+ * Implements the `__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` calling conventions:
88
+ * - `compose(options)(...enhancers)` — what RTK's configureStore does
89
+ * - `compose(...enhancers)` — classic createStore usage from the Redux docs
90
+ */
91
+ function buoyComposeWithDevTools(...args) {
92
+ if (args.length === 0) {
93
+ return composeFns(buoyDevtoolsEnhancer);
94
+ }
95
+ if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) {
96
+ // Options form → return a compose function.
97
+ return (...enhancers) => composeFns(...enhancers, buoyDevtoolsEnhancer);
98
+ }
99
+ return composeFns(...args, buoyDevtoolsEnhancer);
100
+ }
101
+ let claimed = false;
102
+
103
+ /**
104
+ * Claim the DevTools globals — dev builds only, and never over an existing
105
+ * claimant (e.g. the real browser extension on web).
106
+ *
107
+ * Idempotent; runs at module scope so simply importing `@buoy-gg/redux` (which
108
+ * `@buoy-gg/core` does at module scope) performs the claim as early as
109
+ * possible.
110
+ */
111
+ export function claimDevtoolsHook() {
112
+ const isDev = typeof __DEV__ !== "undefined" && __DEV__;
113
+ if (!isDev) return false;
114
+ const g = globalThis;
115
+ if (!g.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
116
+ g.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = buoyComposeWithDevTools;
117
+ claimed = true;
118
+ }
119
+ if (!g.__REDUX_DEVTOOLS_EXTENSION__) {
120
+ // Legacy single-enhancer form: `createStore(reducer, __REDUX_DEVTOOLS_EXTENSION__())`
121
+ g.__REDUX_DEVTOOLS_EXTENSION__ = () => buoyDevtoolsEnhancer;
122
+ }
123
+ return claimed;
124
+ }
125
+
126
+ /** Whether Buoy owns the DevTools compose global (i.e. won the load race). */
127
+ export function isDevtoolsHookClaimed() {
128
+ return claimed;
129
+ }
130
+ claimDevtoolsHook();
@@ -5,4 +5,6 @@ export { buoyReduxMiddleware, createBuoyReduxMiddleware, withBuoyDevTools, jumpT
5
5
  export { createReduxHistoryAdapter, reduxHistoryAdapter } from "./createReduxHistoryAdapter";
6
6
 
7
7
  // Auto-instrumentation (zero-config)
8
- export { instrumentStore, uninstrumentStore, isStoreInstrumented, isAutoInstrumentActive, getActiveStore, jumpToState as autoJumpToState, replayAction as autoReplayAction } from "./autoInstrument";
8
+ export { instrumentStore, uninstrumentStore, isStoreInstrumented, isAutoInstrumentActive, getActiveStore, getInstrumentationStatus, registerReduxStore, jumpToState as autoJumpToState, replayAction as autoReplayAction } from "./autoInstrument";
9
+ // Store-creation-time capture (the primary, full-capture path)
10
+ export { buoyDevtoolsEnhancer, claimDevtoolsHook, isDevtoolsHookClaimed } from "./devtoolsHook";
@@ -27,9 +27,20 @@
27
27
  *
28
28
  * That's it! The Redux tool will auto-detect your store and capture actions.
29
29
  *
30
- * @example Advanced: Manual middleware (only if you need more control)
30
+ * HOW CAPTURE WORKS (in priority order):
31
+ * 1. ENHANCER — importing this package claims the official Redux DevTools
32
+ * global (`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__`), which RTK checks by
33
+ * default. If this package loads before your store module, Buoy is inside
34
+ * the store from creation: FULL capture (thunk/RTK Query actions included)
35
+ * and real time travel. To guarantee this ordering, make
36
+ * `import '@buoy-gg/redux';` the FIRST import of your app entry.
37
+ * 2. MIDDLEWARE — add `buoyReduxMiddleware` for explicit full capture.
38
+ * 3. PATCH — otherwise, `FloatingDevTools` binds the store at first mount by
39
+ * patching store.dispatch. Top-level dispatches only (thunk-internal
40
+ * actions bypass a dispatch patch — that's a Redux architecture fact).
41
+ *
42
+ * @example Advanced: Manual middleware (explicit full capture)
31
43
  * ```tsx
32
- * // For time-travel or custom options, you can optionally use middleware:
33
44
  * import { buoyReduxMiddleware, withBuoyDevTools } from '@buoy-gg/redux';
34
45
  *
35
46
  * const store = configureStore({
@@ -39,21 +50,24 @@
39
50
  * });
40
51
  * ```
41
52
  */
53
+ import "./redux/utils/devtoolsHook";
42
54
  export { reduxToolPreset, createReduxTool } from "./preset";
43
55
  export { buoyReduxMiddleware, createBuoyReduxMiddleware, withBuoyDevTools, jumpToState, replayAction, BUOY_JUMP_TO_STATE, BUOY_REPLAY_ACTION, } from "./redux/utils/buoyReduxMiddleware";
44
- export { instrumentStore, uninstrumentStore, isStoreInstrumented, isAutoInstrumentActive, getActiveStore, } from "./redux/utils/autoInstrument";
45
- export type { AutoInstrumentOptions } from "./redux/utils/autoInstrument";
56
+ export { instrumentStore, uninstrumentStore, isStoreInstrumented, isAutoInstrumentActive, getActiveStore, getInstrumentationStatus, registerReduxStore, } from "./redux/utils/autoInstrument";
57
+ export type { AutoInstrumentOptions, InstrumentationStatus, CaptureMechanism, } from "./redux/utils/autoInstrument";
58
+ export { buoyDevtoolsEnhancer, claimDevtoolsHook, isDevtoolsHookClaimed, } from "./redux/utils/devtoolsHook";
46
59
  export { createReduxHistoryAdapter, reduxHistoryAdapter, type ReduxHistoryAdapterOptions, } from "./redux/utils/createReduxHistoryAdapter";
47
60
  export { reduxSyncAdapter } from "./redux/sync/reduxSyncAdapter";
48
61
  export { useReduxActions } from "./redux/hooks/useReduxActions";
49
62
  export type { UseReduxActionsResult } from "./redux/hooks/useReduxActions";
50
63
  export { useAutoInstrumentRedux, useReduxAvailability, } from "./redux/hooks/useAutoInstrumentRedux";
51
64
  export type { UseAutoInstrumentReduxOptions, UseAutoInstrumentReduxResult, } from "./redux/hooks/useAutoInstrumentRedux";
52
- export { ReduxModal, FREE_TIER_ACTION_LIMIT } from "./redux/components/ReduxModal";
65
+ export { ReduxModal, ReduxToolView, FREE_TIER_ACTION_LIMIT, } from "./redux/components/ReduxModal";
53
66
  export { ReduxActionItem } from "./redux/components/ReduxActionItem";
54
67
  export { ReduxActionDetailView } from "./redux/components/ReduxActionDetailView";
55
68
  export { ReduxActionDetailContent, ReduxActionDetailFooter, } from "./redux/components/ReduxActionDetailContent";
56
69
  export { ReduxIcon } from "./redux/components/ReduxIcon";
70
+ export { ReduxAutoInstrument } from "./redux/components/ReduxAutoInstrument";
57
71
  export type { ActionCategory, ActionMeta, ReduxAction, ReduxFilter, ReduxConnectorOptions, ReduxStoreMinimal, BuoyReduxMiddlewareOptions, } from "./redux/types";
58
72
  /** @internal */
59
73
  export { reduxActionStore } from "./redux/utils/reduxActionStore";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAKH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAK5D,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EACzB,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,mCAAmC,CAAC;AAK3C,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,GACf,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAK1E,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAC;AAKjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,YAAY,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAG3E,OAAO,EACL,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,sCAAsC,CAAC;AAC9C,YAAY,EACV,6BAA6B,EAC7B,4BAA4B,GAC7B,MAAM,sCAAsC,CAAC;AAK9C,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,EACL,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAKzD,YAAY,EACV,cAAc,EACd,UAAU,EACV,WAAW,EACX,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,0BAA0B,GAC3B,MAAM,eAAe,CAAC;AAKvB,gBAAgB;AAChB,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAKH,OAAO,4BAA4B,CAAC;AAKpC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAK5D,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EACzB,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,mCAAmC,CAAC;AAK3C,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EACV,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AAItC,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,4BAA4B,CAAC;AAKpC,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAC;AAKjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,YAAY,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAG3E,OAAO,EACL,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,sCAAsC,CAAC;AAC9C,YAAY,EACV,6BAA6B,EAC7B,4BAA4B,GAC7B,MAAM,sCAAsC,CAAC;AAO9C,OAAO,EACL,UAAU,EACV,aAAa,EACb,sBAAsB,GACvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,EACL,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAGzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAK7E,YAAY,EACV,cAAc,EACd,UAAU,EACV,WAAW,EACX,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,0BAA0B,GAC3B,MAAM,eAAe,CAAC;AAKvB,gBAAgB;AAChB,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Headless startup instrumentation. Auto-rendered by FloatingDevTools (same
3
+ * pattern as RouteTracker & the overlay tools) so the Redux store is bound
4
+ * from app mount — action log, MCP getState/dispatch, and desktop sync all
5
+ * work WITHOUT the user ever opening the Redux tool. Renders nothing.
6
+ *
7
+ * Safe anywhere: if react-redux is missing or there's no Provider above, the
8
+ * hook records the diagnostic and fails gracefully.
9
+ */
10
+ export declare function ReduxAutoInstrument(): null;
11
+ //# sourceMappingURL=ReduxAutoInstrument.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReduxAutoInstrument.d.ts","sourceRoot":"","sources":["../../../../src/redux/components/ReduxAutoInstrument.tsx"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C"}
@@ -7,12 +7,26 @@
7
7
  /** Free tier limit for action history */
8
8
  export declare const FREE_TIER_ACTION_LIMIT = 25;
9
9
  interface ReduxModalProps {
10
+ /**
11
+ * Render as a header-bar + body (+ footer) stack instead of inside `JsModal`,
12
+ * for hosts that place tools in a panel rather than a modal (Buoy Desktop).
13
+ * Only the shell differs — header, screens and state are shared. See
14
+ * `ReduxToolView` at the bottom of this file.
15
+ */
16
+ inline?: boolean;
10
17
  visible: boolean;
11
18
  onClose: () => void;
12
19
  onBack?: () => void;
13
20
  onMinimize?: (modalState: unknown) => void;
14
21
  enableSharedModalDimensions?: boolean;
15
22
  }
16
- export declare function ReduxModal({ visible, onClose, onBack, onMinimize, enableSharedModalDimensions, }: ReduxModalProps): import("react").JSX.Element | null;
23
+ export declare function ReduxModal({ visible, onClose, onBack, onMinimize, enableSharedModalDimensions, inline, }: ReduxModalProps): import("react").JSX.Element | null;
24
+ /**
25
+ * The same Redux tool as `ReduxModal`, rendered INLINE for hosts that place
26
+ * tools in a panel rather than a modal. Exists so there is exactly one
27
+ * implementation — the desktop dashboard used to ship its own copy of this
28
+ * component's header, action list and detail router.
29
+ */
30
+ export declare function ReduxToolView(props?: Omit<ReduxModalProps, "visible" | "onClose" | "inline">): import("react").JSX.Element;
17
31
  export {};
18
32
  //# sourceMappingURL=ReduxModal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ReduxModal.d.ts","sourceRoot":"","sources":["../../../../src/redux/components/ReduxModal.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2CH,yCAAyC;AACzC,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,UAAU,eAAe;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAgBD,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,GACpC,EAAE,eAAe,sCAiYjB"}
1
+ {"version":3,"file":"ReduxModal.d.ts","sourceRoot":"","sources":["../../../../src/redux/components/ReduxModal.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2CH,yCAAyC;AACzC,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,UAAU,eAAe;IACvB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAgBD,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,EACnC,MAAc,GACf,EAAE,eAAe,sCAwajB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,KAAK,GAAE,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAM,+BAGpE"}
@@ -3,4 +3,5 @@ export { ReduxActionItem } from "./ReduxActionItem";
3
3
  export { ReduxActionDetailView } from "./ReduxActionDetailView";
4
4
  export { ReduxActionDetailContent, ReduxActionDetailFooter, } from "./ReduxActionDetailContent";
5
5
  export { ReduxIcon } from "./ReduxIcon";
6
+ export { ReduxAutoInstrument } from "./ReduxAutoInstrument";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/redux/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EACL,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/redux/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EACL,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC"}