Files
collab/AGENTS.md
T
2026-08-11 14:23:50 +08:00

73 lines
4.1 KiB
Markdown

# AGENTS.md — collab
Node.js (CommonJS) Yjs collaboration server. Maintains Yjs documents in memory, persists updates to Redis, and notifies downstream consumers of dirty documents via Redis Pub/Sub or RabbitMQ.
## Commands
```bash
npm start # node server.js — listens on PORT (default 1234)
```
No test, lint, typecheck, or build scripts exist. Verify by reading the code.
## Proto Codegen
`src/gen/` contains gRPC stubs generated from `proto/yoresee_doc/v1/yoresee_doc.proto` (parent repo). This directory is **gitignored**.
- Dev: generated automatically by `deploy/script/gen_proto.sh` from the parent repo.
- Prod: `Dockerfile.prod` generates stubs in a builder stage using `grpc-tools` and `protoc-gen-js`.
The file `src/grpc/client.js` imports from `../gen/yoresee_doc/v1/yoresee_doc_grpc_pb` — code will fail at startup if stubs are missing.
## Architecture
Entry: `server.js` → wires HTTP server, WebSocket gateway, Redis, gRPC, and graceful shutdown.
```
server.js
├── src/http/server.js — plain Node HTTP server (no Express)
│ └── src/http/router.js — routes: /health, /livez, /readyz, /api/active-rooms, /internal/yjs/*
├── src/ws-gateway.js — WebSocket upgrade handler, extracts room name from URL path
├── src/ws-utils.js — Yjs sync/awareness protocol, WSSharedDoc, connection lifecycle
├── src/doc-loader.js — loads doc: Redis list → gRPC snapshot → gRPC content (cascade)
├── src/persistence.js — binds Y.Doc 'update' event → Redis list append + dirty tracking
├── src/redis/ — Redis client, room tracking, document buffer lists
├── src/grpc/ — gRPC client to backend (DocumentService, SystemService)
├── src/mq.js — dirty-doc publisher (Redis Pub/Sub and/or RabbitMQ)
├── src/config.js — all config from process.env (no .env loader)
├── src/key.js — Redis key patterns
├── src/bootstrap.js — listen + init Redis/gRPC
└── src/lifecycle.js — graceful shutdown (SIGTERM/SIGINT)
```
## Key Patterns
- **Module style**: CommonJS (`require`/`module.exports`). No ESM.
- **Config**: plain `process.env` reads in `src/config.js`. No dotenv — env vars must be set externally.
- **Doc loading cascade**: Redis list buffers → gRPC `GetDocumentYjsSnapshot` → gRPC `GetDocumentContent` → empty doc.
- **Persistence**: updates appended to Redis list (`collab:yjs:doc:updates:{docId}`). Compacted into single state update every `DOC_UPDATE_COMPACT_THRESHOLD` (default 1000) updates.
- **Dirty-doc notification**: published after `DIRTY_DOC_NOTIFY_THRESHOLD` (default 200) updates per doc. MQ backend controlled by `DIRTY_DOC_MQ` env var (`redis` | `rabbitmq` | `both`).
- **Room naming**: WebSocket path is the room name (e.g. `/doc-{docId}`). Room name with `doc-` prefix is stripped to get the doc ID.
- **Redis keys**: defined in `src/key.js` — `collab:yjs:doc:updates:{docId}`, `collab:room:doc-{docId}`, `collab:yjs:dirty:doc`.
- **Health probes**: `/health`, `/livez`, `/readyz` — readiness depends on Redis connection and drain state.
## Environment Variables
| Variable | Default | Description |
|---|---|---|
| `PORT` | `1234` | HTTP listen port |
| `REDIS_HOST` | `redis` | Redis hostname |
| `REDIS_PORT` | `6379` | Redis port |
| `REDIS_PASSWORD` | `''` | Redis password |
| `REDIS_DB` | `0` | Redis DB index |
| `BACKEND_ADDR` | `backend:9090` | Backend gRPC address |
| `DIRTY_DOC_MQ` | `redis` | MQ type: `redis`, `rabbitmq`, or `both` |
| `DIRTY_DOC_TOPIC` | `collab.dirty_docs` | Dirty-doc pub/sub topic |
| `RABBITMQ_URL` | `''` | RabbitMQ connection URL (empty = disabled) |
| `INTERNAL_RPC_KEY` | `''` | gRPC metadata auth key (`x-internal-key`) |
| `DIRTY_DOC_NOTIFY_THRESHOLD` | `200` | Updates before publishing dirty-doc event |
| `DOC_UPDATES_TTL` | `259200` | TTL for doc update counters (seconds) |
| `DOC_UPDATE_COMPACT_THRESHOLD` | `1000` | Updates before compacting Redis list |
| `GC` | `true` | Yjs garbage collection (`false`/`0` to disable) |
| `BACKEND_SNAPSHOT_TIMEOUT_MS` | `3000` | Timeout for backend snapshot gRPC call |