#![forbid(unsafe_code)]
#[cfg(feature = "client")]
mod spawn;
use std::path::PathBuf;
pub use kindling_types as types;
pub use kindling_types::{
Capsule, CapsuleStatus, CapsuleType, Id, Observation, ObservationInput, ObservationKind, Pin,
RetrieveOptions, RetrieveResult, ScopeIds,
};
#[cfg(feature = "client")]
pub use kindling_client::{Client, ClientConfig, ClientError, Spawner, Transport};
#[cfg(feature = "spool")]
pub use kindling_client::spool::{AppendOutcome, FlushReport, SpoolError, SpooledClient};
#[cfg(feature = "client")]
use std::time::Duration;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum SpawnStrategy {
#[default]
Embedded,
External,
AttachOnly,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct RuntimeConfig {
pub kindling_home: PathBuf,
pub project_root: String,
pub spool_path: Option<PathBuf>,
pub spawn: SpawnStrategy,
}
impl RuntimeConfig {
pub fn from_default_home(project_root: impl Into<String>) -> Result<Self, RuntimeError> {
let kindling_home = default_kindling_home()
.ok_or_else(|| RuntimeError::Config("could not determine kindling home".into()))?;
Ok(Self {
kindling_home,
project_root: project_root.into(),
spool_path: None,
spawn: SpawnStrategy::default(),
})
}
pub fn embedded(project_root: impl Into<String>) -> Self {
let kindling_home = default_kindling_home().unwrap_or_else(|| PathBuf::from(".kindling"));
Self {
kindling_home,
project_root: project_root.into(),
spool_path: None,
spawn: SpawnStrategy::Embedded,
}
}
pub fn with_home(
kindling_home: impl Into<PathBuf>,
project_root: impl Into<String>,
spawn: SpawnStrategy,
) -> Self {
Self {
kindling_home: kindling_home.into(),
project_root: project_root.into(),
spool_path: None,
spawn,
}
}
pub fn effective_spool_path(&self) -> PathBuf {
self.spool_path
.clone()
.unwrap_or_else(|| self.kindling_home.join("spool.ndjson"))
}
#[cfg(feature = "client")]
fn socket_path(&self) -> PathBuf {
self.kindling_home.join("kindling.sock")
}
#[cfg(feature = "client")]
fn port_path(&self) -> PathBuf {
self.kindling_home.join("kindling.port")
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RuntimeError {
#[error("runtime config error: {0}")]
Config(String),
#[error("runtime feature error: {0}")]
Feature(String),
#[cfg(feature = "client")]
#[error(transparent)]
Client(#[from] kindling_client::ClientError),
}
#[cfg(feature = "client")]
fn expected_schema_version() -> u32 {
kindling_client::EXPECTED_SCHEMA_VERSION
}
fn default_kindling_home() -> Option<PathBuf> {
let home = std::env::var_os("HOME")
.filter(|v| !v.is_empty())
.or_else(|| std::env::var_os("USERPROFILE").filter(|v| !v.is_empty()))?;
Some(PathBuf::from(home).join(".kindling"))
}
#[cfg(feature = "client")]
#[derive(Debug)]
pub struct Runtime {
client: Client,
#[cfg(feature = "spool")]
spooled: SpooledClient,
strategy: SpawnStrategy,
#[cfg(feature = "embedded-daemon")]
server_handle: spawn::ServerHandleSlot,
spawn_flag: spawn::SpawnFlag,
}
#[cfg(feature = "client")]
impl Runtime {
pub async fn start(config: RuntimeConfig) -> Result<Self, RuntimeError> {
let spawn_flag = spawn::SpawnFlag::new();
#[cfg(feature = "embedded-daemon")]
let server_handle: spawn::ServerHandleSlot =
std::sync::Arc::new(std::sync::Mutex::new(None));
let spawner = build_spawner(&config, spawn_flag.clone(), {
#[cfg(feature = "embedded-daemon")]
{
server_handle.clone()
}
#[cfg(not(feature = "embedded-daemon"))]
{
()
}
})?;
let client_config = ClientConfig {
socket_path: config.socket_path(),
port_path: config.port_path(),
project_root: config.project_root.clone(),
expected_schema_version: expected_schema_version(),
connect_timeout: Duration::from_secs(5),
poll_interval: Duration::from_millis(10),
spawn: spawner,
transport: Transport::default(),
spawn_log_path: None,
};
let client = Client::with_config(client_config);
client.health().await?;
#[cfg(feature = "spool")]
let spooled = SpooledClient::new(client.clone(), config.effective_spool_path());
Ok(Self {
client,
#[cfg(feature = "spool")]
spooled,
strategy: config.spawn,
#[cfg(feature = "embedded-daemon")]
server_handle,
spawn_flag,
})
}
pub fn client(&self) -> &Client {
&self.client
}
#[cfg(feature = "spool")]
pub fn spooled_client(&self) -> &SpooledClient {
&self.spooled
}
pub fn strategy(&self) -> &SpawnStrategy {
&self.strategy
}
pub fn spawned_embedded_daemon(&self) -> bool {
self.spawn_flag.fired()
}
pub async fn shutdown(self) -> Result<(), RuntimeError> {
#[cfg(feature = "embedded-daemon")]
{
let handle = self.server_handle.lock().ok().and_then(|mut g| g.take());
if let Some(handle) = handle {
handle.abort();
let _ = handle.await;
}
}
Ok(())
}
}
#[cfg(feature = "client")]
fn build_spawner(
config: &RuntimeConfig,
spawn_flag: spawn::SpawnFlag,
#[cfg(feature = "embedded-daemon")] server_handle: spawn::ServerHandleSlot,
#[cfg(not(feature = "embedded-daemon"))] _server_handle: (),
) -> Result<Spawner, RuntimeError> {
match config.spawn {
SpawnStrategy::Embedded => {
#[cfg(feature = "embedded-daemon")]
{
let server_config = kindling_server::ServerConfig {
socket_path: config.socket_path(),
kindling_home: config.kindling_home.clone(),
pid_path: config.kindling_home.join("kindling.pid"),
port_path: config.port_path(),
idle_timeout: Duration::from_secs(60 * 60),
transport: kindling_server::Transport::default(),
};
Ok(spawn::embedded_spawner(
server_config,
server_handle,
spawn_flag,
))
}
#[cfg(not(feature = "embedded-daemon"))]
{
let _ = spawn_flag;
Err(RuntimeError::Feature(
"SpawnStrategy::Embedded requires the `embedded-daemon` feature".into(),
))
}
}
SpawnStrategy::External => {
#[cfg(feature = "external-spawn")]
{
let _ = spawn_flag;
Ok(Spawner::Command)
}
#[cfg(not(feature = "external-spawn"))]
{
let _ = spawn_flag;
Err(RuntimeError::Feature(
"SpawnStrategy::External requires the `external-spawn` feature".into(),
))
}
}
SpawnStrategy::AttachOnly => Ok(spawn::attach_only_spawner(spawn_flag)),
}
}