From 1fa85ef22b8f31e8a52d58e65ed0dd2e830ca5aa Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Tue, 20 Jan 2026 22:31:41 +0000 Subject: [PATCH] fixed timer desync issues --- .../frontend/src/components/issue-timer.tsx | 25 ++++++------------- .../frontend/src/components/timer-display.tsx | 24 +++++++++++------- .../frontend/src/lib/query/hooks/timers.ts | 8 +++--- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/packages/frontend/src/components/issue-timer.tsx b/packages/frontend/src/components/issue-timer.tsx index 8a5ca91..393950e 100644 --- a/packages/frontend/src/components/issue-timer.tsx +++ b/packages/frontend/src/components/issue-timer.tsx @@ -1,5 +1,6 @@ import type { TimerState } from "@sprint/shared"; import { useEffect, useState } from "react"; +import { getWorkTimeMs } from "@/components/timer-display"; import { Button } from "@/components/ui/button"; import { useEndTimer, useTimerState, useToggleTimer } from "@/lib/query/hooks"; import { parseError } from "@/lib/server"; @@ -9,39 +10,30 @@ export function IssueTimer({ issueId, onEnd }: { issueId: number; onEnd?: (data: const { data: timerState, error } = useTimerState(issueId); const toggleTimer = useToggleTimer(); const endTimer = useEndTimer(); - const [displayTime, setDisplayTime] = useState(0); + const [tick, setTick] = useState(0); const [errorMessage, setErrorMessage] = useState(null); - useEffect(() => { - if (timerState) { - setDisplayTime(timerState.workTimeMs); - } - }, [timerState]); - useEffect(() => { if (!timerState?.isRunning) return; - const startTime = Date.now(); - const baseTime = timerState.workTimeMs; - const interval = setInterval(() => { - setDisplayTime(baseTime + (Date.now() - startTime)); + setTick((t) => t + 1); }, 1000); return () => clearInterval(interval); - }, [timerState?.isRunning, timerState?.workTimeMs]); + }, [timerState?.isRunning]); useEffect(() => { if (!error) return; setErrorMessage(parseError(error as Error)); }, [error]); + void tick; + const displayTime = getWorkTimeMs(timerState?.timestamps); + const handleToggle = async () => { try { - const data = await toggleTimer.mutateAsync({ issueId }); - if (data) { - setDisplayTime(data.workTimeMs); - } + await toggleTimer.mutateAsync({ issueId }); setErrorMessage(null); } catch (err) { setErrorMessage(parseError(err as Error)); @@ -52,7 +44,6 @@ export function IssueTimer({ issueId, onEnd }: { issueId: number; onEnd?: (data: try { const data = await endTimer.mutateAsync({ issueId }); if (data) { - setDisplayTime(data.workTimeMs); onEnd?.(data); } setErrorMessage(null); diff --git a/packages/frontend/src/components/timer-display.tsx b/packages/frontend/src/components/timer-display.tsx index 4fd461d..d882f1c 100644 --- a/packages/frontend/src/components/timer-display.tsx +++ b/packages/frontend/src/components/timer-display.tsx @@ -1,3 +1,4 @@ +import { calculateWorkTimeMs } from "@sprint/shared"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { useInactiveTimers, useTimerState } from "@/lib/query/hooks"; @@ -7,6 +8,12 @@ import { formatTime } from "@/lib/utils"; const FALLBACK_TIME = "--:--:--"; const REFRESH_INTERVAL_MS = 10000; +export function getWorkTimeMs(timestamps: string[] | undefined): number { + if (!timestamps?.length) return 0; + const dates = timestamps.map((t) => new Date(t)); + return calculateWorkTimeMs(dates); +} + export function TimerDisplay({ issueId }: { issueId: number }) { const { data: timerState, error: timerError } = useTimerState(issueId, { refetchInterval: REFRESH_INTERVAL_MS, @@ -15,7 +22,7 @@ export function TimerDisplay({ issueId }: { issueId: number }) { refetchInterval: REFRESH_INTERVAL_MS, }); - const [workTimeMs, setWorkTimeMs] = useState(0); + const [tick, setTick] = useState(0); const [error, setError] = useState(null); const combinedError = timerError ?? inactiveError; @@ -30,27 +37,26 @@ export function TimerDisplay({ issueId }: { issueId: number }) { return; } setError(null); - setWorkTimeMs(timerState?.workTimeMs ?? 0); - }, [combinedError, timerState]); + }, [combinedError]); useEffect(() => { if (!timerState?.isRunning) return; - const startTime = Date.now(); - const baseWorkTime = timerState.workTimeMs; const interval = window.setInterval(() => { - setWorkTimeMs(baseWorkTime + (Date.now() - startTime)); + setTick((t) => t + 1); }, 1000); return () => window.clearInterval(interval); - }, [timerState?.isRunning, timerState?.workTimeMs]); + }, [timerState?.isRunning]); const inactiveWorkTimeMs = useMemo( - () => inactiveTimers.reduce((total, session) => total + (session?.workTimeMs ?? 0), 0), + () => inactiveTimers.reduce((total, session) => total + getWorkTimeMs(session?.timestamps), 0), [inactiveTimers], ); - const totalWorkTimeMs = inactiveWorkTimeMs + workTimeMs; + void tick; + const currentWorkTimeMs = getWorkTimeMs(timerState?.timestamps); + const totalWorkTimeMs = inactiveWorkTimeMs + currentWorkTimeMs; const displayWorkTime = error ? FALLBACK_TIME : formatTime(totalWorkTimeMs); return ( diff --git a/packages/frontend/src/lib/query/hooks/timers.ts b/packages/frontend/src/lib/query/hooks/timers.ts index 347794d..ebae3ce 100644 --- a/packages/frontend/src/lib/query/hooks/timers.ts +++ b/packages/frontend/src/lib/query/hooks/timers.ts @@ -29,8 +29,8 @@ export function useToggleTimer() { return useMutation({ mutationKey: ["timers", "toggle"], mutationFn: timer.toggle, - onSuccess: (_data, variables) => { - queryClient.invalidateQueries({ queryKey: queryKeys.timers.active(variables.issueId) }); + onSuccess: (data, variables) => { + queryClient.setQueryData(queryKeys.timers.active(variables.issueId), data); queryClient.invalidateQueries({ queryKey: queryKeys.timers.inactive(variables.issueId) }); }, }); @@ -42,8 +42,8 @@ export function useEndTimer() { return useMutation({ mutationKey: ["timers", "end"], mutationFn: timer.end, - onSuccess: (_data, variables) => { - queryClient.invalidateQueries({ queryKey: queryKeys.timers.active(variables.issueId) }); + onSuccess: (data, variables) => { + queryClient.setQueryData(queryKeys.timers.active(variables.issueId), data); queryClient.invalidateQueries({ queryKey: queryKeys.timers.inactive(variables.issueId) }); }, });