feat(queue): implement backend-driven download queue coordinator

This commit replaces the frontend-imperative download dispatcher with a centralized backend `QueueManager`. It acts as the sole concurrency gatekeeper using a single `tokio::sync::Semaphore` across all download paths (aria2 RPC, native HTTP, yt-dlp media).

Backend Changes:
- **queue**: Added `QueueManager` to manage an ordered `VecDeque` of tasks, semaphore permits, and retirement debt (CAS resize).
- **commands**: Replaced direct start commands with `enqueue_download`, `enqueue_many`, `move_in_queue`, and `remove_from_queue`.
- **ipc**: Exported `DownloadStateEvent` and `QueueDirection` to the frontend.
- **tests**: Added 11 integration tests covering idle-parking, idempotent releases, CAS underflow prevention, and gid-completion races.

Frontend Changes:
- **store**: Made `useDownloadStore` reactive to the backend via the `download-state` event.
- **store**: Removed `processQueue` and introduced `pendingOrder` to track the accurate sequence of queued items.
- **ui**: Updated `DownloadItem` with queue visuals (clock icon, position badge).
- **ui**: Added Move Up/Down controls to interact with the backend queue reordering API.
This commit is contained in:
NimBold
2026-06-16 17:46:58 +03:30
parent 1ec4d2aa17
commit bb618aef7d
16 changed files with 1583 additions and 393 deletions
+34 -3
View File
@@ -87,9 +87,6 @@ pub struct DownloadItem {
#[ts(optional)]
pub media_format_selector: Option<String>,
pub queue_id: String,
#[serde(rename = "_dispatched")]
#[ts(optional)]
pub dispatched: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
@@ -234,3 +231,37 @@ pub struct PersistedSettings {
pub extension_pairing_token: String,
pub auto_check_updates: bool,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export, export_to = "../../src/bindings/")]
pub enum QueueDirection {
Up,
Down,
}
#[derive(Clone, Debug, Serialize, TS)]
#[ts(export, export_to = "../../src/bindings/")]
pub struct DownloadStateEvent {
pub id: String,
pub status: String,
pub error: Option<String>,
}
impl DownloadStateEvent {
pub fn new(id: impl Into<String>, status: DownloadStatus) -> Self {
Self {
id: id.into(),
status: status.as_str().to_string(),
error: None,
}
}
pub fn failed(id: impl Into<String>, error: impl Into<String>) -> Self {
Self {
id: id.into(),
status: DownloadStatus::Failed.as_str().to_string(),
error: Some(error.into()),
}
}
}