@tanstack/pacer-devtools 1.1.5 → 1.2.1

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.
@@ -1,12 +1,12 @@
1
- import { className, createComponent, delegateEvents, effect, insert, memo, setStyleProperty, template } from "solid-js/web";
1
+ import { className, createComponent, delegateEvents, effect, insert, memo, setStyleProperty, style, template, use } from "solid-js/web";
2
2
  import { Header, HeaderLogo, JsonTree, MainPanel, ThemeContextProvider, useTheme } from "@tanstack/devtools-ui";
3
- import { createStore } from "solid-js/store";
4
- import { For, createContext, createEffect, createMemo, createSignal, onCleanup, onMount, useContext } from "solid-js";
5
- import { pacerEventClient } from "@tanstack/pacer/event-client";
3
+ import { createStore, produce } from "solid-js/store";
4
+ import { For, Show, createContext, createEffect, createMemo, createSignal, onCleanup, onMount, useContext } from "solid-js";
5
+ import { getPacerDevtoolsInstance, pacerEventClient } from "@tanstack/pacer/event-client";
6
6
  import * as goober from "goober";
7
7
  import { useStore } from "@tanstack/solid-store";
8
8
  import clsx from "clsx";
9
- import { pacerEventClient as pacerEventClient$1 } from "@tanstack/pacer";
9
+ import { emitChange } from "@tanstack/pacer";
10
10
  import dayjs from "dayjs";
11
11
  import relativeTime from "dayjs/plugin/relativeTime.js";
12
12
  //#region src/PacerContextProvider.tsx
@@ -24,109 +24,78 @@ const initialPacerDevtoolsStore = {
24
24
  lastUpdatedByKey: {}
25
25
  };
26
26
  const PacerDevtoolsContext = createContext([initialPacerDevtoolsStore, () => {}]);
