From 9451980b25ec1534f4ecc19a0a4c86c4ad859680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Juul=20Brunsh=C3=B8j?= Date: Thu, 6 Feb 2025 00:29:54 +0100 Subject: [PATCH] wip --- Cargo.lock | 1 + crates/ascend/Cargo.toml | 3 +- crates/ascend/src/app.rs | 36 +------ crates/ascend/src/models.rs | 4 +- crates/ascend/src/pages/routes.rs | 59 +++++++---- crates/ascend/src/pages/wall.rs | 16 +-- crates/ascend/src/server.rs | 5 +- crates/ascend/src/server/db.rs | 78 +++++++++++---- crates/ascend/src/server/migrations.rs | 35 ++++--- crates/ascend/src/server/operations.rs | 31 +++--- crates/ascend/src/server_functions.rs | 132 ++++++++++++++++++++----- todo.md | 1 + 12 files changed, 260 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07f4268..f6b95b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,7 @@ dependencies = [ "confik", "console_error_panic_hook", "derive_more", + "error_reporter", "http 1.2.0", "leptos", "leptos_axum", diff --git a/crates/ascend/Cargo.toml b/crates/ascend/Cargo.toml index 061ced5..9648a39 100644 --- a/crates/ascend/Cargo.toml +++ b/crates/ascend/Cargo.toml @@ -38,7 +38,7 @@ tower-http = { version = "0.5", features = ["fs"], optional = true } tracing = { version = "0.1" } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber-wasm = "0.1.0" -type-toppings = { version = "0.2.1", features = ["result"] } +type-toppings = { version = "0.2.1", features = ["result", "iterator"] } wasm-bindgen = "=0.2.99" web-sys = { version = "0.3.76", features = ["File", "FileList"] } xdg = { version = "2.5", optional = true } @@ -47,6 +47,7 @@ redb = { version = "2.4", optional = true } bincode = { version = "1.3", optional = true } serde_json = { version = "1" } codee = { version = "0.3" } +error_reporter = { version = "1" } [dev-dependencies.serde_json] version = "1" diff --git a/crates/ascend/src/app.rs b/crates/ascend/src/app.rs index 8933249..4e33b71 100644 --- a/crates/ascend/src/app.rs +++ b/crates/ascend/src/app.rs @@ -1,11 +1,7 @@ -use crate::codec::ron::Ron; -use crate::codec::ron::RonEncoded; -use crate::models; use crate::pages; use leptos::prelude::*; use leptos_router::components::*; use leptos_router::path; -use std::sync::Arc; pub fn shell(options: LeptosOptions) -> impl IntoView { use leptos_meta::MetaTags; @@ -61,7 +57,7 @@ pub fn Home() -> impl leptos::IntoView { let action = Action::new(|()| async move { tracing::debug!("running action"); - let walls = get_walls() + let walls = crate::server_functions::get_walls() .await .inspect_err(|e| { dbg!(e); @@ -85,33 +81,3 @@ pub fn Home() -> impl leptos::IntoView { leptos::view! {} } - -#[server( - input = Ron, - output = Ron, - custom = RonEncoded -)] -#[tracing::instrument(skip_all, err)] -async fn get_walls() -> Result>, ServerFnError> { - use redb::ReadableTable; - - tracing::debug!("get walls"); - - let db = expect_context::>(); - tracing::debug!("got db from context"); - - let walls = tokio::task::spawn_blocking(move || -> Result, ServerFnError> { - tracing::debug!("beginning read transaction"); - let read_txn = db.begin_read()?; - - tracing::debug!("opening table"); - let walls_table = read_txn.open_table(crate::server::db::current::TABLE_WALLS)?; - tracing::debug!("opened table"); - let walls: Vec = walls_table.iter()?.map(|r| r.map(|(_, v)| v.value())).collect::>()?; - - Ok(walls) - }) - .await??; - - Ok(RonEncoded::new(walls)) -} diff --git a/crates/ascend/src/models.rs b/crates/ascend/src/models.rs index 728c940..569b986 100644 --- a/crates/ascend/src/models.rs +++ b/crates/ascend/src/models.rs @@ -32,7 +32,7 @@ pub mod v2 { pub problems: BTreeSet, } - #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, derive_more::FromStr, derive_more::Display)] + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)] pub struct WallUid(pub uuid::Uuid); impl WallUid { pub fn new() -> Self { @@ -50,7 +50,7 @@ pub mod v2 { pub date_added: chrono::DateTime, } - #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)] pub struct ProblemUid(pub uuid::Uuid); impl ProblemUid { pub fn new() -> Self { diff --git a/crates/ascend/src/pages/routes.rs b/crates/ascend/src/pages/routes.rs index 7a93791..a1278c9 100644 --- a/crates/ascend/src/pages/routes.rs +++ b/crates/ascend/src/pages/routes.rs @@ -15,6 +15,7 @@ use std::ops::Deref; #[derive(Params, PartialEq, Clone)] struct RouteParams { + // Is never None wall_uid: Option, } @@ -24,12 +25,13 @@ pub fn Routes() -> impl leptos::IntoView { tracing::debug!("Enter"); let params = leptos_router::hooks::use_params::(); + let wall_uid = Signal::derive(move || params.get().map(|p| p.wall_uid.expect("wall_uid param is never None"))); - let problems = Resource::, Ron>::new_with_options( - move || params.get().map(|p| p.wall_uid), - move |wall_uid: Result, _>| async move { - if let Ok(Some(wall_uid)) = wall_uid { - let wall = crate::server_functions::get_wall(wall_uid).await.unwrap().into_inner(); + let problems = Resource::>, Ron>::new_with_options( + move || wall_uid.get(), + move |wall_uid: Result| async move { + if let Ok(wall_uid) = wall_uid { + let wall = crate::server_functions::get_problems_for_wall(wall_uid).await.unwrap().into_inner(); Some(wall) } else { None @@ -55,22 +57,43 @@ pub fn Routes() -> impl leptos::IntoView {
- - - + {wall_uid.get().map(|wall_uid| view! {})} + + + + } + } + /> +
} } #[component] -fn Ready(data: InitialData) -> impl leptos::IntoView { +#[tracing::instrument(skip_all)] +fn Problem(problem: models::Problem) -> impl IntoView { + tracing::debug!("Enter"); + + view! { +

"problem"

+ } +} + +#[component] +#[tracing::instrument(skip_all)] +fn Import(wall_uid: WallUid) -> impl IntoView { tracing::debug!("ready"); let import_from_mini_moonboard = Action::from(ServerAction::::new()); let onclick = move |_mouse_event| { - import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard {}); + import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard { wall_uid }); }; view! { @@ -81,17 +104,17 @@ fn Ready(data: InitialData) -> impl leptos::IntoView { #[server(name = ImportFromMiniMoonboard)] #[tracing::instrument] -async fn import_from_mini_moonboard() -> Result<(), ServerFnError> { +async fn import_from_mini_moonboard(wall_uid: WallUid) -> Result<(), ServerFnError> { use crate::server::config::Config; + use crate::server::db::Database; - todo!() - // tracing::info!("Importing mini moonboard problems"); + tracing::info!("Importing mini moonboard problems"); - // let config = expect_context::(); - // let state = expect_context::(); + let config = expect_context::(); + let db = expect_context::(); - // crate::server::operations::import_mini_moonboard_problems(&config, &state).await?; + crate::server::operations::import_mini_moonboard_problems(&config, db, wall_uid).await?; - // // TODO: Return information about what was done - // Ok(()) + // TODO: Return information about what was done + Ok(()) } diff --git a/crates/ascend/src/pages/wall.rs b/crates/ascend/src/pages/wall.rs index df5cb30..57ca411 100644 --- a/crates/ascend/src/pages/wall.rs +++ b/crates/ascend/src/pages/wall.rs @@ -23,11 +23,12 @@ pub fn Wall() -> impl leptos::IntoView { tracing::debug!("Enter"); let params = leptos_router::hooks::use_params::(); + let wall_uid = move || params.get().map(|p| p.wall_uid.expect("wall_uid param is never None")); let wall = Resource::, Ron>::new_with_options( - move || params.get().map(|p| p.wall_uid), - move |wall_uid: Result, _>| async move { - if let Ok(Some(wall_uid)) = wall_uid { + move || wall_uid(), + move |wall_uid: Result| async move { + if let Ok(wall_uid) = wall_uid { let wall = crate::server_functions::get_wall(wall_uid).await.unwrap().into_inner(); Some(wall) } else { @@ -37,7 +38,7 @@ pub fn Wall() -> impl leptos::IntoView { false, ); - let header_items = HeaderItems { + let header_items = move || HeaderItems { left: vec![], middle: vec![HeaderItem { text: "ASCEND".to_string(), @@ -46,18 +47,18 @@ pub fn Wall() -> impl leptos::IntoView { right: vec![ HeaderItem { text: "Routes".to_string(), - link: Some("/wall/routes".to_string()), + link: wall_uid().map(|uid| format!("/wall/{uid}/routes")).ok(), }, HeaderItem { text: "Holds".to_string(), - link: Some("/wall/edit".to_string()), + link: wall_uid().map(|uid| format!("/wall/{uid}/edit")).ok(), }, ], }; leptos::view! {
- +
impl leptos::IntoView { } #[component] +#[tracing::instrument(skip_all)] fn Ready(wall: models::Wall) -> impl leptos::IntoView { tracing::debug!("ready"); diff --git a/crates/ascend/src/server.rs b/crates/ascend/src/server.rs index 933c490..a870fab 100644 --- a/crates/ascend/src/server.rs +++ b/crates/ascend/src/server.rs @@ -5,7 +5,6 @@ use config::Config; use confik::Configuration; use confik::EnvSource; use std::path::Path; -use std::sync::Arc; use tower_http::services::ServeDir; use tracing::level_filters::LevelFilter; use type_toppings::ResultExt; @@ -51,7 +50,7 @@ async fn serve(cli: Cli) -> Result<(), Error> { use leptos_axum::generate_route_list; tracing::debug!("Creating DB"); - let db = Arc::new(db::create()?); + let db = db::Database::create()?; migrations::run_migrations(&db).await.map_err(Error::Migration)?; @@ -73,7 +72,7 @@ async fn serve(cli: Cli) -> Result<(), Error> { &leptos_options, routes, move || { - leptos::prelude::provide_context::>(Arc::clone(&db)); + leptos::prelude::provide_context::(db.clone()); leptos::prelude::provide_context::(config.clone()) }, { diff --git a/crates/ascend/src/server/db.rs b/crates/ascend/src/server/db.rs index 39f6c96..ddc03b8 100644 --- a/crates/ascend/src/server/db.rs +++ b/crates/ascend/src/server/db.rs @@ -1,32 +1,74 @@ use bincode::Bincode; -use redb::Database; +use redb::ReadTransaction; use redb::TableDefinition; +use redb::WriteTransaction; use serde::Deserialize; use serde::Serialize; use std::path::PathBuf; +use std::sync::Arc; mod bincode; -pub const DB_FILE: &str = "datastore/private/ascend.redb"; +const DB_FILE: &str = "datastore/private/ascend.redb"; -#[tracing::instrument(skip_all, err)] -pub fn create() -> Result { - let file = PathBuf::from(DB_FILE); - - // Create parent dirs - if let Some(parent_dir) = file.parent() { - std::fs::create_dir_all(parent_dir)?; - } - - let db = Database::create(file)?; - Ok(db) +#[derive(Debug, Clone)] +pub struct Database { + db: Arc, } -#[tracing::instrument(skip_all)] -pub fn get_version(db: &Database) -> Result, redb::Error> { - let txn = db.begin_read()?; - let version = txn.open_table(TABLE_VERSION)?.get(())?.map(|v| v.value()); - Ok(version) +impl Database { + #[tracing::instrument(skip_all, err)] + pub fn create() -> Result { + let file = PathBuf::from(DB_FILE); + + // Create parent dirs + if let Some(parent_dir) = file.parent() { + std::fs::create_dir_all(parent_dir)?; + } + + let db = redb::Database::create(file)?; + Ok(Self { db: Arc::new(db) }) + } + + #[tracing::instrument(skip_all, err)] + pub async fn read(&self, f: impl FnOnce(&'_ ReadTransaction) -> Result) -> Result { + tokio::task::block_in_place(|| { + let dbtx = self.db.begin_read()?; + f(&dbtx) + }) + } + + #[tracing::instrument(skip_all, err)] + pub async fn write(&self, f: impl FnOnce(&'_ WriteTransaction) -> Result) -> Result { + tokio::task::block_in_place(|| { + let dbtx = self.db.begin_write()?; + let res = f(&dbtx)?; + dbtx.commit()?; + Ok(res) + }) + } + + #[tracing::instrument(skip_all)] + pub async fn get_version(&self) -> Result, DatabaseOperationError> { + self.read(|dbtx| dbtx.open_table(TABLE_VERSION)?.get(()).map(|o| o.map(|v| v.value())).map_err(Into::into)) + .await + } +} + +#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] +#[display("DB operation error: {_variant}")] +pub enum DatabaseOperationError { + #[display("redb error")] + #[from(forward)] + Redb(#[error(source)] redb::Error), + + #[from(ignore)] + Custom(#[error(source)] Box), +} +impl DatabaseOperationError { + pub fn custom(err: impl std::error::Error + Send + Sync + 'static) -> Self { + Self::Custom(Box::new(err)) + } } pub const TABLE_VERSION: TableDefinition<(), Bincode> = TableDefinition::new("version"); diff --git a/crates/ascend/src/server/migrations.rs b/crates/ascend/src/server/migrations.rs index 08e4351..d96e3e4 100644 --- a/crates/ascend/src/server/migrations.rs +++ b/crates/ascend/src/server/migrations.rs @@ -1,6 +1,6 @@ use super::db; +use super::db::Database; use crate::models; -use redb::Database; use redb::ReadableTable; use redb::ReadableTableMetadata; use std::collections::BTreeSet; @@ -31,13 +31,12 @@ async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { - let txn = db.begin_write()?; - { + db.write(|txn| { let mut version_table = txn.open_table(db::TABLE_VERSION)?; let is_missing_version = version_table.get(())?.is_none(); if is_missing_version { @@ -90,8 +89,10 @@ async fn init_at_current_version(db: &Database) -> Result<(), Box Result<(), Box Result<(), Box> { use super::db; - let txn = db.begin_write()?; - { + db.write(|txn| { let mut version_table = txn.open_table(db::TABLE_VERSION)?; let version = version_table.get(())?.unwrap().value().version; if version == 1 { @@ -158,9 +158,14 @@ async fn migrate_to_v2(db: &Database) -> Result<(), Box> let mut walls_table = txn.open_table(db::v2::TABLE_WALLS)?; walls_table.insert(wall_v2.uid, wall_v2)?; drop(walls_table); + + let problems_table = txn.open_table(db::v2::TABLE_PROBLEMS)?; + drop(problems_table); } - } - txn.commit()?; + + Ok(()) + }) + .await?; Ok(()) } diff --git a/crates/ascend/src/server/operations.rs b/crates/ascend/src/server/operations.rs index df4ea81..addb02d 100644 --- a/crates/ascend/src/server/operations.rs +++ b/crates/ascend/src/server/operations.rs @@ -1,17 +1,16 @@ //! Server lib module to host re-usable server operations. +use super::db::Database; use crate::models; use crate::models::HoldPosition; use crate::models::HoldRole; use crate::server::config::Config; use crate::server::db; -use redb::Database; use redb::ReadableTable; use std::collections::BTreeMap; -use std::sync::Arc; #[tracing::instrument(skip_all)] -pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc, wall_id: models::WallUid) -> Result<(), Error> { +pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Database, wall_uid: models::WallUid) -> Result<(), Error> { use moonboard_parser::mini_moonboard; let mut problems = Vec::new(); @@ -61,26 +60,22 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc Result<(), redb::Error> { - let write_txn = db.begin_write()?; - { - let mut walls_table = write_txn.open_table(db::current::TABLE_WALLS)?; - let mut problems_table = write_txn.open_table(db::current::TABLE_PROBLEMS)?; + db.write(|txn| { + let mut walls_table = txn.open_table(db::current::TABLE_WALLS)?; + let mut problems_table = txn.open_table(db::current::TABLE_PROBLEMS)?; - let mut wall = walls_table.get(wall_id)?.unwrap().value(); - wall.problems.extend(problems.iter().map(|p| p.uid)); - walls_table.insert(wall_id, wall)?; + let mut wall = walls_table.get(wall_uid)?.unwrap().value(); + wall.problems.extend(problems.iter().map(|p| p.uid)); + walls_table.insert(wall_uid, wall)?; - for problem in problems { - let key = (wall_id, problem.uid); - problems_table.insert(key, problem)?; - } + for problem in problems { + let key = (wall_uid, problem.uid); + problems_table.insert(key, problem)?; } - write_txn.commit()?; Ok(()) }) - .await??; + .await?; Ok(()) } @@ -88,6 +83,6 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc Result>, ServerFnError> { + use crate::server::db::Database; + use redb::ReadableTable; + tracing::debug!("Enter"); + + let db = expect_context::(); + + let walls = db + .read(|txn| { + let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?; + let walls: Vec = walls_table.iter()?.map(|r| r.map(|(_, v)| v.value())).collect::>()?; + Ok(walls) + }) + .await?; + + Ok(RonEncoded::new(walls)) +} + +#[server( + input = Ron, + output = Ron, + custom = RonEncoded +)] +#[tracing::instrument(skip_all, err(Debug))] pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result, ServerFnError> { + use crate::server::db::Database; + use crate::server::db::DatabaseOperationError; tracing::debug!("Enter"); #[derive(Debug, derive_more::Error, derive_more::Display)] @@ -21,17 +47,19 @@ pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result>(); + let db = expect_context::(); - let wall = tokio::task::spawn_blocking(move || -> Result { - let read_txn = db.begin_read()?; - - let walls_table = read_txn.open_table(crate::server::db::current::TABLE_WALLS)?; - let wall = walls_table.get(wall_uid)?.ok_or(Error::NotFound(wall_uid))?.value(); - - Ok(wall) - }) - .await??; + let wall = db + .read(|txn| { + let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?; + let wall = walls_table + .get(wall_uid)? + .ok_or(Error::NotFound(wall_uid)) + .map_err(DatabaseOperationError::custom)? + .value(); + Ok(wall) + }) + .await?; tracing::debug!("ok"); @@ -43,24 +71,80 @@ pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result Result>, ServerFnError> { + use crate::server::db::Database; + use crate::server::db::DatabaseOperationError; + tracing::debug!("Enter"); + + #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] + enum Error { + #[display("Wall not found: {_0:?}")] + WallNotFound(#[error(not(source))] models::WallUid), + + DatabaseOperation(DatabaseOperationError), + } + + async fn inner(wall_uid: models::WallUid) -> Result, Error> { + let db = expect_context::(); + + let problems = db + .read(|txn| { + let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?; + tracing::debug!("getting wall"); + let wall = walls_table + .get(wall_uid)? + .ok_or(Error::WallNotFound(wall_uid)) + .map_err(DatabaseOperationError::custom)? + .value(); + tracing::debug!("got wall"); + drop(walls_table); + + tracing::debug!("open problems table"); + let problems_table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?; + tracing::debug!("opened problems table"); + + let mut problems = Vec::new(); + for &problem_uid in &wall.problems { + if let Some(problem) = problems_table.get((wall_uid, problem_uid))? { + problems.push(problem.value()); + } + } + Ok(problems) + }) + .await?; + + Ok(problems) + } + + let problems = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?; + + tracing::debug!("ok"); + + Ok(RonEncoded::new(problems)) +} + +#[server( + input = Ron, + output = Ron, + custom = RonEncoded +)] +#[tracing::instrument(skip_all, err(Debug))] pub(crate) async fn get_problem( wall_uid: models::WallUid, problem_uid: models::ProblemUid, ) -> Result>, ServerFnError> { + use crate::server::db::Database; tracing::debug!("Enter"); - let db = expect_context::>(); - - let problem = tokio::task::spawn_blocking(move || -> Result, ServerFnError> { - let read_txn = db.begin_read()?; - - let table = read_txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?; - let problem = table.get((wall_uid, problem_uid))?.map(|guard| guard.value()); - - Ok(problem) - }) - .await??; + let db = expect_context::(); + let problem = db + .read(|txn| { + let table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?; + let problem = table.get((wall_uid, problem_uid))?.map(|guard| guard.value()); + Ok(problem) + }) + .await?; Ok(RonEncoded::new(problem)) } diff --git a/todo.md b/todo.md index fd819d7..b048e86 100644 --- a/todo.md +++ b/todo.md @@ -9,3 +9,4 @@ - implement favorite routes feature - use wall id in URL. - decide on routes vs problems terminology +- decide on holds vs wall-edit terminology