diff --git a/crates/weaver-app/src/blobcache.rs b/crates/weaver-app/src/blobcache.rs index f13b20b..66f1404 100644 --- a/crates/weaver-app/src/blobcache.rs +++ b/crates/weaver-app/src/blobcache.rs @@ -58,9 +58,9 @@ impl BlobCache { pds_url: jacquard::url::Url, cid: &Cid<'_>, ) -> Result { - if let Ok(blob_stream) = self + match self .client - .xrpc(pds_url) + .xrpc(pds_url.clone()) .send( &GetBlob::new() .cid(cid.clone()) @@ -69,17 +69,25 @@ impl BlobCache { ) .await { - Ok(blob_stream.buffer().clone()) - } else { - // Fallback to Bluesky CDN (works for blobs stored on bsky PDSes) - let bytes = reqwest::get(format!( - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@jpeg", - did, cid - )) - .await? - .bytes() - .await?; - Ok(bytes) + Ok(blob_stream) => Ok(blob_stream.buffer().clone()), + Err(e) => { + tracing::warn!( + did = %did, + cid = %cid, + pds = %pds_url, + error = %e, + "PDS blob fetch failed, falling back to Bluesky CDN" + ); + // Fallback to Bluesky CDN (works for blobs stored on bsky PDSes) + let bytes = reqwest::get(format!( + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@jpeg", + did, cid + )) + .await? + .bytes() + .await?; + Ok(bytes) + } } } diff --git a/crates/weaver-app/src/components/editor/actions.rs b/crates/weaver-app/src/components/editor/actions.rs index 26fb0d0..58e0246 100644 --- a/crates/weaver-app/src/components/editor/actions.rs +++ b/crates/weaver-app/src/components/editor/actions.rs @@ -1266,15 +1266,6 @@ pub enum KeydownResult { /// This handles keyboard shortcuts only. Text input and deletion /// are handled by beforeinput. Navigation (arrows, etc.) is passed /// through to the browser. -/// -/// # Arguments -/// * `doc` - The editor document -/// * `config` - Keybinding configuration -/// * `combo` - The key combination from the keyboard event -/// * `range` - Current cursor position / selection range -/// -/// # Returns -/// Whether the event was handled. pub fn handle_keydown_with_bindings( doc: &mut EditorDocument, config: &KeybindingConfig, diff --git a/crates/weaver-app/src/components/editor/cursor.rs b/crates/weaver-app/src/components/editor/cursor.rs index 9316f8d..03749ef 100644 --- a/crates/weaver-app/src/components/editor/cursor.rs +++ b/crates/weaver-app/src/components/editor/cursor.rs @@ -13,18 +13,6 @@ use super::offset_map::{OffsetMapping, SnapDirection, find_mapping_for_char, fin use wasm_bindgen::JsCast; /// Restore cursor position in the DOM after re-render. -/// -/// # Arguments -/// - `char_offset`: Cursor position as char offset in document -/// - `offset_map`: Mappings from source to DOM positions -/// - `editor_id`: DOM ID of the contenteditable element -/// - `snap_direction`: Optional direction hint for snapping from invisible content -/// -/// # Algorithm -/// 1. Find offset mapping containing char_offset -/// 2. Get DOM node by mapping.node_id -/// 3. Walk text nodes to find UTF-16 position -/// 4. Set cursor with Selection API #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub fn restore_cursor_position( char_offset: usize, diff --git a/crates/weaver-app/src/components/editor/document.rs b/crates/weaver-app/src/components/editor/document.rs index 2c55a12..16f5baa 100644 --- a/crates/weaver-app/src/components/editor/document.rs +++ b/crates/weaver-app/src/components/editor/document.rs @@ -319,10 +319,6 @@ impl EditorDocument { /// /// MUST be called from within a reactive context (e.g., `use_hook`) to /// properly initialize Dioxus Signals. - /// - /// # Arguments - /// * `entry` - The entry record fetched from PDS - /// * `entry_ref` - StrongRef to the entry (URI + CID) pub fn from_entry(entry: &Entry<'_>, entry_ref: StrongRef<'static>) -> Self { let mut doc = Self::new(entry.content.to_string()); @@ -691,9 +687,7 @@ impl EditorDocument { Ok(()) } - /// Undo the last operation. - /// Returns true if an undo was performed. - /// Automatically updates cursor position from the Loro cursor. + /// Undo the last operation. Automatically updates cursor position. pub fn undo(&mut self) -> LoroResult { // Sync Loro cursor to current position BEFORE undo // so it tracks through the undo operation @@ -709,9 +703,7 @@ impl EditorDocument { Ok(result) } - /// Redo the last undone operation. - /// Returns true if a redo was performed. - /// Automatically updates cursor position from the Loro cursor. + /// Redo the last undone operation. Automatically updates cursor position. pub fn redo(&mut self) -> LoroResult { // Sync Loro cursor to current position BEFORE redo self.sync_loro_cursor(); diff --git a/crates/weaver-app/src/components/editor/dom_sync.rs b/crates/weaver-app/src/components/editor/dom_sync.rs index e9297f2..7c66d47 100644 --- a/crates/weaver-app/src/components/editor/dom_sync.rs +++ b/crates/weaver-app/src/components/editor/dom_sync.rs @@ -71,7 +71,6 @@ pub fn sync_cursor_from_dom_with_direction( let anchor_offset = selection.anchor_offset() as usize; let focus_offset = selection.focus_offset() as usize; - // Convert both DOM positions to rope offsets using cached paragraphs let anchor_rope = dom_position_to_text_offset( &dom_document, &editor_element, @@ -93,13 +92,11 @@ pub fn sync_cursor_from_dom_with_direction( (Some(anchor), Some(focus)) => { doc.cursor.write().offset = focus; if anchor != focus { - // There's an actual selection doc.selection.set(Some(Selection { anchor, head: focus, })); } else { - // Collapsed selection (just cursor) doc.selection.set(None); } } @@ -158,7 +155,6 @@ pub fn dom_position_to_text_offset( let node_id = node_id?; - // Get the container element let container = dom_document.get_element_by_id(&node_id).or_else(|| { let selector = format!("[data-node-id='{}']", node_id); dom_document.query_selector(&selector).ok().flatten() @@ -180,7 +176,6 @@ pub fn dom_position_to_text_offset( } } - // Look up in offset maps for para in paragraphs { for mapping in ¶.offset_map { if mapping.node_id == node_id { @@ -231,7 +226,6 @@ pub fn sync_cursor_from_dom( _editor_id: &str, _paragraphs: &[ParagraphRender], ) { - // No-op on non-wasm } #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] @@ -241,7 +235,6 @@ pub fn sync_cursor_from_dom_with_direction( _paragraphs: &[ParagraphRender], _direction_hint: Option, ) { - // No-op on non-wasm } /// Update paragraph DOM elements incrementally. @@ -284,12 +277,10 @@ pub fn update_paragraph_dom( let mut cursor_para_updated = false; - // Update or create paragraphs for (idx, new_para) in new_paragraphs.iter().enumerate() { let para_id = format!("para-{}", idx); if let Some(old_para) = old_paragraphs.get(idx) { - // Paragraph exists - check if changed if new_para.source_hash != old_para.source_hash { // Changed - clear and update innerHTML // We clear first to ensure any browser-added content (from IME composition, @@ -299,21 +290,17 @@ pub fn update_paragraph_dom( elem.set_inner_html(&new_para.html); } - // Track if we updated the cursor's paragraph if Some(idx) == cursor_para_idx { cursor_para_updated = true; } } - // Unchanged - do nothing, browser preserves cursor } else { - // New paragraph - create div if let Ok(div) = document.create_element("div") { div.set_id(¶_id); div.set_inner_html(&new_para.html); let _ = editor.append_child(&div); } - // Track if we created the cursor's paragraph if Some(idx) == cursor_para_idx { cursor_para_updated = true; } diff --git a/crates/weaver-app/src/components/editor/offset_map.rs b/crates/weaver-app/src/components/editor/offset_map.rs index a809cab..9df5498 100644 --- a/crates/weaver-app/src/components/editor/offset_map.rs +++ b/crates/weaver-app/src/components/editor/offset_map.rs @@ -181,14 +181,6 @@ impl SnappedPosition<'_> { /// If the position is already valid, returns it directly. Otherwise, /// searches in the preferred direction first, falling back to the other /// direction if needed. -/// -/// # Arguments -/// - `offset_map`: The offset mappings for the paragraph -/// - `char_offset`: The target char offset -/// - `preferred_direction`: Which direction to search first when snapping -/// -/// # Returns -/// The snapped position, or None if no valid position exists. pub fn find_nearest_valid_position( offset_map: &[OffsetMapping], char_offset: usize, diff --git a/crates/weaver-app/src/components/editor/publish.rs b/crates/weaver-app/src/components/editor/publish.rs index 9a8c0fc..4a63ac0 100644 --- a/crates/weaver-app/src/components/editor/publish.rs +++ b/crates/weaver-app/src/components/editor/publish.rs @@ -62,8 +62,6 @@ impl PublishResult { } /// Result of fetching an entry for editing. -/// Contains the entry data and URI, but NOT an EditorDocument. -/// The document must be created in a reactive context (use_hook) to properly initialize Signals. #[derive(Clone, PartialEq)] pub struct LoadedEntry { pub entry: Entry<'static>, @@ -71,16 +69,6 @@ pub struct LoadedEntry { } /// Fetch an existing entry from the PDS for editing. -/// -/// Returns the entry data and URI. The caller should create an `EditorDocument` -/// from this data using `EditorDocument::from_entry()` inside a reactive context. -/// -/// # Arguments -/// * `fetcher` - The fetcher for making API calls -/// * `uri` - The AT-URI of the entry to load (e.g., `at://did:plc:xxx/sh.weaver.notebook.entry/rkey`) -/// -/// # Returns -/// The entry and its URI, or an error. pub async fn load_entry_for_editing( fetcher: &Fetcher, uri: &AtUri<'_>, @@ -154,19 +142,8 @@ pub async fn load_entry_for_editing( /// - Without notebook but with entry_uri in doc: uses `put_record` to update existing /// - Without notebook and no entry_uri: uses `create_record` for free-floating entry /// -/// Draft image paths (`/image/{did}/draft/{blob_rkey}/{name}`) are rewritten to -/// published paths (`/image/{did}/{entry_rkey}/{name}`) before publishing. -/// +/// Draft image paths are rewritten to published paths before publishing. /// On successful create, sets `doc.entry_uri` so subsequent publishes update the same record. -/// -/// # Arguments -/// * `fetcher` - The authenticated fetcher/client -/// * `doc` - The editor document containing entry data (mutable to update entry_uri) -/// * `notebook_title` - Optional title of the notebook to publish to -/// * `draft_key` - Storage key for the draft (for cleanup) -/// -/// # Returns -/// The AT-URI of the created/updated entry, or an error. pub async fn publish_entry( fetcher: &Fetcher, doc: &mut EditorDocument, diff --git a/crates/weaver-app/src/components/editor/render.rs b/crates/weaver-app/src/components/editor/render.rs index fa74d9b..2ac3c5b 100644 --- a/crates/weaver-app/src/components/editor/render.rs +++ b/crates/weaver-app/src/components/editor/render.rs @@ -104,15 +104,6 @@ fn adjust_paragraph_positions( /// /// Uses cached paragraph renders when possible, only re-rendering changed paragraphs. /// For "safe" edits (no boundary changes), skips boundary rediscovery entirely. -/// -/// # Arguments -/// - `text`: The document text to render -/// - `cache`: Previous render cache (if any) -/// - `edit`: Information about the most recent edit (if any) -/// - `image_resolver`: Optional resolver for mapping image URLs to data/CDN URLs -/// -/// # Returns -/// Tuple of (rendered paragraphs, updated cache) pub fn render_paragraphs_incremental( text: &LoroText, cache: Option<&RenderCache>, diff --git a/crates/weaver-app/src/components/editor/storage.rs b/crates/weaver-app/src/components/editor/storage.rs index 330aff0..ace433e 100644 --- a/crates/weaver-app/src/components/editor/storage.rs +++ b/crates/weaver-app/src/components/editor/storage.rs @@ -71,10 +71,6 @@ fn storage_key(key: &str) -> String { } /// Save editor state to LocalStorage (WASM only). -/// -/// # Arguments -/// * `doc` - The editor document to save -/// * `key` - Storage key (e.g., "new:abc123" for new entries, or AT-URI for existing) #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub fn save_to_storage( doc: &EditorDocument, @@ -103,9 +99,6 @@ pub fn save_to_storage( /// /// Returns an EditorDocument restored from CRDT snapshot if available, /// otherwise falls back to just the text content. -/// -/// # Arguments -/// * `key` - Storage key (e.g., "new:abc123" for new entries, or AT-URI for existing) #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub fn load_from_storage(key: &str) -> Option { let snapshot: EditorSnapshot = LocalStorage::get(storage_key(key)).ok()?; @@ -158,9 +151,6 @@ pub struct LocalSnapshotData { /// /// Unlike `load_from_storage`, this doesn't create an EditorDocument and is safe /// to call outside of reactive context. Use with `load_and_merge_document`. -/// -/// # Arguments -/// * `key` - Storage key (e.g., "new:abc123" for new entries, or AT-URI for existing) #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub fn load_snapshot_from_storage(key: &str) -> Option { let snapshot: EditorSnapshot = LocalStorage::get(storage_key(key)).ok()?; @@ -195,9 +185,6 @@ pub fn load_snapshot_from_storage(_key: &str) -> Option { } /// Delete a draft from LocalStorage (WASM only). -/// -/// # Arguments -/// * `key` - Storage key to delete #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub fn delete_draft(key: &str) { LocalStorage::delete(storage_key(key)); diff --git a/crates/weaver-app/src/components/editor/sync.rs b/crates/weaver-app/src/components/editor/sync.rs index 5da037b..6695b81 100644 --- a/crates/weaver-app/src/components/editor/sync.rs +++ b/crates/weaver-app/src/components/editor/sync.rs @@ -178,13 +178,6 @@ pub async fn find_diffs_for_root( /// /// Uploads the current Loro snapshot as a blob and creates an `sh.weaver.edit.root` /// record referencing the entry (or draft key if unpublished). -/// -/// # Arguments -/// * `fetcher` - The authenticated fetcher -/// * `doc` - The editor document -/// * `draft_key` - The draft key (used for unpublished entries) -/// * `entry_uri` - Optional AT-URI of the published entry -/// * `entry_cid` - Optional CID of the published entry pub async fn create_edit_root( fetcher: &Fetcher, doc: &EditorDocument, @@ -244,16 +237,6 @@ pub async fn create_edit_root( } /// Create a diff record with updates since the last sync. -/// -/// # Arguments -/// * `fetcher` - The authenticated fetcher -/// * `doc` - The editor document -/// * `root_uri` - URI of the edit root -/// * `root_cid` - CID of the edit root -/// * `prev_diff` - Optional reference to the previous diff -/// * `draft_key` - The draft key (used for doc reference) -/// * `entry_uri` - Optional AT-URI of the published entry -/// * `entry_cid` - Optional CID of the published entry pub async fn create_diff( fetcher: &Fetcher, doc: &EditorDocument, @@ -352,16 +335,7 @@ pub async fn create_diff( /// /// If no edit root exists, creates one with a full snapshot. /// If a root exists, creates a diff with updates since last sync. -/// /// Updates the document's sync state on success. -/// -/// # Arguments -/// * `fetcher` - The authenticated fetcher -/// * `doc` - The editor document (mutable to update sync state) -/// * `draft_key` - The draft key for this document -/// -/// # Returns -/// The sync result indicating what was created. pub async fn sync_to_pds( fetcher: &Fetcher, doc: &mut EditorDocument, @@ -480,13 +454,6 @@ async fn fetch_blob(fetcher: &Fetcher, did: &Did<'_>, cid: &Cid<'_>) -> Result, @@ -500,9 +467,9 @@ pub async fn load_edit_state_from_pds( // Build root URI let root_uri = AtUri::new(&format!( "at://{}/{}/{}", - root_id.did(), + root_id.did, ROOT_NSID, - root_id.rkey().as_ref() + root_id.rkey.as_ref() )) .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid root URI: {}", e)))? .into_static(); @@ -530,7 +497,7 @@ pub async fn load_edit_state_from_pds( // Fetch the root snapshot blob let root_snapshot = fetch_blob( fetcher, - &root_id.did(), + &root_id.did, root_output.value.snapshot.blob().cid(), ) .await?; @@ -555,11 +522,10 @@ pub async fn load_edit_state_from_pds( > = BTreeMap::new(); for diff_id in &diff_ids { - let rkey = diff_id.rkey(); - let rkey_str: &str = rkey.as_ref(); + let rkey_str: &str = diff_id.rkey.as_ref(); let diff_uri = AtUri::new(&format!( "at://{}/{}/{}", - diff_id.did(), + diff_id.did, DIFF_NSID, rkey_str )) @@ -600,7 +566,7 @@ pub async fn load_edit_state_from_pds( let diff_bytes = if let Some(ref inline) = diff.inline_diff { inline.clone() } else if let Some(ref snapshot) = diff.snapshot { - fetch_blob(fetcher, &root_id.did(), snapshot.blob().cid()).await? + fetch_blob(fetcher, &root_id.did, snapshot.blob().cid()).await? } else { tracing::warn!("Diff has neither inline_diff nor snapshot, skipping"); continue; @@ -622,22 +588,9 @@ pub async fn load_edit_state_from_pds( /// Load document state by merging local storage and PDS state. /// -/// This is the main entry point for loading a document with full sync support. -/// It: -/// 1. Loads from localStorage (if available) -/// 2. Loads from PDS (if available) -/// 3. Merges both using Loro's CRDT merge -/// -/// The result is a `LoadedDocState` containing a pre-merged LoroDoc that can be -/// converted to an EditorDocument inside a reactive context using `use_hook`. -/// -/// # Arguments -/// * `fetcher` - The authenticated fetcher -/// * `draft_key` - The localStorage key for this draft -/// * `entry_uri` - Optional AT-URI if editing an existing entry -/// -/// # Returns -/// A `LoadedDocState` with merged state, or None if no state exists anywhere. +/// Loads from localStorage and PDS (if available), then merges both using Loro's +/// CRDT merge. The result is a pre-merged LoroDoc that can be converted to an +/// EditorDocument inside a reactive context using `use_hook`. pub async fn load_and_merge_document( fetcher: &Fetcher, draft_key: &str, diff --git a/crates/weaver-app/src/components/editor/visibility.rs b/crates/weaver-app/src/components/editor/visibility.rs index db9b9b7..fbc9b2b 100644 --- a/crates/weaver-app/src/components/editor/visibility.rs +++ b/crates/weaver-app/src/components/editor/visibility.rs @@ -18,12 +18,6 @@ pub struct VisibilityState { impl VisibilityState { /// Calculate visibility based on cursor position and selection. - /// - /// # Arguments - /// - `cursor_offset`: Current cursor position (char offset) - /// - `selection`: Optional selection range - /// - `syntax_spans`: All syntax spans in the document - /// - `paragraphs`: All paragraphs (for block-level visibility lookup) pub fn calculate( cursor_offset: usize, selection: Option<&Selection>, diff --git a/crates/weaver-app/src/components/editor/writer.rs b/crates/weaver-app/src/components/editor/writer.rs index bf354b6..861c832 100644 --- a/crates/weaver-app/src/components/editor/writer.rs +++ b/crates/weaver-app/src/components/editor/writer.rs @@ -176,20 +176,11 @@ impl EditorImageResolver { } /// Add a pending image with a data URL for immediate preview. - /// - /// # Arguments - /// * `name` - The image name used in markdown (e.g., "photo.jpg") - /// * `data_url` - The base64 data URL for preview pub fn add_pending(&mut self, name: String, data_url: String) { self.images.insert(name, ResolvedImage::Pending(data_url)); } /// Promote a pending image to uploaded (draft) status. - /// - /// # Arguments - /// * `name` - The image name used in markdown - /// * `blob_rkey` - The rkey of the PublishedBlob record - /// * `ident` - The AT identifier (DID or handle) of the blob owner pub fn promote_to_uploaded( &mut self, name: &str, @@ -203,11 +194,6 @@ impl EditorImageResolver { } /// Add an already-uploaded draft image. - /// - /// # Arguments - /// * `name` - The name/URL used in markdown (e.g., "photo.jpg") - /// * `blob_rkey` - The rkey of the PublishedBlob record - /// * `ident` - The AT identifier (DID or handle) of the blob owner pub fn add_uploaded( &mut self, name: String, @@ -219,11 +205,6 @@ impl EditorImageResolver { } /// Add a published image. - /// - /// # Arguments - /// * `name` - The name/URL used in markdown (e.g., "photo.jpg") - /// * `entry_rkey` - The rkey of the entry record containing this image - /// * `ident` - The AT identifier (DID or handle) of the entry owner pub fn add_published( &mut self, name: String, @@ -241,12 +222,6 @@ impl EditorImageResolver { /// Build a resolver from editor images and user identifier. /// - /// # Arguments - /// * `images` - Iterator of editor images - /// * `ident` - The AT identifier (DID or handle) of the user - /// * `entry_rkey` - If Some, images are resolved as published (`/image/{ident}/{entry_rkey}/{name}`). - /// If None, images are resolved as drafts using their `published_blob_uri`. - /// /// For draft mode (entry_rkey=None), only images with a `published_blob_uri` are included. /// For published mode (entry_rkey=Some), all images are included. pub fn from_images<'a>( diff --git a/crates/weaver-app/src/data.rs b/crates/weaver-app/src/data.rs index b50f80f..186df29 100644 --- a/crates/weaver-app/src/data.rs +++ b/crates/weaver-app/src/data.rs @@ -101,7 +101,7 @@ pub fn use_entry_data( book_title: ReadSignal, title: ReadSignal, ) -> ( - Resource>, + Resource, Entry<'static>)>>, Memo, Entry<'static>)>>, ) { let fetcher = use_context::(); @@ -134,27 +134,13 @@ pub fn use_entry_data( } } } - Some(( - serde_json::to_value(entry.0.clone()).unwrap(), - serde_json::to_value(entry.1.clone()).unwrap(), - )) + Some(entry) } else { None } } }); - let memo = use_memo(move || { - if let Some(Some((ev, e))) = &*res.read() { - use jacquard::from_json_value; - - let book_entry = from_json_value::(ev.clone()).unwrap(); - let entry = from_json_value::(e.clone()).unwrap(); - - Some((book_entry, entry)) - } else { - None - } - }); + let memo = use_memo(move || res.read().clone().flatten()); (res, memo) } @@ -496,7 +482,7 @@ pub fn use_profile_data( pub fn use_profile_data( ident: ReadSignal>, ) -> ( - Resource>, + Resource>>, Memo>>, ) { let fetcher = use_context::(); @@ -507,17 +493,10 @@ pub fn use_profile_data( .fetch_profile(&ident()) .await .ok() - .map(|arc| serde_json::to_value(&*arc).ok()) - .flatten() - } - }); - let memo = use_memo(move || { - if let Some(Some(value)) = &*res.read() { - jacquard::from_json_value::(value.clone()).ok() - } else { - None + .map(|arc| (*arc).clone()) } }); + let memo = use_memo(move || res.read().clone().flatten()); (res, memo) } @@ -567,7 +546,7 @@ pub fn use_notebooks_for_did( pub fn use_notebooks_for_did( ident: ReadSignal>, ) -> ( - Resource>>, + Resource, Vec>)>>>, Memo, Vec>)>>>, ) { let fetcher = use_context::(); @@ -581,24 +560,12 @@ pub fn use_notebooks_for_did( .map(|notebooks| { notebooks .iter() - .map(|arc| serde_json::to_value(arc.as_ref()).ok()) - .collect::>>() - }) - .flatten() - } - }); - let memo = use_memo(move || { - if let Some(Some(values)) = &*res.read() { - values - .iter() - .map(|v| { - jacquard::from_json_value::<(NotebookView, Vec)>(v.clone()).ok() + .map(|arc| arc.as_ref().clone()) + .collect::>() }) - .collect::>>() - } else { - None } }); + let memo = use_memo(move || res.read().clone().flatten()); (res, memo) } @@ -644,7 +611,7 @@ pub fn use_notebooks_from_ufos() -> ( /// Fetches notebooks from UFOS client-side only (no SSR) #[cfg(not(feature = "fullstack-server"))] pub fn use_notebooks_from_ufos() -> ( - Resource>>, + Resource, Vec>)>>>, Memo, Vec>)>>>, ) { let fetcher = use_context::(); @@ -658,24 +625,12 @@ pub fn use_notebooks_from_ufos() -> ( .map(|notebooks| { notebooks .iter() - .map(|arc| serde_json::to_value(arc.as_ref()).ok()) - .collect::>>() - }) - .flatten() - } - }); - let memo = use_memo(move || { - if let Some(Some(values)) = &*res.read() { - values - .iter() - .map(|v| { - jacquard::from_json_value::<(NotebookView, Vec)>(v.clone()).ok() + .map(|arc| arc.as_ref().clone()) + .collect::>() }) - .collect::>>() - } else { - None } }); + let memo = use_memo(move || res.read().clone().flatten()); (res, memo) } @@ -718,7 +673,7 @@ pub fn use_notebook( ident: ReadSignal>, book_title: ReadSignal, ) -> ( - Resource>, + Resource, Vec>)>>, Memo, Vec>)>>, ) { let fetcher = use_context::(); @@ -730,17 +685,10 @@ pub fn use_notebook( .await .ok() .flatten() - .map(|arc| serde_json::to_value(arc.as_ref()).ok()) - .flatten() + .map(|arc| arc.as_ref().clone()) } }); - let memo = use_memo(use_reactive!(|res| { - if let Some(Some(value)) = &*res.read() { - jacquard::from_json_value::<(NotebookView, Vec)>(value.clone()).ok() - } else { - None - } - })); + let memo = use_memo(move || res.read().clone().flatten()); (res, memo) } diff --git a/crates/weaver-app/src/main.rs b/crates/weaver-app/src/main.rs index 5cf7b0e..109039d 100644 --- a/crates/weaver-app/src/main.rs +++ b/crates/weaver-app/src/main.rs @@ -321,187 +321,124 @@ pub async fn favicon() -> axum::response::Response { ([(CONTENT_TYPE, "image/jpg")], bytes).into_response() } +/// Build an image response with appropriate headers for immutable blobs. #[cfg(all(feature = "fullstack-server", feature = "server"))] -#[get("/{notebook}/image/{name}", blob_cache: Extension>)] -pub async fn image_named(notebook: SmolStr, name: SmolStr) -> Result { +fn build_image_response(bytes: jacquard::bytes::Bytes) -> axum::response::Response { use axum::{ http::header::{CACHE_CONTROL, CONTENT_TYPE}, response::IntoResponse, }; use mime_sniffer::MimeTypeSniffer; + + let mime = bytes.sniff_mime_type().unwrap_or("image/jpg").to_string(); + ( + [ + (CONTENT_TYPE, mime), + ( + CACHE_CONTROL, + "public, max-age=31536000, immutable".to_string(), + ), + ], + bytes, + ) + .into_response() +} + +/// Return a 404 response for missing images. +#[cfg(all(feature = "fullstack-server", feature = "server"))] +fn image_not_found() -> axum::response::Response { + use axum::{http::StatusCode, response::IntoResponse}; + (StatusCode::NOT_FOUND, "Image not found").into_response() +} + +#[cfg(all(feature = "fullstack-server", feature = "server"))] +#[get("/{_notebook}/image/{name}", blob_cache: Extension>)] +pub async fn image_named(_notebook: SmolStr, name: SmolStr) -> Result { if let Some(bytes) = blob_cache.get_named(&name) { - let blob = bytes.clone(); - let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); - // Blobs are immutable by CID - cache aggressively - Ok(( - [ - (CONTENT_TYPE, mime), - (CACHE_CONTROL, "public, max-age=31536000, immutable"), - ], - bytes, - ) - .into_response()) + Ok(build_image_response(bytes)) } else { - Err(CapturedError::from_display("no image")) + Ok(image_not_found()) } } #[cfg(all(feature = "fullstack-server", feature = "server"))] -#[get("/{notebook}/blob/{cid}", blob_cache: Extension>)] -pub async fn blob(notebook: SmolStr, cid: SmolStr) -> Result { - use axum::{ - http::header::{CACHE_CONTROL, CONTENT_TYPE}, - response::IntoResponse, - }; - use mime_sniffer::MimeTypeSniffer; - if let Some(bytes) = blob_cache.get_cid(&Cid::new_owned(cid.as_bytes())?) { - let blob = bytes.clone(); - let mime = blob.sniff_mime_type().unwrap_or("application/octet-stream"); - // Blobs are immutable by CID - cache aggressively - Ok(( - [ - (CONTENT_TYPE, mime), - (CACHE_CONTROL, "public, max-age=31536000, immutable"), - ], - bytes, - ) - .into_response()) - } else { - Err(CapturedError::from_display("no blob")) +#[get("/{_notebook}/blob/{cid}", blob_cache: Extension>)] +pub async fn blob(_notebook: SmolStr, cid: SmolStr) -> Result { + match Cid::new_owned(cid.as_bytes()) { + Ok(cid) => { + if let Some(bytes) = blob_cache.get_cid(&cid) { + Ok(build_image_response(bytes)) + } else { + Ok(image_not_found()) + } + } + Err(_) => Ok(image_not_found()), } } -// New image routes with unified /image/ prefix // Route: /image/{notebook}/{name} - notebook entry image by name #[cfg(all(feature = "fullstack-server", feature = "server"))] #[get("/image/{notebook}/{name}", blob_cache: Extension>)] pub async fn image_notebook(notebook: SmolStr, name: SmolStr) -> Result { - use axum::{ - http::header::{CACHE_CONTROL, CONTENT_TYPE}, - response::IntoResponse, - }; - use mime_sniffer::MimeTypeSniffer; - // Try name-based lookup first (backwards compat with cached entries) if let Some(bytes) = blob_cache.get_named(&name) { - let blob = bytes.clone(); - let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); - return Ok(( - [ - (CONTENT_TYPE, mime), - (CACHE_CONTROL, "public, max-age=31536000, immutable"), - ], - bytes, - ) - .into_response()); + return Ok(build_image_response(bytes)); } - + // Try to resolve from notebook match blob_cache.resolve_from_notebook(¬ebook, &name).await { - Ok(bytes) => { - let blob = bytes.clone(); - let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); - Ok(( - [ - (CONTENT_TYPE, mime), - (CACHE_CONTROL, "public, max-age=31536000, immutable"), - ], - bytes, - ) - .into_response()) - } - Err(e) => Err(e), + Ok(bytes) => Ok(build_image_response(bytes)), + Err(_) => Ok(image_not_found()), } } // Route: /image/{ident}/draft/{blob_rkey} - draft image (unpublished) -// Route: /image/{ident}/draft/{blob_rkey}/{name} - draft image with name #[cfg(all(feature = "fullstack-server", feature = "server"))] #[get("/image/{ident}/draft/{blob_rkey}", blob_cache: Extension>)] pub async fn image_draft(ident: SmolStr, blob_rkey: SmolStr) -> Result { - use axum::{ - http::header::{CACHE_CONTROL, CONTENT_TYPE}, - response::IntoResponse, + let Ok(at_ident) = AtIdentifier::new_owned(ident.clone()) else { + return Ok(image_not_found()); }; - use mime_sniffer::MimeTypeSniffer; - - let at_ident = AtIdentifier::new_owned(ident.clone()) - .map_err(|e| CapturedError::from_display(format!("Invalid identifier '{}': {}", ident, e)))?; - + match blob_cache.resolve_from_draft(&at_ident, &blob_rkey).await { - Ok(bytes) => { - let blob = bytes.clone(); - let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); - Ok(( - [ - (CONTENT_TYPE, mime), - (CACHE_CONTROL, "public, max-age=31536000, immutable"), - ], - bytes, - ) - .into_response()) - } - Err(e) => Err(e), + Ok(bytes) => Ok(build_image_response(bytes)), + Err(_) => Ok(image_not_found()), } } +// Route: /image/{ident}/draft/{blob_rkey}/{name} - draft image with name (name is decorative) #[cfg(all(feature = "fullstack-server", feature = "server"))] -#[get("/image/{ident}/draft/{blob_rkey}/{name}", blob_cache: Extension>)] -pub async fn image_draft_named(ident: SmolStr, blob_rkey: SmolStr, name: SmolStr) -> Result { - use axum::{ - http::header::{CACHE_CONTROL, CONTENT_TYPE}, - response::IntoResponse, +#[get("/image/{ident}/draft/{blob_rkey}/{_name}", blob_cache: Extension>)] +pub async fn image_draft_named( + ident: SmolStr, + blob_rkey: SmolStr, + _name: SmolStr, +) -> Result { + let Ok(at_ident) = AtIdentifier::new_owned(ident.clone()) else { + return Ok(image_not_found()); }; - use mime_sniffer::MimeTypeSniffer; - - // Name is optional/decorative for drafts, just use the blob_rkey - let at_ident = AtIdentifier::new_owned(ident.clone()) - .map_err(|e| CapturedError::from_display(format!("Invalid identifier '{}': {}", ident, e)))?; - + match blob_cache.resolve_from_draft(&at_ident, &blob_rkey).await { - Ok(bytes) => { - let blob = bytes.clone(); - let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); - Ok(( - [ - (CONTENT_TYPE, mime), - (CACHE_CONTROL, "public, max-age=31536000, immutable"), - ], - bytes, - ) - .into_response()) - } - Err(e) => Err(e), + Ok(bytes) => Ok(build_image_response(bytes)), + Err(_) => Ok(image_not_found()), } } // Route: /image/{ident}/{rkey}/{name} - published entry image #[cfg(all(feature = "fullstack-server", feature = "server"))] #[get("/image/{ident}/{rkey}/{name}", blob_cache: Extension>)] -pub async fn image_entry(ident: SmolStr, rkey: SmolStr, name: SmolStr) -> Result { - use axum::{ - http::header::{CACHE_CONTROL, CONTENT_TYPE}, - response::IntoResponse, +pub async fn image_entry( + ident: SmolStr, + rkey: SmolStr, + name: SmolStr, +) -> Result { + let Ok(at_ident) = AtIdentifier::new_owned(ident.clone()) else { + return Ok(image_not_found()); }; - use mime_sniffer::MimeTypeSniffer; - - let at_ident = AtIdentifier::new_owned(ident.clone()) - .map_err(|e| CapturedError::from_display(format!("Invalid identifier '{}': {}", ident, e)))?; - + match blob_cache.resolve_from_entry(&at_ident, &rkey, &name).await { - Ok(bytes) => { - let blob = bytes.clone(); - let mime = blob.sniff_mime_type().unwrap_or("image/jpg"); - Ok(( - [ - (CONTENT_TYPE, mime), - (CACHE_CONTROL, "public, max-age=31536000, immutable"), - ], - bytes, - ) - .into_response()) - } - Err(e) => Err(e), + Ok(bytes) => Ok(build_image_response(bytes)), + Err(_) => Ok(image_not_found()), } } diff --git a/crates/weaver-common/src/constellation.rs b/crates/weaver-common/src/constellation.rs index 13f13e4..4addcf8 100644 --- a/crates/weaver-common/src/constellation.rs +++ b/crates/weaver-common/src/constellation.rs @@ -9,8 +9,6 @@ use jacquard::{ use serde::{Deserialize, Serialize}; const DEFAULT_CURSOR_LIMIT: u64 = 16; -#[allow(unused)] -const DEFAULT_CURSOR_LIMIT_MAX: u64 = 100; fn get_default_cursor_limit() -> u64 { DEFAULT_CURSOR_LIMIT @@ -59,14 +57,3 @@ pub struct RecordId<'a> { pub rkey: RecordKey>, } -impl RecordId<'_> { - pub fn did(&self) -> Did<'_> { - self.did.clone() - } - pub fn collection(&self) -> Nsid<'_> { - self.collection.clone() - } - pub fn rkey(&self) -> RecordKey> { - self.rkey.clone() - } -} diff --git a/crates/weaver-common/src/lib.rs b/crates/weaver-common/src/lib.rs index e4db919..2701cfa 100644 --- a/crates/weaver-common/src/lib.rs +++ b/crates/weaver-common/src/lib.rs @@ -32,7 +32,6 @@ pub struct PublishResult<'a> { pub entries: Vec>, } -/// too many cows, so we have conversions pub fn mcow_to_cow(cow: CowStr<'_>) -> std::borrow::Cow<'_, str> { match cow { CowStr::Borrowed(s) => std::borrow::Cow::Borrowed(s), @@ -40,7 +39,6 @@ pub fn mcow_to_cow(cow: CowStr<'_>) -> std::borrow::Cow<'_, str> { } } -/// too many cows, so we have conversions pub fn cow_to_mcow(cow: std::borrow::Cow<'_, str>) -> CowStr<'_> { match cow { std::borrow::Cow::Borrowed(s) => CowStr::Borrowed(s), @@ -48,7 +46,6 @@ pub fn cow_to_mcow(cow: std::borrow::Cow<'_, str>) -> CowStr<'_> { } } -/// too many cows, so we have conversions pub fn mdcow_to_cow(cow: markdown_weaver::CowStr<'_>) -> std::borrow::Cow<'_, str> { match cow { markdown_weaver::CowStr::Borrowed(s) => std::borrow::Cow::Borrowed(s),