rhailib/docs/EVENT_DRIVEN_FLOW_ARCHITECTURE.md

13 KiB

Event-Driven Flow Architecture

Overview

A simple, single-threaded architecture where API calls trigger HTTP requests and spawn new Rhai scripts based on responses. No global state, no polling, no blocking - just clean event-driven flows.

Core Concept

graph LR
    RS1[Rhai Script] --> API[create_payment_intent]
    API --> HTTP[HTTP Request]
    HTTP --> SPAWN[Spawn Thread]
    SPAWN --> WAIT[Wait for Response]
    WAIT --> SUCCESS[200 OK]
    WAIT --> ERROR[Error]
    SUCCESS --> RS2[new_payment_intent.rhai]
    ERROR --> RS3[payment_failed.rhai]

Architecture Design

1. Simple Flow Manager

use std::thread;
use std::collections::HashMap;
use reqwest::Client;
use rhai::{Engine, Scope};

pub struct FlowManager {
    pub client: Client,
    pub engine: Engine,
    pub flow_scripts: HashMap<String, String>, // event_name -> script_path
}

impl FlowManager {
    pub fn new() -> Self {
        let mut flow_scripts = HashMap::new();
        
        // Define flow mappings
        flow_scripts.insert("payment_intent_created".to_string(), "flows/payment_intent_created.rhai".to_string());
        flow_scripts.insert("payment_intent_failed".to_string(), "flows/payment_intent_failed.rhai".to_string());
        flow_scripts.insert("product_created".to_string(), "flows/product_created.rhai".to_string());
        flow_scripts.insert("subscription_created".to_string(), "flows/subscription_created.rhai".to_string());
        
        Self {
            client: Client::new(),
            engine: Engine::new(),
            flow_scripts,
        }
    }
    
    // Fire HTTP request and spawn response handler
    pub fn fire_and_continue(&self, 
        endpoint: String, 
        method: String, 
        data: HashMap<String, String>,
        success_event: String,
        error_event: String,
        context: HashMap<String, String>
    ) {
        let client = self.client.clone();
        let flow_scripts = self.flow_scripts.clone();
        
        // Spawn thread for HTTP request
        thread::spawn(move || {
            let result = Self::make_http_request(&client, &endpoint, &method, &data);
            
            match result {
                Ok(response_data) => {
                    // Success: dispatch success flow
                    Self::dispatch_flow(&flow_scripts, &success_event, response_data, context);
                }
                Err(error) => {
                    // Error: dispatch error flow
                    let mut error_data = HashMap::new();
                    error_data.insert("error".to_string(), error);
                    Self::dispatch_flow(&flow_scripts, &error_event, error_data, context);
                }
            }
        });
        
        // Return immediately - no blocking!
    }
    
    // Execute HTTP request
    fn make_http_request(
        client: &Client, 
        endpoint: &str, 
        method: &str, 
        data: &HashMap<String, String>
    ) -> Result<HashMap<String, String>, String> {
        // This runs in spawned thread - can block safely
        let rt = tokio::runtime::Runtime::new().unwrap();
        
        rt.block_on(async {
            let url = format!("https://api.stripe.com/v1/{}", endpoint);
            
            let response = client
                .post(&url)
                .form(data)
                .send()
                .await
                .map_err(|e| format!("HTTP error: {}", e))?;
            
            let response_text = response.text().await
                .map_err(|e| format!("Response read error: {}", e))?;
            
            let json: serde_json::Value = serde_json::from_str(&response_text)
                .map_err(|e| format!("JSON parse error: {}", e))?;
            
            // Convert JSON to HashMap for Rhai
            let mut result = HashMap::new();
            if let Some(id) = json.get("id").and_then(|v| v.as_str()) {
                result.insert("id".to_string(), id.to_string());
            }
            if let Some(status) = json.get("status").and_then(|v| v.as_str()) {
                result.insert("status".to_string(), status.to_string());
            }
            
            Ok(result)
        })
    }
    
