Skip to main content

ironflow_api/
state.rs

1//! Application state and dependency injection.
2//!
3//! [`AppState`] holds the shared [`Store`] and [`Engine`] used by all handlers.
4
5use std::sync::Arc;
6#[cfg(feature = "prometheus")]
7use std::sync::OnceLock;
8
9use axum::extract::FromRef;
10#[cfg(feature = "prometheus")]
11use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
12use tokio::sync::broadcast;
13use uuid::Uuid;
14
15use ironflow_artifacts::blob_store::BlobStore;
16use ironflow_auth::jwt::JwtConfig;
17use ironflow_engine::engine::Engine;
18use ironflow_engine::notify::{Event, WorkflowEventBus};
19use ironflow_store::entities::Run;
20use ironflow_store::store::Store;
21
22use crate::error::ApiError;
23
24/// Global application state.
25///
26/// Holds the shared store (runs, users, API keys, secrets) and engine,
27/// extracted by handlers using Axum's state extraction mechanism.
28///
29/// # Examples
30///
31/// ```no_run
32/// use ironflow_api::state::AppState;
33/// use ironflow_auth::jwt::JwtConfig;
34/// use ironflow_store::prelude::*;
35/// use ironflow_store::store::Store;
36/// use ironflow_engine::engine::Engine;
37/// use ironflow_core::providers::claude::ClaudeCodeProvider;
38/// use std::sync::Arc;
39///
40/// # async fn example() {
41/// let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
42/// let provider = Arc::new(ClaudeCodeProvider::new());
43/// let engine = Arc::new(Engine::new(store.clone(), provider));
44/// let jwt_config = Arc::new(JwtConfig {
45///     secret: "secret".to_string(),
46///     access_token_ttl_secs: 900,
47///     refresh_token_ttl_secs: 604800,
48///     cookie_domain: None,
49///     cookie_secure: false,
50/// });
51/// let broadcaster = ironflow_api::sse::SseBroadcaster::new();
52/// let state = AppState::new(store, engine, jwt_config, "token".to_string(), broadcaster.sender());
53/// # }
54/// ```
55#[derive(Clone)]
56pub struct AppState {
57    /// The unified backing store for runs, steps, users, API keys, and secrets.
58    pub store: Arc<dyn Store>,
59    /// The workflow orchestration engine.
60    pub engine: Arc<Engine>,
61    /// JWT configuration for auth tokens.
62    pub jwt_config: Arc<JwtConfig>,
63    /// Static token for worker-to-API authentication.
64    pub worker_token: String,
65    /// Broadcast sender for SSE event streaming.
66    pub event_sender: broadcast::Sender<Event>,
67    /// Per-run event bus for real-time workflow monitoring.
68    ///
69    /// When set, the `GET /api/v1/runs/{id}/events` route subscribes to this
70    /// bus and streams [`WorkflowEvent`](ironflow_engine::notify::WorkflowEvent)s
71    /// via SSE. `None` when the engine was not configured with a bus.
72    pub event_bus: Option<WorkflowEventBus>,
73    /// Where artifact bytes live, when artifacts are enabled.
74    ///
75    /// `None` on a deployment that has not configured artifact storage: the
76    /// artifact routes answer `501` and every other endpoint is unaffected.
77    pub blob_store: Option<Arc<dyn BlobStore>>,
78    /// Prometheus metrics handle (only when `prometheus` feature is enabled).
79    #[cfg(feature = "prometheus")]
80    pub prometheus_handle: PrometheusHandle,
81}
82
83impl FromRef<AppState> for Arc<dyn Store> {
84    fn from_ref(state: &AppState) -> Self {
85        Arc::clone(&state.store)
86    }
87}
88
89impl FromRef<AppState> for Arc<JwtConfig> {
90    fn from_ref(state: &AppState) -> Self {
91        Arc::clone(&state.jwt_config)
92    }
93}
94
95#[cfg(feature = "prometheus")]
96impl FromRef<AppState> for PrometheusHandle {
97    fn from_ref(state: &AppState) -> Self {
98        state.prometheus_handle.clone()
99    }
100}
101
102impl AppState {
103    /// Create a new `AppState`.
104    ///
105    /// When the `prometheus` feature is enabled, a global Prometheus recorder
106    /// is installed (once) and its handle is stored in the state.
107    ///
108    /// # Panics
109    ///
110    /// Panics if a Prometheus recorder cannot be installed (should only
111    /// happen if another incompatible recorder was set elsewhere).
112    pub fn new(
113        store: Arc<dyn Store>,
114        engine: Arc<Engine>,
115        jwt_config: Arc<JwtConfig>,
116        worker_token: String,
117        event_sender: broadcast::Sender<Event>,
118    ) -> Self {
119        Self {
120            store,
121            engine,
122            jwt_config,
123            worker_token,
124            event_sender,
125            event_bus: None,
126            blob_store: None,
127            #[cfg(feature = "prometheus")]
128            prometheus_handle: Self::global_prometheus_handle(),
129        }
130    }
131
132    /// Enable artifacts by attaching the backend that holds their bytes.
133    ///
134    /// # Examples
135    ///
136    /// ```no_run
137    /// use std::sync::Arc;
138    ///
139    /// use ironflow_api::state::AppState;
140    /// use ironflow_artifacts::blob_store::BlobStore;
141    /// use ironflow_artifacts::local::LocalBlobStore;
142    ///
143    /// # fn example(state: AppState) -> AppState {
144    /// let blob: Arc<dyn BlobStore> = Arc::new(LocalBlobStore::new("/var/lib/ironflow/artifacts"));
145    /// state.with_blob_store(blob)
146    /// # }
147    /// ```
148    pub fn with_blob_store(mut self, blob_store: Arc<dyn BlobStore>) -> Self {
149        self.blob_store = Some(blob_store);
150        self
151    }
152
153    /// Attach a [`WorkflowEventBus`] for per-run SSE streaming.
154    ///
155    /// When set, `GET /api/v1/runs/{id}/events` streams step-level events
156    /// for a specific workflow run. When absent the route returns an empty
157    /// SSE stream (with keep-alive).
158    ///
159    /// # Examples
160    ///
161    /// ```no_run
162    /// use ironflow_api::state::AppState;
163    /// use ironflow_engine::notify::WorkflowEventBus;
164    ///
165    /// # fn example(state: AppState) -> AppState {
166    /// state.with_event_bus(WorkflowEventBus::new())
167    /// # }
168    /// ```
169    pub fn with_event_bus(mut self, bus: WorkflowEventBus) -> Self {
170        self.event_bus = Some(bus);
171        self
172    }
173
174    /// The artifact backend, or a `501` error when artifacts are disabled.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`ApiError::ArtifactStorageUnavailable`] when no backend is attached.
179    pub fn blob_store_or_501(&self) -> Result<&Arc<dyn BlobStore>, ApiError> {
180        self.blob_store
181            .as_ref()
182            .ok_or(ApiError::ArtifactStorageUnavailable)
183    }
184
185    /// Install (or reuse) a global Prometheus recorder and return its handle.
186    #[cfg(feature = "prometheus")]
187    fn global_prometheus_handle() -> PrometheusHandle {
188        static HANDLE: OnceLock<PrometheusHandle> = OnceLock::new();
189        HANDLE
190            .get_or_init(|| {
191                PrometheusBuilder::new()
192                    .install_recorder()
193                    .expect("failed to install Prometheus recorder")
194            })
195            .clone()
196    }
197
198    /// Fetch a run by ID or return 404.
199    ///
200    /// # Errors
201    ///
202    /// Returns `ApiError::RunNotFound` if the run does not exist.
203    /// Returns `ApiError::Store` if there is a store error.
204    pub async fn get_run_or_404(&self, id: Uuid) -> Result<Run, ApiError> {
205        self.store
206            .get_run(id)
207            .await
208            .map_err(ApiError::from)?
209            .ok_or(ApiError::RunNotFound(id))
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use ironflow_core::providers::claude::ClaudeCodeProvider;
217    use ironflow_store::memory::InMemoryStore;
218    use ironflow_store::store::Store;
219
220    fn test_state() -> AppState {
221        let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
222        let provider = Arc::new(ClaudeCodeProvider::new());
223        let engine = Arc::new(Engine::new(store.clone(), provider));
224        let jwt_config = Arc::new(JwtConfig {
225            secret: "test-secret".to_string(),
226            access_token_ttl_secs: 900,
227            refresh_token_ttl_secs: 604800,
228            cookie_domain: None,
229            cookie_secure: false,
230        });
231        let (event_sender, _) = broadcast::channel::<Event>(1);
232        AppState::new(
233            store,
234            engine,
235            jwt_config,
236            "test-worker-token".to_string(),
237            event_sender,
238        )
239    }
240
241    #[test]
242    fn app_state_cloneable() {
243        let state = test_state();
244        let _cloned = state.clone();
245    }
246
247    #[test]
248    fn app_state_from_ref() {
249        let state = test_state();
250        let extracted: Arc<dyn Store> = Arc::from_ref(&state);
251        assert!(Arc::ptr_eq(&extracted, &state.store));
252    }
253}