@buoy-gg/redux 7.0.32 → 7.0.34

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 (32) hide show
  1. package/lib/commonjs/preset.js +14 -22
  2. package/lib/commonjs/redux/components/ReduxActionInfoView.js +8 -60
  3. package/lib/commonjs/redux/components/ReduxActionItem.js +64 -118
  4. package/lib/commonjs/redux/components/ReduxDetailViewToggle.js +15 -109
  5. package/lib/commonjs/redux/components/ReduxModal.js +46 -103
  6. package/lib/commonjs/redux/hooks/useReduxActions.js +16 -33
  7. package/lib/commonjs/redux/sync/reduxSyncAdapter.js +11 -29
  8. package/lib/commonjs/redux/utils/reduxActionStore.js +24 -94
  9. package/lib/commonjs/redux/utils/snapshotProvider.js +17 -80
  10. package/lib/module/preset.js +13 -20
  11. package/lib/module/redux/components/ReduxActionInfoView.js +1 -53
  12. package/lib/module/redux/components/ReduxActionItem.js +65 -119
  13. package/lib/module/redux/components/ReduxDetailViewToggle.js +17 -111
  14. package/lib/module/redux/components/ReduxModal.js +48 -105
  15. package/lib/module/redux/hooks/useReduxActions.js +17 -34
  16. package/lib/module/redux/sync/reduxSyncAdapter.js +12 -30
  17. package/lib/module/redux/utils/reduxActionStore.js +25 -95
  18. package/lib/module/redux/utils/snapshotProvider.js +18 -81
  19. package/lib/typescript/preset.d.ts +3 -9
  20. package/lib/typescript/preset.d.ts.map +1 -1
  21. package/lib/typescript/redux/components/ReduxActionInfoView.d.ts.map +1 -1
  22. package/lib/typescript/redux/components/ReduxActionItem.d.ts.map +1 -1
  23. package/lib/typescript/redux/components/ReduxDetailViewToggle.d.ts +2 -1
  24. package/lib/typescript/redux/components/ReduxDetailViewToggle.d.ts.map +1 -1
  25. package/lib/typescript/redux/components/ReduxModal.d.ts.map +1 -1
  26. package/lib/typescript/redux/hooks/useReduxActions.d.ts +3 -0
  27. package/lib/typescript/redux/hooks/useReduxActions.d.ts.map +1 -1
  28. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts +68 -82
  29. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts.map +1 -1
  30. package/lib/typescript/redux/utils/reduxActionStore.d.ts +2 -29
  31. package/lib/typescript/redux/utils/reduxActionStore.d.ts.map +1 -1
  32. package/package.json +4 -4
@@ -38,24 +38,27 @@ const FREE_TIER_ACTION_LIMIT = exports.FREE_TIER_ACTION_LIMIT = _sharedUi.TIER_L
38
38
  */
39
39
  function EmptyState({
40
40
  isEnabled,
41
- query
41
+ query,
42
+ totalActions
42
43
  }) {
43
- const isFiltered = query.length > 0;
44
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
45
- style: styles.emptyState,
46
- testID: `${_testIDs.REDUX_TEST_ID}-empty`,
47
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Zap, {
48
- size: 32,
49
- color: _sharedUi.night.color.textTertiary
50
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
51
- style: styles.emptyTitle,
52
- testID: `${_testIDs.REDUX_TEST_ID}-empty-title`,
53
- children: isFiltered ? "No Matches" : "No Redux actions"
54
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
55
- style: styles.emptyText,
56
- testID: `${_testIDs.REDUX_TEST_ID}-empty-text`,
57
- children: isFiltered ? `No action type contains "${query}"` : isEnabled ? "Actions will appear here as they are dispatched" : "Enable capture to start recording actions"
58
- })]
44
+ const content = (0, _sharedUi.resolveListEmptyState)({
45
+ subject: "actions",
46
+ search: query,
47
+ total: totalActions,
48
+ capturing: isEnabled,
49
+ waiting: {
50
+ otto: "waiting",
51
+ title: "No Redux actions",
52
+ message: "Actions will appear here as they are dispatched"
53
+ },
54
+ paused: {
55
+ title: "No Redux actions",
56
+ message: "Enable capture to start recording actions"
57
+ }
58
+ });
59
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
60
+ ...content,
61
+ testID: `${_testIDs.REDUX_TEST_ID}-empty`
59
62
  });
