hephaestus/crates/hephd/src/client.rs
Erich Blume ed8c7a733a
Some checks failed
Build / validate (pull_request) Failing after 3s
hephd local mode: file lock + JSON-RPC over unix socket
Slice 6 (tech-spec §3, §6, §10). First async component — the per-device
daemon in local mode.

- `LockGuard`: exclusive advisory flock on a sidecar `<db>.lock`; a second
  acquire fails and releases on drop (the §3.1 lock handoff).
- JSON-RPC (line-delimited): `rpc::dispatch` maps node/task/next/links/log
  methods onto the heph-core Store; `Daemon::serve` accepts unix-socket
  connections and runs dispatch on tokio's blocking pool behind an
  Arc<Mutex<LocalStore>> (DB never touches an async worker).
- Synchronous `Client` for surfaces/CLI; `hephd` binary (clap) opens the
  store under lock and serves the default socket.
- heph-core model/ranking types are now serde-(de)serializable; added
  node.tombstone + Store::tombstone_node.

Tests: 2 lock unit tests + 5 real-socket e2e (round-trip with clock
injection, next, error paths, recurring roll-forward over RPC, 8-client
concurrency). 60 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:28:15 -07:00

61 lines
2 KiB
Rust

//! A minimal **synchronous** JSON-RPC client over the unix socket.
//!
//! Used by the `heph` CLI and by tests. Surfaces never touch SQLite directly
//! (tech-spec §3) — they go through the daemon socket, which this wraps.
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::Path;
use anyhow::{bail, Context, Result};
use serde_json::{json, Value};
use crate::rpc::Response;
/// A connected client. One request/response per [`call`](Client::call).
pub struct Client {
reader: BufReader<UnixStream>,
writer: UnixStream,
next_id: u64,
}
impl Client {
/// Connect to a daemon listening at `socket_path`.
pub fn connect(socket_path: &Path) -> Result<Client> {
let stream = UnixStream::connect(socket_path)
.with_context(|| format!("connecting to hephd at {}", socket_path.display()))?;
let reader = BufReader::new(stream.try_clone()?);
Ok(Client {
reader,
writer: stream,
next_id: 1,
})
}
/// Call `method` with `params`, returning the `result` value (or an error
/// carrying the RPC error's code and message).
pub fn call(&mut self, method: &str, params: Value) -> Result<Value> {
let id = self.next_id;
self.next_id += 1;
let mut line = serde_json::to_string(&json!({
"id": id,
"method": method,
"params": params,
}))?;
line.push('\n');
self.writer.write_all(line.as_bytes())?;
self.writer.flush()?;
let mut response_line = String::new();
let read = self.reader.read_line(&mut response_line)?;
if read == 0 {
bail!("hephd closed the connection");
}
let response: Response = serde_json::from_str(&response_line)?;
if let Some(err) = response.error {
bail!("rpc error {}: {}", err.code, err.message);
}
Ok(response.result.unwrap_or(Value::Null))
}
}