refactor: Configure QueryClient with memory management settings

This commit is contained in:
Peifan Li
2025-12-26 17:26:14 -05:00
parent e5fcf665a5
commit e03db8f8d6
12 changed files with 210 additions and 124 deletions

View File

@@ -123,7 +123,23 @@ function AppContent() {
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
// Configure QueryClient with memory management settings
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Cache data for 5 minutes by default
staleTime: 5 * 60 * 1000,
// Keep unused data in cache for 10 minutes before garbage collection
gcTime: 10 * 60 * 1000, // Previously called cacheTime
// Retry failed requests 3 times instead of default
retry: 3,
// Refetch on window focus only if data is stale
refetchOnWindowFocus: false,
// Don't refetch on reconnect by default
refetchOnReconnect: false,
},
},
});
function App() {
return (

View File

@@ -93,9 +93,11 @@ const Header: React.FC<HeaderProps> = ({
};
checkActiveSubscriptions();
// Poll every 10 seconds to update indicator
const interval = setInterval(checkActiveSubscriptions, 10000);
return () => clearInterval(interval);
// Poll every 30 seconds to update indicator (reduced frequency)
const interval = setInterval(checkActiveSubscriptions, 30000);
return () => {
clearInterval(interval);
};
}, [visitorMode]);
useEffect(() => {

View File

@@ -14,7 +14,7 @@ import {
useMediaQuery,
useTheme
} from '@mui/material';
import React, { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useCollection } from '../contexts/CollectionContext';
import { useLanguage } from '../contexts/LanguageContext';
@@ -75,8 +75,25 @@ const VideoCard: React.FC<VideoCardProps> = ({
const handleMouseLeave = () => {
setIsHovered(false);
setIsVideoPlaying(false);
// Cleanup video element when mouse leaves
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.src = '';
videoRef.current.load();
}
};
// Cleanup video element on unmount
useEffect(() => {
return () => {
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.src = '';
videoRef.current.load();
}
};
}, []);
// Format the date (assuming format YYYYMMDD from youtube-dl)
const formatDate = (dateString: string) => {
if (!dateString || dateString.length !== 8) {

View File

@@ -1,7 +1,7 @@
import { Alert, Box, Divider, Stack } from '@mui/material';
import React from 'react';
import { useVideoResolution } from '../../hooks/useVideoResolution';
import React, { useEffect } from 'react';
import { useCloudStorageUrl } from '../../hooks/useCloudStorageUrl';
import { useVideoResolution } from '../../hooks/useVideoResolution';
import { Collection, Video } from '../../types';
import EditableTitle from './VideoInfo/EditableTitle';
import VideoActionButtons from './VideoInfo/VideoActionButtons';
@@ -53,6 +53,18 @@ const VideoInfo: React.FC<VideoInfoProps> = ({
const { videoRef, videoResolution } = useVideoResolution(video);
const videoUrl = useCloudStorageUrl(video.videoPath, 'video');
// Cleanup video element on unmount
useEffect(() => {
return () => {
const videoElement = videoRef.current;
if (videoElement) {
// Clear video source to free memory
videoElement.src = '';
videoElement.load();
}
};
}, []);
return (
<Box sx={{ mt: 2 }}>
{/* Hidden video element to get resolution */}

View File

@@ -82,8 +82,17 @@ export const DownloadProvider: React.FC<{ children: React.ReactNode }> = ({ chil
const response = await axios.get(`${API_URL}/download-status`);
return response.data;
},
refetchInterval: 2000,
initialData: initialStatus || { activeDownloads: [], queuedDownloads: [] }
// Only poll when there are active or queued downloads
refetchInterval: (query) => {
const data = query.state.data as { activeDownloads?: any[]; queuedDownloads?: any[] } | undefined;
const hasActive = (data?.activeDownloads?.length ?? 0) > 0;
const hasQueued = (data?.queuedDownloads?.length ?? 0) > 0;
// Poll every 2 seconds if there are downloads, otherwise stop polling
return hasActive || hasQueued ? 2000 : false;
},
initialData: initialStatus || { activeDownloads: [], queuedDownloads: [] },
staleTime: 1000, // Consider data stale after 1 second
gcTime: 5 * 60 * 1000, // Garbage collect after 5 minutes
});
const activeDownloads = downloadStatus.activeDownloads || [];

View File

@@ -66,8 +66,10 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
throw err;
}
},
retry: 10,
retry: 3, // Reduced from 10 to 3
retryDelay: 1000,
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
gcTime: 30 * 60 * 1000, // Garbage collect after 30 minutes (videos are important data)
});
// Filter invisible videos when in visitor mode
@@ -85,8 +87,10 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
const response = await axios.get(`${API_URL}/settings`);
return response.data;
},
retry: 10,
retry: 3, // Reduced from 10 to 3
retryDelay: 1000,
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
gcTime: 15 * 60 * 1000, // Garbage collect after 15 minutes
});
const availableTags = settingsData?.tags || [];

