use crate::agents::{CsrfManagerAgent, SessionManagerAgent};
use crate::jobs::JobAgent;
use crate::oauth2::OAuth2Agent;
use crate::template::FrameworkTemplates;
use crate::{config::ActonHtmxConfig, observability::ObservabilityConfig};
use acton_reactive::prelude::{AgentHandle, AgentRuntime};
use sqlx::PgPool;
use std::sync::Arc;
#[cfg(feature = "redis")]
use deadpool_redis::Pool as RedisPool;
#[derive(Clone)]
pub struct ActonHtmxState {
config: Arc<ActonHtmxConfig>,
observability: Arc<ObservabilityConfig>,
session_manager: AgentHandle,
csrf_manager: AgentHandle,
oauth2_manager: AgentHandle,
job_agent: AgentHandle,
database_pool: Option<Arc<PgPool>>,
#[cfg(feature = "redis")]
redis_pool: Option<RedisPool>,
templates: FrameworkTemplates,
}
impl ActonHtmxState {
pub async fn new(runtime: &mut AgentRuntime) -> anyhow::Result<Self> {
let config = ActonHtmxConfig::default();
let observability = ObservabilityConfig::default();
let session_manager = SessionManagerAgent::spawn(runtime).await?;
let csrf_manager = CsrfManagerAgent::spawn(runtime).await?;
let oauth2_manager = OAuth2Agent::spawn(runtime).await?;
let job_agent = JobAgent::spawn(runtime).await?;
let templates = FrameworkTemplates::new()?;
Ok(Self {
config: Arc::new(config),
observability: Arc::new(observability),
session_manager,
csrf_manager,
oauth2_manager,
job_agent,
database_pool: None,
#[cfg(feature = "redis")]
redis_pool: None,
templates,
})
}
pub async fn with_config(
runtime: &mut AgentRuntime,
config: ActonHtmxConfig,
) -> anyhow::Result<Self> {
let observability = ObservabilityConfig::new("acton-htmx");
let session_manager = SessionManagerAgent::spawn(runtime).await?;
let csrf_manager = CsrfManagerAgent::spawn(runtime).await?;
let oauth2_manager = OAuth2Agent::spawn(runtime).await?;
let job_agent = JobAgent::spawn(runtime).await?;
let templates = FrameworkTemplates::new()?;
Ok(Self {
config: Arc::new(config),
observability: Arc::new(observability),
session_manager,
csrf_manager,
oauth2_manager,
job_agent,
database_pool: None,
#[cfg(feature = "redis")]
redis_pool: None,
templates,
})
}
#[must_use]
pub fn config(&self) -> &ActonHtmxConfig {
&self.config
}
#[must_use]
pub fn observability(&self) -> &ObservabilityConfig {
&self.observability
}
#[must_use]
pub const fn templates(&self) -> &FrameworkTemplates {
&self.templates
}
#[must_use]
pub const fn session_manager(&self) -> &AgentHandle {
&self.session_manager
}
#[must_use]
pub const fn csrf_manager(&self) -> &AgentHandle {
&self.csrf_manager
}
#[must_use]
pub const fn oauth2_agent(&self) -> &AgentHandle {
&self.oauth2_manager
}
#[must_use]
pub const fn job_agent(&self) -> &AgentHandle {
&self.job_agent
}
#[must_use]
pub fn database_pool(&self) -> &PgPool {
self.database_pool
.as_ref()
.expect("Database pool not initialized")
}
pub fn set_database_pool(&mut self, pool: PgPool) {
self.database_pool = Some(Arc::new(pool));
}
#[must_use]
#[cfg(feature = "redis")]
pub const fn redis_pool(&self) -> Option<&RedisPool> {
self.redis_pool.as_ref()
}
#[cfg(feature = "redis")]
pub fn set_redis_pool(&mut self, pool: RedisPool) {
self.redis_pool = Some(pool);
}
pub async fn get_job_metrics(&self) -> Result<crate::jobs::agent::JobMetrics, anyhow::Error> {
use acton_reactive::prelude::AgentHandleInterface;
use crate::jobs::agent::GetMetricsRequest;
use std::time::Duration;
let (request, rx) = GetMetricsRequest::new();
self.job_agent().send(request).await;
let timeout = Duration::from_millis(100);
Ok(tokio::time::timeout(timeout, rx).await??)
}
pub async fn get_job_status(
&self,
id: crate::jobs::JobId,
) -> Result<Option<crate::jobs::JobStatus>, anyhow::Error> {
use acton_reactive::prelude::AgentHandleInterface;
use crate::jobs::agent::GetJobStatusRequest;
use std::time::Duration;
let (request, rx) = GetJobStatusRequest::new(id);
self.job_agent().send(request).await;
let timeout = Duration::from_millis(100);
Ok(tokio::time::timeout(timeout, rx).await??)
}
}
#[cfg(test)]
mod tests {
use super::*;
use acton_reactive::prelude::ActonApp;
#[tokio::test(flavor = "multi_thread")]
async fn test_new_state() {
let mut runtime = ActonApp::launch();
let state = ActonHtmxState::new(&mut runtime)
.await
.expect("Failed to create state");
assert_eq!(state.config().htmx.request_timeout_ms, 5000);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_with_config() {
let mut runtime = ActonApp::launch();
let mut config = ActonHtmxConfig::default();
config.htmx.request_timeout_ms = 10000;
let state = ActonHtmxState::with_config(&mut runtime, config)
.await
.expect("Failed to create state");
assert_eq!(state.config().htmx.request_timeout_ms, 10000);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_clone_state() {
let mut runtime = ActonApp::launch();
let state = ActonHtmxState::new(&mut runtime)
.await
.expect("Failed to create state");
let cloned = state.clone();
assert!(Arc::ptr_eq(&state.config, &cloned.config));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_session_manager_accessible() {
let mut runtime = ActonApp::launch();
let state = ActonHtmxState::new(&mut runtime)
.await
.expect("Failed to create state");
let _handle = state.session_manager();
}
}