Files
collab/docs/README_zh.md
2026-08-11 14:23:50 +08:00

5.1 KiB
Raw Permalink Blame History

Collab — Yjs 协作服务器

Node.js (CommonJS) 实时协作服务器,在内存中维护 Yjs 文档,将更新持久化到 Redis,并通过 Redis Pub/Sub 或 RabbitMQ 通知下游消费者脏文档。

快速启动

npm install
npm start

服务器监听 PORT(默认 1234)。

架构

server.js
├── src/http/server.js      — 原生 Node HTTP 服务器(无 Express)
│   └── src/http/router.js  — 路由:/health, /livez, /readyz, /api/active-rooms, /internal/yjs/*
├── src/ws-gateway.js       — WebSocket 升级处理器,从 URL 路径提取房间名
├── src/ws-utils.js         — Yjs 同步/感知协议,WSSharedDoc,连接生命周期
├── src/doc-loader.js       — 加载文档:Redis 列表 → gRPC 快照 → gRPC 内容(级联)
├── src/persistence.js      — 绑定 Y.Doc 'update' 事件 → Redis 列表追加 + 脏文档追踪
├── src/redis/              — Redis 客户端,房间追踪,文档缓冲区列表
├── src/grpc/               — gRPC 客户端连接后端(DocumentService, SystemService)
├── src/mq.js               — 脏文档发布者(Redis Pub/Sub 和/或 RabbitMQ)
├── src/config.js            — 所有配置来自 process.env(无 .env 加载器)
├── src/key.js               — Redis 键模式
├── src/bootstrap.js         — 监听 + 初始化 Redis/gRPC
└── src/lifecycle.js         — 优雅停机(SIGTERM/SIGINT)

连接流程

  1. 客户端通过 WebSocket 连接到 /{roomName}(例如 /doc-{docId})
  2. ws-gateway.js 从 URL 路径提取房间名
  3. ws-utils.js 为房间加载或创建 WSSharedDoc
  4. doc-loader.js 尝试加载已有状态:
    • Redis 列表缓冲区 → gRPC GetDocumentYjsSnapshot → gRPC GetDocumentContent → 空文档
  5. persistence.js 绑定到 Y.Doc 的 update 事件
  6. 更新被追加到 Redis 列表并追踪用于脏文档通知

持久化模型

  • 每次文档更新追加到 Redis 列表:collab:yjs:doc:updates:{docId}
  • 达到 DOC_UPDATE_COMPACT_THRESHOLD(默认 1000)次更新后,列表压缩为单个状态更新
  • 文档在 Redis 集合中标记为脏:collab:yjs:dirty:doc
  • 达到 DIRTY_DOC_NOTIFY_THRESHOLD(默认 200)次更新后,通过 MQ 发布脏文档事件

脏文档通知

脏文档事件通知下游消费者(snapshot-worker)文档已被修改。

MQ 后端由 DIRTY_DOC_MQ 环境变量控制:

  • redis — Redis Pub/Sub(默认,无消费者组)
  • rabbitmq — RabbitMQ 带消费者组
  • both — 同时发布到两者

载荷格式:

{
  "doc_id": "document-id",
  "ts": 1234567890
}

主题:DIRTY_DOC_TOPIC(默认 collab.dirty_docs)

HTTP 路由

健康探针:

  • GET /health — 整体健康状态
  • GET /livez — 存活探针
  • GET /readyz — 就绪探针(drain 期间失败)

API:

  • GET /api/active-rooms — 列出活跃的 WebSocket 房间

内部接口(snapshot-worker 使用):

  • GET /internal/yjs/doc-snapshot/{docId}?type={type} — 获取文档快照
  • GET /internal/yjs/doc/{docId} — 获取文档的二进制 Yjs 更新

WebSocket 协议

房间名从 WebSocket URL 路径提取。对于 /doc-{docId},文档 ID 为 {docId}。

协议消息:

  • 同步消息(Yjs 同步协议)
  • 感知消息(光标位置、用户存在状态)

当房间的最后一个连接关闭时,文档从内存中移除。

Proto 代码生成

src/gen/ 包含从父仓库的 proto/yoresee_doc/v1/yoresee_doc.proto 生成的 gRPC 存根。

此目录已gitignore。生产环境中,Dockerfile.prod 在构建时生成存根。开发环境从父仓库执行:

bash deploy/script/gen_proto.sh

环境变量

变量 默认值 描述
PORT 1234 HTTP 监听端口
REDIS_HOST redis Redis 主机名
REDIS_PORT 6379 Redis 端口
REDIS_PASSWORD '' Redis 密码
REDIS_DB 0 Redis DB 索引
BACKEND_ADDR backend:9090 后端 gRPC 地址
DIRTY_DOC_MQ redis MQ 类型:redis、rabbitmq 或 both
DIRTY_DOC_TOPIC collab.dirty_docs 脏文档发布/订阅主题
RABBITMQ_URL '' RabbitMQ 连接 URL(空 = 禁用)
INTERNAL_RPC_KEY '' gRPC 元数据认证键(x-internal-key)
DIRTY_DOC_NOTIFY_THRESHOLD 200 发布脏文档事件前的更新次数
DOC_UPDATES_TTL 259200 文档更新计数器的 TTL(秒)
DOC_UPDATE_COMPACT_THRESHOLD 1000 压缩 Redis 列表前的更新次数
GC true Yjs 垃圾回收(false/0 禁用)
BACKEND_SNAPSHOT_TIMEOUT_MS 3000 后端快照 gRPC 调用超时

Redis 键

在 src/key.js 中定义:

  • collab:yjs:doc:updates:{docId} — 文档更新列表
  • collab:room:doc-{docId} — 房间元数据
  • collab:yjs:dirty:doc — 脏文档集合

优雅停机

收到 SIGTERM 或 SIGINT 时:

  1. 服务器停止接受新连接
  2. 就绪探针返回 not_ready
  3. 关闭现有 WebSocket 连接
  4. 关闭 MQ 连接
  5. 关闭 Redis 连接
  6. 进程退出(10 秒超时后强制退出)