Skip to main content

euv_ui/component/bridge/hook/
impl.rs

1use crate::*;
2
3/// Implementation of native bridge functionality.
4impl UseEuvNativeBridge {
5    /// Creates native bridge state for accessing platform-native features.
6    ///
7    /// Initializes all signals with default values and sets `available` to `false`.
8    /// The actual data is loaded asynchronously via `Self::load_data`.
9    ///
10    /// # Returns
11    ///
12    /// - `UseEuvNativeBridge`: The native bridge state.
13    pub fn use_bridge_state() -> UseEuvNativeBridge {
14        UseEuvNativeBridge::new(
15            App::use_signal(|| false),
16            App::use_signal(|| true),
17            App::use_signal(String::new),
18        )
19    }
20
21    /// Asynchronously loads native bridge data and updates the provided state signals.
22    ///
23    /// First checks platform availability via `BridgeConfig::is_available`. If unavailable,
24    /// sets `available` to `false` and returns. Otherwise, invokes the
25    /// `resolve_bridge_group_permissions` command, then populates the corresponding
26    /// signal from the result. If the invoke fails, sets `available` to `false`
27    /// so the card is hidden.
28    ///
29    /// # Arguments
30    ///
31    /// - `Option<BridgeConfig>`: Optional custom bridge configuration.
32    pub fn load_data(self, config: Option<BridgeConfig>) {
33        if !BridgeConfig::is_available(config.as_ref()) {
34            self.get_available().set(false);
35            self.get_loading().set(false);
36            return;
37        }
38        let permissions_state: UseEuvNativeBridge = self;
39        let cfg: Option<BridgeConfig> = config;
40        spawn_local(async move {
41            let args_obj: Object = Object::new();
42            Reflect::set(
43                &args_obj,
44                &JsValue::from_str(BRIDGE_GROUP_KEY),
45                &JsValue::from_str(BRIDGE_GROUP_ALL),
46            )
47            .unwrap_or(false);
48            let permissions_result: Result<JsValue, String> = match BridgeConfig::invoke(
49                INVOKE_RESOLVE_BRIDGE_GROUP_PERMISSIONS,
50                Some(&args_obj),
51                cfg.as_ref(),
52            ) {
53                Ok(promise) => {
54                    let future: JsFuture = JsFuture::from(promise);
55                    match future.await {
56                        Ok(value) => Ok(value),
57                        Err(error) => Err(format!("{error:?}")),
58                    }
59                }
60                Err(error) => Err(error),
61            };
62            match permissions_result {
63                Ok(value) => {
64                    let permissions_array: Vec<String> = value
65                        .dyn_into::<Array>()
66                        .map(|array: Array| {
67                            array
68                                .iter()
69                                .filter_map(|item: JsValue| item.as_string())
70                                .collect::<Vec<String>>()
71                        })
72                        .unwrap_or_default();
73                    permissions_state
74                        .get_permissions()
75                        .set(permissions_array.join(", "));
76                    permissions_state.get_available().set(true);
77                }
78                Err(_) => {
79                    permissions_state.get_available().set(false);
80                }
81            }
82            permissions_state.get_loading().set(false);
83        });
84    }
85}
86
87/// Implementation of cache update functionality.
88impl UseCacheUpdate {
89    /// Creates cache update state for tracking documentation build status.
90    ///
91    /// Initializes `doc_status` to `false`, `version` to an empty string,
92    /// and `updating` to `false`. The actual data is loaded asynchronously
93    /// via `Self::load`.
94    ///
95    /// # Returns
96    ///
97    /// - `UseCacheUpdate`: The cache update state.
98    pub fn use_cache_state() -> UseCacheUpdate {
99        UseCacheUpdate::new(
100            App::use_signal(|| false),
101            App::use_signal(String::new),
102            App::use_signal(|| false),
103        )
104    }
105
106    /// Asynchronously fetches the docs.rs status JSON and invokes the bridge
107    /// `update_cache` command to synchronize the local cache.
108    ///
109    /// First fetches `DOCS_STATUS_URL` and parses the `{ "doc_status": bool,
110    /// "version": string }` JSON payload into the provided state signals.
111    /// If the remote version is greater than `EUV_VERSION` and the bridge
112    /// is available, invokes the `update_cache` command via
113    /// `window.bridge.core.invoke("update_cache")` and updates the
114    /// `updating` signal accordingly.
115    ///
116    /// # Arguments
117    ///
118    /// - `&str`: The current version string for comparison.
119    /// - `Option<BridgeConfig>`: Optional custom bridge configuration.
120    pub fn load(self, current_version: &str, config: Option<BridgeConfig>) {
121        let doc_status_signal: Signal<bool> = self.get_doc_status();
122        let version_signal: Signal<String> = self.get_version();
123        let updating_signal: Signal<bool> = self.get_updating();
124        let version_to_compare: String = current_version.to_string();
125        let cfg: Option<BridgeConfig> = config;
126        spawn_local(async move {
127            let window_value: Window = window().expect("no global window exists");
128            let promise: Promise = window_value.fetch_with_str(DOCS_STATUS_URL);
129            let future: JsFuture = JsFuture::from(promise);
130            let response: JsValue = match future.await {
131                Ok(value) => value,
132                Err(error) => {
133                    Console::error(&format!(
134                        "Failed to fetch version status: {}",
135                        error.as_string().unwrap_or_default()
136                    ));
137                    return;
138                }
139            };
140            let response_value: Response = match response.dyn_into() {
141                Ok(value) => value,
142                Err(error) => {
143                    Console::error(&format!(
144                        "Failed to convert fetch response: {}",
145                        error.as_string().unwrap_or_default()
146                    ));
147                    return;
148                }
149            };
150            let text_promise: Promise = match response_value.text() {
151                Ok(promise) => promise,
152                Err(error) => {
153                    Console::error(&format!(
154                        "Failed to get response text promise: {}",
155                        error.as_string().unwrap_or_default()
156                    ));
157                    return;
158                }
159            };
160            let text_future: JsFuture = JsFuture::from(text_promise);
161            let text: JsValue = match text_future.await {
162                Ok(value) => value,
163                Err(error) => {
164                    Console::error(&format!(
165                        "Failed to read response text: {}",
166                        error.as_string().unwrap_or_default()
167                    ));
168                    return;
169                }
170            };
171            let text_string: String = text.as_string().unwrap_or_default();
172            Console::log(&text_string);
173            let parsed: DocsStatus =
174                serde_json::from_str::<DocsStatus>(&text_string).unwrap_or_default();
175            doc_status_signal.set(parsed.get_doc_status());
176            version_signal.set(parsed.get_version().clone());
177            if !matches!(
178                CompareVersion::compare_version(parsed.get_version(), &version_to_compare),
179                Ok(VersionLevel::Greater)
180            ) {
181                Console::log(&format!(
182                    "Current version v{version_to_compare} is already the latest version"
183                ));
184                return;
185            }
186            if !BridgeConfig::is_available(cfg.as_ref()) {
187                return;
188            }
189            updating_signal.set(true);
190            if let Ok(promise) = BridgeConfig::invoke(INVOKE_UPDATE_CACHE, None, cfg.as_ref()) {
191                let future: JsFuture = JsFuture::from(promise);
192                match future.await {
193                    Ok(result) => Console::log(&result.as_string().unwrap_or_default()),
194                    Err(error) => Console::error(&error.as_string().unwrap_or_default()),
195                }
196            }
197            updating_signal.set(false);
198        });
199    }
200}
201
202/// Implementation of bridge configuration and invocation.
203impl BridgeConfig {
204    /// Checks whether the bridge native bridge is available on the current platform.
205    ///
206    /// Looks up `window.___.core` via `Reflect` to determine if the
207    /// bridge runtime is present. Returns `false` if the property chain does not exist
208    /// or if any reflection error occurs.
209    ///
210    /// # Arguments
211    ///
212    /// - `Option<&BridgeConfig>`: Optional custom bridge configuration.
213    ///
214    /// # Returns
215    ///
216    /// - `bool`: `true` if the bridge core module is available.
217    pub(crate) fn is_available(config: Option<&BridgeConfig>) -> bool {
218        let cfg: BridgeConfig =
219            config.map_or_else(BridgeConfig::default, |c: &BridgeConfig| c.clone());
220        let window_value: Window = window().expect("no global window exists");
221        let bridge_key: JsValue = JsValue::from_str(cfg.global_key);
222        let bridge_obj: JsValue = match Reflect::get(&window_value, &bridge_key) {
223            Ok(value) => value,
224            Err(_) => return false,
225        };
226        if bridge_obj.is_undefined() || bridge_obj.is_null() {
227            return false;
228        }
229        let core_key: JsValue = JsValue::from_str(cfg.core_key);
230        let core_obj: JsValue = match Reflect::get(&bridge_obj, &core_key) {
231            Ok(value) => value,
232            Err(_) => return false,
233        };
234        !core_obj.is_undefined() && !core_obj.is_null()
235    }
236
237    /// Invokes a bridge core command by name via `window.___.core.invoke`.
238    ///
239    /// Resolves the `___` → `core` → `invoke` property chain on the global
240    /// `window` object, then calls `invoke` with the given command name and
241    /// optional arguments object. Returns the resulting `Promise`, or an
242    /// error string if any step in the reflection chain fails.
243    ///
244    /// # Arguments
245    ///
246    /// - `&str`: The bridge command name to invoke.
247    /// - `Option<&JsValue>`: Optional arguments object to pass to the command.
248    /// - `Option<&BridgeConfig>`: Optional custom bridge configuration.
249    ///
250    /// # Returns
251    ///
252    /// - `Result<Promise, String>`: The promise returned by the invoke call, or an error message.
253    pub(crate) fn invoke(
254        command: &str,
255        args: Option<&JsValue>,
256        config: Option<&BridgeConfig>,
257    ) -> Result<Promise, String> {
258        let cfg: BridgeConfig =
259            config.map_or_else(BridgeConfig::default, |c: &BridgeConfig| c.clone());
260        let window_value: Window = window().expect("no global window exists");
261        let bridge_key: JsValue = JsValue::from_str(cfg.global_key);
262        let bridge_obj: JsValue = Reflect::get(&window_value, &bridge_key)
263            .map_err(|error: JsValue| format!("{error:?}"))?;
264        let core_key: JsValue = JsValue::from_str(cfg.core_key);
265        let core_obj: JsValue =
266            Reflect::get(&bridge_obj, &core_key).map_err(|error: JsValue| format!("{error:?}"))?;
267        let invoke_key: JsValue = JsValue::from_str(cfg.invoke_key);
268        let invoke_fn: JsValue =
269            Reflect::get(&core_obj, &invoke_key).map_err(|error: JsValue| format!("{error:?}"))?;
270        let invoke_function: Function = invoke_fn
271            .dyn_into::<Function>()
272            .map_err(|error: JsValue| format!("{error:?}"))?;
273        let command_value: JsValue = JsValue::from_str(command);
274        let result: JsValue = match args {
275            Some(arguments) => invoke_function
276                .call2(&core_obj, &command_value, arguments)
277                .map_err(|error: JsValue| format!("{error:?}"))?,
278            None => invoke_function
279                .call1(&core_obj, &command_value)
280                .map_err(|error: JsValue| format!("{error:?}"))?,
281        };
282        result
283            .dyn_into::<Promise>()
284            .map_err(|error: JsValue| format!("{error:?}"))
285    }
286}