sifs 0.3.1

SIFS Is Fast Search: instant local code search for agents
Documentation
use crate::daemon::paths::DaemonPaths;
use crate::daemon::protocol::{
    DAEMON_PROTOCOL_VERSION, DaemonError, DaemonRequest, DaemonRequestEnvelope,
    DaemonResponseEnvelope, ResultEnvelope,
};
use anyhow::{Context, Result, bail};
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::time::Duration;

#[derive(Clone, Debug)]
pub struct DaemonClient {
    paths: DaemonPaths,
    timeout: Duration,
}

impl DaemonClient {
    pub fn new(paths: DaemonPaths) -> Self {
        Self {
            paths,
            timeout: Duration::from_secs(30),
        }
    }

    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    pub fn send(&self, request: DaemonRequest) -> Result<crate::daemon::protocol::DaemonResult> {
        let request_id = format!("{}-{}", std::process::id(), now_nanos());
        let envelope = DaemonRequestEnvelope::new(request_id.clone(), request);
        let mut stream = UnixStream::connect(&self.paths.socket)
            .with_context(|| format!("connect SIFS daemon at {}", self.paths.socket.display()))?;
        stream.set_read_timeout(Some(self.timeout)).ok();
        stream.set_write_timeout(Some(self.timeout)).ok();
        serde_json::to_writer(&mut stream, &envelope)?;
        stream.write_all(b"\n")?;
        stream.flush()?;

        let mut line = String::new();
        BufReader::new(stream).read_line(&mut line)?;
        if line.trim().is_empty() {
            bail!("SIFS daemon closed the connection without a response");
        }
        let response: DaemonResponseEnvelope = serde_json::from_str(&line)?;
        if response.protocol_version != DAEMON_PROTOCOL_VERSION {
            bail!(
                "SIFS daemon protocol mismatch: client={}, daemon={}",
                DAEMON_PROTOCOL_VERSION,
                response.protocol_version
            );
        }
        if response.request_id != request_id {
            bail!(
                "SIFS daemon response id mismatch: expected {}, got {}",
                request_id,
                response.request_id
            );
        }
        match response.result {
            ResultEnvelope::Ok { result } => Ok(result),
            ResultEnvelope::Error { error } => Err(error_into_anyhow(error)),
        }
    }
}

fn error_into_anyhow(error: DaemonError) -> anyhow::Error {
    anyhow::anyhow!("{}: {}", error.code, error.message)
}

fn now_nanos() -> u128 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or_default()
}