Skip to content

Commit 06fba80

Browse files
committed
refactor: move chart refresh out of the component
1 parent 953d526 commit 06fba80

9 files changed

Lines changed: 1056 additions & 482 deletions

File tree

src/hooks/useAdaptivePoll.ts

Lines changed: 73 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,67 @@
11
import { useEffect, useRef, useState } from "react"
22

3+
type AdaptivePollLoopOptions = {
4+
fetchFn: () => Promise<void>
5+
signal: AbortSignal
6+
minIntervalMs: number
7+
maxIntervalMs: number
8+
sampleSize?: number
9+
multiplier?: number
10+
skipInitialFetch?: boolean
11+
onIntervalChange?: (intervalMs: number) => void
12+
}
13+
14+
const sleep = (ms: number, signal: AbortSignal) =>
15+
new Promise<void>((resolve, reject) => {
16+
const timeoutId = setTimeout(resolve, ms)
17+
signal.addEventListener("abort", () => {
18+
clearTimeout(timeoutId)
19+
reject(new DOMException("Aborted", "AbortError"))
20+
})
21+
})
22+
23+
export const runAdaptivePollLoop = async ({
24+
fetchFn,
25+
signal,
26+
minIntervalMs,
27+
maxIntervalMs,
28+
sampleSize = 3,
29+
multiplier = 2,
30+
skipInitialFetch = false,
31+
onIntervalChange,
32+
}: AdaptivePollLoopOptions): Promise<void> => {
33+
let samples: number[] = []
34+
let skip = skipInitialFetch
35+
while (!signal.aborted) {
36+
let nextInterval = minIntervalMs
37+
if (skip) {
38+
// Data was just transferred in — wait one interval before the first
39+
// background refresh instead of re-querying it immediately.
40+
skip = false
41+
} else {
42+
const start = performance.now()
43+
try {
44+
await fetchFn()
45+
} catch {
46+
// fetchFn handles its own errors; never let one kill the loop
47+
}
48+
if (signal.aborted) break
49+
samples = [...samples, performance.now() - start].slice(-sampleSize)
50+
const avg = samples.reduce((a, b) => a + b, 0) / samples.length
51+
nextInterval = Math.min(
52+
maxIntervalMs,
53+
Math.max(minIntervalMs, Math.round(avg * multiplier)),
54+
)
55+
onIntervalChange?.(nextInterval)
56+
}
57+
try {
58+
await sleep(nextInterval, signal)
59+
} catch {
60+
break
61+
}
62+
}
63+
}
64+
365
type AdaptivePollOptions = {
466
fetchFn: () => Promise<void>
567
enabled: boolean
@@ -29,85 +91,28 @@ export const useAdaptivePoll = (
2991
getSkipInitialFetch,
3092
} = options
3193

32-
const samplesRef = useRef<number[]>([])
3394
const abortControllerRef = useRef<AbortController | null>(null)
3495
const [currentInterval, setCurrentInterval] = useState(minIntervalMs)
3596

3697
useEffect(() => {
37-
samplesRef.current = []
3898
setCurrentInterval(minIntervalMs)
39-
40-
// Abort any previous polling loop
41-
if (abortControllerRef.current) {
42-
abortControllerRef.current.abort()
43-
}
99+
abortControllerRef.current?.abort()
44100

45101
if (!enabled) return
46102

47103
const abortController = new AbortController()
48104
abortControllerRef.current = abortController
49105

50-
const calculateInterval = (samples: number[]): number => {
51-
if (samples.length === 0) return minIntervalMs
52-
53-
const avg = samples.reduce((a, b) => a + b, 0) / samples.length
54-
const calculated = avg * multiplier
55-
56-
return Math.min(
57-
maxIntervalMs,
58-
Math.max(minIntervalMs, Math.round(calculated)),
59-
)
60-
}
61-
62-
const sleep = (ms: number, signal: AbortSignal) =>
63-
new Promise<void>((resolve, reject) => {
64-
const timeoutId = setTimeout(resolve, ms)
65-
signal.addEventListener("abort", () => {
66-
clearTimeout(timeoutId)
67-
reject(new DOMException("Aborted", "AbortError"))
68-
})
69-
})
70-
71-
const runPollingLoop = async () => {
72-
let skipFetch = getSkipInitialFetch?.() ?? false
73-
while (!abortController.signal.aborted) {
74-
let nextInterval = minIntervalMs
75-
76-
if (skipFetch) {
77-
// Data was just transferred in — wait one interval before the first
78-
// background refresh instead of re-querying it immediately.
79-
skipFetch = false
80-
} else {
81-
const startTime = performance.now()
82-
83-
try {
84-
await fetchFn()
85-
} catch (error) {
86-
// Silently handle errors - the fetchFn should handle its own errors
87-
}
88-
89-
// Check if we should stop after the fetch completed
90-
if (abortController.signal.aborted) break
91-
92-
const responseTime = performance.now() - startTime
93-
samplesRef.current = [...samplesRef.current, responseTime].slice(
94-
-sampleSize,
95-
)
96-
97-
nextInterval = calculateInterval(samplesRef.current)
98-
setCurrentInterval(nextInterval)
99-
}
100-
101-
try {
102-
await sleep(nextInterval, abortController.signal)
103-
} catch (error) {
104-
// Sleep was aborted, exit the loop
105-
break
106-
}
107-
}
108-
}
109-
110-
void runPollingLoop()
106+
void runAdaptivePollLoop({
107+
fetchFn,
108+
signal: abortController.signal,
109+
minIntervalMs,
110+
maxIntervalMs,
111+
sampleSize,
112+
multiplier,
113+
skipInitialFetch: getSkipInitialFetch?.() ?? false,
114+
onIntervalChange: setCurrentInterval,
115+
})
111116

112117
return () => {
113118
abortController.abort()

0 commit comments

Comments
 (0)