Skip to content

Commit

Permalink
feat: added suspended interactivity system
Browse files Browse the repository at this point in the history
This is gated behind the non-default feature `suspended-interaction`, as
it is quite opinionated, and may worsen user experience in some cases.
  • Loading branch information
arctic-hen7 committed Dec 31, 2022
1 parent 53de18d commit 5efcad4
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 2 deletions.
4 changes: 3 additions & 1 deletion packages/perseus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ js-sys = { version = "0.3", optional = true }
# Note that this is not needed in production, but that can't be specified, so it will just be compiled away to nothing
console_error_panic_hook = { version = "0.1.6", optional = true }
# TODO review feature flags here
web-sys = { version = "0.3", features = [ "Headers", "Navigator", "NodeList", "Request", "RequestInit", "RequestMode", "Response", "ReadableStream", "Window" ] }
web-sys = { version = "0.3", features = [ "Headers", "Navigator", "NodeList", "Request", "RequestInit", "RequestMode", "Response", "ReadableStream", "Window", "CustomEvent", "CustomEventInit" ] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"

Expand All @@ -53,6 +53,8 @@ wasm-bindgen-futures = "0.4"
default = [ "live-reload", "hsr", "client-helpers", "macros", "dflt-engine", "minify", "minify-css", "cache-initial-load" ]
translator-fluent = ["fluent-bundle", "unic-langid", "intl-memoizer"]
translator-lightweight = []
# Suspends all `click` events at the document root until Perseus is fully loaded, then passing them through.
suspended-interaction = []
# This feature adds support for a number of macros that will make your life MUCH easier (read: use this unless you have very specific needs or are completely insane)
macros = [ "perseus-macro" ]
# This feature enable support for functions that make using the default engine configuration much easier.
Expand Down
32 changes: 32 additions & 0 deletions packages/perseus/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use sycamore::prelude::create_scope;
#[cfg(feature = "hydrate")]
use sycamore::utils::hydrate::with_hydration_context;
use wasm_bindgen::JsValue;
use web_sys::{CustomEvent, CustomEventInit};

/// The entrypoint into the app itself. This will be compiled to Wasm and
/// actually executed, rendering the rest of the app. Runs the app in the
Expand Down Expand Up @@ -46,6 +47,15 @@ pub fn run_client<M: MutableStore, T: TranslationsManager>(
#[cfg(not(debug_assertions))]
crate::web_log!("[CRITICAL ERROR]: Perseus has panicked! An error message has hopefully been displayed on your screen explaining this; if not, then reloading the page might help.");

// Make it clear that apps compiled with unwinding panics might continue now
// (for completeness)
#[cfg(panic = "unwind")]
crate::web_log!("[WARNING]: The app has been compiled with unwinding panics, and it is possible that the app will now continue normal operation if this panic is handled.");

// Make sure the load event is submitted so interaction isn't totally suspended
// forever
dispatch_loaded(false, true);

// Run the user's arbitrary panic handler
if let Some(panic_handler) = &general_panic_handler {
panic_handler(panic_info);
Expand Down Expand Up @@ -102,6 +112,8 @@ pub fn run_client<M: MutableStore, T: TranslationsManager>(
}
});

dispatch_loaded(running, false);

// If we failed, terminate
if !running {
// SAFETY We're outside the app's scope.
Expand All @@ -121,3 +133,23 @@ pub fn run_client<M: MutableStore, T: TranslationsManager>(
/// A convenience type wrapper for the type returned by nearly all client-side
/// entrypoints.
pub type ClientReturn = Result<(), JsValue>;

/// Regardless of whether an error or a proper render was triggered, allow the
/// browser to send through click events etc. (these are suspended until we've
/// rendered to improve user experience and apparent responsiveness, but this
/// has no impact on machine-measured metrics on UX). This also allows neat
/// interoperability with other code running outside Perseus.
///
/// This will provide as part of the event whether or not the app is running. If
/// this is `false`, the app will terminate immediately afterward. If it is
/// `null` (thank you dynamic typing!), the app has panicked, and further
/// behavior is unspecified, because the app might have been built with
/// unwinding panics, and there could be a user function for catching panics.
fn dispatch_loaded(running: bool, panic: bool) {
let document = web_sys::window().unwrap().document().unwrap();
let mut ev_init = CustomEventInit::new();
// We provide whether or not the app is actually running to this
ev_init.detail(&if panic { JsValue::NULL } else { running.into() });
let ev = CustomEvent::new_with_event_init_dict("__perseus_loaded", &ev_init).unwrap();
document.dispatch_event(&ev).unwrap();
}
2 changes: 1 addition & 1 deletion packages/perseus/src/reactor/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
use sycamore::prelude::{create_effect, create_signal, on_mount, view, ReadSignal, Scope, View};
use sycamore_futures::spawn_local_scoped;
use sycamore_router::{navigate_replace, HistoryIntegration, RouterBase};
use web_sys::Element;
use web_sys::{CustomEvent, CustomEventInit, Element};

// We don't want to bring in a styling library, so we do this the old-fashioned
// way! We're particularly comprehensive with these because the user could
Expand Down
37 changes: 37 additions & 0 deletions packages/perseus/src/server/html_shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,39 @@ impl HtmlShell {
scripts_before_boundary.push("window.__PERSEUS_TESTING = true;".into());
}

// This script stores all user-provided clicks in a JS array, blocking them
// until the app has loaded, at which time they'll all be re-emitted.
// This *can* improve user experience in some cases, and it's available
// as a feature flag. Clicks on links are deliberately exempted from
// this, because they can be resolved by the browser without needing
// interactivity (it'll be slower, but instant navigation is generally a
// better experience).
//
// This feature is heavily opinionated, and should not be enabled by default.
#[cfg(feature = "suspended-interaction")]
let suspend_script = r#"
window.__PERSEUS_SUSPENDED = true;
window.__PERSEUS_SUSPENDED_EVENTS = [];
const clickHandler = (ev) => {{
if (window.__PERSEUS_SUSPENDED && ev.target.tagName !== "A") {{
ev.preventDefault();
ev.stopPropagation();
window.__PERSEUS_SUSPENDED_EVENTS.push(ev);
}}
}};
document.addEventListener("click", clickHandler, true);
document.addEventListener("__perseus_loaded", () => {{
window.__PERSEUS_SUSPENDED = false;
for (const ev of window.__PERSEUS_SUSPENDED_EVENTS) {{
ev.target.dispatchEvent(ev);
}}
document.removeEventListener("click", clickHandler);
}});
"#;
#[cfg(not(feature = "suspended-interaction"))]
let suspend_script = "";
// Define the script that will load the Wasm bundle (inlined to avoid
// unnecessary extra requests) If we're using the `wasm2js` feature,
// this will try to load a JS version instead (expected to be at
Expand All @@ -90,6 +123,8 @@ impl HtmlShell {
await init("{path_prefix}/.perseus/bundle.wasm");
}}
main();
{suspend_script}
"#,
path_prefix = path_prefix
);
Expand All @@ -101,6 +136,8 @@ impl HtmlShell {
await init("{path_prefix}/.perseus/bundle.wasm.js");
}}
main();
{suspend_script}
"#,
path_prefix = path_prefix
);
Expand Down

0 comments on commit 5efcad4

Please sign in to comment.