@buoy-gg/redux 7.0.38 → 7.0.39

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 (68) hide show
  1. package/lib/commonjs/index.js +256 -1
  2. package/lib/commonjs/preset.js +90 -1
  3. package/lib/commonjs/redux/components/ReduxActionButton.js +131 -1
  4. package/lib/commonjs/redux/components/ReduxActionDetailContent.js +437 -1
  5. package/lib/commonjs/redux/components/ReduxActionDetailView.js +401 -1
  6. package/lib/commonjs/redux/components/ReduxActionInfoView.js +836 -1
  7. package/lib/commonjs/redux/components/ReduxActionItem.js +334 -1
  8. package/lib/commonjs/redux/components/ReduxAutoInstrument.js +20 -1
  9. package/lib/commonjs/redux/components/ReduxDetailViewToggle.js +50 -1
  10. package/lib/commonjs/redux/components/ReduxIcon.js +18 -1
  11. package/lib/commonjs/redux/components/ReduxModal.js +703 -1
  12. package/lib/commonjs/redux/hooks/useAutoInstrumentRedux.js +187 -1
  13. package/lib/commonjs/redux/hooks/useReduxActions.js +58 -1
  14. package/lib/commonjs/redux/sync/reduxSyncAdapter.js +191 -1
  15. package/lib/commonjs/redux/utils/activeStoreRegistry.js +104 -1
  16. package/lib/commonjs/redux/utils/autoInstrument.js +231 -1
  17. package/lib/commonjs/redux/utils/buoyReduxMiddleware.js +202 -1
  18. package/lib/commonjs/redux/utils/createReduxHistoryAdapter.js +146 -1
  19. package/lib/commonjs/redux/utils/devtoolsHook.js +152 -1
  20. package/lib/commonjs/redux/utils/reduxActionStore.js +473 -1
  21. package/lib/commonjs/redux/utils/snapshotProvider.js +158 -1
  22. package/lib/commonjs/redux/utils/testIDs.js +75 -1
  23. package/lib/module/index.js +125 -1
  24. package/lib/module/preset.js +87 -1
  25. package/lib/module/redux/components/ReduxActionButton.js +128 -1
  26. package/lib/module/redux/components/ReduxActionDetailContent.js +433 -1
  27. package/lib/module/redux/components/ReduxActionDetailView.js +397 -1
  28. package/lib/module/redux/components/ReduxActionInfoView.js +831 -1
  29. package/lib/module/redux/components/ReduxActionItem.js +330 -1
  30. package/lib/module/redux/components/ReduxAutoInstrument.js +17 -1
  31. package/lib/module/redux/components/ReduxDetailViewToggle.js +45 -1
  32. package/lib/module/redux/components/ReduxIcon.js +8 -1
  33. package/lib/module/redux/components/ReduxModal.js +698 -1
  34. package/lib/module/redux/hooks/useAutoInstrumentRedux.js +183 -1
  35. package/lib/module/redux/hooks/useReduxActions.js +54 -1
  36. package/lib/module/redux/sync/reduxSyncAdapter.js +188 -1
  37. package/lib/module/redux/utils/activeStoreRegistry.js +92 -1
  38. package/lib/module/redux/utils/autoInstrument.js +201 -1
  39. package/lib/module/redux/utils/buoyReduxMiddleware.js +188 -1
  40. package/lib/module/redux/utils/createReduxHistoryAdapter.js +142 -1
  41. package/lib/module/redux/utils/devtoolsHook.js +145 -1
  42. package/lib/module/redux/utils/reduxActionStore.js +465 -1
  43. package/lib/module/redux/utils/snapshotProvider.js +157 -1
  44. package/lib/module/redux/utils/testIDs.js +68 -1
  45. package/lib/typescript/index.d.ts.map +1 -0
  46. package/lib/typescript/preset.d.ts.map +1 -0
  47. package/lib/typescript/redux/components/ReduxActionButton.d.ts.map +1 -0
  48. package/lib/typescript/redux/components/ReduxActionDetailContent.d.ts.map +1 -0
  49. package/lib/typescript/redux/components/ReduxActionDetailView.d.ts.map +1 -0
  50. package/lib/typescript/redux/components/ReduxActionInfoView.d.ts.map +1 -0
  51. package/lib/typescript/redux/components/ReduxActionItem.d.ts.map +1 -0
  52. package/lib/typescript/redux/components/ReduxAutoInstrument.d.ts.map +1 -0
  53. package/lib/typescript/redux/components/ReduxDetailViewToggle.d.ts.map +1 -0
  54. package/lib/typescript/redux/components/ReduxIcon.d.ts.map +1 -0
  55. package/lib/typescript/redux/components/ReduxModal.d.ts.map +1 -0
  56. package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts.map +1 -0
  57. package/lib/typescript/redux/hooks/useReduxActions.d.ts.map +1 -0
  58. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts.map +1 -0
  59. package/lib/typescript/redux/types/index.d.ts.map +1 -0
  60. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts.map +1 -0
  61. package/lib/typescript/redux/utils/autoInstrument.d.ts.map +1 -0
  62. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts.map +1 -0
  63. package/lib/typescript/redux/utils/createReduxHistoryAdapter.d.ts.map +1 -0
  64. package/lib/typescript/redux/utils/devtoolsHook.d.ts.map +1 -0
  65. package/lib/typescript/redux/utils/reduxActionStore.d.ts.map +1 -0
  66. package/lib/typescript/redux/utils/snapshotProvider.d.ts.map +1 -0
  67. package/lib/typescript/redux/utils/testIDs.d.ts.map +1 -0
  68. package/package.json +4 -4
