refactored app to work totally client-side wrt blob fetches and urls

Orual 9f6c28e4 4a293a67

+415 -92
+4 -1
Cargo.lock
··· 7242 7242 source = "registry+https://github.com/rust-lang/crates.io-index" 7243 7243 checksum = "54e4348c16a3d2e2a45437eff67efc5462b60443de76f61b5d0ed9111c626d9d" 7244 7244 dependencies = [ 7245 - "cc", 7246 7245 "indexed_db_futures", 7247 7246 "js-sys", 7248 7247 "once_cell", ··· 8644 8643 "dioxus-primitives", 8645 8644 "jacquard", 8646 8645 "jacquard-axum", 8646 + "js-sys", 8647 8647 "markdown-weaver", 8648 8648 "mime-sniffer", 8649 8649 "mini-moka", ··· 8651 8651 "serde_json", 8652 8652 "sqlite-wasm-rs", 8653 8653 "time", 8654 + "wasm-bindgen", 8655 + "wasm-bindgen-futures", 8654 8656 "weaver-api", 8655 8657 "weaver-common", 8656 8658 "weaver-renderer", 8659 + "web-sys", 8657 8660 ] 8658 8661 8659 8662 [[package]]
+17 -12
crates/weaver-app/Cargo.toml
··· 6 6 7 7 [features] 8 8 default = ["web"] 9 - # The feature that are only required for the web = ["dioxus/web"] build target should be optional and only enabled in the web = ["dioxus/web"] feature 10 - web = ["dioxus/web", "chrono/wasmbind"] 11 - # The feature that are only required for the desktop = ["dioxus/desktop"] build target should be optional and only enabled in the desktop = ["dioxus/desktop"] feature 9 + # Fullstack mode with SSR and server functions 10 + fullstack-server = ["dioxus/fullstack"] 11 + 12 + web = ["dioxus/web"] 12 13 desktop = ["dioxus/desktop"] 13 - # The feature that are only required for the mobile = ["dioxus/mobile"] build target should be optional and only enabled in the mobile = ["dioxus/mobile"] feature 14 14 mobile = ["dioxus/mobile"] 15 - # The feature that are only required for the server = ["dioxus/server"] build target should be optional and only enabled in the server = ["dioxus/server"] feature 16 - server = ["dioxus/server", "dep:jacquard-axum", "dep:axum"] 15 + server = [ "dioxus/server", "dep:jacquard-axum", "dep:axum"] 16 + 17 + 18 + 17 19 18 20 [dependencies] 19 21 dashmap = "6.1.0" 20 - dioxus = { version = "0.7.0", features = ["router", "fullstack"] } 22 + 23 + dioxus = { version = "0.7.0", features = ["router"] } 24 + #dioxus = { version = "0.7.0", features = ["router", "fullstack"] } 21 25 weaver-common = { path = "../weaver-common" } 22 26 jacquard = { workspace = true, features = ["streaming"] } 23 27 jacquard-axum = { workspace = true, optional = true } ··· 37 41 38 42 39 43 [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] 40 - sqlite-wasm-rs = { version = "0.4", features = ["relaxed-idb"] } 41 - 42 - 43 - 44 - [target.'cfg(target_arch = "wasm32")'.dependencies] 44 + sqlite-wasm-rs = { version = "0.4", default-features = false, features = ["precompiled", "relaxed-idb"] } 45 45 time = { version = "0.3", features = ["wasm-bindgen"] } 46 46 console_error_panic_hook = "0.1" 47 47 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d", features = ["js"] } 48 + chrono = { version = "0.4", features = ["wasmbind"] } 49 + wasm-bindgen = "0.2" 50 + wasm-bindgen-futures = "0.4" 51 + web-sys = { version = "0.3", features = ["ServiceWorkerContainer", "ServiceWorker", "ServiceWorkerRegistration", "RegistrationOptions", "Window", "Navigator", "MessageEvent", "console"] } 52 + js-sys = "0.3"
+1 -1
crates/weaver-app/Dioxus.toml
··· 12 12 style = [] 13 13 14 14 # Additional JavaScript files 15 - script = [] 15 + script = ["assets/sw.js"] 16 16 17 17 [web.resource.dev] 18 18
+92
crates/weaver-app/assets/sw.js
··· 1 + // Weaver Service Worker 2 + // Handles blob/image requests by intercepting fetch and serving from PDS 3 + 4 + const CACHE_NAME = "weaver-blobs-v1"; 5 + 6 + // Map of notebook/path -> real URL 7 + // e.g., "notebook/image/foo.jpg" -> "https://pds.example.com/xrpc/com.atproto.sync.getBlob?..." 8 + const urlMappings = new Map(); 9 + 10 + // Install and activate immediately 11 + self.addEventListener("install", (event) => { 12 + console.log("[SW] Installing service worker"); 13 + self.skipWaiting(); 14 + }); 15 + 16 + self.addEventListener("activate", (event) => { 17 + console.log("[SW] Activating service worker"); 18 + event.waitUntil(clients.claim()); 19 + }); 20 + 21 + // Receive mappings from main thread 22 + self.addEventListener("message", (event) => { 23 + if (event.data.type === "register_mappings") { 24 + const notebook = event.data.notebook; 25 + console.log("[SW] Registering blob mappings for notebook:", notebook); 26 + 27 + // Store blob URL mappings 28 + for (const [name, url] of Object.entries(event.data.blobs)) { 29 + const key = `${notebook}/image/${name}`; 30 + urlMappings.set(key, url); 31 + console.log("[SW] Registered mapping:", key, "->", url); 32 + } 33 + } 34 + }); 35 + 36 + // Intercept fetch requests 37 + self.addEventListener("fetch", (event) => { 38 + const url = new URL(event.request.url); 39 + 40 + // Extract key from path (e.g., "/notebook/image/foo.jpg" -> "notebook/image/foo.jpg") 41 + const pathParts = url.pathname.split("/").filter((p) => p); 42 + 43 + // Check if this looks like an image request (format: /:notebook/image/:name) 44 + if (pathParts.length >= 3 && pathParts[pathParts.length - 2] === "image") { 45 + // Reconstruct the key 46 + const key = pathParts.join("/"); 47 + 48 + console.log("[SW] Intercepted image request:", key); 49 + 50 + const mapping = urlMappings.get(key); 51 + if (mapping) { 52 + console.log("[SW] Found mapping for:", key, "->", mapping); 53 + event.respondWith(handleBlobRequest(mapping, key)); 54 + return; 55 + } else { 56 + console.log("[SW] No mapping found for:", key); 57 + } 58 + } 59 + 60 + // Let other requests pass through 61 + }); 62 + 63 + async function handleBlobRequest(url, key) { 64 + try { 65 + // Check cache first 66 + const cache = await caches.open(CACHE_NAME); 67 + let response = await cache.match(key); 68 + 69 + if (response) { 70 + console.log("[SW] Cache hit for:", key); 71 + return response; 72 + } 73 + 74 + // Fetch from PDS 75 + console.log("[SW] Fetching from PDS:", url); 76 + response = await fetch(url); 77 + 78 + if (!response.ok) { 79 + console.error("[SW] Fetch failed:", response.status, response.statusText); 80 + return new Response("Blob not found", { status: 404 }); 81 + } 82 + 83 + // Cache the response 84 + await cache.put(key, response.clone()); 85 + console.log("[SW] Cached blob:", key); 86 + 87 + return response; 88 + } catch (error) { 89 + console.error("[SW] Error handling blob request:", error); 90 + return new Response("Error fetching blob", { status: 500 }); 91 + } 92 + }
+5 -10
crates/weaver-app/src/blobcache.rs
··· 1 + use crate::cache_impl; 1 2 use dioxus::{CapturedError, Result}; 2 3 use jacquard::{ 3 4 bytes::Bytes, ··· 15 16 #[derive(Clone)] 16 17 pub struct BlobCache { 17 18 client: Arc<BasicClient>, 18 - cache: mini_moka::sync::Cache<Cid<'static>, Bytes>, 19 - map: mini_moka::sync::Cache<SmolStr, Cid<'static>>, 19 + cache: cache_impl::Cache<Cid<'static>, Bytes>, 20 + map: cache_impl::Cache<SmolStr, Cid<'static>>, 20 21 } 21 22 22 23 impl BlobCache { 23 24 pub fn new(client: Arc<BasicClient>) -> Self { 24 - let cache = mini_moka::sync::Cache::builder() 25 - .max_capacity(100) 26 - .time_to_idle(Duration::from_secs(1200)) 27 - .build(); 28 - let map = mini_moka::sync::Cache::builder() 29 - .max_capacity(500) 30 - .time_to_idle(Duration::from_secs(1200)) 31 - .build(); 25 + let cache = cache_impl::new_cache(100, Duration::from_secs(1200)); 26 + let map = cache_impl::new_cache(500, Duration::from_secs(1200)); 32 27 33 28 Self { client, cache, map } 34 29 }
+71 -9
crates/weaver-app/src/components/css.rs
··· 1 1 #[allow(unused_imports)] 2 2 use crate::fetch; 3 3 #[allow(unused_imports)] 4 - use dioxus::{ 5 - fullstack::{ 6 - get_server_url, 7 - headers::ContentType, 8 - http::header::CONTENT_TYPE, 9 - response::{self, Response}, 10 - }, 11 - prelude::*, 12 - CapturedError, 4 + use dioxus::{prelude::*, CapturedError}; 5 + 6 + #[cfg(feature = "fullstack-server")] 7 + use dioxus::fullstack::{ 8 + get_server_url, 9 + headers::ContentType, 10 + http::header::CONTENT_TYPE, 11 + response::{self, Response}, 13 12 }; 14 13 use jacquard::smol_str::SmolStr; 15 14 #[allow(unused_imports)] ··· 20 19 #[cfg(feature = "server")] 21 20 use axum::{extract::Extension, response::IntoResponse}; 22 21 22 + #[cfg(feature = "fullstack-server")] 23 23 #[component] 24 24 pub fn NotebookCss(ident: SmolStr, notebook: SmolStr) -> Element { 25 25 rsx! { ··· 29 29 } 30 30 } 31 31 32 + #[cfg(not(feature = "fullstack-server"))] 33 + #[component] 34 + pub fn NotebookCss(ident: SmolStr, notebook: SmolStr) -> Element { 35 + use jacquard::client::AgentSessionExt; 36 + use jacquard::types::ident::AtIdentifier; 37 + use jacquard::{from_data, CowStr}; 38 + use weaver_api::sh_weaver::notebook::book::Book; 39 + use weaver_renderer::css::{generate_base_css, generate_syntax_css}; 40 + use weaver_renderer::theme::{default_resolved_theme, resolve_theme}; 41 + 42 + let fetcher = use_context::<fetch::CachedFetcher>(); 43 + 44 + let css_content = use_resource(move || { 45 + let ident = ident.clone(); 46 + let notebook = notebook.clone(); 47 + let fetcher = fetcher.clone(); 48 + 49 + async move { 50 + let ident = AtIdentifier::new_owned(ident).ok()?; 51 + let resolved_theme = 52 + if let Some(notebook) = fetcher.get_notebook(ident, notebook).await.ok()? { 53 + let book: Book = from_data(&notebook.0.record).ok()?; 54 + if let Some(theme_ref) = book.theme { 55 + if let Ok(theme_response) = 56 + fetcher.client.get_record::<Theme>(&theme_ref.uri).await 57 + { 58 + if let Ok(theme_output) = theme_response.into_output() { 59 + let theme: Theme = theme_output.into(); 60 + resolve_theme(fetcher.client.as_ref(), &theme) 61 + .await 62 + .unwrap_or_else(|_| default_resolved_theme()) 63 + } else { 64 + default_resolved_theme() 65 + } 66 + } else { 67 + default_resolved_theme() 68 + } 69 + } else { 70 + default_resolved_theme() 71 + } 72 + } else { 73 + default_resolved_theme() 74 + }; 75 + 76 + let mut css = generate_base_css(&resolved_theme); 77 + css.push_str( 78 + &generate_syntax_css(&resolved_theme) 79 + .await 80 + .unwrap_or_default(), 81 + ); 82 + 83 + Some(css) 84 + } 85 + }); 86 + 87 + match css_content() { 88 + Some(Some(css)) => rsx! { document::Style { {css} } }, 89 + _ => rsx! {}, 90 + } 91 + } 92 + 93 + #[cfg(feature = "fullstack-server")] 32 94 #[get("/{ident}/{notebook}/css", fetcher: Extension<Arc<fetch::CachedFetcher>>)] 33 95 pub async fn css(ident: SmolStr, notebook: SmolStr) -> Result<Response> { 34 96 use jacquard::client::AgentSessionExt;
+36 -16
crates/weaver-app/src/components/entry.rs
··· 11 11 use dioxus::prelude::*; 12 12 13 13 const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css"); 14 + #[cfg(feature = "fullstack-server")] 15 + use dioxus::{fullstack::extract::Extension, CapturedError}; 16 + use jacquard::prelude::*; 14 17 #[allow(unused_imports)] 15 - use dioxus::{fullstack::extract::Extension, CapturedError}; 16 - use jacquard::{ 17 - from_data, prelude::IdentityResolver, smol_str::ToSmolStr, types::string::Datetime, 18 - }; 18 + use jacquard::smol_str::ToSmolStr; 19 + use jacquard::{from_data, types::string::Datetime}; 19 20 #[allow(unused_imports)] 20 21 use jacquard::{ 21 22 smol_str::SmolStr, ··· 32 33 let entry = use_resource(use_reactive!(|(ident, book_title, title)| async move { 33 34 let fetcher = use_context::<fetch::CachedFetcher>(); 34 35 let entry = fetcher 35 - .get_entry(ident.clone(), book_title, title) 36 + .get_entry(ident.clone(), book_title.clone(), title) 36 37 .await 37 38 .ok() 38 39 .flatten(); 39 - if let Some(entry) = &entry { 40 - let entry = &entry.1; 41 - if let Some(embeds) = &entry.embeds { 40 + if let Some(entry_data) = &entry { 41 + let entry_record = &entry_data.1; 42 + if let Some(embeds) = &entry_record.embeds { 42 43 if let Some(images) = &embeds.images { 43 - for image in &images.images { 44 - let cid = image.image.blob().cid(); 45 - cache_blob( 46 - ident.to_smolstr(), 47 - cid.to_smolstr(), 48 - image.name.as_ref().map(|n| n.to_smolstr()), 44 + // Register blob mappings with service worker (client-side only) 45 + #[cfg(all( 46 + target_family = "wasm", 47 + target_os = "unknown", 48 + not(feature = "fullstack-server") 49 + ))] 50 + { 51 + let _ = crate::service_worker::register_entry_blobs( 52 + &ident, 53 + book_title.as_str(), 54 + images, 55 + &fetcher, 49 56 ) 50 - .await 51 - .ok(); 57 + .await; 58 + } 59 + #[cfg(feature = "fullstack-server")] 60 + { 61 + for image in &images.images { 62 + let cid = image.image.blob().cid(); 63 + cache_blob( 64 + ident.to_smolstr(), 65 + cid.to_smolstr(), 66 + image.name.as_ref().map(|n| n.to_smolstr()), 67 + ) 68 + .await 69 + .ok(); 70 + } 52 71 } 53 72 } 54 73 } ··· 461 480 } 462 481 } 463 482 483 + #[cfg(feature = "fullstack-server")] 464 484 #[put("/cache/{ident}/{cid}?name", cache: Extension<Arc<BlobCache>>)] 465 485 pub async fn cache_blob(ident: SmolStr, cid: SmolStr, name: Option<SmolStr>) -> Result<()> { 466 486 let ident = AtIdentifier::new_owned(ident)?;
+74 -43
crates/weaver-app/src/main.rs
··· 2 2 // need dioxus 3 3 use components::{Entry, Repository, RepositoryIndex}; 4 4 #[allow(unused)] 5 - use dioxus::{ 6 - fullstack::{response::Extension, FullstackContext}, 7 - prelude::*, 8 - CapturedError, 9 - }; 5 + use dioxus::{prelude::*, CapturedError}; 6 + 7 + #[cfg(feature = "fullstack-server")] 8 + use dioxus::fullstack::{response::Extension, FullstackContext}; 10 9 #[allow(unused)] 11 10 use jacquard::{ 12 11 client::BasicClient, ··· 24 23 /// Define a components module that contains all shared components for our app. 25 24 mod components; 26 25 mod fetch; 26 + mod service_worker; 27 27 /// Define a views module that contains the UI for all Layouts and Routes for our app. 28 28 mod views; 29 29 ··· 62 62 // The asset macro also minifies some assets like CSS and JS to make bundled smaller 63 63 const MAIN_CSS: Asset = asset!("/assets/styling/main.css"); 64 64 65 + #[cfg(not(feature = "fullstack-server"))] 66 + #[cfg(feature = "server")] 67 + async fn serve_sw() -> impl axum::response::IntoResponse { 68 + use axum::response::IntoResponse; 69 + let sw_js = include_str!("../assets/sw.js"); 70 + ( 71 + [(axum::http::header::CONTENT_TYPE, "application/javascript")], 72 + sw_js, 73 + ) 74 + .into_response() 75 + } 76 + 65 77 fn main() { 66 78 // Set up better panic messages for wasm 67 79 #[cfg(target_arch = "wasm32")] ··· 76 88 extract::{Extension, Request}, 77 89 middleware, 78 90 middleware::Next, 91 + routing::get, 79 92 }; 80 93 use std::convert::Infallible; 81 94 use std::sync::Arc; 82 95 83 - // Create shared state 84 - let fetcher = Arc::new(CachedFetcher::new(Arc::new(BasicClient::unauthenticated()))); 85 - let blob_cache = Arc::new(BlobCache::new(Arc::new(BasicClient::unauthenticated()))); 96 + #[cfg(not(feature = "fullstack-server"))] 97 + let router = { 98 + axum::Router::new() 99 + .route("/sw.js", get(serve_sw)) 100 + .merge(dioxus::server::router(App)) 101 + }; 86 102 87 - // Create a new router for our app using the `router` function 88 - let router = dioxus::server::router(App).layer(middleware::from_fn({ 89 - let fetcher = fetcher.clone(); 90 - let blob_cache = blob_cache.clone(); 91 - move |mut req: Request, next: Next| { 103 + #[cfg(feature = "fullstack-server")] 104 + let router = { 105 + let fetcher = Arc::new(CachedFetcher::new(Arc::new(BasicClient::unauthenticated()))); 106 + let blob_cache = Arc::new(BlobCache::new(Arc::new(BasicClient::unauthenticated()))); 107 + dioxus::server::router(App).layer(middleware::from_fn({ 92 108 let fetcher = fetcher.clone(); 93 109 let blob_cache = blob_cache.clone(); 94 - async move { 95 - // Attach extensions for dioxus server functions 96 - req.extensions_mut().insert(fetcher); 97 - req.extensions_mut().insert(blob_cache); 110 + move |mut req: Request, next: Next| { 111 + let fetcher = fetcher.clone(); 112 + let blob_cache = blob_cache.clone(); 113 + async move { 114 + // Attach extensions for dioxus server functions 115 + req.extensions_mut().insert(fetcher); 116 + req.extensions_mut().insert(blob_cache); 98 117 99 - // And then return the response with `next.run() 100 - Ok::<_, Infallible>(next.run(req).await) 118 + // And then return the response with `next.run() 119 + Ok::<_, Infallible>(next.run(req).await) 120 + } 101 121 } 102 - } 103 - })); 122 + })) 123 + }; 104 124 // And then return the router 105 125 Ok(router) 106 126 }); ··· 118 138 fn App() -> Element { 119 139 // The `rsx!` macro lets us define HTML inside of rust. It expands to an Element with all of our HTML inside. 120 140 use_context_provider(|| fetch::CachedFetcher::new(Arc::new(BasicClient::unauthenticated()))); 141 + 142 + // Register service worker on startup (only on web) 143 + #[cfg(all( 144 + target_family = "wasm", 145 + target_os = "unknown", 146 + not(feature = "fullstack-server") 147 + ))] 148 + use_effect(move || { 149 + spawn(async move { 150 + let _ = service_worker::register_service_worker().await; 151 + }); 152 + }); 153 + 121 154 rsx! { 122 155 document::Link { rel: "icon", href: FAVICON } 123 156 document::Link { rel: "stylesheet", href: MAIN_CSS } ··· 134 167 rsx! { 135 168 ErrorBoundary { 136 169 handle_error: move |err: ErrorContext| { 137 - let http_error = FullstackContext::commit_error_status(err.error().unwrap()); 138 - match http_error.status { 139 - StatusCode::NOT_FOUND => rsx! { div { "404 - Page not found" } }, 140 - _ => rsx! { div { "An unknown error occurred" } }, 170 + #[cfg(feature = "fullstack-server")] 171 + { 172 + let http_error = FullstackContext::commit_error_status(err.error().unwrap()); 173 + match http_error.status { 174 + StatusCode::NOT_FOUND => rsx! { div { "404 - Page not found" } }, 175 + _ => rsx! { div { "An unknown error occurred" } }, 176 + } 177 + } 178 + #[cfg(not(feature = "fullstack-server"))] 179 + { 180 + rsx! { div { "An error occurred" } } 141 181 } 142 182 }, 143 183 Outlet::<Route> {} ··· 145 185 } 146 186 } 147 187 188 + #[cfg(all(feature = "fullstack-server", feature = "server"))] 148 189 #[get("/{notebook}/image/{name}", blob_cache: Extension<Arc<crate::blobcache::BlobCache>>)] 149 - pub async fn image_named( 150 - notebook: SmolStr, 151 - name: SmolStr, 152 - ) -> Result<dioxus_fullstack::response::Response> { 153 - use axum::response::IntoResponse; 190 + pub async fn image_named(notebook: SmolStr, name: SmolStr) -> Result<axum::response::Response> { 191 + use axum::{http::header::CONTENT_TYPE, response::IntoResponse}; 154 192 use mime_sniffer::MimeTypeSniffer; 155 193 if let Some(bytes) = blob_cache.get_named(&name) { 156 194 let blob = bytes.clone(); 157 - let mime = blob.sniff_mime_type().unwrap_or("application/octet-stream"); 158 - Ok(( 159 - [(dioxus_fullstack::http::header::CONTENT_TYPE, mime)], 160 - bytes, 161 - ) 162 - .into_response()) 195 + let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); 196 + Ok(([(CONTENT_TYPE, mime)], bytes).into_response()) 163 197 } else { 164 198 Err(CapturedError::from_display("no image")) 165 199 } 166 200 } 167 201 202 + #[cfg(all(feature = "fullstack-server", feature = "server"))] 168 203 #[get("/{notebook}/blob/{cid}", blob_cache: Extension<Arc<crate::blobcache::BlobCache>>)] 169 - pub async fn blob(notebook: SmolStr, cid: SmolStr) -> Result<dioxus_fullstack::response::Response> { 170 - use axum::response::IntoResponse; 204 + pub async fn blob(notebook: SmolStr, cid: SmolStr) -> Result<axum::response::Response> { 205 + use axum::{http::header::CONTENT_TYPE, response::IntoResponse}; 171 206 use mime_sniffer::MimeTypeSniffer; 172 207 if let Some(bytes) = blob_cache.get_cid(&Cid::new_owned(cid.as_bytes())?) { 173 208 let blob = bytes.clone(); 174 209 let mime = blob.sniff_mime_type().unwrap_or("application/octet-stream"); 175 - Ok(( 176 - [(dioxus_fullstack::http::header::CONTENT_TYPE, mime)], 177 - bytes, 178 - ) 179 - .into_response()) 210 + Ok(([(CONTENT_TYPE, mime)], bytes).into_response()) 180 211 } else { 181 212 Err(CapturedError::from_display("no blob")) 182 213 }
+115
crates/weaver-app/src/service_worker.rs
··· 1 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 2 + use wasm_bindgen::prelude::*; 3 + 4 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 5 + use wasm_bindgen_futures::JsFuture; 6 + 7 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 8 + use web_sys::{RegistrationOptions, ServiceWorkerContainer, Window}; 9 + 10 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 11 + pub async fn register_service_worker() -> Result<(), JsValue> { 12 + let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; 13 + let navigator = window.navigator(); 14 + let sw_container = navigator.service_worker(); 15 + 16 + let promise = sw_container.register("/sw.js"); 17 + JsFuture::from(promise).await?; 18 + 19 + Ok(()) 20 + } 21 + 22 + /// Register blob mappings from entry images with the service worker 23 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 24 + pub async fn register_entry_blobs( 25 + ident: &jacquard::types::ident::AtIdentifier<'_>, 26 + book_title: &str, 27 + images: &weaver_api::sh_weaver::embed::images::Images<'_>, 28 + fetcher: &crate::fetch::CachedFetcher, 29 + ) -> Result<(), JsValue> { 30 + use jacquard::prelude::IdentityResolver; 31 + use std::collections::HashMap; 32 + 33 + let mut blob_mappings = HashMap::new(); 34 + 35 + // Resolve DID and PDS URL 36 + let (did, pds_url) = match ident { 37 + jacquard::types::ident::AtIdentifier::Did(d) => { 38 + let pds = fetcher.client.pds_for_did(d).await.ok(); 39 + (d.clone(), pds) 40 + } 41 + jacquard::types::ident::AtIdentifier::Handle(h) => { 42 + if let Ok((did, pds)) = fetcher.client.pds_for_handle(h).await { 43 + (did, Some(pds)) 44 + } else { 45 + return Ok(()); 46 + } 47 + } 48 + }; 49 + 50 + if let Some(pds_url) = pds_url { 51 + for image in &images.images { 52 + let cid = image.image.blob().cid(); 53 + 54 + if let Some(name) = &image.name { 55 + let blob_url = format!( 56 + "{}xrpc/com.atproto.sync.getBlob?did={}&cid={}", 57 + pds_url.as_str(), 58 + did.as_ref(), 59 + cid.as_ref() 60 + ); 61 + blob_mappings.insert(name.as_ref().to_string(), blob_url); 62 + } 63 + } 64 + 65 + // Send mappings to service worker 66 + if !blob_mappings.is_empty() { 67 + send_blob_mappings(book_title, blob_mappings)?; 68 + } 69 + } 70 + 71 + Ok(()) 72 + } 73 + 74 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 75 + fn send_blob_mappings( 76 + notebook: &str, 77 + mappings: std::collections::HashMap<String, String>, 78 + ) -> Result<(), JsValue> { 79 + let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; 80 + let navigator = window.navigator(); 81 + let sw_container = navigator.service_worker(); 82 + 83 + let controller = sw_container 84 + .controller() 85 + .ok_or_else(|| JsValue::from_str("no service worker controller"))?; 86 + 87 + // Build message object 88 + let msg = js_sys::Object::new(); 89 + js_sys::Reflect::set(&msg, &"type".into(), &"register_mappings".into())?; 90 + js_sys::Reflect::set(&msg, &"notebook".into(), &notebook.into())?; 91 + 92 + // Convert HashMap to JS Object 93 + let blobs_obj = js_sys::Object::new(); 94 + for (name, url) in mappings { 95 + js_sys::Reflect::set(&blobs_obj, &name.into(), &url.into())?; 96 + } 97 + js_sys::Reflect::set(&msg, &"blobs".into(), &blobs_obj)?; 98 + 99 + controller.post_message(&msg)?; 100 + 101 + Ok(()) 102 + } 103 + 104 + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 105 + pub async fn register_service_worker() -> Result<(), String> { 106 + Ok(()) 107 + } 108 + 109 + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 110 + pub fn send_blob_mappings( 111 + _notebook: &str, 112 + _mappings: std::collections::HashMap<String, String>, 113 + ) -> Result<(), String> { 114 + Ok(()) 115 + }