noema-actix-webapi 0.1.0

Actix-web backend runtime on Noema (modules, sqlx, UoW, swagger, WebSocket dispatch)
use std::sync::OnceLock;
use std::time::Duration;

use noema::core::{Container, Injectable};
use sqlx::postgres::PgPoolOptions;

use crate::config::DatabaseConfig;
use crate::uow::Tx;

/// Hatch for the sqlx handle. `Injectable` is sync, so `start` (async) must run first.
/// `sqlx::PgPool` is already a cheap `Arc` clone; DI then holds `Arc<PgPool>`.
static POOL: OnceLock<sqlx::PgPool> = OnceLock::new();

/// Process-wide sqlx pool from [`DatabaseConfig`]. Resolve as `Arc<PgPool>` after [`start`].
/// One pool per process. Extra databases (read replica, etc.) are application types.
#[derive(Clone)]
pub struct PgPool {
    inner: sqlx::PgPool,
}

impl PgPool {
    pub fn connection(&self) -> &sqlx::PgPool {
        &self.inner
    }
}

pub(crate) fn pool() -> &'static sqlx::PgPool {
    POOL.get()
        .expect("database pool not started; call Application::start")
}

/// Open Postgres and install the process-wide pool. Must run before `resolve::<PgPool>()`.
pub async fn start(cfg: &DatabaseConfig) -> Result<(), sqlx::Error> {
    if POOL.get().is_some() {
        return Ok(());
    }
    let pool = PgPoolOptions::new()
        .max_connections(cfg.max_connections)
        .acquire_timeout(Duration::from_millis(u64::from(cfg.acquire_timeout_ms)))
        .connect(&cfg.url)
        .await?;
    let _ = POOL.set(pool);
    Ok(())
}

pub(crate) fn is_connected() -> bool {
    POOL.get().is_some()
}

impl Injectable<Container> for PgPool {
    fn inject(_: &Container) -> Self {
        Self {
            inner: pool().clone(),
        }
    }
}

/// Infrastructure hatch: sqlx transaction inside an opaque [`Tx`].
///
/// Import from [`crate::db`], not the application prelude.
pub fn postgres_tx(tx: &mut Tx) -> &mut sqlx::Transaction<'static, sqlx::Postgres> {
    tx.inner_mut()
}

pub async fn ping() -> Result<(), sqlx::Error> {
    sqlx::query("SELECT 1").execute(pool()).await?;
    Ok(())
}