View File

@@ -21,7 +21,9 @@ export const VisitorModeProvider: React.FC<{ children: ReactNode }> = ({ childre
const response = await axios.get(`${API_URL}/settings`);
return response.data;
},
refetchInterval: 5000, // Refetch every 5 seconds to keep visitor mode state in sync
refetchInterval: 30000, // Refetch every 30 seconds (reduced frequency)
staleTime: 10000, // Consider data fresh for 10 seconds
gcTime: 10 * 60 * 1000, // Garbage collect after 10 minutes
});
const visitorMode = settingsData?.visitorMode === true;

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
const API_URL = import.meta.env.VITE_API_URL;
@@ -12,13 +12,21 @@ interface CloudflareStatus {
export const useCloudflareStatus = (enabled: boolean = true) => {
return useQuery<CloudflareStatus>({
queryKey: ['cloudflaredStatus'],
queryKey: ["cloudflaredStatus"],
queryFn: async () => {
if (!enabled) return { isRunning: false, tunnelId: null, accountTag: null, publicUrl: null };
if (!enabled)
return {
isRunning: false,
tunnelId: null,
accountTag: null,
publicUrl: null,
};
const res = await axios.get(`${API_URL}/settings/cloudflared/status`);
return res.data;
},
enabled: !!enabled,
refetchInterval: 5000 // Poll every 5 seconds
refetchInterval: enabled ? 10000 : false, // Poll every 10 seconds when enabled (reduced frequency)
staleTime: 5000, // Consider data fresh for 5 seconds
gcTime: 5 * 60 * 1000, // Garbage collect after 5 minutes
});
};

View File

@@ -1,8 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { Video } from '../types';
import { useCloudStorageUrl } from './useCloudStorageUrl';
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
import { useEffect, useRef, useState } from "react";
import { Video } from "../types";
import { useCloudStorageUrl } from "./useCloudStorageUrl";
// Format video resolution from video object
export const formatResolution = (video: Video): string | null => {
@@ -24,25 +22,25 @@ export const formatResolution = (video: Video): string | null => {
const height = (video as any).height;
if (width && height) {
const h = typeof height === 'number' ? height : parseInt(String(height));
const h = typeof height === "number" ? height : parseInt(String(height));
if (!isNaN(h)) {
if (h >= 2160) return '4K';
if (h >= 1440) return '1440P';
if (h >= 1080) return '1080P';
if (h >= 720) return '720P';
if (h >= 480) return '480P';
if (h >= 360) return '360P';
if (h >= 240) return '240P';
if (h >= 144) return '144P';
if (h >= 2160) return "4K";
if (h >= 1440) return "1440P";
if (h >= 1080) return "1080P";
if (h >= 720) return "720P";
if (h >= 480) return "480P";
if (h >= 360) return "360P";
if (h >= 240) return "240P";
if (h >= 144) return "144P";
}
}
// Check if there's a format_id or format that might contain resolution info
const formatId = (video as any).format_id;
if (formatId && typeof formatId === 'string') {
if (formatId && typeof formatId === "string") {
const match = formatId.match(/(\d+)p/i);
if (match) {
return match[1].toUpperCase() + 'P';
return match[1].toUpperCase() + "P";
}
}
@@ -51,8 +49,10 @@ export const formatResolution = (video: Video): string | null => {
export const useVideoResolution = (video: Video) => {
const videoRef = useRef<HTMLVideoElement>(null);
const [detectedResolution, setDetectedResolution] = useState<string | null>(null);
const videoUrl = useCloudStorageUrl(video.videoPath, 'video');
const [detectedResolution, setDetectedResolution] = useState<string | null>(
null
);
const videoUrl = useCloudStorageUrl(video.videoPath, "video");
useEffect(() => {
const videoElement = videoRef.current;
@@ -71,14 +71,14 @@ export const useVideoResolution = (video: Video) => {
const handleLoadedMetadata = () => {
const height = videoElement.videoHeight;
if (height && height > 0) {
if (height >= 2160) setDetectedResolution('4K');
else if (height >= 1440) setDetectedResolution('1440P');
else if (height >= 1080) setDetectedResolution('1080P');
else if (height >= 720) setDetectedResolution('720P');
else if (height >= 480) setDetectedResolution('480P');
else if (height >= 360) setDetectedResolution('360P');
else if (height >= 240) setDetectedResolution('240P');
else if (height >= 144) setDetectedResolution('144P');
if (height >= 2160) setDetectedResolution("4K");
else if (height >= 1440) setDetectedResolution("1440P");
else if (height >= 1080) setDetectedResolution("1080P");
else if (height >= 720) setDetectedResolution("720P");
else if (height >= 480) setDetectedResolution("480P");
else if (height >= 360) setDetectedResolution("360P");
else if (height >= 240) setDetectedResolution("240P");
else if (height >= 144) setDetectedResolution("144P");
else setDetectedResolution(null);
} else {
setDetectedResolution(null);
@@ -89,8 +89,8 @@ export const useVideoResolution = (video: Video) => {
setDetectedResolution(null);
};
videoElement.addEventListener('loadedmetadata', handleLoadedMetadata);
videoElement.addEventListener('error', handleError);
videoElement.addEventListener("loadedmetadata", handleLoadedMetadata);
videoElement.addEventListener("error", handleError);
// If metadata is already loaded
if (videoElement.readyState >= 1 && videoElement.videoHeight > 0) {
@@ -98,8 +98,12 @@ export const useVideoResolution = (video: Video) => {
}
return () => {
videoElement.removeEventListener('loadedmetadata', handleLoadedMetadata);
videoElement.removeEventListener('error', handleError);
// Cleanup: remove event listeners
videoElement.removeEventListener("loadedmetadata", handleLoadedMetadata);
videoElement.removeEventListener("error", handleError);
// Cleanup: clear video source to free memory
videoElement.src = "";
videoElement.load();
};
}, [videoUrl, video.sourceUrl, video.id]);
@@ -109,4 +113,3 @@ export const useVideoResolution = (video: Video) => {
return { videoRef, videoResolution };
};

View File

@@ -49,14 +49,17 @@ const DownloadPage: React.FC = () => {
}
};
// Fetch history with polling
// Fetch history with polling - only when on downloads page
const { data: history = [] } = useQuery({
queryKey: ['downloadHistory'],
queryFn: async () => {
const response = await axios.get(`${API_URL}/downloads/history`);
return response.data;
},
refetchInterval: 2000
// Only poll when tab is active (downloads tab)
refetchInterval: tabValue === 0 ? 2000 : false,
staleTime: 1000, // Consider data stale after 1 second
gcTime: 5 * 60 * 1000, // Garbage collect after 5 minutes
});
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {

View File

@@ -423,7 +423,9 @@ const SettingsPage: React.FC = () => {
const response = await axios.get(`${API_URL}/settings/last-backup-info`);
return response.data;
},
refetchInterval: 30000, // Refetch every 30 seconds
refetchInterval: 60000, // Refetch every 60 seconds (reduced frequency)
staleTime: 30000, // Consider data fresh for 30 seconds
gcTime: 10 * 60 * 1000, // Garbage collect after 10 minutes
});
// Restore from last backup mutation

View File

@@ -75,6 +75,7 @@ const SubscriptionsPage: React.FC = () => {
},
refetchInterval: 30000, // Refetch every 30 seconds (less frequent)
staleTime: 10000, // Consider data fresh for 10 seconds
gcTime: 10 * 60 * 1000, // Garbage collect after 10 minutes
});
const { data: tasks = [], refetch: refetchTasks } = useQuery({
@@ -83,8 +84,15 @@ const SubscriptionsPage: React.FC = () => {
const response = await axios.get(`${API_URL}/subscriptions/tasks`);
return response.data as ContinuousDownloadTask[];
},
refetchInterval: 10000, // Poll every 10 seconds
// Only poll when there are active tasks
refetchInterval: (query) => {
const data = query.state.data as ContinuousDownloadTask[] | undefined;
const hasActive = data?.some(task => task.status === 'active' || task.status === 'paused') ?? false;
// Poll every 10 seconds if there are active tasks, otherwise every 60 seconds
return hasActive ? 10000 : 60000;
},
staleTime: 5000, // Consider data fresh for 5 seconds
gcTime: 10 * 60 * 1000, // Garbage collect after 10 minutes
});
const handleUnsubscribeClick = (id: string, author: string) => {