Skip to content

Commit ffd956a

Browse files
committed
feat: major refactoring into workers, better file download
1 parent 5ba57e9 commit ffd956a

14 files changed

Lines changed: 2540 additions & 1104 deletions

File tree

web/rsonpath-website/Cargo.lock

Lines changed: 161 additions & 326 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/rsonpath-website/Cargo.toml

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,55 +9,55 @@ homepage = "https://rsonquery.github.io/web"
99
edition = "2024"
1010

1111
[dependencies]
12-
eframe = { version = "0.32.0", default-features = false, features = [
12+
color-eyre = { version = "0.6.5", default-features = false, features = ["track-caller"] }
13+
eframe = { version = "0.33.2", default-features = false, features = [
1314
"accesskit",
1415
"default_fonts",
1516
"glow",
1617
"persistence",
1718
"wayland",
1819
"x11",
1920
] }
20-
egui = "0.32.0"
21-
egui_extras = { version = "0.32.0", features = ["all_loaders", "svg"] }
22-
wasm-bindgen = "0.2"
21+
egui = "0.33.2"
22+
egui-async = "0.2.5"
23+
egui_extras = { version = "0.33.2", features = ["all_loaders", "svg"] }
24+
eyre = "0.6.12"
25+
js-sys = "0.3.83"
2326
rsonpath = { path = "../../crates/rsonpath-lib", package = "rsonpath-lib" }
2427
rsonpath-syntax = { path = "../../crates/rsonpath-syntax" }
25-
rfd = { version = "0.15.3", optional = true }
26-
js-sys = "0.3.77"
27-
color-eyre = { version = "0.6", default-features = false, features = ["track-caller"] }
28-
eyre = "0.6"
29-
strip-ansi-escapes = "0.2.1"
30-
web-time = "1.0"
28+
wasm-bindgen = "0.2.106"
29+
web-time = "1.1.0"
3130

32-
[target.'cfg(target_arch = "wasm32")'.dependencies]
33-
wasm-bindgen-futures = "0.4.50"
34-
web-sys = "0.3.70"
3531
console_error_panic_hook = "0.1.7"
36-
gloo-timers = "0.3"
37-
38-
[dependencies.web-sys]
39-
version = "0.3"
40-
features = [
41-
"Url",
32+
wasm-bindgen-futures = "0.4.56"
33+
web-sys = { version = "0.3.83", features = [
4234
"Blob",
35+
"DedicatedWorkerGlobalScope",
36+
"Document",
37+
"DomException",
38+
"Event",
4339
"FileReader",
4440
"HtmlInputElement",
4541
"HtmlAnchorElement",
46-
"Event",
47-
"Document",
48-
"Window",
42+
"MessageEvent",
4943
"Node",
50-
]
51-
52-
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
53-
rfd = "0.15.3"
54-
55-
#[lints]
56-
#workspace = true
44+
"ProgressEvent",
45+
"Url",
46+
"Window",
47+
"Worker",
48+
] }
5749

5850
[lib]
5951
path = "src/lib.rs"
6052
crate-type = ["cdylib", "rlib"]
6153

54+
[[bin]]
55+
path = "src/eframe.rs"
56+
name = "eframe"
57+
58+
[[bin]]
59+
path = "src/runner.rs"
60+
name = "runner"
61+
6262
[profile.release]
6363
incremental = true

web/rsonpath-website/index.html

Lines changed: 4 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>RsonQuery Web</title>
7-
<link data-trunk rel="rust" data-bin="rsonpath-website" />
7+
<link data-trunk rel="rust" data-bin="eframe" data-wasm-opt="z" data-type="main" />
8+
<link data-trunk rel="rust" data-bin="runner" data-wasm-opt="4" data-type="worker" />
89
<style>
910
html, body {
1011
margin: 0;
@@ -22,48 +23,8 @@
2223
</style>
2324
</head>
2425
<body>
25-
<canvas id="rsonquery_web"></canvas>
26+
<canvas id="rsonquery-web-canvas"></canvas>
27+
<input type="file" id="file-input" name="file-input" accept=".json" hidden aria-hidden="true">
2628

27-
<script type="text/javascript">
28-
function setupDropListener() {
29-
const canvas = document.querySelector("canvas");
30-
31-
canvas.addEventListener("dragover", (e) => {
32-
e.preventDefault();
33-
});
34-
35-
canvas.addEventListener("drop", async (event) => {
36-
event.preventDefault();
37-
38-
const file = event.dataTransfer.files[0];
39-
40-
if (file && file.name.endsWith(".json")) {
41-
const start = performance.now();
42-
const text = await file.text();
43-
44-
if (typeof window.handle_dropped_file === "function") {
45-
window.handle_dropped_file(text);
46-
47-
console.log(`File read took ${(performance.now() - start).toFixed(2)} ms`);
48-
} else {
49-
console.warn("Rust WASM not ready yet — drop ignored.");
50-
}
51-
}
52-
});
53-
}
54-
55-
function waitForWasmExport() {
56-
const checkInterval = setInterval(() => {
57-
if (typeof window.handle_dropped_file === "function") {
58-
clearInterval(checkInterval);
59-
setupDropListener();
60-
} else {
61-
console.log("⏳ Waiting for WASM export...");
62-
}
63-
}, 200);
64-
}
65-
66-
window.addEventListener("DOMContentLoaded", waitForWasmExport);
67-
</script>
6829
</body>
6930
</html>

web/rsonpath-website/rustfmt.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
edition = "2021"
2+
max_width = 120
3+
newline_style = "Unix"
4+
use_field_init_shorthand = true
5+
use_try_shorthand = true
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
pub const CANVAS_ELEMENT_ID: &str = "rsonquery-web-canvas";
2+
pub const FILE_INPUT_ELEMENT_ID: &str = "file-input";
3+
pub const WORKER_BIN_NAME: &str = "runner";
4+
pub const FILE_PREVIEW_CHARS: usize = 64_000;
5+
pub const MAX_OUTPUT_BYTES: usize = 10_000_000;
6+
pub const FILE_MAX_BYTES: usize = 1_000_000_000;
7+
pub const WARMUP_RUNS: usize = 2;
8+
pub const FONT_SIZE_NORMAL: f32 = 15.0;
9+
pub const RQ_COLOR: egui::Color32 = egui::Color32::from_rgb(227, 60, 38);

web/rsonpath-website/src/eframe.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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

Comments
 (0)