arcature 2026.2.1

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! The lifecycle-managed serve path — start subsystems, serve on a
//! caller-provided listener, and shut down.
//!
//! [`Application::serve_with_lifecycle`] is the testable analogue of
//! [`super::run_with_lifecycle`]: it runs the same startup → state → serve →
//! shutdown sequence but accepts a pre-bound listener and a caller-provided
//! shutdown signal. A real application uses `run_with_lifecycle` (which binds
//! the listener and wires `ctrl_c`); an integration test uses
//! `serve_with_lifecycle` with an ephemeral listener and a `oneshot` channel
//! so it can drive the lifecycle deterministically without sending OS signals.
//!
//! Gated by the `macros` feature (needs `tokio` for `TcpListener` / `ctrl_c`
//! and `tokio-util` for `CancellationToken`) plus at least one lifecycle
//! subsystem (db/cache/storage/mail/jobs). Under `macros`-alone the
//! `Resources` type is empty and the lifecycle is a no-op — the plain `serve`
//! path is the correct entry point there.

use std::future::Future;

use crate::application::Result;
use crate::application::shutdown;
use crate::application::startup;
use crate::application::ty::Application;

impl<S> Application<S>
where
    S: Clone + Send + Sync + 'static,
{
    /// Serve with a full subsystem lifecycle on a caller-provided listener.
    ///
    /// This is the testable entry point for an application with subsystems.
    /// It runs the same lifecycle as [`run_with_lifecycle`](Self::run_with_lifecycle)
    /// but accepts a pre-bound `listener` and a caller-provided
    /// `shutdown_signal`, so integration tests can drive the lifecycle
    /// deterministically (e.g. with a `tokio::sync::oneshot` channel) instead
    /// of sending `SIGINT`.
    ///
    /// # Lifecycle
    ///
    /// 1. **Startup** — `startup()` connects all configured subsystems in
    ///    order (db → jobs → cache → storage, mail). On failure, the
    ///    already-started subsystems are torn down (reverse order) and the
    ///    error is returned.
    /// 2. **State** — `state_fn(&resources)` builds the `AppState` from the
    ///    live handles (e.g. cloning the `Db` into the state). The engine
    ///    calls `routes.with_state(state)` to resolve the router to `()`.
    /// 3. **Serve** — the engine assembles the pipeline and serves it on
    ///    `listener` until `shutdown_signal` resolves.
    /// 4. **Shutdown** — `shutdown()` tears down subsystems in reverse
    ///    startup order (worker drain → db close → cache, storage, mail).
    ///
    /// # Errors
    ///
    /// Returns [`EngineError::Startup`](crate::EngineError::Startup) if a subsystem fails to start,
    /// [`EngineError::Serve`](crate::EngineError::Serve) if the server fails, or
    /// [`EngineError::Shutdown`](crate::EngineError::Shutdown) if a subsystem fails to shut down. A serve
    /// error takes precedence over a shutdown error (the operator sees the
    /// root cause first).
    pub async fn serve_with_lifecycle<L, F, Sh>(
        self,
        listener: L,
        state_fn: F,
        shutdown_signal: Sh,
    ) -> Result<()>
    where
        L: crate::axum::serve::Listener,
        L::Addr: std::fmt::Debug,
        F: Fn(&crate::application::Resources) -> S + Send + Sync + 'static,
        Sh: Future<Output = ()> + Send + 'static,
    {
        // ── 1. Startup ──────────────────────────────────────────────────
        let (started, resolved_app) = startup::startup(self).await?;

        // ── 2. State ────────────────────────────────────────────────────
        let state = state_fn(&started.resources);
        let routes = resolved_app.routes.with_state(state);
        let app = Application {
            routes,
            proxy: resolved_app.proxy,
            bind_address: resolved_app.bind_address,
            port: resolved_app.port,
            #[cfg(feature = "inertia")]
            inertia_config: resolved_app.inertia_config,
            #[cfg(feature = "inertia")]
            page_contracts: resolved_app.page_contracts,
            #[cfg(feature = "pages")]
            pages: resolved_app.pages,
            #[cfg(feature = "pages")]
            maintenance_guard: resolved_app.maintenance_guard,
            #[cfg(feature = "db")]
            database: None,
            #[cfg(feature = "cache")]
            cache_config: None,
            #[cfg(feature = "storage")]
            storage_config: None,
            #[cfg(feature = "mail")]
            mail_config: None,
            #[cfg(feature = "jobs")]
            jobs_registry: None,
            #[cfg(feature = "jobs")]
            worker_config: None,
            #[cfg(feature = "dx")]
            error_mapping: resolved_app.error_mapping,
            #[cfg(feature = "dev-proxy")]
            dev_proxy_endpoint: resolved_app.dev_proxy_endpoint,
        };

        // ── 3. Serve ───────────────────────────────────────────────────
        // `serve_with_shutdown` assembles the pipeline (proxy, observe,
        // Inertia, maintenance, 404 fallback) and serves on `listener` until
        // `shutdown_signal` resolves. It is the same path `run_with_lifecycle`
        // uses, just with a caller-provided listener and signal.
        let serve_result = app.serve_with_shutdown(listener, shutdown_signal).await;

        // ── 4. Shutdown (regardless of serve result) ────────────────────
        // Always shut down the subsystems, even if the server failed. A
        // partial-shutdown leak is worse than a shutdown-after-error.
        let shutdown_result = shutdown::shutdown(started).await;

        // Return the first error encountered (serve takes precedence over
        // shutdown — the operator sees the root cause first).
        match serve_result {
            Ok(()) => shutdown_result,
            Err(serve_err) => {
                // Log the shutdown error if it also failed, but return the
                // serve error (the root cause).
                if let Err(ref shutdown_err) = shutdown_result {
                    eprintln!("warning: shutdown also failed after serve error: {shutdown_err}");
                }
                Err(serve_err)
            }
        }
    }
}