@buoy-gg/redux 7.0.10 → 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/reduxActionStore.js +67 -4
- package/lib/module/redux/components/ReduxModal.js +14 -2
- package/lib/module/redux/sync/reduxSyncAdapter.js +82 -3
- package/lib/module/redux/utils/reduxActionStore.js +66 -4
- 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/reduxActionStore.d.ts +42 -1
- 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).
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.STATE_RETENTION_LIMIT = exports.STATE_EVICTED = void 0;
|
|
6
|
+
exports.STATE_RETENTION_LIMIT = exports.STATE_ON_DEVICE = exports.STATE_EVICTED = exports.PAYLOAD_ON_DEVICE = void 0;
|
|
7
7
|
exports.isEvictedState = isEvictedState;
|
|
8
|
+
exports.jsonLongerThan = jsonLongerThan;
|
|
8
9
|
exports.reduxActionStore = void 0;
|
|
9
10
|
/**
|
|
10
11
|
* Redux action store - captures and stores Redux Toolkit actions
|
|
@@ -26,9 +27,39 @@ const STATE_EVICTED = exports.STATE_EVICTED = Object.freeze({
|
|
|
26
27
|
note: `Raw state retained for the ${STATE_RETENTION_LIMIT} most recent actions only.`
|
|
27
28
|
});
|
|
28
29
|
|
|
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
|
+
*/
|
|
30
59
|
function isEvictedState(value) {
|
|
31
|
-
|
|
60
|
+
if (typeof value !== "object" || value === null) return false;
|
|
61
|
+
const v = value;
|
|
62
|
+
return v.__buoyStateEvicted === true || v.__buoyStateOnDevice === true;
|
|
32
63
|
}
|
|
33
64
|
|
|
34
65
|
// ============================================
|
|
@@ -356,7 +387,39 @@ class ReduxActionStore {
|
|
|
356
387
|
if (!this.isEnabled) {
|
|
357
388
|
return;
|
|
358
389
|
}
|
|
359
|
-
|
|
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)];
|
|
360
423
|
this.notifyListeners();
|
|
361
424
|
}
|
|
362
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).
|
|
@@ -20,9 +20,39 @@ export const STATE_EVICTED = Object.freeze({
|
|
|
20
20
|
note: `Raw state retained for the ${STATE_RETENTION_LIMIT} most recent actions only.`
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
/**
|
|
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
|
+
*/
|
|
24
52
|
export function isEvictedState(value) {
|
|
25
|
-
|
|
53
|
+
if (typeof value !== "object" || value === null) return false;
|
|
54
|
+
const v = value;
|
|
55
|
+
return v.__buoyStateEvicted === true || v.__buoyStateOnDevice === true;
|
|
26
56
|
}
|
|
27
57
|
|
|
28
58
|
// ============================================
|
|
@@ -85,7 +115,7 @@ function parseActionType(type) {
|
|
|
85
115
|
* 40-char preview; that full stringify ran on EVERY dispatch and blocked the
|
|
86
116
|
* JS thread at enterprise state sizes.
|
|
87
117
|
*/
|
|
88
|
-
function jsonLongerThan(value, limit) {
|
|
118
|
+
export function jsonLongerThan(value, limit) {
|
|
89
119
|
let bytes = 0;
|
|
90
120
|
const stack = [value];
|
|
91
121
|
const seen = new Set();
|
|
@@ -350,7 +380,39 @@ class ReduxActionStore {
|
|
|
350
380
|
if (!this.isEnabled) {
|
|
351
381
|
return;
|
|
352
382
|
}
|
|
353
|
-
|
|
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)];
|
|
354
416
|
this.notifyListeners();
|
|
355
417
|
}
|
|
356
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"}
|
|
@@ -16,8 +16,43 @@ export declare const STATE_EVICTED: Readonly<{
|
|
|
16
16
|
__buoyStateEvicted: true;
|
|
17
17
|
note: "Raw state retained for the 25 most recent actions only.";
|
|
18
18
|
}>;
|
|
19
|
-
/**
|
|
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
|
+
*/
|
|
20
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;
|
|
21
56
|
declare class ReduxActionStore {
|
|
22
57
|
private actions;
|
|
23
58
|
private listeners;
|
|
@@ -53,6 +88,12 @@ declare class ReduxActionStore {
|
|
|
53
88
|
* the enabled flag so the UI's pause button freezes the mirror.
|
|
54
89
|
*/
|
|
55
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;
|
|
56
97
|
setEnabled(enabled: boolean): void;
|
|
57
98
|
getEnabled(): boolean;
|
|
58
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;AAErF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;GAGG;AACH,eAAO,MAAM,aAAa;;;EAGxB,CAAC;AAEH,
|
|
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/floating-tools-core": "^7.0.
|
|
34
|
-
"@buoy-gg/license": "^7.0.
|
|
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",
|