···1+//! Re-export cache implementation from weaver-common.
2+//!
3+//! This module exists for backwards compatibility during migration.
0000000000000000000000000000000000000000000000000045+pub use weaver_common::cache::*;
000000000000000000000000000000000000000000000000
+8-94
crates/weaver-app/src/components/editor/input.rs
···8use super::formatting::{self, FormatAction};
9use weaver_editor_core::SnapDirection;
100000000011/// Check if we need to intercept this key event.
12/// Returns true for content-modifying operations, false for navigation.
13#[allow(unused)]
···478 {
479 let _ = (evt, doc); // suppress unused warnings
480 }
481-}
482-483-/// Copy markdown as rendered HTML to clipboard.
484-#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
485-pub async fn copy_as_html(markdown: &str) -> Result<(), wasm_bindgen::JsValue> {
486- use js_sys::Array;
487- use wasm_bindgen::JsValue;
488- use web_sys::{Blob, BlobPropertyBag, ClipboardItem};
489-490- // Render markdown to HTML using ClientWriter
491- let parser = markdown_weaver::Parser::new(markdown).into_offset_iter();
492- let mut html = String::new();
493- weaver_renderer::atproto::ClientWriter::<_, _, ()>::new(parser, &mut html, markdown)
494- .run()
495- .map_err(|e| JsValue::from_str(&format!("render error: {e}")))?;
496-497- let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
498- let clipboard = window.navigator().clipboard();
499-500- // Create blobs for both HTML and plain text (raw HTML for inspection)
501- let parts = Array::new();
502- parts.push(&JsValue::from_str(&html));
503-504- let mut html_opts = BlobPropertyBag::new();
505- html_opts.type_("text/html");
506- let html_blob = Blob::new_with_str_sequence_and_options(&parts, &html_opts)?;
507-508- let mut text_opts = BlobPropertyBag::new();
509- text_opts.type_("text/plain");
510- let text_blob = Blob::new_with_str_sequence_and_options(&parts, &text_opts)?;
511-512- // Create ClipboardItem with both types
513- let item_data = js_sys::Object::new();
514- js_sys::Reflect::set(&item_data, &JsValue::from_str("text/html"), &html_blob)?;
515- js_sys::Reflect::set(&item_data, &JsValue::from_str("text/plain"), &text_blob)?;
516-517- let clipboard_item = ClipboardItem::new_with_record_from_str_to_blob_promise(&item_data)?;
518- let items = Array::new();
519- items.push(&clipboard_item);
520-521- wasm_bindgen_futures::JsFuture::from(clipboard.write(&items)).await?;
522- tracing::info!("[COPY HTML] Success - {} bytes of HTML", html.len());
523- Ok(())
524-}
525-526-/// Write text to clipboard with both text/plain and custom MIME type.
527-#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
528-pub async fn write_clipboard_with_custom_type(text: &str) -> Result<(), wasm_bindgen::JsValue> {
529- use js_sys::{Array, Object, Reflect};
530- use wasm_bindgen::JsValue;
531- use web_sys::{Blob, BlobPropertyBag, ClipboardItem};
532-533- let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
534- let navigator = window.navigator();
535- let clipboard = navigator.clipboard();
536-537- // Create blobs for each MIME type
538- let text_parts = Array::new();
539- text_parts.push(&JsValue::from_str(text));
540-541- let mut text_opts = BlobPropertyBag::new();
542- text_opts.type_("text/plain");
543- let text_blob = Blob::new_with_str_sequence_and_options(&text_parts, &text_opts)?;
544-545- let mut custom_opts = BlobPropertyBag::new();
546- custom_opts.type_("text/x-weaver-md");
547- let custom_blob = Blob::new_with_str_sequence_and_options(&text_parts, &custom_opts)?;
548-549- // Create ClipboardItem with both types
550- let item_data = Object::new();
551- Reflect::set(&item_data, &JsValue::from_str("text/plain"), &text_blob)?;
552- Reflect::set(
553- &item_data,
554- &JsValue::from_str("text/x-weaver-md"),
555- &custom_blob,
556- )?;
557-558- let clipboard_item = ClipboardItem::new_with_record_from_str_to_blob_promise(&item_data)?;
559- let items = Array::new();
560- items.push(&clipboard_item);
561-562- let promise = clipboard.write(&items);
563- wasm_bindgen_futures::JsFuture::from(promise).await?;
564-565- Ok(())
566-}
567-568-/// Describes what kind of list item the cursor is in, if any.
569-#[derive(Debug, Clone)]
570-pub enum ListContext {
571- /// Unordered list with the given marker char ('-' or '*') and indentation.
572- Unordered { indent: String, marker: char },
573- /// Ordered list with the current number and indentation.
574- Ordered { indent: String, number: usize },
575}
576577/// Detect if cursor is in a list item and return context for continuation.
···8use super::formatting::{self, FormatAction};
9use weaver_editor_core::SnapDirection;
1011+// Re-export ListContext from core - the logic is duplicated below for Loro-specific usage,
12+// but the type itself comes from core.
13+pub use weaver_editor_core::ListContext;
14+15+// Re-export clipboard helpers from browser crate.
16+#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
17+pub use weaver_editor_browser::{copy_as_html, write_clipboard_with_custom_type};
18+19/// Check if we need to intercept this key event.
20/// Returns true for content-modifying operations, false for navigation.
21#[allow(unused)]
···486 {
487 let _ = (evt, doc); // suppress unused warnings
488 }
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000489}
490491/// Detect if cursor is in a list item and return context for continuation.
···96#[allow(unused_imports)]
97pub use log_buffer::LogCaptureLayer;
9899+// Worker - EditorReactor stays local, EmbedWorker comes from weaver-embed-worker
100#[cfg(all(target_family = "wasm", target_os = "unknown"))]
101+pub use worker::{EditorReactor, WorkerInput, WorkerOutput};
102+#[cfg(all(target_family = "wasm", target_os = "unknown"))]
103+pub use weaver_embed_worker::{EmbedWorker, EmbedWorkerInput, EmbedWorkerOutput};
104105// Collab coordinator
106#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
-165
crates/weaver-app/src/components/editor/worker.rs
···6//!
7//! When the `collab-worker` feature is enabled, also handles iroh P2P
8//! networking for real-time collaboration.
9-//!
10-//! Also handles embed fetching with a persistent cache to avoid re-fetching.
1112#[cfg(all(target_family = "wasm", target_os = "unknown"))]
13use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
14use serde::{Deserialize, Serialize};
15-use std::collections::HashMap;
16use weaver_common::transport::PresenceSnapshot;
1718#[cfg(all(target_family = "wasm", target_os = "unknown"))]
···772773#[cfg(all(target_family = "wasm", target_os = "unknown"))]
774pub use worker_impl::EditorReactor;
775-776-// ============================================================================
777-// Embed Worker - fetches and caches AT Protocol embeds
778-// ============================================================================
779-780-/// Input messages to the embed worker.
781-#[derive(Serialize, Deserialize, Debug, Clone)]
782-pub enum EmbedWorkerInput {
783- /// Request embeds for a list of AT URIs.
784- /// Worker returns cached results immediately and fetches missing ones.
785- FetchEmbeds {
786- /// AT URIs to fetch (e.g., "at://did:plc:xxx/app.bsky.feed.post/yyy")
787- uris: Vec<String>,
788- },
789- /// Clear the cache (e.g., on session change)
790- ClearCache,
791-}
792-793-/// Output messages from the embed worker.
794-#[derive(Serialize, Deserialize, Debug, Clone)]
795-pub enum EmbedWorkerOutput {
796- /// Embed results (may be partial if some failed)
797- Embeds {
798- /// Successfully fetched/cached embeds: uri -> rendered HTML
799- results: HashMap<String, String>,
800- /// URIs that failed to fetch
801- errors: HashMap<String, String>,
802- /// Timing info
803- fetch_ms: f64,
804- },
805- /// Cache was cleared
806- CacheCleared,
807-}
808-809-#[cfg(all(target_family = "wasm", target_os = "unknown"))]
810-mod embed_worker_impl {
811- use super::*;
812- use crate::cache_impl;
813- use gloo_worker::{HandlerId, Worker, WorkerScope};
814- use jacquard::IntoStatic;
815- use jacquard::client::UnauthenticatedSession;
816- use jacquard::identity::JacquardResolver;
817- use jacquard::prelude::*;
818- use jacquard::types::string::AtUri;
819- use std::time::Duration;
820-821- /// Embed worker with persistent cache.
822- pub struct EmbedWorker {
823- /// Cached rendered embeds with TTL and max capacity
824- cache: cache_impl::Cache<AtUri<'static>, String>,
825- /// Unauthenticated session for public API calls
826- session: UnauthenticatedSession<JacquardResolver>,
827- }
828-829- impl Worker for EmbedWorker {
830- type Message = ();
831- type Input = EmbedWorkerInput;
832- type Output = EmbedWorkerOutput;
833-834- fn create(_scope: &WorkerScope<Self>) -> Self {
835- Self {
836- // Cache up to 500 embeds, TTL of 1 hour
837- cache: cache_impl::new_cache(500, Duration::from_secs(3600)),
838- session: UnauthenticatedSession::default(),
839- }
840- }
841-842- fn update(&mut self, _scope: &WorkerScope<Self>, _msg: Self::Message) {}
843-844- fn received(&mut self, scope: &WorkerScope<Self>, msg: Self::Input, id: HandlerId) {
845- match msg {
846- EmbedWorkerInput::FetchEmbeds { uris } => {
847- let mut results = HashMap::new();
848- let mut errors = HashMap::new();
849- let mut to_fetch = Vec::new();
850-851- // Parse URIs and check cache
852- for uri_str in uris {
853- let at_uri = match AtUri::new_owned(uri_str.clone()) {
854- Ok(u) => u,
855- Err(e) => {
856- errors.insert(uri_str, format!("Invalid AT URI: {e}"));
857- continue;
858- }
859- };
860-861- if let Some(html) = cache_impl::get(&self.cache, &at_uri) {
862- results.insert(uri_str, html);
863- } else {
864- to_fetch.push((uri_str, at_uri));
865- }
866- }
867-868- // If nothing to fetch, respond immediately
869- if to_fetch.is_empty() {
870- scope.respond(
871- id,
872- EmbedWorkerOutput::Embeds {
873- results,
874- errors,
875- fetch_ms: 0.0,
876- },
877- );
878- return;
879- }
880-881- // Fetch missing embeds asynchronously
882- let session = self.session.clone();
883- let cache = self.cache.clone();
884- let scope = scope.clone();
885-886- wasm_bindgen_futures::spawn_local(async move {
887- // Use weaver-index when use-index feature is enabled
888- #[cfg(feature = "use-index")]
889- {
890- use jacquard::xrpc::XrpcClient;
891- use jacquard::url::Url;
892- if let Ok(url) = Url::parse("https://index.weaver.sh") {
893- session.set_base_uri(url).await;
894- }
895- }
896-897- let fetch_start = crate::perf::now();
898-899- for (uri_str, at_uri) in to_fetch {
900- match weaver_renderer::atproto::fetch_and_render(&at_uri, &session)
901- .await
902- {
903- Ok(html) => {
904- cache_impl::insert(&cache, at_uri, html.clone());
905- results.insert(uri_str, html);
906- }
907- Err(e) => {
908- errors.insert(uri_str, format!("{:?}", e));
909- }
910- }
911- }
912-913- let fetch_ms = crate::perf::now() - fetch_start;
914- scope.respond(
915- id,
916- EmbedWorkerOutput::Embeds {
917- results,
918- errors,
919- fetch_ms,
920- },
921- );
922- });
923- }
924-925- EmbedWorkerInput::ClearCache => {
926- // mini-moka doesn't have a clear method, so we just recreate
927- // (this is fine since ClearCache is rarely called)
928- scope.respond(id, EmbedWorkerOutput::CacheCleared);
929- }
930- }
931- }
932- }
933-}
934-935-#[cfg(all(target_family = "wasm", target_os = "unknown"))]
936-pub use embed_worker_impl::EmbedWorker;
···6//!
7//! When the `collab-worker` feature is enabled, also handles iroh P2P
8//! networking for real-time collaboration.
00910#[cfg(all(target_family = "wasm", target_os = "unknown"))]
11use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
12use serde::{Deserialize, Serialize};
013use weaver_common::transport::PresenceSnapshot;
1415#[cfg(all(target_family = "wasm", target_os = "unknown"))]
···769770#[cfg(all(target_family = "wasm", target_os = "unknown"))]
771pub use worker_impl::EditorReactor;
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+3-56
crates/weaver-app/src/perf.rs
···1-//! Performance timing utilities for instrumentation.
2//!
3-//! Provides a cross-platform wrapper around Performance.now() for WASM
4-//! and a no-op fallback for native builds.
5-6-/// Get the current high-resolution timestamp in milliseconds.
7-///
8-/// On WASM, this uses `Performance.now()` from the Web Performance API.
9-/// On native builds, returns 0.0 (instrumentation is primarily for browser profiling).
10-#[cfg(all(target_family = "wasm", target_os = "unknown"))]
11-pub fn now() -> f64 {
12- web_sys::window()
13- .and_then(|w| w.performance())
14- .map(|p| p.now())
15- .unwrap_or(0.0)
16-}
17-18-#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
19-pub fn now() -> f64 {
20- 0.0
21-}
2223-/// Measure the execution time of a closure and log it.
24-///
25-/// Returns the closure's result and logs the elapsed time via tracing.
26-#[allow(dead_code)]
27-pub fn measure<T, F: FnOnce() -> T>(label: &str, f: F) -> T {
28- let start = now();
29- let result = f();
30- let elapsed = now() - start;
31- tracing::debug!(elapsed_ms = elapsed, "{}", label);
32- result
33-}
34-35-/// A guard that logs elapsed time when dropped.
36-///
37-/// Useful for timing blocks of code without closures.
38-#[allow(dead_code)]
39-pub struct TimingGuard {
40- label: &'static str,
41- start: f64,
42-}
43-44-impl TimingGuard {
45- pub fn new(label: &'static str) -> Self {
46- Self {
47- label,
48- start: now(),
49- }
50- }
51-}
52-53-impl Drop for TimingGuard {
54- fn drop(&mut self) {
55- let elapsed = now() - self.start;
56- tracing::debug!(elapsed_ms = elapsed, "{}", self.label);
57- }
58-}
···1+//! Re-export perf utilities from weaver-common.
2//!
3+//! This module exists for backwards compatibility during migration.
00000000000000000045+pub use weaver_common::perf::*;
00000000000000000000000000000000000
···1//! Weaver common library - thin wrapper around jacquard with notebook-specific conveniences
23pub mod agent;
004pub mod constellation;
5pub mod error;
006pub mod resolve;
7#[cfg(feature = "telemetry")]
8pub mod telemetry;
···1//! Weaver common library - thin wrapper around jacquard with notebook-specific conveniences
23pub mod agent;
4+#[cfg(feature = "cache")]
5+pub mod cache;
6pub mod constellation;
7pub mod error;
8+#[cfg(feature = "perf")]
9+pub mod perf;
10pub mod resolve;
11#[cfg(feature = "telemetry")]
12pub mod telemetry;
···1+//! Entry point for the embed web worker.
2+//!
3+//! This binary is compiled separately and loaded by the main app
4+//! to fetch and cache AT Protocol embeds off the main thread.
5+6+#[cfg(all(target_family = "wasm", target_os = "unknown"))]
7+fn main() {
8+ console_error_panic_hook::set_once();
9+10+ use gloo_worker::Registrable;
11+ use weaver_embed_worker::EmbedWorker;
12+13+ EmbedWorker::registrar().register();
14+}
15+16+#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
17+fn main() {
18+ eprintln!("This binary is only meant to run as a WASM web worker");
19+}