yggdrasil/bridges/
library.rs1use leptos::prelude::*;
7
8#[cfg(target_arch = "wasm32")]
9#[wasm_bindgen::prelude::wasm_bindgen]
10extern "C" {
11 #[wasm_bindgen(js_namespace = window, js_name = __loadBrowserLibrary, catch)]
12 async fn load_browser_library(
13 name: &str,
14 ) -> Result<wasm_bindgen::JsValue, wasm_bindgen::JsValue>;
15}
16
17#[derive(Clone, Copy)]
27pub struct LibraryResource {
28 reload: RwSignal<u32>,
29 state: RwSignal<Option<Result<bool, String>>>,
30}
31
32impl LibraryResource {
33 pub fn restart(&self) {
35 self.reload.update(|v| *v = v.wrapping_add(1));
36 }
37
38 pub fn read_state(&self) -> Option<Result<bool, String>> {
40 self.state.get()
41 }
42
43 pub fn ready_untracked(&self) -> bool {
45 self.state
46 .with_untracked(|state| matches!(state, Some(Ok(true))))
47 }
48}
49
50#[derive(Clone, Copy, PartialEq, Eq)]
52pub enum BrowserLibrary {
53 Tiptap,
54 CodeMirror,
55 Xterm,
56}
57
58impl BrowserLibrary {
59 pub const fn name(self) -> &'static str {
61 match self {
62 Self::Tiptap => "tiptap",
63 Self::CodeMirror => "codemirror",
64 Self::Xterm => "xterm",
65 }
66 }
67}
68
69pub fn use_browser_library(
72 library: BrowserLibrary,
73 enabled: impl Fn() -> bool + Send + Sync + 'static,
74) -> LibraryResource {
75 let reload = RwSignal::new(0u32);
76 let state = RwSignal::new(None);
77 let generation = StoredValue::new(0u64);
79 Effect::new(move |_| {
81 reload.track();
82 let enabled = enabled();
83 let current = generation.try_update_value(|g| {
84 *g += 1;
85 *g
86 });
87 let Some(current) = current else { return };
88 #[cfg(target_arch = "wasm32")]
89 {
90 if !enabled {
91 state.set(Some(Ok(false)));
92 return;
93 }
94 state.set(None);
95 leptos::task::spawn_local(async move {
96 let result = load_browser_library(library.name())
97 .await
98 .map(|_| true)
99 .map_err(|error| {
100 web_sys::console::error_1(&error);
101 "加载失败,请检查网络后重试。".to_string()
102 });
103 if generation.try_get_value() == Some(current) {
104 let _ = state.try_set(Some(result));
105 }
106 });
107 }
108 #[cfg(not(target_arch = "wasm32"))]
109 let _ = (library, enabled, current);
110 });
111 LibraryResource { reload, state }
112}
113
114pub fn library_ready(library: LibraryResource) -> bool {
116 library.state.with(|state| matches!(state, Some(Ok(true))))
117}
118
119#[component]
121pub fn LibraryLoadError(library: LibraryResource) -> impl IntoView {
122 move || {
123 library
124 .read_state()
125 .and_then(|result| result.err())
126 .map(|message| {
127 view! {
128 <div role="alert" class="flex items-center gap-3 p-3 text-sm text-red-500 dark:text-red-400">
129 <span>{message}</span>
130 <button type="button" class="underline underline-offset-4" on:click=move |_| library.restart()>
131 "重新加载"
132 </button>
133 </div>
134 }
135 })
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use leptos::reactive::owner::Owner;
143
144 #[test]
145 fn server_side_state_stays_loading_and_not_ready() {
146 Owner::new().with(|| {
147 let library = use_browser_library(BrowserLibrary::Tiptap, || true);
148 assert!(library.read_state().is_none());
149 assert!(!library.ready_untracked());
150 assert!(!library_ready(library));
151 });
152 }
153
154 #[test]
155 fn load_error_renders_alert_with_retry_button() {
156 Owner::new().with(|| {
157 let library = use_browser_library(BrowserLibrary::Tiptap, || true);
158 library
159 .state
160 .set(Some(Err("加载失败,请检查网络后重试。".to_string())));
161 let html = view! { <LibraryLoadError library/> }.to_html();
162 assert!(html.contains(r#"role="alert""#), "{html}");
163 assert!(html.contains("加载失败,请检查网络后重试。"), "{html}");
164 assert!(html.contains("重新加载"), "{html}");
165 });
166 }
167
168 #[test]
169 fn ready_library_renders_nothing() {
170 Owner::new().with(|| {
171 let library = use_browser_library(BrowserLibrary::Tiptap, || true);
172 library.state.set(Some(Ok(true)));
173 assert!(library_ready(library));
174 let html = view! { <LibraryLoadError library/> }.to_html();
175 assert!(!html.contains("alert"), "{html}");
176 });
177 }
178}