use std::sync::{Arc, OnceLock};
use std::time::Duration;
use basilisk_rust_client::{BasiliskClient, BasiliskClientConfig};
use crate::env::AppEnvironment;
use crate::retry::RetryStrategy;
const MAX_CONNECT_ATTEMPTS: usize = 5;
static INSTANCE: OnceLock<Arc<GatewayConnect>> = OnceLock::new();
#[derive(Clone)]
pub struct GatewayConnect {
pub client: Option<BasiliskClient>,
}
impl GatewayConnect {
fn new(client: BasiliskClient) -> Self {
Self { client: Some(client) }
}
pub fn instance() -> Option<Arc<Self>> {
INSTANCE.get().cloned()
}
pub fn get_client(&self) -> Option<&BasiliskClient> {
self.client.as_ref()
}
pub fn require_client(&self) -> anyhow::Result<&BasiliskClient> {
self.client.as_ref().ok_or_else(|| anyhow::anyhow!("gateway not connected — no BasiliskClient (empty mountPaths)"))
}
pub async fn set_up(
mount_paths: Vec<String>,
scheme: impl Into<String>,
weight: i32,
auth_type: impl Into<String>,
) -> anyhow::Result<Option<Arc<Self>>> {
if INSTANCE.get().is_some() {
anyhow::bail!("Already initialized");
}
if mount_paths.is_empty() {
return Ok(None);
}
let (host, port, bus_port, token, base_url, service_id, fingerprint) =
resolve_env()?;
let config = BasiliskClientConfig {
gateway_base_url: base_url,
bus_host: host.clone(),
bus_port,
service_id: service_id.clone(),
fingerprint,
path_prefixes: mount_paths.clone(),
scheme: scheme.into(),
host,
port,
weight,
registration_auth_type: auth_type.into(),
registration_token: token,
};
let client = Self::connect_with_retry(config).await?;
let instance = Arc::new(Self::new(client));
let _ = INSTANCE.set(instance.clone());
Ok(Some(instance))
}
pub async fn set_up_with_config(config: BasiliskClientConfig) -> anyhow::Result<Arc<Self>> {
if INSTANCE.get().is_some() {
anyhow::bail!("Already initialized");
}
let client = Self::connect_with_retry(config).await?;
let instance = Arc::new(Self::new(client));
let _ = INSTANCE.set(instance.clone());
Ok(instance)
}
async fn connect_with_retry(config: BasiliskClientConfig) -> anyhow::Result<BasiliskClient> {
let strategy = RetryStrategy::new()
.with_base_delay(Duration::from_millis(1_000))
.with_max_delay(Duration::from_millis(30_000))
.with_max_attempts(MAX_CONNECT_ATTEMPTS);
let cfg = config.clone();
let result = strategy
.retry(|| {
let cfg = cfg.clone();
async move { BasiliskClient::connect(cfg).await.map_err(|e| e.to_string()) }
})
.await;
match result {
Ok(client) => Ok(client),
Err(e) => anyhow::bail!("basilisk connect exhausted after {} attempts: {}", e.attempts, e.source),
}
}
pub async fn deregister(&self) -> anyhow::Result<()> {
if let Some(c) = self.client.as_ref() {
c.deregister().await
} else {
Ok(())
}
}
}
fn resolve_env() -> anyhow::Result<(String, u16, u16, String, String, String, String)> {
let env = AppEnvironment::try_get();
let get_required = |key: &str| -> anyhow::Result<String> {
if let Some(e) = env {
if let Some(v) = e.get_value(key) {
return Ok(v);
}
}
std::env::var(key).map_err(|_| anyhow::anyhow!("missing required env {}", key))
};
let host = get_required("BASILISK_HOST")?;
let port: u16 = if let Some(e) = env {
e.get_value("SERVER_PORT")
.and_then(|v| v.parse().ok())
.unwrap_or(8080)
} else {
std::env::var("SERVER_PORT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(8080)
};
let bus_port: u16 = get_required("BASILISK_BUS_PORT")?
.parse()
.map_err(|_| anyhow::anyhow!("invalid BASILISK_BUS_PORT"))?;
let token = get_required("BASILISK_TOKEN")?;
let base_url = get_required("BASILISK_GATEWAY_URL")?;
let service_id = get_required("BASILISK_SERVICE_ID")?;
let fingerprint = get_required("SERVICE_KEY")?;
Ok((host, port, bus_port, token, base_url, service_id, fingerprint))
}