server side cache back in

Orual e2b0d156 21e174ee

+329 -373
+44
crates/weaver-app/src/data.rs
··· 655 655 async move { 656 656 match fetcher.fetch_entries_from_ufos().await { 657 657 Ok(entries) => { 658 + // Cache blobs for each entry's embedded images 659 + for arc in &entries { 660 + let (view, entry, _) = arc.as_ref(); 661 + if let Some(embeds) = &entry.embeds { 662 + if let Some(images) = &embeds.images { 663 + use jacquard::smol_str::ToSmolStr; 664 + use jacquard::types::aturi::AtUri; 665 + // Extract ident from the entry's at-uri 666 + if let Ok(at_uri) = AtUri::new(view.uri.as_ref()) { 667 + let ident = at_uri.authority().to_smolstr(); 668 + for image in &images.images { 669 + let cid = image.image.blob().cid(); 670 + cache_blob( 671 + ident.clone(), 672 + cid.to_smolstr(), 673 + image.name.as_ref().map(|n| n.to_smolstr()), 674 + ) 675 + .await 676 + .ok(); 677 + } 678 + } 679 + } 680 + } 681 + } 658 682 Some( 659 683 entries 660 684 .iter() ··· 945 969 async move { 946 970 match fetcher.get_entry_by_rkey(ident(), rkey()).await { 947 971 Ok(Some(data)) => { 972 + // Cache blobs for embedded images 973 + if let Some(embeds) = &data.entry.embeds { 974 + if let Some(images) = &embeds.images { 975 + use jacquard::smol_str::ToSmolStr; 976 + use jacquard::types::aturi::AtUri; 977 + if let Ok(at_uri) = AtUri::new(data.entry_view.uri.as_ref()) { 978 + let ident_str = at_uri.authority().to_smolstr(); 979 + for image in &images.images { 980 + let cid = image.image.blob().cid(); 981 + cache_blob( 982 + ident_str.clone(), 983 + cid.to_smolstr(), 984 + image.name.as_ref().map(|n| n.to_smolstr()), 985 + ) 986 + .await 987 + .ok(); 988 + } 989 + } 990 + } 991 + } 948 992 let entry_json = serde_json::to_value(&data.entry).ok()?; 949 993 let entry_view_json = serde_json::to_value(&data.entry_view).ok()?; 950 994 let notebook_ctx_json = data.notebook_context.as_ref().map(|ctx| {
+142 -328
crates/weaver-app/src/fetch.rs
··· 352 352 #[derive(Clone)] 353 353 pub struct Fetcher { 354 354 pub client: Arc<Client>, 355 + #[cfg(feature = "server")] 356 + book_cache: cache_impl::Cache< 357 + (AtIdentifier<'static>, SmolStr), 358 + Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>, 359 + >, 360 + #[cfg(feature = "server")] 361 + entry_cache: cache_impl::Cache< 362 + (AtIdentifier<'static>, SmolStr), 363 + Arc<(BookEntryView<'static>, Entry<'static>)>, 364 + >, 365 + #[cfg(feature = "server")] 366 + profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>, 367 + #[cfg(feature = "server")] 368 + standalone_entry_cache: 369 + cache_impl::Cache<(AtIdentifier<'static>, SmolStr), Arc<StandaloneEntryData>>, 355 370 } 356 371 357 372 //#[cfg(not(feature = "server"))] ··· 359 374 pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 360 375 Self { 361 376 client: Arc::new(Client::new(client)), 377 + #[cfg(feature = "server")] 378 + book_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(30)), 379 + #[cfg(feature = "server")] 380 + entry_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(30)), 381 + #[cfg(feature = "server")] 382 + profile_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(1800)), 383 + #[cfg(feature = "server")] 384 + standalone_entry_cache: cache_impl::new_cache(100, std::time::Duration::from_secs(30)), 362 385 } 363 386 } 364 387 ··· 396 419 ident: AtIdentifier<'static>, 397 420 title: SmolStr, 398 421 ) -> Result<Option<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 422 + #[cfg(feature = "server")] 423 + if let Some(cached) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { 424 + return Ok(Some(cached)); 425 + } 426 + 399 427 let client = self.get_client(); 400 428 if let Some((notebook, entries)) = client 401 429 .notebook_by_title(&ident, &title) ··· 403 431 .map_err(|e| dioxus::CapturedError::from_display(e))? 404 432 { 405 433 let stored = Arc::new((notebook, entries)); 434 + #[cfg(feature = "server")] 435 + cache_impl::insert(&self.book_cache, (ident, title), stored.clone()); 406 436 Ok(Some(stored)) 407 437 } else { 408 438 Err(dioxus::CapturedError::from_display("Notebook not found")) ··· 415 445 book_title: SmolStr, 416 446 entry_title: SmolStr, 417 447 ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 448 + #[cfg(feature = "server")] 449 + if let Some(cached) = 450 + cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) 451 + { 452 + return Ok(Some(cached)); 453 + } 454 + 418 455 if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 419 456 let (notebook, entries) = result.as_ref(); 420 457 let client = self.get_client(); ··· 424 461 .map_err(|e| dioxus::CapturedError::from_display(e))? 425 462 { 426 463 let stored = Arc::new(entry); 464 + #[cfg(feature = "server")] 465 + cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); 427 466 Ok(Some(stored)) 428 467 } else { 429 468 Err(dioxus::CapturedError::from_display("Entry not found")) ··· 459 498 ); 460 499 let uri = AtUri::new_owned(uri_str) 461 500 .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?; 462 - 463 - // Fetch the full notebook view (which hydrates authors) 464 501 match client.view_notebook(&uri).await { 465 502 Ok((notebook, entries)) => { 466 503 let ident = uri.authority().clone().into_static(); ··· 471 508 .unwrap_or_else(|| SmolStr::new("Untitled")); 472 509 473 510 let result = Arc::new((notebook, entries)); 511 + #[cfg(feature = "server")] 512 + cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 474 513 notebooks.push(result); 475 514 } 476 515 Err(_) => continue, // Skip notebooks that fail to load ··· 488 527 489 528 let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.entry"; 490 529 491 - let response = reqwest::get(url) 492 - .await 493 - .map_err(|e| { 494 - tracing::error!("[fetch_entries_from_ufos] request failed: {:?}", e); 495 - dioxus::CapturedError::from_display(e) 496 - })?; 497 - 498 - let mut records: Vec<UfosRecord> = response 499 - .json() 500 - .await 501 - .map_err(|e| { 502 - tracing::error!("[fetch_entries_from_ufos] json parse failed: {:?}", e); 503 - dioxus::CapturedError::from_display(e) 504 - })?; 530 + let response = reqwest::get(url).await.map_err(|e| { 531 + tracing::error!("[fetch_entries_from_ufos] request failed: {:?}", e); 532 + dioxus::CapturedError::from_display(e) 533 + })?; 505 534 506 - // Sort by time_us descending (reverse chronological) 535 + let mut records: Vec<UfosRecord> = response.json().await.map_err(|e| { 536 + tracing::error!("[fetch_entries_from_ufos] json parse failed: {:?}", e); 537 + dioxus::CapturedError::from_display(e) 538 + })?; 507 539 records.sort_by(|a, b| b.time_us.cmp(&a.time_us)); 508 540 509 541 let mut entries = Vec::new(); 510 542 let client = self.get_client(); 511 543 512 544 for ufos_record in records { 513 - // Parse DID 514 545 let did = match Did::new(&ufos_record.did) { 515 546 Ok(d) => d.into_static(), 516 547 Err(e) => { 517 - tracing::warn!("[fetch_entries_from_ufos] invalid DID {}: {:?}", ufos_record.did, e); 548 + tracing::warn!( 549 + "[fetch_entries_from_ufos] invalid DID {}: {:?}", 550 + ufos_record.did, 551 + e 552 + ); 518 553 continue; 519 554 } 520 555 }; 521 556 let ident = AtIdentifier::Did(did); 522 - 523 - // Fetch the entry view 524 - match client 525 - .fetch_entry_by_rkey(&ident, &ufos_record.rkey) 526 - .await 527 - { 557 + match client.fetch_entry_by_rkey(&ident, &ufos_record.rkey).await { 528 558 Ok((entry_view, entry)) => { 529 559 entries.push(Arc::new(( 530 560 entry_view.into_static(), ··· 533 563 ))); 534 564 } 535 565 Err(e) => { 536 - tracing::warn!("[fetch_entries_from_ufos] failed to load entry {}: {:?}", ufos_record.rkey, e); 566 + tracing::warn!( 567 + "[fetch_entries_from_ufos] failed to load entry {}: {:?}", 568 + ufos_record.rkey, 569 + e 570 + ); 537 571 continue; 538 572 } 539 573 } ··· 592 626 // View the notebook (which hydrates authors) 593 627 match client.view_notebook(&record.uri).await { 594 628 Ok((notebook, entries)) => { 629 + let ident = record.uri.authority().clone().into_static(); 630 + let title = notebook 631 + .title 632 + .as_ref() 633 + .map(|t| SmolStr::new(t.as_ref())) 634 + .unwrap_or_else(|| SmolStr::new("Untitled")); 635 + 595 636 let result = Arc::new((notebook, entries)); 637 + #[cfg(feature = "server")] 638 + cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 596 639 notebooks.push(result); 597 640 } 598 641 Err(_) => continue, // Skip notebooks that fail to load ··· 607 650 ident: AtIdentifier<'static>, 608 651 book_title: SmolStr, 609 652 ) -> Result<Option<Vec<BookEntryView<'static>>>> { 653 + use jacquard::types::aturi::AtUri; 654 + 610 655 if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 611 - let (notebook, entries) = result.as_ref(); 656 + let (notebook, entry_refs) = result.as_ref(); 612 657 let mut book_entries = Vec::new(); 613 658 let client = self.get_client(); 614 659 615 - for index in 0..entries.len() { 616 - match client.view_entry(notebook, entries, index).await { 617 - Ok(book_entry) => book_entries.push(book_entry), 618 - Err(_) => continue, // Skip entries that fail to load 660 + for (index, entry_ref) in entry_refs.iter().enumerate() { 661 + // Try to extract rkey from URI 662 + let rkey = AtUri::new(entry_ref.uri.as_ref()) 663 + .ok() 664 + .and_then(|uri| uri.rkey().map(|r| SmolStr::new(r.as_ref()))); 665 + 666 + // Check cache first 667 + #[cfg(feature = "server")] 668 + if let Some(ref rkey) = rkey { 669 + if let Some(cached) = 670 + cache_impl::get(&self.entry_cache, &(ident.clone(), rkey.clone())) 671 + { 672 + book_entries.push(cached.0.clone()); 673 + continue; 674 + } 675 + } 676 + 677 + // Fetch if not cached 678 + if let Ok(book_entry) = client.view_entry(notebook, entry_refs, index).await { 679 + // Try to populate cache by deserializing Entry from the view's record 680 + #[cfg(feature = "server")] 681 + if let Some(rkey) = rkey { 682 + use jacquard::IntoStatic; 683 + use weaver_api::sh_weaver::notebook::entry::Entry; 684 + if let Ok(entry) = 685 + jacquard::from_data::<Entry<'_>>(&book_entry.entry.record) 686 + { 687 + let cached = 688 + Arc::new((book_entry.clone().into_static(), entry.into_static())); 689 + cache_impl::insert(&self.entry_cache, (ident.clone(), rkey), cached); 690 + } 691 + } 692 + book_entries.push(book_entry); 619 693 } 620 694 } 621 695 ··· 629 703 &self, 630 704 ident: &AtIdentifier<'_>, 631 705 ) -> Result<Arc<ProfileDataView<'static>>> { 706 + use jacquard::IntoStatic; 707 + 708 + let ident_static = ident.clone().into_static(); 709 + 710 + #[cfg(feature = "server")] 711 + if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 712 + return Ok(cached); 713 + } 714 + 632 715 let client = self.get_client(); 633 716 634 717 let did = match ident { ··· 644 727 .await 645 728 .map_err(|e| dioxus::CapturedError::from_display(e))?; 646 729 647 - Ok(Arc::new(profile_view)) 730 + let result = Arc::new(profile_view); 731 + #[cfg(feature = "server")] 732 + cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 733 + 734 + Ok(result) 648 735 } 649 736 650 737 /// Fetch an entry by rkey with optional notebook context lookup. ··· 654 741 rkey: SmolStr, 655 742 ) -> Result<Option<Arc<StandaloneEntryData>>> { 656 743 use jacquard::types::aturi::AtUri; 744 + 745 + #[cfg(feature = "server")] 746 + if let Some(cached) = 747 + cache_impl::get(&self.standalone_entry_cache, &(ident.clone(), rkey.clone())) 748 + { 749 + return Ok(Some(cached)); 750 + } 657 751 658 752 let client = self.get_client(); 659 753 ··· 711 805 None 712 806 }; 713 807 714 - Ok(Some(Arc::new(StandaloneEntryData { 808 + let result = Arc::new(StandaloneEntryData { 715 809 entry, 716 810 entry_view, 717 811 notebook_context, 718 - }))) 812 + }); 813 + #[cfg(feature = "server")] 814 + cache_impl::insert(&self.standalone_entry_cache, (ident, rkey), result.clone()); 815 + 816 + Ok(Some(result)) 719 817 } 720 818 721 819 /// Fetch an entry by rkey within a specific notebook context. ··· 729 827 rkey: SmolStr, 730 828 ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 731 829 use jacquard::types::aturi::AtUri; 830 + 831 + #[cfg(feature = "server")] 832 + if let Some(cached) = cache_impl::get(&self.entry_cache, &(ident.clone(), rkey.clone())) { 833 + return Ok(Some(cached)); 834 + } 732 835 733 836 let client = self.get_client(); 734 837 ··· 783 886 .build(); 784 887 } 785 888 786 - Ok(Some(Arc::new((book_entry_view.into_static(), entry)))) 889 + let result = Arc::new((book_entry_view.into_static(), entry)); 890 + #[cfg(feature = "server")] 891 + cache_impl::insert(&self.entry_cache, (ident, rkey), result.clone()); 892 + 893 + Ok(Some(result)) 787 894 } 788 895 } 789 - 790 - // #[cfg(feature = "server")] 791 - // #[derive(Clone)] 792 - // pub struct Fetcher { 793 - // pub client: Arc<Client>, 794 - // book_cache: cache_impl::Cache< 795 - // (AtIdentifier<'static>, SmolStr), 796 - // Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>, 797 - // >, 798 - // entry_cache: cache_impl::Cache< 799 - // (AtIdentifier<'static>, SmolStr), 800 - // Arc<(BookEntryView<'static>, Entry<'static>)>, 801 - // >, 802 - // profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>, 803 - // } 804 - 805 - // // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 806 - // //#[cfg(feature = "server")] 807 - // unsafe impl Sync for Fetcher {} 808 - 809 - // // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 810 - // //#[cfg(feature = "server")] 811 - // unsafe impl Send for Fetcher {} 812 - 813 - // #[cfg(feature = "server")] 814 - // impl Fetcher { 815 - // pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 816 - // Self { 817 - // client: Arc::new(Client::new(client)), 818 - // book_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 819 - // entry_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 820 - // profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)), 821 - // } 822 - // } 823 - 824 - // pub async fn upgrade_to_authenticated( 825 - // &self, 826 - // session: OAuthSession<JacquardResolver, crate::auth::AuthStore>, 827 - // ) { 828 - // let mut session_slot = self.client.session.write().await; 829 - // *session_slot = Some(Arc::new(Agent::new(session))); 830 - // } 831 - 832 - // pub async fn downgrade_to_unauthenticated(&self) { 833 - // let mut session_slot = self.client.session.write().await; 834 - // if let Some(session) = session_slot.take() { 835 - // session.inner().logout().await.ok(); 836 - // } 837 - // } 838 - 839 - // #[allow(dead_code)] 840 - // pub async fn current_did(&self) -> Option<Did<'static>> { 841 - // let session_slot = self.client.session.read().await; 842 - // if let Some(session) = session_slot.as_ref() { 843 - // session.info().await.map(|(d, _)| d) 844 - // } else { 845 - // None 846 - // } 847 - // } 848 - 849 - // pub fn get_client(&self) -> Arc<Client> { 850 - // self.client.clone() 851 - // } 852 - 853 - // pub async fn get_notebook( 854 - // &self, 855 - // ident: AtIdentifier<'static>, 856 - // title: SmolStr, 857 - // ) -> Result<Option<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 858 - // if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { 859 - // Ok(Some(entry)) 860 - // } else { 861 - // let client = self.get_client(); 862 - // if let Some((notebook, entries)) = client 863 - // .notebook_by_title(&ident, &title) 864 - // .await 865 - // .map_err(|e| dioxus::CapturedError::from_display(e))? 866 - // { 867 - // let stored = Arc::new((notebook, entries)); 868 - // cache_impl::insert(&self.book_cache, (ident, title), stored.clone()); 869 - // Ok(Some(stored)) 870 - // } else { 871 - // Ok(None) 872 - // } 873 - // } 874 - // } 875 - 876 - // pub async fn get_entry( 877 - // &self, 878 - // ident: AtIdentifier<'static>, 879 - // book_title: SmolStr, 880 - // entry_title: SmolStr, 881 - // ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 882 - // if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 883 - // let (notebook, entries) = result.as_ref(); 884 - // if let Some(entry) = 885 - // cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) 886 - // { 887 - // Ok(Some(entry)) 888 - // } else { 889 - // let client = self.get_client(); 890 - // if let Some(entry) = client 891 - // .entry_by_title(notebook, entries.as_ref(), &entry_title) 892 - // .await 893 - // .map_err(|e| dioxus::CapturedError::from_display(e))? 894 - // { 895 - // let stored = Arc::new(entry); 896 - // cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); 897 - // Ok(Some(stored)) 898 - // } else { 899 - // Ok(None) 900 - // } 901 - // } 902 - // } else { 903 - // Ok(None) 904 - // } 905 - // } 906 - 907 - // pub async fn fetch_notebooks_from_ufos( 908 - // &self, 909 - // ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 910 - // use jacquard::{IntoStatic, types::aturi::AtUri}; 911 - 912 - // let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.book"; 913 - // let response = reqwest::get(url) 914 - // .await 915 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 916 - 917 - // let records: Vec<UfosRecord> = response 918 - // .json() 919 - // .await 920 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 921 - 922 - // let mut notebooks = Vec::new(); 923 - // let client = self.get_client(); 924 - 925 - // for ufos_record in records { 926 - // // Construct URI 927 - // let uri_str = format!( 928 - // "at://{}/{}/{}", 929 - // ufos_record.did, ufos_record.collection, ufos_record.rkey 930 - // ); 931 - // let uri = AtUri::new_owned(uri_str) 932 - // .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?; 933 - 934 - // // Fetch the full notebook view (which hydrates authors) 935 - // match client.view_notebook(&uri).await { 936 - // Ok((notebook, entries)) => { 937 - // let ident = uri.authority().clone().into_static(); 938 - // let title = notebook 939 - // .title 940 - // .as_ref() 941 - // .map(|t| SmolStr::new(t.as_ref())) 942 - // .unwrap_or_else(|| SmolStr::new("Untitled")); 943 - 944 - // let result = Arc::new((notebook, entries)); 945 - // // Cache it 946 - // cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 947 - // notebooks.push(result); 948 - // } 949 - // Err(_) => continue, // Skip notebooks that fail to load 950 - // } 951 - // } 952 - 953 - // Ok(notebooks) 954 - // } 955 - 956 - // pub async fn fetch_notebooks_for_did( 957 - // &self, 958 - // ident: &AtIdentifier<'_>, 959 - // ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 960 - // use jacquard::{ 961 - // IntoStatic, 962 - // types::{collection::Collection, nsid::Nsid}, 963 - // xrpc::XrpcExt, 964 - // }; 965 - // use weaver_api::{ 966 - // com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book, 967 - // }; 968 - 969 - // let client = self.get_client(); 970 - 971 - // // Resolve DID and PDS 972 - // let (repo_did, pds_url) = match ident { 973 - // AtIdentifier::Did(did) => { 974 - // let pds = client 975 - // .pds_for_did(did) 976 - // .await 977 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 978 - // (did.clone(), pds) 979 - // } 980 - // AtIdentifier::Handle(handle) => client 981 - // .pds_for_handle(handle) 982 - // .await 983 - // .map_err(|e| dioxus::CapturedError::from_display(e))?, 984 - // }; 985 - 986 - // // Fetch all notebook records for this repo 987 - // let resp = client 988 - // .xrpc(pds_url) 989 - // .send( 990 - // &ListRecords::new() 991 - // .repo(repo_did) 992 - // .collection(Nsid::raw(Book::NSID)) 993 - // .limit(100) 994 - // .build(), 995 - // ) 996 - // .await 997 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 998 - 999 - // let mut notebooks = Vec::new(); 1000 - 1001 - // if let Ok(list) = resp.parse() { 1002 - // for record in list.records { 1003 - // // View the notebook (which hydrates authors) 1004 - // match client.view_notebook(&record.uri).await { 1005 - // Ok((notebook, entries)) => { 1006 - // let ident = record.uri.authority().clone().into_static(); 1007 - // let title = notebook 1008 - // .title 1009 - // .as_ref() 1010 - // .map(|t| SmolStr::new(t.as_ref())) 1011 - // .unwrap_or_else(|| SmolStr::new("Untitled")); 1012 - 1013 - // let result = Arc::new((notebook, entries)); 1014 - // // Cache it 1015 - // cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 1016 - // notebooks.push(result); 1017 - // } 1018 - // Err(_) => continue, // Skip notebooks that fail to load 1019 - // } 1020 - // } 1021 - // } 1022 - 1023 - // Ok(notebooks) 1024 - // } 1025 - 1026 - // pub async fn list_notebook_entries( 1027 - // &self, 1028 - // ident: AtIdentifier<'static>, 1029 - // book_title: SmolStr, 1030 - // ) -> Result<Option<Vec<BookEntryView<'static>>>> { 1031 - // if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 1032 - // let (notebook, entries) = result.as_ref(); 1033 - // let mut book_entries = Vec::new(); 1034 - // let client = self.get_client(); 1035 - 1036 - // for index in 0..entries.len() { 1037 - // match client.view_entry(notebook, entries, index).await { 1038 - // Ok(book_entry) => book_entries.push(book_entry), 1039 - // Err(_) => continue, // Skip entries that fail to load 1040 - // } 1041 - // } 1042 - 1043 - // Ok(Some(book_entries)) 1044 - // } else { 1045 - // Ok(None) 1046 - // } 1047 - // } 1048 - 1049 - // pub async fn fetch_profile( 1050 - // &self, 1051 - // ident: &AtIdentifier<'_>, 1052 - // ) -> Result<Arc<ProfileDataView<'static>>> { 1053 - // use jacquard::IntoStatic; 1054 - 1055 - // let ident_static = ident.clone().into_static(); 1056 - 1057 - // if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 1058 - // return Ok(cached); 1059 - // } 1060 - 1061 - // let client = self.get_client(); 1062 - 1063 - // let did = match ident { 1064 - // AtIdentifier::Did(d) => d.clone(), 1065 - // AtIdentifier::Handle(h) => client 1066 - // .resolve_handle(h) 1067 - // .await 1068 - // .map_err(|e| dioxus::CapturedError::from_display(e))?, 1069 - // }; 1070 - 1071 - // let (_uri, profile_view) = client 1072 - // .hydrate_profile_view(&did) 1073 - // .await 1074 - // .map_err(|e| dioxus::CapturedError::from_display(e))?; 1075 - 1076 - // let result = Arc::new(profile_view); 1077 - // cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 1078 - 1079 - // Ok(result) 1080 - // } 1081 - // } 1082 896 1083 897 impl HttpClient for Fetcher { 1084 898 type Error = IdentityError;
+143 -45
crates/weaver-app/src/main.rs
··· 31 31 #[cfg(feature = "server")] 32 32 mod blobcache; 33 33 mod cache_impl; 34 - #[cfg(feature = "server")] 35 - mod og; 36 34 /// Define a components module that contains all shared components for our app. 37 35 mod components; 38 36 mod config; 39 37 mod data; 40 38 mod env; 41 39 mod fetch; 40 + #[cfg(feature = "server")] 41 + mod og; 42 42 mod record_utils; 43 43 mod service_worker; 44 44 /// Define a views module that contains the UI for all Layouts and Routes for our app. ··· 207 207 } 208 208 } 209 209 })) 210 + // .layer(axum::middleware::from_fn( 211 + // |request: Request, next: Next| async move { 212 + // let mut res = next.run(request).await; 213 + 214 + // // Cache all HTML responses 215 + // if res 216 + // .headers() 217 + // .get("content-type") 218 + // .and_then(|v| v.to_str().ok()) 219 + // .map(|t| t.contains("text/html")) 220 + // .unwrap_or(false) 221 + // { 222 + // res.headers_mut().insert( 223 + // http::header::CACHE_CONTROL, 224 + // "public, max-age=300".parse().unwrap(), 225 + // ); 226 + // } 227 + // res 228 + // }, 229 + // )) 210 230 }; 211 231 Ok(router) 212 232 }); ··· 432 452 entry_title: SmolStr, 433 453 ) -> Result<axum::response::Response> { 434 454 use axum::{ 435 - http::{header::{CACHE_CONTROL, CONTENT_TYPE}, StatusCode}, 455 + http::{ 456 + StatusCode, 457 + header::{CACHE_CONTROL, CONTENT_TYPE}, 458 + }, 436 459 response::IntoResponse, 437 460 }; 438 461 use weaver_api::sh_weaver::actor::ProfileDataViewInner; ··· 446 469 }; 447 470 448 471 // Fetch entry data 449 - let entry_result = fetcher.get_entry(at_ident.clone(), book_title.clone(), entry_title.into()).await; 472 + let entry_result = fetcher 473 + .get_entry(at_ident.clone(), book_title.clone(), entry_title.into()) 474 + .await; 450 475 451 476 let arc_data = match entry_result { 452 477 Ok(Some(data)) => data, ··· 470 495 (CACHE_CONTROL, "public, max-age=3600"), 471 496 ], 472 497 cached, 473 - ).into_response()); 498 + ) 499 + .into_response()); 474 500 } 475 501 476 502 // Extract metadata ··· 480 506 // TODO: Could fetch actual notebook record to get display title 481 507 let notebook_title_str: String = book_title.to_string(); 482 508 483 - let author_handle = book_entry.entry.authors.first() 509 + let author_handle = book_entry 510 + .entry 511 + .authors 512 + .first() 484 513 .map(|a| match &a.record.inner { 485 514 ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(), 486 515 ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(), ··· 504 533 // Build CDN URL 505 534 let cdn_url = format!( 506 535 "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", 507 - did.as_str(), cid.as_ref(), format 536 + did.as_str(), 537 + cid.as_ref(), 538 + format 508 539 ); 509 540 510 541 // Fetch the image ··· 513 544 match response.bytes().await { 514 545 Ok(bytes) => { 515 546 use base64::Engine; 516 - let base64_str = base64::engine::general_purpose::STANDARD.encode(&bytes); 547 + let base64_str = 548 + base64::engine::general_purpose::STANDARD.encode(&bytes); 517 549 Some(format!("data:{};base64,{}", mime, base64_str)) 518 550 } 519 - Err(_) => None 551 + Err(_) => None, 520 552 } 521 553 } 522 - _ => None 554 + _ => None, 523 555 } 524 556 } else { 525 557 None ··· 556 588 match og::generate_hero_image(hero_data, title, &notebook_title_str, &author_handle) { 557 589 Ok(bytes) => bytes, 558 590 Err(e) => { 559 - tracing::error!("Failed to generate hero OG image: {:?}, falling back to text", e); 591 + tracing::error!( 592 + "Failed to generate hero OG image: {:?}, falling back to text", 593 + e 594 + ); 560 595 og::generate_text_only(title, &content_snippet, &notebook_title_str, &author_handle) 561 596 .map_err(|e| { 562 597 tracing::error!("Failed to generate text OG image: {:?}", e); ··· 570 605 Ok(bytes) => bytes, 571 606 Err(e) => { 572 607 tracing::error!("Failed to generate OG image: {:?}", e); 573 - return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate image").into_response()); 608 + return Ok(( 609 + StatusCode::INTERNAL_SERVER_ERROR, 610 + "Failed to generate image", 611 + ) 612 + .into_response()); 574 613 } 575 614 } 576 615 }; ··· 584 623 (CACHE_CONTROL, "public, max-age=3600"), 585 624 ], 586 625 png_bytes, 587 - ).into_response()) 626 + ) 627 + .into_response()) 588 628 } 589 629 590 630 // Route: /og/notebook/{ident}/{book_title}.png - OpenGraph image for notebook index ··· 595 635 book_title: SmolStr, 596 636 ) -> Result<axum::response::Response> { 597 637 use axum::{ 598 - http::{header::{CACHE_CONTROL, CONTENT_TYPE}, StatusCode}, 638 + http::{ 639 + StatusCode, 640 + header::{CACHE_CONTROL, CONTENT_TYPE}, 641 + }, 599 642 response::IntoResponse, 600 643 }; 601 644 use weaver_api::sh_weaver::actor::ProfileDataViewInner; ··· 608 651 }; 609 652 610 653 // Fetch notebook data 611 - let notebook_result = fetcher.get_notebook(at_ident.clone(), book_title.into()).await; 654 + let notebook_result = fetcher 655 + .get_notebook(at_ident.clone(), book_title.into()) 656 + .await; 612 657 613 658 let arc_data = match notebook_result { 614 659 Ok(Some(data)) => data, 615 660 Ok(None) => return Ok((StatusCode::NOT_FOUND, "Notebook not found").into_response()), 616 661 Err(e) => { 617 662 tracing::error!("Failed to fetch notebook for OG image: {:?}", e); 618 - return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch notebook").into_response()); 663 + return Ok(( 664 + StatusCode::INTERNAL_SERVER_ERROR, 665 + "Failed to fetch notebook", 666 + ) 667 + .into_response()); 619 668 } 620 669 }; 621 670 let (notebook_view, _entries) = arc_data.as_ref(); ··· 632 681 (CACHE_CONTROL, "public, max-age=3600"), 633 682 ], 634 683 cached, 635 - ).into_response()); 684 + ) 685 + .into_response()); 636 686 } 637 687 638 688 // Extract metadata 639 - let title = notebook_view.title 689 + let title = notebook_view 690 + .title 640 691 .as_ref() 641 692 .map(|t| t.as_ref()) 642 693 .unwrap_or("Untitled Notebook"); 643 694 644 - let author_handle = notebook_view.authors.first() 695 + let author_handle = notebook_view 696 + .authors 697 + .first() 645 698 .map(|a| match &a.record.inner { 646 699 ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(), 647 700 ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(), ··· 651 704 .unwrap_or_else(|| "unknown".to_string()); 652 705 653 706 // Fetch entries to get entry titles and count 654 - let entries_result = fetcher.list_notebook_entries(at_ident.clone(), book_title.into()).await; 707 + let entries_result = fetcher 708 + .list_notebook_entries(at_ident.clone(), book_title.into()) 709 + .await; 655 710 let (entry_count, entry_titles) = match entries_result { 656 711 Ok(Some(entries)) => { 657 712 let count = entries.len(); ··· 659 714 .iter() 660 715 .take(4) 661 716 .map(|e| { 662 - e.entry.title 717 + e.entry 718 + .title 663 719 .as_ref() 664 720 .map(|t| t.as_ref().to_string()) 665 721 .unwrap_or_else(|| "Untitled".to_string()) ··· 671 727 }; 672 728 673 729 // Generate image 674 - let png_bytes = match og::generate_notebook_og(title, &author_handle, entry_count, entry_titles) { 730 + let png_bytes = match og::generate_notebook_og(title, &author_handle, entry_count, entry_titles) 731 + { 675 732 Ok(bytes) => bytes, 676 733 Err(e) => { 677 734 tracing::error!("Failed to generate notebook OG image: {:?}", e); 678 - return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate image").into_response()); 735 + return Ok(( 736 + StatusCode::INTERNAL_SERVER_ERROR, 737 + "Failed to generate image", 738 + ) 739 + .into_response()); 679 740 } 680 741 }; 681 742 ··· 688 749 (CACHE_CONTROL, "public, max-age=3600"), 689 750 ], 690 751 png_bytes, 691 - ).into_response()) 752 + ) 753 + .into_response()) 692 754 } 693 755 694 756 // Route: /og/profile/{ident}.png - OpenGraph image for profile/repository 695 757 #[cfg(all(feature = "fullstack-server", feature = "server"))] 696 758 #[get("/og/profile/{ident}", fetcher: Extension<Arc<fetch::Fetcher>>)] 697 - pub async fn og_profile_image( 698 - ident: SmolStr, 699 - ) -> Result<axum::response::Response> { 759 + pub async fn og_profile_image(ident: SmolStr) -> Result<axum::response::Response> { 700 760 use axum::{ 701 - http::{header::{CACHE_CONTROL, CONTENT_TYPE}, StatusCode}, 761 + http::{ 762 + StatusCode, 763 + header::{CACHE_CONTROL, CONTENT_TYPE}, 764 + }, 702 765 response::IntoResponse, 703 766 }; 704 767 use weaver_api::sh_weaver::actor::ProfileDataViewInner; ··· 717 780 Ok(data) => data, 718 781 Err(e) => { 719 782 tracing::error!("Failed to fetch profile for OG image: {:?}", e); 720 - return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch profile").into_response()); 783 + return Ok( 784 + (StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch profile").into_response(), 785 + ); 721 786 } 722 787 }; 723 788 ··· 725 790 // Use DID as cache key since profiles don't have a CID field 726 791 let (display_name, handle, bio, avatar_url, banner_url, cache_id) = match &profile_view.inner { 727 792 ProfileDataViewInner::ProfileView(p) => ( 728 - p.display_name.as_ref().map(|n| n.as_ref().to_string()).unwrap_or_default(), 793 + p.display_name 794 + .as_ref() 795 + .map(|n| n.as_ref().to_string()) 796 + .unwrap_or_default(), 729 797 p.handle.as_ref().to_string(), 730 - p.description.as_ref().map(|d| d.as_ref().to_string()).unwrap_or_default(), 798 + p.description 799 + .as_ref() 800 + .map(|d| d.as_ref().to_string()) 801 + .unwrap_or_default(), 731 802 p.avatar.as_ref().map(|u| u.as_ref().to_string()), 732 803 None::<String>, 733 804 p.did.as_ref().to_string(), 734 805 ), 735 806 ProfileDataViewInner::ProfileViewDetailed(p) => ( 736 - p.display_name.as_ref().map(|n| n.as_ref().to_string()).unwrap_or_default(), 807 + p.display_name 808 + .as_ref() 809 + .map(|n| n.as_ref().to_string()) 810 + .unwrap_or_default(), 737 811 p.handle.as_ref().to_string(), 738 - p.description.as_ref().map(|d| d.as_ref().to_string()).unwrap_or_default(), 812 + p.description 813 + .as_ref() 814 + .map(|d| d.as_ref().to_string()) 815 + .unwrap_or_default(), 739 816 p.avatar.as_ref().map(|u| u.as_ref().to_string()), 740 817 p.banner.as_ref().map(|u| u.as_ref().to_string()), 741 818 p.did.as_ref().to_string(), ··· 762 839 (CACHE_CONTROL, "public, max-age=3600"), 763 840 ], 764 841 cached, 765 - ).into_response()); 842 + ) 843 + .into_response()); 766 844 } 767 845 768 846 // Fetch notebook count ··· 828 906 ) { 829 907 Ok(bytes) => bytes, 830 908 Err(e) => { 831 - tracing::error!("Failed to generate profile banner OG image: {:?}, falling back", e); 832 - og::generate_profile_og(&display_name, &handle, &bio, avatar_data, notebook_count) 833 - .unwrap_or_default() 909 + tracing::error!( 910 + "Failed to generate profile banner OG image: {:?}, falling back", 911 + e 912 + ); 913 + og::generate_profile_og( 914 + &display_name, 915 + &handle, 916 + &bio, 917 + avatar_data, 918 + notebook_count, 919 + ) 920 + .unwrap_or_default() 834 921 } 835 922 } 836 923 } else { ··· 842 929 Ok(bytes) => bytes, 843 930 Err(e) => { 844 931 tracing::error!("Failed to generate profile OG image: {:?}", e); 845 - return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate image").into_response()); 932 + return Ok(( 933 + StatusCode::INTERNAL_SERVER_ERROR, 934 + "Failed to generate image", 935 + ) 936 + .into_response()); 846 937 } 847 938 } 848 939 }; ··· 856 947 (CACHE_CONTROL, "public, max-age=3600"), 857 948 ], 858 949 png_bytes, 859 - ).into_response()) 950 + ) 951 + .into_response()) 860 952 } 861 953 862 954 // Route: /og/site.png - OpenGraph image for homepage ··· 864 956 #[get("/og/site.png")] 865 957 pub async fn og_site_image() -> Result<axum::response::Response> { 866 958 use axum::{ 867 - http::{header::{CACHE_CONTROL, CONTENT_TYPE}, StatusCode}, 959 + http::{ 960 + StatusCode, 961 + header::{CACHE_CONTROL, CONTENT_TYPE}, 962 + }, 868 963 response::IntoResponse, 869 964 }; 870 965 871 966 // Site OG is static, cache aggressively 872 967 static SITE_OG_CACHE: std::sync::OnceLock<Vec<u8>> = std::sync::OnceLock::new(); 873 968 874 - let png_bytes = SITE_OG_CACHE.get_or_init(|| { 875 - og::generate_site_og().unwrap_or_default() 876 - }); 969 + let png_bytes = SITE_OG_CACHE.get_or_init(|| og::generate_site_og().unwrap_or_default()); 877 970 878 971 if png_bytes.is_empty() { 879 - return Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate image").into_response()); 972 + return Ok(( 973 + StatusCode::INTERNAL_SERVER_ERROR, 974 + "Failed to generate image", 975 + ) 976 + .into_response()); 880 977 } 881 978 882 979 Ok(( ··· 885 982 (CACHE_CONTROL, "public, max-age=86400"), 886 983 ], 887 984 png_bytes.clone(), 888 - ).into_response()) 985 + ) 986 + .into_response()) 889 987 } 890 988 891 989 // #[server(endpoint = "static_routes", output = server_fn::codec::Json)]