@tanstack/pacer-devtools 1.1.5 → 1.2.0
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.
- package/dist/{components-DDmnxkL2.js → components-CGEHIK6F.js} +344 -231
- package/dist/{components-ByKT1JiI.js → components-vap4_VJT.js} +267 -166
- package/dist/index.d.ts +4 -8
- package/dist/index.js +3 -1
- package/dist/production/index.d.ts +3 -15
- package/dist/production/index.js +3 -1
- package/dist/production/server.js +3 -1
- package/dist/server.js +3 -1
- package/package.json +6 -6
- package/src/PacerContextProvider.tsx +96 -127
- package/src/components/ActionButtons.tsx +57 -33
- package/src/components/DetailsPanel.tsx +2 -1
- package/src/components/Shell.tsx +103 -34
- package/src/components/StateHeader.tsx +96 -66
- package/src/components/UtilStateJsonTree.tsx +25 -0
- package/src/core.tsx +4 -4
- package/src/index.ts +1 -1
- package/src/styles/use-styles.ts +22 -1
- package/src/utils/read-pacer-store-state.ts +33 -0
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
const interval = setInterval(() => {
|
|
19
|
-
setNow(Date.now())
|
|
20
|
-
}, 1000)
|
|
37
|
+
let requestCount = 0
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
28
|
-
|
|
49
|
+
const reduction = requestCount - completedExecutions
|
|
50
|
+
return Math.round((reduction / requestCount) * 100)
|
|
51
|
+
}
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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(
|
|
71
|
+
return dayjs(at).fromNow()
|
|
42
72
|
}
|
|
43
73
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 =
|
|
84
|
+
const reductionPercentage = () =>
|
|
85
|
+
reductionFromState(props.entry, stateAccessor())
|
|
84
86
|
|
|
85
87
|
return (
|
|
86
|
-
<div class={styles
|
|
87
|
-
<div class={styles
|
|
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
|
|
90
|
-
<div class={styles
|
|
91
|
-
<div class={styles
|
|
92
|
-
<div class={styles
|
|
93
|
-
<div class={styles
|
|
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
|
|
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
|
|
6
|
-
() => import('./components'),
|
|
7
|
-
)
|
|
6
|
+
const coreClasses = constructCoreClass(() => import('./components'))
|
|
8
7
|
|
|
9
|
-
export
|
|
8
|
+
export const PacerDevtoolsCore: ClassType = coreClasses[0]
|
|
9
|
+
export const PacerDevtoolsCoreNoOp: ClassType = coreClasses[1]
|
package/src/index.ts
CHANGED
package/src/styles/use-styles.ts
CHANGED
|
@@ -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
|
+
}
|