60
63
  }
61
64
  function ReduxModal({
@@ -268,49 +271,29 @@ function ReduxModal({
268
271
  onBack: onBack
269
272
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
270
273
  title: "",
271
- children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
272
- style: styles.headerSearchContainer,
273
- testID: `${_testIDs.REDUX_TEST_ID}-search`,
274
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
275
- size: 14,
276
- color: _sharedUi.night.color.textSecondary
277
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
278
- ref: searchInputRef,
279
- testID: `${_testIDs.REDUX_TEST_ID}-search-field`,
280
- style: styles.headerSearchInput,
281
- placeholder: "Search action types...",
282
- placeholderTextColor: _sharedUi.night.color.textTertiary,
283
- value: searchText,
284
- onChangeText: handleSearch,
285
- onSubmitEditing: () => searchInputRef.current?.blur()
286
- // Stay open while the query still filters the list.
287
- //
288
- // Collapsing it on blur hid the reason rows were missing: the
289
- // header went back to its chips — which count the WHOLE
290
- // capture, not the filtered list — while the list stayed
291
- // narrowed, so the tool showed "4 actions" above a single row
292
- // and offered nothing to explain the gap. The only way back
293
- // was to reopen a field the user had no reason to think was
294
- // still armed.
295
- ,
296
- onBlur: () => {
297
- if (searchText.length === 0) setIsSearchActive(false);
298
- },
299
- accessibilityLabel: "Search action types",
300
- autoCapitalize: "none",
301
- autoCorrect: false,
302
- returnKeyType: "search"
303
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
304
- onPress: closeSearch,
305
- style: styles.headerSearchClear,
306
- testID: `${_testIDs.REDUX_TEST_ID}-search-clear`,
307
- accessibilityRole: "button",
308
- accessibilityLabel: "Close search",
309
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.X, {
310
- size: 14,
311
- color: _sharedUi.night.color.textSecondary
312
- })
313
- })]
274
+ children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Search, {
275
+ value: searchText,
276
+ onChangeText: handleSearch,
277
+ onCancel: closeSearch,
278
+ placeholder: "Search action types...",
279
+ accessibilityLabel: "Search action types",
280
+ iconSize: 14,
281
+ testID: `${_testIDs.REDUX_TEST_ID}-search-field`,
282
+ containerTestID: `${_testIDs.REDUX_TEST_ID}-search`,
283
+ clearTestID: `${_testIDs.REDUX_TEST_ID}-search-clear`
284
+ // Stay open while the query still filters the list.
285
+ //
286
+ // Collapsing it on blur hid the reason rows were missing: the
287
+ // header went back to its chips — which count the WHOLE
288
+ // capture, not the filtered list — while the list stayed
289
+ // narrowed, so the tool showed "4 actions" above a single row
290
+ // and offered nothing to explain the gap. The only way back
291
+ // was to reopen a field the user had no reason to think was
292
+ // still armed.
293
+ ,
294
+ onBlur: () => {
295
+ if (searchText.length === 0) setIsSearchActive(false);
296
+ }
314
297
  }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
315
298
  style: styles.headerChipRow,
316
299
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
@@ -505,14 +488,11 @@ function ReduxModal({
505
488
  keyExtractor: keyExtractor,
506
489
  contentContainerStyle: styles.listContent,
507
490
  showsVerticalScrollIndicator: true,
508
- removeClippedSubviews: true,
509
- initialNumToRender: 12,
510
- maxToRenderPerBatch: 8,
511
- windowSize: 5,
512
- updateCellsBatchingPeriod: 50
491
+ ..._sharedUi.EVENT_LIST_PRESET
513
492
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyState, {
514
493
  isEnabled: isEnabled,
515
- query: searchText
494
+ query: searchText,
495
+ totalActions: stats.totalActions
516
496
  })]
