#Save state on browser tab close in WASM

15 messages · Page 1 of 1 (latest)

sick hull
#

I have the following startup system which successfully writes to local storage just before my Bevy app closes:

pub fn setup_window_onunload_save_world_state() {
    let window = web_sys::window().expect("window not available");

    let on_beforeunload = Closure::wrap(Box::new(move |_event: web_sys::BeforeUnloadEvent| {
        LocalStorage::set::<bool>("example", true);
    }) as Box<dyn FnMut(web_sys::BeforeUnloadEvent)>);

    window
        .add_event_listener_with_callback("beforeunload", on_beforeunload.as_ref().unchecked_ref());
    on_beforeunload.forget();
}

In this example, the window closes, but when I reopen the window I am able to inspect local storage and see the example entry. I understand that this is "best effort" and that if the browser crashes that this code might not run. That's OK.

I am trying to figure out how to modify this code such that I am able to write state of the world to local storage. Originally, I mirrored my logic after some Bevy WindowResize internals: https://github.com/bevyengine/bevy/blob/main/crates/bevy_winit/src/web_resize.rs#L73 but this approach is too slow. If I update a resource in on_beforeunload closure, and then wait for another system to detect it, then the browser window closes before the change is detected. So, I am looking for a way to work around Bevy/ECS and access this data immediately. It's OK if the data is slightly stale.

I thought I might take inspiration from how exclusive systems work. I wrote code like:

pub fn setup_window_onunload_save_world_state(world: &mut World) {
    let window = web_sys::window().expect("window not available");

    let on_beforeunload = Closure::wrap(Box::new(move |_event: web_sys::BeforeUnloadEvent| {
        let settings = world.resource::<Settings>();
        LocalStorage::set::<isize>("example", settings.initial_ant_count);
    }) as Box<dyn FnMut(web_sys::BeforeUnloadEvent)>);

    window
        .add_event_listener_with_callback("beforeunload", on_beforeunload.as_ref().unchecked_ref());
    on_beforeunload.forget();
}

where I request access to world and then, inside the closure, query for some of the world's state. This doesn't work out of the box because of lifetimes. error: lifetime may not live long enough cast requires that '1must outlive'static`

I'm not confident that this lifetime issue is addressable while reading reasonably current world state. In a previous approach, where I was just setting a flag and attempting to react to it, I was able to navigate around the lifetime issue with ArcMutex. For example:

#[derive(Resource)]
pub struct IsWindowUnloading(Arc<Mutex<bool>>);

pub fn setup_window_onunload_save_world_state(is_window_unloading: ResMut<IsWindowUnloading>) {
    let window = web_sys::window().expect("window not available");

    let is_window_unloading_arc = is_window_unloading.0.clone();

    let on_beforeunload = Closure::wrap(Box::new(move |_event: web_sys::BeforeUnloadEvent| {
        let mut is_window_unloading = is_window_unloading_arc.lock().unwrap();
        *is_window_unloading = true;
    }) as Box<dyn FnMut(web_sys::BeforeUnloadEvent)>);

    window
        .add_event_listener_with_callback("beforeunload", on_beforeunload.as_ref().unchecked_ref());
    on_beforeunload.forget();
}

Here I was able to successfully set a flag, but I was unable to react to the flag changing quickly enough to query my world state.

Are there any viable approaches here within Bevy? Otherwise, I guess my option is to continually take a snapshot of the world state I need to save, store that outside of Bevy, and write that when needed.

GitHub

A refreshingly simple data-driven game engine built in Rust - bevy/web_resize.rs at main · bevyengine/bevy

sick hull
#

I think I'm just going to go outside of the Bevy ecosystem for it

sick hull
#

So I guess my "well, at least it works" solution is:

#
  • create a system that runs frequently
#

declare this ugly thing:

#
#[derive(Default, Debug, Serialize, Deserialize, Resource)]
pub struct WorldSaveState {
    pub elements: Vec<ElementSaveState>,
    pub ants: Vec<AntSaveState>,
}

static SAVE_SNAPSHOT: Mutex<WorldSaveState> = Mutex::new(WorldSaveState {
    elements: Vec::new(),
    ants: Vec::new(),
});
#

continually update my snapshot with my system that's running frequently

#
pub fn save_snapshot_system(
    mut elements_query: Query<(&Element, &Position)>,
    mut ants_query: Query<(
        &AntFacing,
        &AntAngle,
        &AntBehavior,
        &AntName,
        &AntColor,
        &Position,
    )>,
) {
    let elements_save_state = elements_query
        .iter_mut()
        .map(|(element, position)| ElementSaveState {
            element: element.clone(),
            position: position.clone(),
        })
        .collect::<Vec<ElementSaveState>>();

    let ants_save_state = ants_query
        .iter_mut()
        .map(
            |(facing, angle, behavior, name, color, position)| AntSaveState {
                facing: facing.clone(),
                angle: angle.clone(),
                behavior: behavior.clone(),
                name: name.clone(),
                color: color.clone(),
                position: position.clone(),
            },
        )
        .collect::<Vec<AntSaveState>>();

    {
        let mut save_snapshot = SAVE_SNAPSHOT.lock().unwrap();
        save_snapshot.elements = elements_save_state;
        save_snapshot.ants = ants_save_state;
    }
}
#
  • create another startup system
#
pub fn setup_window_onunload_save_world_state() {
    let window = web_sys::window().expect("window not available");

    let on_beforeunload = Closure::wrap(Box::new(move |_event: web_sys::BeforeUnloadEvent| {
        let save_snapshot = SAVE_SNAPSHOT.lock().unwrap();
        LocalStorage::set(LOCAL_STORAGE_KEY, save_snapshot.deref().clone());
    }) as Box<dyn FnMut(web_sys::BeforeUnloadEvent)>);

    window
        .add_event_listener_with_callback("beforeunload", on_beforeunload.as_ref().unchecked_ref());
    on_beforeunload.forget();
}
#

write the last known snapshot instantly to local storage

#

🎉

#

but that's like using four different abstractions that I'm not comfortable with after asking AI to help me out and running completely outside of the Bevy framework when I was really hoping to just check for a WindowClosed event

craggy lintel
#

You could probably make a request to winit to add a JavaScript hook to send the window closed event when the tab is killed.

#

It's not really a bevy problem imo