ssr mode works properly now

Orual 17cf62c2 ec7d916e

+590 -424
+22 -31
crates/weaver-app/src/blobcache.rs
··· 42 } 43 AtIdentifier::Handle(handle) => self.client.pds_for_handle(&handle).await?, 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(); 76 77 self.cache.insert(cid.clone(), blob); 78 if let Some(name) = name {
··· 42 } 43 AtIdentifier::Handle(handle) => self.client.pds_for_handle(&handle).await?, 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 self.cache.insert(cid.clone(), blob); 69 if let Some(name) = name {
+32 -25
crates/weaver-app/src/components/entry.rs
··· 24 use std::sync::Arc; 25 use weaver_api::sh_weaver::notebook::{BookEntryView, EntryView, entry}; 26 27 #[component] 28 pub fn EntryPage( 29 ident: ReadSignal<AtIdentifier<'static>>, 30 book_title: ReadSignal<SmolStr>, 31 title: ReadSignal<SmolStr>, 32 ) -> 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 - } 43 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); 58 let fetcher = use_context::<crate::fetch::Fetcher>(); 59 - tracing::debug!("Entry component got entry data"); 60 61 // Handle blob caching when entry data is available 62 match &*entry.read() {
··· 24 use std::sync::Arc; 25 use weaver_api::sh_weaver::notebook::{BookEntryView, EntryView, entry}; 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 + 38 #[component] 39 pub fn EntryPage( 40 ident: ReadSignal<AtIdentifier<'static>>, 41 book_title: ReadSignal<SmolStr>, 42 title: ReadSignal<SmolStr>, 43 ) -> Element { 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()); 48 49 + #[cfg(all( 50 + target_family = "wasm", 51 + target_os = "unknown", 52 + not(feature = "fullstack-server") 53 + ))] 54 let fetcher = use_context::<crate::fetch::Fetcher>(); 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 + })); 67 68 // Handle blob caching when entry data is available 69 match &*entry.read() {
+9 -2
crates/weaver-app/src/components/identity.rs
··· 26 ident() 27 ); 28 use crate::components::ProfileDisplay; 29 - let notebooks = data::use_notebooks_for_did(ident); 30 - let profile = crate::data::use_profile_data(ident); 31 tracing::debug!("RepositoryIndex got profile and notebooks"); 32 rsx! { 33 document::Stylesheet { href: NOTEBOOK_CARD_CSS } 34
··· 26 ident() 27 ); 28 use crate::components::ProfileDisplay; 29 + let (notebooks_result, notebooks) = data::use_notebooks_for_did(ident); 30 + let (profile_result, profile) = crate::data::use_profile_data(ident); 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 + 39 rsx! { 40 document::Stylesheet { href: NOTEBOOK_CARD_CSS } 41
+1 -1
crates/weaver-app/src/components/mod.rs
··· 7 8 mod entry; 9 #[allow(unused_imports)] 10 - pub use entry::{Entry, EntryCard, EntryMarkdown, EntryPage}; 11 12 pub mod identity; 13 #[allow(unused_imports)]
··· 7 8 mod entry; 9 #[allow(unused_imports)] 10 + pub use entry::{EntryCard, EntryMarkdown, EntryPage}; 11 12 pub mod identity; 13 #[allow(unused_imports)]
+215 -86
crates/weaver-app/src/data.rs
··· 36 ident: ReadSignal<AtIdentifier<'static>>, 37 book_title: ReadSignal<SmolStr>, 38 title: ReadSignal<SmolStr>, 39 - ) -> Memo<Option<(BookEntryView<'static>, Entry<'static>)>> { 40 let fetcher = use_context::<crate::fetch::Fetcher>(); 41 let fetcher = fetcher.clone(); 42 - let res = use_server_future(move || { 43 let fetcher = fetcher.clone(); 44 async move { 45 if let Some(entry) = fetcher ··· 51 let (_book_entry_view, entry_record) = (&entry.0, &entry.1); 52 if let Some(embeds) = &entry_record.embeds { 53 if let Some(images) = &embeds.images { 54 - let ident_val = ident(); 55 let images = images.clone(); 56 for image in &images.images { 57 use jacquard::smol_str::ToSmolStr; ··· 75 None 76 } 77 } 78 - }); 79 - use_memo(use_reactive!(|res| { 80 - let res = res.ok()?; 81 if let Some(Some((ev, e))) = &*res.read() { 82 use jacquard::from_json_value; 83 ··· 88 } else { 89 None 90 } 91 - })) 92 } 93 /// Fetches entry data client-side only (no SSR). 94 #[cfg(not(feature = "fullstack-server"))] ··· 96 ident: ReadSignal<AtIdentifier<'static>>, 97 book_title: ReadSignal<SmolStr>, 98 title: ReadSignal<SmolStr>, 99 - ) -> Memo<Option<(BookEntryView<'static>, Entry<'static>)>> { 100 let fetcher = use_context::<crate::fetch::Fetcher>(); 101 let fetcher = fetcher.clone(); 102 let res = use_resource(move || { ··· 136 } 137 } 138 }); 139 - use_memo(move || { 140 if let Some(Some((ev, e))) = &*res.read() { 141 use jacquard::from_json_value; 142 ··· 147 } else { 148 None 149 } 150 }) 151 } 152 153 - pub fn use_get_handle(did: Did<'static>) -> AtIdentifier<'static> { 154 let ident = use_signal(use_reactive!(|did| AtIdentifier::Did(did.clone()))); 155 - use_handle(ident.into()).read().clone() 156 } 157 158 #[cfg(feature = "fullstack-server")] 159 pub fn use_load_handle( 160 ident: Option<AtIdentifier<'static>>, 161 - ) -> ReadSignal<Option<AtIdentifier<'static>>> { 162 let ident = use_signal(use_reactive!(|ident| ident.clone())); 163 let fetcher = use_context::<crate::fetch::Fetcher>(); 164 let fetcher = fetcher.clone(); 165 - let res = use_server_future(move || { 166 let client = fetcher.get_client(); 167 async move { 168 if let Some(ident) = &*ident.read() { ··· 177 None 178 } 179 } 180 - }); 181 182 - use_memo(use_reactive!(|res| { 183 if let Ok(res) = res { 184 if let Some(value) = &*res.read() { 185 if let Some(handle) = value { ··· 193 } else { 194 ident.read().clone() 195 } 196 - })) 197 - .into() 198 } 199 200 #[cfg(not(feature = "fullstack-server"))] 201 pub fn use_load_handle( 202 ident: Option<AtIdentifier<'static>>, 203 - ) -> ReadSignal<Option<AtIdentifier<'static>>> { 204 let ident = use_signal(use_reactive!(|ident| ident.clone())); 205 let fetcher = use_context::<crate::fetch::Fetcher>(); 206 let fetcher = fetcher.clone(); ··· 223 } 224 }); 225 226 - if let Ok(ident) = res.suspend() { 227 - ident.into() 228 - } else { 229 - ident.into() 230 - } 231 } 232 #[cfg(not(feature = "fullstack-server"))] 233 - pub fn use_handle(ident: ReadSignal<AtIdentifier<'static>>) -> ReadSignal<AtIdentifier<'static>> { 234 let old_ident = ident.read().clone(); 235 let fetcher = use_context::<crate::fetch::Fetcher>(); 236 let fetcher = fetcher.clone(); ··· 251 .unwrap_or(old_ident) 252 } 253 }); 254 - if let Ok(ident) = res.suspend() { 255 - ident.into() 256 - } else { 257 - ident 258 - } 259 } 260 #[cfg(feature = "fullstack-server")] 261 - pub fn use_handle(ident: ReadSignal<AtIdentifier<'static>>) -> ReadSignal<AtIdentifier<'static>> { 262 let old_ident = ident.read().clone(); 263 let fetcher = use_context::<crate::fetch::Fetcher>(); 264 let fetcher = fetcher.clone(); 265 - let res = use_server_future(move || { 266 let client = fetcher.get_client(); 267 let old_ident = old_ident.clone(); 268 async move { 269 use jacquard::smol_str::ToSmolStr; 270 271 client 272 - .resolve_ident_owned(&*ident.read()) 273 .await 274 .map(|doc| { 275 use jacquard::smol_str::ToSmolStr; ··· 280 .flatten() 281 .unwrap_or(old_ident.to_smolstr()) 282 } 283 - }); 284 285 - use_memo(use_reactive!(|res| { 286 if let Ok(res) = res { 287 if let Some(value) = &*res.read() { 288 AtIdentifier::new_owned(value).unwrap() ··· 292 } else { 293 ident.read().clone() 294 } 295 - })) 296 - .into() 297 } 298 299 /// Hook to render markdown with SSR support. ··· 303 ident: ReadSignal<AtIdentifier<'static>>, 304 ) -> Memo<Option<String>> { 305 let fetcher = use_context::<crate::fetch::Fetcher>(); 306 - let res = use_server_future(move || { 307 let client = fetcher.get_client(); 308 async move { 309 - let did = match ident() { 310 AtIdentifier::Did(d) => d, 311 AtIdentifier::Handle(h) => client.resolve_handle(&h).await.ok()?, 312 }; 313 Some(render_markdown_impl(content(), did).await) 314 } 315 - }); 316 use_memo(use_reactive!(|res| { 317 let res = res.ok()?; 318 if let Some(Some(value)) = &*res.read() { ··· 374 #[cfg(feature = "fullstack-server")] 375 pub fn use_profile_data( 376 ident: ReadSignal<AtIdentifier<'static>>, 377 - ) -> Memo<Option<ProfileDataView<'static>>> { 378 let fetcher = use_context::<crate::fetch::Fetcher>(); 379 - let res = use_server_future(move || { 380 let fetcher = fetcher.clone(); 381 async move { 382 - tracing::debug!("use_profile_data server future STARTED for {:?}", ident()); 383 let result = fetcher 384 .fetch_profile(&ident()) 385 .await 386 .ok() 387 .map(|arc| serde_json::to_value(&*arc).ok()) 388 .flatten(); 389 - tracing::debug!("use_profile_data server future COMPLETED for {:?}", ident()); 390 result 391 } 392 - }); 393 - use_memo(use_reactive!(|res| { 394 - let res = res.ok()?; 395 if let Some(Some(value)) = &*res.read() { 396 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 397 } else { 398 None 399 } 400 - })) 401 } 402 403 /// Fetches profile data client-side only (no SSR) 404 #[cfg(not(feature = "fullstack-server"))] 405 pub fn use_profile_data( 406 ident: ReadSignal<AtIdentifier<'static>>, 407 - ) -> Memo<Option<ProfileDataView<'static>>> { 408 let fetcher = use_context::<crate::fetch::Fetcher>(); 409 let res = use_resource(move || { 410 let fetcher = fetcher.clone(); ··· 417 .flatten() 418 } 419 }); 420 - use_memo(move || { 421 if let Some(Some(value)) = &*res.read() { 422 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 423 } else { 424 None 425 } 426 - }) 427 } 428 429 /// Fetches notebooks for a specific DID 430 #[cfg(feature = "fullstack-server")] 431 pub fn use_notebooks_for_did( 432 ident: ReadSignal<AtIdentifier<'static>>, 433 - ) -> Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 434 let fetcher = use_context::<crate::fetch::Fetcher>(); 435 - let res = use_server_future(move || { 436 let fetcher = fetcher.clone(); 437 async move { 438 fetcher ··· 447 }) 448 .flatten() 449 } 450 - }); 451 - use_memo(use_reactive!(|res| { 452 - let res = res.ok()?; 453 if let Some(Some(values)) = &*res.read() { 454 values 455 .iter() ··· 460 } else { 461 None 462 } 463 - })) 464 } 465 466 /// Fetches notebooks client-side only (no SSR) 467 #[cfg(not(feature = "fullstack-server"))] 468 pub fn use_notebooks_for_did( 469 ident: ReadSignal<AtIdentifier<'static>>, 470 - ) -> Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 471 let fetcher = use_context::<crate::fetch::Fetcher>(); 472 let res = use_resource(move || { 473 let fetcher = fetcher.clone(); ··· 485 .flatten() 486 } 487 }); 488 - use_memo(move || { 489 if let Some(Some(values)) = &*res.read() { 490 values 491 .iter() ··· 496 } else { 497 None 498 } 499 - }) 500 } 501 502 /// Fetches notebooks from UFOS with SSR support in fullstack mode 503 #[cfg(feature = "fullstack-server")] 504 - pub fn use_notebooks_from_ufos() 505 - -> Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 506 let fetcher = use_context::<crate::fetch::Fetcher>(); 507 let res = use_server_future(move || { 508 let fetcher = fetcher.clone(); ··· 520 .flatten() 521 } 522 }); 523 - use_memo(use_reactive!(|res| { 524 - let res = res.ok()?; 525 if let Some(Some(values)) = &*res.read() { 526 values 527 .iter() ··· 532 } else { 533 None 534 } 535 - })) 536 } 537 538 /// Fetches notebooks from UFOS client-side only (no SSR) 539 #[cfg(not(feature = "fullstack-server"))] 540 - pub fn use_notebooks_from_ufos() 541 - -> Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 542 let fetcher = use_context::<crate::fetch::Fetcher>(); 543 let res = use_resource(move || { 544 let fetcher = fetcher.clone(); ··· 556 .flatten() 557 } 558 }); 559 - use_memo(move || { 560 if let Some(Some(values)) = &*res.read() { 561 values 562 .iter() ··· 567 } else { 568 None 569 } 570 - }) 571 } 572 573 /// Fetches notebook metadata with SSR support in fullstack mode ··· 575 pub fn use_notebook( 576 ident: ReadSignal<AtIdentifier<'static>>, 577 book_title: ReadSignal<SmolStr>, 578 - ) -> Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>> { 579 let fetcher = use_context::<crate::fetch::Fetcher>(); 580 - let res = use_server_future(move || { 581 let fetcher = fetcher.clone(); 582 async move { 583 fetcher ··· 588 .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 589 .flatten() 590 } 591 - }); 592 - use_memo(use_reactive!(|res| { 593 - let res = res.ok()?; 594 if let Some(Some(value)) = &*res.read() { 595 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 596 } else { 597 None 598 } 599 - })) 600 } 601 602 /// Fetches notebook metadata client-side only (no SSR) ··· 604 pub fn use_notebook( 605 ident: ReadSignal<AtIdentifier<'static>>, 606 book_title: ReadSignal<SmolStr>, 607 - ) -> Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>> { 608 let fetcher = use_context::<crate::fetch::Fetcher>(); 609 let res = use_resource(move || { 610 let fetcher = fetcher.clone(); ··· 618 .flatten() 619 } 620 }); 621 - use_memo(use_reactive!(|res| { 622 - let res = res.ok()?; 623 if let Some(Some(value)) = &*res.read() { 624 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 625 } else { 626 None 627 } 628 - })) 629 } 630 631 /// Fetches notebook entries with SSR support in fullstack mode ··· 633 pub fn use_notebook_entries( 634 ident: ReadSignal<AtIdentifier<'static>>, 635 book_title: ReadSignal<SmolStr>, 636 - ) -> Memo<Option<Vec<BookEntryView<'static>>>> { 637 let fetcher = use_context::<crate::fetch::Fetcher>(); 638 - let res = use_server_future(move || { 639 let fetcher = fetcher.clone(); 640 async move { 641 fetcher ··· 651 }) 652 .flatten() 653 } 654 - }); 655 - use_memo(use_reactive!(|res| { 656 - let res = res.ok()?; 657 if let Some(Some(values)) = &*res.read() { 658 values 659 .iter() ··· 662 } else { 663 None 664 } 665 - })) 666 } 667 668 /// Fetches notebook entries client-side only (no SSR) ··· 670 pub fn use_notebook_entries( 671 ident: ReadSignal<AtIdentifier<'static>>, 672 book_title: ReadSignal<SmolStr>, 673 - ) -> Memo<Option<Vec<BookEntryView<'static>>>> { 674 let fetcher = use_context::<crate::fetch::Fetcher>(); 675 let r = use_resource(move || { 676 let fetcher = fetcher.clone(); ··· 682 .flatten() 683 } 684 }); 685 - use_memo(move || r.read().as_ref().and_then(|v| v.clone())) 686 } 687 688 #[cfg(feature = "fullstack-server")]
··· 36 ident: ReadSignal<AtIdentifier<'static>>, 37 book_title: ReadSignal<SmolStr>, 38 title: ReadSignal<SmolStr>, 39 + ) -> ( 40 + Result<Resource<Option<(serde_json::Value, serde_json::Value)>>, RenderError>, 41 + Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, 42 + ) { 43 let fetcher = use_context::<crate::fetch::Fetcher>(); 44 let fetcher = fetcher.clone(); 45 + let res = use_server_future(use_reactive!(|(ident, book_title, title)| { 46 let fetcher = fetcher.clone(); 47 async move { 48 if let Some(entry) = fetcher ··· 54 let (_book_entry_view, entry_record) = (&entry.0, &entry.1); 55 if let Some(embeds) = &entry_record.embeds { 56 if let Some(images) = &embeds.images { 57 + let ident_val = ident.clone(); 58 let images = images.clone(); 59 for image in &images.images { 60 use jacquard::smol_str::ToSmolStr; ··· 78 None 79 } 80 } 81 + })); 82 + let memo = use_memo(use_reactive!(|res| { 83 + let res = res.as_ref().ok()?; 84 if let Some(Some((ev, e))) = &*res.read() { 85 use jacquard::from_json_value; 86 ··· 91 } else { 92 None 93 } 94 + })); 95 + (res, memo) 96 } 97 /// Fetches entry data client-side only (no SSR). 98 #[cfg(not(feature = "fullstack-server"))] ··· 100 ident: ReadSignal<AtIdentifier<'static>>, 101 book_title: ReadSignal<SmolStr>, 102 title: ReadSignal<SmolStr>, 103 + ) -> ( 104 + Resource<Option<(serde_json::Value, serde_json::Value)>>, 105 + Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, 106 + ) { 107 let fetcher = use_context::<crate::fetch::Fetcher>(); 108 let fetcher = fetcher.clone(); 109 let res = use_resource(move || { ··· 143 } 144 } 145 }); 146 + let memo = use_memo(move || { 147 if let Some(Some((ev, e))) = &*res.read() { 148 use jacquard::from_json_value; 149 ··· 154 } else { 155 None 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 + } 190 }) 191 } 192 193 + #[cfg(not(feature = "fullstack-server"))] 194 + pub fn use_get_handle(did: Did<'static>) -> Memo<AtIdentifier<'static>> { 195 let ident = use_signal(use_reactive!(|did| AtIdentifier::Did(did.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 + }) 223 } 224 225 #[cfg(feature = "fullstack-server")] 226 pub fn use_load_handle( 227 ident: Option<AtIdentifier<'static>>, 228 + ) -> ( 229 + Result<Resource<Option<SmolStr>>, RenderError>, 230 + Memo<Option<AtIdentifier<'static>>>, 231 + ) { 232 let ident = use_signal(use_reactive!(|ident| ident.clone())); 233 let fetcher = use_context::<crate::fetch::Fetcher>(); 234 let fetcher = fetcher.clone(); 235 + let res = use_server_future(use_reactive!(|ident| { 236 let client = fetcher.get_client(); 237 async move { 238 if let Some(ident) = &*ident.read() { ··· 247 None 248 } 249 } 250 + })); 251 252 + let memo = use_memo(use_reactive!(|res| { 253 if let Ok(res) = res { 254 if let Some(value) = &*res.read() { 255 if let Some(handle) = value { ··· 263 } else { 264 ident.read().clone() 265 } 266 + })); 267 + 268 + (res, memo) 269 } 270 271 #[cfg(not(feature = "fullstack-server"))] 272 pub fn use_load_handle( 273 ident: Option<AtIdentifier<'static>>, 274 + ) -> ( 275 + Resource<Option<AtIdentifier<'static>>>, 276 + Memo<Option<AtIdentifier<'static>>>, 277 + ) { 278 let ident = use_signal(use_reactive!(|ident| ident.clone())); 279 let fetcher = use_context::<crate::fetch::Fetcher>(); 280 let fetcher = fetcher.clone(); ··· 297 } 298 }); 299 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) 309 } 310 #[cfg(not(feature = "fullstack-server"))] 311 + pub fn use_handle( 312 + ident: ReadSignal<AtIdentifier<'static>>, 313 + ) -> (Resource<AtIdentifier<'static>>, Memo<AtIdentifier<'static>>) { 314 let old_ident = ident.read().clone(); 315 let fetcher = use_context::<crate::fetch::Fetcher>(); 316 let fetcher = fetcher.clone(); ··· 331 .unwrap_or(old_ident) 332 } 333 }); 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) 344 } 345 #[cfg(feature = "fullstack-server")] 346 + pub fn use_handle( 347 + ident: ReadSignal<AtIdentifier<'static>>, 348 + ) -> ( 349 + Result<Resource<SmolStr>, RenderError>, 350 + Memo<AtIdentifier<'static>>, 351 + ) { 352 let old_ident = ident.read().clone(); 353 let fetcher = use_context::<crate::fetch::Fetcher>(); 354 let fetcher = fetcher.clone(); 355 + let res = use_server_future(use_reactive!(|ident| { 356 let client = fetcher.get_client(); 357 let old_ident = old_ident.clone(); 358 async move { 359 use jacquard::smol_str::ToSmolStr; 360 361 client 362 + .resolve_ident_owned(&ident()) 363 .await 364 .map(|doc| { 365 use jacquard::smol_str::ToSmolStr; ··· 370 .flatten() 371 .unwrap_or(old_ident.to_smolstr()) 372 } 373 + })); 374 375 + let memo = use_memo(use_reactive!(|res| { 376 if let Ok(res) = res { 377 if let Some(value) = &*res.read() { 378 AtIdentifier::new_owned(value).unwrap() ··· 382 } else { 383 ident.read().clone() 384 } 385 + })); 386 + 387 + (res, memo) 388 } 389 390 /// Hook to render markdown with SSR support. ··· 394 ident: ReadSignal<AtIdentifier<'static>>, 395 ) -> Memo<Option<String>> { 396 let fetcher = use_context::<crate::fetch::Fetcher>(); 397 + let res = use_server_future(use_reactive!(|(content, ident)| { 398 let client = fetcher.get_client(); 399 async move { 400 + let did = match ident.read().clone() { 401 AtIdentifier::Did(d) => d, 402 AtIdentifier::Handle(h) => client.resolve_handle(&h).await.ok()?, 403 }; 404 Some(render_markdown_impl(content(), did).await) 405 } 406 + })); 407 use_memo(use_reactive!(|res| { 408 let res = res.ok()?; 409 if let Some(Some(value)) = &*res.read() { ··· 465 #[cfg(feature = "fullstack-server")] 466 pub fn use_profile_data( 467 ident: ReadSignal<AtIdentifier<'static>>, 468 + ) -> ( 469 + Result<Resource<Option<serde_json::Value>>, RenderError>, 470 + Memo<Option<ProfileDataView<'static>>>, 471 + ) { 472 let fetcher = use_context::<crate::fetch::Fetcher>(); 473 + let res = use_server_future(use_reactive!(|ident| { 474 let fetcher = fetcher.clone(); 475 async move { 476 + tracing::debug!("use_profile_data server future STARTED for {:?}", ident); 477 let result = fetcher 478 .fetch_profile(&ident()) 479 .await 480 .ok() 481 .map(|arc| serde_json::to_value(&*arc).ok()) 482 .flatten(); 483 + tracing::debug!("use_profile_data server future COMPLETED for {:?}", ident); 484 result 485 } 486 + })); 487 + let memo = use_memo(use_reactive!(|res| { 488 + let res = res.as_ref().ok()?; 489 if let Some(Some(value)) = &*res.read() { 490 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 491 } else { 492 None 493 } 494 + })); 495 + (res, memo) 496 } 497 498 /// Fetches profile data client-side only (no SSR) 499 #[cfg(not(feature = "fullstack-server"))] 500 pub fn use_profile_data( 501 ident: ReadSignal<AtIdentifier<'static>>, 502 + ) -> ( 503 + Resource<Option<serde_json::Value>>, 504 + Memo<Option<ProfileDataView<'static>>>, 505 + ) { 506 let fetcher = use_context::<crate::fetch::Fetcher>(); 507 let res = use_resource(move || { 508 let fetcher = fetcher.clone(); ··· 515 .flatten() 516 } 517 }); 518 + let memo = use_memo(move || { 519 if let Some(Some(value)) = &*res.read() { 520 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 521 } else { 522 None 523 } 524 + }); 525 + (res, memo); 526 } 527 528 /// Fetches notebooks for a specific DID 529 #[cfg(feature = "fullstack-server")] 530 pub fn use_notebooks_for_did( 531 ident: ReadSignal<AtIdentifier<'static>>, 532 + ) -> ( 533 + Result<Resource<Option<Vec<serde_json::Value>>>, RenderError>, 534 + Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 535 + ) { 536 let fetcher = use_context::<crate::fetch::Fetcher>(); 537 + let res = use_server_future(use_reactive!(|ident| { 538 let fetcher = fetcher.clone(); 539 async move { 540 fetcher ··· 549 }) 550 .flatten() 551 } 552 + })); 553 + let memo = use_memo(use_reactive!(|res| { 554 + let res = res.as_ref().ok()?; 555 if let Some(Some(values)) = &*res.read() { 556 values 557 .iter() ··· 562 } else { 563 None 564 } 565 + })); 566 + (res, memo) 567 } 568 569 /// Fetches notebooks client-side only (no SSR) 570 #[cfg(not(feature = "fullstack-server"))] 571 pub fn use_notebooks_for_did( 572 ident: ReadSignal<AtIdentifier<'static>>, 573 + ) -> ( 574 + Resource<Option<Vec<serde_json::Value>>>, 575 + Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 576 + ) { 577 let fetcher = use_context::<crate::fetch::Fetcher>(); 578 let res = use_resource(move || { 579 let fetcher = fetcher.clone(); ··· 591 .flatten() 592 } 593 }); 594 + let memo = use_memo(move || { 595 if let Some(Some(values)) = &*res.read() { 596 values 597 .iter() ··· 602 } else { 603 None 604 } 605 + }); 606 + (res, memo) 607 } 608 609 /// Fetches notebooks from UFOS with SSR support in fullstack mode 610 #[cfg(feature = "fullstack-server")] 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 + ) { 615 let fetcher = use_context::<crate::fetch::Fetcher>(); 616 let res = use_server_future(move || { 617 let fetcher = fetcher.clone(); ··· 629 .flatten() 630 } 631 }); 632 + let memo = use_memo(use_reactive!(|res| { 633 + let res = res.as_ref().ok()?; 634 if let Some(Some(values)) = &*res.read() { 635 values 636 .iter() ··· 641 } else { 642 None 643 } 644 + })); 645 + (res, memo) 646 } 647 648 /// Fetches notebooks from UFOS client-side only (no SSR) 649 #[cfg(not(feature = "fullstack-server"))] 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 + ) { 654 let fetcher = use_context::<crate::fetch::Fetcher>(); 655 let res = use_resource(move || { 656 let fetcher = fetcher.clone(); ··· 668 .flatten() 669 } 670 }); 671 + let memo = use_memo(move || { 672 if let Some(Some(values)) = &*res.read() { 673 values 674 .iter() ··· 679 } else { 680 None 681 } 682 + }); 683 + (res, memo) 684 } 685 686 /// Fetches notebook metadata with SSR support in fullstack mode ··· 688 pub fn use_notebook( 689 ident: ReadSignal<AtIdentifier<'static>>, 690 book_title: ReadSignal<SmolStr>, 691 + ) -> ( 692 + Result<Resource<Option<serde_json::Value>>, RenderError>, 693 + Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>>, 694 + ) { 695 let fetcher = use_context::<crate::fetch::Fetcher>(); 696 + let res = use_server_future(use_reactive!(|(ident, book_title)| { 697 let fetcher = fetcher.clone(); 698 async move { 699 fetcher ··· 704 .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 705 .flatten() 706 } 707 + })); 708 + let memo = use_memo(use_reactive!(|res| { 709 + let res = res.as_ref().ok()?; 710 if let Some(Some(value)) = &*res.read() { 711 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 712 } else { 713 None 714 } 715 + })); 716 + (res, memo) 717 } 718 719 /// Fetches notebook metadata client-side only (no SSR) ··· 721 pub fn use_notebook( 722 ident: ReadSignal<AtIdentifier<'static>>, 723 book_title: ReadSignal<SmolStr>, 724 + ) -> ( 725 + Resource<Option<serde_json::Value>>, 726 + Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>>, 727 + ) { 728 let fetcher = use_context::<crate::fetch::Fetcher>(); 729 let res = use_resource(move || { 730 let fetcher = fetcher.clone(); ··· 738 .flatten() 739 } 740 }); 741 + let memo = use_memo(use_reactive!(|res| { 742 if let Some(Some(value)) = &*res.read() { 743 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 744 } else { 745 None 746 } 747 + })); 748 + (res, memo) 749 } 750 751 /// Fetches notebook entries with SSR support in fullstack mode ··· 753 pub fn use_notebook_entries( 754 ident: ReadSignal<AtIdentifier<'static>>, 755 book_title: ReadSignal<SmolStr>, 756 + ) -> ( 757 + Result<Resource<Option<Vec<serde_json::Value>>>, RenderError>, 758 + Memo<Option<Vec<BookEntryView<'static>>>>, 759 + ) { 760 let fetcher = use_context::<crate::fetch::Fetcher>(); 761 + let res = use_server_future(use_reactive!(|(ident, book_title)| { 762 let fetcher = fetcher.clone(); 763 async move { 764 fetcher ··· 774 }) 775 .flatten() 776 } 777 + })); 778 + let memo = use_memo(use_reactive!(|res| { 779 + let res = res.as_ref().ok()?; 780 if let Some(Some(values)) = &*res.read() { 781 values 782 .iter() ··· 785 } else { 786 None 787 } 788 + })); 789 + 790 + (res, memo) 791 } 792 793 /// Fetches notebook entries client-side only (no SSR) ··· 795 pub fn use_notebook_entries( 796 ident: ReadSignal<AtIdentifier<'static>>, 797 book_title: ReadSignal<SmolStr>, 798 + ) -> ( 799 + Resource<Option<Vec<BookEntryView<'static>>>>, 800 + Memo<Option<Vec<BookEntryView<'static>>>>, 801 + ) { 802 let fetcher = use_context::<crate::fetch::Fetcher>(); 803 let r = use_resource(move || { 804 let fetcher = fetcher.clone(); ··· 810 .flatten() 811 } 812 }); 813 + let memo = use_memo(move || r.read().as_ref().and_then(|v| v.clone())); 814 + (r, memo) 815 } 816 817 #[cfg(feature = "fullstack-server")]
+258 -258
crates/weaver-app/src/fetch.rs
··· 326 } 327 } 328 329 - //#[cfg(not(feature = "server"))] 330 #[derive(Clone)] 331 pub struct Fetcher { 332 pub client: Arc<Client>, 333 } 334 335 - //#[cfg(not(feature = "server"))] 336 impl Fetcher { 337 pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 338 Self { ··· 574 } 575 } 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 - // } 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 {} 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 {} 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 - // } 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 - // } 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 - // } 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 - // } 635 636 - // pub fn get_client(&self) -> Arc<Client> { 637 - // self.client.clone() 638 - // } 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 - // } 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 - // } 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}; 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))?; 703 704 - // let records: Vec<UfosRecord> = response 705 - // .json() 706 - // .await 707 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 708 709 - // let mut notebooks = Vec::new(); 710 - // let client = self.get_client(); 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)))?; 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")); 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 - // } 739 740 - // Ok(notebooks) 741 - // } 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 - // }; 755 756 - // let client = self.get_client(); 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 - // }; 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))?; 785 786 - // let mut notebooks = Vec::new(); 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")); 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 - // } 809 810 - // Ok(notebooks) 811 - // } 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(); 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 - // } 829 830 - // Ok(Some(book_entries)) 831 - // } else { 832 - // Ok(None) 833 - // } 834 - // } 835 836 - // pub async fn fetch_profile( 837 - // &self, 838 - // ident: &AtIdentifier<'_>, 839 - // ) -> Result<Arc<ProfileDataView<'static>>> { 840 - // use jacquard::IntoStatic; 841 842 - // let ident_static = ident.clone().into_static(); 843 844 - // if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 845 - // return Ok(cached); 846 - // } 847 848 - // let client = self.get_client(); 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 - // }; 857 858 - // let (_uri, profile_view) = client 859 - // .hydrate_profile_view(&did) 860 - // .await 861 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 862 863 - // let result = Arc::new(profile_view); 864 - // cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 865 866 - // Ok(result) 867 - // } 868 - // } 869 870 impl HttpClient for Fetcher { 871 #[doc = " Error type returned by the HTTP client"]
··· 326 } 327 } 328 329 + #[cfg(not(feature = "server"))] 330 #[derive(Clone)] 331 pub struct Fetcher { 332 pub client: Arc<Client>, 333 } 334 335 + #[cfg(not(feature = "server"))] 336 impl Fetcher { 337 pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 338 Self { ··· 574 } 575 } 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 + } 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 {} 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 {} 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 + } 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 + } 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 + } 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 + } 635 636 + pub fn get_client(&self) -> Arc<Client> { 637 + self.client.clone() 638 + } 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 + } 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 + } 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}; 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))?; 703 704 + let records: Vec<UfosRecord> = response 705 + .json() 706 + .await 707 + .map_err(|e| dioxus::CapturedError::from_display(e))?; 708 709 + let mut notebooks = Vec::new(); 710 + let client = self.get_client(); 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)))?; 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")); 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 + } 739 740 + Ok(notebooks) 741 + } 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 + }; 755 756 + let client = self.get_client(); 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 + }; 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))?; 785 786 + let mut notebooks = Vec::new(); 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")); 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 + } 809 810 + Ok(notebooks) 811 + } 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(); 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 + } 829 830 + Ok(Some(book_entries)) 831 + } else { 832 + Ok(None) 833 + } 834 + } 835 836 + pub async fn fetch_profile( 837 + &self, 838 + ident: &AtIdentifier<'_>, 839 + ) -> Result<Arc<ProfileDataView<'static>>> { 840 + use jacquard::IntoStatic; 841 842 + let ident_static = ident.clone().into_static(); 843 844 + if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 845 + return Ok(cached); 846 + } 847 848 + let client = self.get_client(); 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 + }; 857 858 + let (_uri, profile_view) = client 859 + .hydrate_profile_view(&did) 860 + .await 861 + .map_err(|e| dioxus::CapturedError::from_display(e))?; 862 863 + let result = Arc::new(profile_view); 864 + cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 865 866 + Ok(result) 867 + } 868 + } 869 870 impl HttpClient for Fetcher { 871 #[doc = " Error type returned by the HTTP client"]
+8 -5
crates/weaver-app/src/main.rs
··· 178 ))] 179 { 180 let fetcher = fetcher.clone(); 181 - use_future(move || { 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); 186 } 187 - } 188 }); 189 } 190
··· 178 ))] 179 { 180 let fetcher = fetcher.clone(); 181 + use_effect(move || { 182 let fetcher = fetcher.clone(); 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 + } 189 } 190 + }); 191 }); 192 } 193
+7 -1
crates/weaver-app/src/views/home.rs
··· 8 #[component] 9 pub fn Home() -> Element { 10 // Fetch notebooks from UFOS with SSR support 11 - let notebooks = data::use_notebooks_from_ufos(); 12 let navigator = use_navigator(); 13 let mut uri_input = use_signal(|| String::new()); 14 ··· 21 } 22 } 23 }; 24 25 rsx! { 26 document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS }
··· 8 #[component] 9 pub fn Home() -> Element { 10 // Fetch notebooks from UFOS with SSR support 11 + let (notebooks_result, notebooks) = data::use_notebooks_from_ufos(); 12 let navigator = use_navigator(); 13 let mut uri_input = use_signal(|| String::new()); 14 ··· 21 } 22 } 23 }; 24 + #[cfg(feature = "fullstack-server")] 25 + notebooks_result 26 + .as_ref() 27 + .ok() 28 + .map(|r| r.suspend()) 29 + .transpose()?; 30 31 rsx! { 32 document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS }
+30 -13
crates/weaver-app/src/views/navbar.rs
··· 5 use crate::data::{use_get_handle, use_load_handle}; 6 use crate::fetch::Fetcher; 7 use dioxus::prelude::*; 8 9 const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); 10 ··· 20 tracing::debug!("Route: {:?}", route); 21 22 let mut auth_state = use_context::<Signal<crate::auth::AuthState>>(); 23 - let route_handle = use_load_handle(match &route { 24 Route::EntryPage { ident, .. } => Some(ident.clone()), 25 Route::RepositoryIndex { ident } => Some(ident.clone()), 26 Route::NotebookIndex { ident, .. } => Some(ident.clone()), 27 _ => None, 28 }); 29 let fetcher = use_context::<Fetcher>(); 30 let mut show_login_modal = use_signal(|| false); 31 32 - tracing::debug!("Navbar got route_handle: {:?}", route_handle); 33 34 rsx! { 35 document::Link { rel: "stylesheet", href: NAVBAR_CSS } ··· 90 } 91 if auth_state.read().is_authenticated() { 92 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 - } 104 } 105 } else { 106 div { ··· 121 Outlet::<Route> {} 122 } 123 }
··· 5 use crate::data::{use_get_handle, use_load_handle}; 6 use crate::fetch::Fetcher; 7 use dioxus::prelude::*; 8 + use jacquard::types::string::Did; 9 10 const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); 11 ··· 21 tracing::debug!("Route: {:?}", route); 22 23 let mut auth_state = use_context::<Signal<crate::auth::AuthState>>(); 24 + let (route_handle_res, route_handle) = use_load_handle(match &route { 25 Route::EntryPage { ident, .. } => Some(ident.clone()), 26 Route::RepositoryIndex { ident } => Some(ident.clone()), 27 Route::NotebookIndex { ident, .. } => Some(ident.clone()), 28 _ => None, 29 }); 30 + 31 + #[cfg(feature = "fullstack-server")] 32 + route_handle_res?; 33 + 34 let fetcher = use_context::<Fetcher>(); 35 let mut show_login_modal = use_signal(|| false); 36 37 + tracing::debug!("Navbar got route_handle: {:?}", route_handle.read()); 38 39 rsx! { 40 document::Link { rel: "stylesheet", href: NAVBAR_CSS } ··· 95 } 96 if auth_state.read().is_authenticated() { 97 if let Some(did) = &auth_state.read().did { 98 + AuthButton { did: did.clone() } 99 } 100 } else { 101 div { ··· 116 Outlet::<Route> {} 117 } 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 ); 41 // Fetch full notebook metadata with SSR support 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); 45 tracing::debug!("NotebookIndex got notebook data and entries"); 46 47 rsx! { 48 document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS }
··· 40 ); 41 // Fetch full notebook metadata with SSR support 42 // IMPORTANT: Call ALL hooks before any ? early returns to maintain hook order 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 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?; 52 53 rsx! { 54 document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS }