use js_sys::Array;
use thiserror::Error;
use tracing::warn;
use wasm_bindgen::JsValue;
use web_sys::{window, Blob, BlobPropertyBag, Url, Worker, WorkerOptions, WorkerType};
use crate::workers::WebWorker;
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum CreateWorkerError {
#[doc(hidden)]
#[error("No window object found")]
NoWindow,
#[doc(hidden)]
#[error("No document object found")]
NoDocument,
#[doc(hidden)]
#[error("Javascript error when getting document.baseURI: {0:?}")]
DocumentBaseUri(JsValue),
#[doc(hidden)]
#[error("Javascript error when building the WASM URL: {0:?}")]
WasmUrl(JsValue),
#[doc(hidden)]
#[error("Javascript error when creating the blob URL: {0:?}")]
BlobUrl(JsValue),
#[error("Javascript error when creating the worker: {0:?}")]
NewWorker(JsValue),
}
fn resolve_js_wasm_path(js_path: &str, wasm_path: &str) -> Result<(Url, Url), CreateWorkerError> {
let document = window()
.ok_or(CreateWorkerError::NoWindow)?
.document()
.ok_or(CreateWorkerError::NoDocument)?;
let base_uri = document
.base_uri()
.map_err(CreateWorkerError::DocumentBaseUri)?;
let create_uri = |url| match &base_uri {
Some(base) => Url::new_with_base(url, base),
None => Url::new(url),
};
let js_url = create_uri(js_path).map_err(CreateWorkerError::WasmUrl)?;
let wasm_url = create_uri(wasm_path).map_err(CreateWorkerError::WasmUrl)?;
Ok((js_url, wasm_url))
}
fn find_js_wasm_urls() -> Result<(Url, Url), CreateWorkerError> {
let output_name = option_env!("LEPTOS_OUTPUT_NAME").or_else(|| option_env!("CARGO_BIN_NAME"));
let output_name_condition = output_name
.map(|s| format!("[href*='{s}']"))
.unwrap_or_default();
if output_name.is_none() {
warn!("No output name found, if the worker is not loading, ensure either LEPTOS_OUTPUT_NAME or CARGO_BIN_NAME matches the output .wasm file name.");
}
let document = window()
.ok_or(CreateWorkerError::NoWindow)?
.document()
.ok_or(CreateWorkerError::NoDocument)?;
let js_path = document
.query_selector(&format!("head > link{output_name_condition}[href$='.js']"))
.expect("query selector format to be valid")
.map(|el| {
el.get_attribute("href")
.expect("query selector to only find <link> tags with href set")
});
let wasm_path = document
.query_selector(&format!(
"head > link{output_name_condition}[href$='.wasm']"
))
.expect("query selector format to be valid")
.map(|el| {
el.get_attribute("href")
.expect("query selector to only find <link> tags with href set")
});
let (js_path, wasm_path) = if let Some((js_path, wasm_path)) = js_path.zip(wasm_path) {
(js_path, wasm_path)
} else {
let site_pkg_dir = option_env!("LEPTOS_SITE_PKG_DIR")
.map(|s| format!("/{s}"))
.unwrap_or_default();
if let Some(output_name) = output_name {
let base = format!("{site_pkg_dir}/{output_name}");
(format!("{base}.js"), format!("{base}.wasm"))
} else if let Some(output_name) = option_env!("CARGO_BIN_NAME") {
(
format!("{output_name}.js"),
format!("{output_name}_bg.wasm"),
)
} else {
(
"wasm-bindgen-test.js".to_string(),
"wasm-bindgen-test_bg.wasm".to_string(),
)
}
};
resolve_js_wasm_path(&js_path, &wasm_path)
}
pub fn create_unifunctional_worker<W: WebWorker>() -> Result<Worker, CreateWorkerError> {
let (js_url, wasm_url) = find_js_wasm_urls()?;
let module_def =
super::unifunctional_worker::web_module(&js_url.to_string(), &wasm_url.to_string());
create_worker_from_module(W::path(), &module_def)
}
fn create_worker_from_module(
worker_name: &str,
module_def: &str,
) -> Result<Worker, CreateWorkerError> {
let bag = BlobPropertyBag::new();
bag.set_type("application/javascript");
let worker_js_blob = string_to_blob(&bag, module_def)?;
let blob_url =
Url::create_object_url_with_blob(&worker_js_blob).map_err(CreateWorkerError::BlobUrl)?;
let opts = WorkerOptions::new();
opts.set_name(worker_name);
opts.set_type(WorkerType::Module);
let worker =
Worker::new_with_options(&blob_url, &opts).map_err(CreateWorkerError::NewWorker)?;
Ok(worker)
}
fn string_to_blob(options: &BlobPropertyBag, s: &str) -> Result<Blob, CreateWorkerError> {
let json_jsvalue = JsValue::from_str(s);
#[allow(clippy::from_iter_instead_of_collect)]
let json_jsvalue_array = Array::from_iter(std::iter::once(json_jsvalue));
Blob::new_with_str_sequence_and_options(&json_jsvalue_array, options)
.map_err(CreateWorkerError::BlobUrl)
}