#pragma once // Read/write access to the `tasks` table, plus the projection onto the wire TaskSummary. // download.list does its filtering, sorting and paging here (M1 DoD: a 1000-row list under // 50 ms, never materialised client-side). #include #include #include #include #include "store/sqlite.hpp" #include "velox_proto.hpp" namespace velox::daemon::store { // One row of `tasks`, 1:1 with the schema. std::optional maps a NULL column. struct TaskRow { std::string task_id; std::string url; std::string save_dir; std::string filename; std::string state = "new"; std::string start_mode = "now"; // contract StartMode: 'now'|'later'|'queue' std::string created_at; std::optional effective_url; std::optional category_id; std::optional queue_id; std::optional description; std::optional pause_reason; std::optional etag; std::optional last_modified; std::optional content_type; std::optional last_try_at; std::optional completed_at; std::optional checksum_algo; std::optional checksum_value; std::optional size_bytes; std::int64_t downloaded_bytes = 0; std::int64_t speed_bps = 0; bool resumable = false; std::optional req_segments; std::int64_t eff_segments = 0; std::optional req_buffer_bytes; std::optional eff_buffer_bytes; std::optional queue_position; std::optional error_code; std::optional error_message; std::optional error_http_status; std::optional error_retryable; std::optional error_attempt; std::optional error_next_retry_at; }; class Tasks { public: explicit Tasks(Db& db) : db_(db) {} DbResult insert(const TaskRow& row); DbResult> get(std::string_view task_id); struct Page { std::int64_t total = 0; // rows matching the filter, ignoring paging std::vector rows; }; DbResult list(const std::optional& filter, const std::optional& sort, std::int64_t offset, std::int64_t limit); // Move a task to `state`; `pause_reason` is written only when state == "paused" // (cleared otherwise). Returns false if there is no such task. DbResult set_state(std::string_view task_id, std::string_view state, const std::optional& pause_reason); DbResult remove(std::string_view task_id); DbResult count(); // capture.offer's dedupe check: true if a non-terminal task already targets this exact // URL (the same rule download.add itself does not enforce — a deliberate re-add is // allowed there; capture is the automatic path where re-grabbing an in-flight download // is almost always a mistake, e.g. two tabs triggering the same link). DbResult has_active_duplicate(std::string_view url); // download.update's patch, already resolved by the caller (new save_dir/filename // canonicalized and root-checked, any file already moved on disk — this only writes // the row). Every field is applied when present; queue_position is written alongside // queue_id (nullopt leaves the existing position alone — the caller decides what // "moved into a queue" should set it to). Note: the generated parser collapses "field // absent" and "field explicitly null" to the same nullopt (DownloadUpdateParamsPatch // has no way to tell them apart on the wire as generated), so this — like the RPC // layer above it — can only ever set category_id/queue_id/description/checksum, never // clear them back to NULL through this call. struct UpdatePatch { std::optional save_dir; std::optional filename; std::optional category_id; std::optional queue_id; std::optional queue_position; std::optional description; std::optional req_segments; std::optional req_buffer_bytes; std::optional checksum_algo; std::optional checksum_value; }; DbResult apply_update(std::string_view task_id, const UpdatePatch& patch); // download.refreshUrl: point the task at a freshly-issued URL. Separate from // apply_update/set_probe_result since neither owns the base `url` column — refreshUrl // is the one caller that changes it after creation. DbResult set_url(std::string_view task_id, std::string_view url); // Byte-counter update from an engine progress tick — cheaper than a full row rewrite, // and keeps download.list / download.get current between state transitions. DbResult update_progress(std::string_view task_id, std::int64_t downloaded_bytes, std::int64_t speed_bps, std::int64_t eff_segments, std::int64_t eff_buffer_bytes); // What the probe learned, persisted before start() so a task that completes before any // progress tick still reports a real sizeBytes / resumable (not the pre-probe default). struct ProbeFields { std::optional size_bytes; bool resumable = false; std::optional etag; std::optional last_modified; std::optional content_type; std::optional effective_url; }; DbResult set_probe_result(std::string_view task_id, const ProbeFields& fields); // on_finished's byte count, for a task that completes before any progress tick ever // ran (see AGENT-DAEMON review: the bug this closes). size_bytes is only filled in if // still unset — the probe's total_size is the more authoritative source when both // exist and happen to disagree (a chunked source with no declared length, say). DbResult set_final_bytes(std::string_view task_id, std::int64_t bytes); private: Db& db_; }; // Project a row onto the wire type. `state` and `error.code` strings are assumed valid // (the CHECK constraints and the state machine keep them so). velox::proto::TaskSummary to_summary(const TaskRow& row); } // namespace velox::daemon::store