5.1 KiB
5.1 KiB
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)
连接流程
- 客户端通过 WebSocket 连接到
/{roomName}(例如/doc-{docId}) ws-gateway.js从 URL 路径提取房间名ws-utils.js为房间加载或创建WSSharedDocdoc-loader.js尝试加载已有状态:- Redis 列表缓冲区 → gRPC
GetDocumentYjsSnapshot→ gRPCGetDocumentContent→ 空文档
- Redis 列表缓冲区 → gRPC
persistence.js绑定到 Y.Doc 的update事件- 更新被追加到 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 时:
- 服务器停止接受新连接
- 就绪探针返回
not_ready - 关闭现有 WebSocket 连接
- 关闭 MQ 连接
- 关闭 Redis 连接
- 进程退出(10 秒超时后强制退出)