refactor(frontend): replace manual react queue state throttling with native tokio concurrency

This commit is contained in:
NimBold
2026-06-13 22:13:23 +03:30
parent 240f62bc6e
commit a2b64aa6d1
3 changed files with 48 additions and 94 deletions
+13 -1
View File
@@ -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!())
+25 -7
View File
@@ -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) => {
+10 -86
View File
@@ -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,