Skip to main content

deckyfx_dioxus_ipc_bridge/
bridge.rs

1//! IPC Bridge - JavaScript/Rust communication layer
2//!
3//! This module provides the core bridge between JavaScript and Rust, enabling
4//! bidirectional communication using an HTTP-like API.
5
6use crate::platform;
7use crate::plugin::BridgePlugin;
8use std::time::Duration;
9
10/// Configuration for the IPC bridge
11#[derive(Clone, PartialEq)]
12pub struct IpcBridge {
13    /// Request timeout duration
14    pub timeout: Duration,
15    /// Custom bridge JavaScript (if provided)
16    pub custom_script: Option<String>,
17    /// Registered plugins
18    plugins: Vec<String>, // Store plugin names for now
19}
20
21impl IpcBridge {
22    /// Create a new IPC bridge builder
23    pub fn builder() -> IpcBridgeBuilder {
24        IpcBridgeBuilder::new()
25    }
26
27    /// Generate the complete bridge initialization script
28    ///
29    /// This includes the core bridge script plus any plugin scripts
30    pub fn generate_script(&self) -> String {
31        let script = if let Some(custom) = &self.custom_script {
32            custom.clone()
33        } else {
34            Self::default_bridge_script(self.timeout.as_millis() as u32)
35        };
36
37        // Add plugin scripts (would iterate over actual plugins in full implementation)
38        // For now, just return the base script
39        script
40    }
41
42    /// Initialize the bridge in the JavaScript runtime
43    pub fn initialize(&self) {
44        let script = self.generate_script();
45        platform::eval_js(&script);
46    }
47
48    /// Default bridge script generation
49    fn default_bridge_script(timeout_ms: u32) -> String {
50        format!(r#"
51        // Complete Dioxus Bridge - Unified IPC API
52        // Preserve existing properties (like IPCBridge from React) if already initialized
53        if (!window.dioxusBridge) {{
54            window.dioxusBridge = {{}};
55        }}
56
57        // Preserve IPCBridge if it was already attached by React
58        const existingIPCBridge = window.dioxusBridge.IPCBridge;
59
60        // Preserve or create callbacks Map (MUST be same reference)
61        if (!window.dioxusBridge.callbacks) {{
62            window.dioxusBridge.callbacks = new Map();
63        }}
64        const callbacks = window.dioxusBridge.callbacks;
65
66        // Add/update core Rust-provided properties
67        Object.assign(window.dioxusBridge, {{
68            // Internal callback storage (previously window.dioxusBridgeCallbacks)
69            callbacks: callbacks,
70
71            // HTTP-like fetch() method for IPC communication
72            fetch: function(url, options = {{}}) {{
73                return new Promise((resolve, reject) => {{
74                    const requestId = Math.floor(Math.random() * 1000000);
75
76                    // Store callback in namespaced location (use explicit reference)
77                    window.dioxusBridge.callbacks.set(requestId, {{resolve, reject}});
78
79                    // Build HTTP-like IPC request
80                    const request = {{
81                        id: requestId,
82                        method: options.method || 'GET',
83                        url: url,
84                        headers: options.headers || {{}},
85                        body: options.body
86                    }};
87
88                    // Send to Rust via dioxus.send()
89                    if (typeof dioxus !== 'undefined' && typeof dioxus.send === 'function') {{
90                        dioxus.send(request);
91                    }} else {{
92                        reject(new Error('dioxus.send() not available'));
93                        return;
94                    }}
95
96                    // Configurable timeout
97                    setTimeout(() => {{
98                        if (window.dioxusBridge && window.dioxusBridge.callbacks && window.dioxusBridge.callbacks.has(requestId)) {{
99                            window.dioxusBridge.callbacks.delete(requestId);
100                            reject(new Error('Request timeout after {} ms'));
101                        }}
102                    }}, {});
103                }});
104            }},
105
106            // Rust → React event emitter (previously window.rustEmit)
107            rustEmit: function(channel, data) {{
108                if (window.dioxusBridge.IPCBridge && typeof window.dioxusBridge.IPCBridge.emit === 'function') {{
109                    window.dioxusBridge.IPCBridge.emit(channel, data);
110                }} else {{
111                    console.warn('[Rust] IPCBridge not available, event not emitted:', channel);
112                }}
113            }},
114
115            // Direct IPC interface
116            ipc: {{
117                send: function(data) {{
118                    if (typeof dioxus !== 'undefined' && typeof dioxus.send === 'function') {{
119                        dioxus.send(data);
120                    }} else {{
121                        console.error('[Rust] dioxus.send() not available');
122                    }}
123                }},
124                hasIPCBridge: function() {{
125                    return typeof window.dioxusBridge.IPCBridge !== 'undefined';
126                }}
127            }},
128
129            // Low-level send wrapper
130            send: function(data) {{
131                if (window.dioxusBridge && window.dioxusBridge.ipc) {{
132                    window.dioxusBridge.ipc.send(data);
133                }}
134            }}
135        }});
136
137        // Restore IPCBridge if it existed before
138        if (existingIPCBridge) {{
139            window.dioxusBridge.IPCBridge = existingIPCBridge;
140        }}
141
142        console.log('[Rust] window.dioxusBridge ready (Unified IPC API)');
143        new Promise(() => {{}}); // Keep eval alive
144        "#, timeout_ms, timeout_ms)
145    }
146}
147
148/// Builder for IpcBridge configuration
149pub struct IpcBridgeBuilder {
150    timeout: Duration,
151    custom_script: Option<String>,
152    plugins: Vec<Box<dyn BridgePlugin>>,
153}
154
155impl IpcBridgeBuilder {
156    /// Create a new builder with default settings
157    pub fn new() -> Self {
158        Self {
159            timeout: Duration::from_secs(10),
160            custom_script: None,
161            plugins: Vec::new(),
162        }
163    }
164
165    /// Set the request timeout duration
166    ///
167    /// Default is 10 seconds
168    ///
169    /// # Example
170    /// ```rust,no_run
171    /// use std::time::Duration;
172    /// use dioxus_ipc_bridge::IpcBridge;
173    ///
174    /// let bridge = IpcBridge::builder()
175    ///     .timeout(Duration::from_secs(30))
176    ///     .build();
177    /// ```
178    pub fn timeout(mut self, duration: Duration) -> Self {
179        self.timeout = duration;
180        self
181    }
182
183    /// Provide a custom bridge script
184    ///
185    /// Use this to completely replace the default bridge JavaScript
186    pub fn custom_script(mut self, script: String) -> Self {
187        self.custom_script = Some(script);
188        self
189    }
190
191    /// Add a plugin to extend bridge functionality
192    ///
193    /// # Example
194    /// ```rust,ignore
195    /// let bridge = IpcBridge::builder()
196    ///     .plugin(Box::new(LoggingPlugin::new()))
197    ///     .plugin(Box::new(AuthPlugin::new()))
198    ///     .build();
199    /// ```
200    pub fn plugin(mut self, plugin: Box<dyn BridgePlugin>) -> Self {
201        self.plugins.push(plugin);
202        self
203    }
204
205    /// Build the IpcBridge configuration
206    pub fn build(self) -> IpcBridge {
207        IpcBridge {
208            timeout: self.timeout,
209            custom_script: self.custom_script,
210            plugins: self.plugins.iter().map(|p| p.name().to_string()).collect(),
211        }
212    }
213}
214
215impl Default for IpcBridgeBuilder {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221/// Generate the default Dioxus Bridge initialization script
222///
223/// This creates a window.dioxusBridge object that provides an HTTP-like fetch API
224/// for React to communicate with Rust via dioxus.send().
225///
226/// The bridge handles:
227/// - Request/response pairing via unique IDs
228/// - Promise-based async communication
229/// - Configurable timeout for requests
230/// - Error handling for failed calls
231/// - HTTP-like request/response format
232///
233/// # Returns
234/// JavaScript code string that sets up window.dioxusBridge
235///
236/// # Example JavaScript Usage
237/// ```javascript
238/// // GET request
239/// const response = await window.dioxusBridge.fetch('ipc://calculator/fibonacci?number=10');
240/// console.log(response.body.result); // 55
241///
242/// // POST request with body
243/// const response = await window.dioxusBridge.fetch('ipc://form/submit', {
244///     method: 'POST',
245///     headers: { 'Content-Type': 'application/json' },
246///     body: { name: 'John', email: 'john@example.com' }
247/// });
248/// ```
249pub fn generate_dioxus_bridge_script() -> &'static str {
250    // Default 10-second timeout
251    r#"
252        // Complete Dioxus Bridge - Unified IPC API
253        // Preserve existing properties (like IPCBridge from React) if already initialized
254        if (!window.dioxusBridge) {
255            window.dioxusBridge = {};
256        }
257
258        // Preserve IPCBridge if it was already attached by React
259        const existingIPCBridge = window.dioxusBridge.IPCBridge;
260
261        // Preserve or create callbacks Map (MUST be same reference)
262        if (!window.dioxusBridge.callbacks) {
263            window.dioxusBridge.callbacks = new Map();
264        }
265        const callbacks = window.dioxusBridge.callbacks;
266
267        // Add/update core Rust-provided properties
268        Object.assign(window.dioxusBridge, {
269            // Internal callback storage (previously window.dioxusBridgeCallbacks)
270            callbacks: callbacks,
271
272            // HTTP-like fetch() method for IPC communication
273            fetch: function(url, options = {}) {
274                return new Promise((resolve, reject) => {
275                    const requestId = Math.floor(Math.random() * 1000000);
276
277                    // Store callback in namespaced location (use explicit reference)
278                    window.dioxusBridge.callbacks.set(requestId, {resolve, reject});
279
280                    // Build HTTP-like IPC request
281                    const request = {
282                        id: requestId,
283                        method: options.method || 'GET',
284                        url: url,
285                        headers: options.headers || {},
286                        body: options.body
287                    };
288
289                    // Send to Rust via dioxus.send()
290                    if (typeof dioxus !== 'undefined' && typeof dioxus.send === 'function') {
291                        dioxus.send(request);
292                    } else {
293                        reject(new Error('dioxus.send() not available'));
294                        return;
295                    }
296
297                    // 10 second timeout
298                    setTimeout(() => {
299                        if (window.dioxusBridge && window.dioxusBridge.callbacks && window.dioxusBridge.callbacks.has(requestId)) {
300                            window.dioxusBridge.callbacks.delete(requestId);
301                            reject(new Error('Request timeout after 10 seconds'));
302                        }
303                    }, 10000);
304                });
305            },
306
307            // Rust → React event emitter (previously window.rustEmit)
308            rustEmit: function(channel, data) {
309                if (window.dioxusBridge.IPCBridge && typeof window.dioxusBridge.IPCBridge.emit === 'function') {
310                    window.dioxusBridge.IPCBridge.emit(channel, data);
311                } else {
312                    console.warn('[Rust] IPCBridge not available, event not emitted:', channel);
313                }
314            },
315
316            // Direct IPC interface
317            ipc: {
318                send: function(data) {
319                    if (typeof dioxus !== 'undefined' && typeof dioxus.send === 'function') {
320                        dioxus.send(data);
321                    } else {
322                        console.error('[Rust] dioxus.send() not available');
323                    }
324                },
325                hasIPCBridge: function() {
326                    return typeof window.dioxusBridge.IPCBridge !== 'undefined';
327                }
328            },
329
330            // Low-level send wrapper
331            send: function(data) {
332                if (window.dioxusBridge && window.dioxusBridge.ipc) {
333                    window.dioxusBridge.ipc.send(data);
334                }
335            }
336        });
337
338        // Restore IPCBridge if it existed before
339        if (existingIPCBridge) {
340            window.dioxusBridge.IPCBridge = existingIPCBridge;
341        }
342
343        console.log('[Rust] window.dioxusBridge ready (Unified IPC API)');
344        new Promise(() => {}); // Keep eval alive
345    "#
346}
347
348/// Emit an event from Rust to JavaScript/React
349///
350/// This allows Rust to actively push data to the JavaScript side.
351/// React components can subscribe to these events using the bridge's event system.
352///
353/// # Arguments
354/// * `channel` - The event channel name (e.g., "notification", "progress", "data:stream")
355/// * `data` - The data to send (must be JSON-serializable)
356///
357/// # Example
358/// ```rust
359/// use serde_json::json;
360/// use dioxus_ipc_bridge::bridge::emit;
361///
362/// // Emit a notification
363/// emit("notification", json!({
364///     "type": "info",
365///     "message": "Processing complete!"
366/// }));
367///
368/// // Stream progress updates
369/// emit("progress", json!({
370///     "percent": 75,
371///     "status": "Almost done..."
372/// }));
373/// ```
374pub fn emit(channel: &str, data: serde_json::Value) {
375    let data_json = data.to_string();
376    let script = format!(
377        r#"
378        if (window.dioxusBridge && window.dioxusBridge.rustEmit) {{
379            window.dioxusBridge.rustEmit('{}', {});
380        }}
381        "#,
382        channel, data_json
383    );
384    platform::eval_js(&script);
385}