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:
asepharyana
2026-07-17 09:08:41 +07:00
parent 86cc412395
commit be0a9582bb
248 changed files with 7901 additions and 1505 deletions
+140
View File
@@ -0,0 +1,140 @@
//! Connection wrapper around a Unix socket stream.
//!
//! [`Connection`] pairs a buffered reader with a raw writer and exposes
//! `send` / `receive` for framed JSON messages.
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
use crate::frame::{read_frame, write_frame};
use anyhow::{Context, Result};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::io::BufReader;
use std::os::unix::net::UnixStream;
/// A framed JSON connection over a Unix socket.
///
/// Wraps the raw [`UnixStream`] with a [`BufReader`] on the read side and
/// direct writes (with explicit flushing) on the write side.
pub struct Connection {
/// Buffered reader for receiving frames.
reader: BufReader<UnixStream>,
/// Unbuffered writer (flushed after every frame).
writer: UnixStream,
}
impl Connection {
/// Create a new `Connection` from an already-connected [`UnixStream`].
pub fn new(stream: UnixStream) -> Self {
// Clone the stream so that reader and writer can reference separate
// file-descriptor handles. `UnixStream::try_clone` is infallible on
// Unix (it calls `dup(2)`).
let reader = BufReader::new(
stream
.try_clone()
.expect("UnixStream::try_clone should never fail on Linux"),
);
let writer = stream;
Self { reader, writer }
}
/// Serialise `msg` to JSON and send it as a length-prefixed frame.
///
/// # Errors
///
/// Delegates to [`serde_json::to_vec`] for serialisation and
/// [`write_frame`] for writing.
pub fn send<T: Serialize>(&mut self, msg: &T) -> Result<()> {
let json =
serde_json::to_vec(msg).context("failed to serialise message to JSON")?;
write_frame(&mut self.writer, &json)
.context("failed to write frame to connection")
}
/// Read one framed JSON message and deserialise it.
///
/// Returns `Ok(None)` when the remote end has closed the connection
/// cleanly (EOF). Returns `Ok(Some(msg))` on a successful read.
///
/// # Errors
///
/// Delegates to [`read_frame`] for reading and
/// [`serde_json::from_slice`] for deserialisation.
pub fn receive<T: DeserializeOwned>(&mut self) -> Result<Option<T>> {
let raw = read_frame(&mut self.reader)
.context("failed to read frame from connection")?;
match raw {
None => Ok(None),
Some(bytes) => {
let msg: T = serde_json::from_slice(&bytes)
.with_context(|| {
format!(
"failed to deserialise frame ({} byte(s))",
bytes.len()
)
})?;
Ok(Some(msg))
}
}
}
}
// Safety: `UnixStream` is `Send` but not `Sync`. Wrapping `Connection` in
// a `Mutex` (as done in `IpcClient`) provides the `Sync` guarantee.
// The type itself is `Send` because both fields are `Send`.
//
// We explicitly assert Send here for clarity:
fn _assert_send()
where
Connection: Send,
{
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct Ping {
seq: u32,
}
/// Helper: create a pair of connected `Connection` values via a
/// Unix socket pair.
fn pair() -> (Connection, Connection) {
let (a, b) = UnixStream::pair().expect("UnixStream::pair failed");
(Connection::new(a), Connection::new(b))
}
#[test]
fn round_trip() {
let (mut left, mut right) = pair();
left.send(&Ping { seq: 42 }).unwrap();
let received: Ping = right.receive().unwrap().expect("expected a frame");
assert_eq!(received, Ping { seq: 42 });
}
#[test]
fn eof_detection() {
let (left, right) = pair();
drop(right); // close remote end
// Send something first so we can read past it... actually let's
// just drop the peer and check that receive returns None.
// Since we dropped right, left's reads should eventually get EOF.
// But with a socket pair, dropping one end signals EOF on the other.
drop(left); // drop left too — we'll test EOF on a fresh pair
let (mut a, _b) = pair();
drop(_b);
let result: Option<Ping> = a.receive().unwrap();
assert!(result.is_none());
}
}