diff --git a/Cargo.lock b/Cargo.lock
index 8f28421..3172a62 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -98,6 +98,7 @@ name = "ascend"
version = "0.0.0"
dependencies = [
"axum",
+ "camino",
"clap",
"console_error_panic_hook",
"derive_more",
@@ -105,12 +106,15 @@ dependencies = [
"leptos",
"leptos_axum",
"leptos_meta",
+ "leptos_router",
"serde",
+ "serde_json",
"tokio",
"tower 0.4.13",
"tower-http 0.5.2",
"tracing",
"tracing-subscriber",
+ "type-toppings",
"wasm-bindgen",
]
@@ -514,6 +518,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+[[package]]
+name = "error_reporter"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8"
+
[[package]]
name = "event-listener"
version = "5.4.0"
@@ -647,8 +657,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"wasi",
+ "wasm-bindgen",
]
[[package]]
@@ -1036,15 +1048,16 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leptos"
-version = "0.7.0"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba5046c590aea121f6ad5e71fcb75453a933425d39527b9a3b1b295235afc8df"
+checksum = "a8a90c679094979aa12927e8e925fe8eead1420d69420b2d8c6540863937ca75"
dependencies = [
"any_spawner",
"base64",
"cfg-if",
"either_of",
"futures",
+ "getrandom",
"hydration_context",
"leptos_config",
"leptos_dom",
@@ -1065,6 +1078,7 @@ dependencies = [
"tachys",
"thiserror 2.0.11",
"throw_error",
+ "tracing",
"typed-builder",
"typed-builder-macro",
"wasm-bindgen",
@@ -1110,15 +1124,16 @@ dependencies = [
[[package]]
name = "leptos_dom"
-version = "0.7.0"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c15aca81dc2edd040b51c46734f65c6f36e6ba8a31347c1354c94b958044ae0"
+checksum = "99803be421344a2184fd5796e1a7645c2090738b2ab5d1a856084816853ec322"
dependencies = [
"js-sys",
"or_poisoned",
"reactive_graph",
"send_wrapper",
"tachys",
+ "tracing",
"wasm-bindgen",
"web-sys",
]
@@ -1175,6 +1190,7 @@ dependencies = [
"rstml",
"server_fn_macro",
"syn",
+ "tracing",
"uuid",
]
@@ -1232,9 +1248,9 @@ dependencies = [
[[package]]
name = "leptos_server"
-version = "0.7.0"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93450589df3b3e398c7f5ea64d8f1c8369b1ba9b90e1f70f6cb996b8d443ca3e"
+checksum = "0fb23bd110ac04c7276aae3d8ba523f94cf06989d00b4e76eaee89451b06b494"
dependencies = [
"any_spawner",
"base64",
@@ -1248,6 +1264,7 @@ dependencies = [
"serde_json",
"server_fn",
"tachys",
+ "tracing",
]
[[package]]
@@ -1715,6 +1732,7 @@ dependencies = [
"serde",
"slotmap",
"thiserror 2.0.11",
+ "tracing",
"web-sys",
]
@@ -1938,9 +1956,9 @@ dependencies = [
[[package]]
name = "server_fn"
-version = "0.7.0"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "033cb8014aa86a7ce0c6ee58d23dce1a078b2e320dc6c53bb439663993199b1f"
+checksum = "d0b9f0d2eecb2bf4f909661acc731009e3657574dec93a0ec9f114e250f74bc4"
dependencies = [
"axum",
"bytes",
@@ -2125,6 +2143,7 @@ dependencies = [
"send_wrapper",
"slotmap",
"throw_error",
+ "tracing",
"wasm-bindgen",
"web-sys",
]
@@ -2423,6 +2442,15 @@ dependencies = [
"tracing-log",
]
+[[package]]
+name = "type-toppings"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2efc827a3c37071ebc9812b9bee5d6c0154fdf9ba6a7ec78b38515dbd4fde8e5"
+dependencies = [
+ "error_reporter",
+]
+
[[package]]
name = "typed-builder"
version = "0.20.0"
diff --git a/Cargo.toml b/Cargo.toml
index 89d60ec..77620b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,20 +9,28 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7", optional = true }
console_error_panic_hook = "0.1"
-leptos = { version = "=0.7.0" }
+leptos = { version = "0.7.3" }
leptos_axum = { version = "0.7", optional = true }
leptos_meta = { version = "0.7" }
-# leptos_router = { version = "0.7.0" }
+leptos_router = { version = "0.7.0" }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "=0.2.99"
-tracing = { version = "0.1", optional = true }
http = "1"
serde = { version = "1", features = ["derive"] }
-tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
derive_more = { version = "1", features = ["display", "error", "from"] }
clap = { version = "4.5.7", features = ["derive"] }
+serde_json = { version = "1", optional = true }
+camino = { version = "1.1", optional = true }
+type-toppings = { version = "0.2.1", features = ["result"] }
+
+# Tracing
+tracing = { version = "0.1", optional = true }
+tracing-subscriber = { version = "0.3.18", features = [
+ "env-filter",
+], optional = true }
+
[features]
hydrate = ["leptos/hydrate"]
@@ -32,11 +40,15 @@ ssr = [
"dep:tower",
"dep:tower-http",
"dep:leptos_axum",
+ "dep:camino",
+ "dep:serde_json",
"leptos/ssr",
"leptos_meta/ssr",
- # "leptos_router/ssr",
- "dep:tracing",
+ "leptos_router/ssr",
+
+ "tracing",
]
+tracing = ["leptos/tracing", "dep:tracing", "dep:tracing-subscriber"]
# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]
diff --git a/justfile b/justfile
index 9725619..61490bc 100644
--- a/justfile
+++ b/justfile
@@ -22,3 +22,7 @@ run-release:
#!/usr/bin/env bash
cd dist
LEPTOS_SITE_ROOT="site" LEPTOS_SITE_ADDR="127.0.0.1:1337" ./ascend serve
+
+reset-state:
+ cargo leptos serve -- reset-state
+
diff --git a/src/app.rs b/src/app.rs
index b47b709..725ad20 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,4 +1,3 @@
-use leptos::logging;
use leptos::prelude::*;
pub fn shell(options: LeptosOptions) -> impl IntoView {
@@ -26,6 +25,9 @@ pub fn App() -> impl leptos::IntoView {
use leptos_meta::Stylesheet;
use leptos_meta::Title;
+ #[cfg(feature = "ssr")]
+ tracing::debug!("Rendering root component");
+
// Provides context that manages stylesheets, titles, meta tags, etc.
leptos_meta::provide_meta_context();
@@ -35,17 +37,15 @@ pub fn App() -> impl leptos::IntoView {
// sets the document title
-
+
+
+
}
}
#[leptos::component]
fn Ascend() -> impl leptos::IntoView {
- logging::log!("Rendering root component");
-
leptos::view! {
-
- { "hello world" }
-
+
}
}
diff --git a/src/lib.rs b/src/lib.rs
index b17fb55..ec43a61 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,11 @@
pub mod app;
-pub mod pages {}
+pub mod pages {
+ pub mod wall;
+}
+pub mod components {}
+
+#[cfg(feature = "ssr")]
+pub mod server;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
@@ -8,9 +14,3 @@ pub fn hydrate() {
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}
-
-#[cfg(feature = "ssr")]
-pub mod server {
- #[derive(Debug, Clone)]
- pub struct AppState {}
-}
diff --git a/src/main.rs b/src/main.rs
index 4e79b79..0a360d0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,73 +1,15 @@
-#[cfg(feature = "ssr")]
-mod cli {
- #[derive(clap::Parser)]
- #[command(version, about, long_about = None)]
- pub struct Cli {
- #[command(subcommand)]
- pub command: Command,
- }
-
- #[derive(clap::Subcommand, Default)]
- pub enum Command {
- #[default]
- Serve,
- }
-}
-
+/// Server-side main function
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
- use ascend::app::App;
- use ascend::app::shell;
- use ascend::server::AppState;
- use axum::Router;
- use clap::Parser as _;
- use leptos::logging;
- use leptos::prelude::*;
- use leptos_axum::LeptosRoutes;
- use leptos_axum::generate_route_list;
- use tracing_subscriber::EnvFilter;
-
- tracing_subscriber::fmt()
- .without_time()
- .with_env_filter(EnvFilter::from_default_env())
- .pretty()
- .init();
-
- let cli = cli::Cli::parse();
- match cli.command {
- cli::Command::Serve => {}
- }
-
- // Setting get_configuration(None) means we'll be using cargo-leptos's env values
- // For deployment these variables are:
- //
- // Alternately a file can be specified such as Some("Cargo.toml")
- // The file would need to be included with the executable when moved to deployment
- let conf = get_configuration(None).unwrap();
- let leptos_options = conf.leptos_options;
- let addr = leptos_options.site_addr;
- let routes = generate_route_list(App);
-
- let app_state = AppState {};
-
- // build our application with a route
- let app = Router::new()
- .leptos_routes_with_context(&leptos_options, routes, move || provide_context(app_state.clone()), {
- let leptos_options = leptos_options.clone();
- move || shell(leptos_options.clone())
- })
- .fallback(leptos_axum::file_and_error_handler(shell))
- .with_state(leptos_options);
-
- let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
- logging::log!("listening on http://{addr}");
- axum::serve(listener, app.into_make_service()).await.unwrap();
+ ascend::server::main().await
}
+/// Client-side main function
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for a purely client-side app
// see lib.rs for hydration function instead
+ eprintln!("Main function is empty. Run with the \"ssr\" feature.");
}
diff --git a/src/pages/wall.rs b/src/pages/wall.rs
new file mode 100644
index 0000000..ed29d63
--- /dev/null
+++ b/src/pages/wall.rs
@@ -0,0 +1,22 @@
+use leptos::prelude::*;
+
+#[leptos::component]
+pub fn Wall() -> impl leptos::IntoView {
+ let cells = (1..=(12 * 12))
+ .into_iter()
+ .map(|i| {
+ let i = i.to_string();
+ view! {
+ { i }
+ }
+ })
+ .collect_view();
+
+ leptos::view! {
+
+ }
+}
diff --git a/src/server.rs b/src/server.rs
new file mode 100644
index 0000000..c8670b2
--- /dev/null
+++ b/src/server.rs
@@ -0,0 +1,178 @@
+//! Server-only features
+
+use persistence::Persistent;
+use state::PersistentState;
+use tracing::level_filters::LevelFilter;
+use type_toppings::ResultExt;
+
+pub mod cli {
+ //! Server CLI interface
+
+ #[derive(clap::Parser)]
+ #[command(version, about, long_about = None)]
+ pub struct Cli {
+ #[command(subcommand)]
+ pub command: Command,
+ }
+
+ #[derive(clap::Subcommand, Default)]
+ pub enum Command {
+ #[default]
+ Serve,
+
+ /// Resets state, replacing it with defaults
+ ResetState,
+ }
+}
+
+pub mod state {
+ //! Server state
+
+ use super::persistence::Persistent;
+ use serde::Deserialize;
+ use serde::Serialize;
+ use std::collections::BTreeMap;
+ use std::collections::BTreeSet;
+
+ #[derive(Clone, Debug)]
+ pub struct State {
+ pub persistent: Persistent,
+ }
+
+ #[derive(Serialize, Deserialize, Clone, Debug, Default)]
+ pub struct PersistentState {
+ pub wall: Wall,
+ }
+
+ #[derive(Serialize, Deserialize, Clone, Debug, Default)]
+ pub struct Wall {
+ pub rows: u32,
+ pub cols: u32,
+ pub holds: BTreeMap,
+ pub routes: BTreeSet,
+ }
+
+ #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
+ pub struct HoldPosition {
+ pub row: u32,
+ pub col: u32,
+ }
+
+ #[derive(Serialize, Deserialize, Clone, Debug, Default)]
+ pub struct Hold {
+ pub position: HoldPosition,
+ pub image: Option,
+ }
+
+ #[derive(Serialize, Deserialize, Clone, Debug, Default)]
+ pub struct Image {}
+
+ mod route {
+ use super::HoldPosition;
+ use serde::Deserialize;
+ use serde::Serialize;
+ use std::collections::BTreeMap;
+
+ #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
+ pub struct Route {
+ pub holds: BTreeMap,
+ }
+
+ /// The role of a hold on a route
+ #[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
+ pub enum HoldRole {
+ /// Start hold
+ Start,
+
+ /// Any hold on the route without a specific role
+ Normal,
+
+ /// Zone hold
+ Zone,
+
+ /// End hold
+ End,
+ }
+ }
+}
+
+pub mod persistence;
+
+pub const STATE_FILE: &str = "state.json";
+
+#[tracing::instrument]
+pub async fn main() {
+ use crate::server::cli::Cli;
+ use crate::server::cli::Command;
+ use clap::Parser as _;
+ use tracing_subscriber::EnvFilter;
+
+ tracing_subscriber::fmt()
+ .without_time()
+ .with_file(true)
+ .with_line_number(true)
+ .with_target(false)
+ .with_ansi(true)
+ .with_env_filter(EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into()))
+ .pretty()
+ .init();
+
+ let cli = Cli::parse();
+ match cli.command {
+ Command::Serve => serve().await.unwrap_or_report(),
+ Command::ResetState => {
+ let s = PersistentState::default();
+ let p = camino::Utf8Path::new(STATE_FILE);
+ tracing::info!("Resetting state to default: {p}");
+ Persistent::persist(p, &s).await.unwrap_or_report();
+ }
+ }
+}
+
+#[tracing::instrument(err)]
+async fn serve() -> Result<(), Error> {
+ use crate::app::App;
+ use crate::app::shell;
+ use crate::server::state::State;
+ use axum::Router;
+ use leptos::prelude::*;
+ use leptos_axum::LeptosRoutes;
+ use leptos_axum::generate_route_list;
+
+ // Setting get_configuration(None) means we'll be using cargo-leptos's env values
+ // For deployment these variables are:
+ //
+ // Alternately a file can be specified such as Some("Cargo.toml")
+ // The file would need to be included with the executable when moved to deployment
+ let conf = get_configuration(None).unwrap_or_report();
+ let leptos_options = conf.leptos_options;
+ let addr = leptos_options.site_addr;
+ let routes = generate_route_list(App);
+
+ tracing::info!("Loading state");
+ let server_state = State {
+ persistent: Persistent::::load(STATE_FILE.into()).await?,
+ };
+
+ // build our application with a route
+ let app = Router::new()
+ .leptos_routes_with_context(&leptos_options, routes, move || provide_context(server_state.clone()), {
+ let leptos_options = leptos_options.clone();
+ move || shell(leptos_options.clone())
+ })
+ .fallback(leptos_axum::file_and_error_handler(shell))
+ .with_state(leptos_options);
+
+ let listener = tokio::net::TcpListener::bind(&addr).await?;
+ tracing::info!("Listening on http://{addr}");
+ axum::serve(listener, app.into_make_service()).await?;
+
+ Ok(())
+}
+
+#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
+#[display("Server crash")]
+pub enum Error {
+ Io(std::io::Error),
+ Persistence(persistence::Error),
+}
diff --git a/src/server/persistence.rs b/src/server/persistence.rs
new file mode 100644
index 0000000..70e90cb
--- /dev/null
+++ b/src/server/persistence.rs
@@ -0,0 +1,122 @@
+use camino::Utf8Path;
+use camino::Utf8PathBuf;
+use serde::Serialize;
+use serde::de::DeserializeOwned;
+use std::sync::Arc;
+use tokio::sync::Mutex;
+
+#[derive(Debug, Clone)]
+pub struct Persistent {
+ state: Arc>,
+ file_path: Utf8PathBuf,
+}
+
+impl Persistent {
+ #[tracing::instrument(skip(state))]
+ pub fn new(state: T, file_path: Utf8PathBuf) -> Self {
+ Self {
+ state: Arc::new(Mutex::new(state)),
+ file_path,
+ }
+ }
+
+ /// Instantiates state from file system
+ #[tracing::instrument]
+ pub async fn load(file_path: Utf8PathBuf) -> Result
+ where
+ T: DeserializeOwned,
+ {
+ let content = tokio::fs::read_to_string(&file_path).await.map_err(|source| Error::Read {
+ file_path: file_path.to_owned(),
+ source,
+ })?;
+
+ let t = serde_json::from_str(&content).map_err(|source| Error::Deserialize {
+ file_path: file_path.to_owned(),
+ source,
+ })?;
+
+ let persistent = Self {
+ state: Arc::new(Mutex::new(t)),
+ file_path,
+ };
+ Ok(persistent)
+ }
+
+ /// Returns state
+ #[tracing::instrument(skip(self))]
+ pub async fn get(&self) -> T
+ where
+ T: Clone,
+ {
+ let state = self.state.lock().await;
+ state.clone()
+ }
+
+ /// Returns state
+ #[tracing::instrument(skip_all)]
+ pub async fn with(&self, f: F) -> R
+ where
+ F: FnOnce(&T) -> R,
+ {
+ let state = self.state.lock().await;
+ f(&state)
+ }
+
+ /// Updates and persists state
+ #[tracing::instrument(skip_all)]
+ pub async fn update(&self, f: F) -> Result<(), Error>
+ where
+ F: FnOnce(&mut T),
+ T: Serialize,
+ {
+ let mut state = self.state.lock().await;
+ f(&mut state);
+ Self::persist(&self.file_path, &state).await?;
+ Ok(())
+ }
+
+ /// Sets and persists state
+ #[tracing::instrument(skip_all)]
+ pub async fn set(&self, new_state: T) -> Result<(), Error>
+ where
+ T: Serialize,
+ {
+ self.update(move |state| {
+ *state = new_state;
+ })
+ .await
+ }
+
+ /// Persist.
+ ///
+ /// Implicitly called by `set` and `update`.
+ #[tracing::instrument(skip_all, err)]
+ pub async fn persist(file_path: &Utf8Path, state: &T) -> Result<(), Error>
+ where
+ T: Serialize,
+ {
+ let serialized = serde_json::to_string(state).map_err(|source| Error::Serialize { source })?;
+ tokio::fs::write(file_path, serialized).await.map_err(|source| Error::Write {
+ file_path: file_path.to_owned(),
+ source,
+ })?;
+ Ok(())
+ }
+}
+
+#[derive(Debug, derive_more::Error, derive_more::Display)]
+#[display("Persistent state error: {_variant}")]
+pub enum Error {
+ #[display("Failed to read file: {file_path}")]
+ Read { file_path: Utf8PathBuf, source: std::io::Error },
+
+ #[display("Failed to deserialize state from file: {file_path}")]
+ Deserialize { file_path: Utf8PathBuf, source: serde_json::Error },
+
+ #[display("Failed to serialize state")]
+ Serialize { source: serde_json::Error },
+
+ #[display("Failed to write file: {file_path}")]
+ Write { file_path: Utf8PathBuf, source: std::io::Error },
+}
diff --git a/state.json b/state.json
new file mode 100644
index 0000000..c283cc2
--- /dev/null
+++ b/state.json
@@ -0,0 +1 @@
+{"wall":{"rows":0,"cols":0,"holds":{},"routes":[]}}
\ No newline at end of file