Skip to content

Conversation

@kagol
Copy link
Member

@kagol kagol commented Oct 22, 2025

PR

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • New Features
    • Real-time collaborative editing with multi-user sync, awareness and live cursors; WebSocket and WebRTC provider options and optional offline IndexedDB persistence.
    • Deployable backend service with Docker/Compose for persistent collaborative sessions.
  • Documentation
    • Detailed quick-starts and integration guides for frontend and backend collaborative setup.
  • Tests
    • End-to-end demo tests validating multi-user synchronization and editor behaviors.
  • Bug Fixes
    • Editor stability improvements via an applied patch to the editor package to reduce batching/conflict issues.

Yinlin124 and others added 10 commits July 27, 2025 22:39
…les (#289)

* feat: collaborative-editing modules init and import quill-cursor modules

* chore: 修改 collaborative-editing 依赖到 devDependencies

* feat:把 collaborative-editing 模块改为为手动注册

* docs: 基础 collaborative-editing 社区文档

* docs: 新增协同编辑演示

* fix:  新增 awareness 构建异常处理,去除 webrtc 和 websocket 中无用的 getter 函数以及 document 命名错误

* docs:修改文档顺序,修复 roomName

* docs:demo 新增表格协同
* feat: 默认开启光标和离线支持,自定义持久化文档,demo采用自部署后端代替

* fix:删除用户手动注册光标模块逻辑与相关文档

* refactor:新增QuillCursors types,新增监听回收

* fix: 修复demo公式 katex 依赖

* fix:修复用户断开光标无法移除问题

* fix:修复光标类型检测,默认开启光标逻辑

* fix: 删除无使用引入

* fix: 修复截图模块依赖
* test: 协同编辑自动化测试

* fix: 修复类型

* fix: 串行测试协同模块

* fix:启动两个浏览器作为协调测试
* feat:新增协同编辑后端子包

* fix:修复env.example URL与docker配置一致,修改 Readme

* fix:修复文档保存防抖内存泄露问题

* fix: 修复保存文档异步操作导致的错误文档释放

* refactor: import y-mongodb-provider as MongodbPersistence

* fix:新增类型判断,增大flushSize

* fix:修复 yjs 版本依赖,引入 vite 构建项目,提示 docker 容器中的 node 版本以支持 vite

* fix:删除无用环境变量

* docs: 修改部署方式和配置,新增自定义持久化指南

* fix:修复 js 引入和 vite config

* feat: 引入 pm2 支持,启动后检查服务器是否可连

* fix:  优化打包选项,pm2  移除项目依赖

* fix: 打包 input config, 本地启动服务器 readme 修复
* demo:修改光标模板样式

* fix: 协同实例 destory  事件绑定,indexdb 修改成 guid 解决本地缓存冲突问题

* fix:demo上传图片逻辑

* refactor:  customProvider 改名为 providerRegistry

* refactor:  简化协同模块参数,自定义 provider 统一类构造传参

* docs:修改文档结构,新增自定义 provider 示例

* fix: destory 异步,新增监听移除

* docs: 文档修复

* fix:修复文档

* fix:修复文档图片路径

* fix: clearIndexedDB 删除多余异步操作
* feat:yuque 协同演示

* fix: roomName

* fix:删除无用模板,解决光标遮挡问题

* fix: 补充 isSynced 类型
* test: 新增冲突编辑用例

* test: 新增webkit浏览器测试

* refactor: 使用不同的浏览器启动页面
* fix: 修复 demo upload 与表格的冲突

* fix:修复光标飘逸

* fix:quill2.0 patch fix batch sync issue

* fix:修复光标逻辑,新增防抖

* fix: 使用相对定位文档光标

* fix: 修复光标逻辑
* doc:修改后端启动方式

* doc: 新增架构图,示例代码
@github-actions github-actions bot added e2e-test enhancement New feature or request labels Oct 22, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 22, 2025

Walkthrough

Adds a full collaborative editing feature set: frontend Yjs-based module (providers, awareness, cursors, IndexedDB), a Node.js WebSocket backend with MongoDB persistence, docs, demo and E2E tests, pnpm patch for quill, and related build/config files.

Changes

Cohort / File(s) Summary
Root config & patch
\package.json`, `patches/[email protected]`, `.npmrc``
Adds pnpm patchedDependencies mapping for [email protected] to a local patch that prevents nested batching; enables pre/post npm scripts in .npmrc.
Collaborative Editing Backend
\packages/collaborative-editing-backend/*``
New backend package with Dockerfile, .dockerignore, .env.example, docker-compose.yml, PM2 ecosystem file, package.json, tsconfig, vite config, README, server startup (src/server.ts), env exports (src/env.ts), persistence interface (src/persistence/index.ts) and Mongo implementation (src/persistence/mongo.ts), and WS utilities (src/utils.ts).
Frontend collaborative module
\packages/fluent-editor/src/modules/collaborative-editing/*``
New collaborative-editing subsystem: types, CollaborativeEditor class, CollaborationModule, awareness (cursor binding), IndexedDB helper, provider registry, WebSocket/WebRTC provider wrappers, barrel exports and wiring.
Frontend dependencies & wiring
\packages/fluent-editor/package.json`, `packages/projects/package.json`, `packages/fluent-editor/src/modules/index.ts``
Adds Yjs-related deps (yjs, y-websocket, y-webrtc, y-quill, y-indexeddb) and quill-cursors; re-exports collaborative-editing from modules index; updates projects package deps.
Docs, demo & tests
\packages/docs/fluent-editor/**`, `packages/docs/fluent-editor/demos/collaborative-editing.vue`, `packages/docs/fluent-editor/demos/collaborative-editing.spec.ts`, `packages/projects/src/views/yuque/YuQue.vue``
Adds documentation page and sidebar entry, Vue demo component using collaborative provider, and an extensive Playwright E2E test suite simulating multi-user sync and editor actions.
Project README tweaks
\README.md`, `README.zh-CN.md``
Minor formatting/spacing adjustments around All Contributors badge.
Docs tooling
\packages/docs/package.json``
Expands install script to install both Chromium and Firefox for Playwright tests.

Sequence Diagram(s)

sequenceDiagram
  participant FE as Frontend Editor
  participant Provider as Provider (WS / WebRTC wrapper)
  participant Backend as WebSocket Backend
  participant DB as MongoDB

  FE->>Provider: init(provider options, room)
  Provider->>Backend: open socket / signal
  Backend->>DB: load persisted Y state on join
  DB-->>Backend: return persisted state
  Backend-->>Provider: send initial sync
  Provider-->>FE: apply initial sync

  FE->>Provider: local edits (encoded)
  Provider->>Backend: forward updates
  Backend->>DB: persist updates
  Backend->>Provider: broadcast update to peers
  Provider->>FE: peers receive update

  alt Awareness change
    FE->>Provider: awareness update
    Provider->>Backend: broadcast awareness
    Provider->>FE: peers awareness
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • vaebe

Poem

🐰 I hopped through diff and code so bright,

cursors twinkled in the midnight byte,
WebSockets hummed and Yjs did bind,
Mongo kept each change in mind,
now editors dance together — what a sight! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: support collaborative editing" accurately summarizes the primary objective of the changeset. The pull request comprehensively introduces collaborative editing capabilities across the project through a new backend service (packages/collaborative-editing-backend) with WebSocket and MongoDB support, a frontend module (CollaborativeEditor) with Yjs integration and multiple provider implementations (WebSocket, WebRTC), awareness/cursors features for real-time cursor tracking, comprehensive documentation, and end-to-end tests. The title is concise, specific, and clearly conveys the main change without being vague or overly detailed.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch main-collaborative

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/projects/src/views/yuque/YuQue.vue (1)

103-176: Add teardown on unmount to avoid leaking sockets, listeners, and timers.

No cleanup is registered; navigating away will leave WS connections alive.

Apply:

-import { onMounted, ref } from 'vue'
+import { onMounted, onBeforeUnmount, ref } from 'vue'
@@
 onMounted(() => {
   editor = new FluentEditor('#editor', {
     theme: 'snow',
     modules: {
       ...
     },
   })
 })
+
+onBeforeUnmount(() => {
+  try {
+    // Prefer editor.destroy() if provided
+    (editor as any)?.destroy?.()
+  } catch {}
+  try {
+    // Fallback: attempt to disconnect collaborative provider if API is exposed
+    const mod = (editor as any)?.getModule?.('collaborative-editing')
+    mod?.disconnect?.()
+  } catch {}
+})
🧹 Nitpick comments (43)
packages/collaborative-editing-backend/README.md (1)

59-68: Add blank line before the table.

The markdown table at line 60 should be preceded by a blank line for proper rendering and to comply with markdown best practices.

Apply this diff:

-可参照下方表格进行配置 `.env` 文件
+可参照下方表格进行配置 `.env` 文件
+
 | 变量名 | 必需 | 默认值 | 说明 |
packages/fluent-editor/src/modules/collaborative-editing/awareness/awareness.ts (2)

43-43: Consider safer null handling for Y.Doc.

The non-null assertion yText.doc! assumes doc is always present. While this may be guaranteed by the caller, consider adding a runtime check for defensive coding.

  const doc = yText.doc!
+  if (!doc) {
+    console.warn('Y.Text has no associated document')
+    return
+  }

58-64: Review the necessity of setTimeout deferral.

The setTimeout(..., 0) at line 58 defers cursor movement to the next event loop tick. While this can help with synchronization timing, consider documenting why this deferral is necessary (e.g., waiting for DOM updates, avoiding race conditions with Quill's internal state).

packages/fluent-editor/src/modules/collaborative-editing/awareness/index.ts (1)

1-2: Awareness barrel looks good; mirror explicit exports later if the surface grows.

Keeps imports tidy; consider naming exports explicitly once stabilized to avoid accidental API expansion.

packages/fluent-editor/src/modules/collaborative-editing/provider/index.ts (1)

1-3: Consider explicit named exports for better API clarity and control.

The star export correctly re-exposes all symbols from the three modules. However, switching to explicit named exports would make the public API more intentional and reduce the risk of unintentionally exposing new symbols if internal exports are added to these modules later. This is an optional improvement to API hygiene.

packages/collaborative-editing-backend/tsconfig.json (1)

3-13: Remove unused tsconfig directives to clarify Vite-based build intent; add Node types for better type support.

The backend uses Vite (vite build), not tsc for emission. With Vite, noEmit: true is correct (tsc only type-checks), and outDir/rootDir are ignored—they should be removed for clarity. Additionally, since server.ts directly uses Node APIs (node:http, process), add "types": ["node"] for better type support.

Remove the ts-node suggestion from the original review—this backend uses vite-node, not ts-node.

Recommended cleanup:

     "skipLibCheck": true
+    "types": ["node"]
   },
   "include": ["src"],
   "exclude": ["dist", "node_modules"]

and remove these unused lines:

-    "rootDir": "src",
-    "outDir": "dist",
packages/collaborative-editing-backend/Dockerfile (1)

1-9: Reproducible builds and container hardening

  • Align with repo’s pnpm workflow; avoid npm install --no-package-lock (non‑deterministic).
  • Don’t rely on global PM2; pin version. Run as non‑root; expose port; consider multi‑stage.

Option A (pnpm + non‑root):

-FROM node:22-alpine
+FROM node:22-alpine

 WORKDIR /app
-COPY package.json ./
-RUN npm install --no-package-lock && npm install pm2@latest -g
+RUN corepack enable && corepack prepare [email protected] --activate
+COPY package.json pnpm-lock.yaml ./
+RUN pnpm install --frozen-lockfile && pnpm add -g pm2@5
 COPY . .

-RUN npm run build
+RUN pnpm build
+EXPOSE 1234
+USER node
 CMD [ "pm2-runtime", "start", "ecosystem.config.cjs" ]

If you must keep npm, ensure a lockfile and use npm ci. Also add a .dockerignore to shrink context.

packages/collaborative-editing-backend/docker-compose.yml (1)

3-26: Pin images, avoid baked‑in creds, and add healthchecks

  • Avoid :latest tags; pin MongoDB (e.g., mongo:7.0) and the websocket image, or build from this repo.
  • Don’t hardcode admin creds in source; prefer .env or Docker secrets.
  • Add MongoDB healthcheck and gate depends_on on health.
   mongodb:
-    image: mongo:latest
+    image: mongo:7.0
     container_name: yjs-mongodb
     restart: always
     ports:
       - "27017:27017"
-    environment:
-      MONGO_INITDB_ROOT_USERNAME: admin
-      MONGO_INITDB_ROOT_PASSWORD: admin!123
+    env_file:
+      - .env
+    healthcheck:
+      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
     volumes:
       - mongodb_data:/data/db

   websocket-server:
-    image: yinlin124/collaborative-editor-backend:latest
+    # Prefer building the local backend image to match this codebase:
+    build: .
     container_name: yjs-websocket-server
     restart: always
     ports:
       - "${PORT:-1234}:${PORT:-1234}"
     env_file:
       - .env
     depends_on:
-      - mongodb
+      mongodb:
+        condition: service_healthy

If distributing a prebuilt image, move it to your org namespace and pin a digest.

packages/collaborative-editing-backend/package.json (2)

5-10: Local start depends on globally installed PM2

npm run start will fail unless PM2 is globally installed. Add PM2 as a dev dependency or a script that uses pnpm dlx/npx.

   "scripts": {
     "dev": "vite-node src/server.ts",
-    "start": "npm run build && pm2 start ecosystem.config.cjs",
-    "stop": "pm2 stop ecosystem.config.cjs",
+    "start": "npm run build && pm2 start ecosystem.config.cjs",
+    "stop": "pm2 stop ecosystem.config.cjs",
     "build": "vite build"
   },
+  "engines": { "node": ">=20" },
+  "private": true,

Add:

   "devDependencies": {
+    "pm2": "^5.4.0",
     ...
   }

Confirm vite.config.* targets Node (no browser shims) so vite build produces a runnable dist/server.js.


12-18: Version pinning and security posture

Caret ranges can drift. For server infra, prefer pinning or at least tilde ranges, and track advisories for mongodb, ws, and y-mongodb-provider.

Would you like a follow‑up PR to lock versions and add npm audit/pnpm audit to CI?

packages/docs/fluent-editor/docs/demo/collaborative-editing.md (4)

64-64: Avoid branch‑specific links in docs

ospp-2025 may 404 after merge. Prefer linking to main or a relative path.

-[YuQue.vue](https://github.com/opentiny/tiny-editor/blob/ospp-2025/collaborative-editing/packages/projects/src/views/yuque/YuQue.vue)
+[YuQue.vue](https://github.com/opentiny/tiny-editor/blob/main/packages/projects/src/views/yuque/YuQue.vue)

200-214: Don’t ship hardcoded public IPs in examples

Use ws(s)://<host>:<port> or import.meta.env.VITE_WS_URL to avoid stale/leaky endpoints.

-    serverUrl: 'wss://120.26.92.145:1234',
+    serverUrl: import.meta.env.VITE_WS_URL ?? 'ws://localhost:1234',

120-130: Markdownlint: add blank lines around tables (MD058)

Insert a blank line before and after each table block to satisfy MD058.


190-199: Minor CN word choice nits from linter

Consider “房间名” instead of “房间名称”,以及“房间口令/密码”保持一致性。Not blocking.

Also applies to: 222-231

packages/docs/fluent-editor/demos/collaborative-editing.vue (4)

11-13: Type‑safe window extensions

Add global typings to avoid TS errors when assigning to window.

 window.katex = katex
 window.Html2Canvas = Html2Canvas
+declare global {
+  interface Window {
+    katex: typeof katex
+    Html2Canvas: typeof Html2Canvas
+  }
+}

96-102: External IP in demo config

Avoid hardcoded IPs; prefer env with a localhost fallback.

-                serverUrl: 'wss://120.26.92.145:1234',
+                serverUrl: import.meta.env.VITE_WS_URL ?? 'ws://localhost:1234',

55-58: Handle dynamic import failures

Add a catch to avoid unhandled rejections and surface errors.

   ]).then(
     ([
       { default: FluentEditor, generateTableUp, CollaborationModule },
       { defaultCustomSelect, TableMenuContextmenu, TableSelection, TableUp },
     ]) => {
       if (!editorRef.value) return
       ...
-    },
-  )
+    },
+  ).catch((err) => {
+    console.error('[collab-demo] bootstrap failed:', err)
+  })

Also applies to: 71-75


71-71: Remove unused variable

Delta is imported but unused.

-      const Delta = FluentEditor.import('delta')
+      // const Delta = FluentEditor.import('delta')
packages/collaborative-editing-backend/src/server.ts (1)

28-31: Graceful shutdown: close WS and HTTP; handle SIGTERM

Docker sends SIGTERM. Also close wss and server before exiting.

-process.on('SIGINT', async () => {
-  await persistence.close()
-  process.exit(0)
-})
+async function shutdown(code = 0) {
+  try { wss.close() } catch {}
+  try { server.closeAllConnections?.() } catch {}
+  try { server.close?.() } catch {}
+  try { await persistence.close() } catch {}
+  process.exit(code)
+}
+process.on('SIGINT', () => void shutdown(0))
+process.on('SIGTERM', () => void shutdown(0))

Consider also setting server.headersTimeout/keepAliveTimeout for slowloris protection.

packages/projects/src/views/yuque/YuQue.vue (4)

163-172: Avoid dangerouslyPasteHTML; convert to Delta or predefine a Delta.

Using Quill’s paste bypass risks if content ever becomes external. Convert then set.

Apply:

-              editor.clipboard.dangerouslyPasteHTML(0, DEFAULT_CONTENT)
+              const delta = editor.clipboard.convert(DEFAULT_CONTENT)
+              editor.setContents(delta, 'api')

19-19: Derive room name from route/doc id to prevent collisions across pages.

Static ROOM_NAME causes unintended shared docs.

Example:

-const ROOM_NAME = import.meta.env.VITE_COLLAB_ROOM ?? 'yuque-document-demo-room'
+const ROOM_NAME =
+  import.meta.env.VITE_COLLAB_ROOM ??
+  `yuque:${typeof location !== 'undefined' ? encodeURIComponent(location.pathname) : 'demo'}`

145-147: Stabilize identity across reloads (optional).

Random user/color resets every load; persist to localStorage for better UX in demos.

Example:

-            name: `user-${Math.floor(Math.random() * 1000)}`,
-            color: `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`,
+            name: localStorage.getItem('demoUser') || (() => {
+              const v = `user-${Math.floor(Math.random() * 1000)}`
+              localStorage.setItem('demoUser', v)
+              return v
+            })(),
+            color: localStorage.getItem('demoColor') || (() => {
+              const v = `#${Math.floor(Math.random() * 0xffffff).toString(16).padStart(6,'0')}`
+              localStorage.setItem('demoColor', v)
+              return v
+            })(),

15-15: Template ref typing nit.

ref<HTMLElement>() can be null until mounted.

Use const headerListRef = ref<HTMLElement | null>(null).

packages/collaborative-editing-backend/ecosystem.config.cjs (1)

12-18: Consider multi‑instance/cluster and graceful shutdown (optional).

For WS servers, 1 instance avoids sticky-session complexity. If you later scale, add sticky session support and wait_ready, listen_timeout, and shutdown_with_message.

I can draft a cluster/sticky PM2 config when needed.

packages/collaborative-editing-backend/src/persistence/index.ts (1)

10-16: Avoid null checks at call sites with a no‑op default.

A default no‑op persistence reduces scattered null checks and simplifies shutdown.

Example:

-let persistence: Persistence | null = null
+const noop: Persistence = {
+  async connect() {},
+  async bindState() {},
+  async writeState() {},
+  async close() {},
+}
+let persistence: Persistence = noop
@@
-export function setPersistence(p: Persistence | null) {
-  persistence = p
+export function setPersistence(p: Persistence | null) {
+  persistence = p ?? noop
 }
 
-export const getPersistence = () => persistence
+export const getPersistence = () => persistence
packages/docs/fluent-editor/demos/collaborative-editing.spec.ts (4)

3-3: Parameterize the demo URL for CI/dev parity.

Hard‑coded localhost hinders CI runs behind different hosts/ports.

Apply:

-const DEMO_URL = 'http://localhost:5173/tiny-editor/docs/demo/collaborative-editing'
+const DEMO_URL = process.env.DEMO_URL ?? 'http://localhost:5173/tiny-editor/docs/demo/collaborative-editing'

Export DEMO_URL in CI (e.g., Playwright config or workflow env).


7-23: Launch contexts: ensure headless and consistent viewport (optional).

Explicit headless/viewport reduces flakiness across runners.

Example:

-  const browser1 = await chromium.launch()
-  const browser2 = await firefox.launch()
+  const browser1 = await chromium.launch({ headless: true })
+  const browser2 = await firefox.launch({ headless: true })
+  const ctx1 = await browser1.newContext({ viewport: { width: 1280, height: 800 } })
+  const ctx2 = await browser2.newContext({ viewport: { width: 1280, height: 800 } })
-  const page1 = await browser1.newPage()
-  const page2 = await browser2.newPage()
+  const page1 = await ctx1.newPage()
+  const page2 = await ctx2.newPage()

41-55: Tear‑down improvements (optional).

Close contexts/pages before browsers; guard with timeouts to avoid hanging CI.

I can provide a small helper to close in order with Promise.allSettled.


299-316: Conflict test: broaden acceptance window (optional).

Depending on provider order, transient divergence can last > one tick. Consider longer timeout or checking for convergence with a max duration.

Use expect.poll(fn, { timeout: 5000 }) with a small interval.

packages/fluent-editor/src/modules/collaborative-editing/types.ts (1)

30-30: Consider using a more specific type than object.

The object type is too broad and doesn't provide meaningful type information. Consider using Record<string, any> or defining a proper interface for cursor configuration options.

Apply this diff if you want to make it more specific:

-export type CursorsConfig = boolean | object
+export type CursorsConfig = boolean | Record<string, any>

Alternatively, define a proper interface if the cursor configuration shape is known:

export interface CursorOptions {
  // Define specific cursor configuration properties
}

export type CursorsConfig = boolean | CursorOptions
packages/collaborative-editing-backend/src/persistence/mongo.ts (1)

62-64: Document why writeState is a no-op.

The writeState method is required by the Persistence interface but implemented as a no-op. Add a comment explaining why this is intentional (likely because updates are persisted automatically via the event handler in bindState).

Apply this diff to add documentation:

 async writeState(docName: string, ydoc: Y.Doc) {
+    // No-op: Updates are automatically persisted via the 'update' event handler
+    // registered in bindState()
     return Promise.resolve()
   }
packages/fluent-editor/src/modules/collaborative-editing/module.ts (1)

10-14: Consider potential memory leak from unremoved event listener.

The beforeunload event listener is registered with { once: true }, which ensures it fires only once. However, if the CollaborationModule is destroyed before the page unloads (e.g., during component unmounting in a SPA), the listener remains attached. While { once: true } prevents repeated execution, consider storing the listener reference and removing it in the destroy method for complete cleanup.

Apply this diff to enable cleanup:

 export class CollaborationModule {
   private collaborativeEditor: CollaborativeEditor
+  private beforeUnloadHandler: () => void

   constructor(public quill: FluentEditor, public options: any) {
     this.collaborativeEditor = new CollaborativeEditor(quill, options)

-    window.addEventListener(
+    this.beforeUnloadHandler = () => {
+      void this.collaborativeEditor.destroy().catch(err => console.warn('[yjs] destroy failed:', err))
+    }
+    window.addEventListener(
       'beforeunload',
-      () => { void this.collaborativeEditor.destroy().catch(err => console.warn('[yjs] destroy failed:', err)) },
+      this.beforeUnloadHandler,
       { once: true },
     )
   }

   // ... other methods ...

   public async destroy() {
+    window.removeEventListener('beforeunload', this.beforeUnloadHandler)
     await this.collaborativeEditor.destroy()
   }
 }
packages/fluent-editor/src/modules/collaborative-editing/collaborative-editing.ts (3)

65-76: Store QuillBinding reference for proper cleanup.

The QuillBinding instance (Line 68-72) is created but not stored in a field, making it impossible to explicitly destroy during cleanup. While Yjs bindings may clean up automatically, storing the reference allows for explicit cleanup and better resource management.

Apply this diff to store and clean up the binding:

 export class CollaborativeEditor {
   private ydoc: Y.Doc = new Y.Doc()
   private provider?: UnifiedProvider
   private awareness: Awareness
   private cursors: QuillCursors | null
+  private quillBinding?: QuillBinding
   private cleanupBindings: (() => void) | null = null
   private clearIndexedDB: (() => void) | null = null

   // ... constructor code ...

   if (this.provider) {
     const ytext = this.ydoc.getText('tiny-editor')
     this.cleanupBindings = bindAwarenessToCursors(this.awareness, this.cursors, quill, ytext) || null
-    new QuillBinding(
+    this.quillBinding = new QuillBinding(
       ytext,
       this.quill,
       this.awareness,
     )
   }

   // ... other methods ...

   public async destroy() {
     this.cleanupBindings?.()
     this.provider?.destroy?.()
+    this.quillBinding?.destroy?.()
     this.cursors?.clearCursors()
     this.awareness?.destroy?.()
     this.clearIndexedDB?.()
     this.ydoc?.destroy?.()
   }

67-67: Remove redundant null coalescing.

The || null on Line 67 is redundant since bindAwarenessToCursors already returns (() => void) | void according to the type signature, and the field type is (() => void) | null. The nullish coalescing doesn't add value here.

Apply this diff to simplify:

-    this.cleanupBindings = bindAwarenessToCursors(this.awareness, this.cursors, quill, ytext) || null
+    this.cleanupBindings = bindAwarenessToCursors(this.awareness, this.cursors, quill, ytext) ?? null

Or even better, adjust the field type to match the return type:

-  private cleanupBindings: (() => void) | null = null
+  private cleanupBindings?: () => void

Then use:

-    this.cleanupBindings = bindAwarenessToCursors(this.awareness, this.cursors, quill, ytext) || null
+    this.cleanupBindings = bindAwarenessToCursors(this.awareness, this.cursors, quill, ytext)

107-114: Add error handling in destroy method.

The destroy method uses optional chaining to safely call methods that might not exist, but it doesn't handle potential errors if these methods throw exceptions. Consider wrapping the cleanup in a try-catch block to ensure all cleanup steps attempt to execute even if one fails.

Apply this diff to add error handling:

 public async destroy() {
-    this.cleanupBindings?.()
-    this.provider?.destroy?.()
-    this.cursors?.clearCursors()
-    this.awareness?.destroy?.()
-    this.clearIndexedDB?.()
-    this.ydoc?.destroy?.()
+    const cleanup = async () => {
+      try { this.cleanupBindings?.() } catch (e) { console.warn('[yjs] cleanupBindings error:', e) }
+      try { this.provider?.destroy?.() } catch (e) { console.warn('[yjs] provider destroy error:', e) }
+      try { this.cursors?.clearCursors() } catch (e) { console.warn('[yjs] cursors clear error:', e) }
+      try { this.awareness?.destroy?.() } catch (e) { console.warn('[yjs] awareness destroy error:', e) }
+      try { this.clearIndexedDB?.() } catch (e) { console.warn('[yjs] IndexedDB clear error:', e) }
+      try { this.ydoc?.destroy?.() } catch (e) { console.warn('[yjs] ydoc destroy error:', e) }
+    }
+    await cleanup()
   }
packages/fluent-editor/src/modules/collaborative-editing/provider/providerRegistry.ts (2)

30-33: Consider making the provider registry immutable.

The providerRegistry is declared as a mutable object, which could lead to unintended modifications. While registerProviderType is provided for extensibility, consider using as const or Object.freeze() to prevent accidental mutations, or document that the registry is intentionally mutable for plugins.

If the registry should be extensible by plugins, add a comment documenting this:

+// Mutable registry to allow custom provider registration via registerProviderType
 const providerRegistry: ProviderRegistry = {
   websocket: WebsocketProviderWrapper,
   webrtc: WebRTCProviderWrapper,
 }

Otherwise, if extensibility isn't needed, make it immutable:

-const providerRegistry: ProviderRegistry = {
+const providerRegistry = {
   websocket: WebsocketProviderWrapper,
   webrtc: WebRTCProviderWrapper,
-}
+} as const

35-38: Simplify registerProviderType implementation.

The type casting on Lines 36-37 is redundant and verbose. The assignment can be simplified since providerClass already matches the expected type.

Apply this diff to simplify:

 export function registerProviderType<T>(type: string, providerClass: ProviderConstructor<T>) {
-  providerRegistry[type as string]
-    = providerClass as ProviderConstructor
+  providerRegistry[type] = providerClass
 }
packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts (2)

35-42: Surface errors via onError in lifecycle methods.

Currently only logged; bubble to consumers.

   connect = () => {
     try {
       this.provider.connect()
     }
     catch (error) {
       console.warn('[yjs] Error connecting WebSocket provider:', error)
+      this.onError?.(error instanceof Error ? error : new Error(String(error)))
     }
   }
@@
   destroy = () => {
     try {
       this.provider.destroy()
     }
     catch (error) {
       console.warn('[yjs] Error destroying WebSocket provider:', error)
+      this.onError?.(error instanceof Error ? error : new Error(String(error)))
     }
   }

Also applies to: 44-51


11-14: Tighten types for options.

Avoid any for awareness; use the actual type.

-  awareness?: any
+  awareness?: Awareness
packages/fluent-editor/src/modules/collaborative-editing/provider/webrtc.ts (1)

30-37: Surface errors via onError in lifecycle methods.

   connect = () => {
     try {
       this.provider.connect()
     }
     catch (error) {
       console.warn('[yjs] Error connecting WebRTC provider:', error)
+      this.onError?.(error instanceof Error ? error : new Error(String(error)))
     }
   }
@@
   destroy = () => {
     try {
       this.provider.destroy()
     }
     catch (error) {
       console.warn('[yjs] Error destroying WebRTC provider:', error)
+      this.onError?.(error instanceof Error ? error : new Error(String(error)))
     }
   }

Also applies to: 39-46, 48-57

packages/collaborative-editing-backend/src/utils.ts (3)

116-125: Remove duplicate docs.delete and avoid early deletion before persistence completes.

docs.delete(doc.name) is called both inside and outside the writeState promise. Keep the one after destroy().

     if (doc.conns.size === 0 && persistence !== null) {
       persistence.writeState(doc.name, doc).then(() => {
         if (doc.conns.size === 0) {
           doc.destroy()
           docs.delete(doc.name)
         }
       })
-      docs.delete(doc.name)
     }

30-33: Nit: rename contentInitializorcontentInitializer for clarity.

Spelling fix improves readability and discoverability.

-let contentInitializor: (ydoc: Y.Doc) => Promise<void> = (_ydoc: Y.Doc) => Promise.resolve()
-export function setContentInitializor(f: (ydoc: Y.Doc) => Promise<void>): void {
-  contentInitializor = f
+let contentInitializer: (ydoc: Y.Doc) => Promise<void> = (_ydoc: Y.Doc) => Promise.resolve()
+export function setContentInitializer(f: (ydoc: Y.Doc) => Promise<void>): void {
+  contentInitializer = f
 }
@@
-    this.whenInitialized = contentInitializor(this)
+    this.whenInitialized = contentInitializer(this)

Also applies to: 35-75


133-145: Minor hardening in send().

Return early after closing stale connections; also standardize callback error check.

   if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) {
     closeConn(doc, conn)
+    return
   }
   try {
-    conn.send(m, (err) => {
-      if (err != null) closeConn(doc, conn)
+    conn.send(m, (err) => {
+      if (err) closeConn(doc, conn)
     })
   }
   catch (e) {
     closeConn(doc, conn)
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 71314b3 and 89c358a.

⛔ Files ignored due to path filters (2)
  • packages/docs/fluent-editor/public/Collab-arch.png is excluded by !**/*.png
  • packages/docs/fluent-editor/public/Tiny-editor-demo.png is excluded by !**/*.png
📒 Files selected for processing (35)
  • package.json (1 hunks)
  • packages/collaborative-editing-backend/.dockerignore (1 hunks)
  • packages/collaborative-editing-backend/.env.example (1 hunks)
  • packages/collaborative-editing-backend/Dockerfile (1 hunks)
  • packages/collaborative-editing-backend/README.md (1 hunks)
  • packages/collaborative-editing-backend/docker-compose.yml (1 hunks)
  • packages/collaborative-editing-backend/ecosystem.config.cjs (1 hunks)
  • packages/collaborative-editing-backend/package.json (1 hunks)
  • packages/collaborative-editing-backend/src/env.ts (1 hunks)
  • packages/collaborative-editing-backend/src/persistence/index.ts (1 hunks)
  • packages/collaborative-editing-backend/src/persistence/mongo.ts (1 hunks)
  • packages/collaborative-editing-backend/src/server.ts (1 hunks)
  • packages/collaborative-editing-backend/src/utils.ts (1 hunks)
  • packages/collaborative-editing-backend/tsconfig.json (1 hunks)
  • packages/collaborative-editing-backend/vite.config.ts (1 hunks)
  • packages/docs/fluent-editor/.vitepress/sidebar.ts (1 hunks)
  • packages/docs/fluent-editor/demos/collaborative-editing.spec.ts (1 hunks)
  • packages/docs/fluent-editor/demos/collaborative-editing.vue (1 hunks)
  • packages/docs/fluent-editor/docs/demo/collaborative-editing.md (1 hunks)
  • packages/fluent-editor/package.json (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/awareness/awareness.ts (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/awareness/index.ts (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/awareness/y-indexeddb.ts (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/collaborative-editing.ts (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/index.ts (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/module.ts (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/provider/index.ts (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/provider/providerRegistry.ts (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/provider/webrtc.ts (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts (1 hunks)
  • packages/fluent-editor/src/modules/collaborative-editing/types.ts (1 hunks)
  • packages/fluent-editor/src/modules/index.ts (1 hunks)
  • packages/projects/package.json (1 hunks)
  • packages/projects/src/views/yuque/YuQue.vue (4 hunks)
  • patches/[email protected] (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-23T02:26:57.912Z
Learnt from: Yinlin124
PR: opentiny/tiny-editor#346
File: patches/[email protected]:5-18
Timestamp: 2025-09-23T02:26:57.912Z
Learning: Quill's scroll.optimize() method has built-in batch protection with `if (this.batch) return;` logic, so it doesn't need to be gated when implementing custom batching logic in Quill patches.

Applied to files:

🧬 Code graph analysis (10)
packages/fluent-editor/src/modules/collaborative-editing/module.ts (1)
packages/fluent-editor/src/modules/collaborative-editing/collaborative-editing.ts (1)
  • CollaborativeEditor (12-115)
packages/collaborative-editing-backend/vite.config.ts (1)
packages/collaborative-editing-backend/ecosystem.config.cjs (1)
  • path (2-2)
packages/collaborative-editing-backend/src/persistence/mongo.ts (2)
packages/collaborative-editing-backend/src/persistence/index.ts (1)
  • Persistence (3-8)
packages/collaborative-editing-backend/src/env.ts (3)
  • MONGODB_URL (4-4)
  • MONGODB_DB (5-5)
  • MONGODB_COLLECTION (6-6)
packages/fluent-editor/src/modules/collaborative-editing/provider/providerRegistry.ts (3)
packages/fluent-editor/src/modules/collaborative-editing/types.ts (1)
  • ProviderEventHandlers (5-10)
packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts (1)
  • WebsocketProviderWrapper (20-142)
packages/fluent-editor/src/modules/collaborative-editing/provider/webrtc.ts (1)
  • WebRTCProviderWrapper (16-125)
packages/fluent-editor/src/modules/collaborative-editing/types.ts (4)
packages/fluent-editor/src/modules/collaborative-editing/collaborative-editing.ts (1)
  • isSynced (99-101)
packages/fluent-editor/src/modules/collaborative-editing/provider/webrtc.ts (2)
  • isSynced (118-120)
  • WebRTCProviderOptions (7-14)
packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts (2)
  • isSynced (135-137)
  • WebsocketProviderOptions (7-18)
packages/fluent-editor/src/modules/collaborative-editing/awareness/awareness.ts (1)
  • AwarenessOptions (17-21)
packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts (4)
packages/fluent-editor/src/modules/collaborative-editing/provider/providerRegistry.ts (1)
  • UnifiedProvider (19-28)
packages/fluent-editor/src/modules/collaborative-editing/collaborative-editing.ts (1)
  • isSynced (99-101)
packages/fluent-editor/src/modules/collaborative-editing/provider/webrtc.ts (1)
  • isSynced (118-120)
packages/fluent-editor/src/modules/collaborative-editing/types.ts (1)
  • ProviderEventHandlers (5-10)
packages/fluent-editor/src/modules/collaborative-editing/provider/webrtc.ts (4)
packages/fluent-editor/src/modules/collaborative-editing/provider/providerRegistry.ts (1)
  • UnifiedProvider (19-28)
packages/fluent-editor/src/modules/collaborative-editing/collaborative-editing.ts (1)
  • isSynced (99-101)
packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts (1)
  • isSynced (135-137)
packages/fluent-editor/src/modules/collaborative-editing/types.ts (1)
  • ProviderEventHandlers (5-10)
packages/collaborative-editing-backend/src/server.ts (4)
packages/collaborative-editing-backend/src/utils.ts (1)
  • setupWSConnection (154-205)
packages/collaborative-editing-backend/src/persistence/mongo.ts (1)
  • MongoPersistence (13-70)
packages/collaborative-editing-backend/src/persistence/index.ts (1)
  • setPersistence (12-14)
packages/collaborative-editing-backend/src/env.ts (2)
  • PORT (2-2)
  • HOST (1-1)
packages/collaborative-editing-backend/src/utils.ts (2)
packages/collaborative-editing-backend/src/env.ts (1)
  • GC_ENABLED (8-8)
packages/collaborative-editing-backend/src/persistence/index.ts (1)
  • getPersistence (16-16)
packages/fluent-editor/src/modules/collaborative-editing/collaborative-editing.ts (4)
packages/fluent-editor/src/modules/collaborative-editing/provider/providerRegistry.ts (2)
  • UnifiedProvider (19-28)
  • createProvider (44-57)
packages/fluent-editor/src/modules/collaborative-editing/types.ts (1)
  • YjsOptions (32-44)
packages/fluent-editor/src/modules/collaborative-editing/awareness/awareness.ts (2)
  • setupAwareness (23-33)
  • bindAwarenessToCursors (35-141)
packages/fluent-editor/src/modules/collaborative-editing/awareness/y-indexeddb.ts (1)
  • setupIndexedDB (4-12)
🪛 dotenv-linter (4.0.0)
packages/collaborative-editing-backend/.env.example

[warning] 5-5: [UnorderedKey] The MONGODB_DB key should go before the MONGODB_URL key

(UnorderedKey)


[warning] 6-6: [UnorderedKey] The MONGODB_COLLECTION key should go before the MONGODB_DB key

(UnorderedKey)

🪛 LanguageTool
packages/docs/fluent-editor/docs/demo/collaborative-editing.md

[uncategorized] ~193-~193: "间"不能与“名称”搭配,请换量词
Context: ...tring | 是 | - | 房间名称 | |connect |b...

(wa5)


[uncategorized] ~225-~225: "间"不能与“名称”搭配,请换量词
Context: ...ring | 是 | - | 房间名称 | |signaling |str...

(wa5)


[uncategorized] ~229-~229: "间"不能与“密码”搭配,请换量词
Context: ...ring | 否 | - | 房间密码 | |peerOpts |Rec...

(wa5)

🪛 markdownlint-cli2 (0.18.1)
packages/collaborative-editing-backend/README.md

60-60: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

packages/docs/fluent-editor/docs/demo/collaborative-editing.md

230-230: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🔇 Additional comments (12)
patches/[email protected] (1)

1-33: LGTM! Batching guard prevents nested batch issues.

The patch correctly prevents nested batching during delta application by checking for existing batch state before calling batchStart()/batchEnd(). This is essential for collaborative editing scenarios where batches may already be active during composition or real-time updates. The unconditional call to scroll.optimize() at line 31 is correct, as it has built-in batch protection.

Based on learnings.

packages/projects/package.json (1)

14-25: Verify y-webrtc version pinning is intentional.

Similar to the fluent-editor package, y-webrtc is pinned to 10.3.0 (line 23) without a caret, while other Yjs dependencies use caret ranges. This inconsistency appears across multiple packages. Please confirm this is intentional for compatibility reasons.

package.json (1)

52-56: LGTM! Patch configuration is correct.

The pnpm patch configuration correctly references the Quill patch at patches/[email protected] to apply the batching guard discussed earlier. This ensures the patch is applied during installation.

packages/collaborative-editing-backend/vite.config.ts (1)

1-30: Verify Node.js version compatibility with ES2022 target.

The build configuration looks solid with SSR mode, module preservation, and sourcemaps. However, the target: 'es2022' at line 10 requires Node.js 18.0+ for full compatibility. Please ensure:

  • The project's Node.js version requirement is documented (package.json engines field)
  • The deployment environment supports Node.js 18+

You can add an engines field to the backend's package.json if not already present:

"engines": {
  "node": ">=18.0.0"
}
packages/collaborative-editing-backend/.env.example (1)

1-6: Static analysis ordering suggestion can be safely ignored.

The dotenv-linter suggests alphabetical key ordering, but the current logical grouping (network config, then MongoDB config) is more readable and maintainable than strict alphabetical order. This is a false positive.

packages/fluent-editor/package.json (1)

52-65: All collaborative editing dependencies are current and secure.

Verification confirms all versions are at their latest npm releases, and y-webrtc has no detected security advisories. The pinning of y-webrtc to 10.3.0 (without caret) is not a concern, as it's the current stable release; this pinning approach is acceptable for critical dependencies when stability is prioritized.

packages/fluent-editor/src/modules/index.ts (1)

1-1: Expose collaborative-editing via modules index — OK.

Consistent with existing star re-export style.

packages/docs/fluent-editor/.vitepress/sidebar.ts (1)

30-30: Docs link addition verified and approved.

The target page exists at packages/docs/fluent-editor/docs/demo/collaborative-editing.md and the sidebar entry is properly configured in the VitePress build structure. Entry format matches existing patterns and the link path correctly aligns with the file location.

packages/fluent-editor/src/modules/collaborative-editing/index.ts (1)

1-2: Barrel looks good

Centralized exports improve DX. No runtime impact.

packages/projects/src/views/yuque/YuQue.vue (1)

150-154: Confirm selectionChangeSource: null is valid for the module.

If the module expects 'user'|'api'|'silent', null may disable logic.

Point me to the module’s type/README, or adjust to a supported literal.

packages/collaborative-editing-backend/ecosystem.config.cjs (1)

11-11: Verify Node/PM2 support for --env-file; add fallback.

--env-file needs Node 20+. Older runtimes will ignore/fail. PM2 also supports env_file or load via dotenv in server.

If Node < 20 in your deploy, switch to one of:

  • PM2 env_file: '.env'
  • Load dotenv in src/server.ts at bootstrap.

Optionally set interpreter: 'node' and pin Node version in your PM2 ecosystem/docker image.

packages/fluent-editor/src/modules/collaborative-editing/provider/webrtc.ts (1)

28-29: Initialize type field.

-  type: 'webrtc'
+  type: 'webrtc' = 'webrtc'

Likely an incorrect or invalid review comment.

Comment on lines +1 to +8
export const HOST = process.env.HOST!
export const PORT = Number.parseInt(process.env.PORT!)

export const MONGODB_URL = process.env.MONGODB_URL! as string
export const MONGODB_DB = process.env.MONGODB_DB! as string
export const MONGODB_COLLECTION = process.env.MONGODB_COLLECTION! as string

export const GC_ENABLED = (process.env.GC !== 'false' && process.env.GC !== '0') as boolean
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden env handling: validate, default sensibly, and fail fast with clear errors.

Non‑null assertions hide misconfig at compile time and crash later; GC undefined currently evaluates to true.

Apply:

+function requireEnv(name: string): string {
+  const v = process.env[name]
+  if (!v) throw new Error(`Missing required env: ${name}`)
+  return v
+}
+
-export const HOST = process.env.HOST!
-export const PORT = Number.parseInt(process.env.PORT!)
+export const HOST = requireEnv('HOST')
+const PORT_STR = requireEnv('PORT')
+export const PORT = Number.parseInt(PORT_STR, 10)
+if (!Number.isFinite(PORT) || PORT <= 0) {
+  throw new Error(`Invalid PORT: ${PORT_STR}`)
+}
 
-export const MONGODB_URL = process.env.MONGODB_URL! as string
-export const MONGODB_DB = process.env.MONGODB_DB! as string
-export const MONGODB_COLLECTION = process.env.MONGODB_COLLECTION! as string
+export const MONGODB_URL = requireEnv('MONGODB_URL')
+export const MONGODB_DB = requireEnv('MONGODB_DB')
+export const MONGODB_COLLECTION = requireEnv('MONGODB_COLLECTION')
 
-export const GC_ENABLED = (process.env.GC !== 'false' && process.env.GC !== '0') as boolean
+// Default GC to true when unset; allow explicit off with 'false' or '0'
+export const GC_ENABLED =
+  process.env.GC === undefined ? true : !(process.env.GC === 'false' || process.env.GC === '0')
🤖 Prompt for AI Agents
In packages/collaborative-editing-backend/src/env.ts around lines 1 to 8, remove
non-null assertions and add explicit validation and sensible defaults: set HOST
default to '0.0.0.0' and parse PORT with Number.parseInt, defaulting to 3000 and
throwing a clear Error if supplied PORT is not a number; require MONGODB_URL,
MONGODB_DB, and MONGODB_COLLECTION and throw descriptive Errors if any are
missing; parse GC strictly so only values 'true' or '1' enable it (default
false), and export typed constants without using the ! operator so
misconfiguration fails fast with clear messages.

Comment on lines +19 to +21
if (!MONGODB_URL) throw new Error('缺少必需的环境变量: MONGODB_URL')
if (!MONGODB_DB) throw new Error('缺少必需的环境变量: MONGODB_DB')
if (!MONGODB_COLLECTION) throw new Error('缺少必需的环境变量: MONGODB_COLLECTION')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use English for error messages.

The error messages are in Chinese, which may not be accessible to all developers working on this codebase. Consider using English for consistency with the rest of the codebase.

Apply this diff to use English error messages:

-    if (!MONGODB_URL) throw new Error('缺少必需的环境变量: MONGODB_URL')
-    if (!MONGODB_DB) throw new Error('缺少必需的环境变量: MONGODB_DB')
-    if (!MONGODB_COLLECTION) throw new Error('缺少必需的环境变量: MONGODB_COLLECTION')
+    if (!MONGODB_URL) throw new Error('Missing required environment variable: MONGODB_URL')
+    if (!MONGODB_DB) throw new Error('Missing required environment variable: MONGODB_DB')
+    if (!MONGODB_COLLECTION) throw new Error('Missing required environment variable: MONGODB_COLLECTION')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!MONGODB_URL) throw new Error('缺少必需的环境变量: MONGODB_URL')
if (!MONGODB_DB) throw new Error('缺少必需的环境变量: MONGODB_DB')
if (!MONGODB_COLLECTION) throw new Error('缺少必需的环境变量: MONGODB_COLLECTION')
if (!MONGODB_URL) throw new Error('Missing required environment variable: MONGODB_URL')
if (!MONGODB_DB) throw new Error('Missing required environment variable: MONGODB_DB')
if (!MONGODB_COLLECTION) throw new Error('Missing required environment variable: MONGODB_COLLECTION')
🤖 Prompt for AI Agents
In packages/collaborative-editing-backend/src/persistence/mongo.ts around lines
19 to 21, the thrown Error messages are in Chinese; replace them with English
messages for consistency (e.g. "Missing required environment variable:
MONGODB_URL", "Missing required environment variable: MONGODB_DB", "Missing
required environment variable: MONGODB_COLLECTION"). Update the three throw
statements accordingly so they use these English strings while keeping the same
error types and behavior.

Comment on lines +45 to +60
async bindState(docName: string, ydoc: Y.Doc) {
await this.connect()

const persistedYDoc = await this.mongodbPersistence.getYDoc(docName)

const newUpdates = Y.encodeStateAsUpdate(ydoc)
if (newUpdates.byteLength > 0) {
this.mongodbPersistence.storeUpdate(docName, newUpdates)
}

Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYDoc))

ydoc.on('update', (update: Uint8Array) => {
this.mongodbPersistence.storeUpdate(docName, update)
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical issues in bindState implementation.

The bindState method has several critical problems:

  1. Data loss risk (Line 52): storeUpdate is called without await, creating a fire-and-forget pattern that could lose data if the operation fails or if the process terminates before completion.

  2. Logic ordering issue (Lines 50-53): New local updates are stored before loading and applying the persisted state. This could create conflicts or inconsistent state, as the local updates should typically be reconciled after loading the persisted state.

  3. Memory leak (Line 57-59): The update event handler is registered but never removed, causing a memory leak if bindState is called multiple times or if documents are frequently created/destroyed.

  4. Missing error handling: No try-catch blocks to handle potential failures from MongoDB operations or Y.js encoding/decoding.

Apply this diff to address these issues:

 async bindState(docName: string, ydoc: Y.Doc) {
-    await this.connect()
+    try {
+      await this.connect()

-    const persistedYDoc = await this.mongodbPersistence.getYDoc(docName)
+      const persistedYDoc = await this.mongodbPersistence.getYDoc(docName)

-    const newUpdates = Y.encodeStateAsUpdate(ydoc)
-    if (newUpdates.byteLength > 0) {
-      this.mongodbPersistence.storeUpdate(docName, newUpdates)
-    }
+      // Apply persisted state first
+      Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYDoc))

-    Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYDoc))
+      // Then store any new local updates
+      const newUpdates = Y.encodeStateAsUpdate(ydoc)
+      if (newUpdates.byteLength > 0) {
+        await this.mongodbPersistence.storeUpdate(docName, newUpdates)
+      }

-    ydoc.on('update', (update: Uint8Array) => {
-      this.mongodbPersistence.storeUpdate(docName, update)
-    })
+      // Store the update handler reference for cleanup
+      const updateHandler = (update: Uint8Array) => {
+        this.mongodbPersistence.storeUpdate(docName, update)
+      }
+      ydoc.on('update', updateHandler)
+      
+      // TODO: Consider storing handlers in a Map for cleanup in close()
+    } catch (error) {
+      console.error(`Failed to bind state for document ${docName}:`, error)
+      throw error
+    }
  }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +16 to +26
const persistence = new MongoPersistence()
setPersistence(persistence)

persistence.connect().then(() => {
server.listen(PORT, HOST, () => {
console.warn(`Server running on http://${HOST}:${PORT}`)
})
}).catch((error) => {
console.error('Failed to connect to MongoDB:', error)
process.exit(1)
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Startup should validate env and handle errors explicitly

Guard missing/invalid HOST/PORT, and use info‑level log. Add server/wss error handlers.

-const persistence = new MongoPersistence()
+const persistence = new MongoPersistence()
 setPersistence(persistence)

-persistence.connect().then(() => {
-  server.listen(PORT, HOST, () => {
-    console.warn(`Server running on http://${HOST}:${PORT}`)
-  })
-}).catch((error) => {
-  console.error('Failed to connect to MongoDB:', error)
-  process.exit(1)
-})
+persistence.connect()
+  .then(() => {
+    const host = process.env.HOST || '0.0.0.0'
+    const port = Number.isFinite(PORT) ? PORT : 1234
+    server.on('error', (e) => console.error('[http] error:', e))
+    ;(wss as any).on?.('error', (e: unknown) => console.error('[ws] error:', e))
+    server.listen(port, host, () => {
+      console.log(`Server running on http://${host}:${port}`)
+    })
+  })
+  .catch((error) => {
+    console.error('Failed to connect to MongoDB:', error)
+    process.exit(1)
+  })
🤖 Prompt for AI Agents
In packages/collaborative-editing-backend/src/server.ts around lines 16 to 26,
validate HOST and PORT before creating the server (check presence and that PORT
parses to a valid number), bail with a clear error log and non-zero exit if
invalid; after persistence.connect resolves, start server and replace
console.warn with an info-level log (e.g., console.info) and attach explicit
error handlers on server and wss (listen for 'error' events and log details then
exit as appropriate); keep the existing MongoDB connect catch but ensure the
error handler logs full error details and exits with a non-zero code.

Comment on lines +53 to +68
disconnect = () => {
try {
this.provider.disconnect()
const wasSynced = this._isSynced

this._isConnected = false
this._isSynced = false

if (wasSynced) {
this.onSyncChange?.(false)
}
}
catch (error) {
console.warn('[yjs] Error disconnecting WebSocket provider:', error)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t zero flags in disconnect; let status events drive state.

Resetting _isConnected/_isSynced before the status handler runs prevents onDisconnect/onSyncChange(false) from firing due to wasConnected becoming false.

   disconnect = () => {
     try {
       this.provider.disconnect()
-      const wasSynced = this._isSynced
-
-      this._isConnected = false
-      this._isSynced = false
-
-      if (wasSynced) {
-        this.onSyncChange?.(false)
-      }
     }
     catch (error) {
-      console.warn('[yjs] Error disconnecting WebSocket provider:', error)
+      console.warn('[yjs] Error disconnecting WebSocket provider:', error)
+      this.onError?.(error instanceof Error ? error : new Error(String(error)))
     }
   }

Also applies to: 101-123

🤖 Prompt for AI Agents
In
packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts
around lines 53-68 (and similarly for lines 101-123), the disconnect()
implementation force-zeros _isConnected/_isSynced and manually calls
onSyncChange, which prevents the provider's status events from driving state and
firing onDisconnect/onSyncChange(false). Fix by removing the manual state resets
and the manual onSyncChange invocation: simply call this.provider.disconnect()
(inside try) and let the provider's status handler update _isConnected/_isSynced
and invoke onDisconnect/onSyncChange; preserve the catch logging but do not
change internal flags in disconnect().

Comment on lines +96 to +99
awareness: this.awareness,
...options,
},
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Awareness override order is backwards.

...options can override awareness: this.awareness, leading to two divergent Awareness instances.

-        {
-          awareness: this.awareness,
-          ...options,
-        },
+        {
+          ...options,
+          awareness: this.awareness,
+        },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
awareness: this.awareness,
...options,
},
)
...options,
awareness: this.awareness,
},
)
🤖 Prompt for AI Agents
In
packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts
around lines 96 to 99, the object spread uses awareness: this.awareness before
...options which allows caller options to override the awareness instance;
change the merge order so the provider's awareness wins by placing ...options
before awareness (or explicitly assign awareness = this.awareness after
spreading options) so the resulting config always contains this.awareness and
cannot be overwritten by passed options.

Comment on lines +101 to +123
this.provider.on('status', (event: { status: 'connected' | 'disconnected' | 'connecting' }) => {
const wasConnected = this._isConnected
this._isConnected = event.status === 'connected'

if (event.status === 'connected') {
if (!wasConnected) {
this.onConnect?.()
}
if (!this._isSynced) {
this._isSynced = true
this.onSyncChange?.(true)
}
}
else if (event.status === 'disconnected') {
if (wasConnected) {
this.onDisconnect?.()
if (this._isSynced) {
this._isSynced = false
this.onSyncChange?.(false)
}
}
}
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Track real sync state using provider events.

isSynced is flipped on connection, which may precede initial document sync. Use the provider’s sync event.

-      this.provider.on('status', (event: { status: 'connected' | 'disconnected' | 'connecting' }) => {
+      this.provider.on('status', (event: { status: 'connected' | 'disconnected' | 'connecting' }) => {
         const wasConnected = this._isConnected
         this._isConnected = event.status === 'connected'
@@
-          if (!this._isSynced) {
-            this._isSynced = true
-            this.onSyncChange?.(true)
-          }
+          // Connection established; await 'sync' for content readiness
         }
         else if (event.status === 'disconnected') {
           if (wasConnected) {
             this.onDisconnect?.()
-            if (this._isSynced) {
-              this._isSynced = false
-              this.onSyncChange?.(false)
-            }
+            if (this._isSynced) { this._isSynced = false; this.onSyncChange?.(false) }
           }
         }
       })
+      // y-websocket emits 'sync' with boolean
+      // https://github.com/yjs/y-websocket
+      this.provider.on('sync', (synced: boolean) => {
+        if (this._isSynced !== synced) {
+          this._isSynced = synced
+          this.onSyncChange?.(synced)
+        }
+      })

To confirm the latest event name/semantics for your y-websocket version, please verify:


🌐 Web query:

Does the current y-websocket provider emit a 'sync' event with a boolean indicating synced state?

💡 Result:

Yes. The y-websocket/WebsocketProvider emits a "sync" event with a boolean argument (isSynced) indicating synced state. [1][2]

Sources:
[1] y-websocket docs (yjs) — wsProvider.on('sync', function(isSynced: boolean)).
[2] y-websocket GitHub README — wsProvider.on('sync', function(isSynced: boolean)).



---

🏁 Script executed:

```shell
#!/bin/bash
# Verify file location and content
fd -H "websocket.ts" packages/fluent-editor/src/modules/collaborative-editing/provider/

Length of output: 78


🏁 Script executed:

#!/bin/bash
# Check the actual file content at lines 101-123
if [ -f "packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts" ]; then
  sed -n '95,130p' packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts
fi

Length of output: 996


🏁 Script executed:

#!/bin/bash
# Search for any existing sync event handlers
rg -n "\.on\('sync'" packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the overall structure of the provider setup
rg -n "this\.provider\.on" packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts

Length of output: 106


Track real sync state using provider's 'sync' event.

The code conflates connection with document sync: isSynced is flipped on 'connected' status, which precedes actual document sync. The y-websocket provider emits a dedicated 'sync' event with a boolean argument that accurately reflects document sync state. Remove the sync logic from the 'status' handler and add a handler for the 'sync' event.

       this.provider.on('status', (event: { status: 'connected' | 'disconnected' | 'connecting' }) => {
         const wasConnected = this._isConnected
         this._isConnected = event.status === 'connected'

         if (event.status === 'connected') {
           if (!wasConnected) {
             this.onConnect?.()
           }
-          if (!this._isSynced) {
-            this._isSynced = true
-            this.onSyncChange?.(true)
-          }
         }
         else if (event.status === 'disconnected') {
           if (wasConnected) {
             this.onDisconnect?.()
-            if (this._isSynced) {
-              this._isSynced = false
-              this.onSyncChange?.(false)
-            }
           }
         }
       })
+      this.provider.on('sync', (synced: boolean) => {
+        if (this._isSynced !== synced) {
+          this._isSynced = synced
+          this.onSyncChange?.(synced)
+        }
+      })
🤖 Prompt for AI Agents
In
packages/fluent-editor/src/modules/collaborative-editing/provider/websocket.ts
around lines 101 to 123, the status handler currently toggles _isSynced based on
connection status which is incorrect; remove the sync-related code from the
'status' event handler so it only updates _isConnected and calls
onConnect/onDisconnect as appropriate, and add a separate provider.on('sync',
(isSynced: boolean) => { this._isSynced = isSynced;
this.onSyncChange?.(isSynced); }) handler to track real document sync state
using the boolean argument the provider emits.

// callback
onConnect?: () => void
onDisconnect?: () => void
onError?: (error) => void
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit type annotation for error parameter.

The error parameter in the onError callback lacks a type annotation, which will cause TypeScript to infer it as any, reducing type safety. This is inconsistent with the onError definition in ProviderEventHandlers (Line 8) where the type is explicitly Error.

Apply this diff to add the type annotation:

-  onError?: (error) => void
+  onError?: (error: Error) => void
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onError?: (error) => void
onError?: (error: Error) => void
🤖 Prompt for AI Agents
In packages/fluent-editor/src/modules/collaborative-editing/types.ts at line 42,
the onError callback parameter is untyped (onError?: (error) => void); update
the signature to explicitly type the parameter as Error (onError?: (error:
Error) => void) so it matches ProviderEventHandlers and preserves type safety;
adjust any related imports or exported types if necessary to ensure Error is
available in this scope.

Comment on lines +136 to +143
provider: {
type: 'websocket',
options: {
serverUrl: 'wss://120.26.92.145:1234',
roomName: ROOM_NAME,
},
},
awareness: {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Don’t hard‑code the WebSocket endpoint (TLS/cert + CI breakage). Make it env/config‑driven.

A bare IP wss://120.26.92.145:1234 will likely fail on HTTPS origins (cert CN mismatch) and makes CI/local runs brittle. Compute ws/wss from location and allow override via env.

Apply:

-const ROOM_NAME = 'yuque-document-demo-room'
+// Prefer env with sensible fallbacks
+const ROOM_NAME = import.meta.env.VITE_COLLAB_ROOM ?? 'yuque-document-demo-room'
+const WS_PORT = import.meta.env.VITE_COLLAB_WS_PORT ?? '1234'
+const WS_HOST = import.meta.env.VITE_COLLAB_WS_HOST ?? (typeof location !== 'undefined' ? location.hostname : 'localhost')
+const WS_PROTO = typeof location !== 'undefined' && location.protocol === 'https:' ? 'wss:' : 'ws:'
+const WS_URL = import.meta.env.VITE_COLLAB_WS_URL ?? `${WS_PROTO}//${WS_HOST}:${WS_PORT}`

And:

-          options: {
-            serverUrl: 'wss://120.26.92.145:1234',
-            roomName: ROOM_NAME,
-          },
+          options: {
+            serverUrl: WS_URL,
+            roomName: ROOM_NAME,
+          },

Run to ensure no other hard‑coded WS endpoints remain:


🏁 Script executed:

#!/bin/bash
rg -nP '(ws|wss)://[0-9a-zA-Z\.\-:]+'

Length of output: 884


Hard‑coded WebSocket endpoint appears in multiple production files; externalize and compute protocol dynamically.

The bare IP wss://120.26.92.145:1234 hard‑coded at line 139 will fail on HTTPS origins due to cert CN mismatch and breaks CI/local runs. The same pattern also exists in packages/docs/fluent-editor/demos/collaborative-editing.vue:99, indicating this should be a shared configuration concern.

Apply the suggested refactor to packages/projects/src/views/yuque/YuQue.vue:

-const ROOM_NAME = 'yuque-document-demo-room'
+const ROOM_NAME = import.meta.env.VITE_COLLAB_ROOM ?? 'yuque-document-demo-room'
+const WS_PORT = import.meta.env.VITE_COLLAB_WS_PORT ?? '1234'
+const WS_HOST = import.meta.env.VITE_COLLAB_WS_HOST ?? (typeof location !== 'undefined' ? location.hostname : 'localhost')
+const WS_PROTO = typeof location !== 'undefined' && location.protocol === 'https:' ? 'wss:' : 'ws:'
+const WS_URL = import.meta.env.VITE_COLLAB_WS_URL ?? `${WS_PROTO}//${WS_HOST}:${WS_PORT}`
-          options: {
-            serverUrl: 'wss://120.26.92.145:1234',
-            roomName: ROOM_NAME,
-          },
+          options: {
+            serverUrl: WS_URL,
+            roomName: ROOM_NAME,
+          },

Also update packages/docs/fluent-editor/demos/collaborative-editing.vue:99 using the same pattern to maintain consistency across the codebase.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/projects/src/views/yuque/YuQue.vue around lines 136 to 143, the
WebSocket URL is hard-coded to wss://120.26.92.145:1234; replace this with a
computed URL built from a centralized configuration or environment variable and
the current page protocol: derive protocol as window.location.protocol ===
'https:' ? 'wss' : 'ws', get host and optional port from an env/config value
(FALLBACK_HOST or process.env.VUE_APP_WS_HOST) rather than a bare IP, then
construct serverUrl = `${protocol}://${host}${port ? `:${port}` : ''}`; export
or reuse this config for
packages/docs/fluent-editor/demos/collaborative-editing.vue:99 to keep behavior
consistent across the codebase.

kagol and others added 2 commits October 22, 2025 16:50
* feat: Enable pre and post scripts for npm

* docs: simplify collaborative editing setup instructions and improve formatting

* refactor: move environment variables from .env file to docker-compose.yml

* feat: add environment variable overrides and documentation for MongoDB config

* feat: add support for loading environment variables from .env file in PM2 config

* chore: remove .env file and add .env.example
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
packages/docs/fluent-editor/docs/demo/collaborative-editing.md (1)

66-106: Address Docker-compose security concerns flagged in previous review.

This block still contains unsafe defaults that were flagged earlier:

  • Unpinned MongoDB image (mongo:latest → pin to mongo:7.0 or specific version)
  • Hardcoded sensitive credentials (MONGO_INITDB_ROOT_PASSWORD: admin!123)
  • Missing MongoDB healthcheck and depends_on with health condition

Apply this diff to harden the example:

  services:
    mongodb:
-     image: mongo:latest
+     image: mongo:7.0
      container_name: yjs-mongodb
      restart: always
      ports:
        - '27017:27017'
      environment:
-       MONGO_INITDB_ROOT_USERNAME: admin
-       MONGO_INITDB_ROOT_PASSWORD: admin!123
+       MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:-admin}
+       MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD}
      volumes:
        - mongodb_data:/data/db
+     healthcheck:
+       test: ['CMD', 'mongo', '--eval', 'db.adminCommand("ping")']
+       interval: 10s
+       timeout: 5s
+       retries: 5

  websocket-server:
    image: yinlin124/collaborative-editor-backend:latest
    container_name: yjs-websocket-server
    restart: always
    ports:
      - '${PORT:-1234}:${PORT:-1234}'
    environment:
      HOST: ${HOST:-0.0.0.0}
      PORT: ${PORT:-1234}
      MONGODB_URL: ${MONGODB_URL}
      MONGODB_DB: ${MONGODB_DB:-tinyeditor}
      MONGODB_COLLECTION: ${MONGODB_COLLECTION:-documents}
    depends_on:
      mongodb:
-       condition: service_started
+       condition: service_healthy

Add a note to users to create/configure a .env file with actual credentials before deploying.

🧹 Nitpick comments (1)
packages/collaborative-editing-backend/.env.example (1)

1-6: Fix key ordering and add trailing newline.

Static analysis flags that keys should be in alphabetical order and the file should end with a blank line. While minor, aligning with project linting rules improves consistency.

Apply this diff to reorder keys alphabetically and add a blank line:

-HOST=0.0.0.0
-PORT=1234
-MONGODB_URL=mongodb://admin:[email protected]:27017/?authSource=admin
-MONGODB_DB=tinyeditor
-MONGODB_COLLECTION=documents
-GC=true
+GC=true
+HOST=0.0.0.0
+MONGODB_COLLECTION=documents
+MONGODB_DB=tinyeditor
+MONGODB_URL=mongodb://<username>:<password>@127.0.0.1:27017/?authSource=admin
+PORT=1234
+
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89c358a and 55d4b32.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • .npmrc (1 hunks)
  • packages/collaborative-editing-backend/.env.example (1 hunks)
  • packages/collaborative-editing-backend/docker-compose.yml (1 hunks)
  • packages/collaborative-editing-backend/ecosystem.config.cjs (1 hunks)
  • packages/docs/fluent-editor/docs/demo/collaborative-editing.md (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/collaborative-editing-backend/ecosystem.config.cjs
  • packages/collaborative-editing-backend/docker-compose.yml
🧰 Additional context used
🪛 dotenv-linter (4.0.0)
packages/collaborative-editing-backend/.env.example

[warning] 3-3: [UnorderedKey] The MONGODB_URL key should go before the PORT key

(UnorderedKey)


[warning] 4-4: [UnorderedKey] The MONGODB_DB key should go before the MONGODB_URL key

(UnorderedKey)


[warning] 5-5: [UnorderedKey] The MONGODB_COLLECTION key should go before the MONGODB_DB key

(UnorderedKey)


[warning] 6-6: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 6-6: [UnorderedKey] The GC key should go before the HOST key

(UnorderedKey)

🪛 GitHub Actions: autofix.ci
packages/docs/fluent-editor/docs/demo/collaborative-editing.md

[error] 193-193: ESLint parsing error: Unexpected token :


[error] 367-367: ESLint parsing error: Unexpected token :


[error] 420-420: ESLint parsing error: Unexpected token :

🪛 LanguageTool
packages/docs/fluent-editor/docs/demo/collaborative-editing.md

[uncategorized] ~181-~181: "间"不能与“名称”搭配,请换量词
Context: ...tring | 是 | - | 房间名称 | |connect |b...

(wa5)


[uncategorized] ~213-~213: "间"不能与“名称”搭配,请换量词
Context: ...ring | 是 | - | 房间名称 | |signaling |str...

(wa5)


[uncategorized] ~217-~217: "间"不能与“密码”搭配,请换量词
Context: ...ring | 否 | - | 房间密码 | |peerOpts |Rec...

(wa5)

🔇 Additional comments (2)
.npmrc (1)

1-1: ****

The original review concern is incorrect. pnpm supports enable-pre-post-scripts=true in .npmrc, and this setting has been supported since pnpm v6.1.0. The project uses [email protected], which fully supports this configuration approach. The .npmrc file is the correct location for this setting in a pnpm project.

packages/docs/fluent-editor/docs/demo/collaborative-editing.md (1)

58-58: Verify git branch references.

Line 58 and line 478 reference the branch ospp-2025/collaborative-editing, but the PR is sourced from main-collaborative. Confirm whether these should be updated to the actual branch name or point to stable released documentation paths. If the code hasn't been merged to main yet, consider using relative documentation paths or marking these as provisional.

- > 在 Vue 项目中集成协作编辑:[YuQue.vue](https://github.com/opentiny/tiny-editor/blob/ospp-2025/collaborative-editing/packages/projects/src/views/yuque/YuQue.vue)
+ > 在 Vue 项目中集成协作编辑:[YuQue.vue](https://github.com/opentiny/tiny-editor/blob/main-collaborative/packages/projects/src/views/yuque/YuQue.vue)

- 详细的自定义持久化服务配置和部署请参考:[collaborative-editing-backend](https://github.com/opentiny/tiny-editor/tree/ospp-2025/collaborative-editing/packages/collaborative-editing-backend)
+ 详细的自定义持久化服务配置和部署请参考:[collaborative-editing-backend](https://github.com/opentiny/tiny-editor/tree/main-collaborative/packages/collaborative-editing-backend)

Also applies to: 478-478

@@ -0,0 +1,6 @@
HOST=0.0.0.0
PORT=1234
MONGODB_URL=mongodb://admin:[email protected]:27017/?authSource=admin
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use placeholder credentials in .env.example, not real/default values.

The hardcoded MongoDB credentials (admin:admin) should be replaced with placeholders to make it clear these are examples and must be configured before use. Even for local development, storing credentials (even simple ones) in example files can pose security risks.

Apply this diff to use placeholder credentials:

-MONGODB_URL=mongodb://admin:[email protected]:27017/?authSource=admin
+MONGODB_URL=mongodb://<username>:<password>@127.0.0.1:27017/?authSource=admin

Optionally, add a comment above this line documenting how to set up credentials:

+# MongoDB connection URL - configure with your credentials
+MONGODB_URL=mongodb://<username>:<password>@127.0.0.1:27017/?authSource=admin
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
MONGODB_URL=mongodb://admin:admin@127.0.0.1:27017/?authSource=admin
MONGODB_URL=mongodb://<username>:<password>@127.0.0.1:27017/?authSource=admin
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 3-3: [UnorderedKey] The MONGODB_URL key should go before the PORT key

(UnorderedKey)

🤖 Prompt for AI Agents
In packages/collaborative-editing-backend/.env.example around line 3, replace
the hardcoded MongoDB credentials ("admin:admin") with placeholder values and
update the example to indicate these must be configured by the user; change the
URL to use non-sensitive placeholders for username, password, host and port and
add a one-line comment above explaining this is an example, where to set real
credentials (e.g., in a local .env or secret manager) and that the placeholders
must be replaced before running.

Comment on lines +127 to +167
#### 本地部署

1. 启动 MongoDB(如果有其他 MongoDB 服务可跳过此步骤)

```bash
docker run -d \
--name mongo \
-p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=admin \
-v mongodb_data:/data/db \
mongo:latest
```

2. 进入协同编辑后端子包目录

```bash
cd packages/collaborative-editing-backend
```

3. 创建 `.env` 文件

```env
HOST=0.0.0.0
PORT=1234
MONGODB_URL=mongodb://admin:[email protected]:27017/?authSource=admin
MONGODB_DB=tinyeditor
MONGODB_COLLECTION=documents
GC=true
```

4. 启动本地服务器

```bash
pnpm install -g pm2
pnpm install
pnpm start
```

启动后即可使用 `ws://localhost:1234` 作为前端 `serverUrl` 配置

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use placeholder credentials and pinned images in local deployment examples.

Lines 132–139 and 150–155 show hardcoded MongoDB credentials and mongo:latest. Replace these with placeholders and pinned versions to prevent security issues and ensure reproducibility:

  docker run -d \
    --name mongo \
    -p 27017:27017 \
-   -e MONGO_INITDB_ROOT_USERNAME=admin \
-   -e MONGO_INITDB_ROOT_PASSWORD=admin \
+   -e MONGO_INITDB_ROOT_USERNAME=${MONGO_ROOT_USER:-admin} \
+   -e MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASSWORD} \
      -v mongodb_data:/data/db \
-     mongo:latest
+     mongo:7.0
  HOST=0.0.0.0
  PORT=1234
- MONGODB_URL=mongodb://admin:[email protected]:27017/?authSource=admin
+ MONGODB_URL=mongodb://${MONGO_ROOT_USER}:${MONGO_ROOT_PASSWORD}@127.0.0.1:27017/?authSource=admin
  MONGODB_DB=tinyeditor
  MONGODB_COLLECTION=documents
  GC=true

Add documentation reminding users to set actual credentials in a local .env file that is not committed to version control.

| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ---------------- | ------------------------ | ---- | ------ | -------------------- |
| `serverUrl` | `string` | 是 | - | WebSocket 服务器地址 |
| `roomName` | `string` | 是 | - | 房间名称 |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix Chinese grammar: measure word misuse.

Static analysis flags incorrect pairings of the measure word "间" with parameter names. In Chinese, "间" (room) is not the appropriate measure word for naming these entities. Use more natural phrasing or correct measure words:

原文 建议改正
房间名称 (line 181) 房间名 或 房间的名称
房间名称 (line 213) 房间名 或 房间的名称
房间密码 (line 217) 房间的密码 或 密码

Apply corrections throughout the document for consistency.

Also applies to: 213-213, 217-217

🧰 Tools
🪛 LanguageTool

[uncategorized] ~181-~181: "间"不能与“名称”搭配,请换量词
Context: ...tring | 是 | - | 房间名称 | |connect |b...

(wa5)

🤖 Prompt for AI Agents
In packages/docs/fluent-editor/docs/demo/collaborative-editing.md around lines
181, 213 and 217, the Chinese phrase "房间名称" and "房间密码" use awkward measure-word
phrasing; update these to more natural alternatives: replace "房间名称" with "房间名"
or "房间的名称" at lines 181 and 213, and replace "房间密码" with "房间的密码" or simply "密码"
at line 217, and apply the same wording consistently elsewhere in the document
where those terms appear.

```javascript
provider: {
type: 'websocket',
options: {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fix ESLint parsing errors in code examples.

The pipeline reports "Unexpected token :" errors at these lines. These are incomplete JavaScript object snippets without broader context. Wrap them in complete, valid code blocks that ESLint can parse, or use a markdown linting config that excludes these fragments from JS linting.

For example, at line 190–202, instead of a bare object, provide a complete assignment or function body:

- provider: {
+ const config = {
+   provider: {
      type: 'websocket',
      options: {
        serverUrl: 'wss://120.26.92.145:1234',
        roomName: 'tiny-editor-demo',
        connect: true,
        resyncInterval: 3000,
        maxBackoffTime: 2500,
        protocols: ['json'],
      }
- }
+ }
+ }

Apply similar wrapping to lines 363–371 and 401–424.

Also applies to: 367-367, 420-420


🏁 Script executed:

#!/bin/bash
# Check the actual content at lines 193, 367, 420 and surrounding context
echo "=== Lines 190-205 (around line 193) ==="
sed -n '190,205p' packages/docs/fluent-editor/docs/demo/collaborative-editing.md

echo ""
echo "=== Lines 360-375 (around line 367) ==="
sed -n '360,375p' packages/docs/fluent-editor/docs/demo/collaborative-editing.md

echo ""
echo "=== Lines 415-430 (around line 420) ==="
sed -n '415,430p' packages/docs/fluent-editor/docs/demo/collaborative-editing.md

echo ""
echo "=== Lines 55-70 (git branch references) ==="
sed -n '55,70p' packages/docs/fluent-editor/docs/demo/collaborative-editing.md

echo ""
echo "=== Lines 66-106 (docker-compose security) ==="
sed -n '66,106p' packages/docs/fluent-editor/docs/demo/collaborative-editing.md

echo ""
echo "=== Lines 150-155 (MongoDB credentials) ==="
sed -n '150,155p' packages/docs/fluent-editor/docs/demo/collaborative-editing.md

echo ""
echo "=== File total lines ==="
wc -l packages/docs/fluent-editor/docs/demo/collaborative-editing.md

Length of output: 2594


🏁 Script executed:

#!/bin/bash
# Check line 478 and surrounding context for git branch references
echo "=== Lines 470-478 (end of file) ==="
sed -n '470,478p' packages/docs/fluent-editor/docs/demo/collaborative-editing.md

echo ""
echo "=== Search for all branch references ==="
grep -n "ospp-2025\|main-collaborative" packages/docs/fluent-editor/docs/demo/collaborative-editing.md

Length of output: 746


Fix ESLint parsing errors and remove hardcoded credentials from documentation examples.

Three code blocks contain incomplete JavaScript object snippets that fail ESLint parsing:

  • Line 193: provider: { ... } – bare object literal
  • Line 367: awareness: { ... } – bare object literal
  • Line 420: Object fragment (tail of incomplete snippet)

Wrap each in valid context (e.g., const config = { ... }) so ESLint can parse them.

Additionally, the documentation examples expose hardcoded credentials:

  • Lines 66–106 (docker-compose): admin:admin!123
  • Lines 150–155 (.env): admin:admin

Replace these with environment variable placeholders (e.g., ${MONGODB_PASSWORD}) to avoid security risks and follow best practices for documentation.

🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 193-193: ESLint parsing error: Unexpected token :

🤖 Prompt for AI Agents
packages/docs/fluent-editor/docs/demo/collaborative-editing.md around lines 193,
367, 420 and ranges 66–106, 150–155: the file contains bare object fragments at
lines 193 (`provider: { ... }`), 367 (`awareness: { ... }`) and an incomplete
tail snippet at 420 which cause ESLint parse errors; wrap each fragment in a
valid JavaScript context (for example prefix with `const config = {` and close
the object) so they become syntactically complete code blocks, and for security
replace hardcoded credentials in the docker-compose block (lines 66–106) and the
.env example (lines 150–155) with environment variable placeholders (e.g.,
`${MONGODB_PASSWORD}` or `${ADMIN_PASSWORD}`) and update any inline references
in the examples to use those placeholders.

Yinlin124 and others added 2 commits October 24, 2025 09:51
* fix(Collaborative-editing): 新增配置文件,修复文档 (#351)

* feat: Enable pre and post scripts for npm

* docs: simplify collaborative editing setup instructions and improve formatting

* refactor: move environment variables from .env file to docker-compose.yml

* feat: add environment variable overrides and documentation for MongoDB config

* feat: add support for loading environment variables from .env file in PM2 config

* chore: remove .env file and add .env.example

* fix: CI lint bug

* fix: clean up .dockerignore by removing redundant entries

* chore: add Firefox browser installation to Playwright setup command
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/docs/fluent-editor/docs/demo/collaborative-editing.md (2)

77-77: [DUPLICATE_COMMENT] Replace hardcoded credentials and pin Docker image versions.

This documentation still exposes hardcoded MongoDB credentials and uses unpinned Docker images, creating security and reproducibility risks that were flagged in prior reviews.

Issues:

  • Lines 77, 138: mongo:latest should be pinned to a specific version (e.g., mongo:7.0).
  • Lines 84, 97, 135–136, 152: Hardcoded credentials (admin!123, admin:admin) should be replaced with environment variable placeholders.

Recommended fixes:

  1. Update docker-compose.yml (lines 74–106):
   services:
     mongodb:
-      image: mongo:latest
+      image: mongo:7.0
       container_name: yjs-mongodb
       restart: always
       ports:
         - '27017:27017'
       environment:
-        MONGO_INITDB_ROOT_USERNAME: admin # 设置 MongoDB 初始用户名
-        MONGO_INITDB_ROOT_PASSWORD: admin!123 # 设置 MongoDB 初始密码
+        MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:-admin} # 设置 MongoDB 初始用户名(生产环境请修改)
+        MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD} # 设置 MongoDB 初始密码(必须通过环境变量设置)
       volumes:
         - mongodb_data:/data/db
   
     websocket-server:
       image: yinlin124/collaborative-editor-backend:latest
       container_name: yjs-websocket-server
       restart: always
       ports:
         - '${PORT:-1234}:${PORT:-1234}'
       environment:
         HOST: ${HOST:-0.0.0.0}
         PORT: ${PORT:-1234}
-        MONGODB_URL: ${MONGODB_URL:-mongodb://admin:admin!123@mongodb:27017/?authSource=admin}
+        MONGODB_URL: mongodb://${MONGO_ROOT_USER}:${MONGO_ROOT_PASSWORD}@mongodb:27017/?authSource=admin
         MONGODB_DB: ${MONGODB_DB:-tinyeditor}
         MONGODB_COLLECTION: ${MONGODB_COLLECTION:-documents}
       depends_on:
         - mongodb
  1. Update local deployment instructions (lines 132–156):
   docker run -d \
     --name mongo \
     -p 27017:27017 \
-    -e MONGO_INITDB_ROOT_USERNAME=admin \
-    -e MONGO_INITDB_ROOT_PASSWORD=admin \
+    -e MONGO_INITDB_ROOT_USERNAME=${MONGO_ROOT_USER:-admin} \
+    -e MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASSWORD} \
      -v mongodb_data:/data/db \
-     mongo:latest
+     mongo:7.0

And update the .env example (lines 149–156):

   HOST=0.0.0.0
   PORT=1234
-  MONGODB_URL=mongodb://admin:[email protected]:27017/?authSource=admin
+  MONGODB_URL=mongodb://${MONGO_ROOT_USER}:${MONGO_ROOT_PASSWORD}@127.0.0.1:27017/?authSource=admin
   MONGODB_DB=tinyeditor
   MONGODB_COLLECTION=documents
   GC=true
  1. Add a note before the deployment sections (e.g., after line 62):

⚠️ 重要:安全建议 | Important: Security Notice

请勿在文档或版本控制中硬编码生产环境凭证。在本地部署或生产环境中,始终通过环境变量或 .env 文件(.gitignored)提供凭证。

Do not hardcode production credentials in documentation or version control. Always provide credentials via environment variables or a .env file (.gitignored) for local or production deployments.

Also applies to: 84-84, 97-97, 135-136, 138-138, 152-152


181-181: [DUPLICATE_COMMENT] Fix Chinese measure-word terminology issues.

The static analysis flags incorrect pairings of the measure word "间" in Chinese. Update the following terms for naturalness and correctness:

  • Line 181, 220: 房间名称房间名 or 房间的名称
  • Line 224: 房间密码房间的密码 or simply 密码

These corrections align with standard Chinese phrasing for collaborative editing documentation.

Proposed changes:

- | `roomName`       | `string`                 | 是   | -      | 房间名称             |
+ | `roomName`       | `string`                 | 是   | -      | 房间名                |
- | `roomName`      | `string`                  | 是   | -      | 房间名称            |
+ | `roomName`      | `string`                  | 是   | -      | 房间名              |
- | `password`      | `string`                  | 否   | -      | 房间密码            |
+ | `password`      | `string`                  | 否   | -      | 房间的密码          |

Also applies to: 220-220, 224-224

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55d4b32 and b39453d.

📒 Files selected for processing (6)
  • README.md (1 hunks)
  • README.zh-CN.md (1 hunks)
  • packages/collaborative-editing-backend/.dockerignore (1 hunks)
  • packages/collaborative-editing-backend/docker-compose.yml (1 hunks)
  • packages/docs/fluent-editor/docs/demo/collaborative-editing.md (1 hunks)
  • packages/docs/package.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/collaborative-editing-backend/.dockerignore
  • packages/collaborative-editing-backend/docker-compose.yml
🧰 Additional context used
🪛 LanguageTool
packages/docs/fluent-editor/docs/demo/collaborative-editing.md

[uncategorized] ~181-~181: "间"不能与“名称”搭配,请换量词
Context: ...tring | 是 | - | 房间名称 | |connect |b...

(wa5)


[uncategorized] ~220-~220: "间"不能与“名称”搭配,请换量词
Context: ...ring | 是 | - | 房间名称 | |signaling |str...

(wa5)


[uncategorized] ~224-~224: "间"不能与“密码”搭配,请换量词
Context: ...ring | 否 | - | 房间密码 | |peerOpts |Rec...

(wa5)

🪛 markdownlint-cli2 (0.18.1)
README.zh-CN.md

5-5: Link fragments should be valid

(MD051, link-fragments)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (3)
packages/docs/package.json (1)

11-11: Expand E2E test coverage to Firefox browsers.

The addition of firefox to the install:browser script is a reasonable enhancement to test coverage for the new collaborative editing features. The script will now install both Chromium and Firefox with their OS-level dependencies.

Verify that the Playwright configuration (e.g., playwright.config.ts) is set up to run E2E tests against Firefox as a project, not just install it. If the config only defines Chromium as a project, the Firefox installation will be unused.

packages/docs/fluent-editor/docs/demo/collaborative-editing.md (1)

58-58: Update outdated branch references.

Lines 58 and 499 reference the ospp-2025/collaborative-editing branch, which may be outdated depending on the PR merge strategy.

  • Line 58: Update link to reflect the current branch after merge (e.g., main or dev).
  • Line 499: Same update needed.

Clarify with the maintainers whether these links should point to main or remain as historical references in a post-merge context.

- > 在 Vue 项目中集成协作编辑:[YuQue.vue](https://github.com/opentiny/tiny-editor/blob/ospp-2025/collaborative-editing/packages/projects/src/views/yuque/YuQue.vue)
+ > 在 Vue 项目中集成协作编辑:[YuQue.vue](https://github.com/opentiny/tiny-editor/blob/main/packages/projects/src/views/yuque/YuQue.vue)
- 详细的自定义持久化服务配置和部署请参考:[collaborative-editing-backend](https://github.com/opentiny/tiny-editor/tree/ospp-2025/collaborative-editing/packages/collaborative-editing-backend)
+ 详细的自定义持久化服务配置和部署请参考:[collaborative-editing-backend](https://github.com/opentiny/tiny-editor/tree/main/packages/collaborative-editing-backend)

Also applies to: 499-499

README.zh-CN.md (1)

4-4: Formatting improvements approved.

The blank lines added around the All Contributors badge improve readability and provide proper spacing within the section.

Also applies to: 6-6


<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors-)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Broken link fragment in All Contributors badge.

The link fragment #contributors- does not match the corresponding heading. The document contains ## ✨ 贡献者 (line 69), which would generate a different ID depending on the markdown processor. The link will fail to navigate to the intended section.

Either update the link fragment to match the heading ID or add an English heading alias:

- [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors-)
+ [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#-贡献者)

Or alternatively, add an English anchor heading before the Chinese one:

+ ## Contributors {#contributors-}
+ 
  ## ✨ 贡献者

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

5-5: Link fragments should be valid

(MD051, link-fragments)

🤖 Prompt for AI Agents
In README.zh-CN.md around line 5, the All Contributors badge links to fragment
"#contributors-" which doesn't match the actual heading ID for "## ✨ 贡献者"; fix
this by updating the badge link fragment to the correct generated ID (for
example "#✨-贡献者" or the actual slug produced by your markdown processor) or add
an English anchor heading such as "## Contributors" immediately before the
Chinese heading so the existing "#contributors" fragment will resolve; ensure
the chosen fragment exactly matches the heading slug used by your renderer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e2e-test enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants