Apollo Download ManagerApollo Download Manager
On this page

The UI contract

The UI (src/) and the native side (src-tauri/, crates/) talk only through typed Tauri commands and events. This page describes the shared types, how they're kept in step, and how to change them (#25).

The shared types

They're defined once in Rust, in crates/velocity-core/src/domain.rs (plus AppInfo in lib.rs), and mirrored by strict Zod schemas in src/contract.ts.

TypeWhat it isNotes
DownloadStatusWhere a download is: queued, connecting, downloading, paused, verifying, retrying, stalled, completed, failed, removed, fetchingMetadata, choosingFiles, seeding, unknown, waitingForNetworkV1's fourteen values, and waitingForNetwork (ADR-0019). A new status needs an ADR, so the UI stays consistent. completed, failed and removed are terminal
DownloadKindhttp, ftp, sftp, metalink, torrent, magnet—
DownloadRecordOne download as the UI sees it: a stable app id, kind, source URL (credentials removed), file name, folder, category, status, progress, error, verification, timestamps (milliseconds since the Unix epoch), retryAtMs: while retrying, when the engine tries again, startedOver: why it last dropped the bytes it had and started again, null while it never did (since contract version 23, #257; the engine's, not saved), startedAtMs: the first time the engine reported it running, averageSpeedBps: the bytes per second it received while it ran, null until it ran for a second, speedLimitBps: its own speed limit (null for none), requestedConnections: the connections a direct download asks for (null for a torrent), and torrent: a TorrentActivity, and fileCount: how many files a torrent has, padding left out, once its metadata is known (null for a direct download, and before; since contract version 27, #313), and waitsUntil: why a queued download waits and until when, schedule or chosenTime with a local YYYY-MM-DDTHH:MM (#350)Never carries an engine's own id. Since contract version 12 (#59): retryAtMs comes only from the engine, is null in every other status, and isn't saved; startedAtMs and averageSpeedBps are the manager's, and survive a restart (DATA_AND_MIGRATIONS.md). Time waiting, paused, or before the engine's first report doesn't count, nor do bytes a download resumed with. Since contract version 13 (#61): speedLimitBps and requestedConnections are the download's stored options; torrent is null for a direct download, and for a torrent until the helper reports after a start
TorrentActivityA torrent's peers and seeds connected now, its uploadedBytes and seedingSecs over its life, and seedQueued: completed and waiting for a seeding slot, as the torrent helper reports themSince contract version 13 (#61); seedQueued since version 14 (#63). The fixture downloads-snapshot.json has a seeding torrent
TorrentDetails, TorrentPreview, TorrentContents, TrackerStatus, PeerStatusA torrent's seeding policy, uploadLimitBps, contents (null before its metadata) and the files chosen (null for every file); a .torrent file's contents and the duplicate it would repeat; its name, info hashes, version (v1, v2, hybrid), whether it is private, its size and its files (index, /-separated path, length, padding left out); a tracker, its passkey redacted; a peerSince contract version 14 (#63). Asked for while shown, never in the snapshot (PO decision on #63). The fixture torrent.json has one of each
VerificationWhat was proven about a completed download's bytes: verified (a checksum or trusted piece hashes matched), sizeVerified (complete, with no hash checked) or notVerified (resumed without a strong validator, ADR-0010). The fixture verifications.json lists themnull until the download completes. Show "Verified" only for verified. Added in contract version 3 (#46)
ProgressCompleted and total bytes (totalBytes is null while unknown), download and upload speeds in bytes per second, the connections in use now, and connectionDetail—
ConnectionDetailA direct download's connections: requested, allowed (at least 1, never above requested) and limitedBy, what holds allowed below requested: perHostCap, sharedBudget, serverThrottling, rampingUp (adaptive connections are still finding how many help, by trying fewer; #221), noGain (more connections stopped making it faster) fileSize, noRanges (the server sends the file in one piece, without byte ranges, so one connection is all that helps; since contract version 24, #274), or speedLimit (the download's speed limit is reached with fewer connections; since contract version 26, #278), or null (#47). The fixture connection-limits.json lists themnull for torrents and while no engine reports it. Added in contract version 2
TransferError{ class, httpStatus, message, retryable, hostKey, confirm, noNetwork, detail }. class is transient, throttled or permanent. hostKey is { host, port, fingerprint, pinned } when an SFTP server's host key isn't trusted, else nullEngine-neutral. A permanent failure is never retryable. With hostKey, the UI shows the SHA-256 fingerprint for the user to confirm; with pinned set, the key changed and is always refused (SECURITY.md). hostKey since contract version 6 (#53). confirm is { kind: "insecureRedirect", host } or { kind: "localAddress", host, address } when the download waits for the user to allow a redirect from https to http, or a link from the browser that reaches a local address (SECURITY.md), else null; since contract version 18 (#67). noNetwork: no connection could be made, so the download can wait for the network; since version 28 (#318). detail: the engine's technical text behind the plain message, such as libcurl error 60: …, shown only behind "Technical details" and kept in the logs, else null; since version 30 (#252)
CapabilitiesWhat an engine really supports: the protocols, each as a ProtocolCapabilities (kind, segmented, resume, validatorResume, http2, and encryption: whether the connection can be encrypted with its certificate checked, https:// or FTPS; a protocol not listed isn't supported), live connection changes, Metalink, and the most connections per host now (the performance profile's cap)Reported, never assumed (research report section 4.2). Per protocol since contract version 4 (#57), with encryption since version 5 (#51). cleartextWarning in src/capabilities.ts holds the warning for a plain ftp:// download (PO decision on #51); SETTINGS.md says how the UI uses it libcurl ({ version, tls }, or null for an engine without one) is in it since contract version 16 (#28)
ErrorCodeStable codes for command failures, in SCREAMING_SNAKE_CASE (23 codes: V1's, without the four aria2 RPC_* codes, plus NAME_TAKEN, since contract version 9, for a category name another category has). Each has a user-facing sentence (user_message)The code is the contract; the message is for people
AppInfo{ name, version, commandApiVersion }Returned by app_info
DownloadsSnapshotWhat the UI is sent about the downloads (#58): downloads (records, the unfinished ones in queue order, then the finished ones, newest first), unavailable (stored downloads this build can't act on: id, sourceUrl, fileName, directory, createdAtMs), needsLogin (ids of downloads that came back after a restart without their login), engine, torrentEngine (the torrent helper's health, since version 8) and storageError (the last failure to save, or null), and schedule: the schedule's window open now or next, { opens, closes, open } in local time, null while it is off (#350)Sent as the downloads-snapshot event and returned by get_snapshot
EngineHealth{ state: "running" }, { state: "restarting", attempt } or { state: "unavailable", code }The fixture engine-health.json lists one of each
CategoryA download category (#31): id, name, color (#rrggbb), icon, extensions (lowercase, without the dot), directory (its own folder, or null) and builtin. The fixture category-icons.json lists the iconsSince contract version 9. Created and edited as a CategoryInput (the same fields without id and builtin). The rules are in FILE_NAMING.md
DestinationCheckWhat adding a download would do (#31): directory, directoryExists, automaticDirectory, categoryId, fileName (null when the server will name it), path (the folder and the name joined in the platform's separator, for the UI to show; null with fileName; since contract version 25, #277), fileExists, partialDownloadExists (Apollo DM's or V1's) and suggestedName (the first free name when it is taken)Since contract version 9. Free disk space isn't reported yet
OnConflictWhat adding does when the name is taken (#31): ask or rename. The fixture on-conflict.json lists themNever replace (PO decision on #31). Used by add_downloads
AddRequestWhat the Add dialog asks for (#60): links (direct and magnet links, and links to .torrent files), torrentFiles (absolute paths), metalinkFiles (absolute paths of .metalink or .meta4 files; since contract version 22, #222), directory, fileName (one direct link only), categoryId (each null for automatic), createDirectory, onConflict, allowDuplicate ("Add anyway"), startPaused, connections (null for the performance profile's), speedLimitKib (0 for none), headers, referrer, credentials ({ userName, password }), sshKey ({ path, passphrase }, for sftp:// links), checksum ({ algorithm, hex }), requireEncryption (for ftp:// links), chooseFiles (torrents wait in Choosing files) and fromBrowser (the browser extension sent the links: a local address needs the user's confirmation; since contract version 18, #67)Since contract version 11. The fixture add-request.json sets every field. The login, headers and passphrase stay in memory
HistoryEntry, HistoryPage, HistoryQuery, StorageSettingsA finished download as the history shows it: id, kind (null for one a later Apollo DM stored), file name, link, folder, category, status, size, when it was added and finished, why it failed, and whether it is still in the list (inList); a page of them with the total; what is asked for; and the retentionSince contract version 15 (#32). The fixture history.json has one of each
DownloadLoginWhat set_download_login gives a download again (#59): credentials ({ userName, password } or null), sshKey ({ path, passphrase } or null) and headers. The fixture download-login.json sets every partSince contract version 12. Kept in memory only, never saved, and redacted in diagnostics
AddResult{ source, outcome } for each link, .torrent file and Metalink file, in the request's order. outcome.kind is added (id), metalinkAdded (ids: every download a Metalink file added, in its order; since contract version 22, #222), conflict (check, a DestinationCheck), duplicate (duplicate: existingId, name, status, inList, canAddAnyway), failed (code) or notFetched (error, a TransferError: a .torrent link couldn't be fetched)Since contract version 11. The fixture add-results.json has one of each
ShellSettingsWhat the window starts with (#29): appearance (theme, density, layout, and the download list's visibleColumns and columnWidths), onboardingCompleted, and defaultDirectory, the folder downloads go to now (null when the system names none). The fixtures themes.json and densities.json list the themes and densities, and columns.json the column ids, the default columns and the width boundsSince contract version 10; the columns since version 12 (#59). The settings are in SETTINGS.md. set_appearance takes and returns an AppearanceSettings
CategoryMoveWhat changing a download's category did with its file (#366): { kind: "moved", path }, { kind: "unchanged" } (already there, or no file yet), { kind: "moving" } (running: it pauses, moves and carries on) or { kind: "stayed", path, code }. The category changes in every caseSince contract version 31. The fixture category-move.json lists one of each
BatchResultWhat a command over several downloads did: done (ids), failed ({ id, code }), and kept ({ path, reason, code }, files a removal with files left in place). reason is notPlain, tooLarge, behindLink, notEmpty or failed; code is set only for failedThe fixture kept-reasons.json lists the reasons

Conventions:

  • Field names are camelCase on the wire (#[serde(rename_all = "camelCase")]).

  • Optional values are null, never omitted.

  • No aria2 terms: no aria2 shapes or option names appear in any of these types.

The contract version

COMMAND_API_VERSION exists in both velocity_core and src/contract.ts, and app_info reports the native side's version. At startup the UI validates the response. If the versions differ, the window refuses to start and explains that the two parts don't match, rather than misreading messages. Raise the version, on both sides in the same PR, whenever a command or shared type changes incompatibly.

The downloads commands

The commands of src-tauri/src/downloads.rs (#58), V1's direct-download commands. A command that fails returns its ErrorCode. Commands that take ids accept at most 10,000 ids, each at most 64 characters, and return a BatchResult.

CommandArgumentsReturns
get_snapshot—DownloadsSnapshot
pause_downloads, resume_downloads, cancel_downloads, retry_downloadsidsBatchResult
remove_downloadsids, deleteFilesBatchResult, with the files kept
retry_failed, pause_all, resume_all—nothing
clear_completed—how many were cleared
move_downloadid, direction (top, up, down, bottom)nothing
set_start_atid, startAt: local YYYY-MM-DDTHH:MM, or null to take it away (#350)nothing; INVALID_STATE for a download running, finished or waiting for the network
set_download_loginid, login (a DownloadLogin)nothing; INVALID_STATE for a download that doesn't need its login, and the Add path's codes for an invalid one. The window then resumes or retries it (#59)
set_download_connectionsid, connectionsthe connections that apply
set_download_speed_limitid, kib (KiB/s; 0 for no limit, otherwise at least 1)nothing
set_download_categoryid, categoryId (null for none)a CategoryMove: the download's file moves to that category's folder (FILE_NAMING.md). Since contract version 31 (#366); nothing moved before
open_download_fileid, allowProgramnothing; opens the finished file, or a torrent's folder, with the system's program. INVALID_STATE before it finished, NOT_FOUND when the file is gone, INVALID_PATH for a link, CONFIRMATION_REQUIRED for a program until allowProgram, and IO when the program can't start (#61)
open_feedback_pagenonenothing; opens the repository's "new issue" chooser in the default browser, a fixed address the window can't change. Sends nothing itself (#363)
open_download_folderidnothing; shows the file selected in its folder (on Linux, the folder), or the folder when the file is gone. DESTINATION_MISSING when the folder is gone (#61)
restart_engine—EngineHealth
trust_host_keyhost, port, fingerprintnothing; the UI then retries the download
allow_downloadid, kind: insecureRedirect or localAddressnothing. Allows what the download's error.confirm asked for, for that download only, stores it, and retries the download. INVALID_STATE unless it failed waiting for that confirmation. Since contract version 18 (#67)
remember_loginidnothing. Saves the download's login, custom headers and SSH key in the system keychain, at the user's request; INVALID_STATE for one without any, KEYCHAIN_UNAVAILABLE when there is no keychain or it refused. Since contract version 20 (#70)
choose_torrent_filesid, files (indexes, at least one), deleteDroppedthe files kept, each a KeptFile. With deleteDropped, the torrent's own files no longer chosen are deleted, by the rules torrents use; otherwise they stay (#63)
get_torrent_detailsidTorrentDetails; UNSUPPORTED for a direct download (#63)
preview_torrent_filepath (absolute)TorrentPreview, read as adding reads the file: never through a link, at most the size limit (#63)
get_torrent_trackers, get_torrent_peersidthe helper's TrackerStatus or PeerStatus list; INVALID_STATE while the helper doesn't have the torrent (#63)
reannounce_torrentidnothing; INVALID_STATE while the helper doesn't have the torrent (#63)
set_torrent_upload_limitid, kib (KiB/s; 0 for no limit)nothing (#63)
set_torrent_seedingid, seeding (a seeding policy; { "mode": "never" } stops seeding)nothing
restart_torrent_helper—EngineHealth

The history's commands (#32), in the same file:

CommandArgumentsReturns
get_historyquery, a HistoryQuery: search (at most 1024 bytes; a name, a link, or a torrent's info hash), status (completed, failed, cancelled), limit (at most 1000; 200 when null), offsetHistoryPage: entries, newest first, and the total that match
clear_history—how many entries left the history
delete_history_entriesids (at most 10,000)nothing; an id that isn't a finished download is left alone
get_storage_settings, set_storage_settings—, or settingsStorageSettings (SETTINGS.md); INVALID_INPUT outside its bounds

An entry still in the list only leaves the history; one out of it is forgotten. Neither touches a file.

Adding downloads is #60's. A download in the snapshot's needsLogin came back after a restart without its login, custom headers or SSH key, which are never saved; set_download_login gives them again, checked as when it was added, and refused whole if one isn't valid (PO decision on #59).

The category commands

The commands of src-tauri/src/categories.rs (#31), V1's category commands and check_destination. A command that fails returns its ErrorCode. The rules are in FILE_NAMING.md.

CommandArgumentsReturns
list_categories—Category[], in the order the automatic category is chosen in
create_categoryinput (a CategoryInput)the Category; INVALID_INPUT, INVALID_PATH or NAME_TAKEN
update_categoryid, inputthe Category; also NOT_FOUND
delete_categoryidnothing; NOT_FOUND, or INVALID_STATE for a built-in one. Its downloads are left without one
set_download_categoryid, categoryId (null for none)a CategoryMove (#366)
check_destinationurl, directory, fileName, categoryId (each null for automatic)DestinationCheck

Diagnostics

The commands of src-tauri/src/diagnostics.rs (#28). What they show and how secrets are kept out is in DIAGNOSTICS.md.

CommandArgumentsReturns
get_diagnostics—DiagnosticReport: generatedAtMs, app, os, arch, transferEngine, capabilities, torrentEngine, libtorrent, downloads (counts by status), storageError, appLog (LogLines: atMs, level, target, message), transferEngineLog and torrentEngineLog, every line redacted. The fixture diagnostics.json has one. Since contract version 13
export_diagnostics—where the report was saved, after the system's chooser, or null when the user cancels; IO when it can't be written
restart_engine, restart_torrent_helper—EngineHealth after asking (the downloads commands)

The window's commands

The commands of src-tauri/src/shell.rs (#29). A command that fails returns its ErrorCode. How the window uses them is in UI_ARCHITECTURE.md.

CommandArgumentsReturns
get_shell_settings—ShellSettings; DATABASE when a section can't be read
set_appearanceappearancethe AppearanceSettings saved; INVALID_INPUT for a panel or column width outside its bounds, or a column list without name or with a column twice
complete_onboardingdirectory: an existing folder, not a linkShellSettings; INVALID_PATH, or DESTINATION_MISSING when it doesn't exist
choose_directoryinitial (null, or a folder to start in)the folder chosen with the system's chooser, or null when the user cancels
quit_app—nothing: the app quits. The window calls it only after the user confirms
set_maximize_button_rectrect (x, y, width, height in CSS pixels)nothing: on Windows the snap layouts' overlay moves there (#359), and elsewhere nothing happens; INVALID_INPUT for numbers no button could have
get_browser_integration—BrowserIntegration: hostPath (the native host beside the app, null when missing), nativeHostName, chromeExtensionIds, firefoxExtensionId, and registrations, one { browser, registered, manifestPath, hostMatches, found, storeUrl } per browser (chrome, chromium, edge, brave, firefox). found is the browser installed here, { program, sandboxed } (sandboxed: a Snap or Flatpak, which may not reach the native host), or null; storeUrl is the extension's store page for that browser, null until it is published (#240). The fixture browser-integration.json. Since contract version 19 (#65); found and storeUrl since version 33 (#241)
register_browser, unregister_browserbrowserBrowserIntegration as it is now. Registering needs the host beside the app (NOT_FOUND); unregistering removes only Apollo DM's registration (BROWSER_EXTENSION.md). Since contract version 19 (#65)
open_extension_pagebrowsernull. Starts that browser on the extension's store page from branding.json, never a link from the window, with an argument list and no shell. NOT_FOUND when the browser isn't installed, UNSUPPORTED while it has no store page, IO when it didn't start (the window then shows the link to copy). Since contract version 33 (#241)
take_browser_requests—The links from the browser waiting for the Add dialog, oldest first, each as in the browser-request event below; afterwards none waits. The app keeps at most 64, dropping the oldest. Since contract version 34 (#415)
get_settings—Settings: every section the Settings screen edits (SETTINGS.md). The fixture settings.json has the defaults. Since contract version 17 (#30); the browser section since version 19 (#65), the proxy section since version 21 (#192)
set_settingssettingsthe Settings saved; INVALID_INPUT, with nothing saved or applied, when a section is out of bounds
reset_settings—the defaults saved, keeping the download folder
forget_learned_servers—nothing; forgets what downloads learned about servers, in the database and in the engine (#325). Since contract version 32
get_proxy_password_set—whether the proxy has a password, typed this session or remembered; the password never comes to the window. Since contract version 21 (#192)
set_proxy_passwordpassword (null clears it)nothing. Gives both engines the proxy with it, and keeps it in the system keychain when proxy.rememberPassword is on; INVALID_INPUT for one a login can't have, KEYCHAIN_UNAVAILABLE when the keychain refused (it is still used). Since contract version 21 (#192)
get_torrent_network—the torrent helper's NetworkStatus: listenPort, listenError and portMapping

The maximize-button event carries idle, hover or pressed: on Windows a native window has the mouse over the maximise button, so it says what the button should show (#359).

The quit-requested event carries { active }: quitting would stop that many downloads, and general.confirmExitWithActive is on. The window asks the user, and calls quit_app only if they confirm.

Adding downloads

The commands of src-tauri/src/adding.rs (#60). A command that fails returns its ErrorCode. The rules are in the module docs of velocity_core::manager::adding and torrent_links.

CommandArgumentsReturns
add_downloadsrequest: an AddRequestan AddResult for each link and file, in order. The whole request is refused with INVALID_INPUT, adding nothing, when it is empty, has more than 500 links and files, names a file for more than one, or has a header, referrer, login, checksum or speed limit outside its rules. .torrent links are fetched first, for up to 60 seconds
get_capabilities—the transfer engine's Capabilities, so the dialog offers only what it can do
choose_torrents—the .torrent files chosen with the system's chooser; empty when the user cancels
choose_metalinks—the Metalink files (.metalink, .meta4) chosen with the system's chooser; empty when the user cancels. Since contract version 22 (#222)
choose_ssh_key—the SSH private key chosen with the system's chooser, starting in ~/.ssh; null when the user cancels

What each link gets:

  • Duplicates (PO decision on #60, V1's rules): a direct link is the same download when its link is, as typed; a torrent when it shares an info hash, from a magnet link or a .torrent file. A torrent still in the list is duplicate with canAddAnyway: false, even with allowDuplicate, because two copies can't run. Anything else in the list or the history is duplicate until allowDuplicate is set.

  • Taken names: conflict with onConflict: ask, or the first free name with rename. A torrent's names come from its metadata, so a taken one is always conflict, with no suggestedName.

  • The chosen folder is created only with createDirectory, one level at a time and never through a link (INVALID_PATH).

  • A torrent's category comes from its largest file, or from a magnet link's display name.

  • .torrent links (an http or https link whose path ends in .torrent, as V1 told them apart) are fetched through the transfer engine into the app's torrent-links folder, at most 16 MiB, then added as the file. The file is kept without its trackers, which stay in memory as every torrent's do. A fetch that fails is notFetched with the server's or network's reason; one larger than 16 MiB, or that isn't a torrent, is failed with INVALID_INPUT.

The browser-request event (#65) says a link the browser sent for the user to check is waiting. It carries nothing: the window takes the waiting links with take_browser_requests, once it listens and again on each event (#415). A link that starts the app arrives before the window listens, and an event then would be lost. Each link is { url, fileName, referrer, fileSize, mime, source } (source is intercept, contextMenu or popup; the fixture browser-request.json). The window opens the Add dialog with it, marked as from the browser while its link is unchanged (AddRequest.fromBrowser). Since contract version 19; the event carries nothing since version 34.

The torrents-dropped event carries { paths, ignored }: the .torrent files dropped on the window (plain files, never through a link, at most 500), and how many other things were dropped. The window opens the Add dialog with the files, or says only .torrent files can be dropped.

The SFTP host key has no command of its own here: an SFTP download stops before logging in with error.hostKey set (#53). The window then asks, and "Trust and continue" calls trust_host_key and then retry_downloads.

How the two sides stay in step

  1. Rust generates contract fixtures in tests/fixtures/contract/ from the real types: statuses.json, error-codes.json, download-record.json, capabilities.json, app-info.json, downloads-snapshot.json, engine-health.json, batch-result.json, kept-reasons.json, category.json, category-icons.json, destination-check.json, on-conflict.json, shell-settings.json, themes.json, densities.json, add-request.json and add-results.json. naming-cases.json holds the names src/naming.ts must give (FILE_NAMING.md). A Rust test fails when a fixture no longer matches the types: "fixture … is out of date; regenerate it with VELOCITY_UPDATE_FIXTURES=1".

  2. Vitest validates the same fixtures with the UI's strict schemas (src/contract.test.ts). An unknown field, a missing field, an unknown enum value or a wrong type fails the test.

  3. So a change on one side alone always fails a test:

    • Change Rust without regenerating, and the Rust contract test fails.

    • Regenerate without updating the schemas, and the Vitest contract test fails.

    • Change only the schemas, and they no longer match the fixtures.

    This was shown in #25 by renaming a field.

Changing the contract

  1. Change the Rust type, test first in domain.rs.

  2. Regenerate the fixtures: VELOCITY_UPDATE_FIXTURES=1 cargo test -p velocity-core contract.

  3. Update src/contract.ts until pnpm test:focused -- frontend passes.

  4. If the change is incompatible, raise COMMAND_API_VERSION on both sides.

  5. Update this page if a type's meaning changed.

Status values and error codes are part of the contract: adding a status needs an ADR, and every new error code needs a user-facing message, which a test checks.