···184184 doc.commit();
185185186186 // Pre-warm blob cache for images
187187+ #[cfg(feature = "fullstack-server")]
187188 if let Some(ref embeds) = loaded.entry.embeds {
188189 if let Some(ref images) = embeds.images {
189190 let ident: &str = match uri.authority() {
···491492 paras
492493 });
493494494494- // Background fetch for AT embeds
495495- let mut resolved_content_for_fetch = resolved_content.clone();
496496- let doc_for_embeds = document.clone();
497497- let fetcher_for_embeds = fetcher.clone();
498498- use_effect(move || {
499499- let refs = doc_for_embeds.collected_refs.read();
500500- let current_resolved = resolved_content_for_fetch.peek();
501501- let fetcher = fetcher_for_embeds.clone();
495495+ // Background fetch for AT embeds via worker
496496+ #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
497497+ {
498498+ use super::worker::{EmbedWorker, EmbedWorkerInput, EmbedWorkerOutput};
499499+ use dioxus::prelude::Writable;
500500+ use gloo_worker::Spawnable;
501501+502502+ let resolved_content_for_fetch = resolved_content;
503503+ let mut embed_worker_bridge: Signal<Option<gloo_worker::WorkerBridge<EmbedWorker>>> =
504504+ use_signal(|| None);
502505503503- // Find AT embeds that need fetching
504504- let to_fetch: Vec<String> = refs
505505- .iter()
506506- .filter_map(|r| match r {
507507- weaver_common::ExtractedRef::AtEmbed { uri, .. } => {
508508- // Skip if already resolved
509509- if let Ok(at_uri) = jacquard::types::string::AtUri::new(uri) {
510510- if current_resolved.get_embed_content(&at_uri).is_none() {
511511- return Some(uri.clone());
506506+ // Spawn embed worker on mount
507507+ let doc_for_embeds = document.clone();
508508+ use_effect(move || {
509509+ // Callback for worker responses - uses write_unchecked since we're in a Fn closure
510510+ let on_output = move |output: EmbedWorkerOutput| match output {
511511+ EmbedWorkerOutput::Embeds {
512512+ results,
513513+ errors,
514514+ fetch_ms,
515515+ } => {
516516+ if !results.is_empty() {
517517+ let mut rc = resolved_content_for_fetch.write_unchecked();
518518+ for (uri_str, html) in results {
519519+ if let Ok(at_uri) =
520520+ jacquard::types::string::AtUri::new_owned(uri_str)
521521+ {
522522+ rc.add_embed(at_uri, html, None);
523523+ }
512524 }
525525+ tracing::debug!(
526526+ count = rc.embed_content.len(),
527527+ fetch_ms,
528528+ "embed worker fetched embeds"
529529+ );
513530 }
514514- None
531531+ for (uri, err) in errors {
532532+ tracing::warn!("embed worker failed to fetch {}: {}", uri, err);
533533+ }
515534 }
516516- _ => None,
517517- })
518518- .collect();
535535+ EmbedWorkerOutput::CacheCleared => {
536536+ tracing::debug!("embed worker cache cleared");
537537+ }
538538+ };
519539520520- if to_fetch.is_empty() {
521521- return;
522522- }
540540+ let bridge = EmbedWorker::spawner()
541541+ .callback(on_output)
542542+ .spawn("/embed_worker.js");
543543+ embed_worker_bridge.set(Some(bridge));
544544+ tracing::info!("Embed worker spawned");
545545+ });
523546524524- // Spawn background fetches
525525- dioxus::prelude::spawn(async move {
526526- for uri_str in to_fetch {
527527- let Ok(at_uri) = jacquard::types::string::AtUri::new(&uri_str) else {
528528- continue;
529529- };
547547+ // Send embeds to worker when collected_refs changes
548548+ use_effect(move || {
549549+ let refs = doc_for_embeds.collected_refs.read();
550550+ let current_resolved = resolved_content_for_fetch.peek();
530551531531- match weaver_renderer::atproto::fetch_and_render(&at_uri, &fetcher).await {
532532- Ok(html) => {
533533- let mut rc = resolved_content_for_fetch.write();
534534- rc.add_embed(at_uri.into_static(), html, None);
552552+ // Find AT embeds that need fetching
553553+ let to_fetch: Vec<String> = refs
554554+ .iter()
555555+ .filter_map(|r| match r {
556556+ weaver_common::ExtractedRef::AtEmbed { uri, .. } => {
557557+ // Skip if already resolved
558558+ if let Ok(at_uri) = jacquard::types::string::AtUri::new(uri) {
559559+ if current_resolved.get_embed_content(&at_uri).is_none() {
560560+ return Some(uri.clone());
561561+ }
562562+ }
563563+ None
535564 }
536536- Err(e) => {
537537- tracing::warn!("failed to fetch embed {}: {}", uri_str, e);
565565+ _ => None,
566566+ })
567567+ .collect();
568568+569569+ if to_fetch.is_empty() {
570570+ return;
571571+ }
572572+573573+ // Send to worker
574574+ if let Some(ref bridge) = *embed_worker_bridge.peek() {
575575+ bridge.send(EmbedWorkerInput::FetchEmbeds { uris: to_fetch });
576576+ }
577577+ });
578578+ }
579579+580580+ // Fallback for non-WASM (server-side rendering)
581581+ #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
582582+ {
583583+ let mut resolved_content_for_fetch = resolved_content.clone();
584584+ let doc_for_embeds = document.clone();
585585+ let fetcher_for_embeds = fetcher.clone();
586586+ use_effect(move || {
587587+ let refs = doc_for_embeds.collected_refs.read();
588588+ let current_resolved = resolved_content_for_fetch.peek();
589589+ let fetcher = fetcher_for_embeds.clone();
590590+591591+ // Find AT embeds that need fetching
592592+ let to_fetch: Vec<String> = refs
593593+ .iter()
594594+ .filter_map(|r| match r {
595595+ weaver_common::ExtractedRef::AtEmbed { uri, .. } => {
596596+ // Skip if already resolved
597597+ if let Ok(at_uri) = jacquard::types::string::AtUri::new(uri) {
598598+ if current_resolved.get_embed_content(&at_uri).is_none() {
599599+ return Some(uri.clone());
600600+ }
601601+ }
602602+ None
603603+ }
604604+ _ => None,
605605+ })
606606+ .collect();
607607+608608+ if to_fetch.is_empty() {
609609+ return;
610610+ }
611611+612612+ // Spawn background fetches (main thread fallback)
613613+ dioxus::prelude::spawn(async move {
614614+ for uri_str in to_fetch {
615615+ let Ok(at_uri) = jacquard::types::string::AtUri::new(&uri_str) else {
616616+ continue;
617617+ };
618618+619619+ match weaver_renderer::atproto::fetch_and_render(&at_uri, &fetcher).await {
620620+ Ok(html) => {
621621+ let mut rc = resolved_content_for_fetch.write();
622622+ rc.add_embed(at_uri.into_static(), html, None);
623623+ }
624624+ Err(e) => {
625625+ tracing::warn!("failed to fetch embed {}: {}", uri_str, e);
626626+ }
538627 }
539628 }
540540- }
629629+ });
541630 });
542542- });
631631+ }
543632544633 let mut new_tag = use_signal(String::new);
545634
+3
crates/weaver-app/src/components/editor/sync.rs
···6767 let embeds_map = doc.get_map("embeds");
68686969 // Pre-warm blob cache for images
7070+ #[cfg(feature = "fullstack-server")]
7071 if let Some(ident) = owner_ident {
7172 if let Ok(images_container) =
7273 embeds_map.get_or_create_container("images", loro::LoroList::new())
···9798 }
9899 }
99100 }
101101+ #[cfg(not(feature = "fullstack-server"))]
102102+ let _ = owner_ident;
100103101104 // Strategy 1: Get embeds from Loro embeds map -> records list
102105