@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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/PacerContextProvider.cjs +156 -0
  3. package/dist/cjs/PacerContextProvider.cjs.map +1 -0
  4. package/dist/cjs/PacerContextProvider.d.cts +19 -0
  5. package/dist/cjs/PacerDevtools.cjs +13 -0
  6. package/dist/cjs/PacerDevtools.cjs.map +1 -0
  7. package/dist/cjs/PacerDevtools.d.cts +1 -0
  8. package/dist/cjs/components/ActionButtons.cjs +157 -0
  9. package/dist/cjs/components/ActionButtons.cjs.map +1 -0
  10. package/dist/cjs/components/ActionButtons.d.cts +6 -0
  11. package/dist/cjs/components/DetailsPanel.cjs +80 -0
  12. package/dist/cjs/components/DetailsPanel.cjs.map +1 -0
  13. package/dist/cjs/components/DetailsPanel.d.cts +11 -0
  14. package/dist/cjs/components/Shell.cjs +102 -0
  15. package/dist/cjs/components/Shell.cjs.map +1 -0
  16. package/dist/cjs/components/Shell.d.cts +1 -0
  17. package/dist/cjs/components/StateHeader.cjs +95 -0
  18. package/dist/cjs/components/StateHeader.cjs.map +1 -0
  19. package/dist/cjs/components/StateHeader.d.cts +11 -0
  20. package/dist/cjs/components/UtilList.cjs +70 -0
  21. package/dist/cjs/components/UtilList.cjs.map +1 -0
  22. package/dist/cjs/components/UtilList.d.cts +8 -0
  23. package/dist/cjs/components/util-groups.cjs +40 -0
  24. package/dist/cjs/components/util-groups.cjs.map +1 -0
  25. package/dist/cjs/components/util-groups.d.cts +8 -0
  26. package/dist/cjs/core.cjs +43 -0
  27. package/dist/cjs/core.cjs.map +1 -0
  28. package/dist/cjs/core.d.cts +8 -0
  29. package/dist/cjs/index.cjs +15 -0
  30. package/dist/cjs/index.cjs.map +1 -0
  31. package/dist/cjs/index.d.cts +3 -0
  32. package/dist/cjs/production.cjs +7 -0
  33. package/dist/cjs/production.cjs.map +1 -0
  34. package/dist/cjs/production.d.cts +3 -0
  35. package/dist/cjs/styles/tokens.cjs +90 -0
  36. package/dist/cjs/styles/tokens.cjs.map +1 -0
  37. package/dist/cjs/styles/tokens.d.cts +298 -0
  38. package/dist/cjs/styles/use-styles.cjs +397 -0
  39. package/dist/cjs/styles/use-styles.cjs.map +1 -0
  40. package/dist/cjs/styles/use-styles.d.cts +42 -0
  41. package/dist/esm/PacerContextProvider.d.ts +19 -0
  42. package/dist/esm/PacerContextProvider.js +156 -0
  43. package/dist/esm/PacerContextProvider.js.map +1 -0
  44. package/dist/esm/PacerDevtools.d.ts +1 -0
  45. package/dist/esm/PacerDevtools.js +14 -0
  46. package/dist/esm/PacerDevtools.js.map +1 -0
  47. package/dist/esm/components/ActionButtons.d.ts +6 -0
  48. package/dist/esm/components/ActionButtons.js +157 -0
  49. package/dist/esm/components/ActionButtons.js.map +1 -0
  50. package/dist/esm/components/DetailsPanel.d.ts +11 -0
  51. package/dist/esm/components/DetailsPanel.js +80 -0
  52. package/dist/esm/components/DetailsPanel.js.map +1 -0
  53. package/dist/esm/components/Shell.d.ts +1 -0
  54. package/dist/esm/components/Shell.js +102 -0
  55. package/dist/esm/components/Shell.js.map +1 -0
  56. package/dist/esm/components/StateHeader.d.ts +11 -0
  57. package/dist/esm/components/StateHeader.js +95 -0
  58. package/dist/esm/components/StateHeader.js.map +1 -0
  59. package/dist/esm/components/UtilList.d.ts +8 -0
  60. package/dist/esm/components/UtilList.js +70 -0
  61. package/dist/esm/components/UtilList.js.map +1 -0
  62. package/dist/esm/components/util-groups.d.ts +8 -0
  63. package/dist/esm/components/util-groups.js +40 -0
  64. package/dist/esm/components/util-groups.js.map +1 -0
  65. package/dist/esm/core.d.ts +8 -0
  66. package/dist/esm/core.js +42 -0
  67. package/dist/esm/core.js.map +1 -0
  68. package/dist/esm/index.d.ts +3 -0
  69. package/dist/esm/index.js +15 -0
  70. package/dist/esm/index.js.map +1 -0
  71. package/dist/esm/production.d.ts +3 -0
  72. package/dist/esm/production.js +7 -0
  73. package/dist/esm/production.js.map +1 -0
  74. package/dist/esm/styles/tokens.d.ts +298 -0
  75. package/dist/esm/styles/tokens.js +90 -0
  76. package/dist/esm/styles/tokens.js.map +1 -0
  77. package/dist/esm/styles/use-styles.d.ts +42 -0
  78. package/dist/esm/styles/use-styles.js +380 -0
  79. package/dist/esm/styles/use-styles.js.map +1 -0
  80. package/package.json +84 -0
  81. package/src/PacerContextProvider.tsx +203 -0
  82. package/src/PacerDevtools.tsx +10 -0
  83. package/src/components/ActionButtons.tsx +143 -0
  84. package/src/components/DetailsPanel.tsx +61 -0
  85. package/src/components/Shell.tsx +102 -0
  86. package/src/components/StateHeader.tsx +106 -0
  87. package/src/components/UtilList.tsx +62 -0
  88. package/src/components/util-groups.ts +54 -0
  89. package/src/core.tsx +41 -0
  90. package/src/index.ts +17 -0
  91. package/src/production.ts +7 -0
  92. package/src/styles/tokens.ts +305 -0
  93. package/src/styles/use-styles.ts +379 -0
@@ -0,0 +1,10 @@
1
+ import { PacerContextProvider } from './PacerContextProvider'
2
+ import { Shell } from './components/Shell'
3
+
4
+ export default function PacerDevtools() {
5
+ return (
6
+ <PacerContextProvider>
7
+ <Shell />
8
+ </PacerContextProvider>
9
+ )
10
+ }
@@ -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'
@@ -0,0 +1,7 @@
1
+ 'use client'
2
+
3
+ import * as Devtools from './core'
4
+
5
+ export const PacerDevtoolsCore = Devtools.PacerDevtoolsCore
6
+
7
+ export type { PacerDevtoolsInit } from './core'