use sqlx::PgPool; use tokio::sync::broadcast; use crate::config::AppConfig; use crate::services::compression::CompressionWorker; use crate::services::config::ConfigCache; use crate::services::disk::DiskCache; use crate::services::rate_limiter::RateLimiter; use crate::services::sse_tickets::SseTicketStore; #[derive(Clone, Debug)] pub struct SseEvent { pub event_type: String, pub data: String, } impl SseEvent { /// Standardised constructor. Prefer this over building the struct inline so the /// event-type strings stay consistent across handlers. pub fn new(event_type: impl Into, data: impl Into) -> Self { Self { event_type: event_type.into(), data: data.into(), } } } #[derive(Clone)] pub struct AppState { pub pool: PgPool, pub config: AppConfig, pub sse_tx: broadcast::Sender, pub compression: CompressionWorker, pub rate_limiter: RateLimiter, pub sse_tickets: SseTicketStore, /// In-memory cache in front of the `config` table. Reads go through here; the /// admin PATCH handler and the test reseed invalidate it after committing. pub config_cache: ConfigCache, /// Cached total/free bytes for the media filesystem (quota + admin stats). pub disk_cache: DiskCache, } impl AppState { pub fn new(pool: PgPool, config: AppConfig) -> Self { // Broadcast buffer for live SSE fan-out. Sized to absorb a burst (e.g. many // uploads landing at once during a busy moment) before a slow consumer lags and // has to `resync`. The resync path is a correctness backstop, not the happy path — // a roomier buffer keeps ~1000 concurrent clients from all resyncing at once. let (sse_tx, _) = broadcast::channel(1024); let compression = CompressionWorker::new( pool.clone(), config.media_path.clone(), config.compression_concurrency, sse_tx.clone(), ); let config_cache = ConfigCache::new(pool.clone()); Self { pool, config, sse_tx, compression, rate_limiter: RateLimiter::new(), sse_tickets: SseTicketStore::new(), config_cache, disk_cache: DiskCache::new(), } } }