Skip to main content

host_extensions/
registry.rs

1use crate::trait_def::{HostExtension, HostPushEvent};
2
3/// Registry of active extensions for an app instance.
4///
5/// Built from the app's manifest permissions and stored on the SPA tab.
6/// The registry owns the extension objects and provides the glue between
7/// the native WKScriptMessageHandler callbacks and each extension's handler.
8pub struct ExtensionRegistry {
9    extensions: Vec<Box<dyn HostExtension>>,
10}
11
12impl Default for ExtensionRegistry {
13    fn default() -> Self {
14        Self::new()
15    }
16}
17
18impl ExtensionRegistry {
19    pub fn new() -> Self {
20        Self {
21            extensions: Vec::new(),
22        }
23    }
24
25    /// Register an extension.
26    pub fn register(&mut self, ext: Box<dyn HostExtension>) {
27        log::info!(
28            "[host-extensions] registering extension: namespace={} channel={}",
29            ext.namespace(),
30            ext.channel()
31        );
32        self.extensions.push(ext);
33    }
34
35    /// Get all registered extensions.
36    pub fn extensions(&self) -> &[Box<dyn HostExtension>] {
37        &self.extensions
38    }
39
40    /// Returns the concatenated JS injection scripts for all registered extensions.
41    ///
42    /// Inject this string into the WebView after `HOST_API_BRIDGE_SCRIPT` so
43    /// that `window.host.ext` is populated before the SPA's own scripts run.
44    pub fn combined_inject_script(&self) -> String {
45        // Initialise window.host.ext if the core bridge hasn't done so yet.
46        let mut out = String::from(
47            "(function(){\
48                if(!window.host){window.host={};}\
49                if(!window.host.ext){window.host.ext={};}\
50            })();\n",
51        );
52        for ext in &self.extensions {
53            out.push_str(ext.inject_script());
54            out.push('\n');
55        }
56        out
57    }
58
59    /// Returns all channel names that need WKScriptMessageHandler registration.
60    pub fn channels(&self) -> Vec<&str> {
61        self.extensions.iter().map(|e| e.channel()).collect()
62    }
63
64    /// Find extensions by channel name and dispatch a message to the first
65    /// one that claims the method.
66    ///
67    /// Returns the serialized JSON result, or `None` for fire-and-forget
68    /// messages and unknown channels.
69    pub fn dispatch(&self, channel: &str, method: &str, params: &str) -> Option<String> {
70        log::info!("[host-extensions] dispatch channel={channel} method={method}");
71        self.extensions
72            .iter()
73            .filter(|ext| ext.channel() == channel)
74            .find_map(|ext| ext.handle_message(method, params))
75    }
76
77    /// Drain queued push events from all registered extensions.
78    pub fn drain_events(&self) -> Vec<HostPushEvent> {
79        self.extensions
80            .iter()
81            .flat_map(|ext| ext.drain_events())
82            .collect()
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    struct EchoExtension;
91
92    impl HostExtension for EchoExtension {
93        fn namespace(&self) -> &str {
94            "echo"
95        }
96        fn channel(&self) -> &str {
97            "hostEchoBridge"
98        }
99        fn inject_script(&self) -> &str {
100            "// echo inject"
101        }
102        fn handle_message(&self, method: &str, params: &str) -> Option<String> {
103            Some(format!(r#"{{"method":"{method}","params":{params}}}"#))
104        }
105    }
106
107    #[test]
108    fn register_and_dispatch() {
109        let mut registry = ExtensionRegistry::new();
110        registry.register(Box::new(EchoExtension));
111
112        assert_eq!(registry.channels(), vec!["hostEchoBridge"]);
113
114        let result = registry.dispatch("hostEchoBridge", "ping", r#"{"x":1}"#);
115        assert_eq!(
116            result,
117            Some(r#"{"method":"ping","params":{"x":1}}"#.to_string())
118        );
119    }
120
121    #[test]
122    fn dispatch_unknown_channel_returns_none() {
123        let registry = ExtensionRegistry::new();
124        assert!(registry.dispatch("nope", "ping", "{}").is_none());
125    }
126
127    #[test]
128    fn combined_inject_script_contains_extension_script() {
129        let mut registry = ExtensionRegistry::new();
130        registry.register(Box::new(EchoExtension));
131        let script = registry.combined_inject_script();
132        assert!(script.contains("// echo inject"));
133        assert!(script.contains("window.host.ext"));
134    }
135
136    struct EmptySharedChannel;
137
138    impl HostExtension for EmptySharedChannel {
139        fn namespace(&self) -> &str {
140            "empty"
141        }
142        fn channel(&self) -> &str {
143            "hostBridge"
144        }
145        fn inject_script(&self) -> &str {
146            "// empty"
147        }
148        fn handle_message(&self, _method: &str, _params: &str) -> Option<String> {
149            None
150        }
151    }
152
153    struct MeshLikeSharedChannel;
154
155    impl HostExtension for MeshLikeSharedChannel {
156        fn namespace(&self) -> &str {
157            "mesh"
158        }
159        fn channel(&self) -> &str {
160            "hostBridge"
161        }
162        fn inject_script(&self) -> &str {
163            "// mesh"
164        }
165        fn handle_message(&self, method: &str, _params: &str) -> Option<String> {
166            (method == "meshStatus").then(|| r#"{"health":"healthy"}"#.to_string())
167        }
168    }
169
170    struct EventfulExtension;
171
172    impl HostExtension for EventfulExtension {
173        fn namespace(&self) -> &str {
174            "events"
175        }
176        fn channel(&self) -> &str {
177            "hostBridge"
178        }
179        fn inject_script(&self) -> &str {
180            "// events"
181        }
182        fn handle_message(&self, _method: &str, _params: &str) -> Option<String> {
183            None
184        }
185        fn drain_events(&self) -> Vec<HostPushEvent> {
186            vec![HostPushEvent {
187                event: "meshTopic".into(),
188                payload_json: r#"{"topic":"room/1"}"#.into(),
189            }]
190        }
191    }
192
193    #[test]
194    fn dispatch_tries_all_extensions_on_shared_channel() {
195        let mut registry = ExtensionRegistry::new();
196        registry.register(Box::new(EmptySharedChannel));
197        registry.register(Box::new(MeshLikeSharedChannel));
198
199        let result = registry.dispatch("hostBridge", "meshStatus", "{}");
200        assert_eq!(result, Some(r#"{"health":"healthy"}"#.to_string()));
201    }
202
203    #[test]
204    fn drain_events_collects_from_registered_extensions() {
205        let mut registry = ExtensionRegistry::new();
206        registry.register(Box::new(EventfulExtension));
207
208        let events = registry.drain_events();
209        assert_eq!(
210            events,
211            vec![HostPushEvent {
212                event: "meshTopic".into(),
213                payload_json: r#"{"topic":"room/1"}"#.into(),
214            }]
215        );
216    }
217}