From 6869cf65dde68a272052e90ca6128e67ccacefb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 21 Aug 2024 09:00:15 +0200 Subject: [PATCH] Reload on panic --- Cargo.toml | 4 ++ script.mjs | 33 +++++++++++--- src/app.rs | 121 ++++++++++++++++++++++++++++++++++------------------ src/main.rs | 24 ++++++++++- 4 files changed, 134 insertions(+), 48 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7993fc4..1424c3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" [dependencies] rinja_derive_standalone = { version = "*", path = "rinja/rinja_derive_standalone", features = ["humansize", "num-traits", "serde_json", "urlencode"] } +console_error_panic_hook = "0.1.7" gloo-utils = "0.2.0" once_cell = "1.19.0" prettyplease = "0.2.20" @@ -31,6 +32,9 @@ features = [ "Storage", ] +[lints.clippy] +type_complexity = "allow" + [profile.release] opt-level = "z" lto = "fat" diff --git a/script.mjs b/script.mjs index 63a3e1d..68b1830 100644 --- a/script.mjs +++ b/script.mjs @@ -85,23 +85,46 @@ window.save_clipboard = function (text) { }); }; -window.toggle_element = function(event, elementId) { +window.toggle_element = function (event, elementId) { if (event.target && event.target.id === elementId) { document.getElementById(elementId).classList.toggle("display"); } }; -window.handle_blur = function(event, elementId) { +window.handle_blur = function (event, elementId) { const parent = document.getElementById(elementId); - if (!parent.contains(document.activeElement) && + if ( + !parent.contains(document.activeElement) && !parent.contains(event.relatedTarget) ) { parent.classList.remove("display"); } }; -window.reset_code = function(event, text) { - const input = event.target.parentElement.parentElement.querySelector("textarea"); +window.reset_code = function (event, text) { + const input = + event.target.parentElement.parentElement.querySelector("textarea"); input.value = text; input.dispatchEvent(new Event("input")); }; + +const state = history.state || {}; +const reload_counter = +state.reload_counter || 0; +if (reload_counter > 0) { + console.warn("reload_counter: ", reload_counter); +} +state.reload_counter = 0; +history.replaceState(state, ""); + +window.panic_reload = function () { + state.reload_counter = reload_counter + 1; + history.replaceState(state, ""); + window.setTimeout(function () { + if (reload_counter > 2) { + console.warn("Hit a panic. Three times."); + } else { + console.warn("Hit a panic, reloading."); + window.location.reload(); + } + }, 0); +}; diff --git a/src/app.rs b/src/app.rs index 9e4a2d6..8f643b2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -20,7 +20,7 @@ use crate::editor::Editor; use crate::{ThrowAt, ASSETS}; #[derive(Properties, PartialEq, Clone)] -pub struct Props { +struct Props { theme: Rc, rust: Rc, tmpl: Rc, @@ -29,50 +29,13 @@ pub struct Props { timeout: Option, } -fn local_storage() -> Option { - let window = window()?; - match window.local_storage() { - Ok(storage) => storage, - _ => None, - } -} - -const THEME_SOURCE_KEY: &str = "play-rinja-theme"; -const STRUCT_SOURCE_KEY: &str = "play-rinja-struct"; -const TMPL_SOURCE_KEY: &str = "play-rinja-template"; - -fn get_data_from_local_storage(storage: &Storage, key: &str) -> Option { - let text = storage.get_item(key).ok()??; - JSON::parse(&text).ok()?.as_string() -} - -fn save_to_local_storage(storage: &Storage, key: &str, data: &str) { - if let Ok(data) = JSON::stringify(&JsValue::from_str(data)) { - if let Some(data) = data.as_string() { - // Doesn't matter whether or not it succeeded. - let _ = storage.set_item(key, &data); - } - } -} - #[function_component] pub fn App() -> Html { let state = use_state(|| { - let local_storage = local_storage(); - let local_storage_or = |default: &str, key: &str| -> Rc { - let value = local_storage - .as_ref() - .and_then(|ls| get_data_from_local_storage(ls, key)); - match value.as_deref() { - Some(value) => value.into(), - None => default.into(), - } - }; - - let theme = local_storage_or(DEFAULT_THEME, THEME_SOURCE_KEY); - let rust = local_storage_or(STRUCT_SOURCE, STRUCT_SOURCE_KEY); - let tmpl = local_storage_or(TMPL_SOURCE, TMPL_SOURCE_KEY); - + let (theme, rust, tmpl) = get_last_editor_state().unwrap_or_default(); + let theme = theme.unwrap_or_else(|| Rc::from(DEFAULT_THEME)); + let rust = rust.unwrap_or_else(|| Rc::from(STRUCT_SOURCE)); + let tmpl = tmpl.unwrap_or_else(|| Rc::from(TMPL_SOURCE)); let (code, duration) = convert_source(&rust, &tmpl); Props { theme, @@ -368,6 +331,80 @@ pub fn App() -> Html { } } +const THEME_SOURCE_KEY: &str = "play-rinja-theme"; +const STRUCT_SOURCE_KEY: &str = "play-rinja-struct"; +const TMPL_SOURCE_KEY: &str = "play-rinja-template"; + +fn local_storage() -> Option { + let window = window()?; + match window.local_storage() { + Ok(storage) => storage, + _ => None, + } +} + +fn save_to_local_storage(storage: &Storage, key: &str, data: &str) { + if let Ok(data) = JSON::stringify(&JsValue::from_str(data)) { + if let Some(data) = data.as_string() { + // Doesn't matter whether or not it succeeded. + let _ = storage.set_item(key, &data); + } + } +} + +// Read last editor state from local storage. +// Then delete the known editor state. +// Then, if the app did not crash while processing the retrieved state, save it again. +fn get_last_editor_state() -> Option<(Option>, Option>, Option>)> { + let window = window()?; + let storage = window.local_storage().ok().flatten()?; + + let mut theme = None; + let mut rust = None; + let mut tmpl = None; + let mut raw_theme = None; + let mut raw_rust = None; + let mut raw_tmpl = None; + + for (key, raw_dest, dest) in [ + (THEME_SOURCE_KEY, &mut raw_theme, &mut theme), + (STRUCT_SOURCE_KEY, &mut raw_rust, &mut rust), + (TMPL_SOURCE_KEY, &mut raw_tmpl, &mut tmpl), + ] { + let Some(raw) = storage.get_item(key).ok().flatten() else { + continue; + }; + let _ = storage.remove_item(key); + let Some(parsed) = JSON::parse(&raw).ok().and_then(|s| s.as_string()) else { + continue; + }; + *raw_dest = Some(raw); + *dest = Some(Rc::from(parsed)); + } + if theme.is_none() && rust.is_none() && theme.is_none() { + return None; + } + + let callback = Closure::once(move || { + if crate::PANICKED.load(std::sync::atomic::Ordering::Acquire) { + return; + } + + for (key, value) in [ + (THEME_SOURCE_KEY, raw_theme.take()), + (STRUCT_SOURCE_KEY, raw_rust.take()), + (TMPL_SOURCE_KEY, raw_tmpl.take()), + ] { + if let Some(value) = value { + let _ = storage.set_item(key, &value); + } + } + }); + let _ = window.set_timeout_with_callback(callback.into_js_value().unchecked_ref()); + + Some((theme, rust, tmpl)) +} + fn replace_timeout(new_state: &mut Props, state: UseStateHandle) { let handler = Closure::::new({ let theme = Rc::clone(&new_state.theme); diff --git a/src/main.rs b/src/main.rs index 0de53ec..e4dc267 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,38 @@ mod app; mod editor; -use std::panic::Location; +use std::panic::{Location, PanicInfo}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::{Relaxed, SeqCst}; use once_cell::sync::Lazy; use syntect::highlighting::Theme; use syntect::parsing::SyntaxSet; use syntect_assets::assets::HighlightingAssets; +use wasm_bindgen::prelude::wasm_bindgen; use web_sys::js_sys::Error; use web_sys::wasm_bindgen::throw_val; use crate::app::App; fn main() { + yew::set_custom_panic_hook({ + Box::new(move |info: &PanicInfo| { + if PANICKED + .compare_exchange(false, true, SeqCst, Relaxed) + .is_ok() + { + panic_reload(); + } + console_error_panic_hook::hook(info); + }) + }); + yew::Renderer::::new().render(); } +static PANICKED: AtomicBool = AtomicBool::new(false); + trait ThrowAt { fn unwrap_at(self) -> T; } @@ -72,3 +89,8 @@ static ASSETS: Lazy<(&SyntaxSet, &[(&str, &Theme)])> = Lazy::new(|| { let syntax_set = assets.get_syntax_set().unwrap_at(); (syntax_set, themes) }); + +#[wasm_bindgen] +extern "C" { + fn panic_reload(); +}