mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
refactor(frontend): replace manual react queue state throttling with native tokio concurrency
This commit is contained in:
@@ -984,6 +984,18 @@ async fn set_concurrent_limit(state: tauri::State<'_, AppState>, limit: usize) -
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_global_speed_limit(state: tauri::State<'_, AppState>, limit: Option<String>) -> Result<(), String> {
|
||||
let limit_str = limit.unwrap_or_else(|| "0".to_string());
|
||||
let _ = rpc_call(
|
||||
state.aria2_port,
|
||||
&state.aria2_secret,
|
||||
"aria2.changeGlobalOption",
|
||||
serde_json::json!([{"max-overall-download-limit": limit_str}])
|
||||
).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn request_automation_permission() -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -1383,7 +1395,7 @@ pub fn run() {
|
||||
request_automation_permission, open_automation_settings,
|
||||
set_keychain_password, get_keychain_password, delete_keychain_password,
|
||||
check_file_exists, delete_file, toggle_tray_icon, set_extension_pairing_token,
|
||||
set_extension_frontend_ready, set_concurrent_limit, remove_download,
|
||||
set_extension_frontend_ready, set_concurrent_limit, set_global_speed_limit, remove_download,
|
||||
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -93,12 +93,24 @@ function App() {
|
||||
useEffect(() => {
|
||||
if (previousSpeedLimit.current === globalSpeedLimit) return;
|
||||
previousSpeedLimit.current = globalSpeedLimit;
|
||||
const timeout = window.setTimeout(() => {
|
||||
useDownloadStore.getState().restartActiveDownloads().catch(error => {
|
||||
console.error('Failed to apply global speed limit:', error);
|
||||
});
|
||||
}, 500);
|
||||
return () => window.clearTimeout(timeout);
|
||||
|
||||
// Convert to aria2 format (e.g. "1M", "500K")
|
||||
let formattedLimit = null;
|
||||
if (globalSpeedLimit) {
|
||||
const match = globalSpeedLimit.trim().match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?(?:\/s)?$/i);
|
||||
if (match) {
|
||||
const amount = Number(match[1]);
|
||||
if (Number.isFinite(amount) && amount > 0) {
|
||||
const multipliers: Record<string, number> = { '': 1, k: 1024, m: 1048576, g: 1073741824 };
|
||||
const bytes = Math.round(amount * multipliers[match[2].toLowerCase()]);
|
||||
formattedLimit = `${bytes}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invoke('set_global_speed_limit', { limit: formattedLimit }).catch(error => {
|
||||
console.error('Failed to apply global speed limit:', error);
|
||||
});
|
||||
}, [globalSpeedLimit]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -192,7 +204,13 @@ function App() {
|
||||
useEffect(() => {
|
||||
const unlistenProgress = listen('download-progress', (event: any) => {
|
||||
const { id, fraction, speed, eta } = event.payload;
|
||||
updateDownload(id, { fraction, speed, eta });
|
||||
const state = useDownloadStore.getState();
|
||||
const current = state.downloads.find(d => d.id === id);
|
||||
if (current && current.status === 'queued') {
|
||||
updateDownload(id, { status: 'downloading', fraction, speed, eta });
|
||||
} else {
|
||||
updateDownload(id, { fraction, speed, eta });
|
||||
}
|
||||
});
|
||||
|
||||
const unlistenComplete = listen('download-complete', (event: any) => {
|
||||
|
||||
@@ -48,41 +48,7 @@ const syncSystemIntegrations = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const speedLimitToKiB = (value?: string | null): number | null => {
|
||||
if (!value) return null;
|
||||
const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?(?:\/s)?$/i);
|
||||
if (!match) return null;
|
||||
|
||||
const amount = Number(match[1]);
|
||||
if (!Number.isFinite(amount) || amount <= 0) return null;
|
||||
|
||||
const multipliers: Record<string, number> = {
|
||||
'': 1,
|
||||
k: 1,
|
||||
m: 1024,
|
||||
g: 1024 * 1024,
|
||||
t: 1024 * 1024 * 1024
|
||||
};
|
||||
return Math.max(1, Math.round(amount * multipliers[match[2].toLowerCase()]));
|
||||
};
|
||||
|
||||
const effectiveSpeedLimit = (
|
||||
itemLimit: string | null | undefined,
|
||||
globalLimit: string,
|
||||
maxConcurrentDownloads: number
|
||||
): string | null => {
|
||||
const itemKiB = speedLimitToKiB(itemLimit);
|
||||
const globalKiB = speedLimitToKiB(globalLimit);
|
||||
const perSlotGlobalKiB = globalKiB
|
||||
? Math.max(1, Math.floor(globalKiB / Math.max(maxConcurrentDownloads, 1)))
|
||||
: null;
|
||||
|
||||
const effectiveKiB = itemKiB && perSlotGlobalKiB
|
||||
? Math.min(itemKiB, perSlotGlobalKiB)
|
||||
: itemKiB ?? perSlotGlobalKiB;
|
||||
|
||||
return effectiveKiB ? `${effectiveKiB}K` : null;
|
||||
};
|
||||
// Legacy manual speed limit math removed
|
||||
|
||||
export type DownloadStatus = 'downloading' | 'paused' | 'completed' | 'failed' | 'queued';
|
||||
export const MAIN_QUEUE_ID = '00000000-0000-0000-0000-000000000001';
|
||||
@@ -117,6 +83,7 @@ export interface DownloadItem {
|
||||
isMedia?: boolean;
|
||||
mediaFormatSelector?: string;
|
||||
queueId: string;
|
||||
_dispatched?: boolean;
|
||||
}
|
||||
|
||||
export interface ExtensionDownloadRequest {
|
||||
@@ -149,7 +116,6 @@ interface DownloadState {
|
||||
addQueue: (name: string) => void;
|
||||
renameQueue: (id: string, name: string) => void;
|
||||
removeQueue: (id: string) => void;
|
||||
restartActiveDownloads: () => Promise<number>;
|
||||
}
|
||||
|
||||
export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
@@ -265,7 +231,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
set((state) => ({
|
||||
downloads: state.downloads.map(d =>
|
||||
d.id === id
|
||||
? { ...d, status: 'queued', fraction: 0, speed: '-', eta: '-' }
|
||||
? { ...d, status: 'queued', _dispatched: false, fraction: 0, speed: '-', eta: '-' }
|
||||
: d
|
||||
)
|
||||
}));
|
||||
@@ -281,7 +247,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
set((state) => ({
|
||||
downloads: state.downloads.map(item =>
|
||||
runnableIds.includes(item.id)
|
||||
? { ...item, status: 'queued', speed: '-', eta: '-' }
|
||||
? { ...item, status: 'queued', _dispatched: false, speed: '-', eta: '-' }
|
||||
: item
|
||||
)
|
||||
}));
|
||||
@@ -320,47 +286,15 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
downloads: state.downloads.map(d => d.queueId === id ? { ...d, queueId: MAIN_QUEUE_ID } : d)
|
||||
}));
|
||||
},
|
||||
restartActiveDownloads: async () => {
|
||||
const activeIds = get().downloads
|
||||
.filter(item => item.status === 'downloading')
|
||||
.map(item => item.id);
|
||||
|
||||
if (activeIds.length === 0) return 0;
|
||||
|
||||
set((state) => ({
|
||||
downloads: state.downloads.map(item =>
|
||||
activeIds.includes(item.id)
|
||||
? { ...item, status: 'paused', speed: '-', eta: '-' }
|
||||
: item
|
||||
)
|
||||
}));
|
||||
await Promise.all(activeIds.map(id => invoke('pause_download', { id }).catch(() => {})));
|
||||
await new Promise(resolve => window.setTimeout(resolve, 350));
|
||||
set((state) => ({
|
||||
downloads: state.downloads.map(item =>
|
||||
activeIds.includes(item.id)
|
||||
? { ...item, status: 'queued' }
|
||||
: item
|
||||
)
|
||||
}));
|
||||
await get().processQueue();
|
||||
return activeIds.length;
|
||||
},
|
||||
processQueue: async () => {
|
||||
const { downloads, updateDownload } = get();
|
||||
const settingsSnapshot = useSettingsStore.getState();
|
||||
const { maxConcurrentDownloads } = settingsSnapshot;
|
||||
|
||||
const activeCount = downloads.filter(d => d.status === 'downloading').length;
|
||||
if (activeCount >= maxConcurrentDownloads) return;
|
||||
|
||||
const queuedItems = downloads.filter(d => d.status === 'queued');
|
||||
const slotsAvailable = maxConcurrentDownloads - activeCount;
|
||||
|
||||
const itemsToStart = queuedItems.slice(0, slotsAvailable);
|
||||
// Find all queued items that haven't been dispatched to the backend yet
|
||||
const itemsToStart = downloads.filter(d => d.status === 'queued' && !d._dispatched);
|
||||
|
||||
for (const item of itemsToStart) {
|
||||
updateDownload(item.id, { status: 'downloading' });
|
||||
// Mark as dispatched so we don't send it again on the next pass
|
||||
updateDownload(item.id, { _dispatched: true });
|
||||
try {
|
||||
const settings = useSettingsStore.getState();
|
||||
const login = getSiteLogin(item.url, settings);
|
||||
@@ -379,11 +313,6 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
'~/Downloads';
|
||||
|
||||
if (item.isMedia) {
|
||||
const speedLimit = effectiveSpeedLimit(
|
||||
item.speedLimit,
|
||||
settings.globalSpeedLimit,
|
||||
settings.maxConcurrentDownloads
|
||||
);
|
||||
await invoke('start_media_download', {
|
||||
id: item.id,
|
||||
url: item.url,
|
||||
@@ -391,7 +320,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
filename: item.fileName,
|
||||
formatSelector: item.mediaFormatSelector || null,
|
||||
cookieSource: settings.mediaCookieSource !== 'none' ? settings.mediaCookieSource : null,
|
||||
speedLimit,
|
||||
speedLimit: item.speedLimit || null,
|
||||
username: item.username || (login ? login.username : null),
|
||||
password: item.password || keychainPassword,
|
||||
headers: item.headers || null,
|
||||
@@ -400,18 +329,13 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
maxTries: settings.maxAutomaticRetries
|
||||
});
|
||||
} else {
|
||||
const speedLimit = effectiveSpeedLimit(
|
||||
item.speedLimit,
|
||||
settings.globalSpeedLimit,
|
||||
settings.maxConcurrentDownloads
|
||||
);
|
||||
await invoke('start_download', {
|
||||
id: item.id,
|
||||
url: item.url,
|
||||
destination: destPath,
|
||||
filename: item.fileName,
|
||||
connections: item.connections || settings.perServerConnections || null,
|
||||
speedLimit,
|
||||
speedLimit: item.speedLimit || null,
|
||||
username: item.username || (login ? login.username : null),
|
||||
password: item.password || keychainPassword,
|
||||
headers: item.headers || null,
|
||||
|
||||
Reference in New Issue
Block a user