mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
fixed timer desync issues
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import type { TimerState } from "@sprint/shared";
|
import type { TimerState } from "@sprint/shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { getWorkTimeMs } from "@/components/timer-display";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useEndTimer, useTimerState, useToggleTimer } from "@/lib/query/hooks";
|
import { useEndTimer, useTimerState, useToggleTimer } from "@/lib/query/hooks";
|
||||||
import { parseError } from "@/lib/server";
|
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 { data: timerState, error } = useTimerState(issueId);
|
||||||
const toggleTimer = useToggleTimer();
|
const toggleTimer = useToggleTimer();
|
||||||
const endTimer = useEndTimer();
|
const endTimer = useEndTimer();
|
||||||
const [displayTime, setDisplayTime] = useState(0);
|
const [tick, setTick] = useState(0);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (timerState) {
|
|
||||||
setDisplayTime(timerState.workTimeMs);
|
|
||||||
}
|
|
||||||
}, [timerState]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!timerState?.isRunning) return;
|
if (!timerState?.isRunning) return;
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const baseTime = timerState.workTimeMs;
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setDisplayTime(baseTime + (Date.now() - startTime));
|
setTick((t) => t + 1);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [timerState?.isRunning, timerState?.workTimeMs]);
|
}, [timerState?.isRunning]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!error) return;
|
if (!error) return;
|
||||||
setErrorMessage(parseError(error as Error));
|
setErrorMessage(parseError(error as Error));
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
|
void tick;
|
||||||
|
const displayTime = getWorkTimeMs(timerState?.timestamps);
|
||||||
|
|
||||||
const handleToggle = async () => {
|
const handleToggle = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await toggleTimer.mutateAsync({ issueId });
|
await toggleTimer.mutateAsync({ issueId });
|
||||||
if (data) {
|
|
||||||
setDisplayTime(data.workTimeMs);
|
|
||||||
}
|
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrorMessage(parseError(err as Error));
|
setErrorMessage(parseError(err as Error));
|
||||||
@@ -52,7 +44,6 @@ export function IssueTimer({ issueId, onEnd }: { issueId: number; onEnd?: (data:
|
|||||||
try {
|
try {
|
||||||
const data = await endTimer.mutateAsync({ issueId });
|
const data = await endTimer.mutateAsync({ issueId });
|
||||||
if (data) {
|
if (data) {
|
||||||
setDisplayTime(data.workTimeMs);
|
|
||||||
onEnd?.(data);
|
onEnd?.(data);
|
||||||
}
|
}
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { calculateWorkTimeMs } from "@sprint/shared";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useInactiveTimers, useTimerState } from "@/lib/query/hooks";
|
import { useInactiveTimers, useTimerState } from "@/lib/query/hooks";
|
||||||
@@ -7,6 +8,12 @@ import { formatTime } from "@/lib/utils";
|
|||||||
const FALLBACK_TIME = "--:--:--";
|
const FALLBACK_TIME = "--:--:--";
|
||||||
const REFRESH_INTERVAL_MS = 10000;
|
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 }) {
|
export function TimerDisplay({ issueId }: { issueId: number }) {
|
||||||
const { data: timerState, error: timerError } = useTimerState(issueId, {
|
const { data: timerState, error: timerError } = useTimerState(issueId, {
|
||||||
refetchInterval: REFRESH_INTERVAL_MS,
|
refetchInterval: REFRESH_INTERVAL_MS,
|
||||||
@@ -15,7 +22,7 @@ export function TimerDisplay({ issueId }: { issueId: number }) {
|
|||||||
refetchInterval: REFRESH_INTERVAL_MS,
|
refetchInterval: REFRESH_INTERVAL_MS,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [workTimeMs, setWorkTimeMs] = useState(0);
|
const [tick, setTick] = useState(0);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const combinedError = timerError ?? inactiveError;
|
const combinedError = timerError ?? inactiveError;
|
||||||
@@ -30,27 +37,26 @@ export function TimerDisplay({ issueId }: { issueId: number }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
setWorkTimeMs(timerState?.workTimeMs ?? 0);
|
}, [combinedError]);
|
||||||
}, [combinedError, timerState]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!timerState?.isRunning) return;
|
if (!timerState?.isRunning) return;
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const baseWorkTime = timerState.workTimeMs;
|
|
||||||
const interval = window.setInterval(() => {
|
const interval = window.setInterval(() => {
|
||||||
setWorkTimeMs(baseWorkTime + (Date.now() - startTime));
|
setTick((t) => t + 1);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => window.clearInterval(interval);
|
return () => window.clearInterval(interval);
|
||||||
}, [timerState?.isRunning, timerState?.workTimeMs]);
|
}, [timerState?.isRunning]);
|
||||||
|
|
||||||
const inactiveWorkTimeMs = useMemo(
|
const inactiveWorkTimeMs = useMemo(
|
||||||
() => inactiveTimers.reduce((total, session) => total + (session?.workTimeMs ?? 0), 0),
|
() => inactiveTimers.reduce((total, session) => total + getWorkTimeMs(session?.timestamps), 0),
|
||||||
[inactiveTimers],
|
[inactiveTimers],
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalWorkTimeMs = inactiveWorkTimeMs + workTimeMs;
|
void tick;
|
||||||
|
const currentWorkTimeMs = getWorkTimeMs(timerState?.timestamps);
|
||||||
|
const totalWorkTimeMs = inactiveWorkTimeMs + currentWorkTimeMs;
|
||||||
const displayWorkTime = error ? FALLBACK_TIME : formatTime(totalWorkTimeMs);
|
const displayWorkTime = error ? FALLBACK_TIME : formatTime(totalWorkTimeMs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export function useToggleTimer() {
|
|||||||
return useMutation<TimerState, Error, TimerToggleRequest>({
|
return useMutation<TimerState, Error, TimerToggleRequest>({
|
||||||
mutationKey: ["timers", "toggle"],
|
mutationKey: ["timers", "toggle"],
|
||||||
mutationFn: timer.toggle,
|
mutationFn: timer.toggle,
|
||||||
onSuccess: (_data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.timers.active(variables.issueId) });
|
queryClient.setQueryData(queryKeys.timers.active(variables.issueId), data);
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.timers.inactive(variables.issueId) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.timers.inactive(variables.issueId) });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -42,8 +42,8 @@ export function useEndTimer() {
|
|||||||
return useMutation<TimerState, Error, TimerEndRequest>({
|
return useMutation<TimerState, Error, TimerEndRequest>({
|
||||||
mutationKey: ["timers", "end"],
|
mutationKey: ["timers", "end"],
|
||||||
mutationFn: timer.end,
|
mutationFn: timer.end,
|
||||||
onSuccess: (_data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.timers.active(variables.issueId) });
|
queryClient.setQueryData(queryKeys.timers.active(variables.issueId), data);
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.timers.inactive(variables.issueId) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.timers.inactive(variables.issueId) });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user