diff --git a/Cargo.lock b/Cargo.lock index a737e85e8..e0584827f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2949,7 +2949,6 @@ dependencies = [ name = "holons_client" version = "0.1.0" dependencies = [ - "async-trait", "base_types", "core_types", "futures-executor", diff --git a/host/Cargo.lock b/host/Cargo.lock index e655db8f1..1013b5ad1 100644 --- a/host/Cargo.lock +++ b/host/Cargo.lock @@ -3485,11 +3485,9 @@ dependencies = [ "holons_boundary", "holons_client", "holons_core", - "holons_loader_client", "holons_trust_channel", "serde_bytes", "tracing", - "type_names", ] [[package]] @@ -3881,7 +3879,6 @@ dependencies = [ name = "holons_client" version = "0.1.0" dependencies = [ - "async-trait", "base_types", "core_types", "futures-executor", @@ -5209,6 +5206,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tracing", ] [[package]] diff --git a/host/conductora/src/map_commands/all_spaces.rs b/host/conductora/src/map_commands/all_spaces.rs deleted file mode 100644 index 50926c4e7..000000000 --- a/host/conductora/src/map_commands/all_spaces.rs +++ /dev/null @@ -1,18 +0,0 @@ - -use holons_client::shared_types::holon_space::SpaceInfo; -use holons_receptor::ReceptorFactory; -use tauri::{command, State}; - - -#[command] -pub async fn all_spaces ( - receptor_factory: State<'_, ReceptorFactory>, -) -> Result { - - tracing::debug!("[TAURI COMMAND] 'all_spaces' command invoked"); - - let spaces = receptor_factory.all_spaces_by_type("holochain") - .await - .map_err(|e| format!("receptor service error: {:?}", e))?; - Ok(spaces) -} \ No newline at end of file diff --git a/host/conductora/src/map_commands/debug_serde.rs b/host/conductora/src/map_commands/debug_serde.rs deleted file mode 100644 index 9aaf54e3b..000000000 --- a/host/conductora/src/map_commands/debug_serde.rs +++ /dev/null @@ -1,28 +0,0 @@ -use holons_client::shared_types::map_request::MapRequestWire; -use tauri::command; - -// Add this temporary command to see the raw JSON -#[command] -pub async fn _debug_dance_raw( - raw_data: serde_json::Value, -) -> Result { - tracing::warn!("[DEBUG] Raw JSON received: {}", raw_data); - - // Try to deserialize step by step - match serde_json::from_value::(raw_data.clone()) { - Ok(map_request) => { - tracing::info!("[DEBUG] Successfully parsed MapRequestWire: {:?}", map_request); - Ok("SUCCESS: MapRequestWire parsed correctly".to_string()) - }, - Err(e) => { - tracing::error!("[DEBUG] Failed to parse MapRequestWire: {}", e); - - // Let's see what fields are missing/wrong - if let Some(obj) = raw_data.as_object() { - tracing::error!("[DEBUG] Available fields: {:?}", obj.keys().collect::>()); - } - - Err(format!("Deserialization error: {}", e)) - } - } -} diff --git a/host/conductora/src/map_commands/map_request.rs b/host/conductora/src/map_commands/map_request.rs deleted file mode 100644 index e5902ce8a..000000000 --- a/host/conductora/src/map_commands/map_request.rs +++ /dev/null @@ -1,102 +0,0 @@ - -use holons_receptor::factory::ReceptorFactory; -use holons_client::shared_types::map_request::MapRequestWire; -use holons_client::shared_types::map_response::MapResponseWire; -use tauri::{command, State}; -use core_types::HolonError; - -#[command] -pub async fn map_request( - map_request: MapRequestWire, - receptor_factory: State<'_, ReceptorFactory>, -) -> Result { - - tracing::debug!("[TAURI COMMAND] 'map_request' command invoked for space: {:?}", map_request); - // a map_request is currently using "holochain" receptor type only - let receptor = receptor_factory.get_receptor_by_type("holochain"); - let context = receptor.transaction_context(); - let bound_request = map_request.bind(&context)?; - - receptor - .handle_map_request(bound_request) - .await - .map_err(|e| { - tracing::error!("Error in handle_map_request: {:?}", e); - HolonError::from(e) - }) - .map(|response| MapResponseWire::from(&response)) -} - - - - -// WORK IN PROGRESS: Refactor to move logic out of command function for easier testing -/* -pub(crate) async fn map_request_impl( - map_request: MapRequest, - receptor_factory: &ReceptorFactory, -) -> Result { - tracing::debug!( - "[TAURI COMMAND] 'map_request' impl invoked for space: {:?}", - map_request - ); - - let receptor = receptor_factory.get_receptor_by_type("holochain"); - receptor - .handle_map_request(map_request) - .await - .map_err(HolonError::from) -} - -//try this with a mock (sweet) conductor.. (no plugin required) -#[cfg(test)] -mod tests { - use super::*; - use tokio; - - #[tokio::test] - async fn test_map_request_impl() { - // Build a ReceptorFactory in whatever β€œtest” shape you want: - let factory = ReceptorFactory::new(); - // maybe load some fake configs or inject a test receptor - - let req = MapRequest { - // ... - }; - - let res = map_request_impl(req, &factory).await; - - // assert whatever you expect - assert!(res.is_ok()); - } -} - //this didnt work .. abandoned for now -#[cfg(test)] -mod tests { - use super::*; - use crate::AppBuilder; - use tauri::{Manager, test::{mock_context, noop_assets}}; - - #[tokio::test] - async fn test_maprequest_command() { - // 1. Build app using your real AppBuilder - let builder = AppBuilder::build(); - - // 2. Use mock_context + noop_assets instead of real config/assets - let app = builder - .build(mock_context(noop_assets())) - .expect("failed to build mock app"); - - let map_request = MapRequest::test_for_stage_new_holon(); - let state: tauri::State<'_, ReceptorFactory> = app.state(); - - - // 3. Call your tauri::command directly - let result = crate::commands::map_request(map_request, state).await; - - // 4. Assert whatever you expect - assert!(result.is_ok()); - } -} -*/ -// The above test is a work in progress to refactor the command function for easier testing. diff --git a/host/conductora/src/map_commands/mod.rs b/host/conductora/src/map_commands/mod.rs index ad40e6bf4..27b5c3aba 100644 --- a/host/conductora/src/map_commands/mod.rs +++ b/host/conductora/src/map_commands/mod.rs @@ -1,13 +1,5 @@ -pub mod all_spaces; -pub mod root_space; pub mod serde_test; -pub mod map_request; pub mod status; -pub mod debug_serde; -pub(crate) use root_space::*; -pub(crate) use all_spaces::*; pub(crate) use status::*; pub(crate) use serde_test::*; -pub(crate) use map_request::*; - diff --git a/host/conductora/src/map_commands/root_space.rs b/host/conductora/src/map_commands/root_space.rs deleted file mode 100644 index 8fb51c66f..000000000 --- a/host/conductora/src/map_commands/root_space.rs +++ /dev/null @@ -1,17 +0,0 @@ - -use holons_client::shared_types::holon_space::SpaceInfo; -use holons_receptor::ReceptorFactory; -use tauri::{command, State}; - - -#[command] -pub async fn root_space ( - receptor_factory: State<'_, ReceptorFactory>, -) -> Result { - - tracing::debug!("[TAURI COMMAND] 'root_space' command invoked"); - let spaces = receptor_factory.all_spaces_by_type("local") - .await - .map_err(|e| format!("receptor service error: {:?}", e))?; - Ok(spaces) -} \ No newline at end of file diff --git a/host/conductora/src/map_commands/serde_test.rs b/host/conductora/src/map_commands/serde_test.rs index ae63dfba7..47746d666 100644 --- a/host/conductora/src/map_commands/serde_test.rs +++ b/host/conductora/src/map_commands/serde_test.rs @@ -1,6 +1,5 @@ - -use holons_client::shared_types::map_request::MapRequestWire; -use tauri::{command }; +use map_commands::wire::MapIpcRequest; +use tauri::command; #[command] pub async fn serde_test( @@ -21,8 +20,7 @@ pub async fn serde_test( } } - //HERE add the type you want to check against in the from_str<> - match serde_json::from_str::(&request_json) { + match serde_json::from_str::(&request_json) { Ok(request) => { tracing::info!("[TEST] Successfully parsed: {:?}", request); Ok("Parsed successfully".to_string()) @@ -57,10 +55,10 @@ pub async fn serde_test( .nth(1) .and_then(|s| s.split('`').next()) .unwrap_or("unknown"); - format!("\nπŸ” Missing field: `{}`", field_name) + format!("\nMissing field: `{}`", field_name) } else if error_string.contains("invalid type") { format!( - "\nπŸ” Type mismatch - check field types match between TypeScript and Rust" + "\nType mismatch - check field types match between TypeScript and Rust" ) } else { String::new() @@ -68,9 +66,9 @@ pub async fn serde_test( let error_msg = format!( "Parse error at position {} (line {}, column {}):\n\ - πŸ“‹ Error: {}\n\ - 🏷️ Type: {}{}\n\ - πŸ“ Context around column {}:\n{}", + Error: {}\n\ + Type: {}{}\n\ + Context around column {}:\n{}", column, line, column, e, classification, path_hint, column, error_snippet ); @@ -82,5 +80,5 @@ pub async fn serde_test( Err(short_error) } } - + } diff --git a/host/conductora/src/map_commands/status.rs b/host/conductora/src/map_commands/status.rs index 5007285b7..917c06ec0 100644 --- a/host/conductora/src/map_commands/status.rs +++ b/host/conductora/src/map_commands/status.rs @@ -1,27 +1,18 @@ use crate::runtime::RuntimeState; -use holons_receptor::ReceptorFactory; use tauri::{command, State}; #[command] pub async fn is_service_ready( - receptor_factory: State<'_, ReceptorFactory>, runtime_state: State<'_, RuntimeState>, ) -> Result { tracing::debug!("[TAURI COMMAND] 'is_service_ready' command invoked"); - let receptors_loaded = receptor_factory.are_receptors_loaded(); - let runtime_ready = runtime_state + let is_ready = runtime_state .read() .map(|guard| guard.is_some()) .unwrap_or(false); - let is_ready = receptors_loaded && runtime_ready; - tracing::debug!( - "Service ready: {} (receptors={}, runtime={})", - is_ready, - receptors_loaded, - runtime_ready, - ); + tracing::debug!("Service ready: {}", is_ready); Ok(is_ready) -} \ No newline at end of file +} diff --git a/host/conductora/src/setup/app_builder.rs b/host/conductora/src/setup/app_builder.rs index de0ea9868..408f810a6 100644 --- a/host/conductora/src/setup/app_builder.rs +++ b/host/conductora/src/setup/app_builder.rs @@ -7,15 +7,13 @@ use crate::{ app_config::load_storage_config, providers::holochain::holochain_plugin, storage_config::{StorageConfig, StorageProvider} }, setup::{ - holochain_setup::{ConductorClientState, HolochainSetup, HolochainWindowSetup}, local_setup::LocalSetup, window_setup::DefaultWindowSetup}, + holochain_setup::{ConductorClientState, HolochainSetup, HolochainWindowSetup}, window_setup::DefaultWindowSetup}, }; use crate::setup::window_setup::ProviderWindowSetup; -use crate::setup::receptor_config_registry::ReceptorConfigRegistry; use holons_client::init_client_runtime; use holons_trust_channel::TrustChannel; use map_commands::dispatch::{Runtime, RuntimeSession}; -use holons_receptor::ReceptorFactory; use tauri::{AppHandle, Manager, Listener}; pub struct AppBuilder; @@ -29,16 +27,10 @@ impl AppBuilder { // Base builder without setup let base = tauri::Builder::default() .manage(storage_cfg.clone()) - .manage(ReceptorFactory::new()) - .manage(ReceptorConfigRegistry::new()) .manage::(RwLock::new(None)) .manage::(RwLock::new(None)) .invoke_handler(tauri::generate_handler![ - commands::root_space, - //commands::load_holons, commands::serde_test, - commands::map_request, - commands::all_spaces, commands::is_service_ready, runtime::dispatch_map_command::dispatch_map_command, ]); @@ -87,12 +79,6 @@ impl AppBuilder { tracing::error!("[APP BUILDER] Provider setup failed: {}", e); } - // Load receptor configs into factory - if let Err(e) = Self::load_receptor_configs(handle).await { - tracing::error!("[APP BUILDER] Failed to load receptor configs: {}", e); - return; - } - // Construct the MAP Commands Runtime (if conductor client is available) Self::initialize_runtime(handle); @@ -105,18 +91,6 @@ impl AppBuilder { tracing::info!("[APP BUILDER] Setup completed successfully."); } - /// Load receptor configs from registry into factory - async fn load_receptor_configs(handle: &AppHandle) -> Result<(), Box> { - if let Some(registry) = handle.try_state::() { - let configs = registry.all(); - if let Some(factory) = handle.try_state::() { - factory.load_from_configs(configs).await?; - tracing::debug!("[APP BUILDER] ReceptorFactory loaded from configs."); - } - } - Ok(()) - } - /// Apply provider-specific plugins based on the storage configuration fn apply_plugins( mut builder: tauri::Builder, @@ -163,10 +137,6 @@ impl AppBuilder { for (_name, provider) in storage_cfg.get_enabled_providers() { match provider.provider_type() { - "local" => { - tracing::info!("[APP BUILDER] Running Local storage setup"); - LocalSetup::setup(handle.clone(), provider).await?; - } "holochain" => { tracing::info!("[APP BUILDER] Running Holochain setup"); HolochainSetup::setup(handle.clone(), provider).await?; diff --git a/host/conductora/src/setup/local_setup.rs b/host/conductora/src/setup/local_setup.rs deleted file mode 100644 index 319599a08..000000000 --- a/host/conductora/src/setup/local_setup.rs +++ /dev/null @@ -1,64 +0,0 @@ -use tauri::{AppHandle, Manager}; -use holons_client::shared_types::base_receptor::BaseReceptor; -use crate::config::{LocalConfig, StorageProvider}; -use crate::setup::receptor_config_registry::ReceptorConfigRegistry; - -pub struct LocalSetup; - -impl LocalSetup { - /// Main setup function for Local integration - pub async fn setup(handle: AppHandle, provider: &StorageProvider) -> anyhow::Result<()> { - let StorageProvider::Local(local_cfg) = provider else { - return Err(anyhow::anyhow!("Invalid storage provider config for Local")); - }; - let receptor_cfg: BaseReceptor = Self::build_receptor(local_cfg).await?; - Self::register_receptor(&handle, receptor_cfg).await?; - - Ok(()) - } - - /// Build the receptor configuration for Local storage - async fn build_receptor( - config: &LocalConfig - ) -> anyhow::Result { - tracing::debug!("[LOCAL SETUP] Building Local storage receptor."); - - // Dynamically collect all properties from the local config - let props = match serde_json::to_value(config)? { - serde_json::Value::Object(map) => { - map.into_iter() - .map(|(k, v)| { - let value_str = match v { - serde_json::Value::String(s) => s, - serde_json::Value::Number(n) => n.to_string(), - serde_json::Value::Bool(b) => b.to_string(), - serde_json::Value::Null => String::new(), - _ => v.to_string(), - }; - (k, value_str) - }) - .collect::>() - } - _ => std::collections::HashMap::new(), - }; - - return Ok(BaseReceptor { - receptor_id: None, - receptor_type: "local".to_string(), - client_handler: None, - properties: props, - }); - } - - - /// Register the Local storage receptor - async fn register_receptor( - handle: &AppHandle, - receptor_cfg: BaseReceptor, - ) -> anyhow::Result<()> { - // Get the registry from app state and register the new config - let registry = handle.state::(); - registry.register(receptor_cfg); - Ok(()) - } -} \ No newline at end of file diff --git a/host/conductora/src/setup/mod.rs b/host/conductora/src/setup/mod.rs index 2a99708b5..f67c9b0d8 100644 --- a/host/conductora/src/setup/mod.rs +++ b/host/conductora/src/setup/mod.rs @@ -1,9 +1,7 @@ pub mod app_builder; -pub mod receptor_config_registry; pub mod providers; pub mod window_setup; -pub mod local_setup; pub use app_builder::AppBuilder; -pub use providers::*; \ No newline at end of file +pub use providers::*; diff --git a/host/conductora/src/setup/providers/holochain_setup.rs b/host/conductora/src/setup/providers/holochain_setup.rs index 2f48e3e96..f36e84018 100644 --- a/host/conductora/src/setup/providers/holochain_setup.rs +++ b/host/conductora/src/setup/providers/holochain_setup.rs @@ -2,11 +2,9 @@ use std::sync::{Arc, Mutex, RwLock}; use crate::config::providers::holochain::*; use crate::config::StorageProvider; -use holons_client::shared_types::base_receptor::BaseReceptor; use tauri::{AppHandle, Manager, Theme}; use holochain_client::{AdminWebsocket, AppWebsocket, AppInfo}; use holochain_receptor::HolochainConductorClient; -use crate::setup::receptor_config_registry::ReceptorConfigRegistry; use tauri_plugin_holochain::{HolochainExt, AppBundle}; use async_trait::async_trait; use crate::setup::window_setup::ProviderWindowSetup; @@ -55,9 +53,8 @@ impl HolochainSetup { let app_ws = handle.holochain()?.app_websocket(app_id.clone()).await?; tracing::debug!("[HOLOCHAIN SETUP] App websocket obtained."); - // After successful setup, build and register the receptor - let (receptor_cfg, client) = Self::build_receptor(app_ws, admin_ws, hc_cfg).await?; - Self::register_receptor(&handle, receptor_cfg).await?; + // Build the conductor client for Runtime construction + let client = Self::build_conductor_client(app_ws, admin_ws, hc_cfg).await?; // Store the conductor client for Runtime construction if let Some(state) = handle.try_state::() { @@ -128,60 +125,20 @@ impl HolochainSetup { Ok(()) } - async fn build_receptor( + async fn build_conductor_client( app_ws: AppWebsocket, admin_ws: AdminWebsocket, hc_cfg: &HolochainConfig, - ) -> anyhow::Result<(BaseReceptor, Arc)> { + ) -> anyhow::Result> { let agent = app_ws.my_pub_key.clone(); let cell_details = hc_cfg.cell_details.as_ref().ok_or_else(|| anyhow::anyhow!("cell_details missing in HolochainConfig"))?; if cell_details.is_empty() { return Err(anyhow::anyhow!("cell_details is empty in HolochainConfig")); } let client = Self::setup_holochain_client(app_ws.clone(), admin_ws.clone(), cell_details[0].clone(), agent).await; - - // Dynamically collect all properties from HolochainConfig - let props = match serde_json::to_value(hc_cfg)? { - serde_json::Value::Object(map) => { - map.into_iter() - .map(|(k, v)| { - let value_str = match v { - serde_json::Value::String(s) => s, - serde_json::Value::Number(n) => n.to_string(), - serde_json::Value::Bool(b) => b.to_string(), - serde_json::Value::Null => String::new(), - _ => v.to_string(), - }; - (k, value_str) - }) - .collect::>() - } - _ => std::collections::HashMap::new(), - }; - - let receptor = BaseReceptor { - receptor_id: None, - receptor_type: "holochain".to_string(), - client_handler: Some(client.clone() as Arc), - properties: props, - }; - - Ok((receptor, client)) - } - - /// Initialize the receptor factory with websockets and load configuration - /// Register the built receptor config into the application state - async fn register_receptor( - handle: &AppHandle, - receptor_cfg: BaseReceptor, - ) -> anyhow::Result<()> { - // Get the registry from app state and register the new config - let registry = handle.state::(); - registry.register(receptor_cfg); - Ok(()) + Ok(client) } - //TODO: this should be done by the receptor setup code (basereceptor properties) and include ROLENAME, ZOMENAME etc pub async fn setup_holochain_client( app_ws: AppWebsocket, admin_ws: AdminWebsocket, diff --git a/host/conductora/src/setup/receptor_config_registry.rs b/host/conductora/src/setup/receptor_config_registry.rs deleted file mode 100644 index 26771c222..000000000 --- a/host/conductora/src/setup/receptor_config_registry.rs +++ /dev/null @@ -1,50 +0,0 @@ -use holons_client::shared_types::base_receptor::BaseReceptor; -use std::sync::Mutex; - -/// Registry for collecting `ReceptorConfig` entries from different setup modules -#[derive(Default)] -pub struct ReceptorConfigRegistry { - configs: Mutex>, -} - -impl ReceptorConfigRegistry { - /// Create an empty registry - pub fn new() -> Self { - Self { - configs: Mutex::new(Vec::new()) - } - } - - /// Register a receptor config - pub fn register(&self, config: BaseReceptor) { - self.configs.lock().unwrap().push(config); - } - - /// Retrieve all registered receptor configs - pub fn all(&self) -> Vec { - let mut configs = self.configs.lock().unwrap().clone(); - Self::ensure_local_receptor_first(&mut configs); - configs - } - - /// Ensure the local receptor is first in the vector for priority processing - fn ensure_local_receptor_first(configs: &mut Vec) { - if configs.is_empty() { - return; - } - // Find the index of the local receptor - let local_index = configs.iter().position(|config| { - config.receptor_type == "local" - }); - - // If found and not already first, move it to the front - if let Some(index) = local_index { - if index != 0 { - let local_config = configs.remove(index); - configs.insert(0, local_config); - } - } else { - tracing::error!("[REGISTRY] No local receptor found in configs"); - } - } -} \ No newline at end of file diff --git a/host/crates/holochain_receptor/Cargo.toml b/host/crates/holochain_receptor/Cargo.toml index 57e94700b..7f21bcd89 100644 --- a/host/crates/holochain_receptor/Cargo.toml +++ b/host/crates/holochain_receptor/Cargo.toml @@ -20,7 +20,5 @@ holons_core = { path = "../../../shared_crates/holons_core" } holons_boundary = { path = "../../../shared_crates/holons_boundary" } holons_client = { path = "../holons_client" } holons_trust_channel = { path = "../../../shared_crates/holons_trust_channel" } -holons_loader_client = { path = "../holons_loader_client" } core_types = { path = "../../../shared_crates/type_system/core_types" } base_types = { path = "../../../shared_crates/type_system/base_types" } -type_names = { path = "../../../shared_crates/type_system/type_names" } diff --git a/host/crates/holochain_receptor/src/holochain_receptor.rs b/host/crates/holochain_receptor/src/holochain_receptor.rs deleted file mode 100644 index bdd71a692..000000000 --- a/host/crates/holochain_receptor/src/holochain_receptor.rs +++ /dev/null @@ -1,250 +0,0 @@ -use std::collections::HashMap; -use std::fmt; -use std::fmt::Debug; -use std::sync::Arc; - -use async_trait::async_trait; - -use crate::holochain_conductor_client::HolochainConductorClient; -use base_types::MapString; -use core_types::HolonError; -use holons_client::{ - dances_client::ClientDanceBuilder, - init_client_context, - shared_types::{ - base_receptor::{BaseReceptor, ReceptorBehavior}, - holon_space::{HolonSpace, SpaceInfo}, - map_request::{MapRequest, MapRequestBody}, - map_response::MapResponse, - }, -}; -use holons_core::core_shared_objects::transactions::TransactionContext; -use holons_core::dances::{DanceInitiator, DanceResponse, ResponseBody, ResponseStatusCode}; -use holons_core::reference_layer::HolonReference; -use holons_loader_client::load_holons_from_files; -use holons_trust_channel::TrustChannel; - -/// POC-safe Holochain Receptor. -/// Enough to satisfy Conductora runtime configuration. -/// Does NOT implement full space loading / root holon discovery yet. -pub struct HolochainReceptor { - receptor_id: Option, - receptor_type: String, - properties: HashMap, - context: Arc, - client_handler: Arc, - _home_space_holon: HolonSpace, -} - -impl HolochainReceptor { - fn is_commit_dance_request(request_name: &str) -> bool { - matches!(request_name, "commit") - } - - fn is_read_only_request(request_name: &str) -> bool { - matches!(request_name, "get_all_holons" | "get_holon_by_id" | "query_relationships") - } - - pub fn new(base: BaseReceptor) -> Self { - // Downcast the stored client into our concrete conductor client - let client_any = - base.client_handler.as_ref().expect("Client is required for HolochainReceptor").clone(); - - let client_handler = client_any - .downcast::() - .expect("Failed to downcast client to HolochainConductorClient"); - - // Trust channel wraps the conductor client - let trust_channel = TrustChannel::new(client_handler.clone()); - let initiator: Arc = Arc::new(trust_channel); - - // Build client context with dance initiator - let context = init_client_context(Some(initiator)); - - // Default until we fully implement space discovery - let _home_space_holon = HolonSpace::default(); - - Self { - receptor_id: base.receptor_id.clone(), - receptor_type: base.receptor_type.clone(), - properties: base.properties.clone(), - context, - client_handler, - _home_space_holon, - } - } -} - -#[async_trait] -impl ReceptorBehavior for HolochainReceptor { - fn transaction_context(&self) -> Arc { - Arc::clone(&self.context) - } - - /// Core request β†’ client dance pipeline - async fn handle_map_request(&self, request: MapRequest) -> Result { - // Temporary Phase 1.4/1.5 bridge: commit-like requests serialize host - // ingress here. In Phase 2 this moves to CommandDispatcher. - if Self::is_commit_dance_request(request.name.as_str()) { - let _commit_guard = self.context.begin_host_commit_ingress_guard()?; - // Preserve request-shape validation before routing to context-owned commit execution. - let _validated_request = ClientDanceBuilder::validate_and_execute(&self.context, &request)?; - let response_reference = self.context.commit()?; - let dance_response = DanceResponse::new( - ResponseStatusCode::OK, - MapString("Commit executed via TransactionContext".to_string()), - ResponseBody::HolonReference(HolonReference::Transient(response_reference)), - None, - ); - - return Ok(MapResponse::new_from_dance_response(request.space.id, dance_response)); - } - - // Temporary Phase 1.4/1.5 bridge: load+commit is commit-like and - // should serialize at host ingress until CommandDispatcher owns this. - if request.name == "load_holons" { - let _commit_guard = self.context.begin_host_commit_ingress_guard()?; - - let content_set = match request.body { - MapRequestBody::LoadHolons(content_set) => content_set, - _ => { - return Err(HolonError::InvalidParameter( - "Expected LoadHolons body for load_holons request".into(), - )) - } - }; - - let response_reference = load_holons_from_files(self.context.clone(), content_set).await?; - tracing::info!( - "HolochainReceptor: loaded holons with reference: {:?}", - response_reference - ); - - let dance_response = DanceResponse::new( - ResponseStatusCode::OK, - MapString("LoadHolons executed via TransactionContext".to_string()), - ResponseBody::HolonReference(HolonReference::Transient(response_reference)), - None, - ); - - return Ok(MapResponse::new_from_dance_response(request.space.id, dance_response)); - } - - // Read/query requests remain available during host commit ingress and after - // lifecycle reaches Committed so clients can inspect commit/load results. - // External write/mutation requests (including transient creation) require - // an open transaction and must be blocked during host commit ingress. - let is_read_only = Self::is_read_only_request(request.name.as_str()); - if !is_read_only { - self.context.ensure_host_mutation_entry_allowed()?; - } - - let dance_request = ClientDanceBuilder::validate_and_execute(&self.context, &request)?; - let dance_response = self - .context - .initiate_ingress_dance(dance_request, is_read_only) - .await?; - - Ok(MapResponse::new_from_dance_response(request.space.id, dance_response)) - } - - /// POC stub for system info - async fn get_space_info(&self) -> Result { - // Call stubbed conductor client - self.client_handler.get_all_spaces().await - } -} - -impl Debug for HolochainReceptor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("HolochainReceptor") - .field("receptor_id", &self.receptor_id) - .field("receptor_type", &self.receptor_type) - .field("properties", &self.properties) - .finish() - } -} - -#[cfg(test)] -mod tests { - use super::HolochainReceptor; - use base_types::{BaseValue, MapString}; - use core_types::{PropertyMap, PropertyName}; - use holons_client::{ - dances_client::ClientDanceBuilder, - init_client_context, - shared_types::{ - holon_space::HolonSpace, - map_request::{MapRequest, MapRequestBody}, - }, - }; - use holons_core::dances::DanceType; - - #[test] - fn commit_route_classification_is_exact() { - assert!(HolochainReceptor::is_commit_dance_request("commit")); - assert!(!HolochainReceptor::is_commit_dance_request("get_all_holons")); - assert!(!HolochainReceptor::is_commit_dance_request("load_holons")); - } - - #[test] - fn read_only_route_classification_includes_supported_reads() { - assert!(HolochainReceptor::is_read_only_request("get_all_holons")); - assert!(HolochainReceptor::is_read_only_request("get_holon_by_id")); - assert!(HolochainReceptor::is_read_only_request("query_relationships")); - } - - #[test] - fn read_only_route_classification_excludes_mutations() { - assert!(!HolochainReceptor::is_read_only_request("commit")); - assert!(!HolochainReceptor::is_read_only_request("create_new_holon")); - assert!(!HolochainReceptor::is_read_only_request("stage_new_holon")); - assert!(!HolochainReceptor::is_read_only_request("load_holons")); - } - - #[test] - fn host_mutation_precheck_blocks_create_new_holon_before_builder_side_effects() { - let context = init_client_context(None); - let _guard = context.begin_host_commit_ingress_guard().expect("guard should acquire"); - - let before = context.lookup().transient_count().expect("count should succeed"); - - let mut props = PropertyMap::new(); - props.insert( - PropertyName(MapString("key".to_string())), - BaseValue::StringValue(MapString("PRECHECK_BLOCK".to_string())), - ); - - let request = MapRequest { - name: "create_new_holon".to_string(), - req_type: DanceType::Standalone, - body: MapRequestBody::ParameterValues(props), - space: HolonSpace::default(), - }; - - let is_read_only = HolochainReceptor::is_read_only_request(request.name.as_str()); - assert!(!is_read_only); - - // New receptor ordering: precheck before request build. - let err = context - .ensure_host_mutation_entry_allowed() - .expect_err("host mutation precheck should reject during commit ingress"); - let msg = format!("{err:?}"); - assert!( - msg.contains("TransactionCommitInProgress"), - "expected TransactionCommitInProgress, got {msg}" - ); - - // Ensure request builder was not run and no transient was created as a side effect. - let after = context.lookup().transient_count().expect("count should succeed"); - assert_eq!(before, after, "transient pool must remain unchanged"); - - // Sanity: builder remains side-effecting for create_new_holon if called directly. - let _ = ClientDanceBuilder::validate_and_execute(&context, &request); - let after_builder = context.lookup().transient_count().expect("count should succeed"); - assert!( - after_builder > after, - "direct builder call should still create transient side effect" - ); - } -} diff --git a/host/crates/holochain_receptor/src/lib.rs b/host/crates/holochain_receptor/src/lib.rs index a3d852217..afaee4c6d 100644 --- a/host/crates/holochain_receptor/src/lib.rs +++ b/host/crates/holochain_receptor/src/lib.rs @@ -1,7 +1,4 @@ mod conductor_dance_caller; pub mod holochain_conductor_client; -pub mod holochain_receptor; -// Re-export key types and traits for external use pub use holochain_conductor_client::HolochainConductorClient; -pub use holochain_receptor::HolochainReceptor; diff --git a/host/crates/holons_client/Cargo.toml b/host/crates/holons_client/Cargo.toml index 8d139c902..44f8ac174 100644 --- a/host/crates/holons_client/Cargo.toml +++ b/host/crates/holons_client/Cargo.toml @@ -10,7 +10,6 @@ name = "holons_client" [dependencies] serde = { version = "1", features = ["derive"] } serde_bytes = "*" -async-trait = "0.1" #tracing = "0.1" futures-executor = "0.3" tokio = { version = "1", features = ["rt", "rt-multi-thread"] } diff --git a/host/crates/holons_client/src/dances_client/client_dance_builder.rs b/host/crates/holons_client/src/dances_client/client_dance_builder.rs deleted file mode 100644 index 208396b02..000000000 --- a/host/crates/holons_client/src/dances_client/client_dance_builder.rs +++ /dev/null @@ -1,243 +0,0 @@ -use crate::shared_types::map_request::{MapRequest, MapRequestBody}; -use base_types::BaseValue::StringValue; -use base_types::MapString; -use core_types::{HolonError, PropertyMap, PropertyName}; -use holon_dance_builders::{ - build_commit_dance_request, build_get_all_holons_dance_request, - build_get_holon_by_id_dance_request, build_with_properties_dance_request, - stage_new_holon_dance::build_stage_new_holon_dance_request, -}; -use holons_core::core_shared_objects::transactions::TransactionContext; -use holons_core::dances::DanceRequest; -use holons_core::HolonReference; -use std::sync::Arc; - -pub struct ClientDanceBuilder; - -const PERMITTED_OPS: &[&str] = &[ - "abandon_staged_changes", - "add_related_holons", - "create_new_holon", - "commit", - "delete_holon", - "get_all_holons", - "get_holon_by_id", - "load_core_schema", - "load_holons", - "query_relationships", - "remove_properties", - "remove_related_holons", - "stage_new_from_clone", - "stage_new_holon", - "stage_new_version", - "with_properties", -]; - -impl ClientDanceBuilder { - pub fn permitted_operations() -> Vec<&'static str> { - PERMITTED_OPS.to_vec() - } - - pub fn validate_and_execute( - context: &Arc, - request: &MapRequest, - ) -> Result { - Self::validate_request(request)?; - - match request.name.as_str() { - "abandon_staged_changes" => Self::abandon_staged_changes_dance(context, request), - "add_related_holons" => Self::add_related_holons_dance(context, request), - "commit" => Self::commit_dance(context, request), - "create_new_holon" => Self::create_new_holon_dance(context, request), - "delete_holon" => Self::delete_holon_dance(context, request), - "get_all_holons" => Self::get_all_holons_dance(), - "get_holon_by_id" => Self::get_holon_by_id_dance(context, request), - "load_core_schema" => Self::load_core_schema_dance(context, request), - "load_holons" => Self::load_holons_dance(context, request), - "query_relationships" => Self::query_relationships_dance(context, request), - "remove_properties" => Self::remove_properties_dance(context, request), - "remove_related_holons" => Self::remove_related_holons_dance(context, request), - "stage_new_from_clone" => Self::stage_new_from_clone_dance(context, request), - "stage_new_holon" => Self::stage_new_holon_dance(context, request), - "stage_new_version" => Self::stage_new_version_dance(context, request), - "with_properties" => Self::with_properties_dance(context, request), - _ => Err(HolonError::NotImplemented(format!("Operation '{}' not found", request.name))), - } - } - - pub fn validate_request(request: &MapRequest) -> Result<(), HolonError> { - if PERMITTED_OPS.contains(&request.name.as_str()) { - Ok(()) - } else { - Err(HolonError::NotImplemented(format!( - "Operation '{}' is not permitted. Allowed operations: {}", - request.name, - PERMITTED_OPS.join(", ") - ))) - } - } - pub fn abandon_staged_changes_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - todo!() - } - - pub fn add_related_holons_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - todo!() - } - - pub fn commit_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - return build_commit_dance_request(); - } - pub fn delete_holon_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - todo!() - } - pub fn get_all_holons_dance(//_context: &Arc, - //_request: &MapRequest, - ) -> Result { - return build_get_all_holons_dance_request(); - } - pub fn get_holon_by_id_dance( - _context: &Arc, - request: &MapRequest, - ) -> Result { - match &request.body { - MapRequestBody::HolonId(holon_id) => { - return build_get_holon_by_id_dance_request(holon_id.clone()) - } - _ => { - return Err(HolonError::InvalidParameter( - "Missing HolonId in request body for get_holon_by_id".into(), - )) - } - } - } - pub fn load_core_schema_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - todo!() - } - - pub fn load_holons_dance( - _context: &Arc, - request: &MapRequest, - ) -> Result { - match &request.body { - MapRequestBody::TransientReference(transient_ref) => { - return holon_dance_builders::load_holons_dance::build_load_holons_dance_request( - transient_ref.clone(), - ); - } - _ => { - return Err(HolonError::InvalidParameter( - "Missing Content data in request body for load_holons".into(), - )) - } - } - } - - pub fn query_relationships_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - todo!() - } - pub fn remove_properties_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - todo!() - } - pub fn remove_related_holons_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - todo!() - } - pub fn stage_new_from_clone_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - todo!() - } - - pub fn create_new_holon_dance( - context: &Arc, - request: &MapRequest, - ) -> Result { - match &request.body { - MapRequestBody::ParameterValues(props) => { - let key = Self::extract_holon_key(&props)?; - let transient_ref = context.mutation().new_holon(Some(key))?; - return build_with_properties_dance_request( - HolonReference::from(transient_ref.clone()), - props.clone(), - ); - } - _ => { - return Err(HolonError::InvalidParameter( - "Missing holon parameters for create_new_holon".into(), - )) - } - } - } - - pub fn stage_new_holon_dance( - _context: &Arc, - request: &MapRequest, - ) -> Result { - match &request.body { - MapRequestBody::TransientReference(reference) => { - return build_stage_new_holon_dance_request(reference.clone()); - } - _ => { - return Err(HolonError::InvalidParameter( - "Missing holon reference for stage_new_holon".into(), - )) - } - } - } - - pub fn stage_new_version_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - todo!() - } - pub fn with_properties_dance( - _context: &Arc, - _request: &MapRequest, - ) -> Result { - todo!() - } - - //helpers - - fn extract_holon_key(props: &PropertyMap) -> Result { - let key_property = props.get(&PropertyName(MapString("key".to_string()))); - - // Convert PropertyValue to MapString - let key = match key_property { - Some(StringValue(map_string)) => map_string, - Some(other) => { - return Err(HolonError::InvalidParameter(format!( - "Expected StringValue for key, got: {:?}", - other - ))) - } - None => return Err(HolonError::HolonNotFound("Key property not found".into())), - }; - Ok(key.clone()) - } -} diff --git a/host/crates/holons_client/src/dances_client/mod.rs b/host/crates/holons_client/src/dances_client/mod.rs deleted file mode 100644 index 10164fe78..000000000 --- a/host/crates/holons_client/src/dances_client/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod client_dance_builder; -pub use client_dance_builder::ClientDanceBuilder; diff --git a/host/crates/holons_client/src/lib.rs b/host/crates/holons_client/src/lib.rs index 3867913ef..c134ac1ca 100644 --- a/host/crates/holons_client/src/lib.rs +++ b/host/crates/holons_client/src/lib.rs @@ -1,6 +1,5 @@ pub mod client_context; pub mod client_shared_objects; -pub mod dances_client; pub mod shared_types; pub use client_context::{init_client_context, init_client_runtime}; diff --git a/host/crates/holons_client/src/shared_types/base_receptor.rs b/host/crates/holons_client/src/shared_types/base_receptor.rs deleted file mode 100644 index ccbd9e89c..000000000 --- a/host/crates/holons_client/src/shared_types/base_receptor.rs +++ /dev/null @@ -1,23 +0,0 @@ -use async_trait::async_trait; -use core_types::{HolonError}; -use std::{any::Any, fmt::Debug, sync::Arc}; -use crate::shared_types::{holon_space::SpaceInfo, map_request::MapRequest, map_response::MapResponse}; -use holons_core::core_shared_objects::transactions::TransactionContext; -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; - -#[async_trait] -pub trait ReceptorBehavior: Debug + Send + Sync { - fn transaction_context(&self) -> Arc; - async fn handle_map_request(&self, request: MapRequest) -> Result; - async fn get_space_info(&self) -> Result; -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct BaseReceptor { - pub receptor_id: Option, - pub receptor_type: String, - #[serde(skip, default)] - pub client_handler: Option>, - pub properties: HashMap, -} diff --git a/host/crates/holons_client/src/shared_types/map_request.rs b/host/crates/holons_client/src/shared_types/map_request.rs deleted file mode 100644 index 98a9c6590..000000000 --- a/host/crates/holons_client/src/shared_types/map_request.rs +++ /dev/null @@ -1,208 +0,0 @@ -use crate::shared_types::holon_space::HolonSpace; -use core_types::{ContentSet, HolonError, HolonId, PropertyMap, RelationshipName}; -use holons_boundary::{ - DanceTypeWire, HolonReferenceWire, HolonWire, StagedReferenceWire, TransientReferenceWire, -}; -use holons_core::{ - core_shared_objects::{transactions::TransactionContext, Holon}, - dances::DanceType, - query_layer::QueryExpression, - reference_layer::TransientReference, - HolonReference, StagedReference, -}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -/// Runtime request body (may contain tx-bound references). -/// -/// This type must not be deserialized across IPC boundaries because it may contain -/// tx-bound references. Use `MapRequestBodyWire` for IPC and call `bind(context)` at ingress. -#[derive(Debug, Clone, PartialEq)] -pub enum MapRequestBody { - None, - Holon(Holon), - TargetHolons(RelationshipName, Vec), - TransientReference(TransientReference), - HolonId(HolonId), - ParameterValues(PropertyMap), - StagedRef(StagedReference), - QueryExpression(QueryExpression), - LoadHolons(ContentSet), -} - -/// IPC-safe wire-form request body. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub enum MapRequestBodyWire { - None, - Holon(HolonWire), - TargetHolons(RelationshipName, Vec), - TransientReference(TransientReferenceWire), - HolonId(HolonId), - ParameterValues(PropertyMap), - StagedRef(StagedReferenceWire), - QueryExpression(QueryExpression), - LoadHolons(ContentSet), -} - -impl MapRequestBody { - pub fn new() -> Self { - Self::None // Assuming 'None' is the default variant - } - - pub fn new_holon(holon: Holon) -> Self { - Self::Holon(holon) - } - - pub fn new_parameter_values(parameters: PropertyMap) -> Self { - Self::ParameterValues(parameters) - } - - pub fn new_target_holons( - relationship_name: RelationshipName, - holons_to_add: Vec, - ) -> Self { - Self::TargetHolons(relationship_name, holons_to_add) - } - - pub fn new_staged_reference(staged_reference: StagedReference) -> Self { - Self::StagedRef(staged_reference) - } - - pub fn new_query_expression(query_expression: QueryExpression) -> Self { - Self::QueryExpression(query_expression) - } - pub fn new_load_holons(content_set: ContentSet) -> Self { - Self::LoadHolons(content_set) - } -} - -#[derive(Debug, Clone)] -pub struct MapRequest { - pub name: String, // unique key within the (single) dispatch table - pub req_type: DanceType, - pub body: MapRequestBody, - pub space: HolonSpace, -} - -/// IPC-safe wire-form map request. -/// -/// This is the context-free shape that may be decoded at IPC boundaries. -/// Convert to runtime via `bind(context)`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct MapRequestWire { - pub name: String, // unique key within the (single) dispatch table - pub req_type: DanceTypeWire, - pub body: MapRequestBodyWire, - pub space: HolonSpace, -} - -impl MapRequest { - pub fn new_for_reference(reference: TransientReference) -> Self { - let name = "get_all_holons".to_string(); - let req_type = DanceType::Standalone; - let body = MapRequestBody::TransientReference(reference); - let space = HolonSpace::default(); - Self { name, req_type, body, space } - } -} - -impl MapRequestWire { - // --------------------------------------------------------------------- - // Binding - // --------------------------------------------------------------------- - - /// Binds a wire request to the supplied transaction, validating `tx_id` - /// for all embedded references and producing a runtime `MapRequest`. - pub fn bind(self, context: &Arc) -> Result { - Ok(MapRequest { - name: self.name, - req_type: self.req_type.bind(context)?, - body: self.body.bind(context)?, - space: self.space, - }) - } -} - -impl MapRequestBodyWire { - pub fn bind(self, context: &Arc) -> Result { - match self { - MapRequestBodyWire::None => Ok(MapRequestBody::None), - MapRequestBodyWire::Holon(holon_wire) => { - Ok(MapRequestBody::Holon(holon_wire.bind(context)?)) - } - MapRequestBodyWire::TargetHolons(name, wires) => { - let mut refs = Vec::with_capacity(wires.len()); - for wire in wires { - refs.push(wire.bind(context)?); - } - Ok(MapRequestBody::TargetHolons(name, refs)) - } - MapRequestBodyWire::TransientReference(wire) => { - Ok(MapRequestBody::TransientReference(wire.bind(context)?)) - } - MapRequestBodyWire::HolonId(id) => Ok(MapRequestBody::HolonId(id)), - MapRequestBodyWire::ParameterValues(values) => { - Ok(MapRequestBody::ParameterValues(values)) - } - MapRequestBodyWire::StagedRef(wire) => { - Ok(MapRequestBody::StagedRef(wire.bind(context)?)) - } - MapRequestBodyWire::QueryExpression(query) => { - Ok(MapRequestBody::QueryExpression(query)) - } - MapRequestBodyWire::LoadHolons(content_set) => { - Ok(MapRequestBody::LoadHolons(content_set)) - } - } - } -} - -impl From<&MapRequest> for MapRequestWire { - fn from(request: &MapRequest) -> Self { - Self { - name: request.name.clone(), - req_type: DanceTypeWire::from(&request.req_type), - body: MapRequestBodyWire::from(&request.body), - space: request.space.clone(), - } - } -} - -impl From<&MapRequestBody> for MapRequestBodyWire { - fn from(body: &MapRequestBody) -> Self { - match body { - MapRequestBody::None => MapRequestBodyWire::None, - MapRequestBody::Holon(holon) => MapRequestBodyWire::Holon(HolonWire::from(holon)), - MapRequestBody::TargetHolons(name, holons) => MapRequestBodyWire::TargetHolons( - name.clone(), - holons.iter().map(HolonReferenceWire::from).collect(), - ), - MapRequestBody::TransientReference(reference) => { - MapRequestBodyWire::TransientReference(TransientReferenceWire::from(reference)) - } - MapRequestBody::HolonId(id) => MapRequestBodyWire::HolonId(id.clone()), - MapRequestBody::ParameterValues(values) => { - MapRequestBodyWire::ParameterValues(values.clone()) - } - MapRequestBody::StagedRef(reference) => { - MapRequestBodyWire::StagedRef(StagedReferenceWire::from(reference)) - } - MapRequestBody::QueryExpression(query) => { - MapRequestBodyWire::QueryExpression(query.clone()) - } - MapRequestBody::LoadHolons(content_set) => { - MapRequestBodyWire::LoadHolons(content_set.clone()) - } - } - } -} - -/* let holon = TransientHolon::with_fields( - MapInteger(1), - HolonState::Mutable, - ValidationState::ValidationRequired, - // None, - property_map, - TransientRelationshipMap::new_empty(), - None, -); */ diff --git a/host/crates/holons_client/src/shared_types/map_response.rs b/host/crates/holons_client/src/shared_types/map_response.rs deleted file mode 100644 index fdef2ba05..000000000 --- a/host/crates/holons_client/src/shared_types/map_response.rs +++ /dev/null @@ -1,77 +0,0 @@ -use holons_boundary::{HolonReferenceWire, ResponseBodyWire}; -use holons_core::{ - core_shared_objects::transactions::TransactionContext, - dances::{DanceResponse, ResponseBody, ResponseStatusCode}, - HolonError, HolonReference, -}; -use holons_boundary::session_state::SessionStateWire; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -#[derive(Debug, Clone)] -pub struct MapResponse { - pub space_id: String, - pub status_code: ResponseStatusCode, - pub description: String, - pub body: ResponseBody, - pub descriptor: Option, // space_id+holon_id of DanceDescriptor - pub state: Option, -} - -/// IPC-safe wire-form map response. -/// -/// This is the context-free shape that may be decoded at IPC boundaries. -/// Convert to runtime via `bind(context)`. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct MapResponseWire { - pub space_id: String, - pub status_code: ResponseStatusCode, - pub description: String, - pub body: ResponseBodyWire, - pub descriptor: Option, // space_id+holon_id of DanceDescriptor - pub state: Option, -} - -impl MapResponse { - pub fn new_from_dance_response(space_id: String, dance_response: DanceResponse) -> Self { - Self { - space_id, - status_code: dance_response.status_code, - description: dance_response.description.to_string(), - body: dance_response.body, - descriptor: dance_response.descriptor, - state: None, - } - } -} - -impl MapResponseWire { - /// Binds a wire response to the supplied transaction, validating `tx_id` - /// for all embedded references. - pub fn bind(self, context: &Arc) -> Result { - Ok(MapResponse { - space_id: self.space_id, - status_code: self.status_code, - description: self.description, - body: self.body.bind(context)?, - descriptor: match self.descriptor { - Some(reference_wire) => Some(reference_wire.bind(context)?), - None => None, - }, - state: self.state, - }) - } -} - -impl From<&MapResponse> for MapResponseWire { - fn from(response: &MapResponse) -> Self { - Self { - space_id: response.space_id.clone(), - status_code: response.status_code.clone(), - description: response.description.clone(), - body: ResponseBodyWire::from(&response.body), - descriptor: response.descriptor.as_ref().map(HolonReferenceWire::from), - state: response.state.clone(), - } - } -} diff --git a/host/crates/holons_client/src/shared_types/mod.rs b/host/crates/holons_client/src/shared_types/mod.rs index 52dbcb540..e14ff884b 100644 --- a/host/crates/holons_client/src/shared_types/mod.rs +++ b/host/crates/holons_client/src/shared_types/mod.rs @@ -1,4 +1 @@ -pub mod map_request; -pub mod map_response; pub mod holon_space; -pub mod base_receptor; \ No newline at end of file diff --git a/host/crates/holons_loader_client/src/loader_client.rs b/host/crates/holons_loader_client/src/loader_client.rs index 7162c3ed2..c67bd90ee 100644 --- a/host/crates/holons_loader_client/src/loader_client.rs +++ b/host/crates/holons_loader_client/src/loader_client.rs @@ -1,7 +1,7 @@ //! Top-level entrypoint for the host-side Holons Loader Client. //! -//! This module is called from the native receptor layer -//! (`ReceptorFactory::load_holons(...)`) and orchestrates: +//! This module is called from `TransactionAction::LoadHolons` dispatch +//! (in the `map_commands` crate) and orchestrates: //! //! 1. Parsing & validation of one or more loader import files into a //! single `HolonLoadSet` via [`crate::parser`] and [`crate::builder`]. @@ -26,7 +26,7 @@ use tracing::debug; /// Primary entry point for the host-side Holon Loader. /// -/// This function is intended to be called from `ReceptorFactory::load_holons(...)`. +/// This function is called from `TransactionAction::LoadHolons` dispatch. /// /// High-level behavior: /// - If no input files are provided, returns `HolonError::InvalidParameter`. diff --git a/host/crates/holons_receptor/Cargo.toml b/host/crates/holons_receptor/Cargo.toml index 328e07dbd..e5bbc223c 100644 --- a/host/crates/holons_receptor/Cargo.toml +++ b/host/crates/holons_receptor/Cargo.toml @@ -2,7 +2,6 @@ name = "holons_receptor" version = "0.1.0" edition = "2021" -#edition = "2024" [dependencies] serde_bytes = "*" @@ -10,7 +9,7 @@ serde_json = "1" async-trait = "0.1" tracing = "0.1" sha2 = "0.10" -hex = "0.4" +hex = "0.4" #map dependencies diff --git a/host/crates/holons_receptor/src/cache.rs b/host/crates/holons_receptor/src/cache.rs index 0991e910a..65d64f63b 100644 --- a/host/crates/holons_receptor/src/cache.rs +++ b/host/crates/holons_receptor/src/cache.rs @@ -1,89 +1,89 @@ -use holons_client::shared_types::base_receptor::ReceptorBehavior; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ReceptorKey { - pub receptor_type: String, - pub receptor_id: String, -} - -impl ReceptorKey { - pub fn new(receptor_type: String, receptor_id: String) -> Self { - Self { receptor_type, receptor_id } - } -} - -// Updated to be thread-safe -#[derive(Clone)] -pub struct ReceptorCache { - cache: Arc>>>, -} - -impl ReceptorCache { - pub fn new() -> Self { - Self { cache: Arc::new(Mutex::new(HashMap::new())) } - } - - pub fn get(&self, key: &ReceptorKey) -> Option> { - self.cache.lock().unwrap().get(key).cloned() - } - - pub fn get_by_type(&self, receptor_type: &str) -> Vec> { - let cache = self.cache.lock().unwrap(); - cache - .iter() - .filter_map(|(key, receptor)| { - if key.receptor_type == receptor_type { - Some(receptor.clone()) - } else { - None - } - }) - .collect() - } - - pub fn insert(&self, key: ReceptorKey, receptor: Arc) { - let mut cache = self.cache.lock().unwrap(); - cache.insert(key, receptor); - tracing::debug!("Cached receptor. Total cached: {}", cache.len()); - } - - pub fn remove(&self, key: &ReceptorKey) -> Option> { - self.cache.lock().unwrap().remove(key) - } - - pub fn clear(&self) { - self.cache.lock().unwrap().clear(); - tracing::debug!("Receptor cache cleared"); - } - - pub fn len(&self) -> usize { - self.cache.lock().unwrap().len() - } - - pub fn is_empty(&self) -> bool { - self.cache.lock().unwrap().is_empty() - } - - // probably remove, Check if a receptor exists for the given space - // pub fn has_receptor_for_space(&self, space_id: &String) -> bool { - // let key = ReceptorKey::from_space_holon(space_id); - // self.cache.lock().unwrap().contains_key(&key) - // } -} - -// Custom Debug implementation since Mutex doesn't derive Debug easily -impl std::fmt::Debug for ReceptorCache { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ReceptorCache") - .field("cache_size", &self.cache.lock().unwrap().len()) - .finish() - } -} - -impl Default for ReceptorCache { - fn default() -> Self { - Self::new() - } -} +// use holons_client::shared_types::base_receptor::ReceptorBehavior; +// use std::collections::HashMap; +// use std::sync::{Arc, Mutex}; +// +// #[derive(Debug, Clone, PartialEq, Eq, Hash)] +// pub struct ReceptorKey { +// pub receptor_type: String, +// pub receptor_id: String, +// } +// +// impl ReceptorKey { +// pub fn new(receptor_type: String, receptor_id: String) -> Self { +// Self { receptor_type, receptor_id } +// } +// } +// +// // Updated to be thread-safe +// #[derive(Clone)] +// pub struct ReceptorCache { +// cache: Arc>>>, +// } +// +// impl ReceptorCache { +// pub fn new() -> Self { +// Self { cache: Arc::new(Mutex::new(HashMap::new())) } +// } +// +// pub fn get(&self, key: &ReceptorKey) -> Option> { +// self.cache.lock().unwrap().get(key).cloned() +// } +// +// pub fn get_by_type(&self, receptor_type: &str) -> Vec> { +// let cache = self.cache.lock().unwrap(); +// cache +// .iter() +// .filter_map(|(key, receptor)| { +// if key.receptor_type == receptor_type { +// Some(receptor.clone()) +// } else { +// None +// } +// }) +// .collect() +// } +// +// pub fn insert(&self, key: ReceptorKey, receptor: Arc) { +// let mut cache = self.cache.lock().unwrap(); +// cache.insert(key, receptor); +// tracing::debug!("Cached receptor. Total cached: {}", cache.len()); +// } +// +// pub fn remove(&self, key: &ReceptorKey) -> Option> { +// self.cache.lock().unwrap().remove(key) +// } +// +// pub fn clear(&self) { +// self.cache.lock().unwrap().clear(); +// tracing::debug!("Receptor cache cleared"); +// } +// +// pub fn len(&self) -> usize { +// self.cache.lock().unwrap().len() +// } +// +// pub fn is_empty(&self) -> bool { +// self.cache.lock().unwrap().is_empty() +// } +// +// // probably remove, Check if a receptor exists for the given space +// // pub fn has_receptor_for_space(&self, space_id: &String) -> bool { +// // let key = ReceptorKey::from_space_holon(space_id); +// // self.cache.lock().unwrap().contains_key(&key) +// // } +// } +// +// // Custom Debug implementation since Mutex doesn't derive Debug easily +// impl std::fmt::Debug for ReceptorCache { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// f.debug_struct("ReceptorCache") +// .field("cache_size", &self.cache.lock().unwrap().len()) +// .finish() +// } +// } +// +// impl Default for ReceptorCache { +// fn default() -> Self { +// Self::new() +// } +// } diff --git a/host/crates/holons_receptor/src/factory.rs b/host/crates/holons_receptor/src/factory.rs index 08c4f86cf..ea3d65815 100644 --- a/host/crates/holons_receptor/src/factory.rs +++ b/host/crates/holons_receptor/src/factory.rs @@ -1,135 +1,135 @@ -use super::{ - local_receptor::LocalReceptor, - cache::{ReceptorCache, ReceptorKey} -}; -use core_types::HolonError; -use holochain_receptor::{HolochainReceptor}; -use holons_client::shared_types::{base_receptor::{BaseReceptor, ReceptorBehavior}, holon_space::SpaceInfo}; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::collections::HashMap; -use sha2::{Digest, Sha256}; -use hex; - -#[derive(Clone)] -pub struct ReceptorFactory { - cache: ReceptorCache, - is_loaded: Arc, -} - -impl ReceptorFactory { - pub fn new() -> Self { - Self { - cache: ReceptorCache::new(), - is_loaded: Arc::new(AtomicBool::new(false)), - } - } - - /// Check if receptors have been loaded - pub fn are_receptors_loaded(&self) -> bool { - self.is_loaded.load(Ordering::SeqCst) - } - - /// Create receptor from base configuration - async fn create_receptor_from_base(&self, base: BaseReceptor) -> Result, Box> { - match base.receptor_type.as_str() { - "local" => { //local should always be created first - tracing::info!("Creating LocalReceptor from base configuration"); - let receptor = LocalReceptor::new(base)?; - Ok(Arc::new(receptor) as Arc) - } - "holochain" => { - tracing::info!("Creating HolochainReceptor from base configuration"); - let receptor = HolochainReceptor::new(base); - Ok(Arc::new(receptor) as Arc) - } - _ => Err(format!("Unsupported receptor type: {}", base.receptor_type).into()) - } - } - - pub fn get_receptor_by_type(&self, receptor_type: &str) -> Arc { - let receptors = self.cache.get_by_type(receptor_type) - .into_iter().collect::>(); - if receptors.is_empty() { - panic!("No receptors found for type: {}", receptor_type); - } - receptors[0].clone() - } - - //function returns all spaces in the root space - pub fn get_root_spaces() -> Result { - todo!("Implement get_root_spaces to return all spaces in the root space") - } - - /// Clear cache (internal use only) - pub(crate) fn _clear_cache(&self) { - self.cache.clear(); - } - - /// Load receptors directly from a list of ReceptorConfig - pub async fn load_from_configs( - &self, - configs: Vec, - ) -> Result> { - // Clear existing cache - self.cache.clear(); - self.is_loaded.store(false, Ordering::SeqCst); - let mut count = 0; - for cfg in configs { - // Precompute key and cache the config - let id = generate_receptor_id(cfg.properties.clone())?; - let key = ReceptorKey::new(cfg.receptor_type.clone(), id); - // Cache the config for conductor lookup later - match self.create_receptor_from_base(cfg).await { - Ok(receptor) => { - self.cache.insert(key, receptor); - count += 1; - } - Err(e) => { - tracing::error!("Failed to create receptor from config: {}", e); - } - } - } - self.is_loaded.store(true, Ordering::SeqCst); - Ok(count) - } - - - pub async fn all_spaces_by_type(&self, rec_type: &str) -> Result { - for receptor in self.cache.get_by_type(rec_type) { - match rec_type { - "holochain" => return receptor.get_space_info().await, - _ => {} - } - } - Err(HolonError::NotImplemented("No conductor found for space".into())) - } - - // These methods only called for Holochain receptors - /* fn extract_cell_id_from_space(&self, space_id: &String) -> Result> { - // Implementation remains the same - todo!("Parse CellId from space holon property_map") - } - - fn extract_zome_name_from_space(&self, space_id: &String) -> Result> { - // Implementation remains the same - Ok("holons".to_string()) - } - */ -} - -//helpers -fn generate_receptor_id(props: HashMap) -> Result> { - let json = serde_json::to_string(&props)?; - Ok(hex::encode(Sha256::digest(json.as_bytes()))) -} - -// Add Debug implementation -impl std::fmt::Debug for ReceptorFactory { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ReceptorFactory") - .field("cache", &self.cache) - .field("is_loaded", &self.is_loaded.load(Ordering::SeqCst)) - .finish() - } -} +// use super::{ +// local_receptor::LocalReceptor, +// cache::{ReceptorCache, ReceptorKey} +// }; +// use core_types::HolonError; +// use holochain_receptor::{HolochainReceptor}; +// use holons_client::shared_types::{base_receptor::{BaseReceptor, ReceptorBehavior}, holon_space::SpaceInfo}; +// use std::sync::Arc; +// use std::sync::atomic::{AtomicBool, Ordering}; +// use std::collections::HashMap; +// use sha2::{Digest, Sha256}; +// use hex; +// +// #[derive(Clone)] +// pub struct ReceptorFactory { +// cache: ReceptorCache, +// is_loaded: Arc, +// } +// +// impl ReceptorFactory { +// pub fn new() -> Self { +// Self { +// cache: ReceptorCache::new(), +// is_loaded: Arc::new(AtomicBool::new(false)), +// } +// } +// +// /// Check if receptors have been loaded +// pub fn are_receptors_loaded(&self) -> bool { +// self.is_loaded.load(Ordering::SeqCst) +// } +// +// /// Create receptor from base configuration +// async fn create_receptor_from_base(&self, base: BaseReceptor) -> Result, Box> { +// match base.receptor_type.as_str() { +// "local" => { //local should always be created first +// tracing::info!("Creating LocalReceptor from base configuration"); +// let receptor = LocalReceptor::new(base)?; +// Ok(Arc::new(receptor) as Arc) +// } +// "holochain" => { +// tracing::info!("Creating HolochainReceptor from base configuration"); +// let receptor = HolochainReceptor::new(base); +// Ok(Arc::new(receptor) as Arc) +// } +// _ => Err(format!("Unsupported receptor type: {}", base.receptor_type).into()) +// } +// } +// +// pub fn get_receptor_by_type(&self, receptor_type: &str) -> Arc { +// let receptors = self.cache.get_by_type(receptor_type) +// .into_iter().collect::>(); +// if receptors.is_empty() { +// panic!("No receptors found for type: {}", receptor_type); +// } +// receptors[0].clone() +// } +// +// //function returns all spaces in the root space +// pub fn get_root_spaces() -> Result { +// todo!("Implement get_root_spaces to return all spaces in the root space") +// } +// +// /// Clear cache (internal use only) +// pub(crate) fn _clear_cache(&self) { +// self.cache.clear(); +// } +// +// /// Load receptors directly from a list of ReceptorConfig +// pub async fn load_from_configs( +// &self, +// configs: Vec, +// ) -> Result> { +// // Clear existing cache +// self.cache.clear(); +// self.is_loaded.store(false, Ordering::SeqCst); +// let mut count = 0; +// for cfg in configs { +// // Precompute key and cache the config +// let id = generate_receptor_id(cfg.properties.clone())?; +// let key = ReceptorKey::new(cfg.receptor_type.clone(), id); +// // Cache the config for conductor lookup later +// match self.create_receptor_from_base(cfg).await { +// Ok(receptor) => { +// self.cache.insert(key, receptor); +// count += 1; +// } +// Err(e) => { +// tracing::error!("Failed to create receptor from config: {}", e); +// } +// } +// } +// self.is_loaded.store(true, Ordering::SeqCst); +// Ok(count) +// } +// +// +// pub async fn all_spaces_by_type(&self, rec_type: &str) -> Result { +// for receptor in self.cache.get_by_type(rec_type) { +// match rec_type { +// "holochain" => return receptor.get_space_info().await, +// _ => {} +// } +// } +// Err(HolonError::NotImplemented("No conductor found for space".into())) +// } +// +// // These methods only called for Holochain receptors +// /* fn extract_cell_id_from_space(&self, space_id: &String) -> Result> { +// // Implementation remains the same +// todo!("Parse CellId from space holon property_map") +// } +// +// fn extract_zome_name_from_space(&self, space_id: &String) -> Result> { +// // Implementation remains the same +// Ok("holons".to_string()) +// } +// */ +// } +// +// //helpers +// fn generate_receptor_id(props: HashMap) -> Result> { +// let json = serde_json::to_string(&props)?; +// Ok(hex::encode(Sha256::digest(json.as_bytes()))) +// } +// +// // Add Debug implementation +// impl std::fmt::Debug for ReceptorFactory { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// f.debug_struct("ReceptorFactory") +// .field("cache", &self.cache) +// .field("is_loaded", &self.is_loaded.load(Ordering::SeqCst)) +// .finish() +// } +// } diff --git a/host/crates/holons_receptor/src/lib.rs b/host/crates/holons_receptor/src/lib.rs index 0b82ba1f6..594ea353d 100644 --- a/host/crates/holons_receptor/src/lib.rs +++ b/host/crates/holons_receptor/src/lib.rs @@ -1,8 +1,3 @@ -pub mod receptors; -pub use receptors::*; - -pub mod cache; -pub mod factory; - -pub use factory::ReceptorFactory; -// Don't export cache types - keep them internal +//! Deprecated: This crate is no longer used by the MAP Commands architecture. +//! Domain command dispatch now goes through `map_commands::dispatch::Runtime`. +//! This crate will be removed in a future cleanup pass. diff --git a/host/crates/holons_receptor/src/receptors/local_receptor/local_client.rs b/host/crates/holons_receptor/src/receptors/local_receptor/local_client.rs deleted file mode 100644 index 1ae63bdf5..000000000 --- a/host/crates/holons_receptor/src/receptors/local_receptor/local_client.rs +++ /dev/null @@ -1,38 +0,0 @@ -use core_types::HolonError; -use holons_client::shared_types::holon_space::{HolonSpace, SpaceInfo}; -use holons_core::core_shared_objects::transactions::TransactionContext; -use holons_core::core_shared_objects::SavedHolon; -use std::sync::Arc; - -#[derive(Debug, Clone)] -pub struct LocalClient; - -impl LocalClient { - pub fn new() -> Self { - Self {} - } - - pub fn load_core_schemas(&self) -> Result<(), HolonError> { - // Implement logic to load core schemas - todo!("Implement load_core_schemas to load core schemas into the local receptor"); - } - - pub fn fetch_or_create_root_holon( - &self, - _context: &Arc, - ) -> Result { - // Implement logic to check and create root holon if it doesn't exist - todo!( - "Implement fetch_or_create_root_holon to get or create root holon if it doesn't exist" - ); - //Ok(mock_root_holon) - } - pub async fn get_all_spaces(&self) -> Result { - Ok(SpaceInfo::default()) - } - - pub fn convert_to_holonspace(&self, _holon: SavedHolon) -> Result { - // Implement conversion logic from SavedHolon to SpaceInfo - todo!("Implement convert_to_space_info to convert SavedHolon to SpaceInfo"); - } -} diff --git a/host/crates/holons_receptor/src/receptors/local_receptor/local_receptor.rs b/host/crates/holons_receptor/src/receptors/local_receptor/local_receptor.rs deleted file mode 100644 index 1a88c8489..000000000 --- a/host/crates/holons_receptor/src/receptors/local_receptor/local_receptor.rs +++ /dev/null @@ -1,100 +0,0 @@ -use core_types::{HolonError}; -use holons_client::shared_types::holon_space::{HolonSpace, SpaceInfo}; -use holons_client::shared_types::base_receptor::{BaseReceptor, ReceptorBehavior}; -use holons_client::{ClientHolonService, init_client_context}; -use holons_client::shared_types::map_request::MapRequest; -use holons_client::shared_types::map_response::MapResponse; -use holons_core::HolonServiceApi; -use holons_core::core_shared_objects::transactions::TransactionContext; -use holons_core::dances::{ResponseBody, ResponseStatusCode}; -use std::collections::HashMap; -use std::fmt; -use std::sync::Arc; -use async_trait::async_trait; -use crate::{LocalClient}; - -pub const ROOT_SPACE_HOLON_PATH: &str = "root_holon_space"; -pub const ROOT_HOLON_SPACE_NAME: &str = "RootHolonSpace"; -pub const ROOT_HOLON_SPACE_DESCRIPTION: &str = "Default Root Holon Space"; - -pub struct LocalReceptor { - receptor_id: Option, - receptor_type: String, - properties: HashMap, - context: Arc, - client_handler: Arc, - _holon_service: Arc, - _root_space: HolonSpace, -} - -/// Implementation of LocalReceptor - local host level - no dancing -impl LocalReceptor { - /// Create a new LocalReceptor, returning Result to handle downcast failures - pub fn new(base_receptor: BaseReceptor) -> Result { - - let context = init_client_context(None); - - //ENSURE ROOT HOLON EXISTS OR CREATE IT - let client = LocalClient::new(); - // let holon = client.fetch_or_create_root_holon(context.as_ref())?; - // let root_space = client.convert_to_holonspace(holon)?; - let client_handler = Arc::new(client); - - let _holon_service: Arc = Arc::new(ClientHolonService); - - Ok(Self { - receptor_id: base_receptor.receptor_id.clone(), - receptor_type: base_receptor.receptor_type.clone(), - properties: base_receptor.properties.clone(), - context, - client_handler, - _holon_service, - _root_space: HolonSpace::default(), //root_space, - }) - } -} - -#[async_trait] -impl ReceptorBehavior for LocalReceptor { - fn transaction_context(&self) -> Arc { - Arc::clone(&self.context) - } - - async fn handle_map_request(&self, request: MapRequest) -> Result { - tracing::warn!("LocalReceptor: handling request: {:?}", self.context); - - //TODO: implement actual handling logic here with the HolonServiceApi - - let mocked_response = MapResponse { - space_id: request.space.id, - status_code: ResponseStatusCode::OK, - description: "Local request completed".into(), - body: ResponseBody::None, - descriptor: None, - state: None, - }; - - tracing::info!("LocalReceptor: request response: {:?}", mocked_response); - - //let res = MapResponse::new_from_dance_response(request.space.id, moocked_response); - Ok(mocked_response) - } - //async fn add_space(&self, holon_for_space: Holon) -> Result<(), HolonError> { - - async fn get_space_info(&self) -> Result { - self.client_handler.get_all_spaces().await - } -} - -//is still needed? -impl fmt::Debug for LocalReceptor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("LocalReceptor") - .field("receptor_id", &self.receptor_id) - .field("receptor_type", &self.receptor_type) - .field("properties", &self.properties) - .field("context", &"ClientHolonsContext") - // .field("root_space", &self.root_space) - .finish() - } -} diff --git a/host/crates/holons_receptor/src/receptors/local_receptor/mod.rs b/host/crates/holons_receptor/src/receptors/local_receptor/mod.rs deleted file mode 100644 index 1d8d818ef..000000000 --- a/host/crates/holons_receptor/src/receptors/local_receptor/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod local_client; -pub mod local_receptor; -// Re-exports for easy access -pub use local_client::*; -pub use local_receptor::*; diff --git a/host/crates/holons_receptor/src/receptors/mod.rs b/host/crates/holons_receptor/src/receptors/mod.rs deleted file mode 100644 index 0e7de0bb8..000000000 --- a/host/crates/holons_receptor/src/receptors/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod local_receptor; -// Re-exports for easy access - -pub use local_receptor::*; diff --git a/host/crates/map_commands/Cargo.toml b/host/crates/map_commands/Cargo.toml index 44a1410d6..f4ed6437a 100644 --- a/host/crates/map_commands/Cargo.toml +++ b/host/crates/map_commands/Cargo.toml @@ -11,6 +11,7 @@ base_types = { workspace = true } integrity_core_types = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +tracing = { workspace = true } [dev-dependencies] tokio = { version = "1", features = ["rt", "macros"] } diff --git a/host/crates/map_commands/src/dispatch/holon_dispatch.rs b/host/crates/map_commands/src/dispatch/holon_dispatch.rs index e9e01f44d..e66a32b8a 100644 --- a/host/crates/map_commands/src/dispatch/holon_dispatch.rs +++ b/host/crates/map_commands/src/dispatch/holon_dispatch.rs @@ -1,12 +1,98 @@ +use base_types::{BaseValue, MapString}; use core_types::HolonError; +use holons_core::reference_layer::{HolonReference, ReadableHolon, WritableHolon}; -use crate::domain::{HolonCommand, MapResult}; +use crate::domain::{HolonAction, HolonCommand, MapResult, ReadableHolonAction, WritableHolonAction}; /// Dispatches holon-scoped commands. -/// -/// Stub: all holon commands return NotImplemented for Phase 2.1. -pub async fn dispatch_holon(_command: HolonCommand) -> Result { - Err(HolonError::NotImplemented( - "HolonCommand dispatch".to_string(), - )) +pub async fn dispatch_holon(command: HolonCommand) -> Result { + match command.action { + HolonAction::Read(action) => dispatch_read(command.target, action), + HolonAction::Write(action) => dispatch_write(command.target, action), + } +} + +fn dispatch_read( + target: HolonReference, + action: ReadableHolonAction, +) -> Result { + match action { + ReadableHolonAction::CloneHolon => { + let transient = target.clone_holon()?; + Ok(MapResult::Reference(HolonReference::Transient(transient))) + } + ReadableHolonAction::EssentialContent => { + let content = target.essential_content()?; + Ok(MapResult::EssentialContent(content)) + } + ReadableHolonAction::Summarize => { + let summary = target.summarize()?; + Ok(MapResult::Value(BaseValue::StringValue(MapString::from(summary)))) + } + ReadableHolonAction::HolonId => { + let id = target.holon_id()?; + Ok(MapResult::HolonId(id)) + } + ReadableHolonAction::Predecessor => match target.predecessor()? { + Some(r) => Ok(MapResult::Reference(r)), + None => Ok(MapResult::None), + }, + ReadableHolonAction::Key => match target.key()? { + Some(s) => Ok(MapResult::Value(BaseValue::StringValue(s))), + None => Ok(MapResult::None), + }, + ReadableHolonAction::VersionedKey => { + let key = target.versioned_key()?; + Ok(MapResult::Value(BaseValue::StringValue(key))) + } + ReadableHolonAction::PropertyValue { name } => match target.property_value(name)? { + Some(v) => Ok(MapResult::Value(v)), + None => Ok(MapResult::None), + }, + ReadableHolonAction::RelatedHolons { name } => { + let collection_arc = target.related_holons(name)?; + let collection = collection_arc + .read() + .map_err(|e| { + HolonError::FailedToAcquireLock(format!( + "Failed to read-lock HolonCollection: {}", + e + )) + })? + .clone(); + Ok(MapResult::Collection(collection)) + } + } +} + +fn dispatch_write( + mut target: HolonReference, + action: WritableHolonAction, +) -> Result { + match action { + WritableHolonAction::WithPropertyValue { name, value } => { + target.with_property_value(name, value)?; + Ok(MapResult::None) + } + WritableHolonAction::RemovePropertyValue { name } => { + target.remove_property_value(name)?; + Ok(MapResult::None) + } + WritableHolonAction::AddRelatedHolons { name, holons } => { + target.add_related_holons(name, holons)?; + Ok(MapResult::None) + } + WritableHolonAction::RemoveRelatedHolons { name, holons } => { + target.remove_related_holons(name, holons)?; + Ok(MapResult::None) + } + WritableHolonAction::WithDescriptor { descriptor } => { + target.with_descriptor(descriptor)?; + Ok(MapResult::None) + } + WritableHolonAction::WithPredecessor { predecessor } => { + target.with_predecessor(predecessor)?; + Ok(MapResult::None) + } + } } diff --git a/host/crates/map_commands/src/dispatch/runtime.rs b/host/crates/map_commands/src/dispatch/runtime.rs index 180429a19..492ad7fd2 100644 --- a/host/crates/map_commands/src/dispatch/runtime.rs +++ b/host/crates/map_commands/src/dispatch/runtime.rs @@ -1,8 +1,9 @@ use std::sync::Arc; use core_types::HolonError; +use tracing::info; -use crate::domain::{MapCommand, MapResult}; +use crate::domain::{MapCommand, MapResult, MutationClassification}; use crate::wire::{MapCommandWire, MapIpcRequest, MapIpcResponse, MapResultWire}; use super::runtime_session::RuntimeSession; @@ -27,11 +28,23 @@ impl Runtime { /// Single IPC dispatch entrypoint (the full sandwich). /// /// 1. Bind wire command β†’ domain command - /// 2. Dispatch domain command - /// 3. Convert domain result β†’ wire result + /// 2. Enforce lifecycle via CommandDescriptor + /// 3. Dispatch domain command + /// 4. Convert domain result β†’ wire result pub async fn dispatch(&self, request: MapIpcRequest) -> Result { let request_id = request.request_id; + // Log gesture context if present + if let Some(ref gesture_id) = request.options.gesture_id { + let label = request.options.gesture_label.as_deref().unwrap_or(""); + info!( + "dispatch request_id={} gesture_id={:?} label={}", + request_id.value(), + gesture_id.0, + label + ); + } + let result = self.dispatch_inner(request.command).await; // Convert domain result to wire, preserving errors @@ -43,10 +56,52 @@ impl Runtime { Ok(MapIpcResponse { request_id, result: wire_result }) } - /// Bind + dispatch (separated for cleaner error handling). + /// Bind + lifecycle enforcement + dispatch. async fn dispatch_inner(&self, command_wire: MapCommandWire) -> Result { let command = self.bind(command_wire)?; + + let descriptor = command.descriptor(); + + // Extract context for lifecycle checks (Transaction and Holon commands have one) + let context = match &command { + MapCommand::Transaction(cmd) => Some(Arc::clone(&cmd.context)), + MapCommand::Holon(cmd) => Some(Arc::clone(&cmd.context)), + MapCommand::Space(_) => None, + }; + + // Open-transaction check: reject commands that require an open transaction + if descriptor.requires_open_tx { + if let Some(ref ctx) = context { + if !ctx.is_open() { + return Err(HolonError::TransactionAlreadyCommitted { + tx_id: ctx.tx_id().value(), + }); + } + } + } + + // Commit guard: hold across dispatch for commit-guarded commands + let _commit_guard = if descriptor.requires_commit_guard { + if let Some(ref ctx) = context { + Some(ctx.begin_host_commit_ingress_guard()?) + } else { + None + } + } else { + None + }; + + // Mutation entry check: for non-commit-guarded mutating commands + if !descriptor.requires_commit_guard + && descriptor.mutation == MutationClassification::Mutating + { + if let Some(ref ctx) = context { + ctx.ensure_host_mutation_entry_allowed()?; + } + } + self.dispatch_command(command).await + // Note: commit guard is automatically released at the end of this function's scope } /// Binds a wire command to its domain equivalent. diff --git a/host/crates/map_commands/src/dispatch/transaction_dispatch.rs b/host/crates/map_commands/src/dispatch/transaction_dispatch.rs index a79f2dede..eb599262d 100644 --- a/host/crates/map_commands/src/dispatch/transaction_dispatch.rs +++ b/host/crates/map_commands/src/dispatch/transaction_dispatch.rs @@ -1,3 +1,4 @@ +use base_types::{BaseValue, MapInteger}; use core_types::HolonError; use holons_core::reference_layer::HolonReference; @@ -7,18 +8,97 @@ use super::runtime_session::RuntimeSession; /// Dispatches transaction-scoped commands. pub async fn dispatch_transaction( - session: &RuntimeSession, + _session: &RuntimeSession, // may be needed for removing a transaction on commit or abort command: TransactionCommand, ) -> Result { - let tx_id = command.context.tx_id(); + let context = &command.context; match command.action { + // ── Commit ─────────────────────────────────────────────────── TransactionAction::Commit => { - let transient_ref = command.context.commit()?; - session.remove_transaction(&tx_id)?; - Ok(MapResult::CommitResponse(HolonReference::Transient(transient_ref))) + let transient_ref = context.commit()?; + Ok(MapResult::Reference(HolonReference::Transient(transient_ref))) + } + + // ── Dance / Query / LoadHolons ─────────────────────────────── + TransactionAction::Dance(_) => Err(HolonError::NotImplemented( + "TransactionAction::Dance: extension dances pending API refactor".to_string(), + )), + TransactionAction::Query(_) => { + Err(HolonError::NotImplemented("TransactionAction::Query".to_string())) + } + TransactionAction::LoadHolons { bundle } => { + // LoadHolons requires a TransientReference; extract from the bound HolonReference + match bundle { + HolonReference::Transient(transient_ref) => { + let result = context.load_holons_and_commit(transient_ref)?; + Ok(MapResult::Reference(HolonReference::Transient(result))) + } + other => Err(HolonError::InvalidParameter(format!( + "LoadHolons requires a TransientReference, got {:?}", + other + ))), + } + } + + // ── Lookup actions (LookupFacade) ──────────────────────────── + TransactionAction::GetAllHolons => { + let collection = context.lookup().get_all_holons()?; + Ok(MapResult::Collection(collection)) + } + TransactionAction::GetStagedHolonByBaseKey { key } => { + let staged = context.lookup().get_staged_holon_by_base_key(&key)?; + Ok(MapResult::Reference(HolonReference::Staged(staged))) + } + TransactionAction::GetStagedHolonsByBaseKey { key } => { + let staged_refs = context.lookup().get_staged_holons_by_base_key(&key)?; + Ok(MapResult::References(staged_refs.into_iter().map(HolonReference::Staged).collect())) + } + TransactionAction::GetStagedHolonByVersionedKey { key } => { + let staged = context.lookup().get_staged_holon_by_versioned_key(&key)?; + Ok(MapResult::Reference(HolonReference::Staged(staged))) + } + TransactionAction::GetTransientHolonByBaseKey { key } => { + let transient = context.lookup().get_transient_holon_by_base_key(&key)?; + Ok(MapResult::Reference(HolonReference::Transient(transient))) + } + TransactionAction::GetTransientHolonByVersionedKey { key } => { + let transient = context.lookup().get_transient_holon_by_versioned_key(&key)?; + Ok(MapResult::Reference(HolonReference::Transient(transient))) + } + TransactionAction::StagedCount => { + let count = context.lookup().staged_count()?; + Ok(MapResult::Value(BaseValue::IntegerValue(MapInteger(count)))) + } + TransactionAction::TransientCount => { + let count = context.lookup().transient_count()?; + Ok(MapResult::Value(BaseValue::IntegerValue(MapInteger(count)))) + } + + // ── Mutation actions (MutationFacade) ──────────────────────── + TransactionAction::NewHolon { key } => { + let transient = context.mutation().new_holon(key)?; + Ok(MapResult::Reference(HolonReference::Transient(transient))) + } + TransactionAction::StageNewHolon { source } => { + let staged = context.mutation().stage_new_holon(source)?; + Ok(MapResult::Reference(HolonReference::Staged(staged))) + } + TransactionAction::StageNewFromClone { original, new_key } => { + let staged = context.mutation().stage_new_from_clone(original, new_key)?; + Ok(MapResult::Reference(HolonReference::Staged(staged))) + } + TransactionAction::StageNewVersion { current_version } => { + let staged = context.mutation().stage_new_version(current_version)?; + Ok(MapResult::Reference(HolonReference::Staged(staged))) + } + TransactionAction::StageNewVersionFromId { holon_id } => { + let staged = context.mutation().stage_new_version_from_id(holon_id)?; + Ok(MapResult::Reference(HolonReference::Staged(staged))) + } + TransactionAction::DeleteHolon { local_id } => { + context.mutation().delete_holon(local_id)?; + Ok(MapResult::None) } - // Dances use the initiate_ingress_dance function - _ => Err(HolonError::NotImplemented(format!("TransactionAction::{:?}", command.action))), } } diff --git a/host/crates/map_commands/src/domain/command_descriptor.rs b/host/crates/map_commands/src/domain/command_descriptor.rs new file mode 100644 index 000000000..90f9da221 --- /dev/null +++ b/host/crates/map_commands/src/domain/command_descriptor.rs @@ -0,0 +1,42 @@ +/// How a command affects transaction state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MutationClassification { + ReadOnly, + Mutating, + /// Dance mutation detection deferred to Phase 2.3 (version counters). + RuntimeDetected, +} + +/// Static metadata describing a command's lifecycle requirements. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CommandDescriptor { + pub mutation: MutationClassification, + pub requires_open_tx: bool, + pub requires_commit_guard: bool, +} + +impl CommandDescriptor { + pub const fn read_only() -> Self { + Self { + mutation: MutationClassification::ReadOnly, + requires_open_tx: false, + requires_commit_guard: false, + } + } + + pub const fn mutating() -> Self { + Self { + mutation: MutationClassification::Mutating, + requires_open_tx: true, + requires_commit_guard: false, + } + } + + pub const fn mutating_with_guard() -> Self { + Self { + mutation: MutationClassification::Mutating, + requires_open_tx: true, + requires_commit_guard: true, + } + } +} diff --git a/host/crates/map_commands/src/domain/holon_command.rs b/host/crates/map_commands/src/domain/holon_command.rs index 09b9651d0..b2f62c799 100644 --- a/host/crates/map_commands/src/domain/holon_command.rs +++ b/host/crates/map_commands/src/domain/holon_command.rs @@ -1,14 +1,21 @@ +use std::sync::Arc; + use base_types::BaseValue; use core_types::{PropertyName, RelationshipName}; +use holons_core::core_shared_objects::transactions::TransactionContext; use holons_core::reference_layer::HolonReference; +use super::CommandDescriptor; + /// Holon-scoped domain command. /// /// Targets a specific holon via a bound runtime reference. -/// Dispatch stops at `HolonReference` β€” action does not include -/// `tx_id` or `TransactionContext` (references are self-resolving). +/// The `context` field enables dispatch-level lifecycle enforcement +/// (e.g. mutation entry checks). References are still self-resolving +/// for their own operations. #[derive(Debug)] pub struct HolonCommand { + pub context: Arc, pub target: HolonReference, pub action: HolonAction, } @@ -20,6 +27,18 @@ pub enum HolonAction { Write(WritableHolonAction), } +impl HolonAction { + pub fn descriptor(&self) -> CommandDescriptor { + match self { + HolonAction::Read(ReadableHolonAction::CloneHolon) => { + CommandDescriptor::mutating() + } + HolonAction::Read(_) => CommandDescriptor::read_only(), + HolonAction::Write(_) => CommandDescriptor::mutating(), + } + } +} + /// Non-mutating holon actions. /// /// Maps 1:1 to the `ReadableHolon` trait methods in diff --git a/host/crates/map_commands/src/domain/map_command.rs b/host/crates/map_commands/src/domain/map_command.rs index 851de731a..b25ca3f89 100644 --- a/host/crates/map_commands/src/domain/map_command.rs +++ b/host/crates/map_commands/src/domain/map_command.rs @@ -1,4 +1,4 @@ -use super::{HolonCommand, SpaceCommand, TransactionCommand}; +use super::{CommandDescriptor, HolonCommand, SpaceCommand, TransactionCommand}; /// Post-binding domain command. /// @@ -10,3 +10,13 @@ pub enum MapCommand { Transaction(TransactionCommand), Holon(HolonCommand), } + +impl MapCommand { + pub fn descriptor(&self) -> CommandDescriptor { + match self { + MapCommand::Space(cmd) => cmd.descriptor(), + MapCommand::Transaction(cmd) => cmd.action.descriptor(), + MapCommand::Holon(cmd) => cmd.action.descriptor(), + } + } +} diff --git a/host/crates/map_commands/src/domain/map_result.rs b/host/crates/map_commands/src/domain/map_result.rs index ac716f1f0..d3d1d9d79 100644 --- a/host/crates/map_commands/src/domain/map_result.rs +++ b/host/crates/map_commands/src/domain/map_result.rs @@ -1,4 +1,4 @@ -use base_types::{BaseValue, MapString}; +use base_types::BaseValue; use core_types::HolonId; use holons_core::core_shared_objects::holon::EssentialHolonContent; use holons_core::core_shared_objects::transactions::TxId; @@ -13,32 +13,26 @@ use holons_core::reference_layer::HolonReference; /// converted to `MapResultWire` before crossing the IPC boundary. #[derive(Debug)] pub enum MapResult { - /// Command completed with no return value. + /// Command completed with no return value (also used for "not found"). None, /// Returns a new transaction id (from BeginTransaction). TransactionCreated { tx_id: TxId }, - /// Returns a committed transaction result. - CommitResponse(HolonReference), - /// Returns a holon reference. - HolonReference(HolonReference), + Reference(HolonReference), /// Returns a collection of holon references. - HolonReferences(Vec), + References(Vec), /// Returns an indexed collection of holons. - HolonCollection(HolonCollection), + Collection(HolonCollection), /// Returns a node collection (query result). NodeCollection(NodeCollection), - /// Returns a single property value. - PropertyValue(Option), - - /// Returns a string value (e.g. from `versioned key()`). - StringValue(MapString), + /// Universal scalar return β€” covers MapString, MapInteger, MapBoolean, PropertyValue. + Value(BaseValue), /// Returns a holon id. HolonId(HolonId), diff --git a/host/crates/map_commands/src/domain/mod.rs b/host/crates/map_commands/src/domain/mod.rs index 603494ed1..e7365f6d7 100644 --- a/host/crates/map_commands/src/domain/mod.rs +++ b/host/crates/map_commands/src/domain/mod.rs @@ -1,9 +1,11 @@ +mod command_descriptor; mod holon_command; mod map_command; mod map_result; mod space_command; mod transaction_command; +pub use command_descriptor::*; pub use holon_command::*; pub use map_command::*; pub use map_result::*; diff --git a/host/crates/map_commands/src/domain/space_command.rs b/host/crates/map_commands/src/domain/space_command.rs index 38c4b00c3..30437c79d 100644 --- a/host/crates/map_commands/src/domain/space_command.rs +++ b/host/crates/map_commands/src/domain/space_command.rs @@ -1,3 +1,5 @@ +use super::{CommandDescriptor, MutationClassification}; + /// Space-scoped domain commands. /// /// Operate outside any transaction context. @@ -6,3 +8,15 @@ pub enum SpaceCommand { /// Opens a new transaction. BeginTransaction, } + +impl SpaceCommand { + pub fn descriptor(&self) -> CommandDescriptor { + match self { + SpaceCommand::BeginTransaction => CommandDescriptor { + mutation: MutationClassification::Mutating, + requires_open_tx: false, + requires_commit_guard: false, + }, + } + } +} diff --git a/host/crates/map_commands/src/domain/transaction_command.rs b/host/crates/map_commands/src/domain/transaction_command.rs index 81aa0f75b..18838fa5e 100644 --- a/host/crates/map_commands/src/domain/transaction_command.rs +++ b/host/crates/map_commands/src/domain/transaction_command.rs @@ -7,6 +7,8 @@ use holons_core::dances::DanceRequest; use holons_core::query_layer::QueryExpression; use holons_core::reference_layer::{HolonReference, SmartReference, TransientReference}; +use super::{CommandDescriptor, MutationClassification}; + /// Transaction-scoped domain command. /// /// The `context` field holds a strong reference to the bound transaction. @@ -84,3 +86,36 @@ pub enum TransactionAction { /// `delete_holon(local_id)` β†’ `()` DeleteHolon { local_id: LocalId }, } + +impl TransactionAction { + pub fn descriptor(&self) -> CommandDescriptor { + match self { + TransactionAction::Commit => CommandDescriptor::mutating_with_guard(), + TransactionAction::LoadHolons { .. } => CommandDescriptor::mutating_with_guard(), + TransactionAction::Dance(_) => CommandDescriptor { + mutation: MutationClassification::RuntimeDetected, + requires_open_tx: true, + requires_commit_guard: false, + }, + TransactionAction::Query(_) => CommandDescriptor::read_only(), + + // Lookups + TransactionAction::GetAllHolons + | TransactionAction::GetStagedHolonByBaseKey { .. } + | TransactionAction::GetStagedHolonsByBaseKey { .. } + | TransactionAction::GetStagedHolonByVersionedKey { .. } + | TransactionAction::GetTransientHolonByBaseKey { .. } + | TransactionAction::GetTransientHolonByVersionedKey { .. } + | TransactionAction::StagedCount + | TransactionAction::TransientCount => CommandDescriptor::read_only(), + + // Mutations + TransactionAction::NewHolon { .. } + | TransactionAction::StageNewHolon { .. } + | TransactionAction::StageNewFromClone { .. } + | TransactionAction::StageNewVersion { .. } + | TransactionAction::StageNewVersionFromId { .. } + | TransactionAction::DeleteHolon { .. } => CommandDescriptor::mutating(), + } + } +} diff --git a/host/crates/map_commands/src/tests/dispatch_tests.rs b/host/crates/map_commands/src/tests/dispatch_tests.rs index 82a3dcad7..aa67d0f0c 100644 --- a/host/crates/map_commands/src/tests/dispatch_tests.rs +++ b/host/crates/map_commands/src/tests/dispatch_tests.rs @@ -1,7 +1,8 @@ use std::any::Any; use std::sync::Arc; -use core_types::{HolonError, HolonId, LocalId, RelationshipName}; +use base_types::{BaseValue, MapInteger, MapString}; +use core_types::{HolonError, HolonId, LocalId, PropertyName, RelationshipName}; use holons_core::core_shared_objects::transactions::TransactionContext; use holons_core::core_shared_objects::space_manager::HolonSpaceManager; use holons_core::core_shared_objects::{ @@ -9,7 +10,13 @@ use holons_core::core_shared_objects::{ }; use holons_core::reference_layer::{HolonServiceApi, StagedReference, TransientReference}; +use holons_core::core_shared_objects::transactions::TxId; + use crate::dispatch::{Runtime, RuntimeSession}; +use crate::domain::{ + CommandDescriptor, HolonAction, MutationClassification, ReadableHolonAction, SpaceCommand, + TransactionAction, WritableHolonAction, +}; use crate::wire::*; // ── Test double ───────────────────────────────────────────────────── @@ -97,12 +104,30 @@ fn build_test_space_manager() -> Arc { )) } +fn test_options() -> RequestOptions { + RequestOptions { gesture_id: None, gesture_label: None, snapshot_after: false } +} + fn build_test_runtime() -> Runtime { let space_manager = build_test_space_manager(); let session = Arc::new(RuntimeSession::new(space_manager)); Runtime::new(session) } +/// Helper: begin a transaction and return the tx_id. +async fn begin_tx(runtime: &Runtime) -> TxId { + let request = MapIpcRequest { + request_id: RequestId::new(0), + command: MapCommandWire::Space(SpaceCommandWire::BeginTransaction), + options: test_options(), + }; + let response = runtime.dispatch(request).await.expect("dispatch should succeed"); + match response.result { + Ok(MapResultWire::TransactionCreated { tx_id }) => tx_id, + other => panic!("expected TransactionCreated, got {:?}", other), + } +} + // ── Dispatch tests ────────────────────────────────────────────────── #[tokio::test] @@ -112,6 +137,7 @@ async fn begin_transaction_returns_valid_tx_id() { let request = MapIpcRequest { request_id: RequestId::new(1), command: MapCommandWire::Space(SpaceCommandWire::BeginTransaction), + options: test_options(), }; let response = runtime.dispatch(request).await.expect("dispatch should succeed"); @@ -134,6 +160,7 @@ async fn begin_transaction_ids_are_unique() { let request = MapIpcRequest { request_id: RequestId::new(i), command: MapCommandWire::Space(SpaceCommandWire::BeginTransaction), + options: test_options(), }; let response = runtime.dispatch(request).await.expect("dispatch should succeed"); match response.result { @@ -151,56 +178,217 @@ async fn begin_transaction_ids_are_unique() { } #[tokio::test] -async fn unimplemented_command_returns_not_implemented() { +async fn invalid_tx_id_returns_error() { let runtime = build_test_runtime(); - // First open a transaction so we have a valid tx_id - let begin_req = MapIpcRequest { + let bad_tx_id = serde_json::from_value(serde_json::json!(999)).unwrap(); + + let request = MapIpcRequest { request_id: RequestId::new(1), - command: MapCommandWire::Space(SpaceCommandWire::BeginTransaction), - }; - let begin_resp = runtime.dispatch(begin_req).await.expect("dispatch should succeed"); - let tx_id = match begin_resp.result { - Ok(MapResultWire::TransactionCreated { tx_id }) => tx_id, - other => panic!("expected TransactionCreated, got {:?}", other), + command: MapCommandWire::Transaction(TransactionCommandWire { + tx_id: bad_tx_id, + action: TransactionActionWire::Commit, + }), + options: test_options(), }; - // Try an unimplemented transaction command + let response = runtime.dispatch(request).await.expect("dispatch should succeed"); + match response.result { + Err(HolonError::InvalidParameter(msg)) => { + assert!(msg.contains("999"), "error should mention the tx_id"); + } + other => panic!("expected InvalidParameter error, got {:?}", other), + } +} + +// ── Transaction lookup dispatch ───────────────────────────────────── + +#[tokio::test] +async fn staged_count_returns_zero_for_new_tx() { + let runtime = build_test_runtime(); + let tx_id = begin_tx(&runtime).await; + let request = MapIpcRequest { - request_id: RequestId::new(2), + request_id: RequestId::new(1), command: MapCommandWire::Transaction(TransactionCommandWire { tx_id, - action: TransactionActionWire::GetAllHolons, + action: TransactionActionWire::StagedCount, }), + options: test_options(), }; + let response = runtime.dispatch(request).await.expect("dispatch should succeed"); + match response.result { + Ok(MapResultWire::Value(BaseValue::IntegerValue(MapInteger(0)))) => {} + other => panic!("expected Value(IntegerValue(0)), got {:?}", other), + } +} +#[tokio::test] +async fn transient_count_returns_zero_for_new_tx() { + let runtime = build_test_runtime(); + let tx_id = begin_tx(&runtime).await; + + let request = MapIpcRequest { + request_id: RequestId::new(1), + command: MapCommandWire::Transaction(TransactionCommandWire { + tx_id, + action: TransactionActionWire::TransientCount, + }), + options: test_options(), + }; let response = runtime.dispatch(request).await.expect("dispatch should succeed"); - assert_eq!(response.request_id, RequestId::new(2)); match response.result { - Err(HolonError::NotImplemented(_)) => {} // expected - other => panic!("expected NotImplemented error, got {:?}", other), + Ok(MapResultWire::Value(BaseValue::IntegerValue(MapInteger(0)))) => {} + other => panic!("expected Value(IntegerValue(0)), got {:?}", other), } } +// ── Transaction mutation dispatch ─────────────────────────────────── + #[tokio::test] -async fn invalid_tx_id_returns_error() { +async fn new_holon_then_staged_count() { let runtime = build_test_runtime(); + let tx_id = begin_tx(&runtime).await; - let bad_tx_id = serde_json::from_value(serde_json::json!(999)).unwrap(); + // NewHolon creates a transient + let request = MapIpcRequest { + request_id: RequestId::new(1), + command: MapCommandWire::Transaction(TransactionCommandWire { + tx_id, + action: TransactionActionWire::NewHolon { + key: Some(MapString::from("test-key")), + }, + }), + options: test_options(), + }; + let response = runtime.dispatch(request).await.expect("dispatch should succeed"); + match &response.result { + Ok(MapResultWire::Reference(_)) => {} + other => panic!("expected Reference, got {:?}", other), + } + + // Transient count should be 1 + let request = MapIpcRequest { + request_id: RequestId::new(2), + command: MapCommandWire::Transaction(TransactionCommandWire { + tx_id, + action: TransactionActionWire::TransientCount, + }), + options: test_options(), + }; + let response = runtime.dispatch(request).await.expect("dispatch should succeed"); + match response.result { + Ok(MapResultWire::Value(BaseValue::IntegerValue(MapInteger(1)))) => {} + other => panic!("expected Value(IntegerValue(1)), got {:?}", other), + } +} + +#[tokio::test] +async fn new_holon_stage_then_staged_count() { + let runtime = build_test_runtime(); + let tx_id = begin_tx(&runtime).await; + // NewHolon β†’ get transient ref wire let request = MapIpcRequest { request_id: RequestId::new(1), command: MapCommandWire::Transaction(TransactionCommandWire { - tx_id: bad_tx_id, - action: TransactionActionWire::Commit, + tx_id, + action: TransactionActionWire::NewHolon { + key: Some(MapString::from("stage-test")), + }, + }), + options: test_options(), + }; + let response = runtime.dispatch(request).await.expect("dispatch should succeed"); + let transient_wire = match response.result { + Ok(MapResultWire::Reference(r)) => r, + other => panic!("expected Reference, got {:?}", other), + }; + + // StageNewHolon needs a TransientReferenceWire + let transient_ref_wire = match transient_wire { + holons_boundary::HolonReferenceWire::Transient(t) => t, + other => panic!("expected Transient wire ref, got {:?}", other), + }; + + let request = MapIpcRequest { + request_id: RequestId::new(2), + command: MapCommandWire::Transaction(TransactionCommandWire { + tx_id, + action: TransactionActionWire::StageNewHolon { + source: transient_ref_wire, + }, }), + options: test_options(), }; + let response = runtime.dispatch(request).await.expect("dispatch should succeed"); + match &response.result { + Ok(MapResultWire::Reference(_)) => {} + other => panic!("expected Reference (staged), got {:?}", other), + } + // StagedCount should be 1 + let request = MapIpcRequest { + request_id: RequestId::new(3), + command: MapCommandWire::Transaction(TransactionCommandWire { + tx_id, + action: TransactionActionWire::StagedCount, + }), + options: test_options(), + }; let response = runtime.dispatch(request).await.expect("dispatch should succeed"); match response.result { - Err(HolonError::InvalidParameter(msg)) => { - assert!(msg.contains("999"), "error should mention the tx_id"); - } - other => panic!("expected InvalidParameter error, got {:?}", other), + Ok(MapResultWire::Value(BaseValue::IntegerValue(MapInteger(1)))) => {} + other => panic!("expected Value(IntegerValue(1)), got {:?}", other), } } + +// ── CommandDescriptor classification tests ────────────────────────── + +#[test] +fn space_begin_transaction_descriptor() { + let desc = SpaceCommand::BeginTransaction.descriptor(); + assert_eq!(desc.mutation, MutationClassification::Mutating); + assert!(!desc.requires_open_tx); + assert!(!desc.requires_commit_guard); +} + +#[test] +fn transaction_action_descriptors() { + assert_eq!(TransactionAction::Commit.descriptor(), CommandDescriptor::mutating_with_guard()); + assert_eq!(TransactionAction::StagedCount.descriptor(), CommandDescriptor::read_only()); + assert_eq!(TransactionAction::TransientCount.descriptor(), CommandDescriptor::read_only()); + assert_eq!(TransactionAction::GetAllHolons.descriptor(), CommandDescriptor::read_only()); + assert_eq!( + TransactionAction::NewHolon { key: None }.descriptor(), + CommandDescriptor::mutating() + ); + assert_eq!( + TransactionAction::DeleteHolon { + local_id: LocalId(vec![]), + } + .descriptor(), + CommandDescriptor::mutating() + ); +} + +#[test] +fn holon_action_descriptors() { + assert_eq!( + HolonAction::Read(ReadableHolonAction::Key).descriptor(), + CommandDescriptor::read_only() + ); + assert_eq!( + HolonAction::Read(ReadableHolonAction::CloneHolon).descriptor(), + CommandDescriptor::mutating(), + "CloneHolon creates a transient β€” mutating despite being a ReadableHolonAction" + ); + assert_eq!( + HolonAction::Write(WritableHolonAction::WithPropertyValue { + name: PropertyName(MapString::from("x")), + value: BaseValue::StringValue(MapString::from("v")), + }) + .descriptor(), + CommandDescriptor::mutating() + ); +} diff --git a/host/crates/map_commands/src/tests/wire_serde_tests.rs b/host/crates/map_commands/src/tests/wire_serde_tests.rs index b1838eda4..641a8694d 100644 --- a/host/crates/map_commands/src/tests/wire_serde_tests.rs +++ b/host/crates/map_commands/src/tests/wire_serde_tests.rs @@ -14,6 +14,10 @@ fn test_holon_id() -> core_types::HolonId { core_types::HolonId::Local(integrity_core_types::LocalId(vec![0u8; 39])) } +fn test_options() -> RequestOptions { + RequestOptions { gesture_id: None, gesture_label: None, snapshot_after: false } +} + /// Helper: serialize to JSON and deserialize back, asserting roundtrip equality. fn assert_roundtrip(value: &T) where @@ -31,6 +35,7 @@ fn roundtrip_ipc_request_space_command() { let request = MapIpcRequest { request_id: RequestId::new(42), command: MapCommandWire::Space(SpaceCommandWire::BeginTransaction), + options: test_options(), }; assert_roundtrip(&request); } @@ -57,6 +62,32 @@ fn roundtrip_ipc_response_error() { assert_roundtrip(&response); } +// ── RequestOptions ────────────────────────────────────────────────── + +#[test] +fn roundtrip_request_options_default() { + let request = MapIpcRequest { + request_id: RequestId::new(1), + command: MapCommandWire::Space(SpaceCommandWire::BeginTransaction), + options: test_options(), + }; + assert_roundtrip(&request); +} + +#[test] +fn roundtrip_request_options_with_gesture() { + let request = MapIpcRequest { + request_id: RequestId::new(1), + command: MapCommandWire::Space(SpaceCommandWire::BeginTransaction), + options: RequestOptions { + gesture_id: Some(GestureId(MapString::from("g-42"))), + gesture_label: Some("Create holon".to_string()), + snapshot_after: true, + }, + }; + assert_roundtrip(&request); +} + // ── SpaceCommandWire ──────────────────────────────────────────────── #[test] @@ -148,9 +179,9 @@ fn roundtrip_result_transaction_created() { } #[test] -fn roundtrip_result_commit_response() { +fn roundtrip_result_reference() { let tx_id = test_tx_id(1); - assert_roundtrip(&MapResultWire::CommitResponse( + assert_roundtrip(&MapResultWire::Reference( holons_boundary::HolonReferenceWire::Smart(SmartReferenceWire::new( tx_id, test_holon_id(), @@ -160,8 +191,10 @@ fn roundtrip_result_commit_response() { } #[test] -fn roundtrip_result_string_value() { - assert_roundtrip(&MapResultWire::StringValue(MapString::from("my-key"))); +fn roundtrip_result_value_string() { + assert_roundtrip(&MapResultWire::Value(BaseValue::StringValue( + MapString::from("my-key"), + ))); } #[test] diff --git a/host/crates/map_commands/src/wire/holon_wire.rs b/host/crates/map_commands/src/wire/holon_wire.rs index c3a060f6b..8cf01b0ca 100644 --- a/host/crates/map_commands/src/wire/holon_wire.rs +++ b/host/crates/map_commands/src/wire/holon_wire.rs @@ -92,7 +92,7 @@ impl HolonCommandWire { pub fn bind(self, context: &Arc) -> Result { let target = self.target.bind(context)?; let action = self.action.bind(context)?; - Ok(HolonCommand { target, action }) + Ok(HolonCommand { context: Arc::clone(context), target, action }) } } diff --git a/host/crates/map_commands/src/wire/ipc_envelope.rs b/host/crates/map_commands/src/wire/ipc_envelope.rs index 81c14efb9..6399eb7a2 100644 --- a/host/crates/map_commands/src/wire/ipc_envelope.rs +++ b/host/crates/map_commands/src/wire/ipc_envelope.rs @@ -1,3 +1,4 @@ +use base_types::{MapInteger, MapString}; use core_types::HolonError; use serde::{Deserialize, Serialize}; @@ -7,19 +8,34 @@ use super::MapResultWire; /// Opaque request identifier assigned by the TypeScript client. /// /// Echoed back in MapIpcResponse so the client can correlate responses. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] -pub struct RequestId(u64); +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RequestId(pub MapInteger); impl RequestId { - pub fn new(id: u64) -> Self { - Self(id) + pub fn new(id: i64) -> Self { + Self(MapInteger(id)) } - pub fn value(&self) -> u64 { - self.0 + pub fn value(&self) -> i64 { + self.0 .0 } } +/// Identifies a user gesture for undo/redo grouping. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GestureId(pub MapString); + +/// Per-request options controlling dispatch behavior. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RequestOptions { + /// Groups this command into a gesture for undo/redo. + pub gesture_id: Option, + /// Human-readable label for the gesture (shown in undo UI). + pub gesture_label: Option, + /// When true, snapshot pool state after mutation (no-op until Phase 2.3). + pub snapshot_after: bool, +} + /// Canonical IPC request envelope for MAP Commands. /// /// This is the only inbound type accepted by `dispatch_map_command`. @@ -28,6 +44,7 @@ impl RequestId { pub struct MapIpcRequest { pub request_id: RequestId, pub command: MapCommandWire, + pub options: RequestOptions, } /// Canonical IPC response envelope for MAP Commands. diff --git a/host/crates/map_commands/src/wire/result_wire.rs b/host/crates/map_commands/src/wire/result_wire.rs index 7e1882723..c404d9661 100644 --- a/host/crates/map_commands/src/wire/result_wire.rs +++ b/host/crates/map_commands/src/wire/result_wire.rs @@ -1,4 +1,4 @@ -use base_types::{BaseValue, MapString}; +use base_types::BaseValue; use core_types::HolonId; use holons_boundary::{ DanceResponseWire, HolonCollectionWire, HolonReferenceWire, NodeCollectionWire, @@ -21,26 +21,20 @@ pub enum MapResultWire { /// Returns a new transaction id (from BeginTransaction). TransactionCreated { tx_id: TxId }, - /// Returns a committed transaction result. - CommitResponse(HolonReferenceWire), - /// Returns a holon reference. - HolonReference(HolonReferenceWire), + Reference(HolonReferenceWire), /// Returns a collection of holon references. - HolonReferences(Vec), + References(Vec), /// Returns an indexed collection of holons. - HolonCollection(HolonCollectionWire), + Collection(HolonCollectionWire), /// Returns a node collection (query result). NodeCollection(NodeCollectionWire), - /// Returns a single property value. - PropertyValue(Option), - - /// Returns a string value (e.g. from `versioned_key()`). - StringValue(MapString), + /// Universal scalar return. + Value(BaseValue), /// Returns a holon id. HolonId(HolonId), @@ -59,25 +53,21 @@ impl From for MapResultWire { MapResult::TransactionCreated { tx_id } => { MapResultWire::TransactionCreated { tx_id } } - MapResult::CommitResponse(r) => { - MapResultWire::CommitResponse(HolonReferenceWire::from(&r)) - } - MapResult::HolonReference(r) => { - MapResultWire::HolonReference(HolonReferenceWire::from(&r)) + MapResult::Reference(r) => { + MapResultWire::Reference(HolonReferenceWire::from(&r)) } - MapResult::HolonReferences(refs) => { - MapResultWire::HolonReferences( + MapResult::References(refs) => { + MapResultWire::References( refs.iter().map(HolonReferenceWire::from).collect(), ) } - MapResult::HolonCollection(c) => { - MapResultWire::HolonCollection(HolonCollectionWire::from(&c)) + MapResult::Collection(c) => { + MapResultWire::Collection(HolonCollectionWire::from(&c)) } MapResult::NodeCollection(n) => { MapResultWire::NodeCollection(NodeCollectionWire::from(&n)) } - MapResult::PropertyValue(v) => MapResultWire::PropertyValue(v), - MapResult::StringValue(s) => MapResultWire::StringValue(s), + MapResult::Value(v) => MapResultWire::Value(v), MapResult::HolonId(id) => MapResultWire::HolonId(id), MapResult::EssentialContent(c) => MapResultWire::EssentialContent(c), MapResult::DanceResponse(r) => {