···184 doc.commit();
185186 // Pre-warm blob cache for images
0187 if let Some(ref embeds) = loaded.entry.embeds {
188 if let Some(ref images) = embeds.images {
189 let ident: &str = match uri.authority() {
···491 paras
492 });
493494- // Background fetch for AT embeds
495- let mut resolved_content_for_fetch = resolved_content.clone();
496- let doc_for_embeds = document.clone();
497- let fetcher_for_embeds = fetcher.clone();
498- use_effect(move || {
499- let refs = doc_for_embeds.collected_refs.read();
500- let current_resolved = resolved_content_for_fetch.peek();
501- let fetcher = fetcher_for_embeds.clone();
00502503- // Find AT embeds that need fetching
504- let to_fetch: Vec<String> = refs
505- .iter()
506- .filter_map(|r| match r {
507- weaver_common::ExtractedRef::AtEmbed { uri, .. } => {
508- // Skip if already resolved
509- if let Ok(at_uri) = jacquard::types::string::AtUri::new(uri) {
510- if current_resolved.get_embed_content(&at_uri).is_none() {
511- return Some(uri.clone());
000000000512 }
00000513 }
514- None
00515 }
516- _ => None,
517- })
518- .collect();
0519520- if to_fetch.is_empty() {
521- return;
522- }
000523524- // Spawn background fetches
525- dioxus::prelude::spawn(async move {
526- for uri_str in to_fetch {
527- let Ok(at_uri) = jacquard::types::string::AtUri::new(&uri_str) else {
528- continue;
529- };
530531- match weaver_renderer::atproto::fetch_and_render(&at_uri, &fetcher).await {
532- Ok(html) => {
533- let mut rc = resolved_content_for_fetch.write();
534- rc.add_embed(at_uri.into_static(), html, None);
00000000535 }
536- Err(e) => {
537- tracing::warn!("failed to fetch embed {}: {}", uri_str, e);
000000000000000000000000000000000000000000000000000000000000538 }
539 }
540- }
541 });
542- });
543544 let mut new_tag = use_signal(String::new);
545
···184 doc.commit();
185186 // Pre-warm blob cache for images
187+ #[cfg(feature = "fullstack-server")]
188 if let Some(ref embeds) = loaded.entry.embeds {
189 if let Some(ref images) = embeds.images {
190 let ident: &str = match uri.authority() {
···492 paras
493 });
494495+ // Background fetch for AT embeds via worker
496+ #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
497+ {
498+ use super::worker::{EmbedWorker, EmbedWorkerInput, EmbedWorkerOutput};
499+ use dioxus::prelude::Writable;
500+ use gloo_worker::Spawnable;
501+502+ let resolved_content_for_fetch = resolved_content;
503+ let mut embed_worker_bridge: Signal<Option<gloo_worker::WorkerBridge<EmbedWorker>>> =
504+ use_signal(|| None);
505506+ // Spawn embed worker on mount
507+ let doc_for_embeds = document.clone();
508+ use_effect(move || {
509+ // Callback for worker responses - uses write_unchecked since we're in a Fn closure
510+ let on_output = move |output: EmbedWorkerOutput| match output {
511+ EmbedWorkerOutput::Embeds {
512+ results,
513+ errors,
514+ fetch_ms,
515+ } => {
516+ if !results.is_empty() {
517+ let mut rc = resolved_content_for_fetch.write_unchecked();
518+ for (uri_str, html) in results {
519+ if let Ok(at_uri) =
520+ jacquard::types::string::AtUri::new_owned(uri_str)
521+ {
522+ rc.add_embed(at_uri, html, None);
523+ }
524 }
525+ tracing::debug!(
526+ count = rc.embed_content.len(),
527+ fetch_ms,
528+ "embed worker fetched embeds"
529+ );
530 }
531+ for (uri, err) in errors {
532+ tracing::warn!("embed worker failed to fetch {}: {}", uri, err);
533+ }
534 }
535+ EmbedWorkerOutput::CacheCleared => {
536+ tracing::debug!("embed worker cache cleared");
537+ }
538+ };
539540+ let bridge = EmbedWorker::spawner()
541+ .callback(on_output)
542+ .spawn("/embed_worker.js");
543+ embed_worker_bridge.set(Some(bridge));
544+ tracing::info!("Embed worker spawned");
545+ });
546547+ // Send embeds to worker when collected_refs changes
548+ use_effect(move || {
549+ let refs = doc_for_embeds.collected_refs.read();
550+ let current_resolved = resolved_content_for_fetch.peek();
00551552+ // Find AT embeds that need fetching
553+ let to_fetch: Vec<String> = refs
554+ .iter()
555+ .filter_map(|r| match r {
556+ weaver_common::ExtractedRef::AtEmbed { uri, .. } => {
557+ // Skip if already resolved
558+ if let Ok(at_uri) = jacquard::types::string::AtUri::new(uri) {
559+ if current_resolved.get_embed_content(&at_uri).is_none() {
560+ return Some(uri.clone());
561+ }
562+ }
563+ None
564 }
565+ _ => None,
566+ })
567+ .collect();
568+569+ if to_fetch.is_empty() {
570+ return;
571+ }
572+573+ // Send to worker
574+ if let Some(ref bridge) = *embed_worker_bridge.peek() {
575+ bridge.send(EmbedWorkerInput::FetchEmbeds { uris: to_fetch });
576+ }
577+ });
578+ }
579+580+ // Fallback for non-WASM (server-side rendering)
581+ #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
582+ {
583+ let mut resolved_content_for_fetch = resolved_content.clone();
584+ let doc_for_embeds = document.clone();
585+ let fetcher_for_embeds = fetcher.clone();
586+ use_effect(move || {
587+ let refs = doc_for_embeds.collected_refs.read();
588+ let current_resolved = resolved_content_for_fetch.peek();
589+ let fetcher = fetcher_for_embeds.clone();
590+591+ // Find AT embeds that need fetching
592+ let to_fetch: Vec<String> = refs
593+ .iter()
594+ .filter_map(|r| match r {
595+ weaver_common::ExtractedRef::AtEmbed { uri, .. } => {
596+ // Skip if already resolved
597+ if let Ok(at_uri) = jacquard::types::string::AtUri::new(uri) {
598+ if current_resolved.get_embed_content(&at_uri).is_none() {
599+ return Some(uri.clone());
600+ }
601+ }
602+ None
603+ }
604+ _ => None,
605+ })
606+ .collect();
607+608+ if to_fetch.is_empty() {
609+ return;
610+ }
611+612+ // Spawn background fetches (main thread fallback)
613+ dioxus::prelude::spawn(async move {
614+ for uri_str in to_fetch {
615+ let Ok(at_uri) = jacquard::types::string::AtUri::new(&uri_str) else {
616+ continue;
617+ };
618+619+ match weaver_renderer::atproto::fetch_and_render(&at_uri, &fetcher).await {
620+ Ok(html) => {
621+ let mut rc = resolved_content_for_fetch.write();
622+ rc.add_embed(at_uri.into_static(), html, None);
623+ }
624+ Err(e) => {
625+ tracing::warn!("failed to fetch embed {}: {}", uri_str, e);
626+ }
627 }
628 }
629+ });
630 });
631+ }
632633 let mut new_tag = use_signal(String::new);
634
+3
crates/weaver-app/src/components/editor/sync.rs
···67 let embeds_map = doc.get_map("embeds");
6869 // Pre-warm blob cache for images
070 if let Some(ident) = owner_ident {
71 if let Ok(images_container) =
72 embeds_map.get_or_create_container("images", loro::LoroList::new())
···97 }
98 }
99 }
00100101 // Strategy 1: Get embeds from Loro embeds map -> records list
102
···67 let embeds_map = doc.get_map("embeds");
6869 // Pre-warm blob cache for images
70+ #[cfg(feature = "fullstack-server")]
71 if let Some(ident) = owner_ident {
72 if let Ok(images_container) =
73 embeds_map.get_or_create_container("images", loro::LoroList::new())
···98 }
99 }
100 }
101+ #[cfg(not(feature = "fullstack-server"))]
102+ let _ = owner_ident;
103104 // Strategy 1: Get embeds from Loro embeds map -> records list
105