Skip to main content

romance_core/addon/
websocket.rs

1use crate::addon::Addon;
2use anyhow::Result;
3use std::path::Path;
4
5pub struct WebsocketAddon;
6
7impl Addon for WebsocketAddon {
8    fn name(&self) -> &str {
9        "websocket"
10    }
11
12    fn check_prerequisites(&self, project_root: &Path) -> Result<()> {
13        super::check_romance_project(project_root)
14    }
15
16    fn is_already_installed(&self, project_root: &Path) -> bool {
17        project_root.join("backend/src/ws.rs").exists()
18    }
19
20    fn install(&self, project_root: &Path) -> Result<()> {
21        install_websocket(project_root)
22    }
23
24    fn uninstall(&self, project_root: &Path) -> Result<()> {
25        use colored::Colorize;
26
27        println!("{}", "Uninstalling WebSocket support...".bold());
28
29        // Delete files
30        if super::remove_file_if_exists(&project_root.join("backend/src/ws.rs"))? {
31            println!("  {} backend/src/ws.rs", "delete".red());
32        }
33        if super::remove_file_if_exists(
34            &project_root.join("frontend/src/lib/useWebSocket.ts"),
35        )? {
36            println!("  {} frontend/src/lib/useWebSocket.ts", "delete".red());
37        }
38
39        // Remove mod declaration from main.rs
40        super::remove_mod_from_main(project_root, "ws")?;
41
42        // Remove ws_handler route from routes/mod.rs
43        super::remove_line_from_file(
44            &project_root.join("backend/src/routes/mod.rs"),
45            "ws_handler",
46        )?;
47
48        // Remove feature flag
49        super::remove_feature_flag(project_root, "websocket")?;
50
51        // Regenerate AI context
52        crate::ai_context::regenerate(project_root).ok();
53
54        println!();
55        println!(
56            "{}",
57            "WebSocket support uninstalled successfully.".green().bold()
58        );
59
60        Ok(())
61    }
62}
63
64fn install_websocket(project_root: &Path) -> Result<()> {
65    use crate::template::TemplateEngine;
66    use crate::utils;
67    use colored::Colorize;
68    use tera::Context;
69
70    println!("{}", "Installing WebSocket support...".bold());
71
72    let engine = TemplateEngine::new()?;
73    let ctx = Context::new();
74
75    // Generate backend ws module
76    let content = engine.render("addon/websocket/ws.rs.tera", &ctx)?;
77    utils::write_file(&project_root.join("backend/src/ws.rs"), &content)?;
78    println!("  {} backend/src/ws.rs", "create".green());
79
80    // Generate frontend useWebSocket hook
81    let content = engine.render("addon/websocket/useWebSocket.ts.tera", &ctx)?;
82    utils::write_file(
83        &project_root.join("frontend/src/lib/useWebSocket.ts"),
84        &content,
85    )?;
86    println!("  {} frontend/src/lib/useWebSocket.ts", "create".green());
87
88    // Add `mod ws;` to main.rs if not present
89    super::add_mod_to_main(project_root, "ws")?;
90
91    // Inject WS route into routes/mod.rs via MIDDLEWARE marker
92    utils::insert_at_marker(
93        &project_root.join("backend/src/routes/mod.rs"),
94        "// === ROMANCE:MIDDLEWARE ===",
95        "        .route(\"/ws\", axum::routing::get(crate::ws::ws_handler))",
96    )?;
97    println!(
98        "  {} backend/src/routes/mod.rs (added /ws route)",
99        "update".green()
100    );
101
102    // Add WebSocketState to AppState in routes/mod.rs
103    let routes_path = project_root.join("backend/src/routes/mod.rs");
104    let routes_content = std::fs::read_to_string(&routes_path)?;
105
106    if !routes_content.contains("pub ws:") {
107        // Add use import for ws module
108        let routes_content = if !routes_content.contains("use crate::ws::WebSocketState;") {
109            routes_content.replace(
110                "use crate::events::EventBus;",
111                "use crate::events::EventBus;\nuse crate::ws::WebSocketState;",
112            )
113        } else {
114            routes_content
115        };
116
117        // Add ws field to AppState struct
118        let routes_content = routes_content.replace(
119            "    pub event_bus: EventBus,\n}",
120            "    pub event_bus: EventBus,\n    pub ws: WebSocketState,\n}",
121        );
122
123        // Add WebSocketState construction and event bridge spawn in create_router
124        let routes_content = routes_content.replace(
125            "    let event_bus = EventBus::new();\n    let state = AppState { db, event_bus };",
126            "    let event_bus = EventBus::new();\n    let ws = WebSocketState::new();\n\n    // Bridge entity events to WebSocket clients\n    tokio::spawn(crate::ws::bridge_events(event_bus.clone(), ws.clone()));\n\n    let state = AppState { db, event_bus, ws };",
127        );
128
129        std::fs::write(&routes_path, routes_content)?;
130        println!(
131            "  {} backend/src/routes/mod.rs (added WebSocketState to AppState)",
132            "update".green()
133        );
134    }
135
136    // Add axum ws feature to Cargo.toml
137    // The scaffold already has axum = { version = "0.8", features = ["json"] }
138    // We need to add the "ws" feature
139    let cargo_path = project_root.join("backend/Cargo.toml");
140    let cargo_content = std::fs::read_to_string(&cargo_path)?;
141    if cargo_content.contains("axum") && !cargo_content.contains("\"ws\"") {
142        let new_content = cargo_content.replace(
143            r#"features = ["json"]"#,
144            r#"features = ["json", "ws"]"#,
145        );
146        std::fs::write(&cargo_path, new_content)?;
147        println!(
148            "  {} backend/Cargo.toml (added ws feature to axum)",
149            "update".green()
150        );
151    }
152
153    // Update romance.toml
154    super::update_feature_flag(project_root, "websocket", true)?;
155
156    println!();
157    println!(
158        "{}",
159        "WebSocket support installed successfully!".green().bold()
160    );
161    println!("  Backend: WebSocket endpoint at /ws");
162    println!("  Frontend: import {{ useWebSocket }} from '@/lib/useWebSocket'");
163    println!("  Entity events are automatically broadcast to connected clients.");
164    println!();
165    println!("  Usage example (frontend):");
166    println!("    const {{ messages, sendMessage, isConnected }} = useWebSocket('ws://localhost:3000/ws');");
167
168    Ok(())
169}