@isthatuzii/create-nano-app
Version:
Desktop application scaffolding tool for the Nano Framework
464 lines (378 loc) • 14.3 kB
Markdown
# 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.