Skip to content

Commit

Permalink
feat: added live reloading
Browse files Browse the repository at this point in the history
Closes #122.
  • Loading branch information
arctic-hen7 committed Jan 29, 2022
1 parent 2260885 commit 2e33424
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 3 deletions.
11 changes: 9 additions & 2 deletions packages/perseus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ sycamore = { version = "^0.7.1", features = ["ssr"] }
sycamore-router = "^0.7.1"
perseus-macro = { path = "../perseus-macro", version = "0.3.2" }
# 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" ] }
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4"
serde = { version = "1", features = ["derive"] }
Expand All @@ -36,9 +36,12 @@ unic-langid = { version = "0.9", optional = true }
intl-memoizer = { version = "0.5", optional = true }
tokio = { version = "1", features = [ "fs", "io-util" ] }
rexie = { version = "0.2", optional = true }
js-sys = { version = "0.3", optional = true }

[features]
default = []
# Live reloading will only take effect in development, and won't impact production
# BUG This adds 400B to the production bundle (that's without size optimizations though)
default = [ "live-reload" ]
translator-fluent = ["fluent-bundle", "unic-langid", "intl-memoizer"]
# This feature makes tinker-only plugins be registered (this flag is enabled internally in the engine)
tinker-plugins = []
Expand All @@ -58,3 +61,7 @@ idb-freezing = [ "rexie", "web-sys/StorageManager" ]
# Switches to expecting the server to provide a JS bundle that's been created from Wasm
# Note that this is highly experimental, and currently blocked by [rustwasm/wasm-bindgen#2735](https://github.com/rustwasm/wasm-bindgen/issues/2735)
wasm2js = []
# Enables automatic browser reloading whenever you make a change
live-reload = [ "js-sys", "web-sys/WebSocket", "web-sys/MessageEvent", "web-sys/ErrorEvent", "web-sys/BinaryType", "web-sys/Location" ]
# Enables hot state reloading, whereby your entire app's state can be frozen and thawed automatically every time you change code in your app
hsr = [ "live-reload", "idb-freezing" ]
6 changes: 5 additions & 1 deletion packages/perseus/src/router/router_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,17 @@ pub fn perseus_router<AppRoute: PerseusRoute<TemplateNodeType> + 'static>(
}),
);

// TODO State thawing in HSR
// If live reloading is enabled, connect to the server now
#[cfg(all(feature = "live-reload", debug_assertions))]
crate::state::connect_to_reload_server();

view! {
Router(RouterProps::new(HistoryIntegration::new(), cloned!(on_route_change_props => move |route: ReadSignal<AppRoute>| {
// Sycamore's reactivity is broken by a future, so we need to explicitly add the route to the reactive dependencies here
// We do need the future though (otherwise `container_rx` doesn't link to anything until it's too late)
create_effect(cloned!(route, on_route_change_props => move || {
let verdict = route.get().get_verdict().clone();
crate::web_log!("test");
on_route_change(verdict, on_route_change_props.clone());
}));

Expand Down
80 changes: 80 additions & 0 deletions packages/perseus/src/state/live_reload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use web_sys::{ErrorEvent, MessageEvent, WebSocket};

/// Connects to the reload server if it's online.
pub fn connect_to_reload_server() {
// Get the host and port
let host = get_window_var("__PERSEUS_RELOAD_SERVER_HOST");
let port = get_window_var("__PERSEUS_RELOAD_SERVER_PORT");
let (host, port) = match (host, port) {
(Some(host), Some(port)) => (host, port),
// If either the host or port weren't set, the server almost certainly isn't online, so we won't connect
_ => return,
};

// Connect to the server (it's expected that the host does not include a protocol)
let ws = match WebSocket::new(&format!("ws://{}:{}/receive", host, port)) {
Ok(ws) => ws,
Err(err) => return log(&format!("Connection failed: {:?}.", err)),
};
// This is apparently more efficient for small bianry messages
ws.set_binary_type(web_sys::BinaryType::Arraybuffer);

// Set up a message handler
let onmessage_callback = Closure::wrap(Box::new(move |_| {
// With this server, if we receive any message it will be telling us to reload, so we'll do so
wasm_bindgen_futures::spawn_local(async move {
// TODO If we're using HSR, freeze the state to IndexedDB
#[cfg(feature = "hsr")]
todo!();
// Force reload the page, getting all resources from the sevrer again (to get the new code)
log("Reloading...");
match web_sys::window()
.unwrap()
.location()
.reload_with_forceget(true)
{
Ok(_) => (),
Err(err) => log(&format!("Reloading failed: {:?}.", err)),
};
// We shouldn't ever get here unless there was an error, the entire page will be fully reloaded
});
}) as Box<dyn FnMut(MessageEvent)>);
ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
// To keep the closure alive, we need to forget about it
onmessage_callback.forget();

// We should log to the console about errors from the server
let onerror_callback = Closure::wrap(Box::new(move |err: ErrorEvent| {
log(&format!("Error: {:?}.", err));
}) as Box<dyn FnMut(ErrorEvent)>);
ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
onerror_callback.forget();

// We'll just log when the connection is successfully established for informational purposes
let onopen_callback = Closure::wrap(Box::new(move |_| {
log("Connected.");
}) as Box<dyn FnMut(JsValue)>);
ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
onopen_callback.forget();
}

fn get_window_var(name: &str) -> Option<String> {
let val_opt = web_sys::window().unwrap().get(name);
let js_obj = match val_opt {
Some(js_obj) => js_obj,
None => return None,
};
// The object should only actually contain the string value that was injected
let state_str = match js_obj.as_string() {
Some(state_str) => state_str,
None => return None,
};
// On the server-side, we encode a `None` value directly (otherwise it will be some convoluted stringified JSON)
Some(state_str)
}

/// An internal function for logging data for development reloading.
fn log(msg: &str) {
web_sys::console::log_1(&JsValue::from("[Live Reload Server]: ".to_string() + msg));
}
6 changes: 6 additions & 0 deletions packages/perseus/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ pub use rx_state::{AnyFreeze, Freeze, MakeRx, MakeUnrx};
mod freeze_idb;
#[cfg(feature = "idb-freezing")]
pub use freeze_idb::*; // TODO Be specific here

// We'll allow live reloading (of which HSR is a subset) if it's feature-enabled and we're in development mode
#[cfg(all(feature = "live-reload", debug_assertions))]
mod live_reload;
#[cfg(all(feature = "live-reload", debug_assertions))]
pub use live_reload::connect_to_reload_server;

0 comments on commit 2e33424

Please sign in to comment.