refactor(web-spawn): use snippets (#52)

This commit is contained in:
sinu.eth
2025-03-07 10:45:38 -08:00
committed by GitHub
parent 425614e457
commit 78a9ae0f65
11 changed files with 182 additions and 142 deletions

View File

@@ -6,6 +6,10 @@ description = "`std` spawn replacement for WASM in the browser."
repository = "https://github.com/tlsnotary/tlsn-utils"
license = "MIT OR Apache-2.0"
[features]
default = []
no-bundler = []
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = { version = "0.4" }

69
web-spawn/js/spawn.js Normal file
View File

@@ -0,0 +1,69 @@
function registerMessageListener(target, type, callback) {
const listener = async (event) => {
const message = event.data;
if (message && message.type === type) {
await callback(message.data);
}
};
target.addEventListener('message', listener);
}
// Register listener for the start spawner message.
registerMessageListener(self, 'web_spawn_start_spawner', async (data) => {
const workerUrl = new URL(
'./spawn.js',
import.meta.url
);
const [module, memory, spawnerPtr] = data;
const pkg = await import('../../..');
await pkg.default({ module, memory });
const spawner = pkg.web_spawn_recover_spawner(spawnerPtr);
postMessage('web_spawn_spawner_ready');
await spawner.run(workerUrl.toString());
close();
});
// Register listener for the start worker message.
registerMessageListener(self, 'web_spawn_start_worker', async (data) => {
const [module, memory, workerPtr] = data;
const pkg = await import('../../..');
await pkg.default({ module, memory });
pkg.web_spawn_start_worker(workerPtr);
close();
});
/// Starts the spawner in a new worker.
export async function startSpawnerWorker(module, memory, spawner) {
const workerUrl = new URL(
'./spawn.js',
import.meta.url
);
const worker = new Worker(
workerUrl,
{
name: 'web-spawn-spawner',
type: 'module'
}
);
const data = [module, memory, spawner.intoRaw()];
worker.postMessage({
type: 'web_spawn_start_spawner',
data: data
})
await new Promise(resolve => {
worker.addEventListener('message', function handler(event) {
if (event.data === 'web_spawn_spawner_ready') {
worker.removeEventListener('message', handler);
resolve();
}
})
})
}

View File

@@ -0,0 +1,62 @@
function registerMessageListener(target, type, callback) {
const listener = async (event) => {
const message = event.data;
if (message && message.type === type) {
await callback(message.data);
}
};
target.addEventListener('message', listener);
}
// Register listener for the start spawner message.
registerMessageListener(self, 'web_spawn_start_spawner', async (data) => {
const [module, memory, workerUrl, wasmUrl, spawner] = data;
const wasm = await import(`${wasmUrl}`);
wasm.initSync({ module, memory });
postMessage('web_spawn_spawner_ready');
await wasm.web_spawn_recover_spawner(spawner).run(workerUrl);
URL.revokeObjectURL(workerUrl);
close();
});
// Register listener for the start worker message.
registerMessageListener(self, 'web_spawn_start_worker', async (data) => {
const [module, memory, wasmUrl, worker] = data;
const wasm = await import(`${wasmUrl}`);
wasm.initSync({ module, memory });
wasm.web_spawn_start_worker(worker);
close();
});
/// Starts the spawner in a new worker.
export async function startSpawnerWorker(module, memory, spawner) {
let workerUrl = import.meta.url;
let scriptBlob = await fetch(`${workerUrl}`).then(r => r.blob());
workerUrl = URL.createObjectURL(scriptBlob);
const worker = new Worker(workerUrl, {
name: 'web-spawn-spawner',
type: 'module'
});
const wasmUrl = spawner.getUrl();
worker.postMessage({
type: 'web_spawn_start_spawner',
data: [module, memory, workerUrl, wasmUrl, spawner.intoRaw()]
});
await new Promise(resolve => {
worker.addEventListener('message', function handler(event) {
if (event.data === 'web_spawn_spawner_ready') {
worker.removeEventListener('message', handler);
resolve();
}
});
});
}

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# Test from main browser thread
WASM_BINDGEN_USE_BROWSER=1 wasm-pack test --headless --chrome --firefox
WASM_BINDGEN_USE_BROWSER=1 wasm-pack test --headless --chrome --firefox -- --features no-bundler
# Test from worker thread
WASM_BINDGEN_USE_DEDICATED_WORKER=1 wasm-pack test --headless --chrome --firefox
WASM_BINDGEN_USE_DEDICATED_WORKER=1 wasm-pack test --headless --chrome --firefox -- --features no-bundler

View File

@@ -1,11 +0,0 @@
/// Extracts current script file path from artificially generated stack trace
function script_path() {
try {
throw new Error();
} catch (e) {
let parts = e.stack.match(/(?:\(|@)(\S+):\d+:\d+/);
return parts[1];
}
}
script_path()

View File

@@ -1,23 +0,0 @@
import init, { web_spawn_recover_spawner } from "WASM_BINDGEN_SHIM_URL";
console.log('spawner spawned');
self.onmessage = event => {
const [module_or_path, memory, spawner] = event.data;
init({ module_or_path, memory })
.catch(err => {
console.error(err);
// Propagate to main `onerror`:
setTimeout(() => {
throw err;
});
throw err;
})
.then(async () => {
self.postMessage('ready');
await web_spawn_recover_spawner(spawner).run();
close();
});
};

View File

@@ -1,21 +0,0 @@
import init, { web_spawn_start_worker } from "WASM_BINDGEN_SHIM_URL";
self.onmessage = event => {
const [module_or_path, memory, worker] = event.data;
init({ module_or_path, memory })
.catch(err => {
console.error(err);
// Propagate to main `onerror`:
setTimeout(() => {
throw err;
});
throw err;
})
.then(() => {
self.postMessage('ready');
web_spawn_start_worker(worker);
close();
});
};

View File

@@ -21,16 +21,24 @@ pub(crate) type Closure = dyn FnOnce() + Send;
/// Global sender channel for spawning threads.
pub(crate) static SENDER: OnceLock<UnboundedSender<(Builder, Box<Closure>)>> = OnceLock::new();
/// Initializes the thread spawner.
#[wasm_bindgen(js_name = initSpawner)]
pub fn init_spawner() -> Spawner {
Spawner::new()
#[cfg_attr(not(feature = "no-bundler"), wasm_bindgen(module = "/js/spawn.js"))]
#[cfg_attr(
feature = "no-bundler",
wasm_bindgen(module = "/js/spawn.no-bundler.js")
)]
extern "C" {
#[wasm_bindgen(js_name = startSpawnerWorker)]
fn start_spawner_worker(module: JsValue, memory: JsValue, spawner: Spawner) -> Promise;
}
/// Starts the thread spawner on a dedicated worker thread.
#[wasm_bindgen(js_name = startSpawner)]
pub fn start_spawner() -> Promise {
Spawner::new().spawn()
start_spawner_worker(
wasm_bindgen::module(),
wasm_bindgen::memory(),
Spawner::new(),
)
}
/// Spawns a closure onto a new thread.

View File

@@ -2,21 +2,13 @@ use futures::{
StreamExt,
channel::mpsc::{UnboundedReceiver, unbounded},
};
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use crate::wasm::{
Closure, SENDER,
thread::Builder,
utils::{callback, encode_script, get_shim_url},
worker::WorkerData,
};
use crate::wasm::{Closure, SENDER, thread::Builder, worker::WorkerData};
/// Global spawner which spawns closures into web workers.
#[wasm_bindgen]
pub struct Spawner {
shim_url: String,
worker_url: String,
receiver: UnboundedReceiver<(Builder, Box<Closure>)>,
}
@@ -34,40 +26,26 @@ impl Spawner {
panic!("spawner already initialized");
}
let shim_url = get_shim_url();
let worker_url = encode_script(&shim_url, include_str!("../js/worker.js"));
Self {
shim_url,
worker_url,
receiver,
}
Self { receiver }
}
/// Spawns the spawner into a dedicated web worker.
pub fn spawn(self) -> Promise {
let options = web_sys::WorkerOptions::new();
options.set_type(web_sys::WorkerType::Module);
options.set_name("web_spawn_spawner");
#[cfg(feature = "no-bundler")]
#[wasm_bindgen(js_name = getUrl)]
pub fn get_url(&self) -> js_sys::JsString {
crate::utils::get_url()
}
let script_url = encode_script(&self.shim_url, include_str!("../js/spawner.js"));
let worker = web_sys::Worker::new_with_options(&script_url, &options).unwrap_throw();
let data = js_sys::Array::new();
data.push(&wasm_bindgen::module());
data.push(&wasm_bindgen::memory());
data.push(&JsValue::from(Box::into_raw(Box::new(self))));
worker.post_message(&data).unwrap_throw();
callback(&worker)
#[wasm_bindgen(js_name = intoRaw)]
pub fn into_raw(self) -> *mut Self {
Box::into_raw(Box::new(self))
}
/// Runs the spawner.
pub async fn run(mut self) {
#[wasm_bindgen]
pub async fn run(&mut self, url: String) {
// Spawn a new worker for every closure.
while let Some((builder, f)) = self.receiver.next().await {
WorkerData::new(f).spawn(builder, &self.worker_url);
WorkerData::new(f).spawn(builder, &url);
}
}
}

View File

@@ -1,46 +1,12 @@
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use web_sys::{Blob, MessageEvent, Url, Worker};
#[cfg(feature = "no-bundler")]
pub(crate) fn get_url() -> js_sys::JsString {
use wasm_bindgen::prelude::*;
/// Returns the URL for the wasm bindgen shim.
pub(crate) fn get_shim_url() -> String {
js_sys::eval(include_str!("../js/script_path.js"))
.unwrap_throw()
.as_string()
.unwrap_throw()
}
/// Generates worker script as URL encoded blob
pub(crate) fn encode_script(wasm_bindgen_shim_url: &str, template: &str) -> String {
let script = template.replace("WASM_BINDGEN_SHIM_URL", &wasm_bindgen_shim_url);
// Create url encoded blob
let arr = js_sys::Array::new();
arr.set(0, JsValue::from_str(&script));
let blob = Blob::new_with_str_sequence(&arr).unwrap();
let url = Url::create_object_url_with_blob(
&blob
.slice_with_f64_and_f64_and_content_type(0.0, blob.size(), "text/javascript")
.unwrap(),
)
.unwrap();
url
}
pub(crate) fn callback(worker: &Worker) -> Promise {
Promise::new(&mut |resolve, _reject| {
// Create a one-time closure that resolves the promise when a message is
// received.
let callback = Closure::once(move |event: MessageEvent| {
// Resolve the promise with the event's data.
resolve.call1(&JsValue::NULL, &event.data()).unwrap();
});
// Attach the callback to the worker's onmessage event.
worker.set_onmessage(Some(callback.as_ref().unchecked_ref()));
// Ensure the callback isn't dropped prematurely.
callback.forget();
})
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(thread_local_v2, js_namespace = ["import", "meta"], js_name = url)]
static URL: js_sys::JsString;
}
URL.with(Clone::clone)
}

View File

@@ -19,6 +19,8 @@ impl WorkerData {
if let Some(name) = builder.name {
options.set_name(&name);
} else {
options.set_name("web-spawn-worker");
}
let worker = web_sys::Worker::new_with_options(script_url, &options).unwrap_throw();
@@ -26,9 +28,15 @@ impl WorkerData {
let data = js_sys::Array::new();
data.push(&wasm_bindgen::module());
data.push(&wasm_bindgen::memory());
#[cfg(feature = "no-bundler")]
data.push(&crate::utils::get_url());
data.push(&JsValue::from(Box::into_raw(Box::new(self))));
worker.post_message(&data).unwrap_throw();
let msg = js_sys::Object::new();
js_sys::Reflect::set(&msg, &"type".into(), &"web_spawn_start_worker".into()).unwrap_throw();
js_sys::Reflect::set(&msg, &"data".into(), &data).unwrap_throw();
worker.post_message(&msg).unwrap_throw();
}
}