fraiseql_server/
runtime_state.rs1use std::{sync::Arc, time::SystemTime};
7
8use fraiseql_error::RuntimeError;
9#[cfg(feature = "database")]
10use sqlx::PgPool;
11
12use crate::lifecycle::shutdown::ShutdownCoordinator;
13
14pub struct AppState {
16 pub config: Arc<crate::config::RuntimeConfig>,
18
19 #[cfg(feature = "database")]
21 pub db: PgPool,
22
23 #[cfg(feature = "database")]
25 pub replicas: Vec<PgPool>,
26
27 pub cache: Option<Arc<dyn CacheClient>>,
29
30 pub rate_limiter: Option<Arc<dyn RateLimiter>>,
32
33 pub idempotency: Option<Arc<dyn IdempotencyStore>>,
35
36 pub shutdown: Arc<ShutdownCoordinator>,
38}
39
40impl AppState {
41 #[allow(unreachable_code, unused_variables)]
46 pub fn new(config: crate::config::RuntimeConfig, shutdown: Arc<ShutdownCoordinator>) -> Self {
47 Self {
48 config: Arc::new(config),
49 #[cfg(feature = "database")]
50 db: panic!("Use new_with_database when database feature is enabled"),
51 #[cfg(feature = "database")]
52 replicas: Vec::new(),
53 cache: None,
54 rate_limiter: None,
55 idempotency: None,
56 shutdown,
57 }
58 }
59
60 #[cfg(feature = "database")]
62 pub async fn new_with_database(
63 config: crate::config::RuntimeConfig,
64 shutdown: Arc<ShutdownCoordinator>,
65 ) -> Result<Self, RuntimeError> {
66 let db_url =
68 std::env::var(&config.database.url_env).map_err(|_| RuntimeError::Internal {
69 message: format!("Missing environment variable: {}", config.database.url_env),
70 source: None,
71 })?;
72 let db = PgPool::connect(&db_url).await.map_err(|e| RuntimeError::Database(e))?;
73
74 let mut replicas = Vec::new();
76 for replica in &config.database.replicas {
77 let url = std::env::var(&replica.url_env).map_err(|_| RuntimeError::Internal {
78 message: format!("Missing environment variable: {}", replica.url_env),
79 source: None,
80 })?;
81 replicas.push(PgPool::connect(&url).await.map_err(|e| RuntimeError::Database(e))?);
82 }
83
84 Ok(Self {
85 config: Arc::new(config),
86 db,
87 replicas,
88 cache: None,
89 rate_limiter: None,
90 idempotency: None,
91 shutdown,
92 })
93 }
94
95 #[cfg(feature = "database")]
97 pub fn read_connection(&self) -> &PgPool {
98 if self.replicas.is_empty() {
99 &self.db
100 } else {
101 use std::sync::atomic::{AtomicUsize, Ordering};
102 static COUNTER: AtomicUsize = AtomicUsize::new(0);
103 let idx = COUNTER.fetch_add(1, Ordering::Relaxed) % self.replicas.len();
104 &self.replicas[idx]
105 }
106 }
107
108 #[cfg(feature = "database")]
110 pub fn write_connection(&self) -> &PgPool {
111 &self.db
112 }
113}
114
115#[async_trait::async_trait]
117pub trait CacheClient: Send + Sync {
118 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, RuntimeError>;
119 async fn set(
120 &self,
121 key: &str,
122 value: &[u8],
123 ttl: Option<std::time::Duration>,
124 ) -> Result<(), RuntimeError>;
125 async fn delete(&self, key: &str) -> Result<(), RuntimeError>;
126 async fn ping(&self) -> Result<(), RuntimeError>;
127}
128
129#[async_trait::async_trait]
131pub trait RateLimiter: Send + Sync {
132 async fn check(
133 &self,
134 key: &str,
135 limit: u32,
136 window: std::time::Duration,
137 ) -> Result<RateLimitResult, RuntimeError>;
138}
139
140pub struct RateLimitResult {
141 pub allowed: bool,
142 pub remaining: u32,
143 pub reset_at: SystemTime,
144}
145
146#[async_trait::async_trait]
148pub trait IdempotencyStore: Send + Sync {
149 async fn check_and_store(
150 &self,
151 key: &str,
152 ttl: std::time::Duration,
153 ) -> Result<bool, RuntimeError>;
154 async fn get_result(&self, key: &str) -> Result<Option<serde_json::Value>, RuntimeError>;
155 async fn store_result(&self, key: &str, result: &serde_json::Value)
156 -> Result<(), RuntimeError>;
157}