refactor(desktop): shared pulse beat + fully-gated cron peek (simplify folds)
Two findings from the simplify pass on the final trio diff: - status-pulse: one pause controller + one aligned period timer shared by all StatusPulse instances (ref-counted), instead of N x (document/window/bridge listeners + unsynchronized 5s wakes) — a sidebar can show dozens of pulsing dots. Pause still cancels in-flight animations so the compositor sleeps immediately. - cron-jobs-section: the runs-peek effect created its interval even while the pane was hidden (callback no-oped but the timer still woke the renderer every 8s/60s per expanded job). Early-return when hidden — visibility is already in the dep array, so becoming visible restarts load + timer.
This commit is contained in:
parent
e2a2149df4
commit
aece98c5f3
|
|
@ -350,14 +350,20 @@ function CronJobSidebarRuns({ jobId, onOpenRun }: { jobId: string; onOpenRun: (s
|
|||
}
|
||||
})
|
||||
|
||||
// Initial load when visible
|
||||
if (visible) {
|
||||
void load()
|
||||
// Hidden pane: skip the peek entirely — no initial load, no interval.
|
||||
// `visible` is in the dep array, so becoming visible re-runs this effect
|
||||
// and starts the load + timer fresh (same shape as the section clock).
|
||||
if (!visible) {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
void load()
|
||||
|
||||
const intervalId = window.setInterval(
|
||||
() => {
|
||||
if (visible && document.visibilityState === 'visible') {
|
||||
if (document.visibilityState === 'visible') {
|
||||
void load()
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,78 @@ import { createRendererLoopPauseController } from '@/lib/renderer-loop-pause'
|
|||
const PULSE_DURATION_MS = 400
|
||||
const PULSE_PERIOD_MS = 5_000
|
||||
|
||||
// One pause controller + one period timer shared by every StatusPulse
|
||||
// instance. A sidebar can show dozens of pulsing dots at once; per-instance
|
||||
// controllers would mean N×(document/window/bridge) listeners and N
|
||||
// unsynchronized 5s wakes. Ref-counted: the controller and timer exist only
|
||||
// while at least one pulse is mounted, and all pulses play in one aligned
|
||||
// wake so the renderer sleeps between beats.
|
||||
type PulseSubscriber = { play: () => void; cancel: () => void }
|
||||
|
||||
const pulseSubscribers = new Set<PulseSubscriber>()
|
||||
let sharedPauseController: ReturnType<typeof createRendererLoopPauseController> | null = null
|
||||
let sharedTimer = 0
|
||||
|
||||
const stopSharedTimer = () => {
|
||||
if (sharedTimer !== 0) {
|
||||
window.clearTimeout(sharedTimer)
|
||||
sharedTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
const beat = () => {
|
||||
sharedTimer = 0
|
||||
|
||||
if (sharedPauseController?.isPaused() || pulseSubscribers.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const subscriber of pulseSubscribers) {
|
||||
subscriber.play()
|
||||
}
|
||||
sharedTimer = window.setTimeout(beat, PULSE_PERIOD_MS)
|
||||
}
|
||||
|
||||
const handleSharedPauseChange = () => {
|
||||
stopSharedTimer()
|
||||
|
||||
if (sharedPauseController?.isPaused()) {
|
||||
// Minimized/hidden: cancel in-flight animations so the compositor can
|
||||
// sleep immediately instead of finishing a pulse nobody sees.
|
||||
for (const subscriber of pulseSubscribers) {
|
||||
subscriber.cancel()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
beat()
|
||||
}
|
||||
|
||||
const subscribePulse = (subscriber: PulseSubscriber): (() => void) => {
|
||||
pulseSubscribers.add(subscriber)
|
||||
|
||||
if (!sharedPauseController) {
|
||||
sharedPauseController = createRendererLoopPauseController(handleSharedPauseChange)
|
||||
}
|
||||
|
||||
// First subscriber (or a new one joining mid-sleep): play immediately and
|
||||
// start the beat. Later joiners just wait for the next aligned beat.
|
||||
if (sharedTimer === 0 && !sharedPauseController.isPaused()) {
|
||||
subscriber.play()
|
||||
sharedTimer = window.setTimeout(beat, PULSE_PERIOD_MS)
|
||||
}
|
||||
|
||||
return () => {
|
||||
pulseSubscribers.delete(subscriber)
|
||||
|
||||
if (pulseSubscribers.size === 0) {
|
||||
stopSharedTimer()
|
||||
sharedPauseController?.dispose()
|
||||
sharedPauseController = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface StatusPulseProps extends Omit<ComponentProps<'span'>, 'children' | 'ref'> {
|
||||
kind: 'opacity' | 'ping'
|
||||
opacity?: number
|
||||
|
|
@ -33,27 +105,8 @@ export function StatusPulse({ kind, opacity = 1, ...props }: StatusPulseProps) {
|
|||
}
|
||||
|
||||
let animation: Animation | null = null
|
||||
let timer = 0
|
||||
let stopped = false
|
||||
let pauseController: ReturnType<typeof createRendererLoopPauseController> | null = null
|
||||
|
||||
const clearScheduled = () => {
|
||||
if (timer !== 0) {
|
||||
window.clearTimeout(timer)
|
||||
timer = 0
|
||||
}
|
||||
|
||||
animation?.cancel()
|
||||
animation = null
|
||||
}
|
||||
|
||||
const play = () => {
|
||||
timer = 0
|
||||
|
||||
if (stopped || pauseController?.isPaused()) {
|
||||
return
|
||||
}
|
||||
|
||||
animation?.cancel()
|
||||
animation = element.animate(
|
||||
kind === 'ping'
|
||||
|
|
@ -68,24 +121,18 @@ export function StatusPulse({ kind, opacity = 1, ...props }: StatusPulseProps) {
|
|||
iterations: 1
|
||||
}
|
||||
)
|
||||
timer = window.setTimeout(play, PULSE_PERIOD_MS)
|
||||
}
|
||||
|
||||
const handlePauseChange = () => {
|
||||
clearScheduled()
|
||||
|
||||
if (!pauseController?.isPaused()) {
|
||||
play()
|
||||
}
|
||||
const cancel = () => {
|
||||
animation?.cancel()
|
||||
animation = null
|
||||
}
|
||||
|
||||
pauseController = createRendererLoopPauseController(handlePauseChange)
|
||||
play()
|
||||
const unsubscribe = subscribePulse({ play, cancel })
|
||||
|
||||
return () => {
|
||||
stopped = true
|
||||
clearScheduled()
|
||||
pauseController?.dispose()
|
||||
unsubscribe()
|
||||
cancel()
|
||||
}
|
||||
}, [kind, opacity])
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue