fix issue 210 slow source search reliability

This commit is contained in:
kuekhaoyang
2026-07-13 21:09:47 +08:00
parent 2ae0a03366
commit 66d8564dd2
14 changed files with 364 additions and 140 deletions
+62 -30
View File
@@ -5,6 +5,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { settingsStore } from '@/lib/store/settings-store';
import { probeLatencyTargets } from '@/lib/utils/latency';
interface LatencyState {
[sourceId: string]: number;
@@ -23,17 +24,18 @@ export function useLatencyPing({
}: UseLatencyPingOptions) {
const [latencies, setLatencies] = useState<LatencyState>({});
const [isLoading, setIsLoading] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const intervalRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const roundInFlightRef = useRef(false);
const mountedRef = useRef(true);
// Check if real-time latency is enabled in settings
const [realtimeEnabled, setRealtimeEnabled] = useState(false);
// Stabilize sourceUrls to prevent unnecessary effect re-runs if parent passes new array
const stableSourceUrls = useMemo(() => sourceUrls, [
// Create a unique key for the sources array
sourceUrls.map(s => `${s.id}|${s.baseUrl}`).join(',')
]);
const stableSourceUrls = useMemo(
() => sourceUrls.map((source) => ({ ...source })),
[sourceUrls],
);
useEffect(() => {
const settings = settingsStore.getSettings();
@@ -50,47 +52,64 @@ export function useLatencyPing({
};
}, []);
const pingSource = useCallback(async (sourceId: string, baseUrl: string): Promise<number | null> => {
const pingSource = useCallback(async (baseUrl: string): Promise<number | null> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 12000);
try {
const response = await fetch('/api/ping', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: baseUrl }),
signal: controller.signal,
});
if (response.ok) {
const data = await response.json();
return data.latency || null;
return data.success && typeof data.latency === 'number'
? data.latency
: null;
}
return null;
} catch {
return null;
} finally {
clearTimeout(timeoutId);
}
}, []);
const pingAllSources = useCallback(async () => {
if (!mountedRef.current || stableSourceUrls.length === 0) return;
if (
!mountedRef.current ||
stableSourceUrls.length === 0 ||
roundInFlightRef.current
) return;
roundInFlightRef.current = true;
setIsLoading(true);
const results = await Promise.all(
stableSourceUrls.map(async ({ id, baseUrl }) => {
const latency = await pingSource(id, baseUrl);
return { id, latency };
})
);
try {
const results = await probeLatencyTargets(
stableSourceUrls,
({ baseUrl }) => pingSource(baseUrl),
);
if (mountedRef.current) {
setLatencies(prev => {
const newState = { ...prev };
results.forEach(({ id, latency }) => {
if (latency !== null) {
newState[id] = latency;
}
if (mountedRef.current) {
setLatencies(prev => {
const newState = { ...prev };
results.forEach(({ id, latency }) => {
if (latency !== null) {
newState[id] = latency;
}
});
return newState;
});
return newState;
});
setIsLoading(false);
}
} finally {
roundInFlightRef.current = false;
if (mountedRef.current) {
setIsLoading(false);
}
}
}, [stableSourceUrls, pingSource]);
@@ -101,17 +120,30 @@ export function useLatencyPing({
const shouldPoll = enabled && realtimeEnabled && stableSourceUrls.length > 0;
if (shouldPoll) {
// Initial ping
pingAllSources();
let cancelled = false;
const poll = async () => {
await pingAllSources();
if (!cancelled && mountedRef.current) {
intervalRef.current = setTimeout(poll, intervalMs);
}
};
// Set up interval
intervalRef.current = setInterval(pingAllSources, intervalMs);
void poll();
return () => {
cancelled = true;
mountedRef.current = false;
if (intervalRef.current) {
clearTimeout(intervalRef.current);
intervalRef.current = null;
}
};
}
return () => {
mountedRef.current = false;
if (intervalRef.current) {
clearInterval(intervalRef.current);
clearTimeout(intervalRef.current);
intervalRef.current = null;
}
};
@@ -120,7 +152,7 @@ export function useLatencyPing({
const refreshLatency = useCallback((sourceId: string) => {
const source = stableSourceUrls.find(s => s.id === sourceId);
if (source) {
pingSource(sourceId, source.baseUrl).then(latency => {
pingSource(source.baseUrl).then(latency => {
if (latency !== null && mountedRef.current) {
setLatencies(prev => ({ ...prev, [sourceId]: latency }));
}
+7 -3
View File
@@ -1,14 +1,13 @@
import { useRef, useCallback } from 'react';
import { SOURCE_IDS } from '@/lib/utils/source-names';
import { sortVideos } from '@/lib/utils/sort';
import { binaryInsertVideos } from '@/lib/utils/sorted-insert';
import { processSearchStream } from '@/lib/utils/search-stream';
import type { SortOption } from '@/lib/store/settings-store';
import { settingsStore } from '@/lib/store/settings-store';
import type { Video } from '@/lib/types';
import { useSearchState } from './useSearchState';
type SearchState = ReturnType<typeof useSearchState>;
type SearchSourceConfig = { id: string; baseUrl?: string };
interface UseSearchActionProps {
state: SearchState;
@@ -77,7 +76,10 @@ export function useSearchAction({ state, onCacheUpdate, onUrlUpdate }: UseSearch
const reader = response.body?.getReader();
if (!reader) throw new Error('No response stream');
const sourcesMap = new Map<string, { count: number; name: string }>();
const sourceConfigs = new Map<string, SearchSourceConfig>(
targetSources.map((source: SearchSourceConfig) => [source.id, source])
);
const sourcesMap = new Map<string, { count: number; name: string; baseUrl?: string }>();
await processSearchStream({
reader,
@@ -95,6 +97,7 @@ export function useSearchAction({ state, onCacheUpdate, onUrlUpdate }: UseSearch
sourcesMap.set(sourceId, {
count: newVideos.length,
name: newVideos[0]?.sourceName || sourceId,
baseUrl: sourceConfigs.get(sourceId)?.baseUrl,
});
}
},
@@ -113,6 +116,7 @@ export function useSearchAction({ state, onCacheUpdate, onUrlUpdate }: UseSearch
id: id,
name: info.name,
count: info.count,
...(info.baseUrl ? { baseUrl: info.baseUrl } : {}),
}));
setAvailableSources(sources);