seems alright

This commit is contained in:
Andras Schmelczer 2026-05-17 13:52:11 +01:00
parent ebe7bbb51d
commit eac1bd0d13
58 changed files with 23125 additions and 153505 deletions

View file

@ -12,8 +12,16 @@ export interface SavedSearch {
created: string;
}
const POLL_INTERVAL_MS = 2000;
const MAX_POLL_ATTEMPTS = 15;
// Exponential backoff: 2s, 3s, 4s, 6s, 8s, 12s, ... capped at 15s.
// Caps total wait under a minute while staying responsive for fast jobs.
const POLL_BASE_MS = 2000;
const POLL_MAX_MS = 15000;
const POLL_BACKOFF = 1.5;
const MAX_POLL_ATTEMPTS = 8;
function nextPollDelay(attempt: number): number {
return Math.min(POLL_MAX_MS, Math.round(POLL_BASE_MS * Math.pow(POLL_BACKOFF, attempt)));
}
export function useSavedSearches(userId: string | null) {
const [searches, setSearches] = useState<SavedSearch[]>([]);
@ -21,14 +29,16 @@ export function useSavedSearches(userId: string | null) {
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const pollAttemptsRef = useRef(0);
const pollInFlightRef = useRef(false);
const isMountedRef = useRef(true);
const userIdRef = useRef(userId);
userIdRef.current = userId;
const stopPolling = useCallback(() => {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
clearTimeout(pollTimerRef.current);
pollTimerRef.current = null;
}
pollAttemptsRef.current = 0;
@ -37,6 +47,15 @@ export function useSavedSearches(userId: string | null) {
// Clean up polling on unmount or userId change
useEffect(() => stopPolling, [userId, stopPolling]);
// Mark the hook as unmounted so late-arriving async work doesn't touch state
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
stopPolling();
};
}, [stopPolling]);
const fetchRecords = useCallback(async (uid: string): Promise<SavedSearch[]> => {
const records = await pb.collection('saved_searches').getFullList({
sort: '-created',
@ -57,28 +76,41 @@ export function useSavedSearches(userId: string | null) {
const startPolling = useCallback(() => {
if (pollTimerRef.current) return;
pollAttemptsRef.current = 0;
pollTimerRef.current = setInterval(async () => {
pollInFlightRef.current = false;
const scheduleNext = () => {
if (!isMountedRef.current) return;
const delay = nextPollDelay(pollAttemptsRef.current);
pollTimerRef.current = setTimeout(tick, delay);
};
const tick = async () => {
pollTimerRef.current = null;
if (pollInFlightRef.current) {
scheduleNext();
return;
}
const uid = userIdRef.current;
if (!uid) {
stopPolling();
return;
}
if (!uid) return;
pollAttemptsRef.current++;
if (pollAttemptsRef.current >= MAX_POLL_ATTEMPTS) {
stopPolling();
return;
}
if (pollAttemptsRef.current > MAX_POLL_ATTEMPTS) return;
pollInFlightRef.current = true;
try {
const mapped = await fetchRecords(uid);
if (!isMountedRef.current) return;
setSearches(mapped);
if (!mapped.some((s) => !s.screenshotUrl)) {
stopPolling();
}
if (!mapped.some((s) => !s.screenshotUrl)) return;
scheduleNext();
} catch {
// Silent — background poll errors don't surface to UI
// Silent — background poll errors don't surface to UI; keep trying.
if (isMountedRef.current) scheduleNext();
} finally {
pollInFlightRef.current = false;
}
}, POLL_INTERVAL_MS);
}, [stopPolling, fetchRecords]);
};
scheduleNext();
}, [fetchRecords]);
const fetchSearches = useCallback(async () => {
if (!userId) return;