Lays out Velox Download Manager (IDM-class download manager for Ubuntu 26.04) as a monorepo ready for parallel lane development. No implementation code by design. - docs/: architecture, roadmap M0-M7, IDM-parity GUI spec, engine design, Firefox extension spec, risks/spikes, packaging - contracts/: wire-contract skeleton (JSON Schema + fixture templates) — the single synchronization point between lanes - docs/agents/: one brief per lane (PROTO, CORE, DAEMON, GUI, EXT, PKG/QA) with owned directories, build order and definition of done - CLAUDE.md: rules of engagement — lane ownership, layering, non-negotiables - CMake scaffolding with dev/tsan/release/ci presets Two environment findings shape the design: Firefox here is the Mozilla snap (native-messaging risk, so the extension carries a loopback-WebSocket fallback), and Wayland forbids passive clipboard monitoring (so clipboard capture is explicit-action-first). Co-Authored-By: Claude Opus 5 <[email protected]>
101 lines
7.0 KiB
Markdown
101 lines
7.0 KiB
Markdown
# 01 — Architecture
|
||
|
||
## 1. Decision summary
|
||
|
||
| Concern | Decision | Why this and not the alternative |
|
||
|---|---|---|
|
||
| Engine language | **C++23** (`libveloxcore`) | You asked for speed and it is the right call here: segment scheduling, a 64 KiB–8 MiB write path, and 32 concurrent sockets are exactly where a GC and a per-chunk allocation tax show up. Rust would be an equally good engine choice, but Qt's C++ API means C++ removes an entire FFI boundary from the GUI lane. |
|
||
| HTTP stack | **libcurl** (multi interface, `curl_multi_poll`) | Writing an HTTP/1.1+2 client on raw epoll costs weeks and buys nothing: curl already does connection reuse, HTTP/2 multiplexing, TLS, proxies, SOCKS5, Digest/NTLM auth, redirect and cookie semantics. One `curl_multi` handle per worker thread, N easy handles = N segments. |
|
||
| GUI toolkit | **Qt 6 Widgets** (not QML, not GTK4) | IDM's UI is a dense data grid, a toolbar of big icons, a tree, and ~12 tabbed dialogs. `QTreeView` + a custom `QAbstractItemModel` renders 100k rows without breaking a sweat; QML would need that grid hand-built. GTK4/libadwaita fights you the moment you want a non-GNOME look, and gtkmm's tree/column story is worse. Qt also gives you `QSystemTrayIcon`, drag-and-drop, a global clipboard API, and `QSS` theming to get the IDM look. LGPLv3 dynamic linking is fine for an open-source app. |
|
||
| Process model | **Daemon + thin clients** | Downloads must survive closing the window, and the extension must work when no window is open. One owner of state also kills every "GUI and extension disagree" bug class. |
|
||
| Wire protocol | **JSON-RPC 2.0** over Unix domain socket (+ loopback WebSocket for the extension fallback) | One protocol for GUI, CLI, and extension = one contract to keep in sync, and it is trivially mockable so three lanes can build in parallel. D-Bus is more idiomatic on Linux but is painful to speak from a WebExtension and adds a second schema. A D-Bus *shim* can be added in M5 for desktop integration only. |
|
||
| Persistence | **SQLite** (WAL) | Task list, segment bitmaps, categories, rules, queues, history. Crash-safe, zero admin, one file. |
|
||
| Extension | **MV3 WebExtension, TypeScript** | Firefox MV3 still allows blocking `webRequest`, which is what makes true "intercept before the browser downloads it" possible — the thing Chrome MV3 took away. |
|
||
| Build | **CMake ≥ 3.28 + Ninja + presets** | Presets mean every agent and CI run the identical configure line. |
|
||
|
||
## 2. Processes
|
||
|
||
### `veloxd` — the daemon
|
||
Owns everything: task list, scheduler, queues, disk, sockets, settings.
|
||
|
||
- Single instance, enforced by an abstract-namespace lock socket.
|
||
- Started by systemd **user** unit `velox.service`, socket-activated by `velox.socket`.
|
||
Also auto-started by the GUI or nmhost if not running (`systemctl --user start velox`).
|
||
- Listens on:
|
||
- `$XDG_RUNTIME_DIR/velox/velox.sock` (mode 0600) — GUI, CLI, nmhost. Peer credentials
|
||
checked via `SO_PEERCRED`; same-UID only. No token needed, the socket permission *is*
|
||
the authorization.
|
||
- `127.0.0.1:<ephemeral, recorded in $XDG_RUNTIME_DIR/velox/ws.port>` — loopback
|
||
WebSocket for the extension fallback transport. **Token-authenticated + pairing
|
||
prompt.** See `docs/05-extension-spec.md` §4.
|
||
- Threads: 1 RPC/event loop, 1 curl-multi transfer thread per ~8 active segments
|
||
(capped), 1 disk writer thread per active task, 1 timer thread for the scheduler.
|
||
Never block the RPC loop on disk or DNS.
|
||
|
||
### `velox-gui` — Qt 6 client
|
||
Stateless view. On start: connect → `session.hello` → `download.list` → subscribe to
|
||
`event.*`. On daemon restart: exponential-backoff reconnect with a banner, never lose the
|
||
window. Everything it displays comes from the daemon; it holds no download state of its own.
|
||
|
||
### `velox` — CLI
|
||
Same RPC, scriptable: `velox add <url> --dir ~/ISOs --segments 16`, `velox ls`,
|
||
`velox pause <id>`. Falls out nearly free once the generated client exists, and it is the
|
||
fastest way to test the daemon before the GUI is ready.
|
||
|
||
### `velox-nmhost` — native-messaging bridge
|
||
Deliberately trivial and boring: reads Firefox's 4-byte-length-prefixed JSON from stdin,
|
||
writes it to the Unix socket, pumps replies back. **No business logic — ever.** Under 300
|
||
lines. It exists only because Firefox's native messaging speaks stdio and the daemon
|
||
speaks sockets. If a feature needs logic, it belongs in the daemon.
|
||
|
||
## 3. The layering rule (enforced in review)
|
||
|
||
```
|
||
libveloxcore → knows nothing about RPC, JSON, SQL, or Qt.
|
||
Input: a DownloadSpec. Output: bytes on disk + callbacks.
|
||
veloxd → knows RPC, SQL, scheduling. Depends on core. No Qt.
|
||
velox-gui → knows Qt and the generated client. Zero engine code.
|
||
extension → knows the browser and the generated TS client. Zero download logic.
|
||
```
|
||
|
||
If the GUI ever needs to know what a "segment steal" is, the layering has been violated.
|
||
The daemon's job is to project the engine's state into the contract's `TaskDetail` type,
|
||
and that is the only shape the GUI ever sees.
|
||
|
||
## 4. How three lanes build at the same time without breaking each other
|
||
|
||
This is the part that makes parallel agents work, so it is a hard process, not a habit:
|
||
|
||
1. **`contracts/` is the single source of truth** and is owned by exactly one lane (PROTO).
|
||
JSON Schema for every type, method, and event, plus an OpenRPC document.
|
||
2. **Codegen, not hand-written types.** `contracts/codegen/gen_cpp.py` emits
|
||
`libveloxproto` (structs + (de)serialization); `gen_ts.py` emits `@velox/protocol`.
|
||
Generated files are committed so nobody is blocked on running the generator. Hand-editing
|
||
a generated file is a merge-blocking offence.
|
||
3. **Golden fixtures.** Every method has request/response JSON examples in
|
||
`contracts/fixtures/`. `tests/conformance/` replays them against *both* the C++ server
|
||
and the TS client. Green fixtures = the two lanes are compatible, without them ever
|
||
having run against each other.
|
||
4. **`tools/mockd`** — a TS mock daemon that serves the fixtures and fakes progress events.
|
||
The GUI and extension lanes develop against it from day one and never wait for `veloxd`.
|
||
5. **Protocol changes are a PR to `contracts/` only**, with a `VERSION` bump and updated
|
||
fixtures. Adding an optional field = minor. Removing/renaming/retyping = major, and
|
||
`session.hello` refuses a mismatched major with a clear error the GUI shows as
|
||
"Update Velox".
|
||
|
||
## 5. Data locations
|
||
|
||
| What | Path |
|
||
|---|---|
|
||
| Config | `~/.config/velox/settings.json` |
|
||
| Database | `~/.local/share/velox/velox.db` |
|
||
| Logs | `~/.local/state/velox/velox.log` (rotated, 5×2 MiB) |
|
||
| Runtime sockets | `$XDG_RUNTIME_DIR/velox/` |
|
||
| Temp/partial data | configurable; default `~/.local/share/velox/temp/` |
|
||
| Default download root | `~/Downloads/` with category subfolders |
|
||
|
||
Partial files: the target file is created **sparse and preallocated at final size** in the
|
||
destination directory as `<name>.veloxpart`, with `<name>.veloxpart.meta` beside it. On
|
||
completion the part file is renamed in place — no copy, no second full-size write, and a
|
||
half-finished download is never mistaken for a real file by other apps.
|