@buoy-gg/redux 7.0.41 → 7.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +11 -106
  2. package/lib/commonjs/index.js +1 -256
  3. package/lib/commonjs/preset.js +1 -90
  4. package/lib/commonjs/redux/components/ReduxActionButton.js +1 -131
  5. package/lib/commonjs/redux/components/ReduxActionDetailContent.js +1 -437
  6. package/lib/commonjs/redux/components/ReduxActionDetailView.js +1 -401
  7. package/lib/commonjs/redux/components/ReduxActionInfoView.js +1 -836
  8. package/lib/commonjs/redux/components/ReduxActionItem.js +1 -337
  9. package/lib/commonjs/redux/components/ReduxAutoInstrument.js +1 -20
  10. package/lib/commonjs/redux/components/ReduxDetailViewToggle.js +1 -50
  11. package/lib/commonjs/redux/components/ReduxIcon.js +1 -18
  12. package/lib/commonjs/redux/components/ReduxModal.js +1 -703
  13. package/lib/commonjs/redux/hooks/reduxPeer.js +1 -17
  14. package/lib/commonjs/redux/hooks/reduxPeer.web.js +1 -10
  15. package/lib/commonjs/redux/hooks/useAutoInstrumentRedux.js +1 -177
  16. package/lib/commonjs/redux/hooks/useReduxActions.js +1 -58
  17. package/lib/commonjs/redux/sync/reduxSyncAdapter.js +1 -191
  18. package/lib/commonjs/redux/types/index.js +1 -1
  19. package/lib/commonjs/redux/utils/activeStoreRegistry.js +1 -104
  20. package/lib/commonjs/redux/utils/autoInstrument.js +1 -231
  21. package/lib/commonjs/redux/utils/buoyReduxMiddleware.js +1 -202
  22. package/lib/commonjs/redux/utils/createReduxHistoryAdapter.js +1 -146
  23. package/lib/commonjs/redux/utils/devtoolsHook.js +1 -152
  24. package/lib/commonjs/redux/utils/reduxActionStore.js +1 -473
  25. package/lib/commonjs/redux/utils/snapshotProvider.js +1 -158
  26. package/lib/commonjs/redux/utils/testIDs.js +1 -75
  27. package/lib/module/index.js +1 -125
  28. package/lib/module/preset.js +1 -87
  29. package/lib/module/redux/components/ReduxActionButton.js +1 -128
  30. package/lib/module/redux/components/ReduxActionDetailContent.js +1 -433
  31. package/lib/module/redux/components/ReduxActionDetailView.js +1 -397
  32. package/lib/module/redux/components/ReduxActionInfoView.js +1 -831
  33. package/lib/module/redux/components/ReduxActionItem.js +1 -333
  34. package/lib/module/redux/components/ReduxAutoInstrument.js +1 -17
  35. package/lib/module/redux/components/ReduxDetailViewToggle.js +1 -45
  36. package/lib/module/redux/components/ReduxIcon.js +1 -8
  37. package/lib/module/redux/components/ReduxModal.js +1 -698
  38. package/lib/module/redux/hooks/reduxPeer.js +1 -13
  39. package/lib/module/redux/hooks/reduxPeer.web.js +1 -6
  40. package/lib/module/redux/hooks/useAutoInstrumentRedux.js +1 -173
  41. package/lib/module/redux/hooks/useReduxActions.js +1 -54
  42. package/lib/module/redux/sync/reduxSyncAdapter.js +1 -188
  43. package/lib/module/redux/types/index.js +1 -1
  44. package/lib/module/redux/utils/activeStoreRegistry.js +1 -92
  45. package/lib/module/redux/utils/autoInstrument.js +1 -201
  46. package/lib/module/redux/utils/buoyReduxMiddleware.js +1 -188
  47. package/lib/module/redux/utils/createReduxHistoryAdapter.js +1 -142
  48. package/lib/module/redux/utils/devtoolsHook.js +1 -145
  49. package/lib/module/redux/utils/reduxActionStore.js +1 -465
  50. package/lib/module/redux/utils/snapshotProvider.js +1 -157
  51. package/lib/module/redux/utils/testIDs.js +1 -68
  52. package/lib/web/index.mjs +1 -2
  53. package/package.json +4 -4
  54. package/lib/typescript/index.d.ts.map +0 -1
  55. package/lib/typescript/preset.d.ts.map +0 -1
  56. package/lib/typescript/redux/components/ReduxActionButton.d.ts.map +0 -1
  57. package/lib/typescript/redux/components/ReduxActionDetailContent.d.ts.map +0 -1
  58. package/lib/typescript/redux/components/ReduxActionDetailView.d.ts.map +0 -1
  59. package/lib/typescript/redux/components/ReduxActionInfoView.d.ts.map +0 -1
  60. package/lib/typescript/redux/components/ReduxActionItem.d.ts.map +0 -1
  61. package/lib/typescript/redux/components/ReduxAutoInstrument.d.ts.map +0 -1
  62. package/lib/typescript/redux/components/ReduxDetailViewToggle.d.ts.map +0 -1
  63. package/lib/typescript/redux/components/ReduxIcon.d.ts.map +0 -1
  64. package/lib/typescript/redux/components/ReduxModal.d.ts.map +0 -1
  65. package/lib/typescript/redux/hooks/reduxPeer.d.ts.map +0 -1
  66. package/lib/typescript/redux/hooks/reduxPeer.web.d.ts.map +0 -1
  67. package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts.map +0 -1
  68. package/lib/typescript/redux/hooks/useReduxActions.d.ts.map +0 -1
  69. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts.map +0 -1
  70. package/lib/typescript/redux/types/index.d.ts.map +0 -1
  71. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts.map +0 -1
  72. package/lib/typescript/redux/utils/autoInstrument.d.ts.map +0 -1
  73. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts.map +0 -1
  74. package/lib/typescript/redux/utils/createReduxHistoryAdapter.d.ts.map +0 -1
  75. package/lib/typescript/redux/utils/devtoolsHook.d.ts.map +0 -1
  76. package/lib/typescript/redux/utils/reduxActionStore.d.ts.map +0 -1
  77. package/lib/typescript/redux/utils/snapshotProvider.d.ts.map +0 -1
  78. package/lib/typescript/redux/utils/testIDs.d.ts.map +0 -1
