moduforge-collaboration 0.4.5

moduforge 协作系统
Documentation

ModuForge 协作 Crate

moduforge-collaboration 是一个为 ModuForge 生态系统提供实时协作功能的 Rust crate。它允许多个用户同时在同一个文档上工作,并实时同步所有更改。

核心技术

为了确保高性能和高可靠性,协作服务器构建在一系列健壮且现代的技术之上:

  • WebSocket: 服务器使用 WebSocket (tokio-tungstenite) 在客户端和服务器之间进行持久、低延迟的双向通信。这对于实现实时协作至关重要。
  • CRDTs (无冲突复制数据类型): 同步逻辑的核心是 yrs,它是流行的 Yjs CRDT 框架的 Rust 移植。CRDTs 允许本地优先的开发模式,并保证即使在多个用户并发编辑的情况下,文档状态最终也能无冲突地达成一致。
  • Tokio: 整个服务器构建在 Tokio 异步运行时之上,使其能够高效地处理大量并发连接。

架构

该 crate 由几个协同工作的关键组件构成,共同提供协作服务:

CollaborationServer

这是服务器的主要入口点。其职责包括:

  • 接受来自客户端的 WebSocket 连接请求。
  • 管理每个客户端连接的生命周期,包括注册和清理。
  • 将客户端组织到"房间"中,每个房间对应一个唯一的文档协作会话。
  • 向房间内的所有客户端广播消息。

SyncService

SyncService 充当业务逻辑层。它处理来自客户端的传入消息,并与其他组件协调。其处理的事务包括:

  • 客户端加入或离开房间的请求。
  • 处理 Yrs 更新消息,并将其应用到相应的文档上。
  • 为新加入的客户端同步文档状态。

YrsManager

该组件负责管理所有活动的 CRDT 文档 (yrs::Doc)。它:

  • 维护一个从 room_id 到 yrs::Doc 的映射。
  • 提供一种线程安全的方式来访问、创建和更新文档。
  • 确保对给定文档的所有更改都得到正确处理。

房间管理和错误处理

🔒 严格的房间存在性检查

从安全性和资源管理角度考虑,系统采用严格的房间管理策略:

房间必须预先初始化

// ✅ 正确方式:预先初始化房间
let server = CollaborationServer::with_sync_service(yrs_manager, sync_service, 8080);
server.init_room_with_data("room-123", &tree).await?;

客户端连接检查

当客户端尝试连接到房间时:

  • ✅ 房间存在 → 允许连接,正常进行 WebSocket 升级
  • ❌ 房间不存在 → 返回 404 错误,拒绝连接
// WebSocket 连接: ws://localhost:8080/collaboration/room-123

// 如果 room-123 不存在,客户端会收到:
{
  "error": "ROOM_NOT_FOUND",
  "message": "房间 'room-123' 不存在",
  "room_id": "room-123",
  "code": 404
}

🎯 房间状态管理

房间状态枚举

pub enum RoomStatus {
    NotExists,    // 房间不存在
    Created,      // 房间已创建但未初始化数据
    Initialized,  // 房间已初始化并有数据
    Shutting,     // 房间正在下线中
    Offline,      // 房间已下线
}

状态检查 API

// 检查房间是否存在
let exists = yrs_manager.room_exists("room-id");

// 获取房间状态
let status = sync_service.get_room_status("room-id").await;

// 获取详细房间信息
let room_info = sync_service.get_room_info("room-id").await;

数据流

  1. 服务器启动 → 预初始化房间并同步现有 Tree 数据
  2. 客户端请求连接到特定房间
  3. 房间存在性检查 → 验证房间是否已初始化
  4. 连接建立 → 仅当房间存在时才升级到 WebSocket
  5. 客户端自动获得完整的文档状态(通过 Yrs 的增量同步机制)
  6. 当客户端进行更改时,通过 YrsMiddleware 同步到 Yrs 文档
  7. 所有更改实时广播给房间内的其他客户端

通信协议

客户端和服务器之间的通信是通过一个 JSON 序列化的枚举 WsMessage 来处理的。这定义了协作服务的 API。

关键消息类型包括:

  • JoinRoom { room_id }: 客户端请求加入房间。
  • LeaveRoom { room_id }: 客户端离开房间。
  • YrsUpdate { room_id, update }: 客户端发送文档更改(作为二进制的 Yrs 更新负载)。
  • YrsSyncRequest { room_id, state_vector }: 客户端请求它尚未拥有的最新更改。

