use meerkat_core::lifecycle::{InputId, RunEvent, RunId};
use serde::{Deserialize, Serialize};
use crate::accept::AcceptOutcome;
use crate::identifiers::LogicalRuntimeId;
use crate::input::Input;
use crate::input_state::InputState;
use crate::runtime_event::RuntimeEventEnvelope;
use crate::runtime_state::RuntimeState;
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum RuntimeDriverError {
#[error("Runtime not ready: {state}")]
NotReady { state: RuntimeState },
#[error("Input validation failed: {reason}")]
ValidationFailed { reason: String },
#[error("Runtime destroyed")]
Destroyed,
#[error("Internal error: {0}")]
Internal(String),
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum RuntimeControlPlaneError {
#[error("Runtime not found: {0}")]
NotFound(LogicalRuntimeId),
#[error("Invalid state for operation: {state}")]
InvalidState { state: RuntimeState },
#[error("Store error: {0}")]
StoreError(String),
#[error("Internal error: {0}")]
Internal(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "command", rename_all = "snake_case")]
pub enum RuntimeControlCommand {
Stop,
Resume,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecoveryReport {
pub inputs_recovered: usize,
pub inputs_abandoned: usize,
pub inputs_requeued: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub details: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetireReport {
pub inputs_abandoned: usize,
#[serde(default)]
pub inputs_pending_drain: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResetReport {
pub inputs_abandoned: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecycleReport {
pub inputs_transferred: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DestroyReport {
pub inputs_abandoned: usize,
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait RuntimeDriver: Send + Sync {
async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError>;
async fn on_runtime_event(
&mut self,
event: RuntimeEventEnvelope,
) -> Result<(), RuntimeDriverError>;
async fn on_run_event(&mut self, event: RunEvent) -> Result<(), RuntimeDriverError>;
async fn on_runtime_control(
&mut self,
command: RuntimeControlCommand,
) -> Result<(), RuntimeDriverError>;
async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError>;
async fn retire(&mut self) -> Result<RetireReport, RuntimeDriverError>;
async fn reset(&mut self) -> Result<ResetReport, RuntimeDriverError>;
async fn destroy(&mut self) -> Result<DestroyReport, RuntimeDriverError>;
fn runtime_state(&self) -> RuntimeState;
fn input_state(&self, input_id: &InputId) -> Option<&InputState>;
fn active_input_ids(&self) -> Vec<InputId>;
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait RuntimeControlPlane: Send + Sync {
async fn ingest(
&self,
runtime_id: &LogicalRuntimeId,
input: Input,
) -> Result<AcceptOutcome, RuntimeControlPlaneError>;
async fn publish_event(
&self,
event: RuntimeEventEnvelope,
) -> Result<(), RuntimeControlPlaneError>;
async fn retire(
&self,
runtime_id: &LogicalRuntimeId,
) -> Result<RetireReport, RuntimeControlPlaneError>;
async fn recycle(
&self,
runtime_id: &LogicalRuntimeId,
) -> Result<RecycleReport, RuntimeControlPlaneError>;
async fn reset(
&self,
runtime_id: &LogicalRuntimeId,
) -> Result<ResetReport, RuntimeControlPlaneError>;
async fn recover(
&self,
runtime_id: &LogicalRuntimeId,
) -> Result<RecoveryReport, RuntimeControlPlaneError>;
async fn runtime_state(
&self,
runtime_id: &LogicalRuntimeId,
) -> Result<RuntimeState, RuntimeControlPlaneError>;
async fn destroy(
&self,
runtime_id: &LogicalRuntimeId,
) -> Result<DestroyReport, RuntimeControlPlaneError>;
async fn load_boundary_receipt(
&self,
runtime_id: &LogicalRuntimeId,
run_id: &RunId,
sequence: u64,
) -> Result<Option<meerkat_core::lifecycle::RunBoundaryReceipt>, RuntimeControlPlaneError>;
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn _assert_driver_object_safe(_: &dyn RuntimeDriver) {}
fn _assert_control_plane_object_safe(_: &dyn RuntimeControlPlane) {}
#[test]
fn runtime_control_command_serde() {
let cmd = RuntimeControlCommand::Stop;
let json = serde_json::to_value(&cmd).unwrap();
assert_eq!(json["command"], "stop");
let cmd = RuntimeControlCommand::Resume;
let json = serde_json::to_value(&cmd).unwrap();
assert_eq!(json["command"], "resume");
}
#[test]
fn runtime_driver_error_display() {
let err = RuntimeDriverError::NotReady {
state: RuntimeState::Initializing,
};
assert!(err.to_string().contains("initializing"));
let err = RuntimeDriverError::ValidationFailed {
reason: "bad input".into(),
};
assert!(err.to_string().contains("bad input"));
}
#[test]
fn runtime_control_plane_error_display() {
let err = RuntimeControlPlaneError::NotFound(LogicalRuntimeId::new("missing"));
assert!(err.to_string().contains("missing"));
}
#[test]
fn recovery_report_serde() {
let report = RecoveryReport {
inputs_recovered: 5,
inputs_abandoned: 1,
inputs_requeued: 3,
details: vec!["requeued 3 staged inputs".into()],
};
let json = serde_json::to_value(&report).unwrap();
let parsed: RecoveryReport = serde_json::from_value(json).unwrap();
assert_eq!(parsed.inputs_recovered, 5);
}
}