@@ -1,152 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.buoyDevtoolsEnhancer = void 0;
7
- exports.claimDevtoolsHook = claimDevtoolsHook;
8
- exports.isDevtoolsHookClaimed = isDevtoolsHookClaimed;
9
- var _reduxActionStore = require("./reduxActionStore");
10
- var _buoyReduxMiddleware = require("./buoyReduxMiddleware");
11
- var _activeStoreRegistry = require("./activeStoreRegistry");
12
- /**
13
- * Store-creation-time capture via the OFFICIAL Redux DevTools hook.
14
- *
15
- * `configureStore` defaults `devTools: true` and, at RTK module-evaluation
16
- * time, snapshots `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` if defined.
17
- * That global doesn't exist in React Native — so Buoy claims it (dev builds
18
- * only, and only when nothing else has). This makes Buoy a real store
19
- * ENHANCER, present from the store's birth:
20
- *
21
- * - FULL capture: actions dispatched from inside middleware/thunks (RTK
22
- * `pending`/`fulfilled`/`rejected`, RTK Query) hit our dispatch because the
23
- * enhancer sits BELOW applyMiddleware in the compose chain.
24
- * - Real time travel: we wrap the reducer, so BUOY_JUMP_TO_STATE actually
25
- * jumps — and replaceReducer is re-wrapped, so hosts that inject reducers
26
- * dynamically keep working.
27
- * - Nothing is monkey-patched: this is the integration point Redux itself
28
- * ships for devtools (same one react-native-debugger used).
29
- *
30
- * Race note: RTK snapshots the global when ITS module first evaluates, so this
31
- * module must load before `@reduxjs/toolkit` does. `@buoy-gg/core` requires us
32
- * at module scope, which usually wins; when it doesn't, the dispatch-patch
33
- * fallback in autoInstrument.ts still binds at first mount. Apps that want a
34
- * guarantee put `import '@buoy-gg/redux';` as the FIRST import of their entry.
35
- */
36
-
37
- /** Standard redux compose. */
38
- function composeFns(...funcs) {
39
- if (funcs.length === 0) return arg => arg;
40
- if (funcs.length === 1) return funcs[0];
41
- return funcs.reduce((a, b) => (...args) => a(b(...args)));
42
- }
43
-
44
- /** Wrap a reducer so BUOY_JUMP_TO_STATE performs a real state jump. */
45
- function wrapReducer(reducer) {
46
- // Announce the capability, not just perform it: with nothing wrapped, a jump
47
- // is dispatched, ignored, and silent — so the UI needs to know in advance
48
- // whether the button it is drawing can keep its promise.
49
- (0, _activeStoreRegistry.markTimeTravelReducer)();
50
- return (state, action) => {
51
- if (action && action.type === _buoyReduxMiddleware.BUOY_JUMP_TO_STATE) {
52
- return action.payload;
53
- }
54
- return reducer(state, action);
55
- };
56
- }
57
-
58
- /** Should this action be recorded? (Mirrors autoInstrument's filters.) */
59
- function isRecordable(action) {
60
- if (typeof action !== "object" || action === null) return false;
61
- const type = action.type;
62
- if (typeof type !== "string") return false;
63
- return !type.startsWith("@@buoy/") && !type.startsWith("@@redux/");
64
- }
65
-
66
- /**
67
- * The Buoy store enhancer. Innermost in the compose chain (below
68
- * applyMiddleware), so every action that reaches the reducer — including ones
69
- * dispatched from inside middleware and thunks — passes through here.
70
- */
71
- const buoyDevtoolsEnhancer = createStore => (reducer, preloadedState) => {
72
- const store = createStore(wrapReducer(reducer), preloadedState);
73
- const dispatch = action => {
74
- // If the explicit middleware is installed it already records — the
75
- // enhancer stays passive to avoid double capture.
76
- if (!isRecordable(action) || (0, _buoyReduxMiddleware.isMiddlewareActive)()) {
77
- return store.dispatch(action);
78
- }
79
- const startTime = performance.now();
80
- const prevState = store.getState();
81
- const result = store.dispatch(action);
82
- _reduxActionStore.reduxActionStore.addAction(action, prevState, store.getState(), performance.now() - startTime);
83
- return result;
84
- };
85
- const enhanced = {
86
- ...store,
87
- dispatch,
88
- // Keep time travel alive across dynamic reducer injection.
89
- replaceReducer: next => store.replaceReducer(wrapReducer(next)),
90
- [_activeStoreRegistry.ENHANCER_SYMBOL]: true
91
- };
92
- (0, _activeStoreRegistry.registerActiveStore)(enhanced, "enhancer");
93
- return enhanced;
94
- };
95
-
96
- /**
97
- * Implements the `__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` calling conventions:
98
- * - `compose(options)(...enhancers)` — what RTK's configureStore does
99
- * - `compose(...enhancers)` — classic createStore usage from the Redux docs
100
- */
101
- exports.buoyDevtoolsEnhancer = buoyDevtoolsEnhancer;
102
- function buoyComposeWithDevTools(...args) {
103
- if (args.length === 0) {
104
- return composeFns(buoyDevtoolsEnhancer);
105
- }
106
- if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) {
107
- // Options form → return a compose function.
108
- return (...enhancers) => composeFns(...enhancers, buoyDevtoolsEnhancer);
109
- }
110
- return composeFns(...args, buoyDevtoolsEnhancer);
111
- }
112
- let claimed = false;
113
-
114
- /**
115
- * Claim the DevTools COMPOSE global — dev builds only, and never over an
116
- * existing claimant (e.g. the real browser extension on web).
117
- *
118
- * Idempotent; runs at module scope so simply importing `@buoy-gg/redux` (which
119
- * `@buoy-gg/core` does at module scope) performs the claim as early as
120
- * possible.
121
- *
122
- * DO NOT claim `__REDUX_DEVTOOLS_EXTENSION__` here — ever. That global is
123
- * feature-detected BY PRESENCE by non-redux libraries (zustand's devtools
124
- * middleware, valtio, jotai-devtools) which then call the extension OBJECT
125
- * API (`.connect()` etc.) with no shape check. A bare function satisfies only
126
- * redux's legacy calling convention, so claiming it crashes any host app
127
- * whose zustand store uses `devtools(...)` — at store-module evaluation, so
128
- * the app doesn't boot, and the surfaced error is a misleading React
129
- * "Element type is invalid" from whatever lazy component imports next.
130
- * (Shipped in 7.0.6/7.0.7; broke a large enterprise app; removed in the next
131
- * patch.) Absent, all those libraries no-op gracefully. The COMPOSE global is
132
- * safe because every known consumer CALLS it as a function — that is its
133
- * whole contract — and it covers the dominant path (RTK's configureStore
134
- * default). The legacy `createStore(reducer, __REDUX_DEVTOOLS_EXTENSION__())`
135
- * pattern simply falls back to Buoy's dispatch-patch instrumentation.
136
- */
137
- function claimDevtoolsHook() {
138
- const isDev = typeof __DEV__ !== "undefined" && __DEV__;
139
- if (!isDev) return false;
140
- const g = globalThis;
141
- if (!g.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
142
- g.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = buoyComposeWithDevTools;
143
- claimed = true;
144
- }
145
- return claimed;
146
- }
147
-
148
- /** Whether Buoy owns the DevTools compose global (i.e. won the load race). */
149
- function isDevtoolsHookClaimed() {
150
- return claimed;
151
- }
152
- claimDevtoolsHook();
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.buoyDevtoolsEnhancer=void 0;exports.claimDevtoolsHook=claimDevtoolsHook;exports.isDevtoolsHookClaimed=isDevtoolsHookClaimed;var _reduxActionStore=require("./reduxActionStore");var _buoyReduxMiddleware=require("./buoyReduxMiddleware");var _activeStoreRegistry=require("./activeStoreRegistry");function composeFns(...e){if(e.length===0)return t=>t;if(e.length===1)return e[0];return e.reduce((t,r)=>(...o)=>t(r(...o)))}function wrapReducer(e){(0,_activeStoreRegistry.markTimeTravelReducer)();return(t,r)=>{if(r&&r.type===_buoyReduxMiddleware.BUOY_JUMP_TO_STATE){return r.payload}return e(t,r)}}function isRecordable(e){if(typeof e!=="object"||e===null)return false;const t=e.type;if(typeof t!=="string")return false;return!t.startsWith("@@buoy/")&&!t.startsWith("@@redux/")}const buoyDevtoolsEnhancer=e=>(t,r)=>{const o=e(wrapReducer(t),r);const i=n=>{if(!isRecordable(n)||(0,_buoyReduxMiddleware.isMiddlewareActive)()){return o.dispatch(n)}const s=performance.now();const c=o.getState();const l=o.dispatch(n);_reduxActionStore.reduxActionStore.addAction(n,c,o.getState(),performance.now()-s);return l};const u={...o,dispatch:i,replaceReducer:n=>o.replaceReducer(wrapReducer(n)),[_activeStoreRegistry.ENHANCER_SYMBOL]:true};(0,_activeStoreRegistry.registerActiveStore)(u,"enhancer");return u};exports.buoyDevtoolsEnhancer=buoyDevtoolsEnhancer;function buoyComposeWithDevTools(...e){if(e.length===0){return composeFns(buoyDevtoolsEnhancer)}if(e.length===1&&typeof e[0]==="object"&&e[0]!==null){return(...t)=>composeFns(...t,buoyDevtoolsEnhancer)}return composeFns(...e,buoyDevtoolsEnhancer)}let claimed=false;function claimDevtoolsHook(){const e=typeof __DEV__!=="undefined"&&__DEV__;if(!e)return false;const t=globalThis;if(!t.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__){t.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__=buoyComposeWithDevTools;claimed=true}return claimed}function isDevtoolsHookClaimed(){return claimed}claimDevtoolsHook();
@@ -1,473 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
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.isPayloadOnDevice = isPayloadOnDevice;
9
- exports.jsonLongerThan = jsonLongerThan;
10
- exports.reduxActionStore = void 0;
11
- var _sharedUi = require("@buoy-gg/shared-ui");
12
- /**
13
- * Redux action store - captures and stores Redux Toolkit actions
14
- */
15
-
16
- /**
17
- * How many of the most-recent actions keep their raw prevState/nextState
18
- * trees. Older actions keep all diff metadata but their trees are replaced
19
- * with STATE_EVICTED so a store switch can't pin hundreds of MB.
20
- */
21
- const STATE_RETENTION_LIMIT = exports.STATE_RETENTION_LIMIT = 25;
22
-
23
- /**
24
- * Sentinel installed in place of evicted state trees. A frozen plain object
25
- * so existing detail viewers render it harmlessly; jumpToState refuses it.
26
- */
27
- const STATE_EVICTED = exports.STATE_EVICTED = Object.freeze({
28
- __buoyStateEvicted: true,
29
- note: `Raw state retained for the ${STATE_RETENTION_LIMIT} most recent actions only.`
30
- });
31
-
32
- /**
33
- * Sentinel used in WIRE-FORM snapshots (sync to desktop/MCP) in place of raw
34
- * prevState/nextState trees. Unlike STATE_EVICTED the trees still exist on
35
- * the device — dashboards fetch them on demand via the adapter's
36
- * `getActionDetail` action. Shipping them in every snapshot serialized up to
37
- * 25×2 full state trees through socket.emit per action (no guard), which
38
- * froze and native-OOM-crashed enterprise-scale apps whenever a dashboard
39
- * was connected (confirmed by patch-and-retest on the affected app).
40
- */
41
- const STATE_ON_DEVICE = exports.STATE_ON_DEVICE = Object.freeze({
42
- __buoyStateOnDevice: true,
43
- note: "State trees stay on the device — fetched on demand via getActionDetail."
44
- });
45
-
46
- /**
47
- * Wire-form sentinel for an oversized action `payload`/`action`. A dispatched
48
- * payload can itself be huge (rehydration actions carry the whole new state),
49
- * so the per-snapshot stream ships `payloadPreview` + this marker instead;
50
- * `getActionDetail` serves the real value on demand.
51
- */
52
- const PAYLOAD_ON_DEVICE = exports.PAYLOAD_ON_DEVICE = Object.freeze({
53
- __buoyPayloadOnDevice: true,
54
- note: "Payload stays on the device — fetched on demand via getActionDetail."
55
- });
56
-
57
- /** True when a wire-form value is the payload-on-device sentinel. */
58
- function isPayloadOnDevice(value) {
59
- return typeof value === "object" && value !== null && value.__buoyPayloadOnDevice === true;
60
- }
61
-
62
- /**
63
- * True when a value is one of the state sentinels (evicted or on-device) —
64
- * guards time travel and any consumer that needs a REAL tree.
65
- */
66
- function isEvictedState(value) {
67
- if (typeof value !== "object" || value === null) return false;
68
- const v = value;
69
- return v.__buoyStateEvicted === true || v.__buoyStateOnDevice === true;
70
- }
71
-
72
- // ============================================
73
- // Helper Functions for Enhanced Data
74
- // ============================================
75
-
76
- /**
77
- * Get action category from type string
78
- */
79
- function getActionCategory(type) {
80
- if (type.startsWith("@@")) return "internal";
81
- if (type.includes("/pending")) return "pending";
82
- if (type.includes("/fulfilled")) return "fulfilled";
83
- if (type.includes("/rejected")) return "rejected";
84
- if (type.includes("/executeQuery")) return "query";
85
- if (type.includes("/executeMutation")) return "mutation";
86
- return "action";
87
- }
88
-
89
- /**
90
- * Parse action type to extract slice and action names
91
- */
92
- function parseActionType(type) {
93
- // Handle internal actions
94
- if (type.startsWith("@@")) {
95
- const parts = type.split("/");
96
- return {
97
- sliceName: null,
98
- actionName: parts[parts.length - 1] || type
99
- };
100
- }
101
-
102
- // Handle RTK pattern: "sliceName/actionName" or "sliceName/actionName/status"
103
- const parts = type.split("/");
104
- if (parts.length >= 2) {
105
- const sliceName = parts[0];
106
- // For async thunks, action name might be "fetchUser/pending" -> we want "fetchUser"
107
- let actionName = parts[1];
108
-
109
- // If it's an async status, use the base action name
110
- if (parts.length >= 3 && ["pending", "fulfilled", "rejected"].includes(parts[parts.length - 1])) {
111
- actionName = parts.slice(1, -1).join("/");
112
- }
113
- return {
114
- sliceName,
115
- actionName: actionName.toUpperCase()
116
- };
117
- }
118
- return {
119
- sliceName: null,
120
- actionName: type.toUpperCase()
121
- };
122
- }
123
-
124
- /**
125
- * Cheap early-abort size probe: is `value`'s JSON form definitely longer than
126
- * `limit`? Bails the moment the running estimate exceeds the limit, so the
127
- * cost is O(limit) — never O(payload). Used to avoid JSON.stringify-ing large
128
- * payloads (a fulfilled thunk's payload can be megabytes) just to produce a
129
- * 40-char preview; that full stringify ran on EVERY dispatch and blocked the
130
- * JS thread at enterprise state sizes.
131
- */
132
- function jsonLongerThan(value, limit) {
133
- return (0, _sharedUi.approxJsonSize)(value, limit).bytes > limit;
134
- }
135
-
136
- /**
137
- * Format payload preview for display
138
- */
139
- function formatPayloadPreview(payload, maxLength = 40) {
140
- if (payload === undefined) return "";
141
- if (payload === null) return "null";
142
- try {
143
- if (typeof payload === "string") {
144
- return payload.length > maxLength ? `"${payload.slice(0, maxLength - 3)}..."` : `"${payload}"`;
145
- }
146
- if (typeof payload === "number" || typeof payload === "boolean") {
147
- return String(payload);
148
- }
149
- if (Array.isArray(payload)) {
150
- if (payload.length === 0) return "[]";
151
- // Probe cheaply first — only stringify payloads that can actually fit.
152
- if (jsonLongerThan(payload, maxLength)) return `[${payload.length} items]`;
153
- const preview = JSON.stringify(payload);
154
- return preview.length > maxLength ? `[${payload.length} items]` : preview;
155
- }
156
- if (typeof payload === "object") {
157
- const keys = Object.keys(payload);
158
- if (keys.length === 0) return "{}";
159
-
160
- // Probe cheaply first — only stringify payloads that can actually fit.
161
- if (jsonLongerThan(payload, maxLength)) return `{ ${keys.length} keys }`;
162
- const preview = JSON.stringify(payload);
163
- if (preview.length <= maxLength) return preview;
164
-
165
- // Show key count
166
- return `{ ${keys.length} keys }`;
167
- }
168
- return String(payload).slice(0, maxLength);
169
- } catch {
170
- return "[complex]";
171
- }
172
- }
173
-
174
- /**
175
- * Calculate state diff summary
176
- */
177
- function getStateDiffSummary(prevState, nextState) {
178
- if (prevState === nextState) {
179
- return {
180
- summary: "no change",
181
- changedCount: 0
182
- };
183
- }
184
-
185
- // Handle non-objects
186
- if (typeof prevState !== "object" || typeof nextState !== "object" || prevState === null || nextState === null) {
187
- return {
188
- summary: "changed",
189
- changedCount: 1
190
- };
191
- }
192
- const prevKeys = Object.keys(prevState);
193
- const nextKeys = Object.keys(nextState);
194
- const added = nextKeys.filter(k => !prevKeys.includes(k)).length;
195
- const removed = prevKeys.filter(k => !nextKeys.includes(k)).length;
196
-
197
- // Check which existing keys changed
198
- let changed = 0;
199
- for (const key of prevKeys) {
200
- if (nextKeys.includes(key) && prevState[key] !== nextState[key]) {
201
- changed++;
202
- }
203
- }
204
- const parts = [];
205
- if (added > 0) parts.push(`+${added}`);
206
- if (removed > 0) parts.push(`-${removed}`);
207
- if (changed > 0) parts.push(`~${changed}`);
208
- const totalChanged = added + removed + changed;
209
- if (parts.length === 0) {
210
- return {
211
- summary: "nested change",
212
- changedCount: 1
213
- };
214
- }
215
- return {
216
- summary: `${parts.join(" ")} ${totalChanged === 1 ? "key" : "keys"}`,
217
- changedCount: totalChanged
218
- };
219
- }
220
-
221
- /**
222
- * Extract RTK async thunk metadata
223
- */
224
- function extractMeta(action) {
225
- const meta = action.meta;
226
- if (!meta) return undefined;
227
- return {
228
- requestId: meta.requestId,
229
- requestStatus: meta.requestStatus,
230
- arg: meta.arg,
231
- rejectedWithValue: meta.rejectedWithValue,
232
- aborted: meta.aborted,
233
- condition: meta.condition
234
- };
235
- }
236
-
237
- // ============================================
238
- // Redux Action Store
239
- // ============================================
240
-
241
- class ReduxActionStore extends _sharedUi.ChangeLog {
242
- addAction(action, prevState, nextState, duration) {
243
- if (!this.canRecord()) {
244
- return;
245
- }
246
- const actionType = action.type || "";
247
-
248
- // Skip actions without a proper type (e.g., thunk functions)
249
- if (!actionType) {
250
- return;
251
- }
252
- const hasStateChange = prevState !== nextState;
253
-
254
- // Enhanced data extraction
255
- const category = getActionCategory(actionType);
256
- const {
257
- sliceName,
258
- actionName
259
- } = parseActionType(actionType);
260
- const meta = extractMeta(action);
261
- const payloadPreview = formatPayloadPreview(action.payload);
262
- const {
263
- summary: diffSummary,
264
- changedCount: changedKeysCount
265
- } = getStateDiffSummary(prevState, nextState);
266
- const isSlowAction = (duration ?? 0) > 16; // Frame budget
267
-
268
- const reduxAction = {
269
- id: this.nextId(),
270
- type: actionType,
271
- payload: action.payload,
272
- action,
273
- timestamp: Date.now(),
274
- prevState,
275
- nextState,
276
- duration,
277
- hasStateChange,
278
- // Enhanced fields
279
- category,
280
- sliceName,
281
- actionName,
282
- meta,
283
- error: action.error,
284
- payloadPreview,
285
- diffSummary,
286
- changedKeysCount,
287
- isSlowAction
288
- };
289
- this.push(reduxAction);
290
-
291
- // Bound raw-state retention. Every action holds prevState/nextState by
292
- // reference; with structural sharing that's normally cheap, but when a
293
- // host replaces large slices wholesale (store switch / rehydration),
294
- // each retained action pins a DISTINCT multi-MB subtree — measured 88MB
295
- // pinned by 50 slice-replacing actions, enough to OOM-kill enterprise
296
- // apps. Detail view + time travel realistically use recent actions, so
297
- // older ones keep their diff metadata but drop the raw trees.
298
- for (let i = STATE_RETENTION_LIMIT; i < this.items.length; i++) {
299
- const a = this.items[i];
300
- if (a.prevState !== STATE_EVICTED) {
301
- a.prevState = STATE_EVICTED;
302
- a.nextState = STATE_EVICTED;
303
- } else {
304
- break; // older entries already evicted (list is newest-first)
305
- }
306
- }
307
- this.notifyListeners();
308
- }
309
- getActions() {
310
- return this.getAll();
311
- }
312
- getActionById(id) {
313
- return this.getById(id);
314
- }
315
- clearActions() {
316
- this.clear();
317
- }
318
-
319
- // ============================================
320
- // Remote Mirror Mode
321
- // ============================================
322
-
323
- /**
324
- * Replace the entire action list and notify listeners. Used in remote
325
- * mirror mode where full snapshots arrive from a synced device. Respects
326
- * the enabled flag so the UI's pause button freezes the mirror.
327
- */
328
- replaceActions(actions) {
329
- if (!this.getEnabled()) {
330
- return;
331
- }
332
- // Preserve any trees a remote mirror already hydrated via
333
- // replaceActionStates — wire-form snapshots always carry markers, and a
334
- // full snapshot arriving after a detail fetch must not blank the detail
335
- // pane back out.
336
- const hydrated = new Map();
337
- for (const existing of this.items) {
338
- if (existing.prevState !== undefined && !isEvictedState(existing.prevState)) {
339
- hydrated.set(existing.id, existing);
340
- }
341
- }
342
- const merged = actions.slice(0, this.max).map(a => {
343
- const h = hydrated.get(a.id);
344
- if (!h || !isEvictedState(a.prevState)) return a;
345
- return {
346
- ...a,
347
- prevState: h.prevState,
348
- nextState: h.nextState,
349
- payload: isPayloadOnDevice(a.payload) ? h.payload : a.payload,
350
- action: isPayloadOnDevice(a.action) ? h.action : a.action,
351
- meta: isPayloadOnDevice(a.meta?.arg) ? h.meta : a.meta,
352
- error: isPayloadOnDevice(a.error) ? h.error : a.error
353
- };
354
- });
355
- this.setItems(merged);
356
- }
357
-
358
- /**
359
- * Hydrate one mirrored action with real values (state trees, payload,
360
- * action) fetched on demand from the device via the adapter's
361
- * `getActionDetail` action. Remote-mirror (desktop) use only.
362
- */
363
- hydrateAction(id, patch) {
364
- this.hydrateById(id, patch);
365
- }
366
- setMaxActions(max) {
367
- this.setMax(max);
368
- }
369
- getStats() {
370
- const total = this.items.length;
371
- const withChanges = this.items.filter(a => a.hasStateChange).length;
372
- const actionTypes = new Set(this.items.map(a => a.type));
373
-
374
- // Calculate average duration
375
- const durations = this.items.filter(a => a.duration !== undefined).map(a => a.duration);
376
- const avgDuration = durations.length > 0 ? durations.reduce((sum, d) => sum + d, 0) / durations.length : 0;
377
- return {
378
- totalActions: total,
379
- actionsWithChanges: withChanges,
380
- actionsWithoutChanges: total - withChanges,
381
- uniqueActionTypes: actionTypes.size,
382
- averageDuration: Math.round(avgDuration * 100) / 100
383
- };
384
- }
385
- filterActions(filter) {
386
- let filtered = [...this.items];
387
- if (filter.searchText) {
388
- const search = filter.searchText.toLowerCase();
389
- filtered = filtered.filter(a => a.type.toLowerCase().includes(search));
390
- }
391
- if (filter.actionTypes && filter.actionTypes.length > 0) {
392
- filtered = filtered.filter(a => filter.actionTypes.includes(a.type));
393
- }
394
- if (filter.onlyWithChanges) {
395
- filtered = filtered.filter(a => a.hasStateChange);
396
- }
397
- return filtered;
398
- }
399
- getUniqueActionTypes() {
400
- return Array.from(new Set(this.items.map(a => a.type))).sort();
401
- }
402
-
403
- /**
404
- * Find related async actions by requestId
405
- */
406
- getRelatedActions(requestId) {
407
- return this.items.filter(a => a.meta?.requestId === requestId).sort((a, b) => a.timestamp - b.timestamp); // Sort chronologically
408
- }
409
-
410
- /**
411
- * Get all actions that share a requestId with the given action
412
- */
413
- getLinkedActions(action) {
414
- if (!action.meta?.requestId) return [];
415
- return this.getRelatedActions(action.meta.requestId);
416
- }
417
-
418
- /**
419
- * Get count of linked actions for a given action
420
- */
421
- getLinkedActionsCount(action) {
422
- if (!action.meta?.requestId) return 0;
423
- return this.items.filter(a => a.meta?.requestId === action.meta?.requestId).length;
424
- }
425
-
426
- /**
427
- * Calculate total duration for an async operation (pending to resolution)
428
- */
429
- getAsyncOperationDuration(requestId) {
430
- const related = this.getRelatedActions(requestId);
431
- if (related.length < 2) return null;
432
- const pending = related.find(a => a.category === "pending");
433
- const resolution = related.find(a => a.category === "fulfilled" || a.category === "rejected");
434
- if (pending && resolution) {
435
- return resolution.timestamp - pending.timestamp;
436
- }
437
- return null;
438
- }
439
-
440
- /**
441
- * Get the base action type (without /pending, /fulfilled, /rejected suffix)
442
- */
443
- getBaseActionType(type) {
444
- const parts = type.split("/");
445
- if (parts.length >= 2) {
446
- const lastPart = parts[parts.length - 1];
447
- if (["pending", "fulfilled", "rejected"].includes(lastPart)) {
448
- return parts.slice(0, -1).join("/");
449
- }
450
- }
451
- return type;
452
- }
453
-
454
- /**
455
- * Get sequential index for an async operation (1, 2, 3, etc.)
456
- * Based on order of when pending actions first appeared
457
- */
458
- getAsyncOperationIndex(requestId) {
459
- // Get all unique requestIds with their first timestamp
460
- const requestIdTimestamps = new Map();
461
- for (const action of this.items) {
462
- if (action.meta?.requestId && !requestIdTimestamps.has(action.meta.requestId)) {
463
- requestIdTimestamps.set(action.meta.requestId, action.timestamp);
464
- }
465
- }
466
-
467
- // Sort by timestamp (oldest first) and find index
468
- const sortedIds = Array.from(requestIdTimestamps.entries()).sort((a, b) => a[1] - b[1]).map(([id]) => id);
469
- const index = sortedIds.indexOf(requestId);
470
- return index >= 0 ? index + 1 : 0;
471
- }
472
- }
473
- const reduxActionStore = exports.reduxActionStore = new ReduxActionStore();
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.STATE_RETENTION_LIMIT=exports.STATE_ON_DEVICE=exports.STATE_EVICTED=exports.PAYLOAD_ON_DEVICE=void 0;exports.isEvictedState=isEvictedState;exports.isPayloadOnDevice=isPayloadOnDevice;exports.jsonLongerThan=jsonLongerThan;exports.reduxActionStore=void 0;var _sharedUi=require("@buoy-gg/shared-ui");const STATE_RETENTION_LIMIT=exports.STATE_RETENTION_LIMIT=25;const STATE_EVICTED=exports.STATE_EVICTED=Object.freeze({__buoyStateEvicted:true,note:`Raw state retained for the ${STATE_RETENTION_LIMIT} most recent actions only.`});const STATE_ON_DEVICE=exports.STATE_ON_DEVICE=Object.freeze({__buoyStateOnDevice:true,note:"State trees stay on the device \u2014 fetched on demand via getActionDetail."});const PAYLOAD_ON_DEVICE=exports.PAYLOAD_ON_DEVICE=Object.freeze({__buoyPayloadOnDevice:true,note:"Payload stays on the device \u2014 fetched on demand via getActionDetail."});function isPayloadOnDevice(n){return typeof n==="object"&&n!==null&&n.__buoyPayloadOnDevice===true}function isEvictedState(n){if(typeof n!=="object"||n===null)return false;const e=n;return e.__buoyStateEvicted===true||e.__buoyStateOnDevice===true}function getActionCategory(n){if(n.startsWith("@@"))return"internal";if(n.includes("/pending"))return"pending";if(n.includes("/fulfilled"))return"fulfilled";if(n.includes("/rejected"))return"rejected";if(n.includes("/executeQuery"))return"query";if(n.includes("/executeMutation"))return"mutation";return"action"}function parseActionType(n){if(n.startsWith("@@")){const t=n.split("/");return{sliceName:null,actionName:t[t.length-1]||n}}const e=n.split("/");if(e.length>=2){const t=e[0];let i=e[1];if(e.length>=3&&["pending","fulfilled","rejected"].includes(e[e.length-1])){i=e.slice(1,-1).join("/")}return{sliceName:t,actionName:i.toUpperCase()}}return{sliceName:null,actionName:n.toUpperCase()}}function jsonLongerThan(n,e){return(0,_sharedUi.approxJsonSize)(n,e).bytes>e}function formatPayloadPreview(n,e=40){if(n===void 0)return"";if(n===null)return"null";try{if(typeof n==="string"){return n.length>e?`"${n.slice(0,e-3)}..."`:`"${n}"`}if(typeof n==="number"||typeof n==="boolean"){return String(n)}if(Array.isArray(n)){if(n.length===0)return"[]";if(jsonLongerThan(n,e))return`[${n.length} items]`;const t=JSON.stringify(n);return t.length>e?`[${n.length} items]`:t}if(typeof n==="object"){const t=Object.keys(n);if(t.length===0)return"{}";if(jsonLongerThan(n,e))return`{ ${t.length} keys }`;const i=JSON.stringify(n);if(i.length<=e)return i;return`{ ${t.length} keys }`}return String(n).slice(0,e)}catch{return"[complex]"}}function getStateDiffSummary(n,e){if(n===e){return{summary:"no change",changedCount:0}}if(typeof n!=="object"||typeof e!=="object"||n===null||e===null){return{summary:"changed",changedCount:1}}const t=Object.keys(n);const i=Object.keys(e);const r=i.filter(u=>!t.includes(u)).length;const s=t.filter(u=>!i.includes(u)).length;let o=0;for(const u of t){if(i.includes(u)&&n[u]!==e[u]){o++}}const c=[];if(r>0)c.push(`+${r}`);if(s>0)c.push(`-${s}`);if(o>0)c.push(`~${o}`);const a=r+s+o;if(c.length===0){return{summary:"nested change",changedCount:1}}return{summary:`${c.join(" ")} ${a===1?"key":"keys"}`,changedCount:a}}function extractMeta(n){const e=n.meta;if(!e)return void 0;return{requestId:e.requestId,requestStatus:e.requestStatus,arg:e.arg,rejectedWithValue:e.rejectedWithValue,aborted:e.aborted,condition:e.condition}}class ReduxActionStore extends _sharedUi.ChangeLog{addAction(e,t,i,r){if(!this.canRecord()){return}const s=e.type||"";if(!s){return}const o=t!==i;const c=getActionCategory(s);const{sliceName:a,actionName:u}=parseActionType(s);const f=extractMeta(e);const h=formatPayloadPreview(e.payload);const{summary:g,changedCount:m}=getStateDiffSummary(t,i);const p=(r??0)>16;const y={id:this.nextId(),type:s,payload:e.payload,action:e,timestamp:Date.now(),prevState:t,nextState:i,duration:r,hasStateChange:o,category:c,sliceName:a,actionName:u,meta:f,error:e.error,payloadPreview:h,diffSummary:g,changedKeysCount:m,isSlowAction:p};this.push(y);for(let d=STATE_RETENTION_LIMIT;d<this.items.length;d++){const l=this.items[d];if(l.prevState!==STATE_EVICTED){l.prevState=STATE_EVICTED;l.nextState=STATE_EVICTED}else{break}}this.notifyListeners()}getActions(){return this.getAll()}getActionById(e){return this.getById(e)}clearActions(){this.clear()}replaceActions(e){if(!this.getEnabled()){return}const t=new Map;for(const r of this.items){if(r.prevState!==void 0&&!isEvictedState(r.prevState)){t.set(r.id,r)}}const i=e.slice(0,this.max).map(r=>{const s=t.get(r.id);if(!s||!isEvictedState(r.prevState))return r;return{...r,prevState:s.prevState,nextState:s.nextState,payload:isPayloadOnDevice(r.payload)?s.payload:r.payload,action:isPayloadOnDevice(r.action)?s.action:r.action,meta:isPayloadOnDevice(r.meta?.arg)?s.meta:r.meta,error:isPayloadOnDevice(r.error)?s.error:r.error}});this.setItems(i)}hydrateAction(e,t){this.hydrateById(e,t)}setMaxActions(e){this.setMax(e)}getStats(){const e=this.items.length;const t=this.items.filter(o=>o.hasStateChange).length;const i=new Set(this.items.map(o=>o.type));const r=this.items.filter(o=>o.duration!==void 0).map(o=>o.duration);const s=r.length>0?r.reduce((o,c)=>o+c,0)/r.length:0;return{totalActions:e,actionsWithChanges:t,actionsWithoutChanges:e-t,uniqueActionTypes:i.size,averageDuration:Math.round(s*100)/100}}filterActions(e){let t=[...this.items];if(e.searchText){const i=e.searchText.toLowerCase();t=t.filter(r=>r.type.toLowerCase().includes(i))}if(e.actionTypes&&e.actionTypes.length>0){t=t.filter(i=>e.actionTypes.includes(i.type))}if(e.onlyWithChanges){t=t.filter(i=>i.hasStateChange)}return t}getUniqueActionTypes(){return Array.from(new Set(this.items.map(e=>e.type))).sort()}getRelatedActions(e){return this.items.filter(t=>t.meta?.requestId===e).sort((t,i)=>t.timestamp-i.timestamp)}getLinkedActions(e){if(!e.meta?.requestId)return[];return this.getRelatedActions(e.meta.requestId)}getLinkedActionsCount(e){if(!e.meta?.requestId)return 0;return this.items.filter(t=>t.meta?.requestId===e.meta?.requestId).length}getAsyncOperationDuration(e){const t=this.getRelatedActions(e);if(t.length<2)return null;const i=t.find(s=>s.category==="pending");const r=t.find(s=>s.category==="fulfilled"||s.category==="rejected");if(i&&r){return r.timestamp-i.timestamp}return null}getBaseActionType(e){const t=e.split("/");if(t.length>=2){const i=t[t.length-1];if(["pending","fulfilled","rejected"].includes(i)){return t.slice(0,-1).join("/")}}return e}getAsyncOperationIndex(e){const t=new Map;for(const s of this.items){if(s.meta?.requestId&&!t.has(s.meta.requestId)){t.set(s.meta.requestId,s.timestamp)}}const i=Array.from(t.entries()).sort((s,o)=>s[1]-o[1]).map(([s])=>s);const r=i.indexOf(e);return r>=0?r+1:0}}const reduxActionStore=exports.reduxActionStore=new ReduxActionStore;