如何运行

要启动协作服务器,您需要实例化主要组件并运行 CollaborationServer。

以下是如何启动服务器的完整示例:

use moduforge_collaboration::{CollaborationServer, YrsManager, SyncService};
use std::sync::Arc;
use tokio::sync::Mutex;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 1. 初始化 YrsManager
    let yrs_manager = Arc::new(YrsManager::new());

    // 2. 初始化 SyncService
    let sync_service = Arc::new(SyncService::new(yrs_manager.clone()));

    // 3. 初始化 CollaborationServer
    let server = CollaborationServer::with_sync_service(
        yrs_manager,
        sync_service.clone(),
        8080
    );

    // 4. 关键步骤:使用现有数据预初始化房间
    let rooms_to_initialize = ["room1", "room2", "project-main"];
    
    for room_id in &rooms_to_initialize {
        // 从存储加载或创建初始 Tree
        if let Some(existing_tree) = load_room_data(room_id).await? {
            server.init_room_with_data(room_id, &existing_tree).await?;
            println!("✅ 房间 '{}' 已初始化", room_id);
        } else {
            println!("⚠️ 房间 '{}' 无初始数据,跳过初始化", room_id);
        }
    }

    // 5. 启动服务器
    println!("正在启动协作服务器于 127.0.0.1:8080...");
    println!("🔒 只有预初始化的房间才能接受客户端连接");
    server.start().await;

    Ok(())
}

async fn load_room_data(room_id: &str) -> anyhow::Result<Option<Tree>> {
    // 这里实现从数据库、文件等加载 Tree 的逻辑
    // 返回 Some(tree) 如果有现有数据,否则返回 None
    Ok(None)
}

错误处理示例

客户端连接错误处理

// 前端 JavaScript 示例
const ws = new WebSocket('ws://localhost:8080/collaboration/non-existent-room');

ws.onerror = function(error) {
    console.error('WebSocket 连接失败:', error);
    // 服务器会返回 404 状态码,表示房间不存在
};

// 或者使用 fetch 检查房间状态
async function checkRoomExists(roomId) {
    try {
        const response = await fetch(`http://localhost:8080/collaboration/${roomId}`);
        if (response.status === 404) {
            const error = await response.json();
            console.log('房间不存在:', error.message);
            return false;
        }
        return true;
    } catch (error) {
        console.error('检查房间状态失败:', error);
        return false;
    }
}

高级使用场景

动态房间管理

// 运行时创建新房间
pub async fn create_room_on_demand(
    server: &CollaborationServer,
    room_id: &str,
    initial_tree: &Tree
) -> Result<bool> {
    // 检查房间是否已存在
    if server.sync_service().yrs_manager().room_exists(room_id) {
        return Ok(false); // 房间已存在
    }
    
    // 初始化新房间
    server.init_room_with_data(room_id, initial_tree).await?;
    Ok(true)
}

房间生命周期管理

// 完整的房间生命周期
async fn room_lifecycle_example() -> Result<()> {
    let server = setup_server().await;
    let room_id = "example-room";
    
    // 1. 检查房间状态
    let initial_status = server.sync_service().get_room_status(room_id).await;
    assert_eq!(initial_status, RoomStatus::NotExists);
    
    // 2. 初始化房间
    let tree = create_initial_tree();
    server.init_room_with_data(room_id, &tree).await?;
    
    // 3. 验证房间已初始化
    let status = server.sync_service().get_room_status(room_id).await;
    assert_eq!(status, RoomStatus::Initialized);
    
    // 4. 客户端现在可以连接
    // WebSocket 连接: ws://localhost:8080/collaboration/example-room
    
    // 5. 房间下线
    server.offline_room(room_id, true).await?;
    
    // 6. 验证房间已下线
    let final_status = server.sync_service().get_room_status(room_id).await;
    assert_eq!(final_status, RoomStatus::NotExists);
    
    Ok(())
}

最佳实践

  1. 🔒 严格房间管理 - 只有预初始化的房间才能接受连接
  2. 📊 状态监控 - 定期检查房间状态和连接数
  3. ⚡ 预初始化 - 在服务器启动时预初始化常用房间
  4. 🧹 生命周期管理 - 及时下线不活跃的房间释放资源
  5. 🚨 错误处理 - 客户端应优雅处理房间不存在的情况
  6. 📝 日志记录 - 详细记录房间操作以便调试和监控

这种设计确保了协作功能的高安全性、可控性和优秀的用户体验。