    // Dispatch new Rhai script based on event
    fn dispatch_flow(
        flow_scripts: &HashMap<String, String>,
        event_name: &str,
        response_data: HashMap<String, String>,
        context: HashMap<String, String>
    ) {
        if let Some(script_path) = flow_scripts.get(event_name) {
            println!("🎯 Dispatching flow: {} -> {}", event_name, script_path);
            
            // Create new engine instance for this flow
            let mut engine = Engine::new();
            register_payment_rhai_module(&mut engine);
            
            // Create scope with response data and context
            let mut scope = Scope::new();
            
            // Add response data
            for (key, value) in response_data {
                scope.push(key, value);
            }
            
            // Add context data
            for (key, value) in context {
                scope.push(format!("context_{}", key), value);
            }
            
            // Execute flow script
            if let Ok(script_content) = std::fs::read_to_string(script_path) {
                match engine.eval_with_scope::<()>(&mut scope, &script_content) {
                    Ok(_) => println!("✅ Flow {} completed successfully", event_name),
                    Err(e) => println!("❌ Flow {} failed: {}", event_name, e),
                }
            } else {
                println!("❌ Flow script not found: {}", script_path);
            }
        } else {
            println!("⚠️ No flow defined for event: {}", event_name);
        }
    }
}

2. Simple Rhai Functions

#[export_module]
mod rhai_flow_module {
    use super::*;
    
    // Global flow manager instance
    static FLOW_MANAGER: std::sync::OnceLock<FlowManager> = std::sync::OnceLock::new();
    
    #[rhai_fn(name = "init_flows")]
    pub fn init_flows() {
        FLOW_MANAGER.set(FlowManager::new()).ok();
        println!("✅ Flow manager initialized");
    }
    
    #[rhai_fn(name = "create_payment_intent")]
    pub fn create_payment_intent(
        amount: i64,
        currency: String,
        customer: String
    ) {
        let manager = FLOW_MANAGER.get().expect("Flow manager not initialized");
        
        let mut data = HashMap::new();
        data.insert("amount".to_string(), amount.to_string());
        data.insert("currency".to_string(), currency);
        data.insert("customer".to_string(), customer.clone());
        
        let mut context = HashMap::new();
        context.insert("customer_id".to_string(), customer);
        context.insert("original_amount".to_string(), amount.to_string());
        
        manager.fire_and_continue(
            "payment_intents".to_string(),
            "POST".to_string(),
            data,
            "payment_intent_created".to_string(),
            "payment_intent_failed".to_string(),
            context
        );
        
        println!("🚀 Payment intent creation started");
        // Returns immediately!
    }
    
    #[rhai_fn(name = "create_product")]
    pub fn create_product(name: String, description: String) {
        let manager = FLOW_MANAGER.get().expect("Flow manager not initialized");
        
        let mut data = HashMap::new();
        data.insert("name".to_string(), name.clone());
        data.insert("description".to_string(), description);
        
        let mut context = HashMap::new();
        context.insert("product_name".to_string(), name);
        
        manager.fire_and_continue(
            "products".to_string(),
            "POST".to_string(),
            data,
            "product_created".to_string(),
            "product_failed".to_string(),
            context
        );
        
        println!("🚀 Product creation started");
    }
    
    #[rhai_fn(name = "create_subscription")]
    pub fn create_subscription(customer: String, price_id: String) {
        let manager = FLOW_MANAGER.get().expect("Flow manager not initialized");
        
        let mut data = HashMap::new();
        data.insert("customer".to_string(), customer.clone());
        data.insert("items[0][price]".to_string(), price_id.clone());
        
        let mut context = HashMap::new();
        context.insert("customer_id".to_string(), customer);
        context.insert("price_id".to_string(), price_id);
        
        manager.fire_and_continue(
            "subscriptions".to_string(),
            "POST".to_string(),
            data,
            "subscription_created".to_string(),
            "subscription_failed".to_string(),
            context
        );
        
        println!("🚀 Subscription creation started");
    }
}

Usage Examples

1. Main Script (Initiator)

// main.rhai
init_flows();

print("Starting payment flow...");

// This returns immediately, spawns HTTP request
create_payment_intent(2000, "usd", "cus_customer123");

print("Payment intent request sent, continuing...");

// Script ends here, but flow continues in background

