1use std::{
12 cell::RefCell,
13 future::{Future, poll_fn},
14 pin::Pin,
15 rc::Rc,
16 task::Poll,
17};
18
19use dioxus::core::Task;
20use dioxus::prelude::*;
21
22use super::{BridgeState, data::GraphData};
23
24struct ChannelSession {
26 identity: Rc<()>,
28 eval: document::Eval,
30 task: Task,
32}
33
34fn same_owner(current: Option<&Rc<()>>, expected: Option<&Rc<()>>) -> bool {
36 match (current, expected) {
37 (Some(current), Some(expected)) => Rc::ptr_eq(current, expected),
38 (None, None) => true,
39 _ => false,
40 }
41}
42
43async fn prefer_message<M: Future, E: Future>(
48 message: M,
49 mut ended: Pin<&mut E>,
50) -> Result<M::Output, E::Output> {
51 let mut message = std::pin::pin!(message);
52 poll_fn(|context| {
53 if let Poll::Ready(message) = message.as_mut().poll(context) {
54 return Poll::Ready(Ok(message));
55 }
56 ended.as_mut().poll(context).map(Err)
57 })
58 .await
59}
60
61impl ChannelSession {
62 fn stop(self) {
64 let _ = self.eval.send(serde_json::json!({ "kind": "close" }));
65 self.task.cancel();
66 }
67}
68
69fn graph_channel_script(container_id: &str, data: &GraphData, external_container: bool) -> String {
73 let id = serde_json::to_string(container_id).expect("strings serialize");
74 let data = serde_json::to_string(data).expect("graph data serializes");
75 format!(
76 "{}\nawait runGraphChannel(window, {id}, {data}, dioxus, {external_container});",
77 include_str!("channel.js")
78 )
79}
80
81fn channel_failed(eval: document::Eval, mut failure: Signal<Option<String>>, message: String) {
83 crate::diagnostics::report_error(&message);
84 failure.set(Some(message.clone()));
85 let _ = eval.send(serde_json::json!({ "kind": "fatal", "message": message }));
86}
87
88async fn receive_views(mut eval: document::Eval, failure: Signal<Option<String>>) {
90 let mut bridge = BridgeState::default();
91 let mut ended = std::pin::pin!(eval.join::<serde_json::Value>());
92 loop {
93 let result = async {
94 let payload =
95 match prefer_message(eval.recv::<serde_json::Value>(), ended.as_mut()).await {
96 Ok(message) => message?,
97 Err(completion) => {
98 completion?;
99 anyhow::bail!("graph channel ended unexpectedly");
100 }
101 };
102 if let Some(reply) = bridge.receive(payload)? {
103 eval.send(reply)?;
104 }
105 Ok::<(), anyhow::Error>(())
106 }
107 .await;
108 if let Err(error) = result {
109 channel_failed(eval, failure, format!("Graph view failed: {error}"));
110 return;
111 }
112 }
113}
114
115#[derive(Clone, Copy, PartialEq, Eq, Debug)]
117pub enum GraphDrive {
118 Init,
121 Update,
123}
124
125impl GraphDrive {
126 fn method(self) -> &'static str {
128 match self {
129 GraphDrive::Init => "init",
130 GraphDrive::Update => "update",
131 }
132 }
133}
134
135pub fn graph_drive_script(container_id: &str, data: &GraphData, call: GraphDrive) -> String {
147 let id = serde_json::to_string(container_id).unwrap_or_else(|_| "\"\"".to_string());
148 let json = serde_json::to_string(data).unwrap_or_else(|_| "null".to_string());
151 let method = call.method();
152 format!(
153 "window.__beginGraphData = window.__beginGraphData || Object.create(null); \
154 window.__beginGraphData[{id}] = {json}; \
155 if (typeof window.beginGraph !== 'undefined') \
156 window.beginGraph.{method}({id}, window.__beginGraphData[{id}]); \
157 dioxus.close(); return null;"
158 )
159}
160
161#[component]
191pub fn GraphView(
192 graph_id: ReadSignal<String>,
193 data: ReadSignal<GraphData>,
194 source_id: ReadSignal<String>,
195 #[props(default = false)] external_container: bool,
196) -> Element {
197 let container_id = graph_id.read().clone();
198 let mut mounted = use_signal(|| false);
199 let mut failure = use_signal(|| None::<String>);
200 let session = use_hook(|| Rc::new(RefCell::new(None::<ChannelSession>)));
201 let cleanup = session.clone();
202 use_drop(move || {
203 if let Some(session) = cleanup.borrow_mut().take() {
204 session.stop();
205 }
206 });
207
208 let update_session = session.clone();
209 use_effect(move || {
210 let id = graph_id.read().clone();
211 let _source = source_id.read();
212 if !mounted() {
213 return;
214 }
215 if let Some(previous) = session.borrow_mut().take() {
216 previous.stop();
217 }
218 failure.set(None);
219 let eval = document::eval(&graph_channel_script(&id, &data.peek(), external_container));
220 let task = spawn(receive_views(eval, failure));
221 *session.borrow_mut() = Some(ChannelSession {
222 identity: Rc::new(()),
223 eval,
224 task,
225 });
226 });
227
228 use_effect(move || {
229 let id = graph_id.read().clone();
230 let script = graph_drive_script(&id, &data.read(), GraphDrive::Update);
231 let update_session = update_session.clone();
232 let identity = update_session
233 .borrow()
234 .as_ref()
235 .map(|session| session.identity.clone());
236 spawn(async move {
237 if let Err(error) = document::eval(&script).await {
238 let current = update_session.borrow();
239 if !same_owner(
240 current.as_ref().map(|session| &session.identity),
241 identity.as_ref(),
242 ) {
243 return;
244 }
245 let message = format!("Graph update failed: {error}");
246 if let Some(session) = current.as_ref() {
247 channel_failed(session.eval, failure, message);
248 } else {
249 crate::diagnostics::report_error(&message);
250 failure.set(Some(message));
251 }
252 }
253 });
254 });
255
256 rsx! {
257 div {
258 id: (!external_container).then_some(container_id),
259 class: if external_container { "graph-channel-owner" } else { "graph-view" },
260 onmounted: move |_| mounted.set(true),
261 if let Some(message) = failure() {
262 p { class: "graph-failure", role: "alert", "{message}" }
263 }
264 }
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
274 fn external_graph_host_does_not_steal_existing_container() {
275 fn external_host() -> Element {
277 let id = use_signal(|| "book-graph".to_string());
278 let data = use_signal(empty_graph_data);
279 let source = use_signal(|| "book-source".to_string());
280 rsx! { GraphView { graph_id: id, data, source_id: source, external_container: true } }
281 }
282 let mut dom = VirtualDom::new(external_host);
283 let mutations = dom.rebuild_to_vec();
284 assert!(!mutations.edits.iter().any(|edit| matches!(
285 edit,
286 dioxus::core::Mutation::SetAttribute {
287 name: "id",
288 value: dioxus::core::AttributeValue::Text(_),
289 ..
290 }
291 )));
292 }
293
294 #[test]
296 fn delayed_update_error_requires_the_same_channel_owner() {
297 let old = Rc::new(());
298 let new = Rc::new(());
299 assert!(same_owner(Some(&old), Some(&old)));
300 assert!(!same_owner(Some(&new), Some(&old)));
301 assert!(!same_owner(None, Some(&old)));
302 assert!(!same_owner(Some(&new), None));
303 assert!(same_owner(None, None));
304 }
305
306 #[test]
308 fn receiver_prioritizes_messages_and_detects_completion() {
309 use std::{
310 future::{pending, ready},
311 task::{Context, Poll, Waker},
312 };
313 let mut context = Context::from_waker(Waker::noop());
314 let mut completed = std::pin::pin!(ready("closed"));
315 let mut message = std::pin::pin!(prefer_message(ready("diagnostic"), completed.as_mut()));
316 assert_eq!(
317 message.as_mut().poll(&mut context),
318 Poll::Ready(Ok("diagnostic"))
319 );
320 let mut completed = std::pin::pin!(ready("closed"));
321 let mut message = std::pin::pin!(prefer_message(pending::<()>(), completed.as_mut()));
322 assert_eq!(
323 message.as_mut().poll(&mut context),
324 Poll::Ready(Err("closed"))
325 );
326 let mut completed = std::pin::pin!(pending::<()>());
327 let mut message = std::pin::pin!(prefer_message(pending::<()>(), completed.as_mut()));
328 assert_eq!(message.as_mut().poll(&mut context), Poll::Pending);
329 }
330 #[test]
332 fn channel_script_embeds_shared_runtime_and_escapes_ids() {
333 let script = graph_channel_script("a\"b", &empty_graph_data(), false);
334 assert!(script.contains("async function runGraphChannel"));
335 assert!(script.contains(r#"await runGraphChannel(window, "a\"b""#));
336 }
337 fn empty_graph_data() -> GraphData {
338 GraphData {
339 nodes: vec![],
340 links: vec![],
341 changed: vec![],
342 forced: vec![],
343 forced_relationships: vec![],
344 }
345 }
346
347 #[test]
348 fn graph_drive_script_init_calls_begin_graph_init() {
349 let script = graph_drive_script("g1", &empty_graph_data(), GraphDrive::Init);
350 assert!(script.contains("window.beginGraph.init(\"g1\""));
352 assert!(script.contains("window.__beginGraphData[\"g1\"]"));
353 }
354
355 #[test]
356 fn graph_drive_script_update_calls_begin_graph_update() {
357 let script = graph_drive_script("g1", &empty_graph_data(), GraphDrive::Update);
358 assert!(script.contains("window.beginGraph.update(\"g1\""));
359 }
360
361 #[test]
362 fn graph_drive_script_guards_on_begin_graph_being_defined() {
363 let script = graph_drive_script("g1", &empty_graph_data(), GraphDrive::Init);
364 assert!(script.contains("typeof window.beginGraph !== 'undefined'"));
365 }
366
367 #[test]
369 fn graph_drive_script_has_explicit_serializable_completion() {
370 let script = graph_drive_script("g1", &empty_graph_data(), GraphDrive::Update);
371 assert!(script.ends_with("dioxus.close(); return null;"));
372 }
373
374 #[test]
375 fn graph_drive_script_embeds_the_serialized_data() {
376 let script = graph_drive_script("g1", &empty_graph_data(), GraphDrive::Init);
377 assert!(script.contains("\"nodes\""));
379 }
380
381 #[test]
382 fn graph_drive_script_escapes_a_quote_in_the_container_id() {
383 let script = graph_drive_script("a'b", &empty_graph_data(), GraphDrive::Init);
387 assert!(script.contains("window.beginGraph.init(\"a'b\""));
388 assert!(!script.contains("['a'b']"));
389 }
390
391 #[test]
392 fn graph_drive_script_escapes_a_double_quote_and_backslash_in_the_container_id() {
393 let script = graph_drive_script("a\"b\\c", &empty_graph_data(), GraphDrive::Init);
396 assert!(script.contains(r#"window.beginGraph.init("a\"b\\c""#));
397 }
398}