@buoy-gg/redux 7.0.9 → 7.0.11

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.
@@ -48,7 +48,8 @@ function ReduxModal({
48
48
  onBack,
49
49
  onMinimize,
50
50
  enableSharedModalDimensions = false,
51
- inline = false
51
+ inline = false,
52
+ onActionSelect
52
53
  }) {
53
54
  const isPro = (0, _license.useIsPro)();
54
55
  const [showProModal, setShowProModal] = (0, _react.useState)(false);
@@ -93,6 +94,17 @@ function ReduxModal({
93
94
 
94
95
  // Get selected action from index
95
96
  const selectedAction = selectedActionIndex !== null ? visibleActions[selectedActionIndex] : null;
97
+
98
+ // Notify the host on selection (keyed by action id so re-renders with the
99
+ // same selection don't re-fire; remote mirrors fetch state trees here).
100
+ const lastNotifiedSelectionRef = (0, _react.useRef)(null);
101
+ (0, _react.useEffect)(() => {
102
+ if (!onActionSelect) return;
103
+ const id = selectedAction?.id ?? null;
104
+ if (id === null || id === lastNotifiedSelectionRef.current) return;
105
+ lastNotifiedSelectionRef.current = id;
106
+ onActionSelect(selectedAction);
107
+ }, [selectedAction, onActionSelect]);
96
108
  const [searchText, setSearchText] = (0, _react.useState)("");
97
109
  const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
98
110
  const searchInputRef = (0, _react.useRef)(null);
@@ -6,6 +6,15 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.reduxSyncAdapter = void 0;
7
7
  var _reduxActionStore = require("../utils/reduxActionStore");
8
8
  var _autoInstrument = require("../utils/autoInstrument");
9
+ /**
10
+ * Upper bound for one on-demand action-detail response. Generous — this is a
11
+ * single explicit fetch, not a per-action stream — but still guarded so a
12
+ * pathological tree can't OOM the device through the reply emit.
13
+ */
14
+ const MAX_ACTION_DETAIL_BYTES = 8 * 1024 * 1024;
15
+
16
+ /** Per-action budget for `payload`/`action` on the snapshot wire. */
17
+ const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
9
18
  /** Top-level slice names of the state tree, for the compact (no-values) view. */
10
19
  function topLevelKeys(state) {
11
20
  return state && typeof state === "object" && !Array.isArray(state) ? Object.keys(state) : [];
@@ -43,13 +52,82 @@ function unavailable() {
43
52
  * action-log snapshot.
44
53
  */
45
54
  const reduxSyncAdapter = exports.reduxSyncAdapter = {
46
- version: 1,
47
- getSnapshot: () => _reduxActionStore.reduxActionStore.getActions(),
55
+ version: 2,
56
+ /**
57
+ * WIRE-FORM snapshot: actions WITHOUT raw prevState/nextState trees.
58
+ * Shipping the trees serialized up to 25×2 full state trees through
59
+ * socket.emit per action — no guard on that path — which froze and
60
+ * native-OOM-crashed enterprise-scale apps whenever a dashboard was
61
+ * connected (root cause confirmed by patch-and-retest). Dashboards fetch
62
+ * trees on demand via `getActionDetail`; already-evicted entries keep the
63
+ * eviction sentinel so consumers can distinguish "fetchable" from "gone".
64
+ */
65
+ getSnapshot: () => _reduxActionStore.reduxActionStore.getActions().map(a => {
66
+ const wire = (0, _reduxActionStore.isEvictedState)(a.prevState) ? {
67
+ ...a
68
+ } : {
69
+ ...a,
70
+ prevState: _reduxActionStore.STATE_ON_DEVICE,
71
+ nextState: _reduxActionStore.STATE_ON_DEVICE
72
+ };
73
+ // A dispatched payload can itself be huge (rehydration actions carry
74
+ // whole state trees). The list view renders payloadPreview anyway —
75
+ // oversized payload/action values stay on-device behind markers.
76
+ if ((0, _reduxActionStore.jsonLongerThan)(wire.payload, MAX_WIRE_PAYLOAD_BYTES)) {
77
+ wire.payload = _reduxActionStore.PAYLOAD_ON_DEVICE;
78
+ }
79
+ if ((0, _reduxActionStore.jsonLongerThan)(wire.action, MAX_WIRE_PAYLOAD_BYTES)) {
80
+ wire.action = _reduxActionStore.PAYLOAD_ON_DEVICE;
81
+ }
82
+ return wire;
83
+ }),
48
84
  subscribe: onChange => _reduxActionStore.reduxActionStore.subscribe(onChange),
49
85
  actions: {
50
86
  clearEvents: () => {
51
87
  _reduxActionStore.reduxActionStore.clearActions();
52
88
  },
89
+ /**
90
+ * On-demand detail: one action's real prevState/nextState trees.
91
+ * The per-snapshot stream stays tree-free; this is the explicit,
92
+ * size-guarded channel for the detail pane.
93
+ */
94
+ getActionDetail: params => {
95
+ const {
96
+ id
97
+ } = params ?? {};
98
+ if (!id) return {
99
+ found: false,
100
+ reason: "missing id"
101
+ };
102
+ const action = _reduxActionStore.reduxActionStore.getActionById(id);
103
+ if (!action) return {
104
+ found: false,
105
+ reason: "unknown id"
106
+ };
107
+ if ((0, _reduxActionStore.isEvictedState)(action.prevState)) {
108
+ return {
109
+ found: true,
110
+ id,
111
+ evicted: true,
112
+ reason: `Raw state was evicted (retained for recent actions only).`
113
+ };
114
+ }
115
+ // Per-field cap: oversized fields degrade individually instead of
116
+ // failing the whole detail (a giant payload shouldn't hide the trees).
117
+ const cappedNote = {
118
+ __buoyTruncated: true,
119
+ note: `Exceeds the ${MAX_ACTION_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
120
+ };
121
+ const cap = value => (0, _reduxActionStore.jsonLongerThan)(value, MAX_ACTION_DETAIL_BYTES) ? cappedNote : value;
122
+ return {
123
+ found: true,
124
+ id,
125
+ prevState: cap(action.prevState),
126
+ nextState: cap(action.nextState),
127
+ payload: cap(action.payload),
128
+ action: cap(action.action)
129
+ };
130
+ },
53
131
  /**
54
132
  * Compact current-state reader: the instrumented store's slice names, and
55
133
  * the full state tree only when `includeValues` is set (heavy).
@@ -198,6 +198,10 @@ let warnedJumpUnsupported = false;
198
198
  * that and warn once instead of pretending it worked.
199
199
  */
200
200
  function jumpToState(state) {
201
+ if ((0, _reduxActionStore.isEvictedState)(state)) {
202
+ console.warn("[BuoyRedux] Cannot jump - this action's state was evicted (raw state is retained for recent actions only)");
203
+ return;
204
+ }
201
205
  const store = (0, _activeStoreRegistry.getActiveStore)();
202
206
  if (!store) {
203
207
  console.warn("[BuoyRedux] Cannot jump to state - no store instrumented");
@@ -56,6 +56,10 @@ const BUOY_REPLAY_ACTION = exports.BUOY_REPLAY_ACTION = "@@buoy/REPLAY_ACTION";
56
56
  * which silently broke the modal's Jump button on the zero-config path.
57
57
  */
58
58
  function jumpToState(state) {
59
+ if ((0, _reduxActionStore.isEvictedState)(state)) {
60
+ console.warn("[BuoyRedux] Cannot jump - this action's state was evicted (raw state is retained for recent actions only)");
61
+ return;
62
+ }
59
63
  const store = (0, _activeStoreRegistry.getActiveStore)();
60
64
  if (store) {
61
65
  store.dispatch({
@@ -3,11 +3,65 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.STATE_RETENTION_LIMIT = exports.STATE_ON_DEVICE = exports.STATE_EVICTED = exports.PAYLOAD_ON_DEVICE = void 0;
7
+ exports.isEvictedState = isEvictedState;
8
+ exports.jsonLongerThan = jsonLongerThan;
6
9
  exports.reduxActionStore = void 0;
7
10
  /**
8
11
  * Redux action store - captures and stores Redux Toolkit actions
9
12
  */
10
13
 
14
+ /**
15
+ * How many of the most-recent actions keep their raw prevState/nextState
16
+ * trees. Older actions keep all diff metadata but their trees are replaced
17
+ * with STATE_EVICTED so a store switch can't pin hundreds of MB.
18
+ */
19
+ const STATE_RETENTION_LIMIT = exports.STATE_RETENTION_LIMIT = 25;
20
+
21
+ /**
22
+ * Sentinel installed in place of evicted state trees. A frozen plain object
23
+ * so existing detail viewers render it harmlessly; jumpToState refuses it.
24
+ */
25
+ const STATE_EVICTED = exports.STATE_EVICTED = Object.freeze({
26
+ __buoyStateEvicted: true,
27
+ note: `Raw state retained for the ${STATE_RETENTION_LIMIT} most recent actions only.`
28
+ });
29
+
30
+ /**
31
+ * Sentinel used in WIRE-FORM snapshots (sync to desktop/MCP) in place of raw
32
+ * prevState/nextState trees. Unlike STATE_EVICTED the trees still exist on
33
+ * the device — dashboards fetch them on demand via the adapter's
34
+ * `getActionDetail` action. Shipping them in every snapshot serialized up to
35
+ * 25×2 full state trees through socket.emit per action (no guard), which
36
+ * froze and native-OOM-crashed enterprise-scale apps whenever a dashboard
37
+ * was connected (confirmed by patch-and-retest on the affected app).
38
+ */
39
+ const STATE_ON_DEVICE = exports.STATE_ON_DEVICE = Object.freeze({
40
+ __buoyStateOnDevice: true,
41
+ note: "State trees stay on the device — fetched on demand via getActionDetail."
42
+ });
43
+
44
+ /**
45
+ * Wire-form sentinel for an oversized action `payload`/`action`. A dispatched
46
+ * payload can itself be huge (rehydration actions carry the whole new state),
47
+ * so the per-snapshot stream ships `payloadPreview` + this marker instead;
48
+ * `getActionDetail` serves the real value on demand.
49
+ */
50
+ const PAYLOAD_ON_DEVICE = exports.PAYLOAD_ON_DEVICE = Object.freeze({
51
+ __buoyPayloadOnDevice: true,
52
+ note: "Payload stays on the device — fetched on demand via getActionDetail."
53
+ });
54
+
55
+ /**
56
+ * True when a value is one of the state sentinels (evicted or on-device) —
57
+ * guards time travel and any consumer that needs a REAL tree.
58
+ */
59
+ function isEvictedState(value) {
60
+ if (typeof value !== "object" || value === null) return false;
61
+ const v = value;
62
+ return v.__buoyStateEvicted === true || v.__buoyStateOnDevice === true;
63
+ }
64
+
11
65
  // ============================================
12
66
  // Helper Functions for Enhanced Data
13
67
  // ============================================
@@ -60,6 +114,39 @@ function parseActionType(type) {
60
114
  };
61
115
  }
62
116
 
117
+ /**
118
+ * Cheap early-abort size probe: is `value`'s JSON form definitely longer than
119
+ * `limit`? Bails the moment the running estimate exceeds the limit, so the
120
+ * cost is O(limit) — never O(payload). Used to avoid JSON.stringify-ing large
121
+ * payloads (a fulfilled thunk's payload can be megabytes) just to produce a
122
+ * 40-char preview; that full stringify ran on EVERY dispatch and blocked the
123
+ * JS thread at enterprise state sizes.
124
+ */
125
+ function jsonLongerThan(value, limit) {
126
+ let bytes = 0;
127
+ const stack = [value];
128
+ const seen = new Set();
129
+ while (stack.length > 0) {
130
+ if (bytes > limit) return true;
131
+ const v = stack.pop();
132
+ if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
133
+ if (seen.has(v)) continue;
134
+ seen.add(v);
135
+ if (Array.isArray(v)) {
136
+ bytes += 2 + v.length;
137
+ for (let i = 0; i < v.length; i++) stack.push(v[i]);
138
+ } else {
139
+ bytes += 2;
140
+ for (const k in v) {
141
+ bytes += k.length + 4;
142
+ stack.push(v[k]);
143
+ }
144
+ }
145
+ } else bytes += 4;
146
+ }
147
+ return bytes > limit;
148
+ }
149
+
63
150
  /**
64
151
  * Format payload preview for display
65
152
  */
@@ -75,6 +162,8 @@ function formatPayloadPreview(payload, maxLength = 40) {
75
162
  }
76
163
  if (Array.isArray(payload)) {
77
164
  if (payload.length === 0) return "[]";
165
+ // Probe cheaply first — only stringify payloads that can actually fit.
166
+ if (jsonLongerThan(payload, maxLength)) return `[${payload.length} items]`;
78
167
  const preview = JSON.stringify(payload);
79
168
  return preview.length > maxLength ? `[${payload.length} items]` : preview;
80
169
  }
@@ -82,7 +171,8 @@ function formatPayloadPreview(payload, maxLength = 40) {
82
171
  const keys = Object.keys(payload);
83
172
  if (keys.length === 0) return "{}";
84
173
 
85
- // Try to create a meaningful preview
174
+ // Probe cheaply first — only stringify payloads that can actually fit.
175
+ if (jsonLongerThan(payload, maxLength)) return `{ ${keys.length} keys }`;
86
176
  const preview = JSON.stringify(payload);
87
177
  if (preview.length <= maxLength) return preview;
88
178
 
@@ -218,6 +308,23 @@ class ReduxActionStore {
218
308
  isSlowAction
219
309
  };
220
310
  this.actions = [reduxAction, ...this.actions].slice(0, this.maxActions);
311
+
312
+ // Bound raw-state retention. Every action holds prevState/nextState by
313
+ // reference; with structural sharing that's normally cheap, but when a
314
+ // host replaces large slices wholesale (store switch / rehydration),
315
+ // each retained action pins a DISTINCT multi-MB subtree — measured 88MB
316
+ // pinned by 50 slice-replacing actions, enough to OOM-kill enterprise
317
+ // apps. Detail view + time travel realistically use recent actions, so
318
+ // older ones keep their diff metadata but drop the raw trees.
319
+ for (let i = STATE_RETENTION_LIMIT; i < this.actions.length; i++) {
320
+ const a = this.actions[i];
321
+ if (a.prevState !== STATE_EVICTED) {
322
+ a.prevState = STATE_EVICTED;
323
+ a.nextState = STATE_EVICTED;
324
+ } else {
325
+ break; // older entries already evicted (list is newest-first)
326
+ }
327
+ }
221
328
  this.notifyListeners();
222
329
  }
223
330
  getActions() {
@@ -280,7 +387,39 @@ class ReduxActionStore {
280
387
  if (!this.isEnabled) {
281
388
  return;
282
389
  }
283
- this.actions = actions.slice(0, this.maxActions);
390
+ // Preserve any trees a remote mirror already hydrated via
391
+ // replaceActionStates — wire-form snapshots always carry markers, and a
392
+ // full snapshot arriving after a detail fetch must not blank the detail
393
+ // pane back out.
394
+ const hydrated = new Map();
395
+ for (const existing of this.actions) {
396
+ if (existing.prevState !== undefined && !isEvictedState(existing.prevState)) {
397
+ hydrated.set(existing.id, existing);
398
+ }
399
+ }
400
+ this.actions = actions.slice(0, this.maxActions).map(a => {
401
+ const h = hydrated.get(a.id);
402
+ return h && isEvictedState(a.prevState) ? {
403
+ ...a,
404
+ prevState: h.prevState,
405
+ nextState: h.nextState
406
+ } : a;
407
+ });
408
+ this.notifyListeners();
409
+ }
410
+
411
+ /**
412
+ * Hydrate one mirrored action with real values (state trees, payload,
413
+ * action) fetched on demand from the device via the adapter's
414
+ * `getActionDetail` action. Remote-mirror (desktop) use only.
415
+ */
416
+ hydrateAction(id, patch) {
417
+ const index = this.actions.findIndex(a => a.id === id);
418
+ if (index === -1) return;
419
+ this.actions = [...this.actions.slice(0, index), {
420
+ ...this.actions[index],
421
+ ...patch
422
+ }, ...this.actions.slice(index + 1)];
284
423
  this.notifyListeners();
285
424
  }
286
425
  setEnabled(enabled) {
@@ -7,7 +7,7 @@
7
7
  * providing zero-config action capture for the DevTools.
8
8
  */
9
9
 
10
- import { useState, useMemo, useRef, useCallback } from "react";
10
+ import { useState, useMemo, useRef, useCallback, useEffect } from "react";
11
11
  import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity } from "react-native";
12
12
  import { JsModal, ModalHeader, macOSColors, buoyColors, Search, Trash2, Power, X, Zap, CheckCircle, Copy, Lock, AlertTriangle, devToolsStorageKeys, copyToClipboard, ProUpgradeModal, PowerToggleButton, TickProvider } from "@buoy-gg/shared-ui";
13
13
  import { useIsPro } from "@buoy-gg/license";
@@ -42,7 +42,8 @@ export function ReduxModal({
42
42
  onBack,
43
43
  onMinimize,
44
44
  enableSharedModalDimensions = false,
45
- inline = false
45
+ inline = false,
46
+ onActionSelect
46
47
  }) {
47
48
  const isPro = useIsPro();
48
49
  const [showProModal, setShowProModal] = useState(false);
@@ -87,6 +88,17 @@ export function ReduxModal({
87
88
 
88
89
  // Get selected action from index
89
90
  const selectedAction = selectedActionIndex !== null ? visibleActions[selectedActionIndex] : null;
91
+
92
+ // Notify the host on selection (keyed by action id so re-renders with the
93
+ // same selection don't re-fire; remote mirrors fetch state trees here).
94
+ const lastNotifiedSelectionRef = useRef(null);
95
+ useEffect(() => {
96
+ if (!onActionSelect) return;
97
+ const id = selectedAction?.id ?? null;
98
+ if (id === null || id === lastNotifiedSelectionRef.current) return;
99
+ lastNotifiedSelectionRef.current = id;
100
+ onActionSelect(selectedAction);
101
+ }, [selectedAction, onActionSelect]);
90
102
  const [searchText, setSearchText] = useState("");
91
103
  const [isSearchActive, setIsSearchActive] = useState(false);
92
104
  const searchInputRef = useRef(null);
@@ -1,7 +1,17 @@
1
1
  "use strict";
2
2
 
3
- import { reduxActionStore } from "../utils/reduxActionStore";
3
+ import { reduxActionStore, STATE_ON_DEVICE, PAYLOAD_ON_DEVICE, isEvictedState, jsonLongerThan } from "../utils/reduxActionStore";
4
4
  import { getActiveStore, getInstrumentationStatus } from "../utils/autoInstrument";
5
+
6
+ /**
7
+ * Upper bound for one on-demand action-detail response. Generous — this is a
8
+ * single explicit fetch, not a per-action stream — but still guarded so a
9
+ * pathological tree can't OOM the device through the reply emit.
10
+ */
11
+ const MAX_ACTION_DETAIL_BYTES = 8 * 1024 * 1024;
12
+
13
+ /** Per-action budget for `payload`/`action` on the snapshot wire. */
14
+ const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
5
15
  /** Top-level slice names of the state tree, for the compact (no-values) view. */
6
16
  function topLevelKeys(state) {
7
17
  return state && typeof state === "object" && !Array.isArray(state) ? Object.keys(state) : [];
@@ -39,13 +49,82 @@ function unavailable() {
39
49
  * action-log snapshot.
40
50
  */
41
51
  export const reduxSyncAdapter = {
42
- version: 1,
43
- getSnapshot: () => reduxActionStore.getActions(),
52
+ version: 2,
53
+ /**
54
+ * WIRE-FORM snapshot: actions WITHOUT raw prevState/nextState trees.
55
+ * Shipping the trees serialized up to 25×2 full state trees through
56
+ * socket.emit per action — no guard on that path — which froze and
57
+ * native-OOM-crashed enterprise-scale apps whenever a dashboard was
58
+ * connected (root cause confirmed by patch-and-retest). Dashboards fetch
59
+ * trees on demand via `getActionDetail`; already-evicted entries keep the
60
+ * eviction sentinel so consumers can distinguish "fetchable" from "gone".
61
+ */
62
+ getSnapshot: () => reduxActionStore.getActions().map(a => {
63
+ const wire = isEvictedState(a.prevState) ? {
64
+ ...a
65
+ } : {
66
+ ...a,
67
+ prevState: STATE_ON_DEVICE,
68
+ nextState: STATE_ON_DEVICE
69
+ };
70
+ // A dispatched payload can itself be huge (rehydration actions carry
71
+ // whole state trees). The list view renders payloadPreview anyway —
72
+ // oversized payload/action values stay on-device behind markers.
73
+ if (jsonLongerThan(wire.payload, MAX_WIRE_PAYLOAD_BYTES)) {
74
+ wire.payload = PAYLOAD_ON_DEVICE;
75
+ }
76
+ if (jsonLongerThan(wire.action, MAX_WIRE_PAYLOAD_BYTES)) {
77
+ wire.action = PAYLOAD_ON_DEVICE;
78
+ }
79
+ return wire;
80
+ }),
44
81
  subscribe: onChange => reduxActionStore.subscribe(onChange),
45
82
  actions: {
46
83
  clearEvents: () => {
47
84
  reduxActionStore.clearActions();
48
85
  },
86
+ /**
87
+ * On-demand detail: one action's real prevState/nextState trees.
88
+ * The per-snapshot stream stays tree-free; this is the explicit,
89
+ * size-guarded channel for the detail pane.
90
+ */
91
+ getActionDetail: params => {
92
+ const {
93
+ id
94
+ } = params ?? {};
95
+ if (!id) return {
96
+ found: false,
97
+ reason: "missing id"
98
+ };
99
+ const action = reduxActionStore.getActionById(id);
100
+ if (!action) return {
101
+ found: false,
102
+ reason: "unknown id"
103
+ };
104
+ if (isEvictedState(action.prevState)) {
105
+ return {
106
+ found: true,
107
+ id,
108
+ evicted: true,
109
+ reason: `Raw state was evicted (retained for recent actions only).`
110
+ };
111
+ }
112
+ // Per-field cap: oversized fields degrade individually instead of
113
+ // failing the whole detail (a giant payload shouldn't hide the trees).
114
+ const cappedNote = {
115
+ __buoyTruncated: true,
116
+ note: `Exceeds the ${MAX_ACTION_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
117
+ };
118
+ const cap = value => jsonLongerThan(value, MAX_ACTION_DETAIL_BYTES) ? cappedNote : value;
119
+ return {
120
+ found: true,
121
+ id,
122
+ prevState: cap(action.prevState),
123
+ nextState: cap(action.nextState),
124
+ payload: cap(action.payload),
125
+ action: cap(action.action)
126
+ };
127
+ },
49
128
  /**
50
129
  * Compact current-state reader: the instrumented store's slice names, and
51
130
  * the full state tree only when `includeValues` is set (heavy).
@@ -15,7 +15,7 @@
15
15
  * capture as "top-level-only" — see activeStoreRegistry.getInstrumentationStatus.
16
16
  */
17
17
 
18
- import { reduxActionStore } from "./reduxActionStore";
18
+ import { reduxActionStore, isEvictedState } from "./reduxActionStore";
19
19
  import { BUOY_JUMP_TO_STATE, isMiddlewareActive } from "./buoyReduxMiddleware";
20
20
  import { registerActiveStore, unregisterActiveStore, getActiveStore as getRegisteredStore, ENHANCER_SYMBOL } from "./activeStoreRegistry";
21
21
 
@@ -183,6 +183,10 @@ let warnedJumpUnsupported = false;
183
183
  * that and warn once instead of pretending it worked.
184
184
  */
185
185
  export function jumpToState(state) {
186
+ if (isEvictedState(state)) {
187
+ console.warn("[BuoyRedux] Cannot jump - this action's state was evicted (raw state is retained for recent actions only)");
188
+ return;
189
+ }
186
190
  const store = getRegisteredStore();
187
191
  if (!store) {
188
192
  console.warn("[BuoyRedux] Cannot jump to state - no store instrumented");
@@ -16,7 +16,7 @@
16
16
  * ```
17
17
  */
18
18
 
19
- import { reduxActionStore } from "./reduxActionStore";
19
+ import { reduxActionStore, isEvictedState } from "./reduxActionStore";
20
20
  import { registerActiveStore, getActiveStore } from "./activeStoreRegistry";
21
21
 
22
22
  // ============================================
@@ -48,6 +48,10 @@ export const BUOY_REPLAY_ACTION = "@@buoy/REPLAY_ACTION";
48
48
  * which silently broke the modal's Jump button on the zero-config path.
49
49
  */
50
50
  export function jumpToState(state) {
51
+ if (isEvictedState(state)) {
52
+ console.warn("[BuoyRedux] Cannot jump - this action's state was evicted (raw state is retained for recent actions only)");
53
+ return;
54
+ }
51
55
  const store = getActiveStore();
52
56
  if (store) {
53
57
  store.dispatch({
@@ -4,6 +4,57 @@
4
4
  * Redux action store - captures and stores Redux Toolkit actions
5
5
  */
6
6
 
7
+ /**
8
+ * How many of the most-recent actions keep their raw prevState/nextState
9
+ * trees. Older actions keep all diff metadata but their trees are replaced
10
+ * with STATE_EVICTED so a store switch can't pin hundreds of MB.
11
+ */
12
+ export const STATE_RETENTION_LIMIT = 25;
13
+
14
+ /**
15
+ * Sentinel installed in place of evicted state trees. A frozen plain object
16
+ * so existing detail viewers render it harmlessly; jumpToState refuses it.
17
+ */
18
+ export const STATE_EVICTED = Object.freeze({
19
+ __buoyStateEvicted: true,
20
+ note: `Raw state retained for the ${STATE_RETENTION_LIMIT} most recent actions only.`
21
+ });
22
+
23
+ /**
24
+ * Sentinel used in WIRE-FORM snapshots (sync to desktop/MCP) in place of raw
25
+ * prevState/nextState trees. Unlike STATE_EVICTED the trees still exist on
26
+ * the device — dashboards fetch them on demand via the adapter's
27
+ * `getActionDetail` action. Shipping them in every snapshot serialized up to
28
+ * 25×2 full state trees through socket.emit per action (no guard), which
29
+ * froze and native-OOM-crashed enterprise-scale apps whenever a dashboard
30
+ * was connected (confirmed by patch-and-retest on the affected app).
31
+ */
32
+ export const STATE_ON_DEVICE = Object.freeze({
33
+ __buoyStateOnDevice: true,
34
+ note: "State trees stay on the device — fetched on demand via getActionDetail."
35
+ });
36
+
37
+ /**
38
+ * Wire-form sentinel for an oversized action `payload`/`action`. A dispatched
39
+ * payload can itself be huge (rehydration actions carry the whole new state),
40
+ * so the per-snapshot stream ships `payloadPreview` + this marker instead;
41
+ * `getActionDetail` serves the real value on demand.
42
+ */
43
+ export const PAYLOAD_ON_DEVICE = Object.freeze({
44
+ __buoyPayloadOnDevice: true,
45
+ note: "Payload stays on the device — fetched on demand via getActionDetail."
46
+ });
47
+
48
+ /**
49
+ * True when a value is one of the state sentinels (evicted or on-device) —
50
+ * guards time travel and any consumer that needs a REAL tree.
51
+ */
52
+ export function isEvictedState(value) {
53
+ if (typeof value !== "object" || value === null) return false;
54
+ const v = value;
55
+ return v.__buoyStateEvicted === true || v.__buoyStateOnDevice === true;
56
+ }
57
+
7
58
  // ============================================
8
59
  // Helper Functions for Enhanced Data
9
60
  // ============================================
@@ -56,6 +107,39 @@ function parseActionType(type) {
56
107
  };
57
108
  }
58
109
 
110
+ /**
111
+ * Cheap early-abort size probe: is `value`'s JSON form definitely longer than
112
+ * `limit`? Bails the moment the running estimate exceeds the limit, so the
113
+ * cost is O(limit) — never O(payload). Used to avoid JSON.stringify-ing large
114
+ * payloads (a fulfilled thunk's payload can be megabytes) just to produce a
115
+ * 40-char preview; that full stringify ran on EVERY dispatch and blocked the
116
+ * JS thread at enterprise state sizes.
117
+ */
118
+ export function jsonLongerThan(value, limit) {
119
+ let bytes = 0;
120
+ const stack = [value];
121
+ const seen = new Set();
122
+ while (stack.length > 0) {
123
+ if (bytes > limit) return true;
124
+ const v = stack.pop();
125
+ if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
126
+ if (seen.has(v)) continue;
127
+ seen.add(v);
128
+ if (Array.isArray(v)) {
129
+ bytes += 2 + v.length;
130
+ for (let i = 0; i < v.length; i++) stack.push(v[i]);
131
+ } else {
132
+ bytes += 2;
133
+ for (const k in v) {
134
+ bytes += k.length + 4;
135
+ stack.push(v[k]);
136
+ }
137
+ }
138
+ } else bytes += 4;
139
+ }
140
+ return bytes > limit;
141
+ }
142
+
59
143
  /**
60
144
  * Format payload preview for display
61
145
  */
@@ -71,6 +155,8 @@ function formatPayloadPreview(payload, maxLength = 40) {
71
155
  }
72
156
  if (Array.isArray(payload)) {
73
157
  if (payload.length === 0) return "[]";
158
+ // Probe cheaply first — only stringify payloads that can actually fit.
159
+ if (jsonLongerThan(payload, maxLength)) return `[${payload.length} items]`;
74
160
  const preview = JSON.stringify(payload);
75
161
  return preview.length > maxLength ? `[${payload.length} items]` : preview;
76
162
  }
@@ -78,7 +164,8 @@ function formatPayloadPreview(payload, maxLength = 40) {
78
164
  const keys = Object.keys(payload);
79
165
  if (keys.length === 0) return "{}";
80
166
 
81
- // Try to create a meaningful preview
167
+ // Probe cheaply first — only stringify payloads that can actually fit.
168
+ if (jsonLongerThan(payload, maxLength)) return `{ ${keys.length} keys }`;
82
169
  const preview = JSON.stringify(payload);
83
170
  if (preview.length <= maxLength) return preview;
84
171
 
@@ -214,6 +301,23 @@ class ReduxActionStore {
214
301
  isSlowAction
215
302
  };
216
303
  this.actions = [reduxAction, ...this.actions].slice(0, this.maxActions);
304
+
305
+ // Bound raw-state retention. Every action holds prevState/nextState by
306
+ // reference; with structural sharing that's normally cheap, but when a
307
+ // host replaces large slices wholesale (store switch / rehydration),
308
+ // each retained action pins a DISTINCT multi-MB subtree — measured 88MB
309
+ // pinned by 50 slice-replacing actions, enough to OOM-kill enterprise
310
+ // apps. Detail view + time travel realistically use recent actions, so
311
+ // older ones keep their diff metadata but drop the raw trees.
312
+ for (let i = STATE_RETENTION_LIMIT; i < this.actions.length; i++) {
313
+ const a = this.actions[i];
314
+ if (a.prevState !== STATE_EVICTED) {
315
+ a.prevState = STATE_EVICTED;
316
+ a.nextState = STATE_EVICTED;
317
+ } else {
318
+ break; // older entries already evicted (list is newest-first)
319
+ }
320
+ }
217
321
  this.notifyListeners();
218
322
  }
219
323
  getActions() {
@@ -276,7 +380,39 @@ class ReduxActionStore {
276
380
  if (!this.isEnabled) {
277
381
  return;
278
382
  }
279
- this.actions = actions.slice(0, this.maxActions);
383
+ // Preserve any trees a remote mirror already hydrated via
384
+ // replaceActionStates — wire-form snapshots always carry markers, and a
385
+ // full snapshot arriving after a detail fetch must not blank the detail
386
+ // pane back out.
387
+ const hydrated = new Map();
388
+ for (const existing of this.actions) {
389
+ if (existing.prevState !== undefined && !isEvictedState(existing.prevState)) {
390
+ hydrated.set(existing.id, existing);
391
+ }
392
+ }
393
+ this.actions = actions.slice(0, this.maxActions).map(a => {
394
+ const h = hydrated.get(a.id);
395
+ return h && isEvictedState(a.prevState) ? {
396
+ ...a,
397
+ prevState: h.prevState,
398
+ nextState: h.nextState
399
+ } : a;
400
+ });
401
+ this.notifyListeners();
402
+ }
403
+
404
+ /**
405
+ * Hydrate one mirrored action with real values (state trees, payload,
406
+ * action) fetched on demand from the device via the adapter's
407
+ * `getActionDetail` action. Remote-mirror (desktop) use only.
408
+ */
409
+ hydrateAction(id, patch) {
410
+ const index = this.actions.findIndex(a => a.id === id);
411
+ if (index === -1) return;
412
+ this.actions = [...this.actions.slice(0, index), {
413
+ ...this.actions[index],
414
+ ...patch
415
+ }, ...this.actions.slice(index + 1)];
280
416
  this.notifyListeners();
281
417
  }
282
418
  setEnabled(enabled) {
@@ -4,6 +4,7 @@
4
4
  * This component auto-instruments the Redux store when it mounts,
5
5
  * providing zero-config action capture for the DevTools.
6
6
  */
7
+ import type { ReduxAction } from "../types";
7
8
  /** Free tier limit for action history */
8
9
  export declare const FREE_TIER_ACTION_LIMIT = 25;
9
10
  interface ReduxModalProps {
@@ -19,8 +20,14 @@ interface ReduxModalProps {
19
20
  onBack?: () => void;
20
21
  onMinimize?: (modalState: unknown) => void;
21
22
  enableSharedModalDimensions?: boolean;
23
+ /**
24
+ * Fired when the user selects an action (opens its detail view). Remote
25
+ * mirrors (Buoy Desktop) use this to fetch the action's real state trees
26
+ * on demand — wire-form snapshots carry markers instead of trees.
27
+ */
28
+ onActionSelect?: (action: ReduxAction) => void;
22
29
  }
23
- export declare function ReduxModal({ visible, onClose, onBack, onMinimize, enableSharedModalDimensions, inline, }: ReduxModalProps): import("react").JSX.Element | null;
30
+ export declare function ReduxModal({ visible, onClose, onBack, onMinimize, enableSharedModalDimensions, inline, onActionSelect, }: ReduxModalProps): import("react").JSX.Element | null;
24
31
  /**
25
32
  * The same Redux tool as `ReduxModal`, rendered INLINE for hosts that place
26
33
  * tools in a panel rather than a modal. Exists so there is exactly one
@@ -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;;;;;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"}
1
+ {"version":3,"file":"ReduxModal.d.ts","sourceRoot":"","sources":["../../../../src/redux/components/ReduxModal.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyCH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,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;IACtC;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CAChD;AAgBD,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,EACnC,MAAc,EACd,cAAc,GACf,EAAE,eAAe,sCAmbjB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,KAAK,GAAE,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAM,+BAGpE"}
@@ -16,10 +16,71 @@
16
16
  */
17
17
  export declare const reduxSyncAdapter: {
18
18
  version: number;
19
- getSnapshot: () => import("../types").ReduxAction[];
19
+ /**
20
+ * WIRE-FORM snapshot: actions WITHOUT raw prevState/nextState trees.
21
+ * Shipping the trees serialized up to 25×2 full state trees through
22
+ * socket.emit per action — no guard on that path — which froze and
23
+ * native-OOM-crashed enterprise-scale apps whenever a dashboard was
24
+ * connected (root cause confirmed by patch-and-retest). Dashboards fetch
25
+ * trees on demand via `getActionDetail`; already-evicted entries keep the
26
+ * eviction sentinel so consumers can distinguish "fetchable" from "gone".
27
+ */
28
+ getSnapshot: () => {
29
+ id: string;
30
+ type: string;
31
+ payload?: unknown;
32
+ action: Record<string, unknown>;
33
+ timestamp: number;
34
+ prevState: unknown;
35
+ nextState: unknown;
36
+ duration?: number;
37
+ hasStateChange: boolean;
38
+ category: import("../types").ActionCategory;
39
+ sliceName: string | null;
40
+ actionName: string;
41
+ meta?: import("../types").ActionMeta;
42
+ error?: unknown;
43
+ payloadPreview: string;
44
+ diffSummary: string;
45
+ changedKeysCount: number;
46
+ isSlowAction: boolean;
47
+ }[];
20
48
  subscribe: (onChange: () => void) => () => void;
21
49
  actions: {
22
50
  clearEvents: () => void;
51
+ /**
52
+ * On-demand detail: one action's real prevState/nextState trees.
53
+ * The per-snapshot stream stays tree-free; this is the explicit,
54
+ * size-guarded channel for the detail pane.
55
+ */
56
+ getActionDetail: (params: unknown) => {
57
+ found: false;
58
+ reason: string;
59
+ id?: undefined;
60
+ evicted?: undefined;
61
+ prevState?: undefined;
62
+ nextState?: undefined;
63
+ payload?: undefined;
64
+ action?: undefined;
65
+ } | {
66
+ found: true;
67
+ id: string;
68
+ evicted: true;
69
+ reason: string;
70
+ prevState?: undefined;
71
+ nextState?: undefined;
72
+ payload?: undefined;
73
+ action?: undefined;
74
+ } | {
75
+ found: true;
76
+ id: string;
77
+ prevState: unknown;
78
+ nextState: unknown;
79
+ payload: unknown;
80
+ action: unknown;
81
+ reason?: undefined;
82
+ evicted?: undefined;
83
+ };
23
84
  /**
24
85
  * Compact current-state reader: the instrumented store's slice names, and
25
86
  * the full state tree only when `includeValues` is set (heavy).
@@ -1 +1 @@
1
- {"version":3,"file":"reduxSyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/redux/sync/reduxSyncAdapter.ts"],"names":[],"mappings":"AAsCA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,gBAAgB;;;0BAGL,MAAM,IAAI;;;QAK9B;;;WAGG;2BACgB,OAAO;;oBAlCZ,MAAM,EAAE;YACtB,4DAA4D;;;;;;YA6CxD,wEAAwE;;;;QAM5E,gEAAgE;2BAC7C,OAAO;;oBArDZ,MAAM,EAAE;YACtB,4DAA4D;;;;;;;;;CA8D/D,CAAC"}
1
+ {"version":3,"file":"reduxSyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/redux/sync/reduxSyncAdapter.ts"],"names":[],"mappings":"AAsDA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,gBAAgB;;IAE3B;;;;;;;;OAQG;;;;;;;;;;;;;;;;;;;;;0BAiBmB,MAAM,IAAI;;;QAK9B;;;;WAIG;kCACuB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA8BjC;;;WAGG;2BACgB,OAAO;;oBA7FZ,MAAM,EAAE;YACtB,4DAA4D;;;;;;YAwGxD,wEAAwE;;;;QAM5E,gEAAgE;2BAC7C,OAAO;;oBAhHZ,MAAM,EAAE;YACtB,4DAA4D;;;;;;;;;CAyH/D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"autoInstrument.d.ts","sourceRoot":"","sources":["../../../../src/redux/utils/autoInstrument.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAG7D,OAAO,EAKL,KAAK,gBAAgB,EACtB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,GACtB,MAAM,uBAAuB,CAAC;AAsB/B,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAMD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAGzD;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,qBAA0B,GAClC,MAAM,IAAI,CAuFZ;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAapD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,qBAA0B,GAClC,MAAM,IAAI,CAEZ;AASD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAoBhD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAOxD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,gBAAgB,GAAG,IAAI,CAExD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD"}
1
+ {"version":3,"file":"autoInstrument.d.ts","sourceRoot":"","sources":["../../../../src/redux/utils/autoInstrument.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAG7D,OAAO,EAKL,KAAK,gBAAgB,EACtB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,GACtB,MAAM,uBAAuB,CAAC;AAsB/B,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAMD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAGzD;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,qBAA0B,GAClC,MAAM,IAAI,CAuFZ;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAapD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,qBAA0B,GAClC,MAAM,IAAI,CAEZ;AASD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CA0BhD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAOxD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,gBAAgB,GAAG,IAAI,CAExD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD"}
@@ -1 +1 @@
1
- {"version":3,"file":"buoyReduxMiddleware.d.ts","sourceRoot":"","sources":["../../../../src/redux/utils/buoyReduxMiddleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC3E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;AAc3D;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAMD,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AACzD,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAczD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAOhD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAOxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAWnE;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,UA4BjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,0BAA+B,GACvC,UAAU,CAoCZ"}
1
+ {"version":3,"file":"buoyReduxMiddleware.d.ts","sourceRoot":"","sources":["../../../../src/redux/utils/buoyReduxMiddleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC3E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;AAc3D;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAMD,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AACzD,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAczD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAahD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAOxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAWnE;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,UA4BjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,0BAA+B,GACvC,UAAU,CAoCZ"}
@@ -2,6 +2,57 @@
2
2
  * Redux action store - captures and stores Redux Toolkit actions
3
3
  */
4
4
  import type { ReduxAction, ReduxFilter } from "../types";
5
+ /**
6
+ * How many of the most-recent actions keep their raw prevState/nextState
7
+ * trees. Older actions keep all diff metadata but their trees are replaced
8
+ * with STATE_EVICTED so a store switch can't pin hundreds of MB.
9
+ */
10
+ export declare const STATE_RETENTION_LIMIT = 25;
11
+ /**
12
+ * Sentinel installed in place of evicted state trees. A frozen plain object
13
+ * so existing detail viewers render it harmlessly; jumpToState refuses it.
14
+ */
15
+ export declare const STATE_EVICTED: Readonly<{
16
+ __buoyStateEvicted: true;
17
+ note: "Raw state retained for the 25 most recent actions only.";
18
+ }>;
19
+ /**
20
+ * Sentinel used in WIRE-FORM snapshots (sync to desktop/MCP) in place of raw
21
+ * prevState/nextState trees. Unlike STATE_EVICTED the trees still exist on
22
+ * the device — dashboards fetch them on demand via the adapter's
23
+ * `getActionDetail` action. Shipping them in every snapshot serialized up to
24
+ * 25×2 full state trees through socket.emit per action (no guard), which
25
+ * froze and native-OOM-crashed enterprise-scale apps whenever a dashboard
26
+ * was connected (confirmed by patch-and-retest on the affected app).
27
+ */
28
+ export declare const STATE_ON_DEVICE: Readonly<{
29
+ __buoyStateOnDevice: true;
30
+ note: "State trees stay on the device — fetched on demand via getActionDetail.";
31
+ }>;
32
+ /**
33
+ * Wire-form sentinel for an oversized action `payload`/`action`. A dispatched
34
+ * payload can itself be huge (rehydration actions carry the whole new state),
35
+ * so the per-snapshot stream ships `payloadPreview` + this marker instead;
36
+ * `getActionDetail` serves the real value on demand.
37
+ */
38
+ export declare const PAYLOAD_ON_DEVICE: Readonly<{
39
+ __buoyPayloadOnDevice: true;
40
+ note: "Payload stays on the device — fetched on demand via getActionDetail.";
41
+ }>;
42
+ /**
43
+ * True when a value is one of the state sentinels (evicted or on-device) —
44
+ * guards time travel and any consumer that needs a REAL tree.
45
+ */
46
+ export declare function isEvictedState(value: unknown): boolean;
47
+ /**
48
+ * Cheap early-abort size probe: is `value`'s JSON form definitely longer than
49
+ * `limit`? Bails the moment the running estimate exceeds the limit, so the
50
+ * cost is O(limit) — never O(payload). Used to avoid JSON.stringify-ing large
51
+ * payloads (a fulfilled thunk's payload can be megabytes) just to produce a
52
+ * 40-char preview; that full stringify ran on EVERY dispatch and blocked the
53
+ * JS thread at enterprise state sizes.
54
+ */
55
+ export declare function jsonLongerThan(value: unknown, limit: number): boolean;
5
56
  declare class ReduxActionStore {
6
57
  private actions;
7
58
  private listeners;
@@ -37,6 +88,12 @@ declare class ReduxActionStore {
37
88
  * the enabled flag so the UI's pause button freezes the mirror.
38
89
  */
39
90
  replaceActions(actions: ReduxAction[]): void;
91
+ /**
92
+ * Hydrate one mirrored action with real values (state trees, payload,
93
+ * action) fetched on demand from the device via the adapter's
94
+ * `getActionDetail` action. Remote-mirror (desktop) use only.
95
+ */
96
+ hydrateAction(id: string, patch: Partial<ReduxAction>): void;
40
97
  setEnabled(enabled: boolean): void;
41
98
  getEnabled(): boolean;
42
99
  subscribe(listener: (actions: ReduxAction[]) => void): () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"reduxActionStore.d.ts","sourceRoot":"","sources":["../../../../src/redux/utils/reduxActionStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAA8B,MAAM,UAAU,CAAC;AAiLrF,cAAM,gBAAgB;IACpB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,SAAS,CAAoD;IACrE,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,UAAU,CAAO;IACzB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,iBAAiB,CAAS;IAElC,SAAS,CACP,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,OAAO,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI;IAmDP,UAAU,IAAI,WAAW,EAAE;IAI3B,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAIlD,YAAY,IAAI,IAAI;IAYpB;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAWzC;;;;OAIG;IACH,cAAc,IAAI,IAAI;IAItB;;;;OAIG;IACH,mBAAmB,IAAI,OAAO;IAI9B;;;;OAIG;IACH,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;IAQ5C,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIlC,UAAU,IAAI,OAAO;IAIrB,SAAS,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI;IAOjE,OAAO,CAAC,eAAe;IAKvB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAQhC,QAAQ;;;;;;;IAuBR,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,EAAE;IAmBjD,oBAAoB,IAAI,MAAM,EAAE;IAIhC;;OAEG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE;IAMnD;;OAEG;IACH,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,EAAE;IAKpD;;OAEG;IACH,qBAAqB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM;IAOlD;;OAEG;IACH,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAe3D;;OAEG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAWvC;;;OAGG;IACH,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;CAkBlD;AAED,eAAO,MAAM,gBAAgB,kBAAyB,CAAC"}
1
+ {"version":3,"file":"reduxActionStore.d.ts","sourceRoot":"","sources":["../../../../src/redux/utils/reduxActionStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAA8B,MAAM,UAAU,CAAC;AAErF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;GAGG;AACH,eAAO,MAAM,aAAa;;;EAGxB,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;EAG1B,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;;EAG5B,CAAC;AAEH;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAItD;AAwDD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CA2BrE;AA8HD,cAAM,gBAAgB;IACpB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,SAAS,CAAoD;IACrE,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,UAAU,CAAO;IACzB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,iBAAiB,CAAS;IAElC,SAAS,CACP,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,OAAO,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI;IAyEP,UAAU,IAAI,WAAW,EAAE;IAI3B,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAIlD,YAAY,IAAI,IAAI;IAYpB;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAWzC;;;;OAIG;IACH,cAAc,IAAI,IAAI;IAItB;;;;OAIG;IACH,mBAAmB,IAAI,OAAO;IAI9B;;;;OAIG;IACH,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;IAuB5C;;;;OAIG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI;IAW5D,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIlC,UAAU,IAAI,OAAO;IAIrB,SAAS,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI;IAOjE,OAAO,CAAC,eAAe;IAKvB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAQhC,QAAQ;;;;;;;IAuBR,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,EAAE;IAmBjD,oBAAoB,IAAI,MAAM,EAAE;IAIhC;;OAEG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE;IAMnD;;OAEG;IACH,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,EAAE;IAKpD;;OAEG;IACH,qBAAqB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM;IAOlD;;OAEG;IACH,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAe3D;;OAEG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAWvC;;;OAGG;IACH,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;CAkBlD;AAED,eAAO,MAAM,gBAAgB,kBAAyB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/redux",
3
- "version": "7.0.9",
3
+ "version": "7.0.11",
4
4
  "description": "Redux DevTools inside your React Native app — action log, state diffs & time travel with zero store changes. Part of Buoy devtools.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -29,9 +29,9 @@
29
29
  "**/devtoolsHook.ts"
30
30
  ],
31
31
  "dependencies": {
32
- "@buoy-gg/shared-ui": "^7.0.9",
33
- "@buoy-gg/license": "^7.0.9",
34
- "@buoy-gg/floating-tools-core": "^7.0.9"
32
+ "@buoy-gg/shared-ui": "^7.0.11",
33
+ "@buoy-gg/floating-tools-core": "^7.0.11",
34
+ "@buoy-gg/license": "^7.0.11"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@reduxjs/toolkit": ">=1.9.0",