@tanstack/pacer-devtools 0.1.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/LICENSE +21 -0
- package/dist/cjs/PacerContextProvider.cjs +156 -0
- package/dist/cjs/PacerContextProvider.cjs.map +1 -0
- package/dist/cjs/PacerContextProvider.d.cts +19 -0
- package/dist/cjs/PacerDevtools.cjs +13 -0
- package/dist/cjs/PacerDevtools.cjs.map +1 -0
- package/dist/cjs/PacerDevtools.d.cts +1 -0
- package/dist/cjs/components/ActionButtons.cjs +157 -0
- package/dist/cjs/components/ActionButtons.cjs.map +1 -0
- package/dist/cjs/components/ActionButtons.d.cts +6 -0
- package/dist/cjs/components/DetailsPanel.cjs +80 -0
- package/dist/cjs/components/DetailsPanel.cjs.map +1 -0
- package/dist/cjs/components/DetailsPanel.d.cts +11 -0
- package/dist/cjs/components/Shell.cjs +102 -0
- package/dist/cjs/components/Shell.cjs.map +1 -0
- package/dist/cjs/components/Shell.d.cts +1 -0
- package/dist/cjs/components/StateHeader.cjs +95 -0
- package/dist/cjs/components/StateHeader.cjs.map +1 -0
- package/dist/cjs/components/StateHeader.d.cts +11 -0
- package/dist/cjs/components/UtilList.cjs +70 -0
- package/dist/cjs/components/UtilList.cjs.map +1 -0
- package/dist/cjs/components/UtilList.d.cts +8 -0
- package/dist/cjs/components/util-groups.cjs +40 -0
- package/dist/cjs/components/util-groups.cjs.map +1 -0
- package/dist/cjs/components/util-groups.d.cts +8 -0
- package/dist/cjs/core.cjs +43 -0
- package/dist/cjs/core.cjs.map +1 -0
- package/dist/cjs/core.d.cts +8 -0
- package/dist/cjs/index.cjs +15 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +3 -0
- package/dist/cjs/production.cjs +7 -0
- package/dist/cjs/production.cjs.map +1 -0
- package/dist/cjs/production.d.cts +3 -0
- package/dist/cjs/styles/tokens.cjs +90 -0
- package/dist/cjs/styles/tokens.cjs.map +1 -0
- package/dist/cjs/styles/tokens.d.cts +298 -0
- package/dist/cjs/styles/use-styles.cjs +397 -0
- package/dist/cjs/styles/use-styles.cjs.map +1 -0
- package/dist/cjs/styles/use-styles.d.cts +42 -0
- package/dist/esm/PacerContextProvider.d.ts +19 -0
- package/dist/esm/PacerContextProvider.js +156 -0
- package/dist/esm/PacerContextProvider.js.map +1 -0
- package/dist/esm/PacerDevtools.d.ts +1 -0
- package/dist/esm/PacerDevtools.js +14 -0
- package/dist/esm/PacerDevtools.js.map +1 -0
- package/dist/esm/components/ActionButtons.d.ts +6 -0
- package/dist/esm/components/ActionButtons.js +157 -0
- package/dist/esm/components/ActionButtons.js.map +1 -0
- package/dist/esm/components/DetailsPanel.d.ts +11 -0
- package/dist/esm/components/DetailsPanel.js +80 -0
- package/dist/esm/components/DetailsPanel.js.map +1 -0
- package/dist/esm/components/Shell.d.ts +1 -0
- package/dist/esm/components/Shell.js +102 -0
- package/dist/esm/components/Shell.js.map +1 -0
- package/dist/esm/components/StateHeader.d.ts +11 -0
- package/dist/esm/components/StateHeader.js +95 -0
- package/dist/esm/components/StateHeader.js.map +1 -0
- package/dist/esm/components/UtilList.d.ts +8 -0
- package/dist/esm/components/UtilList.js +70 -0
- package/dist/esm/components/UtilList.js.map +1 -0
- package/dist/esm/components/util-groups.d.ts +8 -0
- package/dist/esm/components/util-groups.js +40 -0
- package/dist/esm/components/util-groups.js.map +1 -0
- package/dist/esm/core.d.ts +8 -0
- package/dist/esm/core.js +42 -0
- package/dist/esm/core.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/production.d.ts +3 -0
- package/dist/esm/production.js +7 -0
- package/dist/esm/production.js.map +1 -0
- package/dist/esm/styles/tokens.d.ts +298 -0
- package/dist/esm/styles/tokens.js +90 -0
- package/dist/esm/styles/tokens.js.map +1 -0
- package/dist/esm/styles/use-styles.d.ts +42 -0
- package/dist/esm/styles/use-styles.js +380 -0
- package/dist/esm/styles/use-styles.js.map +1 -0
- package/package.json +84 -0
- package/src/PacerContextProvider.tsx +203 -0
- package/src/PacerDevtools.tsx +10 -0
- package/src/components/ActionButtons.tsx +143 -0
- package/src/components/DetailsPanel.tsx +61 -0
- package/src/components/Shell.tsx +102 -0
- package/src/components/StateHeader.tsx +106 -0
- package/src/components/UtilList.tsx +62 -0
- package/src/components/util-groups.ts +54 -0
- package/src/core.tsx +41 -0
- package/src/index.ts +17 -0
- package/src/production.ts +7 -0
- package/src/styles/tokens.ts +305 -0
- package/src/styles/use-styles.ts +379 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { pacerEventClient } from '@tanstack/pacer'
|
|
2
|
+
import { useStyles } from '../styles/use-styles'
|
|
3
|
+
import type { PacerEventName } from '@tanstack/pacer'
|
|
4
|
+
|
|
5
|
+
type ActionButtonsProps = {
|
|
6
|
+
instance: any
|
|
7
|
+
utilName: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ActionButtons(props: ActionButtonsProps) {
|
|
11
|
+
const styles = useStyles()
|
|
12
|
+
const utilInstance = props.instance
|
|
13
|
+
const utilState = utilInstance?.store?.state
|
|
14
|
+
const hasPending = utilState && 'isPending' in utilState
|
|
15
|
+
const hasEmpty = utilState && 'isEmpty' in utilState
|
|
16
|
+
const isPending = hasPending ? !!utilState.isPending : false
|
|
17
|
+
const isEmpty = hasEmpty ? utilState.isEmpty : undefined
|
|
18
|
+
|
|
19
|
+
const hasFlush = typeof utilInstance.flush === 'function'
|
|
20
|
+
const hasCancel = typeof utilInstance.cancel === 'function'
|
|
21
|
+
const hasReset = typeof utilInstance.reset === 'function'
|
|
22
|
+
const hasClear = typeof utilInstance.clear === 'function'
|
|
23
|
+
const hasStart = typeof utilInstance.start === 'function'
|
|
24
|
+
const hasStop = typeof utilInstance.stop === 'function'
|
|
25
|
+
const hasStartStop = hasStart && hasStop
|
|
26
|
+
|
|
27
|
+
const isRunning = utilState?.isRunning ?? true
|
|
28
|
+
|
|
29
|
+
if (!hasPending && !hasFlush && !hasCancel && !hasReset && !hasClear) {
|
|
30
|
+
return (
|
|
31
|
+
<div class={styles().sectionEmpty}>
|
|
32
|
+
No actions available for this util
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const emitName = `d-${props.utilName}` as PacerEventName
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div class={styles().actionsRow}>
|
|
41
|
+
{hasPending && (
|
|
42
|
+
<button
|
|
43
|
+
class={styles().actionButton}
|
|
44
|
+
onMouseDown={() => {
|
|
45
|
+
const next = !isPending
|
|
46
|
+
utilInstance.store.setState((prev: any) => ({
|
|
47
|
+
...prev,
|
|
48
|
+
isPending: next,
|
|
49
|
+
}))
|
|
50
|
+
pacerEventClient.emit(emitName, utilInstance)
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<span class={styles().actionDotBlue} />
|
|
54
|
+
{isPending ? 'Restore Pending' : 'Trigger Pending'}
|
|
55
|
+
</button>
|
|
56
|
+
)}
|
|
57
|
+
{hasFlush && (
|
|
58
|
+
<button
|
|
59
|
+
class={styles().actionButton}
|
|
60
|
+
onMouseDown={() => {
|
|
61
|
+
utilInstance.flush()
|
|
62
|
+
pacerEventClient.emit(emitName, utilInstance)
|
|
63
|
+
}}
|
|
64
|
+
disabled={
|
|
65
|
+
(props.utilName.toLowerCase().includes('debouncer') &&
|
|
66
|
+
!isPending) ||
|
|
67
|
+
(props.utilName.toLowerCase().includes('throttler') &&
|
|
68
|
+
!isPending) ||
|
|
69
|
+
(props.utilName.toLowerCase().includes('batcher') && isEmpty) ||
|
|
70
|
+
(props.utilName.toLowerCase().includes('queuer') && isEmpty)
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
<span class={styles().actionDotGreen} />
|
|
74
|
+
Flush
|
|
75
|
+
</button>
|
|
76
|
+
)}
|
|
77
|
+
{hasCancel && (
|
|
78
|
+
<button
|
|
79
|
+
class={styles().actionButton}
|
|
80
|
+
onMouseDown={() => {
|
|
81
|
+
utilInstance.cancel()
|
|
82
|
+
pacerEventClient.emit(emitName, utilInstance)
|
|
83
|
+
}}
|
|
84
|
+
disabled={
|
|
85
|
+
(props.utilName.toLowerCase().includes('debouncer') &&
|
|
86
|
+
!isPending) ||
|
|
87
|
+
(props.utilName.toLowerCase().includes('throttler') &&
|
|
88
|
+
!isPending) ||
|
|
89
|
+
(props.utilName.toLowerCase().includes('batcher') && isEmpty) ||
|
|
90
|
+
(props.utilName.toLowerCase().includes('queuer') && isEmpty)
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
<span class={styles().actionDotRed} />
|
|
94
|
+
Cancel
|
|
95
|
+
</button>
|
|
96
|
+
)}
|
|
97
|
+
{hasReset && (
|
|
98
|
+
<button
|
|
99
|
+
class={styles().actionButton}
|
|
100
|
+
onMouseDown={() => {
|
|
101
|
+
utilInstance.reset()
|
|
102
|
+
pacerEventClient.emit(emitName, utilInstance)
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<span class={styles().actionDotYellow} />
|
|
106
|
+
Reset
|
|
107
|
+
</button>
|
|
108
|
+
)}
|
|
109
|
+
{hasClear && (
|
|
110
|
+
<button
|
|
111
|
+
class={styles().actionButton}
|
|
112
|
+
onMouseDown={() => {
|
|
113
|
+
utilInstance.clear()
|
|
114
|
+
pacerEventClient.emit(emitName, utilInstance)
|
|
115
|
+
}}
|
|
116
|
+
disabled={
|
|
117
|
+
(props.utilName.toLowerCase().includes('batcher') && isEmpty) ||
|
|
118
|
+
(props.utilName.toLowerCase().includes('queuer') && isEmpty)
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
<span class={styles().actionDotOrange} />
|
|
122
|
+
Clear
|
|
123
|
+
</button>
|
|
124
|
+
)}
|
|
125
|
+
{hasStartStop && (
|
|
126
|
+
<button
|
|
127
|
+
class={styles().actionButton}
|
|
128
|
+
onMouseDown={() => {
|
|
129
|
+
if (isRunning) {
|
|
130
|
+
utilInstance.stop()
|
|
131
|
+
} else {
|
|
132
|
+
utilInstance.start()
|
|
133
|
+
}
|
|
134
|
+
pacerEventClient.emit(emitName, utilInstance)
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<span class={styles().actionDotPurple} />
|
|
138
|
+
{isRunning ? 'Stop' : 'Start'}
|
|
139
|
+
</button>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { JsonTree } from '@tanstack/devtools-ui'
|
|
2
|
+
import { useStyles } from '../styles/use-styles'
|
|
3
|
+
import { ActionButtons } from './ActionButtons'
|
|
4
|
+
import { StateHeader } from './StateHeader'
|
|
5
|
+
|
|
6
|
+
type DetailsPanelProps = {
|
|
7
|
+
selectedInstance: () => { instance: any; type: string } | null
|
|
8
|
+
utilState: () => { lastUpdatedByKey: Record<string, number> }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DetailsPanel(props: DetailsPanelProps) {
|
|
12
|
+
const styles = useStyles()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div class={styles().stateDetails}>
|
|
16
|
+
{(() => {
|
|
17
|
+
const entry = props.selectedInstance()
|
|
18
|
+
if (!entry) {
|
|
19
|
+
return (
|
|
20
|
+
<div class={styles().noSelection}>
|
|
21
|
+
Select a util from the left panel to view its state
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<StateHeader
|
|
29
|
+
selectedInstance={props.selectedInstance}
|
|
30
|
+
utilState={props.utilState}
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<div class={styles().detailsGrid}>
|
|
34
|
+
<div class={styles().detailSection}>
|
|
35
|
+
<div class={styles().detailSectionHeader}>Actions</div>
|
|
36
|
+
<ActionButtons
|
|
37
|
+
instance={entry.instance}
|
|
38
|
+
utilName={entry.type}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class={styles().detailSection}>
|
|
43
|
+
<div class={styles().detailSectionHeader}>State</div>
|
|
44
|
+
<div class={styles().stateContent}>
|
|
45
|
+
<JsonTree value={entry.instance.store?.state} />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class={styles().detailSection}>
|
|
50
|
+
<div class={styles().detailSectionHeader}>Options</div>
|
|
51
|
+
<div class={styles().stateContent}>
|
|
52
|
+
<JsonTree value={entry.instance.options} />
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</>
|
|
57
|
+
)
|
|
58
|
+
})()}
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createMemo, createSignal, onCleanup, onMount } from 'solid-js'
|
|
2
|
+
import { useStyles } from '../styles/use-styles'
|
|
3
|
+
import { usePacerDevtoolsState } from '../PacerContextProvider'
|
|
4
|
+
import { UTIL_GROUPS } from './util-groups'
|
|
5
|
+
import { UtilList } from './UtilList'
|
|
6
|
+
import { DetailsPanel } from './DetailsPanel'
|
|
7
|
+
import type { StateKey } from './util-groups'
|
|
8
|
+
|
|
9
|
+
export function Shell() {
|
|
10
|
+
const styles = useStyles()
|
|
11
|
+
const state = usePacerDevtoolsState()
|
|
12
|
+
const utilState = () => state
|
|
13
|
+
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
|
|
14
|
+
const [leftPanelWidth, setLeftPanelWidth] = createSignal(300)
|
|
15
|
+
const [isDragging, setIsDragging] = createSignal(false)
|
|
16
|
+
|
|
17
|
+
const selectedInstance = createMemo(() => {
|
|
18
|
+
const key = selectedKey()
|
|
19
|
+
if (!key) return null
|
|
20
|
+
for (const group of UTIL_GROUPS) {
|
|
21
|
+
const instance = (utilState() as unknown as Record<StateKey, Array<any>>)[
|
|
22
|
+
group.key
|
|
23
|
+
].find((inst) => inst.key === key)
|
|
24
|
+
if (instance) return { instance, type: group.displayName }
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
let dragStartX = 0
|
|
30
|
+
let dragStartWidth = 0
|
|
31
|
+
|
|
32
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
33
|
+
e.preventDefault()
|
|
34
|
+
e.stopPropagation()
|
|
35
|
+
setIsDragging(true)
|
|
36
|
+
document.body.style.cursor = 'col-resize'
|
|
37
|
+
document.body.style.userSelect = 'none'
|
|
38
|
+
dragStartX = e.clientX
|
|
39
|
+
dragStartWidth = leftPanelWidth()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
43
|
+
if (!isDragging()) return
|
|
44
|
+
|
|
45
|
+
e.preventDefault()
|
|
46
|
+
const deltaX = e.clientX - dragStartX
|
|
47
|
+
const newWidth = Math.max(150, Math.min(800, dragStartWidth + deltaX))
|
|
48
|
+
setLeftPanelWidth(newWidth)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleMouseUp = () => {
|
|
52
|
+
setIsDragging(false)
|
|
53
|
+
document.body.style.cursor = ''
|
|
54
|
+
document.body.style.userSelect = ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMount(() => {
|
|
58
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
59
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
onCleanup(() => {
|
|
63
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
64
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div class={styles().devtoolsPanel}>
|
|
69
|
+
<div class={styles().stickyHeader}>TanStack Pacer</div>
|
|
70
|
+
|
|
71
|
+
<div class={styles().mainContainer}>
|
|
72
|
+
<div
|
|
73
|
+
class={styles().leftPanel}
|
|
74
|
+
style={{
|
|
75
|
+
width: `${leftPanelWidth()}px`,
|
|
76
|
+
'min-width': '150px',
|
|
77
|
+
'max-width': '800px',
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<UtilList
|
|
81
|
+
selectedKey={selectedKey}
|
|
82
|
+
setSelectedKey={setSelectedKey}
|
|
83
|
+
utilState={utilState}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div
|
|
88
|
+
class={`${styles().dragHandle} ${isDragging() ? 'dragging' : ''}`}
|
|
89
|
+
onMouseDown={handleMouseDown}
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
<div class={styles().rightPanel} style={{ flex: 1 }}>
|
|
93
|
+
<div class={styles().panelHeader}>Details</div>
|
|
94
|
+
<DetailsPanel
|
|
95
|
+
selectedInstance={selectedInstance}
|
|
96
|
+
utilState={utilState}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
3
|
+
import { createSignal, onCleanup, onMount } from 'solid-js'
|
|
4
|
+
import { useStyles } from '../styles/use-styles'
|
|
5
|
+
|
|
6
|
+
dayjs.extend(relativeTime)
|
|
7
|
+
|
|
8
|
+
type StateHeaderProps = {
|
|
9
|
+
selectedInstance: () => { instance: any; type: string } | null
|
|
10
|
+
utilState: () => { lastUpdatedByKey: Record<string, number> }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function StateHeader(props: StateHeaderProps) {
|
|
14
|
+
const styles = useStyles()
|
|
15
|
+
const [now, setNow] = createSignal(Date.now())
|
|
16
|
+
|
|
17
|
+
onMount(() => {
|
|
18
|
+
const interval = setInterval(() => {
|
|
19
|
+
setNow(Date.now())
|
|
20
|
+
}, 1000)
|
|
21
|
+
|
|
22
|
+
onCleanup(() => {
|
|
23
|
+
clearInterval(interval)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const entry = props.selectedInstance()
|
|
28
|
+
if (!entry) return null
|
|
29
|
+
|
|
30
|
+
const key = entry.instance.key as string
|
|
31
|
+
const updatedAt = props.utilState().lastUpdatedByKey[key] ?? Date.now()
|
|
32
|
+
|
|
33
|
+
const getRelativeTime = () => {
|
|
34
|
+
const diffMs = now() - updatedAt
|
|
35
|
+
const diffSeconds = Math.floor(diffMs / 1000)
|
|
36
|
+
|
|
37
|
+
if (diffSeconds < 60) {
|
|
38
|
+
return `${diffSeconds} second${diffSeconds !== 1 ? 's' : ''} ago`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return dayjs(updatedAt).fromNow()
|
|
42
|
+
}
|
|
43
|
+
|
|
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,
|
|
62
|
+
)
|
|
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
|
+
}
|
|
82
|
+
|
|
83
|
+
const reductionPercentage = getReductionPercentage()
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div class={styles().stateHeader}>
|
|
87
|
+
<div class={styles().stateTitle}>{entry.type}</div>
|
|
88
|
+
<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()})
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div
|
|
98
|
+
class={styles().infoValueMono}
|
|
99
|
+
style={{ 'margin-left': 'auto', 'font-weight': 'bold' }}
|
|
100
|
+
>
|
|
101
|
+
{reductionPercentage}% reduction
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { For } from 'solid-js'
|
|
2
|
+
import { useStore } from '@tanstack/solid-store'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
import { useStyles } from '../styles/use-styles'
|
|
5
|
+
import { UTIL_GROUPS } from './util-groups'
|
|
6
|
+
import type { StateKey } from './util-groups'
|
|
7
|
+
|
|
8
|
+
type UtilListProps = {
|
|
9
|
+
selectedKey: () => string | null
|
|
10
|
+
setSelectedKey: (key: string | null) => void
|
|
11
|
+
utilState: () => Record<StateKey, Array<any>>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function UtilList(props: UtilListProps) {
|
|
15
|
+
const styles = useStyles()
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div class={styles().utilList}>
|
|
19
|
+
<For each={UTIL_GROUPS}>
|
|
20
|
+
{(group) => (
|
|
21
|
+
<>
|
|
22
|
+
{props.utilState()[group.key].length > 0 && (
|
|
23
|
+
<div class={styles().utilGroup}>
|
|
24
|
+
<div class={styles().utilGroupHeader}>{group.label}</div>
|
|
25
|
+
<For each={props.utilState()[group.key]}>
|
|
26
|
+
{(instance) => {
|
|
27
|
+
const status = (() => {
|
|
28
|
+
try {
|
|
29
|
+
const statusAccessor = useStore(
|
|
30
|
+
instance.store,
|
|
31
|
+
(s: any) => s.status,
|
|
32
|
+
)
|
|
33
|
+
return () => statusAccessor() ?? 'unknown'
|
|
34
|
+
} catch {
|
|
35
|
+
return () => 'unknown'
|
|
36
|
+
}
|
|
37
|
+
})()
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
class={clsx(
|
|
41
|
+
styles().utilRow,
|
|
42
|
+
props.selectedKey() === instance.key &&
|
|
43
|
+
styles().utilRowSelected,
|
|
44
|
+
)}
|
|
45
|
+
onClick={() =>
|
|
46
|
+
props.setSelectedKey(instance.key ?? null)
|
|
47
|
+
}
|
|
48
|
+
>
|
|
49
|
+
<div class={styles().utilKey}>{instance.key}</div>
|
|
50
|
+
<div class={styles().utilStatus}>{status()}</div>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}}
|
|
54
|
+
</For>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
59
|
+
</For>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type StateKey =
|
|
2
|
+
| 'asyncBatchers'
|
|
3
|
+
| 'asyncDebouncers'
|
|
4
|
+
| 'asyncQueuers'
|
|
5
|
+
| 'asyncRateLimiters'
|
|
6
|
+
| 'asyncThrottlers'
|
|
7
|
+
| 'batchers'
|
|
8
|
+
| 'debouncers'
|
|
9
|
+
| 'queuers'
|
|
10
|
+
| 'rateLimiters'
|
|
11
|
+
| 'throttlers'
|
|
12
|
+
|
|
13
|
+
type UtilGroup = {
|
|
14
|
+
key: StateKey
|
|
15
|
+
label: string
|
|
16
|
+
displayName: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const UTIL_GROUPS: Array<UtilGroup> = [
|
|
20
|
+
{ key: 'debouncers', label: 'Debouncers', displayName: 'Debouncer' },
|
|
21
|
+
{ key: 'throttlers', label: 'Throttlers', displayName: 'Throttler' },
|
|
22
|
+
{ key: 'batchers', label: 'Batchers', displayName: 'Batcher' },
|
|
23
|
+
{ key: 'queuers', label: 'Queuers', displayName: 'Queuer' },
|
|
24
|
+
{
|
|
25
|
+
key: 'rateLimiters',
|
|
26
|
+
label: 'Rate Limiters',
|
|
27
|
+
displayName: 'Rate Limiter',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'asyncDebouncers',
|
|
31
|
+
label: 'Async Debouncers',
|
|
32
|
+
displayName: 'Async Debouncer',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'asyncThrottlers',
|
|
36
|
+
label: 'Async Throttlers',
|
|
37
|
+
displayName: 'Async Throttler',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: 'asyncBatchers',
|
|
41
|
+
label: 'Async Batchers',
|
|
42
|
+
displayName: 'Async Batcher',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'asyncQueuers',
|
|
46
|
+
label: 'Async Queuers',
|
|
47
|
+
displayName: 'Async Queuer',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'asyncRateLimiters',
|
|
51
|
+
label: 'Async Rate Limiters',
|
|
52
|
+
displayName: 'Async Rate Limiter',
|
|
53
|
+
},
|
|
54
|
+
]
|
package/src/core.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { lazy } from 'solid-js'
|
|
2
|
+
import { Portal, render } from 'solid-js/web'
|
|
3
|
+
|
|
4
|
+
export interface PacerDevtoolsInit {}
|
|
5
|
+
|
|
6
|
+
export class PacerDevtoolsCore {
|
|
7
|
+
#isMounted = false
|
|
8
|
+
#dispose?: () => void
|
|
9
|
+
#Component: any
|
|
10
|
+
|
|
11
|
+
constructor(_init?: PacerDevtoolsInit | undefined) {}
|
|
12
|
+
|
|
13
|
+
mount<T extends HTMLElement>(el: T) {
|
|
14
|
+
if (this.#isMounted) {
|
|
15
|
+
throw new Error('Devtools is already mounted')
|
|
16
|
+
}
|
|
17
|
+
const mountTo = el
|
|
18
|
+
const dispose = render(() => {
|
|
19
|
+
this.#Component = lazy(() => import('./PacerDevtools'))
|
|
20
|
+
const Devtools = this.#Component
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Portal mount={mountTo}>
|
|
24
|
+
<div style={{ height: '100%' }}>
|
|
25
|
+
<Devtools />
|
|
26
|
+
</div>
|
|
27
|
+
</Portal>
|
|
28
|
+
)
|
|
29
|
+
}, mountTo)
|
|
30
|
+
this.#isMounted = true
|
|
31
|
+
this.#dispose = dispose
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
unmount() {
|
|
35
|
+
if (!this.#isMounted) {
|
|
36
|
+
throw new Error('Devtools is not mounted')
|
|
37
|
+
}
|
|
38
|
+
this.#dispose?.()
|
|
39
|
+
this.#isMounted = false
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as Devtools from './core'
|
|
4
|
+
|
|
5
|
+
// Create a dummy class for production that does nothing
|
|
6
|
+
class DummyPacerDevtoolsCore {
|
|
7
|
+
constructor() {}
|
|
8
|
+
mount() {}
|
|
9
|
+
unmount() {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const PacerDevtoolsCore: (typeof Devtools)['PacerDevtoolsCore'] =
|
|
13
|
+
process.env.NODE_ENV !== 'development'
|
|
14
|
+
? (DummyPacerDevtoolsCore as any)
|
|
15
|
+
: Devtools.PacerDevtoolsCore
|
|
16
|
+
|
|
17
|
+
export type { PacerDevtoolsInit } from './core'
|