···1+class Channel{pending;waiting;constructor(){this.pending=[],this.waiting=[]}send(data){if(this.waiting.length>0){this.waiting.shift()(data);return}this.pending.push(data)}async recv(){return new Promise((resolve,_reject)=>{if(this.pending.length>0){resolve(this.pending.shift());return}this.waiting.push(resolve)})}}class WeakDioxusChannel{inner;constructor(channel){this.inner=new WeakRef(channel)}rustSend(data){let channel=this.inner.deref();if(channel)channel.rustSend(data)}async rustRecv(){let channel=this.inner.deref();if(channel)return await channel.rustRecv()}}class DioxusChannel{weak(){return new WeakDioxusChannel(this)}}globalThis.__nextChannelId=0;globalThis.__channels=[];class WebDioxusChannel extends DioxusChannel{js_to_rust;rust_to_js;owner;id;constructor(owner){super();this.owner=owner,this.js_to_rust=new Channel,this.rust_to_js=new Channel,this.id=globalThis.__nextChannelId,globalThis.__channels[this.id]=this,globalThis.__nextChannelId+=1}weak(){return new WeakDioxusChannel(this)}async recv(){return await this.rust_to_js.recv()}send(data){this.js_to_rust.send(data)}rustSend(data){this.rust_to_js.send(data)}async rustRecv(){return await this.js_to_rust.recv()}close(){globalThis.__channels[this.id]=null}}export{WebDioxusChannel,WeakDioxusChannel};
+19
crates/weaver-app/src/bin/editor_worker.rs
···0000000000000000000
···1+//! Entry point for the editor web worker.
2+//!
3+//! This binary is compiled separately and loaded by the main app
4+//! to handle CPU-intensive editor operations 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_app::components::editor::EditorWorker;
12+13+ EditorWorker::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+}
+19
crates/weaver-app/src/bin/embed_worker.rs
···0000000000000000000
···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_app::components::editor::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+}
···632 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
633 let mut interval_holder: Signal<Option<gloo_timers::callback::Interval>> = use_signal(|| None);
634635- #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
636- let doc_for_autosave = document.clone();
637- #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
638- let draft_key_for_autosave = draft_key.clone();
639 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
640- use_effect(move || {
641- // Check every 500ms if there are unsaved changes
642- let mut doc = doc_for_autosave.clone();
643- let draft_key = draft_key_for_autosave.clone();
644- let interval = gloo_timers::callback::Interval::new(500, move || {
645- let current_frontiers = doc.state_frontiers();
646647- // Only save if frontiers changed (document was edited)
648- let needs_save = {
649- let last_frontiers = last_saved_frontiers.peek();
650- match &*last_frontiers {
651- None => true,
652- Some(last) => ¤t_frontiers != last,
00000000000000000000000000000000000000000000000653 }
654 };
655656- if needs_save {
00000000000000000000000000000000000000657 doc.sync_loro_cursor();
658- let _ = storage::save_to_storage(&doc, &draft_key);
659660- // Update last saved frontiers
00000000000000000000000000000000661 last_saved_frontiers.set(Some(current_frontiers));
662- }
00000663 });
664- // Store in signal instead of forget - interval drops when component unmounts
665- interval_holder.set(Some(interval));
666- });
667668 // Set up beforeinput listener for all text input handling.
669 // This is the primary handler for text insertion, deletion, etc.
···16031604 // Get selection rectangles if there's a selection
1605 let selection_rects = if let Some((start, end)) = selection {
1606- let (start, end) = if start <= end { (start, end) } else { (end, start) };
00001607 get_selection_rects_relative(start, end, &offset_map, "markdown-editor")
1608 } else {
1609 vec![]
···632 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
633 let mut interval_holder: Signal<Option<gloo_timers::callback::Interval>> = use_signal(|| None);
634635+ // Worker-based autosave (offloads export + encode to worker thread)
000636 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
637+ {
638+ use super::worker::{EditorWorker, WorkerInput, WorkerOutput};
639+ use gloo_storage::Storage;
640+ use gloo_worker::Spawnable;
00641642+ // Track if worker is available (false = fallback to main thread)
643+ let use_worker: Signal<bool> = use_signal(|| true);
644+ // Worker bridge handle
645+ let mut worker_bridge: Signal<Option<gloo_worker::WorkerBridge<EditorWorker>>> =
646+ use_signal(|| None);
647+ // Track version vector sent to worker (for incremental updates)
648+ let mut last_worker_vv: Signal<Option<loro::VersionVector>> = use_signal(|| None);
649+650+ // Spawn worker on mount
651+ let doc_for_worker_init = document.clone();
652+ let draft_key_for_worker = draft_key.clone();
653+ use_effect(move || {
654+ let doc = doc_for_worker_init.clone();
655+ let draft_key = draft_key_for_worker.clone();
656+657+ // Callback for worker responses
658+ let on_output = move |output: WorkerOutput| {
659+ match output {
660+ WorkerOutput::Ready => {
661+ tracing::info!("Editor worker ready");
662+ }
663+ WorkerOutput::Snapshot {
664+ draft_key,
665+ b64_snapshot,
666+ content,
667+ title,
668+ cursor_offset,
669+ editing_uri,
670+ editing_cid,
671+ export_ms,
672+ encode_ms,
673+ } => {
674+ // Write to localStorage (fast - just string assignment)
675+ let snapshot = storage::EditorSnapshot {
676+ content,
677+ title,
678+ snapshot: Some(b64_snapshot),
679+ cursor: None, // Worker doesn't have Loro cursor
680+ cursor_offset,
681+ editing_uri,
682+ editing_cid,
683+ };
684+ let write_start = crate::perf::now();
685+ let _ = gloo_storage::LocalStorage::set(
686+ format!("{}{}", storage::DRAFT_KEY_PREFIX, draft_key),
687+ &snapshot,
688+ );
689+ let write_ms = crate::perf::now() - write_start;
690+ tracing::debug!(export_ms, encode_ms, write_ms, "worker autosave complete");
691+ }
692+ WorkerOutput::Error { message } => {
693+ tracing::error!("Worker error: {}", message);
694+ }
695 }
696 };
697698+ // Spawn worker (panics on failure in debug, returns bridge directly)
699+ let bridge = EditorWorker::spawner()
700+ .callback(on_output)
701+ .spawn("/editor_worker.js");
702+703+ // Initialize with current document snapshot
704+ let snapshot = doc.export_snapshot();
705+ bridge.send(WorkerInput::Init {
706+ snapshot,
707+ draft_key: draft_key.clone(),
708+ });
709+ worker_bridge.set(Some(bridge));
710+ tracing::info!("Editor worker spawned");
711+ });
712+713+ // Autosave interval
714+ let doc_for_autosave = document.clone();
715+ let draft_key_for_autosave = draft_key.clone();
716+ use_effect(move || {
717+ let mut doc = doc_for_autosave.clone();
718+ let draft_key = draft_key_for_autosave.clone();
719+720+ let interval = gloo_timers::callback::Interval::new(500, move || {
721+ let callback_start = crate::perf::now();
722+ let current_frontiers = doc.state_frontiers();
723+724+ // Only save if frontiers changed (document was edited)
725+ let needs_save = {
726+ let last_frontiers = last_saved_frontiers.peek();
727+ match &*last_frontiers {
728+ None => true,
729+ Some(last) => ¤t_frontiers != last,
730+ }
731+ };
732+733+ if !needs_save {
734+ return;
735+ }
736+737 doc.sync_loro_cursor();
0738739+ // Try worker path first
740+ if *use_worker.peek() {
741+ if let Some(ref bridge) = *worker_bridge.peek() {
742+ // Send updates to worker (or full snapshot if first time)
743+ let current_vv = doc.version_vector();
744+ let updates = if let Some(ref last_vv) = *last_worker_vv.peek() {
745+ doc.export_updates_from(last_vv).unwrap_or_default()
746+ } else {
747+ doc.export_snapshot()
748+ };
749+750+ if !updates.is_empty() {
751+ bridge.send(WorkerInput::ApplyUpdates { updates });
752+ }
753+754+ // Request snapshot export
755+ bridge.send(WorkerInput::ExportSnapshot {
756+ cursor_offset: doc.cursor.read().offset,
757+ editing_uri: doc.entry_ref().map(|r| r.uri.to_string()),
758+ editing_cid: doc.entry_ref().map(|r| r.cid.to_string()),
759+ });
760+761+ last_worker_vv.set(Some(current_vv));
762+ last_saved_frontiers.set(Some(current_frontiers));
763+764+ let callback_ms = crate::perf::now() - callback_start;
765+ tracing::debug!(callback_ms, "autosave via worker");
766+ return;
767+ }
768+ }
769+770+ // Fallback: main thread save
771+ let _ = storage::save_to_storage(&doc, &draft_key);
772 last_saved_frontiers.set(Some(current_frontiers));
773+774+ let callback_ms = crate::perf::now() - callback_start;
775+ tracing::debug!(callback_ms, "autosave callback (main thread fallback)");
776+ });
777+778+ interval_holder.set(Some(interval));
779 });
780+ }
00781782 // Set up beforeinput listener for all text input handling.
783 // This is the primary handler for text insertion, deletion, etc.
···17171718 // Get selection rectangles if there's a selection
1719 let selection_rects = if let Some((start, end)) = selection {
1720+ let (start, end) = if start <= end {
1721+ (start, end)
1722+ } else {
1723+ (end, start)
1724+ };
1725 get_selection_rects_relative(start, end, &offset_map, "markdown-editor")
1726 } else {
1727 vec![]