This commit is contained in:
2025-02-06 00:29:54 +01:00
parent 503eeef20e
commit 9451980b25
12 changed files with 260 additions and 141 deletions

1
Cargo.lock generated
View File

@@ -128,6 +128,7 @@ dependencies = [
"confik", "confik",
"console_error_panic_hook", "console_error_panic_hook",
"derive_more", "derive_more",
"error_reporter",
"http 1.2.0", "http 1.2.0",
"leptos", "leptos",
"leptos_axum", "leptos_axum",

View File

@@ -38,7 +38,7 @@ tower-http = { version = "0.5", features = ["fs"], optional = true }
tracing = { version = "0.1" } tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-subscriber-wasm = "0.1.0" 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" wasm-bindgen = "=0.2.99"
web-sys = { version = "0.3.76", features = ["File", "FileList"] } web-sys = { version = "0.3.76", features = ["File", "FileList"] }
xdg = { version = "2.5", optional = true } xdg = { version = "2.5", optional = true }
@@ -47,6 +47,7 @@ redb = { version = "2.4", optional = true }
bincode = { version = "1.3", optional = true } bincode = { version = "1.3", optional = true }
serde_json = { version = "1" } serde_json = { version = "1" }
codee = { version = "0.3" } codee = { version = "0.3" }
error_reporter = { version = "1" }
[dev-dependencies.serde_json] [dev-dependencies.serde_json]
version = "1" version = "1"

View File

@@ -1,11 +1,7 @@
use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::models;
use crate::pages; use crate::pages;
use leptos::prelude::*; use leptos::prelude::*;
use leptos_router::components::*; use leptos_router::components::*;
use leptos_router::path; use leptos_router::path;
use std::sync::Arc;
pub fn shell(options: LeptosOptions) -> impl IntoView { pub fn shell(options: LeptosOptions) -> impl IntoView {
use leptos_meta::MetaTags; use leptos_meta::MetaTags;
@@ -61,7 +57,7 @@ pub fn Home() -> impl leptos::IntoView {
let action = Action::new(|()| async move { let action = Action::new(|()| async move {
tracing::debug!("running action"); tracing::debug!("running action");
let walls = get_walls() let walls = crate::server_functions::get_walls()
.await .await
.inspect_err(|e| { .inspect_err(|e| {
dbg!(e); dbg!(e);
@@ -85,33 +81,3 @@ pub fn Home() -> impl leptos::IntoView {
leptos::view! {} leptos::view! {}
} }
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(skip_all, err)]
async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError> {
use redb::ReadableTable;
tracing::debug!("get walls");
let db = expect_context::<Arc<redb::Database>>();
tracing::debug!("got db from context");
let walls = tokio::task::spawn_blocking(move || -> Result<Vec<models::Wall>, 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<models::Wall> = walls_table.iter()?.map(|r| r.map(|(_, v)| v.value())).collect::<Result<_, _>>()?;
Ok(walls)
})
.await??;
Ok(RonEncoded::new(walls))
}

View File

@@ -32,7 +32,7 @@ pub mod v2 {
pub problems: BTreeSet<ProblemUid>, pub problems: BTreeSet<ProblemUid>,
} }
#[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); pub struct WallUid(pub uuid::Uuid);
impl WallUid { impl WallUid {
pub fn new() -> Self { pub fn new() -> Self {
@@ -50,7 +50,7 @@ pub mod v2 {
pub date_added: chrono::DateTime<chrono::Utc>, pub date_added: chrono::DateTime<chrono::Utc>,
} }
#[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); pub struct ProblemUid(pub uuid::Uuid);
impl ProblemUid { impl ProblemUid {
pub fn new() -> Self { pub fn new() -> Self {

View File

@@ -15,6 +15,7 @@ use std::ops::Deref;
#[derive(Params, PartialEq, Clone)] #[derive(Params, PartialEq, Clone)]
struct RouteParams { struct RouteParams {
// Is never None
wall_uid: Option<models::WallUid>, wall_uid: Option<models::WallUid>,
} }
@@ -24,12 +25,13 @@ pub fn Routes() -> impl leptos::IntoView {
tracing::debug!("Enter"); tracing::debug!("Enter");
let params = leptos_router::hooks::use_params::<RouteParams>(); let params = leptos_router::hooks::use_params::<RouteParams>();
let wall_uid = Signal::derive(move || params.get().map(|p| p.wall_uid.expect("wall_uid param is never None")));
let problems = Resource::<Option<models::Wall>, Ron>::new_with_options( let problems = Resource::<Option<Vec<models::Problem>>, Ron>::new_with_options(
move || params.get().map(|p| p.wall_uid), move || wall_uid.get(),
move |wall_uid: Result<Option<WallUid>, _>| async move { move |wall_uid: Result<WallUid, _>| async move {
if let Ok(Some(wall_uid)) = wall_uid { if let Ok(wall_uid) = wall_uid {
let wall = crate::server_functions::get_wall(wall_uid).await.unwrap().into_inner(); let wall = crate::server_functions::get_problems_for_wall(wall_uid).await.unwrap().into_inner();
Some(wall) Some(wall)
} else { } else {
None None
@@ -55,22 +57,43 @@ pub fn Routes() -> impl leptos::IntoView {
<StyledHeader items=header_items /> <StyledHeader items=header_items />
<div class="container mx-auto mt-2"> <div class="container mx-auto mt-2">
<Await future=load let:data> {wall_uid.get().map(|wall_uid| view! {<Import wall_uid/>})}
<Ready data=data.deref().to_owned() />
</Await> <Suspense fallback=|| view! {"loading"}>
<For
each=move || problems.get().flatten().into_iter().flat_map(|v| v.into_iter())
key=|problem| problem.uid
children=move |problem: models::Problem| {
view! {
<Problem problem/>
}
}
/>
</Suspense>
</div> </div>
</div> </div>
} }
} }
#[component] #[component]
fn Ready(data: InitialData) -> impl leptos::IntoView { #[tracing::instrument(skip_all)]
fn Problem(problem: models::Problem) -> impl IntoView {
tracing::debug!("Enter");
view! {
<p>"problem"</p>
}
}
#[component]
#[tracing::instrument(skip_all)]
fn Import(wall_uid: WallUid) -> impl IntoView {
tracing::debug!("ready"); tracing::debug!("ready");
let import_from_mini_moonboard = Action::from(ServerAction::<ImportFromMiniMoonboard>::new()); let import_from_mini_moonboard = Action::from(ServerAction::<ImportFromMiniMoonboard>::new());
let onclick = move |_mouse_event| { let onclick = move |_mouse_event| {
import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard {}); import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard { wall_uid });
}; };
view! { view! {
@@ -81,17 +104,17 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
#[server(name = ImportFromMiniMoonboard)] #[server(name = ImportFromMiniMoonboard)]
#[tracing::instrument] #[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::config::Config;
use crate::server::db::Database;
todo!() tracing::info!("Importing mini moonboard problems");
// tracing::info!("Importing mini moonboard problems");
// let config = expect_context::<Config>(); let config = expect_context::<Config>();
// let state = expect_context::<State>(); let db = expect_context::<Database>();
// 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 // TODO: Return information about what was done
// Ok(()) Ok(())
} }

View File

@@ -23,11 +23,12 @@ pub fn Wall() -> impl leptos::IntoView {
tracing::debug!("Enter"); tracing::debug!("Enter");
let params = leptos_router::hooks::use_params::<RouteParams>(); let params = leptos_router::hooks::use_params::<RouteParams>();
let wall_uid = move || params.get().map(|p| p.wall_uid.expect("wall_uid param is never None"));
let wall = Resource::<Option<models::Wall>, Ron>::new_with_options( let wall = Resource::<Option<models::Wall>, Ron>::new_with_options(
move || params.get().map(|p| p.wall_uid), move || wall_uid(),
move |wall_uid: Result<Option<WallUid>, _>| async move { move |wall_uid: Result<WallUid, _>| async move {
if let Ok(Some(wall_uid)) = wall_uid { if let Ok(wall_uid) = wall_uid {
let wall = crate::server_functions::get_wall(wall_uid).await.unwrap().into_inner(); let wall = crate::server_functions::get_wall(wall_uid).await.unwrap().into_inner();
Some(wall) Some(wall)
} else { } else {
@@ -37,7 +38,7 @@ pub fn Wall() -> impl leptos::IntoView {
false, false,
); );
let header_items = HeaderItems { let header_items = move || HeaderItems {
left: vec![], left: vec![],
middle: vec![HeaderItem { middle: vec![HeaderItem {
text: "ASCEND".to_string(), text: "ASCEND".to_string(),
@@ -46,18 +47,18 @@ pub fn Wall() -> impl leptos::IntoView {
right: vec![ right: vec![
HeaderItem { HeaderItem {
text: "Routes".to_string(), text: "Routes".to_string(),
link: Some("/wall/routes".to_string()), link: wall_uid().map(|uid| format!("/wall/{uid}/routes")).ok(),
}, },
HeaderItem { HeaderItem {
text: "Holds".to_string(), text: "Holds".to_string(),
link: Some("/wall/edit".to_string()), link: wall_uid().map(|uid| format!("/wall/{uid}/edit")).ok(),
}, },
], ],
}; };
leptos::view! { leptos::view! {
<div class="min-w-screen min-h-screen bg-slate-900"> <div class="min-w-screen min-h-screen bg-slate-900">
<StyledHeader items=header_items /> <StyledHeader items=header_items() />
<div class="m-2"> <div class="m-2">
<Suspense fallback=move || { <Suspense fallback=move || {
@@ -76,6 +77,7 @@ pub fn Wall() -> impl leptos::IntoView {
} }
#[component] #[component]
#[tracing::instrument(skip_all)]
fn Ready(wall: models::Wall) -> impl leptos::IntoView { fn Ready(wall: models::Wall) -> impl leptos::IntoView {
tracing::debug!("ready"); tracing::debug!("ready");

View File

@@ -5,7 +5,6 @@ use config::Config;
use confik::Configuration; use confik::Configuration;
use confik::EnvSource; use confik::EnvSource;
use std::path::Path; use std::path::Path;
use std::sync::Arc;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use type_toppings::ResultExt; use type_toppings::ResultExt;
@@ -51,7 +50,7 @@ async fn serve(cli: Cli) -> Result<(), Error> {
use leptos_axum::generate_route_list; use leptos_axum::generate_route_list;
tracing::debug!("Creating DB"); tracing::debug!("Creating DB");
let db = Arc::new(db::create()?); let db = db::Database::create()?;
migrations::run_migrations(&db).await.map_err(Error::Migration)?; migrations::run_migrations(&db).await.map_err(Error::Migration)?;
@@ -73,7 +72,7 @@ async fn serve(cli: Cli) -> Result<(), Error> {
&leptos_options, &leptos_options,
routes, routes,
move || { move || {
leptos::prelude::provide_context::<Arc<redb::Database>>(Arc::clone(&db)); leptos::prelude::provide_context::<db::Database>(db.clone());
leptos::prelude::provide_context::<Config>(config.clone()) leptos::prelude::provide_context::<Config>(config.clone())
}, },
{ {

View File

@@ -1,16 +1,24 @@
use bincode::Bincode; use bincode::Bincode;
use redb::Database; use redb::ReadTransaction;
use redb::TableDefinition; use redb::TableDefinition;
use redb::WriteTransaction;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
mod bincode; mod bincode;
pub const DB_FILE: &str = "datastore/private/ascend.redb"; const DB_FILE: &str = "datastore/private/ascend.redb";
#[tracing::instrument(skip_all, err)] #[derive(Debug, Clone)]
pub fn create() -> Result<Database, redb::Error> { pub struct Database {
db: Arc<redb::Database>,
}
impl Database {
#[tracing::instrument(skip_all, err)]
pub fn create() -> Result<Database, redb::Error> {
let file = PathBuf::from(DB_FILE); let file = PathBuf::from(DB_FILE);
// Create parent dirs // Create parent dirs
@@ -18,15 +26,49 @@ pub fn create() -> Result<Database, redb::Error> {
std::fs::create_dir_all(parent_dir)?; std::fs::create_dir_all(parent_dir)?;
} }
let db = Database::create(file)?; let db = redb::Database::create(file)?;
Ok(db) Ok(Self { db: Arc::new(db) })
}
#[tracing::instrument(skip_all, err)]
pub async fn read<T>(&self, f: impl FnOnce(&'_ ReadTransaction) -> Result<T, DatabaseOperationError>) -> Result<T, DatabaseOperationError> {
tokio::task::block_in_place(|| {
let dbtx = self.db.begin_read()?;
f(&dbtx)
})
}
#[tracing::instrument(skip_all, err)]
pub async fn write<T>(&self, f: impl FnOnce(&'_ WriteTransaction) -> Result<T, DatabaseOperationError>) -> Result<T, DatabaseOperationError> {
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<Option<Version>, DatabaseOperationError> {
self.read(|dbtx| dbtx.open_table(TABLE_VERSION)?.get(()).map(|o| o.map(|v| v.value())).map_err(Into::into))
.await
}
} }
#[tracing::instrument(skip_all)] #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
pub fn get_version(db: &Database) -> Result<Option<Version>, redb::Error> { #[display("DB operation error: {_variant}")]
let txn = db.begin_read()?; pub enum DatabaseOperationError {
let version = txn.open_table(TABLE_VERSION)?.get(())?.map(|v| v.value()); #[display("redb error")]
Ok(version) #[from(forward)]
Redb(#[error(source)] redb::Error),
#[from(ignore)]
Custom(#[error(source)] Box<dyn std::error::Error + Send + Sync + 'static>),
}
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<Version>> = TableDefinition::new("version"); pub const TABLE_VERSION: TableDefinition<(), Bincode<Version>> = TableDefinition::new("version");

View File

@@ -1,6 +1,6 @@
use super::db; use super::db;
use super::db::Database;
use crate::models; use crate::models;
use redb::Database;
use redb::ReadableTable; use redb::ReadableTable;
use redb::ReadableTableMetadata; use redb::ReadableTableMetadata;
use std::collections::BTreeSet; use std::collections::BTreeSet;
@@ -31,13 +31,12 @@ async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box<dyn std::erro
ron::from_str(&content)? ron::from_str(&content)?
}; };
let write_txn = db.begin_write()?; db.write(|txn| {
{ let mut version_table = txn.open_table(db::TABLE_VERSION)?;
let mut version_table = write_txn.open_table(db::TABLE_VERSION)?;
assert!(version_table.is_empty()?); assert!(version_table.is_empty()?);
version_table.insert((), db::Version { version: 1 })?; version_table.insert((), db::Version { version: 1 })?;
let mut root_table = write_txn.open_table(db::v1::TABLE_ROOT)?; let mut root_table = txn.open_table(db::v1::TABLE_ROOT)?;
assert!(root_table.is_empty()?); assert!(root_table.is_empty()?);
let persistent_state = models::v1::PersistentState { let persistent_state = models::v1::PersistentState {
@@ -47,8 +46,9 @@ async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box<dyn std::erro
}; };
root_table.insert((), persistent_state)?; root_table.insert((), persistent_state)?;
} Ok(())
write_txn.commit()?; })
.await?;
tracing::info!("Removing ron state"); tracing::info!("Removing ron state");
tokio::fs::remove_file(ron_state_file_path).await?; tokio::fs::remove_file(ron_state_file_path).await?;
@@ -60,8 +60,7 @@ async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box<dyn std::erro
// TODO: Move out, is not really a migration // TODO: Move out, is not really a migration
#[tracing::instrument(skip_all, err)] #[tracing::instrument(skip_all, err)]
async fn init_at_current_version(db: &Database) -> Result<(), Box<dyn std::error::Error>> { async fn init_at_current_version(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
let txn = db.begin_write()?; db.write(|txn| {
{
let mut version_table = txn.open_table(db::TABLE_VERSION)?; let mut version_table = txn.open_table(db::TABLE_VERSION)?;
let is_missing_version = version_table.get(())?.is_none(); let is_missing_version = version_table.get(())?.is_none();
if is_missing_version { if is_missing_version {
@@ -90,8 +89,10 @@ async fn init_at_current_version(db: &Database) -> Result<(), Box<dyn std::error
assert!(table.is_empty()?); assert!(table.is_empty()?);
} }
} }
}
txn.commit()?; Ok(())
})
.await?;
Ok(()) Ok(())
} }
@@ -100,8 +101,7 @@ async fn init_at_current_version(db: &Database) -> Result<(), Box<dyn std::error
async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>> { async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
use super::db; use super::db;
let txn = db.begin_write()?; db.write(|txn| {
{
let mut version_table = txn.open_table(db::TABLE_VERSION)?; let mut version_table = txn.open_table(db::TABLE_VERSION)?;
let version = version_table.get(())?.unwrap().value().version; let version = version_table.get(())?.unwrap().value().version;
if version == 1 { if version == 1 {
@@ -158,9 +158,14 @@ async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>>
let mut walls_table = txn.open_table(db::v2::TABLE_WALLS)?; let mut walls_table = txn.open_table(db::v2::TABLE_WALLS)?;
walls_table.insert(wall_v2.uid, wall_v2)?; walls_table.insert(wall_v2.uid, wall_v2)?;
drop(walls_table); drop(walls_table);
let problems_table = txn.open_table(db::v2::TABLE_PROBLEMS)?;
drop(problems_table);
} }
}
txn.commit()?; Ok(())
})
.await?;
Ok(()) Ok(())
} }

View File

@@ -1,17 +1,16 @@
//! Server lib module to host re-usable server operations. //! Server lib module to host re-usable server operations.
use super::db::Database;
use crate::models; use crate::models;
use crate::models::HoldPosition; use crate::models::HoldPosition;
use crate::models::HoldRole; use crate::models::HoldRole;
use crate::server::config::Config; use crate::server::config::Config;
use crate::server::db; use crate::server::db;
use redb::Database;
use redb::ReadableTable; use redb::ReadableTable;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::Arc;
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc<Database>, 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; use moonboard_parser::mini_moonboard;
let mut problems = Vec::new(); let mut problems = Vec::new();
@@ -61,26 +60,22 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc<Data
problems.push(problem); problems.push(problem);
} }
tokio::task::spawn_blocking(move || -> Result<(), redb::Error> { db.write(|txn| {
let write_txn = db.begin_write()?; let mut walls_table = txn.open_table(db::current::TABLE_WALLS)?;
{ let mut problems_table = txn.open_table(db::current::TABLE_PROBLEMS)?;
let mut walls_table = write_txn.open_table(db::current::TABLE_WALLS)?;
let mut problems_table = write_txn.open_table(db::current::TABLE_PROBLEMS)?;
let mut wall = walls_table.get(wall_id)?.unwrap().value(); let mut wall = walls_table.get(wall_uid)?.unwrap().value();
wall.problems.extend(problems.iter().map(|p| p.uid)); wall.problems.extend(problems.iter().map(|p| p.uid));
walls_table.insert(wall_id, wall)?; walls_table.insert(wall_uid, wall)?;
for problem in problems { for problem in problems {
let key = (wall_id, problem.uid); let key = (wall_uid, problem.uid);
problems_table.insert(key, problem)?; problems_table.insert(key, problem)?;
} }
}
write_txn.commit()?;
Ok(()) Ok(())
}) })
.await??; .await?;
Ok(()) Ok(())
} }
@@ -88,6 +83,6 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc<Data
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
pub enum Error { pub enum Error {
Parser(moonboard_parser::Error), Parser(moonboard_parser::Error),
Redb(redb::Error),
Tokio(tokio::task::JoinError), Tokio(tokio::task::JoinError),
DbOperation(crate::server::db::DatabaseOperationError),
} }

View File

@@ -4,15 +4,41 @@ use crate::models;
use leptos::prelude::expect_context; use leptos::prelude::expect_context;
use leptos::server; use leptos::server;
use server_fn::ServerFnError; use server_fn::ServerFnError;
use std::sync::Arc; use server_fn::error::ServerFnErrorErr;
#[server( #[server(
input = Ron, input = Ron,
output = Ron, output = Ron,
custom = RonEncoded custom = RonEncoded
)] )]
#[tracing::instrument(skip_all, err)] #[tracing::instrument(skip_all, err(Debug))]
pub(crate) async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError> {
use crate::server::db::Database;
use redb::ReadableTable;
tracing::debug!("Enter");
let db = expect_context::<Database>();
let walls = db
.read(|txn| {
let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?;
let walls: Vec<models::Wall> = walls_table.iter()?.map(|r| r.map(|(_, v)| v.value())).collect::<Result<_, _>>()?;
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<RonEncoded<models::Wall>, ServerFnError> { pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<models::Wall>, ServerFnError> {
use crate::server::db::Database;
use crate::server::db::DatabaseOperationError;
tracing::debug!("Enter"); tracing::debug!("Enter");
#[derive(Debug, derive_more::Error, derive_more::Display)] #[derive(Debug, derive_more::Error, derive_more::Display)]
@@ -21,17 +47,19 @@ pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<mod
NotFound(#[error(not(source))] models::WallUid), NotFound(#[error(not(source))] models::WallUid),
} }
let db = expect_context::<Arc<redb::Database>>(); let db = expect_context::<Database>();
let wall = tokio::task::spawn_blocking(move || -> Result<models::Wall, ServerFnError> {
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();
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) Ok(wall)
}) })
.await??; .await?;
tracing::debug!("ok"); tracing::debug!("ok");
@@ -43,24 +71,80 @@ pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<mod
output = Ron, output = Ron,
custom = RonEncoded custom = RonEncoded
)] )]
#[tracing::instrument(skip_all, err)] #[tracing::instrument(err(Debug))]
pub(crate) async fn get_problems_for_wall(wall_uid: models::WallUid) -> Result<RonEncoded<Vec<models::Problem>>, 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<Vec<models::Problem>, Error> {
let db = expect_context::<Database>();
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( pub(crate) async fn get_problem(
wall_uid: models::WallUid, wall_uid: models::WallUid,
problem_uid: models::ProblemUid, problem_uid: models::ProblemUid,
) -> Result<RonEncoded<Option<models::Problem>>, ServerFnError> { ) -> Result<RonEncoded<Option<models::Problem>>, ServerFnError> {
use crate::server::db::Database;
tracing::debug!("Enter"); tracing::debug!("Enter");
let db = expect_context::<Arc<redb::Database>>(); let db = expect_context::<Database>();
let problem = db
let problem = tokio::task::spawn_blocking(move || -> Result<Option<models::Problem>, ServerFnError> { .read(|txn| {
let read_txn = db.begin_read()?; let table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
let table = read_txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
let problem = table.get((wall_uid, problem_uid))?.map(|guard| guard.value()); let problem = table.get((wall_uid, problem_uid))?.map(|guard| guard.value());
Ok(problem) Ok(problem)
}) })
.await??; .await?;
Ok(RonEncoded::new(problem)) Ok(RonEncoded::new(problem))
} }

View File

@@ -9,3 +9,4 @@
- implement favorite routes feature - implement favorite routes feature
- use wall id in URL. - use wall id in URL.
- decide on routes vs problems terminology - decide on routes vs problems terminology
- decide on holds vs wall-edit terminology