Under the hood
Architecture
System overview
┌──────────── Tauri v2 WebView (Next.js static export) ─────────────┐
│ Themeable React/Tailwind SPA ⇄ @tauri-apps/api: invoke/listen │
└───────────────────┬──────────────────────────▲────────────────────┘
│ commands (JSON only) │ events (JSON, ~1 Hz)
▼ │
┌───────────────────┴──────────────────────────┴────────────────────┐
│ Rust core (tokio runtime) │
│ │
│ src-tauri/src/engine/ thin librqbit::Session wrapper. │
│ No Tauri types. Unit testable. │
│ src-tauri/src/commands/ #[tauri::command] handlers. Thin. │
│ src-tauri/src/state/ app state and persistence. │
└───────────────────┬───────────────────────────────────────────────┘
│ pieces written directly
▼
┌───────────┐
│ Disk │
└───────────┘
The rules
These are structural guarantees, not conventions.
1. The webview never touches torrent binary data
librqbit writes pieces straight to disk. The UI receives only small JSON payloads: progress percentages, transfer rates, peer counts, file listings.
This is what keeps memory bounded on a multi-gigabyte ISO. Routing piece data through IPC would mean serialising it, copying it into the WebView heap, and holding it there — which is how torrent clients end up consuming gigabytes of RAM on a large download.
2. The engine layer imports no Tauri types
src-tauri/src/engine/ compiles and runs under plain cargo test. It is the
layer most exposed to librqbit churn, so it must be testable without spawning a
WebView. See src-tauri/tests/engine.rs, which drives a real Session with no
Tauri runtime present.
3. Command handlers are thin
Handlers unwrap shared state, call the engine, and map errors. Anything worth testing lives in the engine, where it can be tested.
4. Telemetry is throttled and batched
Status updates run at approximately 1 Hz. Per-piece events would flood the IPC channel on a fast download and jank the UI for no informational gain.
5. Flume owns its IPC types
The types crossing the boundary are defined in src-tauri/src/engine/status.rs
— they are not re-exports of librqbit’s internal stats structs. A librqbit
upgrade therefore cannot silently change the contract the frontend depends on;
the compiler forces a look at the mapping instead.
The IPC contract
Rust serde structs use #[serde(rename_all = "camelCase")]. Their TypeScript
mirrors live in src/lib/ipc/types.ts and must change in the same commit.
get_core_status
Returns CoreStatus.
| Field | Type | Meaning |
|---|---|---|
clientVersion |
string |
Client string, e.g. "Flume 0.1.0" |
listenPort |
number | null |
Bound peer port, null if not listening |
announcePort |
number | null |
Port announced to trackers |
dht |
DhtStatus |
DHT subsystem health |
downloadDir |
string |
Absolute download path |
uptimeSeconds |
number |
Seconds since session start |
downloadBps |
number |
Aggregate download rate, bytes/sec |
uploadBps |
number |
Aggregate upload rate, bytes/sec |
livePeers |
number |
Connected peers across all torrents |
health |
EngineHealth |
Derived readiness indicator |
DhtStatus: { enabled, nodesV4, nodesV6, outstandingRequests }.
EngineHealth: "starting" | "connecting" | "ready" | "degraded".
starting— the peer listener has not bound a port yetconnecting— listening, DHT enabled, routing table below 8 nodesready— listening with a usable DHT routing tabledegraded— listening, but DHT is disabled, so magnet links cannot resolve
Errors
Commands reject with CommandError:
{
kind: string;
message: string;
}
kind is a stable machine-readable identifier so the frontend can branch
without matching on message text. Currently: engineNotReady.
Startup sequence
- Tauri builds the app and registers
AppStateand the command handlers. - The window opens immediately — first paint is never blocked.
- A background task derives
EngineConfigfrom OS conventions and starts the session (DHT bootstrap, listener bind, persistence restore). - Until the engine is ready,
get_core_statusreturnsengineNotReadyand the UI shows astartingstate. - On
RunEvent::Exit, session shutdown is awaited so fast-resume state is flushed and a restart resumes rather than re-hashing.
Where state lives
| What | Where |
|---|---|
| Downloads | User’s Downloads folder by default (configurable) |
| Session state, fast-resume | OS app-data dir, e.g. ~/Library/Application Support/io.github.adamgreenwell.Flume |
| DHT routing table | dht.json inside the session directory |
The DHT path is set explicitly. librqbit’s default is a global OS path shared across instances, which both leaks state and causes port collisions between two running copies — see issue #19.