2. Success Flow Script

// flows/payment_intent_created.rhai

print("🎉 Payment intent created successfully!");
print(`Payment Intent ID: ${id}`);
print(`Status: ${status}`);
print(`Customer: ${context_customer_id}`);
print(`Amount: ${context_original_amount}`);

// Continue the flow - create subscription
if status == "requires_payment_method" {
    print("Creating subscription for customer...");
    create_subscription(context_customer_id, "price_monthly_plan");
}

3. Error Flow Script

// flows/payment_intent_failed.rhai

print("❌ Payment intent creation failed");
print(`Error: ${error}`);
print(`Customer: ${context_customer_id}`);

// Handle error - maybe retry or notify
print("Sending notification to customer...");
// Could trigger email notification flow here

4. Subscription Success Flow

// flows/subscription_created.rhai

print("🎉 Subscription created!");
print(`Subscription ID: ${id}`);
print(`Customer: ${context_customer_id}`);
print(`Price: ${context_price_id}`);

// Final step - send welcome email
print("Sending welcome email...");
// Could trigger email flow here

Flow Configuration

1. Flow Mapping

// Define in FlowManager::new()
flow_scripts.insert("payment_intent_created".to_string(), "flows/payment_intent_created.rhai".to_string());
flow_scripts.insert("payment_intent_failed".to_string(), "flows/payment_intent_failed.rhai".to_string());
flow_scripts.insert("product_created".to_string(), "flows/product_created.rhai".to_string());
flow_scripts.insert("subscription_created".to_string(), "flows/subscription_created.rhai".to_string());

2. Directory Structure

project/
├── main.rhai                          # Main script
├── flows/
│   ├── payment_intent_created.rhai    # Success flow
│   ├── payment_intent_failed.rhai     # Error flow
│   ├── product_created.rhai           # Product success
│   ├── subscription_created.rhai      # Subscription success
│   └── email_notification.rhai        # Email flow
└── src/
    └── flow_manager.rs                 # Flow manager code

Execution Flow

sequenceDiagram
    participant MS as Main Script
    participant FM as FlowManager
    participant TH as Spawned Thread
    participant API as Stripe API
    participant FS as Flow Script

    MS->>FM: create_payment_intent()
    FM->>TH: spawn thread
    FM->>MS: return immediately
    Note over MS: Script ends
    
    TH->>API: HTTP POST /payment_intents
    API->>TH: 200 OK + payment_intent data
    TH->>FS: dispatch payment_intent_created.rhai
    Note over FS: New Rhai execution
    FS->>FM: create_subscription()
    FM->>TH: spawn new thread
    TH->>API: HTTP POST /subscriptions
    API->>TH: 200 OK + subscription data
    TH->>FS: dispatch subscription_created.rhai

Benefits

1. Simplicity

  • No global state management
  • No complex polling or callbacks
  • Each flow is a simple Rhai script

2. Single-Threaded Rhai

  • Main Rhai engine never blocks
  • Each flow script runs in its own engine instance
  • No concurrency issues in Rhai code

3. Event-Driven

  • Clear separation of concerns
  • Easy to add new flows
  • Composable flow chains

4. No Blocking

  • HTTP requests happen in background threads
  • Main script continues immediately
  • Flows trigger based on responses

Advanced Features

1. Flow Chaining

// flows/payment_intent_created.rhai
if status == "requires_payment_method" {
    // Chain to next flow
    create_subscription(context_customer_id, "price_monthly");
}

2. Conditional Flows

// flows/subscription_created.rhai
if context_customer_type == "enterprise" {
    // Enterprise-specific flow
    create_enterprise_setup(context_customer_id);
} else {
    // Standard flow
    send_welcome_email(context_customer_id);
}

3. Error Recovery

// flows/payment_intent_failed.rhai
if error.contains("insufficient_funds") {
    // Retry with smaller amount
    let retry_amount = context_original_amount / 2;
    create_payment_intent(retry_amount, "usd", context_customer_id);
} else {
    // Send error notification
    send_error_notification(context_customer_id, error);
}

This architecture is much simpler, has no global state, and provides clean event-driven flows that are easy to understand and maintain.