@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.
- package/lib/commonjs/redux/components/ReduxModal.js +13 -1
- package/lib/commonjs/redux/sync/reduxSyncAdapter.js +80 -2
- package/lib/commonjs/redux/utils/autoInstrument.js +4 -0
- package/lib/commonjs/redux/utils/buoyReduxMiddleware.js +4 -0
- package/lib/commonjs/redux/utils/reduxActionStore.js +141 -2
- package/lib/module/redux/components/ReduxModal.js +14 -2
- package/lib/module/redux/sync/reduxSyncAdapter.js +82 -3
- package/lib/module/redux/utils/autoInstrument.js +5 -1
- package/lib/module/redux/utils/buoyReduxMiddleware.js +5 -1
- package/lib/module/redux/utils/reduxActionStore.js +138 -2
- package/lib/typescript/redux/components/ReduxModal.d.ts +8 -1
- package/lib/typescript/redux/components/ReduxModal.d.ts.map +1 -1
- package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts +62 -1
- package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/redux/utils/autoInstrument.d.ts.map +1 -1
- package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts.map +1 -1
- package/lib/typescript/redux/utils/reduxActionStore.d.ts +57 -0
- package/lib/typescript/redux/utils/reduxActionStore.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -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:
|
|
47
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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:
|
|
43
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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":"
|
|
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,
|
|
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,
|
|
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;
|
|
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.
|
|
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.
|
|
33
|
-
"@buoy-gg/
|
|
34
|
-
"@buoy-gg/
|
|
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",
|