UNPKG

@isthatuzii/create-nano-app

Version:

Desktop application scaffolding tool for the Nano Framework

464 lines (378 loc) 14.3 kB
# Nano Framework IPC System The Nano Framework IPC (Inter-Process Communication) system provides a clean, Tauri-like interface for communication between the Rust backend and frontend applications. It's designed to be simple, type-safe, and efficient. ## Architecture Overview The IPC system consists of three main components: ``` ┌─────────────────────────────────────────────────────────────────┐ │ Frontend (JavaScript/TypeScript) │ ├─────────────────────────────────────────────────────────────────┤ │ invoke("command_name", { arg1: "value", arg2: 123 }) │ │ │ │ │ ▼ │ │ HTTP POST /ipc │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Axum HTTP Server (Rust) │ ├─────────────────────────────────────────────────────────────────┤ │ dispatcher.rs → handle_ipc_request() │ │ │ │ │ ▼ │ │ simple_registry.rs → NANO_REGISTRY.get(command) │ │ │ │ │ ▼ │ │ CommandHandler → execute function │ │ │ │ │ ▼ │ │ Return JSON response │ └─────────────────────────────────────────────────────────────────┘ ``` ## Core Components ### 1. `mod.rs` - Module Declaration - Exports the main IPC types and functions - Initializes tracing for debugging - Sets up the global registry ### 2. `dispatcher.rs` - Request Handler Handles incoming IPC requests and dispatches them to registered functions. **Key Types:** ```rust // Request from frontend #[derive(Debug, Deserialize)] pub struct IpcRequest { pub cmd: String, // Function name to call pub args: Value, // Arguments as JSON pub id: Option<String>, // Optional request ID } // Response to frontend #[derive(Debug, Serialize)] pub struct IpcResponse { pub result: Option<Value>, // Success result pub error: Option<String>, // Error message pub code: Option<String>, // Error code pub id: Option<String>, // Request ID } ``` **Error Handling:** - `CommandNotFound` - Function not registered - `InvalidArguments` - Deserialization failed - `ExecutionError` - Function execution failed - `SecurityViolation` - Security check failed - `SerializationError` - Response serialization failed ### 3. `simple_registry.rs` - Function Registry A thread-safe registry system for registering and executing commands. **Key Features:** - **Thread-Safe**: Uses `Arc<RwLock<HashMap>>` for concurrent access - **Type-Safe**: Automatic serialization/deserialization of arguments and results - **Flexible**: Supports any function signature matching the pattern - **Global**: Single static registry instance accessible everywhere ## Function Registration System ### Registration Pattern All functions must follow this signature pattern: ```rust pub async fn function_name(args: ArgsType) -> Result<ReturnType, String> ``` ### Registration Process 1. **Define Types** (in `nano_types.rs`): ```rust #[derive(Debug, Deserialize)] pub struct GreetArgs { pub name: String, } #[derive(Debug, Serialize)] pub struct GreetResponse { pub message: String, } ``` 2. **Implement Function** (in `nano_impl.rs` or external modules): ```rust pub async fn greet(args: GreetArgs) -> Result<String, String> { if args.name.is_empty() { return Err("Name cannot be empty".to_string()); } Ok(format!("Hello, {}!", args.name)) } ``` 3. **Register Function** (in `nano_registry.rs`): ```rust pub fn register_all_nano_functions() { register_nano_command!("greet", greet); } ``` ### The `register_nano_command!` Macro This macro creates a type-safe handler that: - Deserializes JSON arguments to the expected type - Calls your function with typed arguments - Serializes the result back to JSON - Handles errors gracefully ```rust #[macro_export] macro_rules! register_nano_command { ($name:expr, $func:expr) => { NANO_REGISTRY.register($name, create_handler($func)); }; } ``` ## Advanced Features ### 1. Empty Arguments Handling Functions can accept no arguments by using unit types: ```rust pub async fn get_timestamp() -> Result<String, String> { Ok(chrono::Utc::now().to_rfc3339()) } // Frontend: await invoke("get_timestamp") ``` ### 2. Complex Argument Types Support for nested structures and arrays: ```rust #[derive(Debug, Deserialize)] pub struct CalculationArgs { pub operation: String, pub values: Vec<f64>, pub options: Option<CalculationOptions>, } #[derive(Debug, Deserialize)] pub struct CalculationOptions { pub precision: u8, pub round_result: bool, } ``` ### 3. External System Integration Functions from external modules can be registered seamlessly: ```rust // In nano_registry.rs register_nano_command!("read_file", file_manager::read_file); register_nano_command!("calculate", calculator::calculate_expression); ``` ## HTTP Integration The IPC system integrates with the Axum web server through a single endpoint: ```rust // In server.rs Router::new() .route("/ipc", post(handle_ipc_request)) ``` **Request Flow:** 1. Frontend sends POST to `/ipc` with JSON body 2. `handle_ipc_request` deserializes to `IpcRequest` 3. `dispatch_request` looks up function in registry 4. Function executes with type-safe arguments 5. Result serialized to `IpcResponse` and returned ## Frontend Usage ### Using `nano-invoke` Package (Recommended) ```typescript import { invoke, configure } from 'nano-invoke'; // Configure once at startup configure({ debug: true, timeout: 5000 }); // Call any registered function const greeting = await invoke('greet', { name: 'Alice' }); const result = await invoke('calculate', { operation: 'add', values: [1, 2, 3, 4, 5] }); ``` ### Direct HTTP Calls ```typescript async function invoke(cmd: string, args: any) { const response = await fetch('/ipc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cmd, args }) }); const result = await response.json(); if (result.error) { throw new Error(result.error); } return result.result; } ``` ## Error Handling ### Backend Error Handling Functions should return descriptive error messages: ```rust pub async fn divide(args: DivideArgs) -> Result<f64, String> { if args.b == 0.0 { return Err("Division by zero is not allowed".to_string()); } Ok(args.a / args.b) } ``` ### Frontend Error Handling ```typescript try { const result = await invoke('divide', { a: 10, b: 0 }); } catch (error) { console.error('Division failed:', error.message); // Handle error appropriately } ``` ## Debugging and Logging ### Backend Logging The system uses the `tracing` crate for structured logging: ```rust tracing::info!("IPC request [{}]: cmd='{}'", request_id, request.cmd); tracing::error!("Nano command '{}' failed: {}", cmd, error); ``` ### Frontend Debugging Enable debug mode to see detailed logs: ```typescript configure({ debug: true }); // Now all IPC calls will be logged to console ``` ## Security Considerations ### Input Validation Always validate inputs in your functions: ```rust pub async fn create_user(args: CreateUserArgs) -> Result<UserInfo, String> { // Validate email format if !args.email.contains('@') { return Err("Invalid email format".to_string()); } // Validate age range if args.age == 0 || args.age > 150 { return Err("Invalid age".to_string()); } // Create user... } ``` ### Rate Limiting Consider implementing rate limiting for resource-intensive operations: ```rust use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; static LAST_CALL: AtomicU64 = AtomicU64::new(0); pub async fn expensive_operation(args: Args) -> Result<Data, String> { let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); let last = LAST_CALL.load(Ordering::Relaxed); if now - last < 1 { // 1 second rate limit return Err("Rate limit exceeded".to_string()); } LAST_CALL.store(now, Ordering::Relaxed); // Continue with operation... } ``` ## Performance Optimization ### 1. Lazy Initialization Use lazy initialization for expensive resources: ```rust use once_cell::sync::Lazy; static DATABASE: Lazy<Database> = Lazy::new(|| { Database::connect("sqlite://app.db").expect("Failed to connect to database") }); ``` ### 2. Connection Pooling For database operations, use connection pooling: ```rust use sqlx::SqlitePool; use once_cell::sync::Lazy; static DB_POOL: Lazy<SqlitePool> = Lazy::new(|| { SqlitePool::connect("sqlite://app.db").await.expect("Failed to create pool") }); ``` ### 3. Async Best Practices - Use `tokio::spawn` for CPU-intensive tasks - Use `tokio::time::timeout` for operations with time limits - Prefer streaming for large data transfers ## Testing ### Unit Testing Functions ```rust #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_greet_function() { let args = GreetArgs { name: "Test User".to_string(), }; let result = greet(args).await; assert!(result.is_ok()); assert_eq!(result.unwrap(), "Hello, Test User!"); } #[tokio::test] async fn test_greet_empty_name() { let args = GreetArgs { name: "".to_string(), }; let result = greet(args).await; assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Name cannot be empty"); } } ``` ### Integration Testing ```rust #[cfg(test)] mod integration_tests { use super::*; use axum::http::StatusCode; use axum::test::TestRequest; #[tokio::test] async fn test_ipc_endpoint() { // Register functions register_all_nano_functions(); let app = create_test_app(); let request = TestRequest::post("/ipc") .json(&IpcRequest { cmd: "greet".to_string(), args: serde_json::json!({"name": "Test"}), id: None, }) .build(); let response = app.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); } } ``` ## Best Practices ### 1. Function Organization - Keep simple functions in `nano_impl.rs` - Put complex domain logic in separate modules - Use `external_systems/` for third-party integrations ### 2. Type Design - Use descriptive type names - Add validation in the deserialize step when possible - Prefer `Option<T>` over nullable types ### 3. Error Messages - Provide user-friendly error messages - Include context about what went wrong - Use error codes for programmatic handling ### 4. Documentation - Document all public functions with `///` comments - Include usage examples in function docs - Maintain type definitions with clear field descriptions ## Extending the System ### Adding New Commands 1. Define argument and return types in `nano_types.rs` 2. Implement function in appropriate module 3. Register in `nano_registry.rs` 4. Test with frontend ### Creating Custom Command Types ```rust // For functions that need special handling pub type SpecialCommandHandler = Arc<dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value, IpcError>> + Send>> + Send + Sync>; impl SimpleRegistry { pub fn register_special(&self, name: &str, handler: SpecialCommandHandler) { // Custom registration logic } } ``` ## Future Enhancements ### WebSocket Support The system is designed to support WebSocket communication for real-time features: - Bidirectional communication - Server-initiated events - Real-time data streaming ### Command Discovery Automatic command discovery and documentation generation: - List available commands - Generate API documentation - Runtime introspection ### Middleware System Pluggable middleware for: - Authentication - Rate limiting - Request/response transformation - Caching This IPC system provides a solid foundation for building complex desktop applications with the Nano Framework while maintaining simplicity and type safety.