mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
Merge pull request #43 from Troray/fix/prevent-infinite-loop-in-subscription-sync
fix: stop infinite requests after adding new subscription
This commit is contained in:
@@ -1,83 +1,108 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
import { fetchSourcesFromUrl, mergeSources } from '@/lib/utils/source-import-utils';
|
||||
import type { SourceSubscription } from '@/lib/types';
|
||||
|
||||
// Minimum time between syncs for the same subscription (5 minutes)
|
||||
const SYNC_COOLDOWN_MS = 5 * 60 * 1000;
|
||||
// Delay before initial sync to ensure settings are fully loaded
|
||||
const INITIAL_SYNC_DELAY_MS = 1000;
|
||||
|
||||
export function useSubscriptionSync() {
|
||||
const [subscriptions, setSubscriptions] = useState(() => settingsStore.getSettings().subscriptions);
|
||||
// Track if we've already synced during this component lifecycle
|
||||
const hasSyncedRef = useRef(false);
|
||||
// Track if sync is currently in progress to avoid concurrent syncs
|
||||
const isSyncingRef = useRef(false);
|
||||
|
||||
// Subscribe to settings changes to detect when subscriptions are updated (e.g. from PasswordGate env sync)
|
||||
// Effect to run sync only once on mount
|
||||
useEffect(() => {
|
||||
const unsubscribe = settingsStore.subscribe(() => {
|
||||
const currentSubs = settingsStore.getSettings().subscriptions;
|
||||
setSubscriptions(currentSubs);
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
// Prevent multiple syncs if this effect runs multiple times (React StrictMode)
|
||||
if (hasSyncedRef.current || isSyncingRef.current) return;
|
||||
|
||||
// Effect to run the sync when subscriptions change
|
||||
useEffect(() => {
|
||||
const sync = async () => {
|
||||
const activeSubscriptions = subscriptions.filter((s: any) => s.autoRefresh !== false);
|
||||
if (activeSubscriptions.length === 0) return;
|
||||
// Double-check to prevent race conditions
|
||||
if (hasSyncedRef.current || isSyncingRef.current) return;
|
||||
|
||||
// We need to check if we actually need to sync.
|
||||
// If we just synced, or if nothing changed, maybe skip?
|
||||
// For now, let's rely on a simplified approach:
|
||||
// If the subscription list length/content changes, we might want to re-sync.
|
||||
// But be careful of infinite loops if we update sources inside this effect.
|
||||
isSyncingRef.current = true;
|
||||
|
||||
const settings = settingsStore.getSettings();
|
||||
let anyChanged = false;
|
||||
let currentSources = [...settings.sources];
|
||||
let currentPremiumSources = [...settings.premiumSources];
|
||||
// We use a local copy of subscriptions to avoid re-triggering this effect when we update 'lastUpdated'
|
||||
let updatedSubscriptions = [...subscriptions];
|
||||
try {
|
||||
// Read subscriptions directly from store (not via state to avoid re-renders)
|
||||
const settings = settingsStore.getSettings();
|
||||
const activeSubscriptions = settings.subscriptions.filter((s: SourceSubscription) => s.autoRefresh !== false);
|
||||
|
||||
for (let i = 0; i < activeSubscriptions.length; i++) {
|
||||
const sub = activeSubscriptions[i];
|
||||
|
||||
// Optional: Check if we synced this recently (e.g. within 5 minutes) to avoid spamming on hot-reload/nav
|
||||
// const now = Date.now();
|
||||
// if (sub.lastUpdated && now - sub.lastUpdated < 5 * 60 * 1000) continue;
|
||||
|
||||
try {
|
||||
const result = await fetchSourcesFromUrl(sub.url);
|
||||
|
||||
if (result.normalSources.length > 0) {
|
||||
currentSources = mergeSources(currentSources, result.normalSources);
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
if (result.premiumSources.length > 0) {
|
||||
currentPremiumSources = mergeSources(currentPremiumSources, result.premiumSources);
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
// Update timestamp
|
||||
const subIdx = updatedSubscriptions.findIndex(s => s.id === sub.id);
|
||||
if (subIdx !== -1) {
|
||||
updatedSubscriptions[subIdx] = {
|
||||
...updatedSubscriptions[subIdx],
|
||||
lastUpdated: Date.now()
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to sync subscription: ${sub.name}`, e);
|
||||
if (activeSubscriptions.length === 0) {
|
||||
hasSyncedRef.current = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyChanged) {
|
||||
settingsStore.saveSettings({
|
||||
...settings,
|
||||
sources: currentSources,
|
||||
premiumSources: currentPremiumSources,
|
||||
subscriptions: updatedSubscriptions
|
||||
let anyChanged = false;
|
||||
let currentSources = [...settings.sources];
|
||||
let currentPremiumSources = [...settings.premiumSources];
|
||||
let updatedSubscriptions = [...settings.subscriptions];
|
||||
const now = Date.now();
|
||||
|
||||
// Filter out subscriptions that were synced recently (within cooldown period)
|
||||
const subsToSync = activeSubscriptions.filter(
|
||||
(sub: SourceSubscription) => !(sub.lastUpdated && now - sub.lastUpdated < SYNC_COOLDOWN_MS)
|
||||
);
|
||||
|
||||
if (subsToSync.length === 0) {
|
||||
hasSyncedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all subscriptions in parallel for better performance
|
||||
const results = await Promise.allSettled(
|
||||
subsToSync.map((sub: SourceSubscription) => fetchSourcesFromUrl(sub.url))
|
||||
);
|
||||
|
||||
// Process results
|
||||
results.forEach((result, index) => {
|
||||
const sub = subsToSync[index];
|
||||
if (result.status === 'fulfilled') {
|
||||
const fetchResult = result.value;
|
||||
|
||||
if (fetchResult.normalSources.length > 0) {
|
||||
currentSources = mergeSources(currentSources, fetchResult.normalSources);
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
if (fetchResult.premiumSources.length > 0) {
|
||||
currentPremiumSources = mergeSources(currentPremiumSources, fetchResult.premiumSources);
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
// Update timestamp for successful sync
|
||||
const subIdx = updatedSubscriptions.findIndex(s => s.id === sub.id);
|
||||
if (subIdx !== -1) {
|
||||
updatedSubscriptions[subIdx] = {
|
||||
...updatedSubscriptions[subIdx],
|
||||
lastUpdated: now
|
||||
};
|
||||
anyChanged = true;
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed to sync subscription: ${sub.name}`, result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
if (anyChanged) {
|
||||
settingsStore.saveSettings({
|
||||
...settings,
|
||||
sources: currentSources,
|
||||
premiumSources: currentPremiumSources,
|
||||
subscriptions: updatedSubscriptions
|
||||
});
|
||||
}
|
||||
|
||||
hasSyncedRef.current = true;
|
||||
} finally {
|
||||
isSyncingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Debounce slightly to avoid rapid-fire updates if multiple settings change
|
||||
const timeoutId = setTimeout(sync, 1000);
|
||||
// Small delay to ensure settings are fully loaded
|
||||
const timeoutId = setTimeout(sync, INITIAL_SYNC_DELAY_MS);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [subscriptions]); // Only re-run if subscriptions array reference changes (which happens on saveSettings)
|
||||
}, []); // Empty dependency array - only run once on mount
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user