27
- const updateOrAddToArray = (oldArray, newItem) => {
28
- const index = oldArray.findIndex((item) => item.key === newItem.key);
29
- if (index !== -1) return oldArray.map((item, i) => i === index ? newItem : item);
30
- return [...oldArray, newItem];
31
- };
27
+ /**
28
+ * Match TanStack Form devtools: subscribe with {@link pacerEventClient.on} per
29
+ * event suffix so the same `pacer:${suffix}` channel the library emits on is
30
+ * observed. `onAllPluginEvents` only sees `tanstack-devtools-global`, which is
31
+ * not always relayed the same way by the shell.
32
+ *
33
+ * `d-*` suffixes are used when the devtools panel pushes state back (see
34
+ * ActionButtons); those must update the panel store too.
35
+ */
36
+ const PACER_DEVTOOLS_UTIL_EVENTS = [
37
+ {
38
+ listKey: "asyncBatchers",
39
+ suffixes: ["AsyncBatcher", "d-AsyncBatcher"]
40
+ },
41
+ {
42
+ listKey: "asyncDebouncers",
43
+ suffixes: ["AsyncDebouncer", "d-AsyncDebouncer"]
44
+ },
45
+ {
46
+ listKey: "asyncQueuers",
47
+ suffixes: ["AsyncQueuer", "d-AsyncQueuer"]
48
+ },
49
+ {
50
+ listKey: "asyncRateLimiters",
51
+ suffixes: ["AsyncRateLimiter", "d-AsyncRateLimiter"]
52
+ },
53
+ {
54
+ listKey: "asyncThrottlers",
55
+ suffixes: ["AsyncThrottler", "d-AsyncThrottler"]
56
+ },
57
+ {
58
+ listKey: "batchers",
59
+ suffixes: ["Batcher", "d-Batcher"]
60
+ },
61
+ {
62
+ listKey: "debouncers",
63
+ suffixes: ["Debouncer", "d-Debouncer"]
64
+ },
65
+ {
66
+ listKey: "queuers",
67
+ suffixes: ["Queuer", "d-Queuer"]
68
+ },
69
+ {
70
+ listKey: "rateLimiters",
71
+ suffixes: ["RateLimiter", "d-RateLimiter"]
72
+ },
73
+ {
74
+ listKey: "throttlers",
75
+ suffixes: ["Throttler", "d-Throttler"]
76
+ }
77
+ ];
32
78
  function PacerContextProvider(props) {
33
79
  const [store, setStore] = createStore(initialPacerDevtoolsStore);
34
80
  createEffect(() => {
35
- onCleanup(pacerEventClient.onAllPluginEvents((_e) => {
36
- const e = _e;
37
- switch (e.type) {
38
- case "pacer:AsyncBatcher":
39
- setStore({
40
- asyncBatchers: updateOrAddToArray(store.asyncBatchers, e.payload),
41
- lastUpdatedByKey: {
42
- ...store.lastUpdatedByKey,
43
- [e.payload.key]: Date.now()
44
- }
45
- });
46
- break;
47
- case "pacer:AsyncDebouncer":
48
- setStore({
49
- asyncDebouncers: updateOrAddToArray(store.asyncDebouncers, e.payload),
50
- lastUpdatedByKey: {
51
- ...store.lastUpdatedByKey,
52
- [e.payload.key]: Date.now()
53
- }
54
- });
55
- break;
56
- case "pacer:AsyncQueuer":
57
- setStore({
58
- asyncQueuers: updateOrAddToArray(store.asyncQueuers, e.payload),
59
- lastUpdatedByKey: {
60
- ...store.lastUpdatedByKey,
61
- [e.payload.key]: Date.now()
62
- }
63
- });
64
- break;
65
- case "pacer:AsyncRateLimiter":
66
- setStore({
67
- asyncRateLimiters: updateOrAddToArray(store.asyncRateLimiters, e.payload),
68
- lastUpdatedByKey: {
69
- ...store.lastUpdatedByKey,
70
- [e.payload.key]: Date.now()
71
- }
72
- });
73
- break;
74
- case "pacer:AsyncThrottler":
75
- setStore({
76
- asyncThrottlers: updateOrAddToArray(store.asyncThrottlers, e.payload),
77
- lastUpdatedByKey: {
78
- ...store.lastUpdatedByKey,
79
- [e.payload.key]: Date.now()
80
- }
81
- });
82
- break;
83
- case "pacer:Batcher":
84
- setStore({
85
- batchers: updateOrAddToArray(store.batchers, e.payload),
86
- lastUpdatedByKey: {
87
- ...store.lastUpdatedByKey,
88
- [e.payload.key]: Date.now()
89
- }
90
- });
91
- break;
92
- case "pacer:Debouncer":
93
- setStore({
94
- debouncers: updateOrAddToArray(store.debouncers, e.payload),
95
- lastUpdatedByKey: {
96
- ...store.lastUpdatedByKey,
97
- [e.payload.key]: Date.now()
98
- }
99
- });
100
- break;
101
- case "pacer:Queuer":
102
- setStore({
103
- queuers: updateOrAddToArray(store.queuers, e.payload),
104
- lastUpdatedByKey: {
105
- ...store.lastUpdatedByKey,
106
- [e.payload.key]: Date.now()
107
- }
108
- });
109
- break;
110
- case "pacer:RateLimiter":
111
- setStore({
112
- rateLimiters: updateOrAddToArray(store.rateLimiters, e.payload),
113
- lastUpdatedByKey: {
114
- ...store.lastUpdatedByKey,
115
- [e.payload.key]: Date.now()
116
- }
117
- });
118
- break;
119
- case "pacer:Throttler":
120
- setStore({
121
- throttlers: updateOrAddToArray(store.throttlers, e.payload),
122
- lastUpdatedByKey: {
123
- ...store.lastUpdatedByKey,
124
- [e.payload.key]: Date.now()
125
- }
126
- });
127
- break;
128
- }
81
+ const cleanups = [];
82
+ for (const { listKey, suffixes } of PACER_DEVTOOLS_UTIL_EVENTS) for (const suffix of suffixes) cleanups.push(pacerEventClient.on(suffix, (e) => {
83
+ const key = e.payload.key;
84
+ if (!key) return;
85
+ const instance = getPacerDevtoolsInstance(key);
86
+ if (!instance || typeof instance !== "object") return;
87
+ setStore(produce((draft) => {
88
+ const list = draft[listKey];
89
+ const index = list.findIndex((item) => item.key === key);
90
+ const inst = instance;
91
+ if (index !== -1) list[index] = inst;
92
+ else list.push(inst);
93
+ draft.lastUpdatedByKey[key] = Date.now();
94
+ }));
129
95
  }));
96
+ onCleanup(() => {
97
+ for (const cleanup of cleanups) cleanup();
98
+ });
130
99
  });
131
100
  return createComponent(PacerDevtoolsContext.Provider, {
132
101
  value: [store, setStore],
@@ -448,9 +417,25 @@ const stylesFactory = (theme) => {
448
417
  const css = goober.css;
449
418
  const t = (light, dark) => theme === "light" ? light : dark;
450
419
  return {
420
+ shellRoot: css`
421
+ display: flex;
422
+ flex-direction: column;
423
+ box-sizing: border-box;
424
+ width: 100%;
425
+ height: 100%;
426
+ min-height: 0;
427
+ overflow: hidden;
428
+ `,
429
+ mainPanelShell: css`
430
+ display: flex;
431
+ flex-direction: column;
432
+ flex: 1 1 0%;
433
+ min-height: 0;
434
+ overflow: hidden !important;
435
+ `,
451
436
  mainContainer: css`
452
437
  display: flex;
453
- flex: 1;
438
+ flex: 1 1 0%;
454
439
  min-height: 0;
455
440
  overflow: hidden;
456
441
  padding: ${size[2]};
@@ -852,7 +837,7 @@ const UTIL_GROUPS = [
852
837
  ];
853
838
  //#endregion
854
839
  //#region src/components/UtilList.tsx
855
- var _tmpl$$4 = /* @__PURE__ */ template(`<div>`), _tmpl$2$2 = /* @__PURE__ */ template(`<div><div>`), _tmpl$3$2 = /* @__PURE__ */ template(`<div><div></div><div>`);
840
+ var _tmpl$$4 = /* @__PURE__ */ template(`<div>`), _tmpl$2$3 = /* @__PURE__ */ template(`<div><div>`), _tmpl$3$2 = /* @__PURE__ */ template(`<div><div></div><div>`);
856
841
  function UtilList(props) {
857
842
  const styles = useStyles();
858
843
  return (() => {
@@ -860,7 +845,7 @@ function UtilList(props) {
860
845
  insert(_el$, createComponent(For, {
861
846
  each: UTIL_GROUPS,
862
847
  children: (group) => memo(() => memo(() => props.utilState()[group.key].length > 0)() && (() => {
863
- var _el$2 = _tmpl$2$2(), _el$3 = _el$2.firstChild;
848
+ var _el$2 = _tmpl$2$3(), _el$3 = _el$2.firstChild;
864
849
  insert(_el$3, () => group.label);
865
850
  insert(_el$2, createComponent(For, {
866
851
  get each() {
@@ -913,16 +898,42 @@ function UtilList(props) {
913
898
  }
914
899
  delegateEvents(["click"]);
915
900
  //#endregion
901
+ //#region src/utils/read-pacer-store-state.ts
902
+ /** TanStack Store (`@tanstack/store`) used by pacer utils. */
903
+ function isPacerUtilTanStackStore(x) {
904
+ return typeof x === "object" && x !== null && "subscribe" in x && typeof x.subscribe === "function" && "get" in x && typeof x.get === "function";
905
+ }
906
+ /**
907
+ * Core pacer utils expose TanStack `Store` (`get()` / `state`). React hooks may
908
+ * also expose a reactive `state` field. Use this for devtools UI that needs a
909
+ * plain snapshot (e.g. JsonTree).
910
+ */
911
+ function getPacerUtilStoreState(instance) {
912
+ const store = instance.store;
913
+ if (store && typeof store.get === "function") return store.get();
914
+ if (store && store.state !== void 0) return store.state;
915
+ return instance.state;
916
+ }
917
+ //#endregion
916
918
  //#region src/components/ActionButtons.tsx
917
- var _tmpl$$3 = /* @__PURE__ */ template(`<div>No actions available for this util`), _tmpl$2$1 = /* @__PURE__ */ template(`<div>`), _tmpl$3$1 = /* @__PURE__ */ template(`<button><span>`), _tmpl$4 = /* @__PURE__ */ template(`<button><span></span>Flush`), _tmpl$5 = /* @__PURE__ */ template(`<button><span></span>Cancel`), _tmpl$6 = /* @__PURE__ */ template(`<button><span></span>Reset`), _tmpl$7 = /* @__PURE__ */ template(`<button><span></span>Clear`);
919
+ var _tmpl$$3 = /* @__PURE__ */ template(`<div>No actions available for this util`), _tmpl$2$2 = /* @__PURE__ */ template(`<div>`), _tmpl$3$1 = /* @__PURE__ */ template(`<button><span>`), _tmpl$4 = /* @__PURE__ */ template(`<button><span></span>Flush`), _tmpl$5 = /* @__PURE__ */ template(`<button><span></span>Cancel`), _tmpl$6 = /* @__PURE__ */ template(`<button><span></span>Reset`), _tmpl$7 = /* @__PURE__ */ template(`<button><span></span>Clear`);
918
920
  function ActionButtons(props) {
919
921
  const styles = useStyles();
920
922
  const utilInstance = props.instance;
921
- const utilState = utilInstance?.store?.state;
922
- const hasPending = utilState && "isPending" in utilState;
923
- const hasEmpty = utilState && "isEmpty" in utilState;
924
- const isPending = hasPending ? !!utilState.isPending : false;
925
- const isEmpty = hasEmpty ? utilState.isEmpty : void 0;
923
+ const store = utilInstance?.store;
924
+ const stateAccessor = isPacerUtilTanStackStore(store) ? useStore(store, (s) => s !== null && s !== void 0 && typeof s === "object" ? s : {}) : () => getPacerUtilStoreState(utilInstance);
925
+ const getState = () => stateAccessor();
926
+ const hasPending = () => {
927
+ return "isPending" in getState();
928
+ };
929
+ const isPending = () => {
930
+ const u = getState();
931
+ return "isPending" in u ? !!u.isPending : false;
932
+ };
933
+ const isEmptyFlag = () => {
934
+ const u = getState();
935
+ return "isEmpty" in u ? !!u.isEmpty : false;
936
+ };
926
937
  const hasFlush = typeof utilInstance.flush === "function";
927
938
  const hasCancel = typeof utilInstance.cancel === "function";
928
939
  const hasReset = typeof utilInstance.reset === "function";
@@ -930,45 +941,52 @@ function ActionButtons(props) {
930
941
  const hasStart = typeof utilInstance.start === "function";
931
942
  const hasStop = typeof utilInstance.stop === "function";
932
943
  const hasStartStop = hasStart && hasStop;
933
- const isRunning = utilState?.isRunning ?? true;
934
- if (!hasPending && !hasFlush && !hasCancel && !hasReset && !hasClear) return (() => {
944
+ const isRunning = () => {
945
+ const u = getState();
946
+ return "isRunning" in u ? !!u.isRunning : true;
947
+ };
948
+ if (!hasPending() && !hasFlush && !hasCancel && !hasReset && !hasClear) return (() => {
935
949
  var _el$ = _tmpl$$3();
936
950
  effect(() => className(_el$, styles().sectionEmpty));
937
951
  return _el$;
938
952
  })();
939
953
  const emitName = `d-${props.utilName}`;
954
+ const utilNameLower = props.utilName.toLowerCase();
940
955
  return (() => {
941
- var _el$2 = _tmpl$2$1();
942
- insert(_el$2, hasPending && (() => {
943
- var _el$3 = _tmpl$3$1(), _el$4 = _el$3.firstChild;
944
- _el$3.$$mousedown = () => {
945
- const next = !isPending;
946
- utilInstance.store.setState((prev) => ({
947
- ...prev,
948
- isPending: next
949
- }));
950
- pacerEventClient$1.emit(emitName, utilInstance);
951
- };
952
- insert(_el$3, isPending ? "Restore Pending" : "Trigger Pending", null);
953
- effect((_p$) => {
954
- var _v$ = styles().actionButton, _v$2 = styles().actionDotBlue;
955
- _v$ !== _p$.e && className(_el$3, _p$.e = _v$);
956
- _v$2 !== _p$.t && className(_el$4, _p$.t = _v$2);
957
- return _p$;
958
- }, {
959
- e: void 0,
960
- t: void 0
961
- });
962
- return _el$3;
956
+ var _el$2 = _tmpl$2$2();
957
+ insert(_el$2, (() => {
958
+ var _c$ = memo(() => !!hasPending());
959
+ return () => _c$() && (() => {
960
+ var _el$3 = _tmpl$3$1(), _el$4 = _el$3.firstChild;
961
+ _el$3.$$mousedown = () => {
962
+ const next = !isPending();
963
+ utilInstance.store.setState((prev) => ({
964
+ ...prev,
965
+ isPending: next
966
+ }));
967
+ emitChange(emitName, utilInstance);
968
+ };
969
+ insert(_el$3, () => isPending() ? "Restore Pending" : "Trigger Pending", null);
970
+ effect((_p$) => {
971
+ var _v$ = styles().actionButton, _v$2 = styles().actionDotBlue;
972
+ _v$ !== _p$.e && className(_el$3, _p$.e = _v$);
973
+ _v$2 !== _p$.t && className(_el$4, _p$.t = _v$2);
974
+ return _p$;
975
+ }, {
976
+ e: void 0,
977
+ t: void 0
978
+ });
979
+ return _el$3;
980
+ })();
963
981
  })(), null);
964
982
  insert(_el$2, hasFlush && (() => {
965
983
  var _el$5 = _tmpl$4(), _el$6 = _el$5.firstChild;
966
984
  _el$5.$$mousedown = () => {
967
985
  utilInstance.flush();
968
- pacerEventClient$1.emit(emitName, utilInstance);
986
+ emitChange(emitName, utilInstance);
969
987
  };
970
988
  effect((_p$) => {
971
- var _v$3 = styles().actionButton, _v$4 = props.utilName.toLowerCase().includes("debouncer") && !isPending || props.utilName.toLowerCase().includes("throttler") && !isPending || props.utilName.toLowerCase().includes("batcher") && isEmpty || props.utilName.toLowerCase().includes("queuer") && isEmpty, _v$5 = styles().actionDotGreen;
989
+ var _v$3 = styles().actionButton, _v$4 = utilNameLower.includes("debouncer") && !isPending() || utilNameLower.includes("throttler") && !isPending() || utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), _v$5 = styles().actionDotGreen;
972
990
  _v$3 !== _p$.e && className(_el$5, _p$.e = _v$3);
973
991
  _v$4 !== _p$.t && (_el$5.disabled = _p$.t = _v$4);
974
992
  _v$5 !== _p$.a && className(_el$6, _p$.a = _v$5);
@@ -984,10 +1002,10 @@ function ActionButtons(props) {
984
1002
  var _el$7 = _tmpl$5(), _el$8 = _el$7.firstChild;
985
1003
  _el$7.$$mousedown = () => {
986
1004
  utilInstance.cancel();
987
- pacerEventClient$1.emit(emitName, utilInstance);
1005
+ emitChange(emitName, utilInstance);
988
1006
  };
989
1007
  effect((_p$) => {
990
- var _v$6 = styles().actionButton, _v$7 = props.utilName.toLowerCase().includes("debouncer") && !isPending || props.utilName.toLowerCase().includes("throttler") && !isPending || props.utilName.toLowerCase().includes("batcher") && isEmpty || props.utilName.toLowerCase().includes("queuer") && isEmpty, _v$8 = styles().actionDotRed;
1008
+ var _v$6 = styles().actionButton, _v$7 = utilNameLower.includes("debouncer") && !isPending() || utilNameLower.includes("throttler") && !isPending() || utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), _v$8 = styles().actionDotRed;
991
1009
  _v$6 !== _p$.e && className(_el$7, _p$.e = _v$6);
992
1010
  _v$7 !== _p$.t && (_el$7.disabled = _p$.t = _v$7);
993
1011
  _v$8 !== _p$.a && className(_el$8, _p$.a = _v$8);
@@ -1003,7 +1021,7 @@ function ActionButtons(props) {
1003
1021
  var _el$9 = _tmpl$6(), _el$0 = _el$9.firstChild;
1004
1022
  _el$9.$$mousedown = () => {
1005
1023
  utilInstance.reset();
1006
- pacerEventClient$1.emit(emitName, utilInstance);
1024
+ emitChange(emitName, utilInstance);
1007
1025
  };
1008
1026
  effect((_p$) => {
1009
1027
  var _v$9 = styles().actionButton, _v$0 = styles().actionDotYellow;
@@ -1020,10 +1038,10 @@ function ActionButtons(props) {
1020
1038
  var _el$1 = _tmpl$7(), _el$10 = _el$1.firstChild;
1021
1039
  _el$1.$$mousedown = () => {
1022
1040
  utilInstance.clear();
1023
- pacerEventClient$1.emit(emitName, utilInstance);
1041
+ emitChange(emitName, utilInstance);
1024
1042
  };
1025
1043
  effect((_p$) => {
1026
- var _v$1 = styles().actionButton, _v$10 = props.utilName.toLowerCase().includes("batcher") && isEmpty || props.utilName.toLowerCase().includes("queuer") && isEmpty, _v$11 = styles().actionDotOrange;
1044
+ var _v$1 = styles().actionButton, _v$10 = utilNameLower.includes("batcher") && isEmptyFlag() || utilNameLower.includes("queuer") && isEmptyFlag(), _v$11 = styles().actionDotOrange;
1027
1045
  _v$1 !== _p$.e && className(_el$1, _p$.e = _v$1);
1028
1046
  _v$10 !== _p$.t && (_el$1.disabled = _p$.t = _v$10);
1029
1047
  _v$11 !== _p$.a && className(_el$10, _p$.a = _v$11);
@@ -1038,11 +1056,11 @@ function ActionButtons(props) {
1038
1056
  insert(_el$2, hasStartStop && (() => {
1039
1057
  var _el$11 = _tmpl$3$1(), _el$12 = _el$11.firstChild;
1040
1058
  _el$11.$$mousedown = () => {
1041
- if (isRunning) utilInstance.stop();
1059
+ if (isRunning()) utilInstance.stop();
1042
1060
  else utilInstance.start();
1043
- pacerEventClient$1.emit(emitName, utilInstance);
1061
+ emitChange(emitName, utilInstance);
1044
1062
  };
1045
- insert(_el$11, isRunning ? "Stop" : "Start", null);
1063
+ insert(_el$11, () => isRunning() ? "Stop" : "Start", null);
1046
1064
  effect((_p$) => {
1047
1065
  var _v$12 = styles().actionButton, _v$13 = styles().actionDotPurple;
1048
1066
  _v$12 !== _p$.e && className(_el$11, _p$.e = _v$12);
@@ -1063,56 +1081,46 @@ delegateEvents(["mousedown"]);
1063
1081
  //#region src/components/StateHeader.tsx
1064
1082
  var _tmpl$$2 = /* @__PURE__ */ template(`<div><div></div><div style=display:flex;align-items:center;gap:16px><div><div>Key</div><div></div><div>Last Updated</div><div> (<!>)</div></div><div style=margin-left:auto;font-weight:bold>% reduction`);
1065
1083
  dayjs.extend(relativeTime);
1066
- function StateHeader(props) {
1067
- const styles = useStyles();
1068
- const [now, setNow] = createSignal(Date.now());
1069
- onMount(() => {
1070
- const interval = setInterval(() => {
1071
- setNow(Date.now());
1072
- }, 1e3);
1073
- onCleanup(() => {
1074
- clearInterval(interval);
1075
- });
1076
- });
1077
- const entry = props.selectedInstance();
1078
- if (!entry) return null;
1079
- const key = entry.instance.key;
1080
- const updatedAt = props.utilState().lastUpdatedByKey[key] ?? Date.now();
1084
+ function reductionFromState(entry, state) {
1085
+ if (!state) return 0;
1086
+ const completedExecutions = entry.type.toLowerCase().includes("async") ? Number(state.settleCount) || 0 : Number(state.executionCount) || 0;
1087
+ if (entry.type.toLowerCase().includes("batcher")) {
1088
+ const totalItemsProcessed = Number(state.totalItemsProcessed) || 0;
1089
+ if (totalItemsProcessed === 0) return 0;
1090
+ return Math.round((totalItemsProcessed - completedExecutions) / totalItemsProcessed * 100);
1091
+ }
1092
+ let requestCount = 0;
1093
+ if (state.maybeExecuteCount !== void 0) requestCount = Number(state.maybeExecuteCount) || 0;
1094
+ else if (state.addItemCount !== void 0) requestCount = Number(state.addItemCount) || 0;
1095
+ else return 0;
1096
+ if (requestCount === 0) return 0;
1097
+ const reduction = requestCount - completedExecutions;
1098
+ return Math.round(reduction / requestCount * 100);
1099
+ }
1100
+ function StateHeaderInner(props) {
1101
+ const key = props.entry.instance.key;
1102
+ const updatedAt = () => props.utilState().lastUpdatedByKey[key] ?? Date.now();
1081
1103
  const getRelativeTime = () => {
1082
- const diffMs = now() - updatedAt;
1104
+ const at = updatedAt();
1105
+ const diffMs = props.now() - at;
1083
1106
  const diffSeconds = Math.floor(diffMs / 1e3);
1084
1107
  if (diffSeconds < 60) return `${diffSeconds} second${diffSeconds !== 1 ? "s" : ""} ago`;
1085
- return dayjs(updatedAt).fromNow();
1108
+ return dayjs(at).fromNow();
1086
1109
  };
1087
- const getReductionPercentage = () => {
1088
- const state = entry.instance.store?.state || entry.instance.state;
1089
- if (!state) return 0;
1090
- const completedExecutions = entry.type.toLowerCase().includes("async") ? state.settleCount || 0 : state.executionCount || 0;
1091
- if (entry.type.toLowerCase().includes("batcher")) {
1092
- const totalItemsProcessed = state.totalItemsProcessed || 0;
1093
- if (totalItemsProcessed === 0) return 0;
1094
- return Math.round((totalItemsProcessed - completedExecutions) / totalItemsProcessed * 100);
1095
- }
1096
- let requestCount = 0;
1097
- if (state.maybeExecuteCount !== void 0) requestCount = state.maybeExecuteCount;
1098
- else if (state.addItemCount !== void 0) requestCount = state.addItemCount;
1099
- else return 0;
1100
- if (requestCount === 0) return 0;
1101
- const reduction = requestCount - completedExecutions;
1102
- return Math.round(reduction / requestCount * 100);
1103
- };
1104
- const reductionPercentage = getReductionPercentage();
1110
+ const store = props.entry.instance?.store;
1111
+ const stateAccessor = isPacerUtilTanStackStore(store) ? useStore(store, (s) => s !== null && s !== void 0 && typeof s === "object" ? s : {}) : () => getPacerUtilStoreState(props.entry.instance);
1112
+ const reductionPercentage = () => reductionFromState(props.entry, stateAccessor());
1105
1113
  return (() => {
1106
1114
  var _el$ = _tmpl$$2(), _el$2 = _el$.firstChild, _el$4 = _el$2.nextSibling.firstChild, _el$5 = _el$4.firstChild, _el$6 = _el$5.nextSibling, _el$7 = _el$6.nextSibling, _el$8 = _el$7.nextSibling, _el$9 = _el$8.firstChild, _el$1 = _el$9.nextSibling;
1107
1115
  _el$1.nextSibling;
1108
1116
  var _el$10 = _el$4.nextSibling, _el$11 = _el$10.firstChild;
1109
- insert(_el$2, () => entry.type);
1117
+ insert(_el$2, () => props.entry.type);
1110
1118
  insert(_el$6, key);
1111
- insert(_el$8, () => new Date(updatedAt).toLocaleTimeString(), _el$9);
1119
+ insert(_el$8, () => new Date(updatedAt()).toLocaleTimeString(), _el$9);
1112
1120
  insert(_el$8, getRelativeTime, _el$1);
1113
1121
  insert(_el$10, reductionPercentage, _el$11);
1114
1122
  effect((_p$) => {
1115
- var _v$ = styles().stateHeader, _v$2 = styles().stateTitle, _v$3 = styles().infoGrid, _v$4 = styles().infoLabel, _v$5 = styles().infoValueMono, _v$6 = styles().infoLabel, _v$7 = styles().infoValueMono, _v$8 = styles().infoValueMono;
1123
+ var _v$ = props.styles.stateHeader, _v$2 = props.styles.stateTitle, _v$3 = props.styles.infoGrid, _v$4 = props.styles.infoLabel, _v$5 = props.styles.infoValueMono, _v$6 = props.styles.infoLabel, _v$7 = props.styles.infoValueMono, _v$8 = props.styles.infoValueMono;
1116
1124
  _v$ !== _p$.e && className(_el$, _p$.e = _v$);
1117
1125
  _v$2 !== _p$.t && className(_el$2, _p$.t = _v$2);
1118
1126
  _v$3 !== _p$.a && className(_el$4, _p$.a = _v$3);
@@ -1135,9 +1143,57 @@ function StateHeader(props) {
1135
1143
  return _el$;
1136
1144
  })();
1137
1145
  }
1146
+ function StateHeader(props) {
1147
+ const styles = useStyles();
1148
+ const [now, setNow] = createSignal(Date.now());
1149
+ onMount(() => {
1150
+ const interval = setInterval(() => {
1151
+ setNow(Date.now());
1152
+ }, 1e3);
1153
+ onCleanup(() => {
1154
+ clearInterval(interval);
1155
+ });
1156
+ });
1157
+ return createComponent(Show, {
1158
+ get when() {
1159
+ return props.selectedInstance();
1160
+ },
1161
+ children: (entry) => createComponent(StateHeaderInner, {
1162
+ get entry() {
1163
+ return entry();
1164
+ },
1165
+ get styles() {
1166
+ return styles();
1167
+ },
1168
+ now,
1169
+ get utilState() {
1170
+ return props.utilState;
1171
+ }
1172
+ })
1173
+ });
1174
+ }
1175
+ //#endregion
1176
+ //#region src/components/UtilStateJsonTree.tsx
1177
+ /**
1178
+ * Subscribes to the util's TanStack Store so the tree updates live (same
1179
+ * idea as {@link UtilList} status). A plain snapshot would only refresh when the
1180
+ * devtools shell re-renders, which does not happen on nested store updates.
1181
+ */
1182
+ function UtilStateJsonTree(props) {
1183
+ const store = props.instance?.store;
1184
+ if (isPacerUtilTanStackStore(store)) {
1185
+ const snapshot = useStore(store, (s) => s);
1186
+ return createComponent(JsonTree, { get value() {
1187
+ return snapshot();
1188
+ } });
1189
+ }
1190
+ return createComponent(JsonTree, { get value() {
1191
+ return getPacerUtilStoreState(props.instance);
1192
+ } });
1193
+ }
1138
1194
  //#endregion
1139
1195
  //#region src/components/DetailsPanel.tsx
1140
- var _tmpl$$1 = /* @__PURE__ */ template(`<div>`), _tmpl$2 = /* @__PURE__ */ template(`<div>Select a util from the left panel to view its state`), _tmpl$3 = /* @__PURE__ */ template(`<div><div><div>Actions</div></div><div><div>State</div><div></div></div><div><div>Options</div><div>`);
1196
+ var _tmpl$$1 = /* @__PURE__ */ template(`<div>`), _tmpl$2$1 = /* @__PURE__ */ template(`<div>Select a util from the left panel to view its state`), _tmpl$3 = /* @__PURE__ */ template(`<div><div><div>Actions</div></div><div><div>State</div><div></div></div><div><div>Options</div><div>`);
1141
1197
  function DetailsPanel(props) {
1142
1198
  const styles = useStyles();
1143
1199
  return (() => {
@@ -1145,7 +1201,7 @@ function DetailsPanel(props) {
1145
1201
  insert(_el$, () => {
1146
1202
  const entry = props.selectedInstance();
1147
1203
  if (!entry) return (() => {
1148
- var _el$2 = _tmpl$2();
1204
+ var _el$2 = _tmpl$2$1();
1149
1205
  effect(() => className(_el$2, styles().noSelection));
1150
1206
  return _el$2;
1151
1207
  })();
@@ -1166,8 +1222,8 @@ function DetailsPanel(props) {
1166
1222
  return entry.type;
1167
1223
  }
1168
1224
  }), null);
1169
- insert(_el$8, createComponent(JsonTree, { get value() {
1170
- return entry.instance.store?.state;
1225
+ insert(_el$8, createComponent(UtilStateJsonTree, { get instance() {
1226
+ return entry.instance;
1171
1227
  } }));
1172
1228
  insert(_el$1, createComponent(JsonTree, { get value() {
1173
1229
  return entry.instance.options;
@@ -1204,7 +1260,18 @@ function DetailsPanel(props) {
1204
1260
  }
1205
1261
  //#endregion
1206
1262
  //#region src/components/Shell.tsx
1207
- var _tmpl$ = /* @__PURE__ */ template(`<div><div style=min-width:150px;max-width:800px></div><div></div><div style=flex:1><div>Details`);
1263
+ var _tmpl$ = /* @__PURE__ */ template(`<div><div style=min-width:150px;max-width:800px></div><div></div><div style=flex:1><div>Details`), _tmpl$2 = /* @__PURE__ */ template(`<div>`);
1264
+ function readResizeObserverBlockSize(entry) {
1265
+ const [box] = entry.borderBoxSize;
1266
+ if (box !== void 0) return box.blockSize;
1267
+ return entry.contentRect.height;
1268
+ }
1269
+ /** Outer plugin slot from TanStack Devtools (not the inner React mount div, which can report a tiny height). */
1270
+ function resolvePluginHost(el) {
1271
+ const byId = el.closest("[id^=\"plugin-container-\"]");
1272
+ if (byId) return byId;
1273
+ return el.parentElement?.parentElement?.parentElement ?? el.parentElement?.parentElement ?? el.parentElement;
1274
+ }
1208
1275
  function Shell() {
1209
1276
  const styles = useStyles();
1210
1277
  const state = usePacerDevtoolsState();
@@ -1212,6 +1279,8 @@ function Shell() {
1212
1279
  const [selectedKey, setSelectedKey] = createSignal(null);
1213
1280
  const [leftPanelWidth, setLeftPanelWidth] = createSignal(300);
1214
1281
  const [isDragging, setIsDragging] = createSignal(false);
1282
+ const [shellRootEl, setShellRootEl] = createSignal(null);
1283
+ const [slotHeightPx, setSlotHeightPx] = createSignal(void 0);
1215
1284
  const selectedInstance = createMemo(() => {
1216
1285
  const key = selectedKey();
1217
1286
  if (!key) return null;
@@ -1254,47 +1323,91 @@ function Shell() {
1254
1323
  document.removeEventListener("mousemove", handleMouseMove);
1255
1324
  document.removeEventListener("mouseup", handleMouseUp);
1256
1325
  });
1257
- return createComponent(MainPanel, { get children() {
1258
- return [createComponent(Header, { get children() {
1259
- return createComponent(HeaderLogo, {
1260
- flavor: {
1261
- light: "#84cc16",
1262
- dark: "#84cc16"
1263
- },
1264
- children: "TanStack Pacer"
1265
- });
1266
- } }), (() => {
1267
- var _el$ = _tmpl$(), _el$2 = _el$.firstChild, _el$3 = _el$2.nextSibling, _el$4 = _el$3.nextSibling, _el$5 = _el$4.firstChild;
1268
- insert(_el$2, createComponent(UtilList, {
1269
- selectedKey,
1270
- setSelectedKey,
1271
- utilState
1272
- }));
1273
- _el$3.$$mousedown = handleMouseDown;
1274
- insert(_el$4, createComponent(DetailsPanel, {
1275
- selectedInstance,
1276
- utilState
1277
- }), null);
1278
- effect((_p$) => {
1279
- var _v$ = styles().mainContainer, _v$2 = styles().leftPanel, _v$3 = `${leftPanelWidth()}px`, _v$4 = `${styles().dragHandle} ${isDragging() ? "dragging" : ""}`, _v$5 = styles().rightPanel, _v$6 = styles().panelHeader;
1280
- _v$ !== _p$.e && className(_el$, _p$.e = _v$);
1281
- _v$2 !== _p$.t && className(_el$2, _p$.t = _v$2);
1282
- _v$3 !== _p$.a && setStyleProperty(_el$2, "width", _p$.a = _v$3);
1283
- _v$4 !== _p$.o && className(_el$3, _p$.o = _v$4);
1284
- _v$5 !== _p$.i && className(_el$4, _p$.i = _v$5);
1285
- _v$6 !== _p$.n && className(_el$5, _p$.n = _v$6);
1286
- return _p$;
1287
- }, {
1288
- e: void 0,
1289
- t: void 0,
1290
- a: void 0,
1291
- o: void 0,
1292
- i: void 0,
1293
- n: void 0
1294
- });
1295
- return _el$;
1296
- })()];
1297
- } });
1326
+ /**
1327
+ * TanStack Devtools plugin slots use height: 100% without min-height: 0 on flex
1328
+ * ancestors, so percentage height often never constrains our tree. Observing the
1329
+ * outer `plugin-container-*` host (not the immediate React mount wrapper) and
1330
+ * setting an explicit pixel height makes inner overflow-y regions scroll without
1331
+ * capping to the mount div’s collapsed height.
1332
+ */
1333
+ createEffect(() => {
1334
+ const el = shellRootEl();
1335
+ if (!el || typeof ResizeObserver === "undefined") return;
1336
+ const pluginSlot = resolvePluginHost(el);
1337
+ if (!pluginSlot) return;
1338
+ const ro = new ResizeObserver((entries) => {
1339
+ const entry = entries[0];
1340
+ if (!entry) return;
1341
+ const target = entry.target;
1342
+ const h = target.clientHeight > 0 ? target.clientHeight : readResizeObserverBlockSize(entry);
1343
+ if (h > 0) setSlotHeightPx(Math.round(h));
1344
+ });
1345
+ ro.observe(pluginSlot);
1346
+ onCleanup(() => ro.disconnect());
1347
+ });
1348
+ return (() => {
1349
+ var _el$ = _tmpl$2();
1350
+ use(setShellRootEl, _el$);
1351
+ insert(_el$, createComponent(MainPanel, {
1352
+ get ["class"]() {
1353
+ return styles().mainPanelShell;
1354
+ },
1355
+ get children() {
1356
+ return [createComponent(Header, { get children() {
1357
+ return createComponent(HeaderLogo, {
1358
+ flavor: {
1359
+ light: "#84cc16",
1360
+ dark: "#84cc16"
1361
+ },
1362
+ children: "TanStack Pacer"
1363
+ });
1364
+ } }), (() => {
1365
+ var _el$2 = _tmpl$(), _el$3 = _el$2.firstChild, _el$4 = _el$3.nextSibling, _el$5 = _el$4.nextSibling, _el$6 = _el$5.firstChild;
1366
+ insert(_el$3, createComponent(UtilList, {
1367
+ selectedKey,
1368
+ setSelectedKey,
1369
+ utilState
1370
+ }));
1371
+ _el$4.$$mousedown = handleMouseDown;
1372
+ insert(_el$5, createComponent(DetailsPanel, {
1373
+ selectedInstance,
1374
+ utilState
1375
+ }), null);
1376
+ effect((_p$) => {
1377
+ var _v$ = styles().mainContainer, _v$2 = styles().leftPanel, _v$3 = `${leftPanelWidth()}px`, _v$4 = `${styles().dragHandle} ${isDragging() ? "dragging" : ""}`, _v$5 = styles().rightPanel, _v$6 = styles().panelHeader;
1378
+ _v$ !== _p$.e && className(_el$2, _p$.e = _v$);
1379
+ _v$2 !== _p$.t && className(_el$3, _p$.t = _v$2);
1380
+ _v$3 !== _p$.a && setStyleProperty(_el$3, "width", _p$.a = _v$3);
1381
+ _v$4 !== _p$.o && className(_el$4, _p$.o = _v$4);
1382
+ _v$5 !== _p$.i && className(_el$5, _p$.i = _v$5);
1383
+ _v$6 !== _p$.n && className(_el$6, _p$.n = _v$6);
1384
+ return _p$;
1385
+ }, {
1386
+ e: void 0,
1387
+ t: void 0,
1388
+ a: void 0,
1389
+ o: void 0,
1390
+ i: void 0,
1391
+ n: void 0
1392
+ });
1393
+ return _el$2;
1394
+ })()];
1395
+ }
1396
+ }));
1397
+ effect((_p$) => {
1398
+ var _v$7 = styles().shellRoot, _v$8 = { ...slotHeightPx() !== void 0 ? {
1399
+ height: `${slotHeightPx()}px`,
1400
+ "max-height": `${slotHeightPx()}px`
1401
+ } : {} };
1402
+ _v$7 !== _p$.e && className(_el$, _p$.e = _v$7);
1403
+ _p$.t = style(_el$, _v$8, _p$.t);
1404
+ return _p$;
1405
+ }, {
1406
+ e: void 0,
1407
+ t: void 0
1408
+ });
1409
+ return _el$;
1410
+ })();
1298
1411
  }
1299
1412
  delegateEvents(["mousedown"]);
1300
1413
  //#endregion