Skip to main content

claw_spawn/server/
state.rs

1use crate::application::{BotLifecycleService, ProvisioningService};
2use crate::infrastructure::{
3    AppConfig, DigitalOceanClient, PostgresAccountRepository, PostgresBotRepository,
4    PostgresConfigRepository, PostgresDropletRepository, SecretsEncryption,
5};
6use anyhow::Context;
7use sqlx::PgPool;
8use std::sync::Arc;
9
10pub type ProvisioningServiceType = ProvisioningService<
11    PostgresAccountRepository,
12    PostgresBotRepository,
13    PostgresConfigRepository,
14    PostgresDropletRepository,
15>;
16
17pub type BotLifecycleServiceType =
18    BotLifecycleService<PostgresBotRepository, PostgresConfigRepository>;
19
20#[derive(Clone)]
21pub struct AppState {
22    pub pool: PgPool,
23    pub account_repo: Arc<PostgresAccountRepository>,
24    pub provisioning: Arc<ProvisioningServiceType>,
25    pub lifecycle: Arc<BotLifecycleServiceType>,
26}
27
28/// Build full state from config + an existing pool.
29///
30/// Intended for embedding into a larger service that already manages a `PgPool`.
31pub async fn build_state_with_pool(
32    config: AppConfig,
33    pool: PgPool,
34    run_migrations: bool,
35) -> anyhow::Result<AppState> {
36    if run_migrations {
37        sqlx::migrate!("./migrations")
38            .run(&pool)
39            .await
40            .context("run migrations")?;
41    }
42
43    let encryption =
44        Arc::new(SecretsEncryption::new(&config.encryption_key).context("init encryption")?);
45
46    let do_client = Arc::new(
47        DigitalOceanClient::new(config.digitalocean_token).context("init DigitalOcean client")?,
48    );
49
50    let account_repo = Arc::new(PostgresAccountRepository::new(pool.clone()));
51    let bot_repo = Arc::new(PostgresBotRepository::new(pool.clone()));
52    let config_repo = Arc::new(PostgresConfigRepository::new(pool.clone()));
53    let droplet_repo = Arc::new(PostgresDropletRepository::new(pool.clone()));
54
55    let provisioning = Arc::new(ProvisioningService::new(
56        do_client,
57        account_repo.clone(),
58        bot_repo.clone(),
59        config_repo.clone(),
60        droplet_repo.clone(),
61        encryption,
62        config.openclaw_image,
63        config.control_plane_url,
64        config.customizer_repo_url,
65        config.customizer_ref,
66        config.customizer_workspace_dir,
67        config.customizer_agent_name,
68        config.customizer_owner_name,
69        config.customizer_skip_qmd,
70        config.customizer_skip_cron,
71        config.customizer_skip_git,
72        config.customizer_skip_heartbeat,
73    ));
74
75    let lifecycle = Arc::new(BotLifecycleService::new(
76        bot_repo.clone(),
77        config_repo.clone(),
78    ));
79
80    Ok(AppState {
81        pool,
82        account_repo,
83        provisioning,
84        lifecycle,
85    })
86}
87
88/// Build state for the standalone server.
89///
90/// Creates the `PgPool`, runs migrations, and wires repositories/services.
91pub async fn build_state_from_env(config: AppConfig) -> anyhow::Result<AppState> {
92    let pool = PgPool::connect(&config.database_url)
93        .await
94        .context("connect database")?;
95    build_state_with_pool(config, pool, true).await
96}