@@ -1 +1,465 @@
1
- "use strict";import{ChangeLog,approxJsonSize}from"@buoy-gg/shared-ui";export const STATE_RETENTION_LIMIT=25;export const STATE_EVICTED=Object.freeze({__buoyStateEvicted:!0,note:"Raw state retained for the 25 most recent actions only."});export const STATE_ON_DEVICE=Object.freeze({__buoyStateOnDevice:!0,note:"State trees stay on the device — fetched on demand via getActionDetail."});export const PAYLOAD_ON_DEVICE=Object.freeze({__buoyPayloadOnDevice:!0,note:"Payload stays on the device — fetched on demand via getActionDetail."});export function isPayloadOnDevice(e){return"object"==typeof e&&null!==e&&!0===e.__buoyPayloadOnDevice}export function isEvictedState(e){if("object"!=typeof e||null===e)return!1;const t=e;return!0===t.__buoyStateEvicted||!0===t.__buoyStateOnDevice}function getActionCategory(e){return e.startsWith("@@")?"internal":e.includes("/pending")?"pending":e.includes("/fulfilled")?"fulfilled":e.includes("/rejected")?"rejected":e.includes("/executeQuery")?"query":e.includes("/executeMutation")?"mutation":"action"}function parseActionType(e){if(e.startsWith("@@")){const t=e.split("/");return{sliceName:null,actionName:t[t.length-1]||e}}const t=e.split("/");if(t.length>=2){const e=t[0];let n=t[1];return t.length>=3&&["pending","fulfilled","rejected"].includes(t[t.length-1])&&(n=t.slice(1,-1).join("/")),{sliceName:e,actionName:n.toUpperCase()}}return{sliceName:null,actionName:e.toUpperCase()}}export function jsonLongerThan(e,t){return approxJsonSize(e,t).bytes>t}function formatPayloadPreview(e,t=40){if(void 0===e)return"";if(null===e)return"null";try{if("string"==typeof e)return e.length>t?`"${e.slice(0,t-3)}..."`:`"${e}"`;if("number"==typeof e||"boolean"==typeof e)return String(e);if(Array.isArray(e)){if(0===e.length)return"[]";if(jsonLongerThan(e,t))return`[${e.length} items]`;const n=JSON.stringify(e);return n.length>t?`[${e.length} items]`:n}if("object"==typeof e){const n=Object.keys(e);if(0===n.length)return"{}";if(jsonLongerThan(e,t))return`{ ${n.length} keys }`;const i=JSON.stringify(e);return i.length<=t?i:`{ ${n.length} keys }`}return String(e).slice(0,t)}catch{return"[complex]"}}function getStateDiffSummary(e,t){if(e===t)return{summary:"no change",changedCount:0};if("object"!=typeof e||"object"!=typeof t||null===e||null===t)return{summary:"changed",changedCount:1};const n=Object.keys(e),i=Object.keys(t),r=i.filter(e=>!n.includes(e)).length,a=n.filter(e=>!i.includes(e)).length;let o=0;for(const r of n)i.includes(r)&&e[r]!==t[r]&&o++;const s=[];r>0&&s.push(`+${r}`),a>0&&s.push(`-${a}`),o>0&&s.push(`~${o}`);const c=r+a+o;return 0===s.length?{summary:"nested change",changedCount:1}:{summary:`${s.join(" ")} ${1===c?"key":"keys"}`,changedCount:c}}function extractMeta(e){const t=e.meta;if(t)return{requestId:t.requestId,requestStatus:t.requestStatus,arg:t.arg,rejectedWithValue:t.rejectedWithValue,aborted:t.aborted,condition:t.condition}}class ReduxActionStore extends ChangeLog{addAction(e,t,n,i){if(!this.canRecord())return;const r=e.type||"";if(!r)return;const a=t!==n,o=getActionCategory(r),{sliceName:s,actionName:c}=parseActionType(r),u=extractMeta(e),l=formatPayloadPreview(e.payload),{summary:d,changedCount:h}=getStateDiffSummary(t,n),g=(i??0)>16,y={id:this.nextId(),type:r,payload:e.payload,action:e,timestamp:Date.now(),prevState:t,nextState:n,duration:i,hasStateChange:a,category:o,sliceName:s,actionName:c,meta:u,error:e.error,payloadPreview:l,diffSummary:d,changedKeysCount:h,isSlowAction:g};this.push(y);for(let e=25;e<this.items.length;e++){const t=this.items[e];if(t.prevState===STATE_EVICTED)break;t.prevState=STATE_EVICTED,t.nextState=STATE_EVICTED}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 e of this.items)void 0===e.prevState||isEvictedState(e.prevState)||t.set(e.id,e);const n=e.slice(0,this.max).map(e=>{const n=t.get(e.id);return n&&isEvictedState(e.prevState)?{...e,prevState:n.prevState,nextState:n.nextState,payload:isPayloadOnDevice(e.payload)?n.payload:e.payload,action:isPayloadOnDevice(e.action)?n.action:e.action,meta:isPayloadOnDevice(e.meta?.arg)?n.meta:e.meta,error:isPayloadOnDevice(e.error)?n.error:e.error}:e});this.setItems(n)}hydrateAction(e,t){this.hydrateById(e,t)}setMaxActions(e){this.setMax(e)}getStats(){const e=this.items.length,t=this.items.filter(e=>e.hasStateChange).length,n=new Set(this.items.map(e=>e.type)),i=this.items.filter(e=>void 0!==e.duration).map(e=>e.duration),r=i.length>0?i.reduce((e,t)=>e+t,0)/i.length:0;return{totalActions:e,actionsWithChanges:t,actionsWithoutChanges:e-t,uniqueActionTypes:n.size,averageDuration:Math.round(100*r)/100}}filterActions(e){let t=[...this.items];if(e.searchText){const n=e.searchText.toLowerCase();t=t.filter(e=>e.type.toLowerCase().includes(n))}return e.actionTypes&&e.actionTypes.length>0&&(t=t.filter(t=>e.actionTypes.includes(t.type))),e.onlyWithChanges&&(t=t.filter(e=>e.hasStateChange)),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((e,t)=>e.timestamp-t.timestamp)}getLinkedActions(e){return e.meta?.requestId?this.getRelatedActions(e.meta.requestId):[]}getLinkedActionsCount(e){return e.meta?.requestId?this.items.filter(t=>t.meta?.requestId===e.meta?.requestId).length:0}getAsyncOperationDuration(e){const t=this.getRelatedActions(e);if(t.length<2)return null;const n=t.find(e=>"pending"===e.category),i=t.find(e=>"fulfilled"===e.category||"rejected"===e.category);return n&&i?i.timestamp-n.timestamp:null}getBaseActionType(e){const t=e.split("/");if(t.length>=2){const e=t[t.length-1];if(["pending","fulfilled","rejected"].includes(e))return t.slice(0,-1).join("/")}return e}getAsyncOperationIndex(e){const t=new Map;for(const e of this.items)e.meta?.requestId&&!t.has(e.meta.requestId)&&t.set(e.meta.requestId,e.timestamp);const n=Array.from(t.entries()).sort((e,t)=>e[1]-t[1]).map(([e])=>e).indexOf(e);return n>=0?n+1:0}}export const reduxActionStore=new ReduxActionStore;
1
+ "use strict";
2
+
3
+ /**
4
+ * Redux action store - captures and stores Redux Toolkit actions
5
+ */
6
+
7
+ import { ChangeLog, approxJsonSize } from "@buoy-gg/shared-ui";
8
+ /**
9
+ * How many of the most-recent actions keep their raw prevState/nextState
10
+ * trees. Older actions keep all diff metadata but their trees are replaced
11
+ * with STATE_EVICTED so a store switch can't pin hundreds of MB.
12
+ */
13
+ export const STATE_RETENTION_LIMIT = 25;
14
+
15
+ /**
16
+ * Sentinel installed in place of evicted state trees. A frozen plain object
17
+ * so existing detail viewers render it harmlessly; jumpToState refuses it.
18
+ */
19
+ export const STATE_EVICTED = Object.freeze({
20
+ __buoyStateEvicted: true,
21
+ note: `Raw state retained for the ${STATE_RETENTION_LIMIT} most recent actions only.`
22
+ });
23
+
24
+ /**
25
+ * Sentinel used in WIRE-FORM snapshots (sync to desktop/MCP) in place of raw
26
+ * prevState/nextState trees. Unlike STATE_EVICTED the trees still exist on
27
+ * the device — dashboards fetch them on demand via the adapter's
28
+ * `getActionDetail` action. Shipping them in every snapshot serialized up to
29
+ * 25×2 full state trees through socket.emit per action (no guard), which
30
+ * froze and native-OOM-crashed enterprise-scale apps whenever a dashboard
31
+ * was connected (confirmed by patch-and-retest on the affected app).
32
+ */
33
+ export const STATE_ON_DEVICE = Object.freeze({
34
+ __buoyStateOnDevice: true,
35
+ note: "State trees stay on the device — fetched on demand via getActionDetail."
36
+ });
37
+
38
+ /**
39
+ * Wire-form sentinel for an oversized action `payload`/`action`. A dispatched
40
+ * payload can itself be huge (rehydration actions carry the whole new state),
41
+ * so the per-snapshot stream ships `payloadPreview` + this marker instead;
42
+ * `getActionDetail` serves the real value on demand.
43
+ */
44
+ export const PAYLOAD_ON_DEVICE = Object.freeze({
45
+ __buoyPayloadOnDevice: true,
46
+ note: "Payload stays on the device — fetched on demand via getActionDetail."
47
+ });
48
+
49
+ /** True when a wire-form value is the payload-on-device sentinel. */
50
+ export function isPayloadOnDevice(value) {
51
+ return typeof value === "object" && value !== null && value.__buoyPayloadOnDevice === true;
52
+ }
53
+
54
+ /**
55
+ * True when a value is one of the state sentinels (evicted or on-device) —
56
+ * guards time travel and any consumer that needs a REAL tree.
57
+ */
58
+ export function isEvictedState(value) {
59
+ if (typeof value !== "object" || value === null) return false;
60
+ const v = value;
61
+ return v.__buoyStateEvicted === true || v.__buoyStateOnDevice === true;
62
+ }
63
+
64
+ // ============================================
65
+ // Helper Functions for Enhanced Data
66
+ // ============================================
67
+
68
+ /**
69
+ * Get action category from type string
70
+ */
71
+ function getActionCategory(type) {
72
+ if (type.startsWith("@@")) return "internal";
73
+ if (type.includes("/pending")) return "pending";
74
+ if (type.includes("/fulfilled")) return "fulfilled";
75
+ if (type.includes("/rejected")) return "rejected";
76
+ if (type.includes("/executeQuery")) return "query";
77
+ if (type.includes("/executeMutation")) return "mutation";
78
+ return "action";
79
+ }
80
+
81
+ /**
82
+ * Parse action type to extract slice and action names
83
+ */
84
+ function parseActionType(type) {
85
+ // Handle internal actions
86
+ if (type.startsWith("@@")) {
87
+ const parts = type.split("/");
88
+ return {
89
+ sliceName: null,
90
+ actionName: parts[parts.length - 1] || type
91
+ };
92
+ }
93
+
94
+ // Handle RTK pattern: "sliceName/actionName" or "sliceName/actionName/status"
95
+ const parts = type.split("/");
96
+ if (parts.length >= 2) {
97
+ const sliceName = parts[0];
98
+ // For async thunks, action name might be "fetchUser/pending" -> we want "fetchUser"
99
+ let actionName = parts[1];
100
+
101
+ // If it's an async status, use the base action name
102
+ if (parts.length >= 3 && ["pending", "fulfilled", "rejected"].includes(parts[parts.length - 1])) {
103
+ actionName = parts.slice(1, -1).join("/");
104
+ }
105
+ return {
106
+ sliceName,
107
+ actionName: actionName.toUpperCase()
108
+ };
109
+ }
110
+ return {
111
+ sliceName: null,
112
+ actionName: type.toUpperCase()
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Cheap early-abort size probe: is `value`'s JSON form definitely longer than
118
+ * `limit`? Bails the moment the running estimate exceeds the limit, so the
119
+ * cost is O(limit) — never O(payload). Used to avoid JSON.stringify-ing large
120
+ * payloads (a fulfilled thunk's payload can be megabytes) just to produce a
121
+ * 40-char preview; that full stringify ran on EVERY dispatch and blocked the
122
+ * JS thread at enterprise state sizes.
123
+ */
124
+ export function jsonLongerThan(value, limit) {
125
+ return approxJsonSize(value, limit).bytes > limit;
126
+ }
127
+
128
+ /**
129
+ * Format payload preview for display
130
+ */
131
+ function formatPayloadPreview(payload, maxLength = 40) {
132
+ if (payload === undefined) return "";
133
+ if (payload === null) return "null";
134
+ try {
135
+ if (typeof payload === "string") {
136
+ return payload.length > maxLength ? `"${payload.slice(0, maxLength - 3)}..."` : `"${payload}"`;
137
+ }
138
+ if (typeof payload === "number" || typeof payload === "boolean") {
139
+ return String(payload);
140
+ }
141
+ if (Array.isArray(payload)) {
142
+ if (payload.length === 0) return "[]";
143
+ // Probe cheaply first — only stringify payloads that can actually fit.
144
+ if (jsonLongerThan(payload, maxLength)) return `[${payload.length} items]`;
145
+ const preview = JSON.stringify(payload);
146
+ return preview.length > maxLength ? `[${payload.length} items]` : preview;
147
+ }
148
+ if (typeof payload === "object") {
149
+ const keys = Object.keys(payload);
150
+ if (keys.length === 0) return "{}";
151
+
152
+ // Probe cheaply first — only stringify payloads that can actually fit.
153
+ if (jsonLongerThan(payload, maxLength)) return `{ ${keys.length} keys }`;
154
+ const preview = JSON.stringify(payload);
155
+ if (preview.length <= maxLength) return preview;
156
+
157
+ // Show key count
158
+ return `{ ${keys.length} keys }`;
159
+ }
160
+ return String(payload).slice(0, maxLength);
161
+ } catch {
162
+ return "[complex]";
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Calculate state diff summary
168
+ */
169
+ function getStateDiffSummary(prevState, nextState) {
170
+ if (prevState === nextState) {
171
+ return {
172
+ summary: "no change",
173
+ changedCount: 0
174
+ };
175
+ }
176
+
177
+ // Handle non-objects
178
+ if (typeof prevState !== "object" || typeof nextState !== "object" || prevState === null || nextState === null) {
179
+ return {
180
+ summary: "changed",
181
+ changedCount: 1
182
+ };
183
+ }
184
+ const prevKeys = Object.keys(prevState);
185
+ const nextKeys = Object.keys(nextState);
186
+ const added = nextKeys.filter(k => !prevKeys.includes(k)).length;
187
+ const removed = prevKeys.filter(k => !nextKeys.includes(k)).length;
188
+
189
+ // Check which existing keys changed
190
+ let changed = 0;
191
+ for (const key of prevKeys) {
192
+ if (nextKeys.includes(key) && prevState[key] !== nextState[key]) {
193
+ changed++;
194
+ }
195
+ }
196
+ const parts = [];
197
+ if (added > 0) parts.push(`+${added}`);
198
+ if (removed > 0) parts.push(`-${removed}`);
199
+ if (changed > 0) parts.push(`~${changed}`);
200
+ const totalChanged = added + removed + changed;
201
+ if (parts.length === 0) {
202
+ return {
203
+ summary: "nested change",
204
+ changedCount: 1
205
+ };
206
+ }
207
+ return {
208
+ summary: `${parts.join(" ")} ${totalChanged === 1 ? "key" : "keys"}`,
209
+ changedCount: totalChanged
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Extract RTK async thunk metadata
215
+ */
216
+ function extractMeta(action) {
217
+ const meta = action.meta;
218
+ if (!meta) return undefined;
219
+ return {
220
+ requestId: meta.requestId,
221
+ requestStatus: meta.requestStatus,
222
+ arg: meta.arg,
223
+ rejectedWithValue: meta.rejectedWithValue,
224
+ aborted: meta.aborted,
225
+ condition: meta.condition
226
+ };
227
+ }
228
+
229
+ // ============================================
230
+ // Redux Action Store
231
+ // ============================================
232
+
233
+ class ReduxActionStore extends ChangeLog {
234
+ addAction(action, prevState, nextState, duration) {
235
+ if (!this.canRecord()) {
236
+ return;
237
+ }
238
+ const actionType = action.type || "";
239
+
240
+ // Skip actions without a proper type (e.g., thunk functions)
241
+ if (!actionType) {
242
+ return;
243
+ }
244
+ const hasStateChange = prevState !== nextState;
245
+
246
+ // Enhanced data extraction
247
+ const category = getActionCategory(actionType);
248
+ const {
249
+ sliceName,
250
+ actionName
251
+ } = parseActionType(actionType);
252
+ const meta = extractMeta(action);
253
+ const payloadPreview = formatPayloadPreview(action.payload);
254
+ const {
255
+ summary: diffSummary,
256
+ changedCount: changedKeysCount
257
+ } = getStateDiffSummary(prevState, nextState);
258
+ const isSlowAction = (duration ?? 0) > 16; // Frame budget
259
+
260
+ const reduxAction = {
261
+ id: this.nextId(),
262
+ type: actionType,
263
+ payload: action.payload,
264
+ action,
265
+ timestamp: Date.now(),
266
+ prevState,
267
+ nextState,
268
+ duration,
269
+ hasStateChange,
270
+ // Enhanced fields
271
+ category,
272
+ sliceName,
273
+ actionName,
274
+ meta,
275
+ error: action.error,
276
+ payloadPreview,
277
+ diffSummary,
278
+ changedKeysCount,
279
+ isSlowAction
280
+ };
281
+ this.push(reduxAction);
282
+
283
+ // Bound raw-state retention. Every action holds prevState/nextState by
284
+ // reference; with structural sharing that's normally cheap, but when a
285
+ // host replaces large slices wholesale (store switch / rehydration),
286
+ // each retained action pins a DISTINCT multi-MB subtree — measured 88MB
287
+ // pinned by 50 slice-replacing actions, enough to OOM-kill enterprise
288
+ // apps. Detail view + time travel realistically use recent actions, so
289
+ // older ones keep their diff metadata but drop the raw trees.
290
+ for (let i = STATE_RETENTION_LIMIT; i < this.items.length; i++) {
291
+ const a = this.items[i];
292
+ if (a.prevState !== STATE_EVICTED) {
293
+ a.prevState = STATE_EVICTED;
294
+ a.nextState = STATE_EVICTED;
295
+ } else {
296
+ break; // older entries already evicted (list is newest-first)
297
+ }
298
+ }
299
+ this.notifyListeners();
300
+ }
301
+ getActions() {
302
+ return this.getAll();
303
+ }
304
+ getActionById(id) {
305
+ return this.getById(id);
306
+ }
307
+ clearActions() {
308
+ this.clear();
309
+ }
310
+
311
+ // ============================================
312
+ // Remote Mirror Mode
313
+ // ============================================
314
+
315
+ /**
316
+ * Replace the entire action list and notify listeners. Used in remote
317
+ * mirror mode where full snapshots arrive from a synced device. Respects
318
+ * the enabled flag so the UI's pause button freezes the mirror.
319
+ */
320
+ replaceActions(actions) {
321
+ if (!this.getEnabled()) {
322
+ return;
323
+ }
324
+ // Preserve any trees a remote mirror already hydrated via
325
+ // replaceActionStates — wire-form snapshots always carry markers, and a
326
+ // full snapshot arriving after a detail fetch must not blank the detail
327
+ // pane back out.
328
+ const hydrated = new Map();
329
+ for (const existing of this.items) {
330
+ if (existing.prevState !== undefined && !isEvictedState(existing.prevState)) {
331
+ hydrated.set(existing.id, existing);
332
+ }
333
+ }
334
+ const merged = actions.slice(0, this.max).map(a => {
335
+ const h = hydrated.get(a.id);
336
+ if (!h || !isEvictedState(a.prevState)) return a;
337
+ return {
338
+ ...a,
339
+ prevState: h.prevState,
340
+ nextState: h.nextState,
341
+ payload: isPayloadOnDevice(a.payload) ? h.payload : a.payload,
342
+ action: isPayloadOnDevice(a.action) ? h.action : a.action,
343
+ meta: isPayloadOnDevice(a.meta?.arg) ? h.meta : a.meta,
344
+ error: isPayloadOnDevice(a.error) ? h.error : a.error
345
+ };
346
+ });
347
+ this.setItems(merged);
348
+ }
349
+
350
+ /**
351
+ * Hydrate one mirrored action with real values (state trees, payload,
352
+ * action) fetched on demand from the device via the adapter's
353
+ * `getActionDetail` action. Remote-mirror (desktop) use only.
354
+ */
355
+ hydrateAction(id, patch) {
356
+ this.hydrateById(id, patch);
357
+ }
358
+ setMaxActions(max) {
359
+ this.setMax(max);
360
+ }
361
+ getStats() {
362
+ const total = this.items.length;
363
+ const withChanges = this.items.filter(a => a.hasStateChange).length;
364
+ const actionTypes = new Set(this.items.map(a => a.type));
365
+
366
+ // Calculate average duration
367
+ const durations = this.items.filter(a => a.duration !== undefined).map(a => a.duration);
368
+ const avgDuration = durations.length > 0 ? durations.reduce((sum, d) => sum + d, 0) / durations.length : 0;
369
+ return {
370
+ totalActions: total,
371
+ actionsWithChanges: withChanges,
372
+ actionsWithoutChanges: total - withChanges,
373
+ uniqueActionTypes: actionTypes.size,
374
+ averageDuration: Math.round(avgDuration * 100) / 100
375
+ };
376
+ }
377
+ filterActions(filter) {
378
+ let filtered = [...this.items];
379
+ if (filter.searchText) {
380
+ const search = filter.searchText.toLowerCase();
381
+ filtered = filtered.filter(a => a.type.toLowerCase().includes(search));
382
+ }
383
+ if (filter.actionTypes && filter.actionTypes.length > 0) {
384
+ filtered = filtered.filter(a => filter.actionTypes.includes(a.type));
385
+ }
386
+ if (filter.onlyWithChanges) {
387
+ filtered = filtered.filter(a => a.hasStateChange);
388
+ }
389
+ return filtered;
390
+ }
391
+ getUniqueActionTypes() {
392
+ return Array.from(new Set(this.items.map(a => a.type))).sort();
393
+ }
394
+
395
+ /**
396
+ * Find related async actions by requestId
397
+ */
398
+ getRelatedActions(requestId) {
399
+ return this.items.filter(a => a.meta?.requestId === requestId).sort((a, b) => a.timestamp - b.timestamp); // Sort chronologically
400
+ }
401
+
402
+ /**
403
+ * Get all actions that share a requestId with the given action
404
+ */
405
+ getLinkedActions(action) {
406
+ if (!action.meta?.requestId) return [];
407
+ return this.getRelatedActions(action.meta.requestId);
408
+ }
409
+
410
+ /**
411
+ * Get count of linked actions for a given action
412
+ */
413
+ getLinkedActionsCount(action) {
414
+ if (!action.meta?.requestId) return 0;
415
+ return this.items.filter(a => a.meta?.requestId === action.meta?.requestId).length;
416
+ }
417
+
418
+ /**
419
+ * Calculate total duration for an async operation (pending to resolution)
420
+ */
421
+ getAsyncOperationDuration(requestId) {
422
+ const related = this.getRelatedActions(requestId);
423
+ if (related.length < 2) return null;
424
+ const pending = related.find(a => a.category === "pending");
425
+ const resolution = related.find(a => a.category === "fulfilled" || a.category === "rejected");
426
+ if (pending && resolution) {
427
+ return resolution.timestamp - pending.timestamp;
428
+ }
429
+ return null;
430
+ }
431
+
432
+ /**
433
+ * Get the base action type (without /pending, /fulfilled, /rejected suffix)
434
+ */
435
+ getBaseActionType(type) {
436
+ const parts = type.split("/");
437
+ if (parts.length >= 2) {
438
+ const lastPart = parts[parts.length - 1];
439
+ if (["pending", "fulfilled", "rejected"].includes(lastPart)) {
440
+ return parts.slice(0, -1).join("/");
441
+ }
442
+ }
443
+ return type;
444
+ }
445
+
446
+ /**
447
+ * Get sequential index for an async operation (1, 2, 3, etc.)
448
+ * Based on order of when pending actions first appeared
449
+ */
450
+ getAsyncOperationIndex(requestId) {
451
+ // Get all unique requestIds with their first timestamp
452
+ const requestIdTimestamps = new Map();
453
+ for (const action of this.items) {
454
+ if (action.meta?.requestId && !requestIdTimestamps.has(action.meta.requestId)) {
455
+ requestIdTimestamps.set(action.meta.requestId, action.timestamp);
456
+ }
457
+ }
458
+
459
+ // Sort by timestamp (oldest first) and find index
460
+ const sortedIds = Array.from(requestIdTimestamps.entries()).sort((a, b) => a[1] - b[1]).map(([id]) => id);
461
+ const index = sortedIds.indexOf(requestId);
462
+ return index >= 0 ? index + 1 : 0;
463
+ }
464
+ }
465
+ export const reduxActionStore = new ReduxActionStore();
@@ -1 +1,157 @@
1
- "use strict";import{classifyPreviewItem,createRestoreScope,registerStateSnapshotProvider,skippedRestore,unionKeys,wontApply}from"@buoy-gg/shared-ui";import{getActiveStore,getInstrumentationStatus}from"./activeStoreRegistry";import{BUOY_JUMP_TO_STATE}from"./buoyReduxMiddleware";const ENABLE_HINT="enable restore by using the Buoy enhancer or wrapping your root reducer in withBuoyDevTools()";function status(){const{mechanism:e,hasStore:t,diagnostic:r}=getInstrumentationStatus();return t?"patch"===e?{canCapture:!0,canRestore:!1,detail:`Auto-instrumented store cannot be restored — ${ENABLE_HINT}`}:{canCapture:!0,canRestore:!0,detail:"middleware"===e?"Restore requires withBuoyDevTools(rootReducer) — verified at restore time":void 0}:{canCapture:!1,canRestore:!1,detail:"no-react-redux"===r?"react-redux not installed":"no-provider"===r?"No Redux <Provider> found":"No Redux store instrumented"}}async function capture(){const e=getActiveStore();return e?{data:{state:e.getState()},warnings:[]}:{data:null,warnings:["no Redux store instrumented"]}}async function restore(e,t){const r=e,o=getActiveStore();if(!o||null==r)return skippedRestore("redux",o?"empty redux snapshot":"no Redux store instrumented");const{mechanism:s}=getInstrumentationStatus();if("patch"===s)return skippedRestore("redux",`restore unavailable on auto-instrumented stores — ${ENABLE_HINT}`);const n=createRestoreScope(t),a=Object.keys(r.state??{}).filter(e=>!n.touch(e));let i=r.state;if(a.length>0){const e=o.getState(),t={...r.state};for(const r of a)t[r]=e?.[r];i=t}return o.dispatch({type:BUOY_JUMP_TO_STATE,payload:i}),o.getState()===i?{ok:!0,applied:1,skipped:[],warnings:a.length>0?[`${a.length} slice(s) excluded by you`]:[]}:skippedRestore("redux",`store ignored the state jump — ${ENABLE_HINT}`)}async function previewItems(e,t){const r=e?.state,o=void 0===t,s=getActiveStore(),n=o?s?.getState():t?.state,a=[],i=[];let u;if(o){const{mechanism:e,hasStore:t}=getInstrumentationStatus();t?"patch"===e&&(u=`auto-instrumented store cannot be restored — ${ENABLE_HINT}`):u="no Redux store instrumented"}const c=n??{},d=r??{};for(const e of unionKeys(c,d)){let t=classifyPreviewItem({key:e,label:e,oldValue:c[e],newValue:d[e],inOld:Object.prototype.hasOwnProperty.call(c,e),inNew:Object.prototype.hasOwnProperty.call(d,e)},{tooLargeReason:"slice too large to render a diff — restore still applies it"});u&&(t=wontApply(t,u)),a.push(t)}return u&&i.push(`Redux restore unavailable: ${u}`),{items:a,notes:i}}registerStateSnapshotProvider({id:"redux",label:"Redux",status:status,capture:capture,restore:restore,previewItems:previewItems});
1
+ "use strict";
2
+
3
+ /**
4
+ * Time Machine snapshot provider for Redux.
5
+ *
6
+ * Registered into @buoy-gg/shared-ui's stateSnapshotRegistry at module init;
7
+ * consumed by @buoy-gg/time-machine.
8
+ *
9
+ * Restore rides the existing `@@buoy/JUMP_TO_STATE` full-state-replace action:
10
+ * - enhancer path: always handled (wrapReducer).
11
+ * - middleware path: handled ONLY when the app wrapped its root reducer in
12
+ * `withBuoyDevTools()` — unverifiable statically, so restore verifies by
13
+ * reference identity after dispatch (the wrapped reducer returns the payload
14
+ * object itself).
15
+ * - patch path: never handled; restore is reported as unavailable up front.
16
+ */
17
+ import { classifyPreviewItem, createRestoreScope, registerStateSnapshotProvider, skippedRestore, unionKeys, wontApply } from "@buoy-gg/shared-ui";
18
+ import { getActiveStore, getInstrumentationStatus } from "./activeStoreRegistry";
19
+ import { BUOY_JUMP_TO_STATE } from "./buoyReduxMiddleware";
20
+ const ENABLE_HINT = "enable restore by using the Buoy enhancer or wrapping your root reducer in withBuoyDevTools()";
21
+ function status() {
22
+ const {
23
+ mechanism,
24
+ hasStore,
25
+ diagnostic
26
+ } = getInstrumentationStatus();
27
+ if (!hasStore) {
28
+ return {
29
+ canCapture: false,
30
+ canRestore: false,
31
+ detail: diagnostic === "no-react-redux" ? "react-redux not installed" : diagnostic === "no-provider" ? "No Redux <Provider> found" : "No Redux store instrumented"
32
+ };
33
+ }
34
+ if (mechanism === "patch") {
35
+ return {
36
+ canCapture: true,
37
+ canRestore: false,
38
+ detail: `Auto-instrumented store cannot be restored — ${ENABLE_HINT}`
39
+ };
40
+ }
41
+ return {
42
+ canCapture: true,
43
+ canRestore: true,
44
+ detail: mechanism === "middleware" ? "Restore requires withBuoyDevTools(rootReducer) — verified at restore time" : undefined
45
+ };
46
+ }
47
+ async function capture() {
48
+ const store = getActiveStore();
49
+ if (!store) {
50
+ return {
51
+ data: null,
52
+ warnings: ["no Redux store instrumented"]
53
+ };
54
+ }
55
+ return {
56
+ data: {
57
+ state: store.getState()
58
+ },
59
+ warnings: []
60
+ };
61
+ }
62
+ async function restore(data, options) {
63
+ const snapshot = data;
64
+ const store = getActiveStore();
65
+ if (!store || snapshot === undefined || snapshot === null) {
66
+ return skippedRestore("redux", store ? "empty redux snapshot" : "no Redux store instrumented");
67
+ }
68
+ const {
69
+ mechanism
70
+ } = getInstrumentationStatus();
71
+ if (mechanism === "patch") {
72
+ return skippedRestore("redux", `restore unavailable on auto-instrumented stores — ${ENABLE_HINT}`);
73
+ }
74
+ // Untouched slices keep their LIVE values inside the jump payload, so the
75
+ // full-state replace leaves them alone. A slice is untouched when it's
76
+ // explicitly excluded OR outside the includeKeys scope.
77
+ const scope = createRestoreScope(options);
78
+ const excluded = Object.keys(snapshot.state ?? {}).filter(k => !scope.touch(k));
79
+ let payload = snapshot.state;
80
+ if (excluded.length > 0) {
81
+ const live = store.getState();
82
+ const merged = {
83
+ ...snapshot.state
84
+ };
85
+ for (const slice of excluded) merged[slice] = live?.[slice];
86
+ payload = merged;
87
+ }
88
+ store.dispatch({
89
+ type: BUOY_JUMP_TO_STATE,
90
+ payload
91
+ });
92
+ // The wrapped reducer returns the payload object itself, so reference
93
+ // identity proves the jump was actually handled (and not silently ignored
94
+ // by an unwrapped middleware-path reducer).
95
+ if (store.getState() === payload) {
96
+ const warnings = excluded.length > 0 ? [`${excluded.length} slice(s) excluded by you`] : [];
97
+ return {
98
+ ok: true,
99
+ applied: 1,
100
+ skipped: [],
101
+ warnings
102
+ };
103
+ }
104
+ return skippedRestore("redux", `store ignored the state jump — ${ENABLE_HINT}`);
105
+ }
106
+
107
+ /** Per-slice restore preview. `base` omitted = compare against the live store. */
108
+ async function previewItems(target, base) {
109
+ const targetState = target?.state;
110
+ const liveMode = base === undefined;
111
+ const store = getActiveStore();
112
+ const baseState = liveMode ? store?.getState() : base?.state;
113
+ const items = [];
114
+ const notes = [];
115
+
116
+ // Restore-capability gate — a diff restore can't apply must say so, not
117
+ // render as a green change.
118
+ let wontApplyReason;
119
+ if (liveMode) {
120
+ const {
121
+ mechanism,
122
+ hasStore
123
+ } = getInstrumentationStatus();
124
+ if (!hasStore) wontApplyReason = "no Redux store instrumented";else if (mechanism === "patch") {
125
+ wontApplyReason = `auto-instrumented store cannot be restored — ${ENABLE_HINT}`;
126
+ }
127
+ }
128
+ const oldObj = baseState ?? {};
129
+ const newObj = targetState ?? {};
130
+ for (const key of unionKeys(oldObj, newObj)) {
131
+ let item = classifyPreviewItem({
132
+ key,
133
+ label: key,
134
+ oldValue: oldObj[key],
135
+ newValue: newObj[key],
136
+ inOld: Object.prototype.hasOwnProperty.call(oldObj, key),
137
+ inNew: Object.prototype.hasOwnProperty.call(newObj, key)
138
+ }, {
139
+ tooLargeReason: "slice too large to render a diff — restore still applies it"
140
+ });
141
+ if (wontApplyReason) item = wontApply(item, wontApplyReason);
142
+ items.push(item);
143
+ }
144
+ if (wontApplyReason) notes.push(`Redux restore unavailable: ${wontApplyReason}`);
145
+ return {
146
+ items,
147
+ notes
148
+ };
149
+ }
150
+ registerStateSnapshotProvider({
151
+ id: "redux",
152
+ label: "Redux",
153
+ status,
154
+ capture,
155
+ restore,
156
+ previewItems
157
+ });