All checks were successful
CI / ci (push) Successful in 7m16s
Restructure the monolithic 252-line main.rs into a 10-crate workspace with clean hexagonal architecture, swappable adapters, and a production- ready deployment pipeline. Backend architecture: - domain: Canvas, Color/Position/PixelUpdate value objects, port traits (CanvasStore, CanvasPersistence, EventBroadcaster), BroadcastEvent - application: use cases (place_pixel, get_state, save/restore snapshot, connect/disconnect), AppState with Arc snapshot cache - config: AppConfig structs + ConfigSource trait - api-types: shared DTOs, event name constants - adapters: config-env, canvas-file, http-axum (rust-embed), socketio, websocket — all behind port traits, swappable via feature flags - server: composition root with graceful shutdown (SIGTERM/SIGINT) Frontend: - Transport abstraction: Socket.IO and native WebSocket via VITE_TRANSPORT - Canvas zoom/pan with mouse wheel, pinch-to-zoom, and +/- buttons - ImageData rendering (~50x faster than fillRect loop) - Touch support, responsive CSS scaling, mobile-friendly layout - OG/Twitter Card meta tags for rich link previews Production: - Docker: musl static build on scratch — 2.73MB image - CI workflows for Gitea and GitHub Actions (fmt, clippy, test, Docker push) - deploy.sh for private registry - 39 unit tests across domain and application - Zero unwraps, zero unsafe, graceful error handling with tracing - Periodic canvas snapshots with rotation, restored on startup - All config via environment variables with typed defaults
62 lines
1.2 KiB
Rust
62 lines
1.2 KiB
Rust
use thiserror::Error;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum ConfigError {
|
|
#[error("invalid value for '{field}': {reason}")]
|
|
InvalidValue { field: String, reason: String },
|
|
|
|
#[error("failed to load config: {0}")]
|
|
LoadFailed(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct AppConfig {
|
|
pub server: ServerConfig,
|
|
pub canvas: CanvasConfig,
|
|
pub cooldown: CooldownConfig,
|
|
pub rate_limit: RateLimitConfig,
|
|
pub broadcast: BroadcastConfig,
|
|
pub snapshot: SnapshotConfig,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ServerConfig {
|
|
pub address: String,
|
|
pub port: u16,
|
|
pub enable_cors: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CanvasConfig {
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CooldownConfig {
|
|
pub placement_secs: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct RateLimitConfig {
|
|
pub burst_size: u32,
|
|
pub per_second: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct BroadcastConfig {
|
|
pub channel_capacity: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct SnapshotConfig {
|
|
pub enabled: bool,
|
|
pub interval_secs: u64,
|
|
pub max_snapshots: usize,
|
|
pub directory: String,
|
|
}
|
|
|
|
pub trait ConfigSource {
|
|
fn load(&self) -> Result<AppConfig, ConfigError>;
|
|
}
|