|
| 1 | +use rsonpath_website::constants; |
| 2 | +use rsonpath_website::message::*; |
| 3 | +use std::pin::Pin; |
| 4 | +use std::sync::{ |
| 5 | + Arc, |
| 6 | + atomic::{AtomicI32, Ordering}, |
| 7 | +}; |
| 8 | +use std::task::{Context, Poll}; |
| 9 | +use wasm_bindgen::prelude::*; |
| 10 | +use web_sys::{Blob, BlobPropertyBag, MessageEvent, Url, Worker, console, js_sys::Array}; |
| 11 | + |
| 12 | +fn main() -> Result<(), JsValue> { |
| 13 | + console_error_panic_hook::set_once(); |
| 14 | + color_eyre::install().expect("color_eyre install"); |
| 15 | + |
| 16 | + let window = web_sys::window().expect("no global `window` exists"); |
| 17 | + let document = window.document().expect("should have a document"); |
| 18 | + let canvas = document |
| 19 | + .get_element_by_id(constants::CANVAS_ELEMENT_ID) |
| 20 | + .expect("canvas element not found, update CANVAS_ELEMENT_ID") |
| 21 | + .dyn_into::<web_sys::HtmlCanvasElement>()?; |
| 22 | + |
| 23 | + wasm_bindgen_futures::spawn_local(async move { |
| 24 | + let runner = eframe::WebRunner::new(); |
| 25 | + // The GUI must wait for the Runner to spawn, or else actions such as Run or Open File will not be handled. |
| 26 | + console::log_1(&"⏳ Waiting for rsonpath worker to spawn...".into()); |
| 27 | + let worker = create_worker().await.expect("Failed to create worker"); |
| 28 | + console::log_1(&"Worker ready.".into()); |
| 29 | + // Handle control to the Website struct. |
| 30 | + worker.set_onmessage(None); |
| 31 | + runner |
| 32 | + .start( |
| 33 | + canvas, |
| 34 | + eframe::WebOptions::default(), |
| 35 | + Box::new(|cc| Ok(Box::new(rsonpath_website::start(cc, worker)))), |
| 36 | + ) |
| 37 | + .await |
| 38 | + .expect("Failed to start eframe WebRunner"); |
| 39 | + }); |
| 40 | + |
| 41 | + Ok(()) |
| 42 | +} |
| 43 | + |
| 44 | +fn create_worker() -> impl Future<Output = Result<web_sys::Worker, JsValue>> { |
| 45 | + // This is basically magic, modelled after trunk webworker example: |
| 46 | + // https://github.com/trunk-rs/trunk/blob/f1ee3d4032cb939b8513d1d8fabfcb84ce46d811/examples/webworker/src/bin/app.rs#L5-L27 |
| 47 | + let origin = web_sys::window() |
| 48 | + .expect("window to be available") |
| 49 | + .location() |
| 50 | + .origin() |
| 51 | + .expect("origin to be available"); |
| 52 | + |
| 53 | + let script = Array::new(); |
| 54 | + script.push( |
| 55 | + &format!( |
| 56 | + r#"importScripts("{origin}/{}.js");wasm_bindgen("{origin}/{}_bg.wasm");"#, |
| 57 | + constants::WORKER_BIN_NAME, |
| 58 | + constants::WORKER_BIN_NAME |
| 59 | + ) |
| 60 | + .into(), |
| 61 | + ); |
| 62 | + |
| 63 | + let blob_property_bag = BlobPropertyBag::new(); |
| 64 | + blob_property_bag.set_type("text/javascript"); |
| 65 | + let blob = Blob::new_with_str_sequence_and_options(&script, &blob_property_bag).expect("blob to be created"); |
| 66 | + |
| 67 | + let url = Url::create_object_url_with_blob(&blob).expect("url to be created"); |
| 68 | + |
| 69 | + let worker = Worker::new(&url).expect("worker to be created"); |
| 70 | + let worker_clone = worker.clone(); |
| 71 | + // Here we handle control over to our custom Future that coordinates with the spawned worker. |
| 72 | + CreateWorkerFuture::new(worker_clone) |
| 73 | +} |
| 74 | + |
| 75 | +struct CreateWorkerFuture { |
| 76 | + worker: Worker, |
| 77 | + state: CreateWorkerFutureState, |
| 78 | +} |
| 79 | + |
| 80 | +/// Internal state of the [`CreateWorkerFuture`]. |
| 81 | +enum CreateWorkerFutureState { |
| 82 | + /// Sentinel value, must not be used between polls. |
| 83 | + None, |
| 84 | + /// Future was created and not polled yet, setup of event handlers required. |
| 85 | + Init, |
| 86 | + /// Message channels were initialized, we are waiting for the worker to report back. |
| 87 | + /// The inner value is the status code Arc with a value of -1 while waiting, set to the actual |
| 88 | + /// status after the worker reports back. |
| 89 | + Launched(Arc<AtomicI32>), |
| 90 | +} |
| 91 | + |
| 92 | +impl CreateWorkerFuture { |
| 93 | + pub fn new(worker: Worker) -> Self { |
| 94 | + Self { |
| 95 | + worker, |
| 96 | + state: CreateWorkerFutureState::Init, |
| 97 | + } |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +impl Future for CreateWorkerFuture { |
| 102 | + type Output = Result<Worker, JsValue>; |
| 103 | + |
| 104 | + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { |
| 105 | + let mut state = CreateWorkerFutureState::None; |
| 106 | + std::mem::swap(&mut self.state, &mut state); |
| 107 | + let state = state; |
| 108 | + let (state, response) = match state { |
| 109 | + CreateWorkerFutureState::Init => { |
| 110 | + // Set up the callback and the status code Arc to communicate the message back. |
| 111 | + let status_code = Arc::new(AtomicI32::new(-1)); |
| 112 | + let status_code_clone = status_code.clone(); |
| 113 | + let waker_clone = cx.waker().clone(); |
| 114 | + let onmessage = Closure::wrap(Box::new(move |msg: MessageEvent| { |
| 115 | + assert_eq!(msg.ty(), MessageType::WorkerStarted, "worker reported invalid message"); |
| 116 | + let msg = msg |
| 117 | + .deserialize::<WorkerStartedMessage>() |
| 118 | + .expect("correct WorkerStartedMessage"); |
| 119 | + status_code_clone.store(msg.status(), Ordering::Release); |
| 120 | + waker_clone.wake_by_ref(); |
| 121 | + }) as Box<dyn Fn(MessageEvent)>); |
| 122 | + self.worker.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); |
| 123 | + onmessage.forget(); |
| 124 | + (CreateWorkerFutureState::Launched(status_code), Poll::Pending) |
| 125 | + } |
| 126 | + CreateWorkerFutureState::Launched(status_code_arc) => { |
| 127 | + let status_code = status_code_arc.load(Ordering::Acquire); |
| 128 | + if status_code == -1 { |
| 129 | + // It's not the message handler that woke us up, we're not ready yet. |
| 130 | + (CreateWorkerFutureState::Launched(status_code_arc), Poll::Pending) |
| 131 | + } else if status_code == 0 { |
| 132 | + // Successful start. |
| 133 | + let worker = self.worker.clone(); |
| 134 | + ( |
| 135 | + CreateWorkerFutureState::Launched(status_code_arc), |
| 136 | + Poll::Ready(Ok(worker)), |
| 137 | + ) |
| 138 | + } else { |
| 139 | + // Some error occurred. |
| 140 | + let error = format!("worker failed to create: status code {status_code}").into(); |
| 141 | + ( |
| 142 | + CreateWorkerFutureState::Launched(status_code_arc), |
| 143 | + Poll::Ready(Err(error)), |
| 144 | + ) |
| 145 | + } |
| 146 | + } |
| 147 | + CreateWorkerFutureState::None => unreachable!("CreateWorkerFutureState::None cannot happen"), |
| 148 | + }; |
| 149 | + |
| 150 | + self.state = state; |
| 151 | + response |
| 152 | + } |
| 153 | +} |
0 commit comments