use bevy_app::{App, Plugin, Startup};
use bevy_ecs::{resource::Resource, world::World};
use bevy_winit::{EventLoopProxyWrapper, WakeUp};
use std::rc::Rc;
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use web_sys::{js_sys::Array, window, Blob, Url, Worker};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WebKeepalivePlugin {
pub wake_delay: f64,
}
impl Default for WebKeepalivePlugin {
fn default() -> Self {
Self {
wake_delay: 1000.0 / 60.0,
}
}
}
impl Plugin for WebKeepalivePlugin {
fn build(&self, app: &mut App) {
app.insert_resource(KeepaliveSettings {
wake_delay: self.wake_delay,
worker: None,
});
app.add_systems(Startup, system_init_background_worker);
}
}
#[derive(Clone, Debug, PartialEq, Default, Resource)]
pub struct KeepaliveSettings {
pub wake_delay: f64,
worker: Option<Worker>,
}
unsafe impl Send for KeepaliveSettings {}
unsafe impl Sync for KeepaliveSettings {}
impl Drop for KeepaliveSettings {
fn drop(&mut self) {
if let Some(worker) = &self.worker {
worker.terminate();
}
}
}
fn system_init_background_worker(world: &mut World) {
let mut settings = world.resource_mut::<KeepaliveSettings>();
let script = Blob::new_with_str_sequence(
&Array::of1(&JsValue::from_str(&format!(
"
let interval = setInterval(() => self.postMessage(null), {});
self.onmessage = v => {{
const delay = parseInt(v);
if (isNaN(delay)) return;
clearInterval(interval);
interval = setInterval(() => self.postMessage(null), delay);
}};
",
settings.wake_delay
)))
.unchecked_into(),
)
.unwrap();
let worker = Worker::new(&Url::create_object_url_with_blob(&script).unwrap()).unwrap();
settings.worker = Some(worker.clone());
let world_ptr = Rc::new(world as *mut World);
let closure = Closure::<dyn FnMut()>::new({
let world = world_ptr.clone();
move || {
if window()
.and_then(|w| w.document())
.is_some_and(|d| !d.hidden())
{
return;
}
unsafe {
let Some(world) = world.as_mut() else {
return;
};
if let Some(proxy) = world.get_resource::<EventLoopProxyWrapper<WakeUp>>() {
let _ = proxy.send_event(WakeUp);
}
}
}
});
worker.set_onmessage(Some(closure.as_ref().unchecked_ref()));
closure.forget();
}