refactor: migrate monolithic crate to Cargo Workspace with Clean Architecture
Transform the single binary crate into a 9-crate workspace monorepo: - Root Cargo.toml as [workspace] manager with resolver = "2" - zesdex-entities: Domain entity types (session, settings, store, message, etc.) - zesdex-utils: Pure utility functions (error, logger, pagination, slug, clipboard) - zesdex-dto: Data Transfer Objects for LLM provider API communication - zesdex-ipc: Unix-socket IPC layer (client/server/framing/protocol) - zesdex-iam: Identity & Access Management (Clean Architecture: domain/application/infrastructure) - zesdex-cms: Content Management (Clean Architecture: domain/application/infrastructure) - zesdex-middleware: HTTP middleware (Auth, CORS, Rate Limiting) - zesdex-libs: Composition root (AppContext, DB init, JWT, Argon2) - zesdex-backend: Main binary entry point + seed/migrate binaries - DevOps: Dockerfile, docker-compose, Nix (flake/shell/default), CI/CD updates - Remove dead root src/ and src-misc/ directories All crate re-exports maintain backward compatibility with original crate::model::*, crate::dto::*, crate::ipc::* module paths. Feature crates enforce strict layer separation: domain -> application -> infrastructure with generic trait-based dependency injection.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
//! IPC client — connects to the daemon's Unix socket and sends/receives
|
||||
//! framed JSON messages.
|
||||
//!
|
||||
//! [`IpcClient`] wraps a [`Connection`] behind a [`Mutex`] so it can be
|
||||
//! shared across threads (e.g. the TUI event loop and the render task).
|
||||
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
|
||||
use crate::conn::Connection;
|
||||
use anyhow::{Context, Result};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// A thread-safe IPC client connected to a Zesdex daemon over a Unix
|
||||
/// socket.
|
||||
pub struct IpcClient {
|
||||
/// Inner connection protected by a mutex for shared access.
|
||||
conn: Mutex<Connection>,
|
||||
}
|
||||
|
||||
impl IpcClient {
|
||||
/// Connect to the daemon listening at `path` (a Unix socket path).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the socket path does not exist, the connection
|
||||
/// is refused, or the caller lacks permission.
|
||||
pub fn connect_unix(path: &str) -> Result<Self> {
|
||||
let stream = UnixStream::connect(path)
|
||||
.with_context(|| format!("failed to connect to Unix socket at {path:?}"))?;
|
||||
let conn = Connection::new(stream);
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
})
|
||||
}
|
||||
|
||||
/// Serialise `msg` to JSON and send it as a length-prefixed frame.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Delegates to the underlying [`Connection::send`].
|
||||
pub fn send<T: Serialize>(&self, msg: &T) -> Result<()> {
|
||||
let mut guard = self
|
||||
.conn
|
||||
.lock()
|
||||
.expect("IpcClient mutex poisoned — the previous operation panicked");
|
||||
guard.send(msg)
|
||||
}
|
||||
|
||||
/// Read one framed JSON message and deserialise it.
|
||||
///
|
||||
/// Returns `Ok(None)` on clean EOF (daemon closed the connection).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Delegates to the underlying [`Connection::receive`].
|
||||
pub fn receive<T: DeserializeOwned>(&self) -> Result<Option<T>> {
|
||||
let mut guard = self
|
||||
.conn
|
||||
.lock()
|
||||
.expect("IpcClient mutex poisoned — the previous operation panicked");
|
||||
guard.receive()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::os::unix::net::UnixListener;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
struct Ping {
|
||||
seq: u32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_and_round_trip() {
|
||||
let dir = std::env::temp_dir().join(format!("zesdex-ipc-test-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let sock_path = dir.join("test.sock");
|
||||
let sock_path_str = sock_path.to_string_lossy().to_string();
|
||||
|
||||
// Start a minimal echo server in a background thread.
|
||||
let listener = UnixListener::bind(&sock_path).unwrap();
|
||||
let server_handle = std::thread::spawn(move || {
|
||||
let (stream, _) = listener.accept().unwrap();
|
||||
let mut conn = Connection::new(stream);
|
||||
// Echo one message back.
|
||||
let req: Ping = conn.receive().unwrap().unwrap();
|
||||
conn.send(&req).unwrap();
|
||||
});
|
||||
|
||||
// Client connects and sends a ping, then receives the echo.
|
||||
let client = IpcClient::connect_unix(&sock_path_str).unwrap();
|
||||
client.send(&Ping { seq: 7 }).unwrap();
|
||||
let resp: Ping = client.receive().unwrap().expect("expected a response");
|
||||
assert_eq!(resp, Ping { seq: 7 });
|
||||
|
||||
server_handle.join().unwrap();
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user