517
497
  })
518
498
  });
@@ -594,27 +574,6 @@ const styles = _reactNative.StyleSheet.create({
594
574
  // Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
595
575
  backgroundColor: "transparent"
596
576
  },
597
- headerSearchContainer: {
598
- flexDirection: "row",
599
- alignItems: "center",
600
- backgroundColor: _sharedUi.night.color.surfaceElevated,
601
- borderRadius: 10,
602
- borderWidth: 1,
603
- borderColor: _sharedUi.night.color.border,
604
- paddingHorizontal: 12,
605
- paddingVertical: 5
606
- },
607
- headerSearchInput: {
608
- flex: 1,
609
- color: _sharedUi.night.color.text,
610
- fontSize: 13,
611
- marginLeft: 6,
612
- paddingVertical: 2
613
- },
614
- headerSearchClear: {
615
- marginLeft: 6,
616
- padding: 4
617
- },
618
577
  headerChipRow: {
619
578
  flexDirection: "row",
620
579
  alignItems: "center",
@@ -709,22 +668,6 @@ const styles = _reactNative.StyleSheet.create({
709
668
  paddingTop: 8,
710
669
  paddingBottom: 20
711
670
  },
712
- emptyState: {
713
- alignItems: "center",
714
- paddingVertical: 40
715
- },
716
- emptyTitle: {
717
- color: _sharedUi.night.color.text,
718
- fontSize: 14,
719
- fontWeight: "600",
720
- marginTop: 12,
721
- marginBottom: 6
722
- },
723
- emptyText: {
724
- color: _sharedUi.night.color.textTertiary,
725
- fontSize: 12,
726
- textAlign: "center"
727
- },
728
671
  // Locked actions banner styles
729
672
  lockedBanner: {
730
673
  flexDirection: "row",
@@ -5,41 +5,31 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.useReduxActions = useReduxActions;
7
7
  var _react = require("react");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
8
9
  var _reduxActionStore = require("../utils/reduxActionStore");
9
10
  /**
10
11
  * Hook for consuming Redux actions from the store
12
+ *
13
+ * The subscribe / filter / pause / clear plumbing is shared `useChangeLog`;
14
+ * this file owns the redux-specific stats, action types and selection.
11
15
  */
12
16
 
17
+ const filterActions = (_actions, filter) => _reduxActionStore.reduxActionStore.filterActions(filter);
13
18
  function useReduxActions() {
14
- const [actions, setActions] = (0, _react.useState)(() => _reduxActionStore.reduxActionStore.getActions());
15
- const [filter, setFilter] = (0, _react.useState)({});
16
- const [isEnabled, setIsEnabled] = (0, _react.useState)(() => _reduxActionStore.reduxActionStore.getEnabled());
19
+ const log = (0, _sharedUi.useChangeLog)(_reduxActionStore.reduxActionStore, {
20
+ filterItems: filterActions
21
+ });
17
22
  const [selectedAction, setSelectedAction] = (0, _react.useState)(null);
18
23
 
19
- // Subscribe to store changes
20
- (0, _react.useEffect)(() => {
21
- const unsubscribe = _reduxActionStore.reduxActionStore.subscribe(newActions => {
22
- setActions(newActions);
23
- });
24
- return () => {
25
- unsubscribe();
26
- };
27
- }, []);
28
-
29
- // Filter actions based on current filter
30
- const filteredActions = (0, _react.useMemo)(() => {
31
- return _reduxActionStore.reduxActionStore.filterActions(filter);
32
- }, [actions, filter]);
33
-
34
24
  // Get stats
35
25
  const stats = (0, _react.useMemo)(() => {
36
26
  return _reduxActionStore.reduxActionStore.getStats();
37
- }, [actions]);
27
+ }, [log.items]);
38
28
 
39
29
  // Get unique action types
40
30
  const actionTypes = (0, _react.useMemo)(() => {
41
31
  return _reduxActionStore.reduxActionStore.getUniqueActionTypes();
42
- }, [actions]);
32
+ }, [log.items]);
43
33
 
44
34
  // Clear actions
45
35
  const clearActions = (0, _react.useCallback)(() => {
@@ -47,26 +37,19 @@ function useReduxActions() {
47
37
  setSelectedAction(null);
48
38
  }, []);
49
39
 
50
- // Toggle capture
51
- const toggleCapture = (0, _react.useCallback)(() => {
52
- const newEnabled = !isEnabled;
53
- _reduxActionStore.reduxActionStore.setEnabled(newEnabled);
54
- setIsEnabled(newEnabled);
55
- }, [isEnabled]);
56
-
57
40
  // Get action by ID
58
41
  const getActionById = (0, _react.useCallback)(id => {
59
42
  return _reduxActionStore.reduxActionStore.getActionById(id);
60
43
  }, []);
61
44
  return {
62
- actions,
63
- filteredActions,
64
- filter,
65
- setFilter,
45
+ actions: log.items,
46
+ filteredActions: log.filteredItems,
47
+ filter: log.filter,
48
+ setFilter: log.setFilter,
66
49
  stats,
67
50
  clearActions,
68
- isEnabled,
69
- toggleCapture,
51
+ isEnabled: log.isEnabled,
52
+ toggleCapture: log.toggleCapture,
70
53
  actionTypes,
71
54
  getActionById,
72
55
  selectedAction,
@@ -7,19 +7,8 @@ exports.reduxSyncAdapter = void 0;
7
7
  var _reduxActionStore = require("../utils/reduxActionStore");
8
8
  var _sharedUi = require("@buoy-gg/shared-ui");
9
9
  var _autoInstrument = require("../utils/autoInstrument");
10
- /**
11
- * Upper bound for one on-demand action-detail response. Generous — this is a
12
- * single explicit fetch, not a per-action stream — but still guarded so a
13
- * pathological tree can't OOM the device through the reply emit.
14
- */
15
- const MAX_ACTION_DETAIL_BYTES = 8 * 1024 * 1024;
16
-
17
10
  /** Per-action budget for `payload`/`action`/`meta.arg`/`error` on the snapshot wire. */
18
- const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
19
- function toWireValue(value) {
20
- if (value === undefined) return value;
21
- return (0, _sharedUi.isOverWireBudget)(value, MAX_WIRE_PAYLOAD_BYTES) ? _reduxActionStore.PAYLOAD_ON_DEVICE : value;
22
- }
11
+ const toWireValue = value => (0, _sharedUi.toWire)(value, _reduxActionStore.PAYLOAD_ON_DEVICE);
23
12
 
24
13
  /**
25
14
  * Per-action wire cache. `reduxActionStore` never mutates an action in place —
@@ -28,10 +17,7 @@ function toWireValue(value) {
28
17
  * `action`, `meta.arg` and `error` in the list is re-measured on all ~5
29
18
  * snapshots per second for as long as it stays in the list.
30
19
  */
31
- const wireCache = new WeakMap();
32
- function toWireAction(a) {
33
- const cached = wireCache.get(a);
34
- if (cached) return cached;
20
+ const toWireAction = (0, _sharedUi.memoByIdentity)(a => {
35
21
  const wire = (0, _reduxActionStore.isEvictedState)(a.prevState) ? {
36
22
  ...a
37
23
  } : {
@@ -54,9 +40,8 @@ function toWireAction(a) {
54
40
  arg
55
41
  };
56
42
  }
57
- wireCache.set(a, wire);
58
43
  return wire;
59
- }
44
+ });
60
45
  /** Top-level slice names of the state tree, for the compact (no-values) view. */
61
46
  function topLevelKeys(state) {
62
47
  return state && typeof state === "object" && !Array.isArray(state) ? Object.keys(state) : [];
@@ -93,7 +78,7 @@ function unavailable() {
93
78
  * dispatch actions — redux is action-based, so the *current state* isn't in the
94
79
  * action-log snapshot.
95
80
  */
96
- const reduxSyncAdapter = exports.reduxSyncAdapter = {
81
+ const reduxSyncAdapter = exports.reduxSyncAdapter = (0, _sharedUi.createSyncAdapter)({
97
82
  version: 2,
98
83
  /**
99
84
  * WIRE-FORM snapshot: actions WITHOUT raw prevState/nextState trees.
@@ -118,7 +103,7 @@ const reduxSyncAdapter = exports.reduxSyncAdapter = {
118
103
  getActionDetail: params => {
119
104
  const {
120
105
  id
121
- } = params ?? {};
106
+ } = (0, _sharedUi.readParams)(params);
122
107
  if (!id) return {
123
108
  found: false,
124
109
  reason: "missing id"
@@ -138,11 +123,7 @@ const reduxSyncAdapter = exports.reduxSyncAdapter = {
138
123
  }
139
124
  // Per-field cap: oversized fields degrade individually instead of
140
125
  // failing the whole detail (a giant payload shouldn't hide the trees).
141
- const cappedNote = {
142
- __buoyTruncated: true,
143
- note: `Exceeds the ${MAX_ACTION_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
144
- };
145
- const cap = value => (0, _sharedUi.isOverWireBudget)(value, MAX_ACTION_DETAIL_BYTES) ? cappedNote : value;
126
+ const cap = value => (0, _sharedUi.capDetail)(value, _sharedUi.WIRE_DETAIL_LIMIT_BYTES);
146
127
  const capMeta = meta => {
147
128
  if (!meta) return meta;
148
129
  const arg = cap(meta.arg);
@@ -169,7 +150,7 @@ const reduxSyncAdapter = exports.reduxSyncAdapter = {
169
150
  getState: params => {
170
151
  const {
171
152
  includeValues = false
172
- } = params ?? {};
153
+ } = (0, _sharedUi.readParams)(params);
173
154
  const store = (0, _autoInstrument.getActiveStore)();
174
155
  if (!store) {
175
156
  return unavailable();
@@ -191,7 +172,7 @@ const reduxSyncAdapter = exports.reduxSyncAdapter = {
191
172
  dispatch: params => {
192
173
  const {
193
174
  action
194
- } = params;
175
+ } = (0, _sharedUi.readParams)(params);
195
176
  const store = (0, _autoInstrument.getActiveStore)();
196
177
  if (!store) {
197
178
  return {
@@ -199,11 +180,12 @@ const reduxSyncAdapter = exports.reduxSyncAdapter = {
199
180
  ...unavailable()
200
181
  };
201
182
  }
183
+ if (!action) throw new Error("dispatch requires an `action`");
202
184
  store.dispatch(action);
203
185
  return {
204
186
  dispatched: true,
205
- type: action?.type
187
+ type: action.type
206
188
  };
207
189
  }
208
190
  }
209
- };
191
+ });
@@ -238,16 +238,9 @@ function extractMeta(action) {
238
238
  // Redux Action Store
239
239
  // ============================================
240
240
 
241
- class ReduxActionStore {
242
- actions = [];
243
- listeners = new Set();
244
- clearListeners = new Set();
245
- maxActions = 200;
246
- idCounter = 0;
247
- isEnabled = true;
248
- captureSuppressed = false;
241
+ class ReduxActionStore extends _sharedUi.ChangeLog {
249
242
  addAction(action, prevState, nextState, duration) {
250
- if (!this.isEnabled || this.captureSuppressed) {
243
+ if (!this.canRecord()) {
251
244
  return;
252
245
  }
253
246
  const actionType = action.type || "";
@@ -273,7 +266,7 @@ class ReduxActionStore {
273
266
  const isSlowAction = (duration ?? 0) > 16; // Frame budget
274
267
 
275
268
  const reduxAction = {
276
- id: `${Date.now()}-${++this.idCounter}`,
269
+ id: this.nextId(),
277
270
  type: actionType,
278
271
  payload: action.payload,
279
272
  action,
@@ -293,7 +286,7 @@ class ReduxActionStore {
293
286
  changedKeysCount,
294
287
  isSlowAction
295
288
  };
296
- this.actions = [reduxAction, ...this.actions].slice(0, this.maxActions);
289
+ this.push(reduxAction);
297
290
 
298
291
  // Bound raw-state retention. Every action holds prevState/nextState by
299
292
  // reference; with structural sharing that's normally cheap, but when a
@@ -302,8 +295,8 @@ class ReduxActionStore {
302
295
  // pinned by 50 slice-replacing actions, enough to OOM-kill enterprise
303
296
  // apps. Detail view + time travel realistically use recent actions, so
304
297
  // older ones keep their diff metadata but drop the raw trees.
305
- for (let i = STATE_RETENTION_LIMIT; i < this.actions.length; i++) {
306
- const a = this.actions[i];
298
+ for (let i = STATE_RETENTION_LIMIT; i < this.items.length; i++) {
299
+ const a = this.items[i];
307
300
  if (a.prevState !== STATE_EVICTED) {
308
301
  a.prevState = STATE_EVICTED;
309
302
  a.nextState = STATE_EVICTED;
@@ -314,63 +307,26 @@ class ReduxActionStore {
314
307
  this.notifyListeners();
315
308
  }
316
309
  getActions() {
317
- return [...this.actions];
310
+ return this.getAll();
318
311
  }
319
312
  getActionById(id) {
320
- return this.actions.find(a => a.id === id);
313
+ return this.getById(id);
321
314
  }
322
315
  clearActions() {
323
- this.actions = [];
324
- this.notifyListeners();
325
- this.clearListeners.forEach(listener => {
326
- try {
327
- listener();
328
- } catch {
329
- // Ignore listener errors
330
- }
331
- });
332
- }
333
-
334
- /**
335
- * Listen for clearActions() calls. Used in remote mirror mode to forward a
336
- * clear performed in the dashboard UI to the synced device.
337
- */
338
- onClear(listener) {
339
- this.clearListeners.add(listener);
340
- return () => {
341
- this.clearListeners.delete(listener);
342
- };
316
+ this.clear();
343
317
  }
344
318
 
345
319
  // ============================================
346
320
  // Remote Mirror Mode
347
321
  // ============================================
348
322
 
349
- /**
350
- * Permanently suppress local capture. Use when this store acts as a mirror
351
- * of a remote device's actions (e.g. the desktop dashboard): actions arrive
352
- * via replaceActions() and local instrumentation must never record.
353
- */
354
- disableCapture() {
355
- this.captureSuppressed = true;
356
- }
357
-
358
- /**
359
- * Whether the store is in remote mirror mode (capture suppressed). The UI
360
- * uses this to skip local-instrumentation affordances like the
361
- * "react-redux not installed" banner.
362
- */
363
- isCaptureSuppressed() {
364
- return this.captureSuppressed;
365
- }
366
-
367
323
  /**
368
324
  * Replace the entire action list and notify listeners. Used in remote
369
325
  * mirror mode where full snapshots arrive from a synced device. Respects
370
326
  * the enabled flag so the UI's pause button freezes the mirror.
371
327
  */
372
328
  replaceActions(actions) {
373
- if (!this.isEnabled) {
329
+ if (!this.getEnabled()) {
374
330
  return;
375
331
  }
376
332
  // Preserve any trees a remote mirror already hydrated via
@@ -378,12 +334,12 @@ class ReduxActionStore {
378
334
  // full snapshot arriving after a detail fetch must not blank the detail
379
335
  // pane back out.
380
336
  const hydrated = new Map();
381
- for (const existing of this.actions) {
337
+ for (const existing of this.items) {
382
338
  if (existing.prevState !== undefined && !isEvictedState(existing.prevState)) {
383
339
  hydrated.set(existing.id, existing);
384
340
  }
385
341
  }
386
- this.actions = actions.slice(0, this.maxActions).map(a => {
342
+ const merged = actions.slice(0, this.max).map(a => {
387
343
  const h = hydrated.get(a.id);
388
344
  if (!h || !isEvictedState(a.prevState)) return a;
389
345
  return {
@@ -396,7 +352,7 @@ class ReduxActionStore {
396
352
  error: isPayloadOnDevice(a.error) ? h.error : a.error
397
353
  };
398
354
  });
399
- this.notifyListeners();
355
+ this.setItems(merged);
400
356
  }
401
357
 
402
358
  /**
@@ -405,44 +361,18 @@ class ReduxActionStore {
405
361
  * `getActionDetail` action. Remote-mirror (desktop) use only.
406
362
  */
407
363
  hydrateAction(id, patch) {
408
- const index = this.actions.findIndex(a => a.id === id);
409
- if (index === -1) return;
410
- this.actions = [...this.actions.slice(0, index), {
411
- ...this.actions[index],
412
- ...patch
413
- }, ...this.actions.slice(index + 1)];
414
- this.notifyListeners();
415
- }
416
- setEnabled(enabled) {
417
- this.isEnabled = enabled;
418
- }
419
- getEnabled() {
420
- return this.isEnabled;
421
- }
422
- subscribe(listener) {
423
- this.listeners.add(listener);
424
- return () => {
425
- this.listeners.delete(listener);
426
- };
427
- }
428
- notifyListeners() {
429
- const actions = this.getActions();
430
- this.listeners.forEach(listener => listener(actions));
364
+ this.hydrateById(id, patch);
431
365
  }
432
366
  setMaxActions(max) {
433
- this.maxActions = max;
434
- if (this.actions.length > max) {
435
- this.actions = this.actions.slice(0, max);
436
- this.notifyListeners();
437
- }
367
+ this.setMax(max);
438
368
  }
439
369
  getStats() {
440
- const total = this.actions.length;
441
- const withChanges = this.actions.filter(a => a.hasStateChange).length;
442
- const actionTypes = new Set(this.actions.map(a => a.type));
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));
443
373
 
444
374
  // Calculate average duration
445
- const durations = this.actions.filter(a => a.duration !== undefined).map(a => a.duration);
375
+ const durations = this.items.filter(a => a.duration !== undefined).map(a => a.duration);
446
376
  const avgDuration = durations.length > 0 ? durations.reduce((sum, d) => sum + d, 0) / durations.length : 0;
447
377
  return {
448
378
  totalActions: total,
@@ -453,7 +383,7 @@ class ReduxActionStore {
453
383
  };
454
384
  }
455
385
  filterActions(filter) {
456
- let filtered = [...this.actions];
386
+ let filtered = [...this.items];
457
387
  if (filter.searchText) {
458
388
  const search = filter.searchText.toLowerCase();
459
389
  filtered = filtered.filter(a => a.type.toLowerCase().includes(search));
@@ -467,14 +397,14 @@ class ReduxActionStore {
467
397
  return filtered;
468
398
  }
469
399
  getUniqueActionTypes() {
470
- return Array.from(new Set(this.actions.map(a => a.type))).sort();
400
+ return Array.from(new Set(this.items.map(a => a.type))).sort();
471
401
  }
472
402
 
473
403
  /**
474
404
  * Find related async actions by requestId
475
405
  */
476
406
  getRelatedActions(requestId) {
477
- return this.actions.filter(a => a.meta?.requestId === requestId).sort((a, b) => a.timestamp - b.timestamp); // Sort chronologically
407
+ return this.items.filter(a => a.meta?.requestId === requestId).sort((a, b) => a.timestamp - b.timestamp); // Sort chronologically
478
408
  }
479
409
 
480
410
  /**
@@ -490,7 +420,7 @@ class ReduxActionStore {
490
420
  */
491
421
  getLinkedActionsCount(action) {
492
422
  if (!action.meta?.requestId) return 0;
493
- return this.actions.filter(a => a.meta?.requestId === action.meta?.requestId).length;
423
+ return this.items.filter(a => a.meta?.requestId === action.meta?.requestId).length;
494
424
  }
495
425
 
496
426
  /**
@@ -528,7 +458,7 @@ class ReduxActionStore {
528
458
  getAsyncOperationIndex(requestId) {
529
459
  // Get all unique requestIds with their first timestamp
530
460
  const requestIdTimestamps = new Map();
531
- for (const action of this.actions) {
461
+ for (const action of this.items) {
532
462
  if (action.meta?.requestId && !requestIdTimestamps.has(action.meta.requestId)) {
533
463
  requestIdTimestamps.set(action.meta.requestId, action.timestamp);
534
464
  }