// vdm/net/http_client.hpp — a libcurl-multi wrapper for the download engine. // // One HttpClient owns a small pool of worker threads, each with its own curl_multi // (curl handles are not thread-safe; a handle lives on exactly one worker for its life). // The engine hands it a Request plus callbacks and gets back a Transfer handle it can // pause / resume / cancel from any thread. // // Layering: this is the ONLY core header that pulls in libcurl, and only in its .cpp — // nothing here exposes a curl type. No JSON / SQL / Qt / RPC (CLAUDE.md §3). // // Callbacks run ON THE WORKER THREAD, one transfer at a time for a given Transfer. // They must not block (that stalls every other transfer on that worker) and must not // call back into this Transfer's pause/resume/cancel re-entrantly — post that work // elsewhere. on_data must not allocate on the hot path (AGENT-CORE); the ring buffer it // writes into is preallocated by the caller (stage 4). // // This header compiles standalone. #ifndef VDM_NET_HTTP_CLIENT_HPP #define VDM_NET_HTTP_CLIENT_HPP #include #include #include #include #include #include "vdm/net/http_types.hpp" #include "vdm/util/bytes.hpp" #include "vdm/util/result.hpp" namespace vdm::net { // What on_data tells the client to do with the transfer after this chunk. enum class DataAction { proceed, // keep receiving pause, // stop receiving; resumes on Transfer::resume() abort, // end the transfer now (finishes with Error::canceled) }; struct TransferStats { std::uint64_t bytes_received = 0; long http_status = 0; std::string effective_url; // Timings in milliseconds (from CURLINFO_*_TIME_T), 0 if the phase didn't happen. long namelookup_ms = 0; long connect_ms = 0; long appconnect_ms = 0; // TLS handshake done long starttransfer_ms = 0; // first response byte long total_ms = 0; }; struct TransferCallbacks { // Response headers are in. Called at most once per transfer (a followed redirect's // intermediate headers are not delivered). If the caller only wanted headers (a // probe), return DataAction::abort here. std::function on_head; // A chunk of body bytes. The span is valid only for the duration of the call. std::function on_data; // The transfer ended — success carries stats, failure carries the mapped error // (ErrorInfo.http_status is set for HTTP-status failures). Always called exactly once, // last. std::function)> on_finished; }; class HttpClient; // Lightweight handle to a running transfer. Copyable (shared state). All methods are // safe to call from any thread; they post to the owning worker and return immediately. // Dropping the last handle does NOT cancel — call cancel() for that. class Transfer { public: Transfer() = default; [[nodiscard]] std::uint64_t id() const noexcept; [[nodiscard]] bool valid() const noexcept { return static_cast(state_); } void pause(); // no-op if already paused / finished void resume(); // no-op if not paused / finished void cancel(); // idempotent; on_finished fires with Error::canceled private: friend class HttpClient; struct State; explicit Transfer(std::shared_ptr s) : state_(std::move(s)) {} std::shared_ptr state_; }; class HttpClient { public: struct Options { // Worker threads, each with its own curl_multi. New transfers are assigned // round-robin. 0 => pick from hardware_concurrency (min 1, max 4). unsigned workers = 0; // CURLMOPT_MAX_TOTAL_CONNECTIONS per worker (0 = curl default). long max_connections_per_worker = 0; // Shared DNS + TLS-session cache across this client's workers (curl_share). bool share_dns_and_tls = true; }; HttpClient(); // default Options explicit HttpClient(Options opts); ~HttpClient(); HttpClient(const HttpClient &) = delete; HttpClient &operator=(const HttpClient &) = delete; [[nodiscard]] unsigned worker_count() const noexcept; // Start a transfer. The Request is consumed (moved). Returns an invalid Transfer and // never calls the callbacks only if the client is shutting down; every other failure // (bad URL, DNS, ...) is delivered through on_finished. Transfer start(Request req, TransferCallbacks cbs); private: friend class Transfer; // Transfer posts pause/resume/cancel commands to Impl struct Impl; std::unique_ptr impl_; }; } // namespace vdm::net #endif // VDM_NET_HTTP_CLIENT_HPP