@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,7 +1,12 @@
1
1
  import dayjs from 'dayjs'
2
2
  import relativeTime from 'dayjs/plugin/relativeTime.js'
3
- import { createSignal, onCleanup, onMount } from 'solid-js'
3
+ import { Show, createSignal, onCleanup, onMount } from 'solid-js'
4
+ import { useStore } from '@tanstack/solid-store'
4
5
  import { useStyles } from '../styles/use-styles'
6
+ import {
7
+ getPacerUtilStoreState,
8
+ isPacerUtilTanStackStore,
9
+ } from '../utils/read-pacer-store-state'
5
10
 
6
11
  dayjs.extend(relativeTime)
7
12
 
@@ -10,97 +15,122 @@ type StateHeaderProps = {
10
15
  utilState: () => { lastUpdatedByKey: Record<string, number> }
11
16
  }
12
17
 
13
- export function StateHeader(props: StateHeaderProps) {
14
- const styles = useStyles()
15
- const [now, setNow] = createSignal(Date.now())
18
+ function reductionFromState(
19
+ entry: { type: string },
20
+ state: Record<string, unknown> | null | undefined,
21
+ ): number {
22
+ if (!state) return 0
23
+
24
+ const isAsync = entry.type.toLowerCase().includes('async')
25
+ const completedExecutions = isAsync
26
+ ? Number(state.settleCount) || 0
27
+ : Number(state.executionCount) || 0
28
+
29
+ if (entry.type.toLowerCase().includes('batcher')) {
30
+ const totalItemsProcessed = Number(state.totalItemsProcessed) || 0
31
+ if (totalItemsProcessed === 0) return 0
32
+ return Math.round(
33
+ ((totalItemsProcessed - completedExecutions) / totalItemsProcessed) * 100,
34
+ )
35
+ }
16
36
 
17
- onMount(() => {
18
- const interval = setInterval(() => {
19
- setNow(Date.now())
20
- }, 1000)
37
+ let requestCount = 0
21
38
 
22
- onCleanup(() => {
23
- clearInterval(interval)
24
- })
25
- })
39
+ if (state.maybeExecuteCount !== undefined) {
40
+ requestCount = Number(state.maybeExecuteCount) || 0
41
+ } else if (state.addItemCount !== undefined) {
42
+ requestCount = Number(state.addItemCount) || 0
43
+ } else {
44
+ return 0
45
+ }
46
+
47
+ if (requestCount === 0) return 0
26
48
 
27
- const entry = props.selectedInstance()
28
- if (!entry) return null
49
+ const reduction = requestCount - completedExecutions
50
+ return Math.round((reduction / requestCount) * 100)
51
+ }
29
52
 
30
- const key = entry.instance.key as string
31
- const updatedAt = props.utilState().lastUpdatedByKey[key] ?? Date.now()
53
+ function StateHeaderInner(props: {
54
+ entry: { instance: any; type: string }
55
+ styles: ReturnType<ReturnType<typeof useStyles>>
56
+ now: () => number
57
+ utilState: () => { lastUpdatedByKey: Record<string, number> }
58
+ }) {
59
+ const key = props.entry.instance.key as string
60
+ const updatedAt = () => props.utilState().lastUpdatedByKey[key] ?? Date.now()
32
61
 
33
62
  const getRelativeTime = () => {
34
- const diffMs = now() - updatedAt
63
+ const at = updatedAt()
64
+ const diffMs = props.now() - at
35
65
  const diffSeconds = Math.floor(diffMs / 1000)
36
66
 
37
67
  if (diffSeconds < 60) {
38
68
  return `${diffSeconds} second${diffSeconds !== 1 ? 's' : ''} ago`
39
69
  }
40
70
 
41
- return dayjs(updatedAt).fromNow()
71
+ return dayjs(at).fromNow()
42
72
  }
43
73
 
44
- const getReductionPercentage = () => {
45
- const state = entry.instance.store?.state || entry.instance.state
46
-
47
- if (!state) return 0
48
-
49
- // Use settleCount for async utilities, executionCount for sync utilities
50
- const isAsync = entry.type.toLowerCase().includes('async')
51
- const completedExecutions = isAsync
52
- ? state.settleCount || 0
53
- : state.executionCount || 0
54
-
55
- // For batchers, calculate reduction based on items processed vs executions
56
- if (entry.type.toLowerCase().includes('batcher')) {
57
- const totalItemsProcessed = state.totalItemsProcessed || 0
58
- if (totalItemsProcessed === 0) return 0
59
- return Math.round(
60
- ((totalItemsProcessed - completedExecutions) / totalItemsProcessed) *
61
- 100,
74
+ const store = props.entry.instance?.store
75
+ const stateAccessor = isPacerUtilTanStackStore(store)
76
+ ? useStore(store as never, (s: unknown) =>
77
+ s !== null && s !== undefined && typeof s === 'object'
78
+ ? (s as Record<string, unknown>)
79
+ : {},
62
80
  )
63
- }
64
-
65
- // For other utilities, calculate reduction based on request tracking
66
- let requestCount = 0
67
-
68
- if (state.maybeExecuteCount !== undefined) {
69
- requestCount = state.maybeExecuteCount
70
- } else if (state.addItemCount !== undefined) {
71
- requestCount = state.addItemCount
72
- } else {
73
- // For utilities that don't track requests, show 0%
74
- return 0
75
- }
76
-
77
- if (requestCount === 0) return 0
78
-
79
- const reduction = requestCount - completedExecutions
80
- return Math.round((reduction / requestCount) * 100)
81
- }
81
+ : () =>
82
+ getPacerUtilStoreState(props.entry.instance) as Record<string, unknown>
82
83
 
83
- const reductionPercentage = getReductionPercentage()
84
+ const reductionPercentage = () =>
85
+ reductionFromState(props.entry, stateAccessor())
84
86
 
85
87
  return (
86
- <div class={styles().stateHeader}>
87
- <div class={styles().stateTitle}>{entry.type}</div>
88
+ <div class={props.styles.stateHeader}>
89
+ <div class={props.styles.stateTitle}>{props.entry.type}</div>
88
90
  <div style={{ display: 'flex', 'align-items': 'center', gap: '16px' }}>
89
- <div class={styles().infoGrid}>
90
- <div class={styles().infoLabel}>Key</div>
91
- <div class={styles().infoValueMono}>{key}</div>
92
- <div class={styles().infoLabel}>Last Updated</div>
93
- <div class={styles().infoValueMono}>
94
- {new Date(updatedAt).toLocaleTimeString()} ({getRelativeTime()})
91
+ <div class={props.styles.infoGrid}>
92
+ <div class={props.styles.infoLabel}>Key</div>
93
+ <div class={props.styles.infoValueMono}>{key}</div>
94
+ <div class={props.styles.infoLabel}>Last Updated</div>
95
+ <div class={props.styles.infoValueMono}>
96
+ {new Date(updatedAt()).toLocaleTimeString()} ({getRelativeTime()})
95
97
  </div>
96
98
  </div>
97
99
  <div
98
- class={styles().infoValueMono}
100
+ class={props.styles.infoValueMono}
99
101
  style={{ 'margin-left': 'auto', 'font-weight': 'bold' }}
100
102
  >
101
- {reductionPercentage}% reduction
103
+ {reductionPercentage()}% reduction
102
104
  </div>
103
105
  </div>
104
106
  </div>
105
107
  )
106
108
  }
109
+
110
+ export function StateHeader(props: StateHeaderProps) {
111
+ const styles = useStyles()
112
+ const [now, setNow] = createSignal(Date.now())
113
+
114
+ onMount(() => {
115
+ const interval = setInterval(() => {
116
+ setNow(Date.now())
117
+ }, 1000)
118
+
119
+ onCleanup(() => {
120
+ clearInterval(interval)
121
+ })
122
+ })
123
+
124
+ return (
125
+ <Show when={props.selectedInstance()}>
126
+ {(entry) => (
127
+ <StateHeaderInner
128
+ entry={entry()}
129
+ styles={styles()}
130
+ now={now}
131
+ utilState={props.utilState}
132
+ />
133
+ )}
134
+ </Show>
135
+ )
136
+ }
@@ -0,0 +1,25 @@
1
+ import { JsonTree } from '@tanstack/devtools-ui'
2
+ import { useStore } from '@tanstack/solid-store'
3
+ import {
4
+ getPacerUtilStoreState,
5
+ isPacerUtilTanStackStore,
6
+ } from '../utils/read-pacer-store-state'
7
+ import type { JSX } from 'solid-js'
8
+
9
+ type UtilStateJsonTreeProps = {
10
+ instance: any
11
+ }
12
+
13
+ /**
14
+ * Subscribes to the util's TanStack Store so the tree updates live (same
15
+ * idea as {@link UtilList} status). A plain snapshot would only refresh when the
16
+ * devtools shell re-renders, which does not happen on nested store updates.
17
+ */
18
+ export function UtilStateJsonTree(props: UtilStateJsonTreeProps): JSX.Element {
19
+ const store = props.instance?.store
20
+ if (isPacerUtilTanStackStore(store)) {
21
+ const snapshot = useStore(store as never, (s: unknown) => s)
22
+ return <JsonTree value={snapshot()} />
23
+ }
24
+ return <JsonTree value={getPacerUtilStoreState(props.instance)} />
25
+ }
package/src/core.tsx CHANGED
@@ -1,9 +1,9 @@
1
1
  import { constructCoreClass } from '@tanstack/devtools-utils/solid'
2
+ import type { ClassType } from '@tanstack/devtools-utils/solid'
2
3
 
3
4
  export interface PacerDevtoolsInit {}
4
5
 
5
- const [PacerDevtoolsCore, PacerDevtoolsCoreNoOp] = constructCoreClass(
6
- () => import('./components'),
7
- )
6
+ const coreClasses = constructCoreClass(() => import('./components'))
8
7
 
9
- export { PacerDevtoolsCore, PacerDevtoolsCoreNoOp }
8
+ export const PacerDevtoolsCore: ClassType = coreClasses[0]
9
+ export const PacerDevtoolsCoreNoOp: ClassType = coreClasses[1]
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as Devtools from './core'
4
4
 
5
- export const PacerDevtoolsCore =
5
+ export const PacerDevtoolsCore: typeof Devtools.PacerDevtoolsCore =
6
6
  process.env.NODE_ENV !== 'development'
7
7
  ? Devtools.PacerDevtoolsCoreNoOp
8
8
  : Devtools.PacerDevtoolsCore
@@ -10,9 +10,30 @@ const stylesFactory = (theme: 'light' | 'dark') => {
10
10
  const t = (light: string, dark: string) => (theme === 'light' ? light : dark)
11
11
 
12
12
  return {
13
+ /**
14
+ * Fills the devtools-utils portal div (height: 100%) and is a flex parent so MainPanel
15
+ * gets a real flex height budget (not just height: 100% without a flex chain).
16
+ */
17
+ shellRoot: css`
18
+ display: flex;
19
+ flex-direction: column;
20
+ box-sizing: border-box;
21
+ width: 100%;
22
+ height: 100%;
23
+ min-height: 0;
24
+ overflow: hidden;
25
+ `,
26
+ /** Overrides devtools-ui MainPanel overflow-y: auto so inner columns own scrolling */
27
+ mainPanelShell: css`
28
+ display: flex;
29
+ flex-direction: column;
30
+ flex: 1 1 0%;
31
+ min-height: 0;
32
+ overflow: hidden !important;
33
+ `,
13
34
  mainContainer: css`
14
35
  display: flex;
15
- flex: 1;
36
+ flex: 1 1 0%;
16
37
  min-height: 0;
17
38
  overflow: hidden;
18
39
  padding: ${size[2]};
@@ -0,0 +1,33 @@
1
+ /** TanStack Store (`@tanstack/store`) used by pacer utils. */
2
+ export function isPacerUtilTanStackStore(x: unknown): x is {
3
+ subscribe: (cb: (s: unknown) => void) => { unsubscribe: () => void }
4
+ get: () => unknown
5
+ } {
6
+ return (
7
+ typeof x === 'object' &&
8
+ x !== null &&
9
+ 'subscribe' in x &&
10
+ typeof (x as { subscribe: unknown }).subscribe === 'function' &&
11
+ 'get' in x &&
12
+ typeof (x as { get: unknown }).get === 'function'
13
+ )
14
+ }
15
+
16
+ /**
17
+ * Core pacer utils expose TanStack `Store` (`get()` / `state`). React hooks may
18
+ * also expose a reactive `state` field. Use this for devtools UI that needs a
19
+ * plain snapshot (e.g. JsonTree).
20
+ */
21
+ export function getPacerUtilStoreState(instance: {
22
+ store?: { get?: () => unknown; state?: unknown }
23
+ state?: unknown
24
+ }): unknown {
25
+ const store = instance.store
26
+ if (store && typeof store.get === 'function') {
27
+ return store.get()
28
+ }
29
+ if (store && store.state !== undefined) {
30
+ return store.state
31
+ }
32
+ return instance.state
33
+ }