core: add util layer — Result, Error taxonomy, bytes, event bus, pool, log

util/ carries no wire surface, so it lands before the contract freeze.

- error: enum class Error, the engine-wide failure taxonomy; is_retryable
  enumerates every value (no default:) so -Wswitch forces the retry
  decision on each future addition. ErrorInfo carries context/http_status.
- result: Result<T> over std::expected<T, ErrorInfo>, Result<void>,
  VDM_TRY / VDM_TRY_ASSIGN. Errors returned, never thrown, on the
  transfer path.
- bytes: span aliases, LE load_le/store_le (debug-asserted precondition,
  not input validation), and a bounds-checked latching ByteReader for the
  .veloxpart.meta reader.
- event_bus: typed thread-safe pub/sub; header states plainly that
  unsubscribe is not a quiesce point and download_task will need its own
  drain.
- thread_pool: std::jthread pool; dtor joins in the body before members
  die (fixed a use-after-destruction on cv_/mu_). Header notes shutdown is
  drain-only and DAEMON will need a cancel mode.
- log: sink interface (core does no I/O); DAEMON installs one.

Tested: -Werror clean, 6 binaries green under plain / ASan+UBSan / TSan.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
This commit is contained in:
2026-09-09 19:03:11 +04:00
co-authored by Claude Sonnet 5
parent 5ac74ecbd9
commit ddf36e848a
17 changed files with 1401 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
# `libveloxcore` — public API
**Status: M1 in progress.** Only `util/` is landed. The download-facing API
(`DownloadSpec`, `DownloadTask`, probe, typed callbacks) arrives with later stages and
is reviewed by DAEMON before M2 (AGENT-CORE DoD).
Layering (CLAUDE.md §3): this library knows nothing about JSON, SQL, Qt, or RPC. Input is
a spec value; output is bytes on disk plus typed callbacks. DAEMON projects engine state
onto the wire contract's `TaskSummary` / `TaskDetail` / events — see
`core/docs/proto-requests-m1.md` for the shapes that projection needs frozen.
Every header under `core/include/vdm/` compiles standalone (`-Wall -Wextra -Wpedantic
-Werror`, C++23). Clean under ASan/UBSan and TSan.
---
## `util/` — foundations
### `vdm/util/error.hpp`
`enum class Error` — the engine-wide failure taxonomy (network / HTTP / content / local
I/O / metadata / probe / retry / internal). This is CORE's own vocabulary; it is **not**
a wire type. `error_name(Error)` gives a stable snake_case string; `is_retryable(Error)`
is the advisory retry hint the task policy consults.
`struct ErrorInfo { Error code; std::string context; int http_status; bool retryable;
Error cause; }` — the payload carried by every failed `Result`. `.to_string()` renders
`"<name>: <context> (HTTP <n>)"`.
### `vdm/util/result.hpp`
`Result<T>` — return-based error channel, a thin wrapper over
`std::expected<T, ErrorInfo>`. Errors are **returned, never thrown**, on anything that
runs during a transfer.
- `Result<int> r = 42;` / `Result<int> r = Err{Error::timeout, "..."};` /
`Result<T> r = Error::not_found;`
- `r.has_value()`, `explicit operator bool`, `r.value()` / `*r` / `r->`, `r.error()`,
`r.code()`, `r.value_or(x)`
- monadic `and_then` / `transform` / `transform_error` (forward to `std::expected`)
- `Result<void>` specialization; `vdm::ok()` success sentinel
- `VDM_TRY(expr)` — return the error if `expr` failed
- `VDM_TRY_ASSIGN(auto x, expr)` — bind the value or return the error
### `vdm/util/bytes.hpp`
`Byte` / `ByteSpan` / `ConstByteSpan` aliases; `as_bytes(string_view)` /
`as_chars(span)`. Little-endian fixed-width codec `load_le<T>` / `store_le<T>` and a
bounds-checked sequential `ByteReader` (`.u8/.u16/.u32/.u64`, `.raw(n)`, `.lp_string()`,
`.overran()`). Built for the `.veloxpart.meta` reader and the 4-byte NM framing; every
read is bounds-checked and latches on overrun (reader-first, fuzz-ready).
### `vdm/util/event_bus.hpp`
`EventBus` — typed, thread-safe in-process pub/sub. `subscribe<E>(fn) -> Token`,
`publish<E>(ev)` (synchronous, calling thread, registration order), `unsubscribe(Token)`,
and RAII `subscribe_scoped<E>` returning a `Subscription`. Handlers may (un)subscribe or
publish during dispatch. Handlers must not throw. Not a hot-path structure — progress is
coalesced to ≤4 Hz upstream.
### `vdm/util/thread_pool.hpp`
`ThreadPool` — fixed-size `std::jthread` pool for bounded off-loop work (hashing, fsync
batches, DNS pre-resolve). `submit(fn, args...) -> std::future<R>`; propagates exceptions
through the future; drains already-queued tasks on destruction. **Not** the transfer
loop — `net/` will own one `curl_multi` per dedicated worker.
### `vdm/util/log.hpp`
Sink interface — core does no I/O itself. `LogSink` abstract base; DAEMON installs one
via `set_log_sink()`, default discards. `CallbackSink` adapter (with a min-level filter).
`VDM_LOG_{TRACE,DEBUG,INFO,WARN,ERROR}(category, fmt, args...)``std::format` syntax,
only formatted when a sink is installed and wants the level.