ssr mode works properly now

Orual 17cf62c2 ec7d916e

+590 -424
+22 -31
crates/weaver-app/src/blobcache.rs
··· 42 42 } 43 43 AtIdentifier::Handle(handle) => self.client.pds_for_handle(&handle).await?, 44 44 }; 45 - // let blob = if let Ok(blob_stream) = self 46 - // .client 47 - // .xrpc(pds_url) 48 - // .send( 49 - // &GetBlob::new() 50 - // .cid(cid.clone()) 51 - // .did(repo_did.clone()) 52 - // .build(), 53 - // ) 54 - // .await 55 - // { 56 - // blob_stream.buffer().clone() 57 - // } else { 58 - // reqwest::get(format!( 59 - // "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@jpeg", 60 - // repo_did, cid 61 - // )) 62 - // .await? 63 - // .bytes() 64 - // .await? 65 - // .clone() 66 - // }; 67 - // 68 - let blob = reqwest::get(format!( 69 - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@jpeg", 70 - repo_did, cid 71 - )) 72 - .await? 73 - .bytes() 74 - .await? 75 - .clone(); 45 + let blob = if let Ok(blob_stream) = self 46 + .client 47 + .xrpc(pds_url) 48 + .send( 49 + &GetBlob::new() 50 + .cid(cid.clone()) 51 + .did(repo_did.clone()) 52 + .build(), 53 + ) 54 + .await 55 + { 56 + blob_stream.buffer().clone() 57 + } else { 58 + reqwest::get(format!( 59 + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@jpeg", 60 + repo_did, cid 61 + )) 62 + .await? 63 + .bytes() 64 + .await? 65 + .clone() 66 + }; 76 67 77 68 self.cache.insert(cid.clone(), blob); 78 69 if let Some(name) = name {
+32 -25
crates/weaver-app/src/components/entry.rs
··· 24 24 use std::sync::Arc; 25 25 use weaver_api::sh_weaver::notebook::{BookEntryView, EntryView, entry}; 26 26 27 + // #[component] 28 + // pub fn EntryPage( 29 + // ident: ReadSignal<AtIdentifier<'static>>, 30 + // book_title: ReadSignal<SmolStr>, 31 + // title: ReadSignal<SmolStr>, 32 + // ) -> Element { 33 + // rsx! { 34 + // {std::iter::once(rsx! {Entry {ident, book_title, title}})} 35 + // } 36 + // } 37 + 27 38 #[component] 28 39 pub fn EntryPage( 29 40 ident: ReadSignal<AtIdentifier<'static>>, 30 41 book_title: ReadSignal<SmolStr>, 31 42 title: ReadSignal<SmolStr>, 32 43 ) -> Element { 33 - tracing::debug!( 34 - "EntryPage component rendering for ident: {:?}, book: {}, title: {}", 35 - ident(), 36 - book_title(), 37 - title() 38 - ); 39 - rsx! { 40 - {std::iter::once(rsx! {Entry {ident, book_title, title}})} 41 - } 42 - } 44 + // Use feature-gated hook for SSR support 45 + let (entry_res, entry) = crate::data::use_entry_data(ident, book_title, title); 46 + let route = use_route::<Route>(); 47 + let mut last_route = use_signal(|| route.clone()); 43 48 44 - #[component] 45 - pub fn Entry( 46 - ident: ReadSignal<AtIdentifier<'static>>, 47 - book_title: ReadSignal<SmolStr>, 48 - title: ReadSignal<SmolStr>, 49 - ) -> Element { 50 - tracing::debug!( 51 - "Entry component rendering for ident: {:?}, book: {}, title: {}", 52 - ident(), 53 - book_title(), 54 - title() 55 - ); 56 - // Use feature-gated hook for SSR support 57 - let entry = crate::data::use_entry_data(ident, book_title, title); 49 + #[cfg(all( 50 + target_family = "wasm", 51 + target_os = "unknown", 52 + not(feature = "fullstack-server") 53 + ))] 58 54 let fetcher = use_context::<crate::fetch::Fetcher>(); 59 - tracing::debug!("Entry component got entry data"); 55 + 56 + // Suspend SSR until entry loads 57 + #[cfg(feature = "fullstack-server")] 58 + let mut entry_res = entry_res?; 59 + 60 + #[cfg(feature = "fullstack-server")] 61 + use_effect(use_reactive!(|route| { 62 + if route != last_route() { 63 + entry_res.restart(); 64 + last_route.set(route.clone()); 65 + } 66 + })); 60 67 61 68 // Handle blob caching when entry data is available 62 69 match &*entry.read() {
+9 -2
crates/weaver-app/src/components/identity.rs
··· 26 26 ident() 27 27 ); 28 28 use crate::components::ProfileDisplay; 29 - let notebooks = data::use_notebooks_for_did(ident); 30 - let profile = crate::data::use_profile_data(ident); 29 + let (notebooks_result, notebooks) = data::use_notebooks_for_did(ident); 30 + let (profile_result, profile) = crate::data::use_profile_data(ident); 31 31 tracing::debug!("RepositoryIndex got profile and notebooks"); 32 + 33 + #[cfg(feature = "fullstack-server")] 34 + notebooks_result?; 35 + 36 + #[cfg(feature = "fullstack-server")] 37 + profile_result?; 38 + 32 39 rsx! { 33 40 document::Stylesheet { href: NOTEBOOK_CARD_CSS } 34 41
+1 -1
crates/weaver-app/src/components/mod.rs
··· 7 7 8 8 mod entry; 9 9 #[allow(unused_imports)] 10 - pub use entry::{Entry, EntryCard, EntryMarkdown, EntryPage}; 10 + pub use entry::{EntryCard, EntryMarkdown, EntryPage}; 11 11 12 12 pub mod identity; 13 13 #[allow(unused_imports)]
+215 -86
crates/weaver-app/src/data.rs
··· 36 36 ident: ReadSignal<AtIdentifier<'static>>, 37 37 book_title: ReadSignal<SmolStr>, 38 38 title: ReadSignal<SmolStr>, 39 - ) -> Memo<Option<(BookEntryView<'static>, Entry<'static>)>> { 39 + ) -> ( 40 + Result<Resource<Option<(serde_json::Value, serde_json::Value)>>, RenderError>, 41 + Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, 42 + ) { 40 43 let fetcher = use_context::<crate::fetch::Fetcher>(); 41 44 let fetcher = fetcher.clone(); 42 - let res = use_server_future(move || { 45 + let res = use_server_future(use_reactive!(|(ident, book_title, title)| { 43 46 let fetcher = fetcher.clone(); 44 47 async move { 45 48 if let Some(entry) = fetcher ··· 51 54 let (_book_entry_view, entry_record) = (&entry.0, &entry.1); 52 55 if let Some(embeds) = &entry_record.embeds { 53 56 if let Some(images) = &embeds.images { 54 - let ident_val = ident(); 57 + let ident_val = ident.clone(); 55 58 let images = images.clone(); 56 59 for image in &images.images { 57 60 use jacquard::smol_str::ToSmolStr; ··· 75 78 None 76 79 } 77 80 } 78 - }); 79 - use_memo(use_reactive!(|res| { 80 - let res = res.ok()?; 81 + })); 82 + let memo = use_memo(use_reactive!(|res| { 83 + let res = res.as_ref().ok()?; 81 84 if let Some(Some((ev, e))) = &*res.read() { 82 85 use jacquard::from_json_value; 83 86 ··· 88 91 } else { 89 92 None 90 93 } 91 - })) 94 + })); 95 + (res, memo) 92 96 } 93 97 /// Fetches entry data client-side only (no SSR). 94 98 #[cfg(not(feature = "fullstack-server"))] ··· 96 100 ident: ReadSignal<AtIdentifier<'static>>, 97 101 book_title: ReadSignal<SmolStr>, 98 102 title: ReadSignal<SmolStr>, 99 - ) -> Memo<Option<(BookEntryView<'static>, Entry<'static>)>> { 103 + ) -> ( 104 + Resource<Option<(serde_json::Value, serde_json::Value)>>, 105 + Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, 106 + ) { 100 107 let fetcher = use_context::<crate::fetch::Fetcher>(); 101 108 let fetcher = fetcher.clone(); 102 109 let res = use_resource(move || { ··· 136 143 } 137 144 } 138 145 }); 139 - use_memo(move || { 146 + let memo = use_memo(move || { 140 147 if let Some(Some((ev, e))) = &*res.read() { 141 148 use jacquard::from_json_value; 142 149 ··· 147 154 } else { 148 155 None 149 156 } 157 + }); 158 + (res, memo) 159 + } 160 + 161 + #[cfg(feature = "fullstack-server")] 162 + pub fn use_get_handle(did: Did<'static>) -> Memo<AtIdentifier<'static>> { 163 + let ident = use_signal(use_reactive!(|did| AtIdentifier::Did(did.clone()))); 164 + let old_ident = ident.read().clone(); 165 + let fetcher = use_context::<crate::fetch::Fetcher>(); 166 + let fetcher = fetcher.clone(); 167 + let res = use_resource(move || { 168 + let client = fetcher.get_client(); 169 + let old_ident = old_ident.clone(); 170 + async move { 171 + client 172 + .resolve_ident_owned(&*ident.read()) 173 + .await 174 + .map(|doc| { 175 + doc.handles() 176 + .first() 177 + .map(|h| AtIdentifier::Handle(h.clone()).into_static()) 178 + }) 179 + .ok() 180 + .flatten() 181 + .unwrap_or(old_ident) 182 + } 183 + }); 184 + use_memo(move || { 185 + if let Some(value) = &*res.read() { 186 + value.clone() 187 + } else { 188 + ident.read().clone() 189 + } 150 190 }) 151 191 } 152 192 153 - pub fn use_get_handle(did: Did<'static>) -> AtIdentifier<'static> { 193 + #[cfg(not(feature = "fullstack-server"))] 194 + pub fn use_get_handle(did: Did<'static>) -> Memo<AtIdentifier<'static>> { 154 195 let ident = use_signal(use_reactive!(|did| AtIdentifier::Did(did.clone()))); 155 - use_handle(ident.into()).read().clone() 196 + let old_ident = ident.read().clone(); 197 + let fetcher = use_context::<crate::fetch::Fetcher>(); 198 + let fetcher = fetcher.clone(); 199 + let res = use_resource(move || { 200 + let client = fetcher.get_client(); 201 + let old_ident = old_ident.clone(); 202 + async move { 203 + client 204 + .resolve_ident_owned(&*ident.read()) 205 + .await 206 + .map(|doc| { 207 + doc.handles() 208 + .first() 209 + .map(|h| AtIdentifier::Handle(h.clone()).into_static()) 210 + }) 211 + .ok() 212 + .flatten() 213 + .unwrap_or(old_ident) 214 + } 215 + }); 216 + use_memo(move || { 217 + if let Some(value) = &*res.read() { 218 + value.clone() 219 + } else { 220 + ident.read().clone() 221 + } 222 + }) 156 223 } 157 224 158 225 #[cfg(feature = "fullstack-server")] 159 226 pub fn use_load_handle( 160 227 ident: Option<AtIdentifier<'static>>, 161 - ) -> ReadSignal<Option<AtIdentifier<'static>>> { 228 + ) -> ( 229 + Result<Resource<Option<SmolStr>>, RenderError>, 230 + Memo<Option<AtIdentifier<'static>>>, 231 + ) { 162 232 let ident = use_signal(use_reactive!(|ident| ident.clone())); 163 233 let fetcher = use_context::<crate::fetch::Fetcher>(); 164 234 let fetcher = fetcher.clone(); 165 - let res = use_server_future(move || { 235 + let res = use_server_future(use_reactive!(|ident| { 166 236 let client = fetcher.get_client(); 167 237 async move { 168 238 if let Some(ident) = &*ident.read() { ··· 177 247 None 178 248 } 179 249 } 180 - }); 250 + })); 181 251 182 - use_memo(use_reactive!(|res| { 252 + let memo = use_memo(use_reactive!(|res| { 183 253 if let Ok(res) = res { 184 254 if let Some(value) = &*res.read() { 185 255 if let Some(handle) = value { ··· 193 263 } else { 194 264 ident.read().clone() 195 265 } 196 - })) 197 - .into() 266 + })); 267 + 268 + (res, memo) 198 269 } 199 270 200 271 #[cfg(not(feature = "fullstack-server"))] 201 272 pub fn use_load_handle( 202 273 ident: Option<AtIdentifier<'static>>, 203 - ) -> ReadSignal<Option<AtIdentifier<'static>>> { 274 + ) -> ( 275 + Resource<Option<AtIdentifier<'static>>>, 276 + Memo<Option<AtIdentifier<'static>>>, 277 + ) { 204 278 let ident = use_signal(use_reactive!(|ident| ident.clone())); 205 279 let fetcher = use_context::<crate::fetch::Fetcher>(); 206 280 let fetcher = fetcher.clone(); ··· 223 297 } 224 298 }); 225 299 226 - if let Ok(ident) = res.suspend() { 227 - ident.into() 228 - } else { 229 - ident.into() 230 - } 300 + let memo = use_memo(move || { 301 + if let Some(value) = &*res.read() { 302 + value.clone() 303 + } else { 304 + ident.read().clone() 305 + } 306 + }); 307 + 308 + (res, memo) 231 309 } 232 310 #[cfg(not(feature = "fullstack-server"))] 233 - pub fn use_handle(ident: ReadSignal<AtIdentifier<'static>>) -> ReadSignal<AtIdentifier<'static>> { 311 + pub fn use_handle( 312 + ident: ReadSignal<AtIdentifier<'static>>, 313 + ) -> (Resource<AtIdentifier<'static>>, Memo<AtIdentifier<'static>>) { 234 314 let old_ident = ident.read().clone(); 235 315 let fetcher = use_context::<crate::fetch::Fetcher>(); 236 316 let fetcher = fetcher.clone(); ··· 251 331 .unwrap_or(old_ident) 252 332 } 253 333 }); 254 - if let Ok(ident) = res.suspend() { 255 - ident.into() 256 - } else { 257 - ident 258 - } 334 + 335 + let memo = use_memo(move || { 336 + if let Some(value) = &*res.read() { 337 + value.clone() 338 + } else { 339 + ident.read().clone() 340 + } 341 + }); 342 + 343 + (res, memo) 259 344 } 260 345 #[cfg(feature = "fullstack-server")] 261 - pub fn use_handle(ident: ReadSignal<AtIdentifier<'static>>) -> ReadSignal<AtIdentifier<'static>> { 346 + pub fn use_handle( 347 + ident: ReadSignal<AtIdentifier<'static>>, 348 + ) -> ( 349 + Result<Resource<SmolStr>, RenderError>, 350 + Memo<AtIdentifier<'static>>, 351 + ) { 262 352 let old_ident = ident.read().clone(); 263 353 let fetcher = use_context::<crate::fetch::Fetcher>(); 264 354 let fetcher = fetcher.clone(); 265 - let res = use_server_future(move || { 355 + let res = use_server_future(use_reactive!(|ident| { 266 356 let client = fetcher.get_client(); 267 357 let old_ident = old_ident.clone(); 268 358 async move { 269 359 use jacquard::smol_str::ToSmolStr; 270 360 271 361 client 272 - .resolve_ident_owned(&*ident.read()) 362 + .resolve_ident_owned(&ident()) 273 363 .await 274 364 .map(|doc| { 275 365 use jacquard::smol_str::ToSmolStr; ··· 280 370 .flatten() 281 371 .unwrap_or(old_ident.to_smolstr()) 282 372 } 283 - }); 373 + })); 284 374 285 - use_memo(use_reactive!(|res| { 375 + let memo = use_memo(use_reactive!(|res| { 286 376 if let Ok(res) = res { 287 377 if let Some(value) = &*res.read() { 288 378 AtIdentifier::new_owned(value).unwrap() ··· 292 382 } else { 293 383 ident.read().clone() 294 384 } 295 - })) 296 - .into() 385 + })); 386 + 387 + (res, memo) 297 388 } 298 389 299 390 /// Hook to render markdown with SSR support. ··· 303 394 ident: ReadSignal<AtIdentifier<'static>>, 304 395 ) -> Memo<Option<String>> { 305 396 let fetcher = use_context::<crate::fetch::Fetcher>(); 306 - let res = use_server_future(move || { 397 + let res = use_server_future(use_reactive!(|(content, ident)| { 307 398 let client = fetcher.get_client(); 308 399 async move { 309 - let did = match ident() { 400 + let did = match ident.read().clone() { 310 401 AtIdentifier::Did(d) => d, 311 402 AtIdentifier::Handle(h) => client.resolve_handle(&h).await.ok()?, 312 403 }; 313 404 Some(render_markdown_impl(content(), did).await) 314 405 } 315 - }); 406 + })); 316 407 use_memo(use_reactive!(|res| { 317 408 let res = res.ok()?; 318 409 if let Some(Some(value)) = &*res.read() { ··· 374 465 #[cfg(feature = "fullstack-server")] 375 466 pub fn use_profile_data( 376 467 ident: ReadSignal<AtIdentifier<'static>>, 377 - ) -> Memo<Option<ProfileDataView<'static>>> { 468 + ) -> ( 469 + Result<Resource<Option<serde_json::Value>>, RenderError>, 470 + Memo<Option<ProfileDataView<'static>>>, 471 + ) { 378 472 let fetcher = use_context::<crate::fetch::Fetcher>(); 379 - let res = use_server_future(move || { 473 + let res = use_server_future(use_reactive!(|ident| { 380 474 let fetcher = fetcher.clone(); 381 475 async move { 382 - tracing::debug!("use_profile_data server future STARTED for {:?}", ident()); 476 + tracing::debug!("use_profile_data server future STARTED for {:?}", ident); 383 477 let result = fetcher 384 478 .fetch_profile(&ident()) 385 479 .await 386 480 .ok() 387 481 .map(|arc| serde_json::to_value(&*arc).ok()) 388 482 .flatten(); 389 - tracing::debug!("use_profile_data server future COMPLETED for {:?}", ident()); 483 + tracing::debug!("use_profile_data server future COMPLETED for {:?}", ident); 390 484 result 391 485 } 392 - }); 393 - use_memo(use_reactive!(|res| { 394 - let res = res.ok()?; 486 + })); 487 + let memo = use_memo(use_reactive!(|res| { 488 + let res = res.as_ref().ok()?; 395 489 if let Some(Some(value)) = &*res.read() { 396 490 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 397 491 } else { 398 492 None 399 493 } 400 - })) 494 + })); 495 + (res, memo) 401 496 } 402 497 403 498 /// Fetches profile data client-side only (no SSR) 404 499 #[cfg(not(feature = "fullstack-server"))] 405 500 pub fn use_profile_data( 406 501 ident: ReadSignal<AtIdentifier<'static>>, 407 - ) -> Memo<Option<ProfileDataView<'static>>> { 502 + ) -> ( 503 + Resource<Option<serde_json::Value>>, 504 + Memo<Option<ProfileDataView<'static>>>, 505 + ) { 408 506 let fetcher = use_context::<crate::fetch::Fetcher>(); 409 507 let res = use_resource(move || { 410 508 let fetcher = fetcher.clone(); ··· 417 515 .flatten() 418 516 } 419 517 }); 420 - use_memo(move || { 518 + let memo = use_memo(move || { 421 519 if let Some(Some(value)) = &*res.read() { 422 520 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 423 521 } else { 424 522 None 425 523 } 426 - }) 524 + }); 525 + (res, memo); 427 526 } 428 527 429 528 /// Fetches notebooks for a specific DID 430 529 #[cfg(feature = "fullstack-server")] 431 530 pub fn use_notebooks_for_did( 432 531 ident: ReadSignal<AtIdentifier<'static>>, 433 - ) -> Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 532 + ) -> ( 533 + Result<Resource<Option<Vec<serde_json::Value>>>, RenderError>, 534 + Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 535 + ) { 434 536 let fetcher = use_context::<crate::fetch::Fetcher>(); 435 - let res = use_server_future(move || { 537 + let res = use_server_future(use_reactive!(|ident| { 436 538 let fetcher = fetcher.clone(); 437 539 async move { 438 540 fetcher ··· 447 549 }) 448 550 .flatten() 449 551 } 450 - }); 451 - use_memo(use_reactive!(|res| { 452 - let res = res.ok()?; 552 + })); 553 + let memo = use_memo(use_reactive!(|res| { 554 + let res = res.as_ref().ok()?; 453 555 if let Some(Some(values)) = &*res.read() { 454 556 values 455 557 .iter() ··· 460 562 } else { 461 563 None 462 564 } 463 - })) 565 + })); 566 + (res, memo) 464 567 } 465 568 466 569 /// Fetches notebooks client-side only (no SSR) 467 570 #[cfg(not(feature = "fullstack-server"))] 468 571 pub fn use_notebooks_for_did( 469 572 ident: ReadSignal<AtIdentifier<'static>>, 470 - ) -> Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 573 + ) -> ( 574 + Resource<Option<Vec<serde_json::Value>>>, 575 + Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 576 + ) { 471 577 let fetcher = use_context::<crate::fetch::Fetcher>(); 472 578 let res = use_resource(move || { 473 579 let fetcher = fetcher.clone(); ··· 485 591 .flatten() 486 592 } 487 593 }); 488 - use_memo(move || { 594 + let memo = use_memo(move || { 489 595 if let Some(Some(values)) = &*res.read() { 490 596 values 491 597 .iter() ··· 496 602 } else { 497 603 None 498 604 } 499 - }) 605 + }); 606 + (res, memo) 500 607 } 501 608 502 609 /// Fetches notebooks from UFOS with SSR support in fullstack mode 503 610 #[cfg(feature = "fullstack-server")] 504 - pub fn use_notebooks_from_ufos() 505 - -> Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 611 + pub fn use_notebooks_from_ufos() -> ( 612 + Result<Resource<Option<Vec<serde_json::Value>>>, RenderError>, 613 + Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 614 + ) { 506 615 let fetcher = use_context::<crate::fetch::Fetcher>(); 507 616 let res = use_server_future(move || { 508 617 let fetcher = fetcher.clone(); ··· 520 629 .flatten() 521 630 } 522 631 }); 523 - use_memo(use_reactive!(|res| { 524 - let res = res.ok()?; 632 + let memo = use_memo(use_reactive!(|res| { 633 + let res = res.as_ref().ok()?; 525 634 if let Some(Some(values)) = &*res.read() { 526 635 values 527 636 .iter() ··· 532 641 } else { 533 642 None 534 643 } 535 - })) 644 + })); 645 + (res, memo) 536 646 } 537 647 538 648 /// Fetches notebooks from UFOS client-side only (no SSR) 539 649 #[cfg(not(feature = "fullstack-server"))] 540 - pub fn use_notebooks_from_ufos() 541 - -> Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 650 + pub fn use_notebooks_from_ufos() -> ( 651 + Resource<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 652 + Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 653 + ) { 542 654 let fetcher = use_context::<crate::fetch::Fetcher>(); 543 655 let res = use_resource(move || { 544 656 let fetcher = fetcher.clone(); ··· 556 668 .flatten() 557 669 } 558 670 }); 559 - use_memo(move || { 671 + let memo = use_memo(move || { 560 672 if let Some(Some(values)) = &*res.read() { 561 673 values 562 674 .iter() ··· 567 679 } else { 568 680 None 569 681 } 570 - }) 682 + }); 683 + (res, memo) 571 684 } 572 685 573 686 /// Fetches notebook metadata with SSR support in fullstack mode ··· 575 688 pub fn use_notebook( 576 689 ident: ReadSignal<AtIdentifier<'static>>, 577 690 book_title: ReadSignal<SmolStr>, 578 - ) -> Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>> { 691 + ) -> ( 692 + Result<Resource<Option<serde_json::Value>>, RenderError>, 693 + Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>>, 694 + ) { 579 695 let fetcher = use_context::<crate::fetch::Fetcher>(); 580 - let res = use_server_future(move || { 696 + let res = use_server_future(use_reactive!(|(ident, book_title)| { 581 697 let fetcher = fetcher.clone(); 582 698 async move { 583 699 fetcher ··· 588 704 .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 589 705 .flatten() 590 706 } 591 - }); 592 - use_memo(use_reactive!(|res| { 593 - let res = res.ok()?; 707 + })); 708 + let memo = use_memo(use_reactive!(|res| { 709 + let res = res.as_ref().ok()?; 594 710 if let Some(Some(value)) = &*res.read() { 595 711 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 596 712 } else { 597 713 None 598 714 } 599 - })) 715 + })); 716 + (res, memo) 600 717 } 601 718 602 719 /// Fetches notebook metadata client-side only (no SSR) ··· 604 721 pub fn use_notebook( 605 722 ident: ReadSignal<AtIdentifier<'static>>, 606 723 book_title: ReadSignal<SmolStr>, 607 - ) -> Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>> { 724 + ) -> ( 725 + Resource<Option<serde_json::Value>>, 726 + Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>>, 727 + ) { 608 728 let fetcher = use_context::<crate::fetch::Fetcher>(); 609 729 let res = use_resource(move || { 610 730 let fetcher = fetcher.clone(); ··· 618 738 .flatten() 619 739 } 620 740 }); 621 - use_memo(use_reactive!(|res| { 622 - let res = res.ok()?; 741 + let memo = use_memo(use_reactive!(|res| { 623 742 if let Some(Some(value)) = &*res.read() { 624 743 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 625 744 } else { 626 745 None 627 746 } 628 - })) 747 + })); 748 + (res, memo) 629 749 } 630 750 631 751 /// Fetches notebook entries with SSR support in fullstack mode ··· 633 753 pub fn use_notebook_entries( 634 754 ident: ReadSignal<AtIdentifier<'static>>, 635 755 book_title: ReadSignal<SmolStr>, 636 - ) -> Memo<Option<Vec<BookEntryView<'static>>>> { 756 + ) -> ( 757 + Result<Resource<Option<Vec<serde_json::Value>>>, RenderError>, 758 + Memo<Option<Vec<BookEntryView<'static>>>>, 759 + ) { 637 760 let fetcher = use_context::<crate::fetch::Fetcher>(); 638 - let res = use_server_future(move || { 761 + let res = use_server_future(use_reactive!(|(ident, book_title)| { 639 762 let fetcher = fetcher.clone(); 640 763 async move { 641 764 fetcher ··· 651 774 }) 652 775 .flatten() 653 776 } 654 - }); 655 - use_memo(use_reactive!(|res| { 656 - let res = res.ok()?; 777 + })); 778 + let memo = use_memo(use_reactive!(|res| { 779 + let res = res.as_ref().ok()?; 657 780 if let Some(Some(values)) = &*res.read() { 658 781 values 659 782 .iter() ··· 662 785 } else { 663 786 None 664 787 } 665 - })) 788 + })); 789 + 790 + (res, memo) 666 791 } 667 792 668 793 /// Fetches notebook entries client-side only (no SSR) ··· 670 795 pub fn use_notebook_entries( 671 796 ident: ReadSignal<AtIdentifier<'static>>, 672 797 book_title: ReadSignal<SmolStr>, 673 - ) -> Memo<Option<Vec<BookEntryView<'static>>>> { 798 + ) -> ( 799 + Resource<Option<Vec<BookEntryView<'static>>>>, 800 + Memo<Option<Vec<BookEntryView<'static>>>>, 801 + ) { 674 802 let fetcher = use_context::<crate::fetch::Fetcher>(); 675 803 let r = use_resource(move || { 676 804 let fetcher = fetcher.clone(); ··· 682 810 .flatten() 683 811 } 684 812 }); 685 - use_memo(move || r.read().as_ref().and_then(|v| v.clone())) 813 + let memo = use_memo(move || r.read().as_ref().and_then(|v| v.clone())); 814 + (r, memo) 686 815 } 687 816 688 817 #[cfg(feature = "fullstack-server")]
+258 -258
crates/weaver-app/src/fetch.rs
··· 326 326 } 327 327 } 328 328 329 - //#[cfg(not(feature = "server"))] 329 + #[cfg(not(feature = "server"))] 330 330 #[derive(Clone)] 331 331 pub struct Fetcher { 332 332 pub client: Arc<Client>, 333 333 } 334 334 335 - //#[cfg(not(feature = "server"))] 335 + #[cfg(not(feature = "server"))] 336 336 impl Fetcher { 337 337 pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 338 338 Self { ··· 574 574 } 575 575 } 576 576 577 - // //#[cfg(feature = "server")] 578 - // #[derive(Clone)] 579 - // pub struct Fetcher { 580 - // pub client: Arc<Client>, 581 - // book_cache: cache_impl::Cache< 582 - // (AtIdentifier<'static>, SmolStr), 583 - // Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>, 584 - // >, 585 - // entry_cache: cache_impl::Cache< 586 - // (AtIdentifier<'static>, SmolStr), 587 - // Arc<(BookEntryView<'static>, Entry<'static>)>, 588 - // >, 589 - // profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>, 590 - // } 577 + #[cfg(feature = "server")] 578 + #[derive(Clone)] 579 + pub struct Fetcher { 580 + pub client: Arc<Client>, 581 + book_cache: cache_impl::Cache< 582 + (AtIdentifier<'static>, SmolStr), 583 + Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>, 584 + >, 585 + entry_cache: cache_impl::Cache< 586 + (AtIdentifier<'static>, SmolStr), 587 + Arc<(BookEntryView<'static>, Entry<'static>)>, 588 + >, 589 + profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>, 590 + } 591 591 592 - // // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 593 - // //#[cfg(feature = "server")] 594 - // unsafe impl Sync for Fetcher {} 592 + // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 593 + //#[cfg(feature = "server")] 594 + unsafe impl Sync for Fetcher {} 595 595 596 - // // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 597 - // //#[cfg(feature = "server")] 598 - // unsafe impl Send for Fetcher {} 596 + // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 597 + //#[cfg(feature = "server")] 598 + unsafe impl Send for Fetcher {} 599 599 600 - // //#[cfg(feature = "server")] 601 - // impl Fetcher { 602 - // pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 603 - // Self { 604 - // client: Arc::new(Client::new(client)), 605 - // book_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 606 - // entry_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 607 - // profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)), 608 - // } 609 - // } 600 + #[cfg(feature = "server")] 601 + impl Fetcher { 602 + pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 603 + Self { 604 + client: Arc::new(Client::new(client)), 605 + book_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 606 + entry_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 607 + profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)), 608 + } 609 + } 610 610 611 - // pub async fn upgrade_to_authenticated( 612 - // &self, 613 - // session: OAuthSession<JacquardResolver, crate::auth::AuthStore>, 614 - // ) { 615 - // let mut session_slot = self.client.session.write().await; 616 - // *session_slot = Some(Arc::new(Agent::new(session))); 617 - // } 611 + pub async fn upgrade_to_authenticated( 612 + &self, 613 + session: OAuthSession<JacquardResolver, crate::auth::AuthStore>, 614 + ) { 615 + let mut session_slot = self.client.session.write().await; 616 + *session_slot = Some(Arc::new(Agent::new(session))); 617 + } 618 618 619 - // pub async fn downgrade_to_unauthenticated(&self) { 620 - // let mut session_slot = self.client.session.write().await; 621 - // if let Some(session) = session_slot.take() { 622 - // session.inner().logout().await.ok(); 623 - // } 624 - // } 619 + pub async fn downgrade_to_unauthenticated(&self) { 620 + let mut session_slot = self.client.session.write().await; 621 + if let Some(session) = session_slot.take() { 622 + session.inner().logout().await.ok(); 623 + } 624 + } 625 625 626 - // #[allow(dead_code)] 627 - // pub async fn current_did(&self) -> Option<Did<'static>> { 628 - // let session_slot = self.client.session.read().await; 629 - // if let Some(session) = session_slot.as_ref() { 630 - // session.info().await.map(|(d, _)| d) 631 - // } else { 632 - // None 633 - // } 634 - // } 626 + #[allow(dead_code)] 627 + pub async fn current_did(&self) -> Option<Did<'static>> { 628 + let session_slot = self.client.session.read().await; 629 + if let Some(session) = session_slot.as_ref() { 630 + session.info().await.map(|(d, _)| d) 631 + } else { 632 + None 633 + } 634 + } 635 635 636 - // pub fn get_client(&self) -> Arc<Client> { 637 - // self.client.clone() 638 - // } 636 + pub fn get_client(&self) -> Arc<Client> { 637 + self.client.clone() 638 + } 639 639 640 - // pub async fn get_notebook( 641 - // &self, 642 - // ident: AtIdentifier<'static>, 643 - // title: SmolStr, 644 - // ) -> Result<Option<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 645 - // if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { 646 - // Ok(Some(entry)) 647 - // } else { 648 - // let client = self.get_client(); 649 - // if let Some((notebook, entries)) = client 650 - // .notebook_by_title(&ident, &title) 651 - // .await 652 - // .map_err(|e| dioxus::CapturedError::from_display(e))? 653 - // { 654 - // let stored = Arc::new((notebook, entries)); 655 - // cache_impl::insert(&self.book_cache, (ident, title), stored.clone()); 656 - // Ok(Some(stored)) 657 - // } else { 658 - // Ok(None) 659 - // } 660 - // } 661 - // } 640 + pub async fn get_notebook( 641 + &self, 642 + ident: AtIdentifier<'static>, 643 + title: SmolStr, 644 + ) -> Result<Option<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 645 + if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { 646 + Ok(Some(entry)) 647 + } else { 648 + let client = self.get_client(); 649 + if let Some((notebook, entries)) = client 650 + .notebook_by_title(&ident, &title) 651 + .await 652 + .map_err(|e| dioxus::CapturedError::from_display(e))? 653 + { 654 + let stored = Arc::new((notebook, entries)); 655 + cache_impl::insert(&self.book_cache, (ident, title), stored.clone()); 656 + Ok(Some(stored)) 657 + } else { 658 + Ok(None) 659 + } 660 + } 661 + } 662 662 663 - // pub async fn get_entry( 664 - // &self, 665 - // ident: AtIdentifier<'static>, 666 - // book_title: SmolStr, 667 - // entry_title: SmolStr, 668 - // ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 669 - // if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 670 - // let (notebook, entries) = result.as_ref(); 671 - // if let Some(entry) = 672 - // cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) 673 - // { 674 - // Ok(Some(entry)) 675 - // } else { 676 - // let client = self.get_client(); 677 - // if let Some(entry) = client 678 - // .entry_by_title(notebook, entries.as_ref(), &entry_title) 679 - // .await 680 - // .map_err(|e| dioxus::CapturedError::from_display(e))? 681 - // { 682 - // let stored = Arc::new(entry); 683 - // cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); 684 - // Ok(Some(stored)) 685 - // } else { 686 - // Ok(None) 687 - // } 688 - // } 689 - // } else { 690 - // Ok(None) 691 - // } 692 - // } 663 + pub async fn get_entry( 664 + &self, 665 + ident: AtIdentifier<'static>, 666 + book_title: SmolStr, 667 + entry_title: SmolStr, 668 + ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 669 + if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 670 + let (notebook, entries) = result.as_ref(); 671 + if let Some(entry) = 672 + cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) 673 + { 674 + Ok(Some(entry)) 675 + } else { 676 + let client = self.get_client(); 677 + if let Some(entry) = client 678 + .entry_by_title(notebook, entries.as_ref(), &entry_title) 679 + .await 680 + .map_err(|e| dioxus::CapturedError::from_display(e))? 681 + { 682 + let stored = Arc::new(entry); 683 + cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); 684 + Ok(Some(stored)) 685 + } else { 686 + Ok(None) 687 + } 688 + } 689 + } else { 690 + Ok(None) 691 + } 692 + } 693 693 694 - // pub async fn fetch_notebooks_from_ufos( 695 - // &self, 696 - // ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 697 - // use jacquard::{IntoStatic, types::aturi::AtUri}; 694 + pub async fn fetch_notebooks_from_ufos( 695 + &self, 696 + ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 697 + use jacquard::{IntoStatic, types::aturi::AtUri}; 698 698 699 - // let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.book"; 700 - // let response = reqwest::get(url) 701 - // .await 702 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 699 + let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.book"; 700 + let response = reqwest::get(url) 701 + .await 702 + .map_err(|e| dioxus::CapturedError::from_display(e))?; 703 703 704 - // let records: Vec<UfosRecord> = response 705 - // .json() 706 - // .await 707 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 704 + let records: Vec<UfosRecord> = response 705 + .json() 706 + .await 707 + .map_err(|e| dioxus::CapturedError::from_display(e))?; 708 708 709 - // let mut notebooks = Vec::new(); 710 - // let client = self.get_client(); 709 + let mut notebooks = Vec::new(); 710 + let client = self.get_client(); 711 711 712 - // for ufos_record in records { 713 - // // Construct URI 714 - // let uri_str = format!( 715 - // "at://{}/{}/{}", 716 - // ufos_record.did, ufos_record.collection, ufos_record.rkey 717 - // ); 718 - // let uri = AtUri::new_owned(uri_str) 719 - // .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?; 712 + for ufos_record in records { 713 + // Construct URI 714 + let uri_str = format!( 715 + "at://{}/{}/{}", 716 + ufos_record.did, ufos_record.collection, ufos_record.rkey 717 + ); 718 + let uri = AtUri::new_owned(uri_str) 719 + .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?; 720 720 721 - // // Fetch the full notebook view (which hydrates authors) 722 - // match client.view_notebook(&uri).await { 723 - // Ok((notebook, entries)) => { 724 - // let ident = uri.authority().clone().into_static(); 725 - // let title = notebook 726 - // .title 727 - // .as_ref() 728 - // .map(|t| SmolStr::new(t.as_ref())) 729 - // .unwrap_or_else(|| SmolStr::new("Untitled")); 721 + // Fetch the full notebook view (which hydrates authors) 722 + match client.view_notebook(&uri).await { 723 + Ok((notebook, entries)) => { 724 + let ident = uri.authority().clone().into_static(); 725 + let title = notebook 726 + .title 727 + .as_ref() 728 + .map(|t| SmolStr::new(t.as_ref())) 729 + .unwrap_or_else(|| SmolStr::new("Untitled")); 730 730 731 - // let result = Arc::new((notebook, entries)); 732 - // // Cache it 733 - // cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 734 - // notebooks.push(result); 735 - // } 736 - // Err(_) => continue, // Skip notebooks that fail to load 737 - // } 738 - // } 731 + let result = Arc::new((notebook, entries)); 732 + // Cache it 733 + cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 734 + notebooks.push(result); 735 + } 736 + Err(_) => continue, // Skip notebooks that fail to load 737 + } 738 + } 739 739 740 - // Ok(notebooks) 741 - // } 740 + Ok(notebooks) 741 + } 742 742 743 - // pub async fn fetch_notebooks_for_did( 744 - // &self, 745 - // ident: &AtIdentifier<'_>, 746 - // ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 747 - // use jacquard::{ 748 - // IntoStatic, 749 - // types::{collection::Collection, nsid::Nsid}, 750 - // xrpc::XrpcExt, 751 - // }; 752 - // use weaver_api::{ 753 - // com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book, 754 - // }; 743 + pub async fn fetch_notebooks_for_did( 744 + &self, 745 + ident: &AtIdentifier<'_>, 746 + ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 747 + use jacquard::{ 748 + IntoStatic, 749 + types::{collection::Collection, nsid::Nsid}, 750 + xrpc::XrpcExt, 751 + }; 752 + use weaver_api::{ 753 + com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book, 754 + }; 755 755 756 - // let client = self.get_client(); 756 + let client = self.get_client(); 757 757 758 - // // Resolve DID and PDS 759 - // let (repo_did, pds_url) = match ident { 760 - // AtIdentifier::Did(did) => { 761 - // let pds = client 762 - // .pds_for_did(did) 763 - // .await 764 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 765 - // (did.clone(), pds) 766 - // } 767 - // AtIdentifier::Handle(handle) => client 768 - // .pds_for_handle(handle) 769 - // .await 770 - // .map_err(|e| dioxus::CapturedError::from_display(e))?, 771 - // }; 758 + // Resolve DID and PDS 759 + let (repo_did, pds_url) = match ident { 760 + AtIdentifier::Did(did) => { 761 + let pds = client 762 + .pds_for_did(did) 763 + .await 764 + .map_err(|e| dioxus::CapturedError::from_display(e))?; 765 + (did.clone(), pds) 766 + } 767 + AtIdentifier::Handle(handle) => client 768 + .pds_for_handle(handle) 769 + .await 770 + .map_err(|e| dioxus::CapturedError::from_display(e))?, 771 + }; 772 772 773 - // // Fetch all notebook records for this repo 774 - // let resp = client 775 - // .xrpc(pds_url) 776 - // .send( 777 - // &ListRecords::new() 778 - // .repo(repo_did) 779 - // .collection(Nsid::raw(Book::NSID)) 780 - // .limit(100) 781 - // .build(), 782 - // ) 783 - // .await 784 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 773 + // Fetch all notebook records for this repo 774 + let resp = client 775 + .xrpc(pds_url) 776 + .send( 777 + &ListRecords::new() 778 + .repo(repo_did) 779 + .collection(Nsid::raw(Book::NSID)) 780 + .limit(100) 781 + .build(), 782 + ) 783 + .await 784 + .map_err(|e| dioxus::CapturedError::from_display(e))?; 785 785 786 - // let mut notebooks = Vec::new(); 786 + let mut notebooks = Vec::new(); 787 787 788 - // if let Ok(list) = resp.parse() { 789 - // for record in list.records { 790 - // // View the notebook (which hydrates authors) 791 - // match client.view_notebook(&record.uri).await { 792 - // Ok((notebook, entries)) => { 793 - // let ident = record.uri.authority().clone().into_static(); 794 - // let title = notebook 795 - // .title 796 - // .as_ref() 797 - // .map(|t| SmolStr::new(t.as_ref())) 798 - // .unwrap_or_else(|| SmolStr::new("Untitled")); 788 + if let Ok(list) = resp.parse() { 789 + for record in list.records { 790 + // View the notebook (which hydrates authors) 791 + match client.view_notebook(&record.uri).await { 792 + Ok((notebook, entries)) => { 793 + let ident = record.uri.authority().clone().into_static(); 794 + let title = notebook 795 + .title 796 + .as_ref() 797 + .map(|t| SmolStr::new(t.as_ref())) 798 + .unwrap_or_else(|| SmolStr::new("Untitled")); 799 799 800 - // let result = Arc::new((notebook, entries)); 801 - // // Cache it 802 - // cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 803 - // notebooks.push(result); 804 - // } 805 - // Err(_) => continue, // Skip notebooks that fail to load 806 - // } 807 - // } 808 - // } 800 + let result = Arc::new((notebook, entries)); 801 + // Cache it 802 + cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 803 + notebooks.push(result); 804 + } 805 + Err(_) => continue, // Skip notebooks that fail to load 806 + } 807 + } 808 + } 809 809 810 - // Ok(notebooks) 811 - // } 810 + Ok(notebooks) 811 + } 812 812 813 - // pub async fn list_notebook_entries( 814 - // &self, 815 - // ident: AtIdentifier<'static>, 816 - // book_title: SmolStr, 817 - // ) -> Result<Option<Vec<BookEntryView<'static>>>> { 818 - // if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 819 - // let (notebook, entries) = result.as_ref(); 820 - // let mut book_entries = Vec::new(); 821 - // let client = self.get_client(); 813 + pub async fn list_notebook_entries( 814 + &self, 815 + ident: AtIdentifier<'static>, 816 + book_title: SmolStr, 817 + ) -> Result<Option<Vec<BookEntryView<'static>>>> { 818 + if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 819 + let (notebook, entries) = result.as_ref(); 820 + let mut book_entries = Vec::new(); 821 + let client = self.get_client(); 822 822 823 - // for index in 0..entries.len() { 824 - // match client.view_entry(notebook, entries, index).await { 825 - // Ok(book_entry) => book_entries.push(book_entry), 826 - // Err(_) => continue, // Skip entries that fail to load 827 - // } 828 - // } 823 + for index in 0..entries.len() { 824 + match client.view_entry(notebook, entries, index).await { 825 + Ok(book_entry) => book_entries.push(book_entry), 826 + Err(_) => continue, // Skip entries that fail to load 827 + } 828 + } 829 829 830 - // Ok(Some(book_entries)) 831 - // } else { 832 - // Ok(None) 833 - // } 834 - // } 830 + Ok(Some(book_entries)) 831 + } else { 832 + Ok(None) 833 + } 834 + } 835 835 836 - // pub async fn fetch_profile( 837 - // &self, 838 - // ident: &AtIdentifier<'_>, 839 - // ) -> Result<Arc<ProfileDataView<'static>>> { 840 - // use jacquard::IntoStatic; 836 + pub async fn fetch_profile( 837 + &self, 838 + ident: &AtIdentifier<'_>, 839 + ) -> Result<Arc<ProfileDataView<'static>>> { 840 + use jacquard::IntoStatic; 841 841 842 - // let ident_static = ident.clone().into_static(); 842 + let ident_static = ident.clone().into_static(); 843 843 844 - // if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 845 - // return Ok(cached); 846 - // } 844 + if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 845 + return Ok(cached); 846 + } 847 847 848 - // let client = self.get_client(); 848 + let client = self.get_client(); 849 849 850 - // let did = match ident { 851 - // AtIdentifier::Did(d) => d.clone(), 852 - // AtIdentifier::Handle(h) => client 853 - // .resolve_handle(h) 854 - // .await 855 - // .map_err(|e| dioxus::CapturedError::from_display(e))?, 856 - // }; 850 + let did = match ident { 851 + AtIdentifier::Did(d) => d.clone(), 852 + AtIdentifier::Handle(h) => client 853 + .resolve_handle(h) 854 + .await 855 + .map_err(|e| dioxus::CapturedError::from_display(e))?, 856 + }; 857 857 858 - // let (_uri, profile_view) = client 859 - // .hydrate_profile_view(&did) 860 - // .await 861 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 858 + let (_uri, profile_view) = client 859 + .hydrate_profile_view(&did) 860 + .await 861 + .map_err(|e| dioxus::CapturedError::from_display(e))?; 862 862 863 - // let result = Arc::new(profile_view); 864 - // cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 863 + let result = Arc::new(profile_view); 864 + cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 865 865 866 - // Ok(result) 867 - // } 868 - // } 866 + Ok(result) 867 + } 868 + } 869 869 870 870 impl HttpClient for Fetcher { 871 871 #[doc = " Error type returned by the HTTP client"]
+8 -5
crates/weaver-app/src/main.rs
··· 178 178 ))] 179 179 { 180 180 let fetcher = fetcher.clone(); 181 - use_future(move || { 181 + use_effect(move || { 182 182 let fetcher = fetcher.clone(); 183 - async move { 184 - if let Err(e) = auth::restore_session(fetcher, auth_state).await { 185 - tracing::debug!("Session restoration failed: {}", e); 183 + use_future(move || { 184 + let fetcher = fetcher.clone(); 185 + async move { 186 + if let Err(e) = auth::restore_session(fetcher, auth_state).await { 187 + tracing::debug!("Session restoration failed: {}", e); 188 + } 186 189 } 187 - } 190 + }); 188 191 }); 189 192 } 190 193
+7 -1
crates/weaver-app/src/views/home.rs
··· 8 8 #[component] 9 9 pub fn Home() -> Element { 10 10 // Fetch notebooks from UFOS with SSR support 11 - let notebooks = data::use_notebooks_from_ufos(); 11 + let (notebooks_result, notebooks) = data::use_notebooks_from_ufos(); 12 12 let navigator = use_navigator(); 13 13 let mut uri_input = use_signal(|| String::new()); 14 14 ··· 21 21 } 22 22 } 23 23 }; 24 + #[cfg(feature = "fullstack-server")] 25 + notebooks_result 26 + .as_ref() 27 + .ok() 28 + .map(|r| r.suspend()) 29 + .transpose()?; 24 30 25 31 rsx! { 26 32 document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS }
+30 -13
crates/weaver-app/src/views/navbar.rs
··· 5 5 use crate::data::{use_get_handle, use_load_handle}; 6 6 use crate::fetch::Fetcher; 7 7 use dioxus::prelude::*; 8 + use jacquard::types::string::Did; 8 9 9 10 const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); 10 11 ··· 20 21 tracing::debug!("Route: {:?}", route); 21 22 22 23 let mut auth_state = use_context::<Signal<crate::auth::AuthState>>(); 23 - let route_handle = use_load_handle(match &route { 24 + let (route_handle_res, route_handle) = use_load_handle(match &route { 24 25 Route::EntryPage { ident, .. } => Some(ident.clone()), 25 26 Route::RepositoryIndex { ident } => Some(ident.clone()), 26 27 Route::NotebookIndex { ident, .. } => Some(ident.clone()), 27 28 _ => None, 28 29 }); 30 + 31 + #[cfg(feature = "fullstack-server")] 32 + route_handle_res?; 33 + 29 34 let fetcher = use_context::<Fetcher>(); 30 35 let mut show_login_modal = use_signal(|| false); 31 36 32 - tracing::debug!("Navbar got route_handle: {:?}", route_handle); 37 + tracing::debug!("Navbar got route_handle: {:?}", route_handle.read()); 33 38 34 39 rsx! { 35 40 document::Link { rel: "stylesheet", href: NAVBAR_CSS } ··· 90 95 } 91 96 if auth_state.read().is_authenticated() { 92 97 if let Some(did) = &auth_state.read().did { 93 - Button { 94 - variant: ButtonVariant::Ghost, 95 - onclick: move |_| { 96 - let fetcher = fetcher.clone(); 97 - auth_state.write().clear(); 98 - async move { 99 - fetcher.downgrade_to_unauthenticated().await; 100 - } 101 - }, 102 - span { class: "auth-handle", "@{use_get_handle(did.clone())}" } 103 - } 98 + AuthButton { did: did.clone() } 104 99 } 105 100 } else { 106 101 div { ··· 121 116 Outlet::<Route> {} 122 117 } 123 118 } 119 + 120 + #[component] 121 + fn AuthButton(did: Did<'static>) -> Element { 122 + let auth_handle = use_get_handle(did); 123 + 124 + let fetcher = use_context::<Fetcher>(); 125 + let mut auth_state = use_context::<Signal<AuthState>>(); 126 + 127 + rsx! { 128 + Button { 129 + variant: ButtonVariant::Ghost, 130 + onclick: move |_| { 131 + let fetcher = fetcher.clone(); 132 + auth_state.write().clear(); 133 + async move { 134 + fetcher.downgrade_to_unauthenticated().await; 135 + } 136 + }, 137 + span { class: "auth-handle", "@{auth_handle()}" } 138 + } 139 + } 140 + }
+8 -2
crates/weaver-app/src/views/notebook.rs
··· 40 40 ); 41 41 // Fetch full notebook metadata with SSR support 42 42 // IMPORTANT: Call ALL hooks before any ? early returns to maintain hook order 43 - let notebook_data = data::use_notebook(ident, book_title); 44 - let entries_resource = data::use_notebook_entries(ident, book_title); 43 + let (notebook_result, notebook_data) = data::use_notebook(ident, book_title); 44 + let (entries_result, entries_resource) = data::use_notebook_entries(ident, book_title); 45 45 tracing::debug!("NotebookIndex got notebook data and entries"); 46 + 47 + #[cfg(feature = "fullstack-server")] 48 + notebook_result?; 49 + 50 + #[cfg(feature = "fullstack-server")] 51 + entries_result?; 46 52 47 53 rsx! { 48 54 document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS }