WIP moving iroh to worker

Orual a89ecef2 a2ceb29f

+221 -75
+1
crates/weaver-app/Cargo.toml
··· 33 33 desktop = ["dioxus/desktop"] 34 34 mobile = ["dioxus/mobile"] 35 35 server = [ "dioxus/server", "dep:jacquard-axum", "dep:axum", "dep:resvg", "dep:usvg", "dep:tiny-skia", "dep:textwrap", "dep:askama", "dep:fontdb"] 36 + collab-worker = ["weaver-common/iroh"] 36 37 37 38 38 39 [dependencies]
+45
crates/weaver-app/src/components/editor/worker.rs
··· 10 10 use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; 11 11 use serde::{Deserialize, Serialize}; 12 12 use std::collections::HashMap; 13 + use weaver_common::transport::PresenceSnapshot; 13 14 14 15 /// Input messages to the editor worker. 15 16 #[derive(Serialize, Deserialize, Debug, Clone)] ··· 35 36 /// Editing CID if editing existing entry 36 37 editing_cid: Option<String>, 37 38 }, 39 + /// Start collab session (worker will spawn CollabNode) 40 + StartCollab { 41 + /// blake3 hash of resource URI (32 bytes) 42 + topic: [u8; 32], 43 + /// Bootstrap peer node IDs (z-base32 strings) 44 + bootstrap_peers: Vec<String>, 45 + }, 46 + /// Loro updates from local edits (forward to gossip) 47 + BroadcastUpdate { 48 + /// Loro update bytes 49 + data: Vec<u8>, 50 + }, 51 + /// New peers discovered by main thread 52 + AddPeers { 53 + /// Node ID strings 54 + peers: Vec<String>, 55 + }, 56 + /// Local cursor position changed 57 + BroadcastCursor { 58 + /// Cursor position 59 + position: usize, 60 + /// Selection range if any 61 + selection: Option<(usize, usize)>, 62 + }, 63 + /// Stop collab session 64 + StopCollab, 38 65 } 39 66 40 67 /// Output messages from the editor worker. ··· 65 92 }, 66 93 /// Error occurred. 67 94 Error { message: String }, 95 + /// Collab node ready, here's info for session record 96 + CollabReady { 97 + /// Node ID (z-base32 string) 98 + node_id: String, 99 + /// Relay URL for browser connectivity 100 + relay_url: Option<String>, 101 + }, 102 + /// Collab session joined successfully 103 + CollabJoined, 104 + /// Remote updates to merge into main doc 105 + RemoteUpdates { 106 + /// Loro update bytes 107 + data: Vec<u8>, 108 + }, 109 + /// Presence state changed 110 + PresenceUpdate(PresenceSnapshot), 111 + /// Collab session ended 112 + CollabStopped, 68 113 } 69 114 70 115 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
+7 -4
crates/weaver-common/Cargo.toml
··· 9 9 default = ["dev"] 10 10 dev = [] 11 11 native = ["jacquard/dns"] 12 + iroh = ["dep:iroh", "dep:iroh-gossip", "dep:iroh-tickets"] 12 13 13 14 [dependencies] 14 15 n0-future = { workspace = true } ··· 34 35 # Real-time collaboration transport (iroh P2P) 35 36 chrono = "0.4" 36 37 blake3 = { version = "1", default-features = false, features = ["pure"] } 37 - iroh = { version = "0.95", default-features = false } 38 - iroh-gossip = { version = "0.95", default-features = false, features = ["net"] } 39 38 postcard = "1.1.1" 40 39 rand = "0.9.2" 40 + web-time = "1" 41 + 42 + iroh = { version = "0.95", default-features = false, optional = true } 43 + iroh-gossip = { version = "0.95", default-features = false, features = ["net"], optional = true } 44 + iroh-tickets = { version = "0.2", optional = true } 45 + 41 46 getrandom = { version = "0.3", features = [] } 42 - iroh-tickets = "0.2" 43 47 ring = { version = "0.17", default-features = false } 44 - web-time = "1" 45 48 46 49 47 50 [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies]
+2
crates/weaver-common/src/transport/discovery.rs
··· 1 + #![cfg(feature = "iroh")] 2 + 1 3 //! Peer discovery via AT Protocol session records. 2 4 //! 3 5 //! Collaborators publish `sh.weaver.collab.session` records to their PDS
+84 -63
crates/weaver-common/src/transport/messages.rs
··· 1 1 //! Wire protocol for collaborative editing messages. 2 2 3 - use iroh::{PublicKey, SecretKey, Signature}; 4 3 use serde::{Deserialize, Serialize}; 5 4 6 5 /// Messages exchanged between collaborators over gossip. ··· 65 64 } 66 65 } 67 66 68 - /// A signed message wrapper for authenticated transport. 69 - /// 70 - /// Includes the sender's public key so receivers can verify without context. 71 - #[derive(Debug, Clone, Serialize, Deserialize)] 72 - pub struct SignedMessage { 73 - /// Sender's public key (also their EndpointId). 74 - pub from: PublicKey, 75 - /// The serialized TimestampedMessage (postcard bytes). 76 - pub data: Vec<u8>, 77 - /// Ed25519 signature over data. 78 - pub signature: Signature, 79 - } 67 + // ============================================================================ 68 + // Signed message wrapper (requires iroh for crypto) 69 + // ============================================================================ 80 70 81 - /// Versioned wire format with timestamp. 82 - #[derive(Debug, Clone, Serialize, Deserialize)] 83 - enum WireMessage { 84 - V0 { timestamp: u64, message: CollabMessage }, 85 - } 71 + #[cfg(feature = "iroh")] 72 + mod signed { 73 + use super::*; 74 + use iroh::{PublicKey, SecretKey, Signature}; 86 75 87 - /// A verified message with sender and timestamp info. 88 - #[derive(Debug, Clone)] 89 - pub struct ReceivedMessage { 90 - /// Sender's public key. 91 - pub from: PublicKey, 92 - /// When the message was sent (micros since epoch). 93 - pub timestamp: u64, 94 - /// The decoded message. 95 - pub message: CollabMessage, 96 - } 76 + /// A signed message wrapper for authenticated transport. 77 + #[derive(Debug, Clone, Serialize, Deserialize)] 78 + pub struct SignedMessage { 79 + /// Sender's public key (also their EndpointId). 80 + pub from: PublicKey, 81 + /// The serialized TimestampedMessage (postcard bytes). 82 + pub data: Vec<u8>, 83 + /// Ed25519 signature over data. 84 + pub signature: Signature, 85 + } 97 86 98 - /// Error type for signed message operations. 99 - #[derive(Debug, thiserror::Error)] 100 - pub enum SignedMessageError { 101 - #[error("serialization failed: {0}")] 102 - Serialization(#[from] postcard::Error), 103 - #[error("signature verification failed")] 104 - InvalidSignature, 105 - } 87 + /// Versioned wire format with timestamp. 88 + #[derive(Debug, Clone, Serialize, Deserialize)] 89 + enum WireMessage { 90 + V0 { timestamp: u64, message: CollabMessage }, 91 + } 106 92 107 - impl SignedMessage { 108 - /// Sign a message and encode to bytes for wire transmission. 109 - pub fn sign_and_encode(secret_key: &SecretKey, message: &CollabMessage) -> Result<Vec<u8>, SignedMessageError> { 110 - use web_time::SystemTime; 93 + /// A verified message with sender and timestamp info. 94 + #[derive(Debug, Clone)] 95 + pub struct ReceivedMessage { 96 + /// Sender's public key. 97 + pub from: PublicKey, 98 + /// When the message was sent (micros since epoch). 99 + pub timestamp: u64, 100 + /// The decoded message. 101 + pub message: CollabMessage, 102 + } 111 103 112 - let timestamp = SystemTime::now() 113 - .duration_since(SystemTime::UNIX_EPOCH) 114 - .unwrap() 115 - .as_micros() as u64; 116 - let wire = WireMessage::V0 { timestamp, message: message.clone() }; 117 - let data = postcard::to_stdvec(&wire)?; 118 - let signature = secret_key.sign(&data); 119 - let from = secret_key.public(); 120 - let signed = Self { from, data, signature }; 121 - Ok(postcard::to_stdvec(&signed)?) 104 + /// Error type for signed message operations. 105 + #[derive(Debug, thiserror::Error)] 106 + pub enum SignedMessageError { 107 + #[error("serialization failed: {0}")] 108 + Serialization(#[from] postcard::Error), 109 + #[error("signature verification failed")] 110 + InvalidSignature, 122 111 } 123 112 124 - /// Decode from bytes and verify signature. 125 - pub fn decode_and_verify(bytes: &[u8]) -> Result<ReceivedMessage, SignedMessageError> { 126 - let signed: Self = postcard::from_bytes(bytes)?; 127 - signed.from 128 - .verify(&signed.data, &signed.signature) 129 - .map_err(|_| SignedMessageError::InvalidSignature)?; 130 - let wire: WireMessage = postcard::from_bytes(&signed.data)?; 131 - let WireMessage::V0 { timestamp, message } = wire; 132 - Ok(ReceivedMessage { 133 - from: signed.from, 134 - timestamp, 135 - message, 136 - }) 113 + impl SignedMessage { 114 + /// Sign a message and encode to bytes for wire transmission. 115 + pub fn sign_and_encode( 116 + secret_key: &SecretKey, 117 + message: &CollabMessage, 118 + ) -> Result<Vec<u8>, SignedMessageError> { 119 + use web_time::SystemTime; 120 + 121 + let timestamp = SystemTime::now() 122 + .duration_since(SystemTime::UNIX_EPOCH) 123 + .unwrap() 124 + .as_micros() as u64; 125 + let wire = WireMessage::V0 { 126 + timestamp, 127 + message: message.clone(), 128 + }; 129 + let data = postcard::to_stdvec(&wire)?; 130 + let signature = secret_key.sign(&data); 131 + let from = secret_key.public(); 132 + let signed = Self { 133 + from, 134 + data, 135 + signature, 136 + }; 137 + Ok(postcard::to_stdvec(&signed)?) 138 + } 139 + 140 + /// Decode from bytes and verify signature. 141 + pub fn decode_and_verify(bytes: &[u8]) -> Result<ReceivedMessage, SignedMessageError> { 142 + let signed: Self = postcard::from_bytes(bytes)?; 143 + signed 144 + .from 145 + .verify(&signed.data, &signed.signature) 146 + .map_err(|_| SignedMessageError::InvalidSignature)?; 147 + let wire: WireMessage = postcard::from_bytes(&signed.data)?; 148 + let WireMessage::V0 { timestamp, message } = wire; 149 + Ok(ReceivedMessage { 150 + from: signed.from, 151 + timestamp, 152 + message, 153 + }) 154 + } 137 155 } 138 156 } 157 + 158 + #[cfg(feature = "iroh")] 159 + pub use signed::{ReceivedMessage, SignedMessage, SignedMessageError}; 139 160 140 161 #[cfg(test)] 141 162 mod tests {
+22 -8
crates/weaver-common/src/transport/mod.rs
··· 1 - //! Real-time collaboration transport layer using iroh P2P networking. 1 + //! Real-time collaboration transport layer. 2 2 //! 3 - //! This module provides the infrastructure for real-time collaborative editing: 4 - //! - `CollabNode`: iroh endpoint + gossip router (one per app instance) 5 - //! - `CollabSession`: per-resource session management 6 - //! - `CollabMessage`: wire protocol for CRDT updates, cursors, presence 7 - //! - `discovery`: utilities for parsing NodeIds from session records 3 + //! Core message types are always available. iroh-based networking 4 + //! requires the `iroh` feature. 5 + 6 + mod messages; 7 + mod presence_types; 8 8 9 + #[cfg(feature = "iroh")] 9 10 mod discovery; 10 - mod messages; 11 + #[cfg(feature = "iroh")] 11 12 mod node; 13 + #[cfg(feature = "iroh")] 12 14 mod presence; 15 + #[cfg(feature = "iroh")] 13 16 mod session; 14 17 18 + // Always available - wire protocol 19 + pub use messages::CollabMessage; 20 + pub use presence_types::{CollaboratorInfo, PresenceSnapshot, RemoteCursorInfo}; 21 + 22 + // iroh feature - networking 23 + #[cfg(feature = "iroh")] 15 24 pub use discovery::{node_id_to_string, parse_node_id, DiscoveredPeer, DiscoveryError}; 25 + #[cfg(feature = "iroh")] 16 26 pub use iroh::EndpointId; 17 - pub use messages::{CollabMessage, ReceivedMessage, SignedMessage, SignedMessageError}; 27 + #[cfg(feature = "iroh")] 28 + pub use messages::{ReceivedMessage, SignedMessage, SignedMessageError}; 29 + #[cfg(feature = "iroh")] 18 30 pub use node::{CollabNode, TransportError}; 31 + #[cfg(feature = "iroh")] 19 32 pub use presence::{Collaborator, PresenceTracker, RemoteCursor}; 33 + #[cfg(feature = "iroh")] 20 34 pub use session::{CollabSession, SessionError, SessionEvent, TopicId};
+2
crates/weaver-common/src/transport/node.rs
··· 1 + #![cfg(feature = "iroh")] 2 + 1 3 //! CollabNode - iroh endpoint with gossip router for real-time collaboration. 2 4 3 5 use iroh::Endpoint;
+2
crates/weaver-common/src/transport/presence.rs
··· 1 + #![cfg(feature = "iroh")] 2 + 1 3 //! Presence tracking for collaborative editing sessions. 2 4 //! 3 5 //! Tracks active collaborators, their cursor positions, and display info.
+54
crates/weaver-common/src/transport/presence_types.rs
··· 1 + //! Presence types for main thread rendering. 2 + //! 3 + //! These types use String node IDs instead of EndpointId, 4 + //! allowing them to be used without the iroh feature. 5 + 6 + use serde::{Deserialize, Serialize}; 7 + 8 + /// A remote collaborator's cursor for rendering. 9 + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 10 + pub struct RemoteCursorInfo { 11 + /// Node ID as string (z-base32 encoded) 12 + pub node_id: String, 13 + /// Character offset in the document 14 + pub position: usize, 15 + /// Selection range (anchor, head) if any 16 + pub selection: Option<(usize, usize)>, 17 + /// Assigned colour (RGBA) 18 + pub color: u32, 19 + } 20 + 21 + /// Collaborator info for presence display. 22 + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 23 + pub struct CollaboratorInfo { 24 + /// Node ID as string 25 + pub node_id: String, 26 + /// The collaborator's DID 27 + pub did: String, 28 + /// Display name for UI 29 + pub display_name: String, 30 + /// Assigned colour (RGBA) 31 + pub color: u32, 32 + /// Current cursor position (if known) 33 + pub cursor_position: Option<usize>, 34 + /// Current selection (if any) 35 + pub selection: Option<(usize, usize)>, 36 + } 37 + 38 + /// Presence update sent from worker to main thread. 39 + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 40 + pub struct PresenceSnapshot { 41 + /// All known collaborators 42 + pub collaborators: Vec<CollaboratorInfo>, 43 + /// Number of connected peers 44 + pub peer_count: usize, 45 + } 46 + 47 + impl Default for PresenceSnapshot { 48 + fn default() -> Self { 49 + Self { 50 + collaborators: Vec::new(), 51 + peer_count: 0, 52 + } 53 + } 54 + }
+2
crates/weaver-common/src/transport/session.rs
··· 1 + #![cfg(feature = "iroh")] 2 + 1 3 //! CollabSession - per-resource gossip session for real-time collaboration. 2 4 3 5 use std::sync::Arc;