关于 yjs-nextjs
yjs-nextjs 是面向在 Next.js 16 App Router 中实现 Yjs 协作编辑的编码参考技能,涵盖同步、持久化及 React 绑定。规则针对 Yjs 13.6.31,聚焦并发编辑中容易被忽略的数据丢失与配置问题。
主要功能
- 01覆盖同步拓扑与依赖选择
- 02说明二进制更新传输和持久化
- 03指导共享类型的数据建模
- 04处理 React 文档生命周期与绑定
- 05涵盖撤销、离线加载和光标
- 06指导 Tiptap 与表单集成
适用场景
- 01为 Next.js 应用添加协作编辑或离线同步
- 02排查并发编辑导致的内容丢失或重复
- 03设计 Y.Doc 持久化与更新压缩
- 04检查 React 中 Yjs Provider 的生命周期
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ pproenca/ dot-skills --skill yjs-nextjs安装后在 Claude Code 中输入/yjs-nextjs确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdmetadata.jsonassets/templates/_template.mdreferences/_sections.mdhist-scope-undo-by-origin.mdhist-snapshots-require-gc-disabled.mdhist-wait-for-indexeddb-sync.mdhost-authorize-rooms-not-updates.mdhost-pin-the-yjs-13-track.mdhost-route-handlers-cannot-upgrade.mdhost-trpc-is-not-the-sync-channel.mdhost-vercel-lacks-instance-affinity.mdhost-websocket-server-is-not-production.mdmodel-nested-types-not-plain-objects.mdmodel-seed-the-document-once.mdmodel-yarray-has-no-move.mdmodel-ytext-only-for-prose.mdpres-awareness-is-ephemeral.mdpres-relative-positions-for-cursors.mdreact-keep-yjs-client-only.mdreact-stable-doc-and-provider.mdreact-subscribe-via-usesyncexternalstore.mdui-do-not-drive-inputs-from-the-doc.mdui-tiptap-collaboration-wiring.mdwire-base64-not-superjson.mdwire-compaction-needs-a-doc-roundtrip.mdwire-rehydrate-to-uint8array.mdwire-send-updates-not-state.md详细介绍
Next.js 16、tRPC 与 shadcn/ui 中的 Yjs
在 App Router 代码库中使用 Yjs 13.6.31——协同编辑带来的决策,以及如何解决这些问题。本文旨在让 Agent 在编写或审查代码时应用这些规则。每条规则都指出它要纠正的错误默认做法;对于模型本来就能处理好的内容,不设规则。
这些问题大多不会立即显现。Yjs 会收敛,编辑器仍能正常工作,数据丢失却要等到后来才暴露为“我的修改被还原了”——因此,以下规则会先给出问题发生的证据,其中大部分是直接针对 Yjs 13.6.31 测得的,而非仅凭断言。
适用场景
- 在 Next.js 16 App Router 应用中构建协同编辑、在线状态或离线同步功能
- 为文档模型选择共享类型,或审查在并发情况下会丢失编辑内容的模型
- 决定同步后端的运行位置,或在 Yjs Provider 旁接入 tRPC procedure
- 将
Y.Doc状态持久化到数据库,或压缩存储的更新 - 将 Yjs 文档绑定到 React——Provider 生命周期、订阅、Strict Mode、SSR
- 将 Tiptap 或 shadcn/ui 表单控件接入实时文档
- 调试已完成同步,却出现内容重复、空白或还原的文档
本技能不涵盖通用的 Next.js App Router 模式(请使用 nextjs 技能)、通用 tRPC 用法(trpc)或 shadcn/ui 组件约定(shadcn)——只涵盖这些内容与 CRDT 交互的部分。
版本锁定
规则针对 Yjs 13.6.31、y-websocket 3.0.0、y-protocols 1.0.7 和 y-indexeddb 9.0.12。Yjs 正在迁移到 @y/* 作用域下的 v14,多个软件包已将 latest 或 main 切换到该预发布版本轨道——@y/[email protected] 依赖 yjs@^14.0.0-7。安装任何内容之前,请参阅 host-pin-the-yjs-13-track。
规则类别
| # | 类别 | 前缀 | 涵盖内容 |
|---|---|---|---|
| 1 | 同步拓扑与依赖项 | host- | 同步循环的运行位置、要基于哪种后端构建、信任边界的位置,以及哪些版本属于 v13 轨道 |
| 2 | 二进制传输与持久化 | wire- | 在 JSON 边界间传输 Uint8Array 更新并写入存储 |
| 3 | 在共享类型中建模状态 | model- | 每个字段应使用哪种共享类型——决定并发编辑是合并还是互相覆盖 |
| 4 | 文档生命周期与 React 绑定 | react- | 跨渲染共享一个文档、Strict Mode、订阅、服务端边界 |
| 5 | 撤销、快照与离线加载 | hist- | 按来源区分历史记录、垃圾回收、加载顺序 |
| 6 | 在线状态与光标 | pres- | 临时在线状态,以及在并发编辑后仍能定位的光标 |
| 7 | 编辑器与表单集成 | ui- | Tiptap 和 shadcn/ui 表单控件与实时文档的集成 |
快速参考
1. 同步拓扑与依赖项
host-route-handlers-cannot-upgrade— App Router 没有套接字升级 API;同步服务需要独立于请求运行host-vercel-lacks-instance-affinity— Vercel 支持 WebSocket,但不会将同一个房间固定到同一实例;内存注册表会把一个文档拆成两份host-websocket-server-is-not-production— 参考服务只是起点,并且YPERSISTENCE已不存在host-pin-the-yjs-13-track— 安装latest会将 Yjs 14 预发布版引入 v13 应用host-trpc-is-not-the-sync-channel— SSE 是单向的,二进制输入只能通过 POST 发送;使用 tRPC 进行授权和处理快照host-authorize-rooms-not-updates— CRDT 更新无法通过 schema 验证或进行权限检查,因此房间就是信任边界
2. 二进制传输与持久化
wire-rehydrate-to-uint8array—applyUpdate接受number[],但会静默地不应用任何内容wire-base64-not-superjson— superjson 往返序列化会让类型化数组的负载膨胀至 3.69 倍;base64 的开销为 1.33 倍wire-send-updates-not-state— 输入一个字符时,增量为 24 字节,而完整状态为 5041 字节wire-compaction-needs-a-doc-roundtrip—mergeUpdates会重新编码,但不会回收已删除的内容
3. 在共享类型中建模状态
model-nested-types-not-plain-objects—Y.Map中的普通对象是一个冲突单元;并发编辑不同字段时,其中一个编辑会丢失model-yarray-has-no-move— 两个人同时拖动同一行时,先删除再插入会导致该行重复model-ytext-only-for-prose— 两个人重新输入同一个Y.Text标题时,两个值会拼接在一起model-seed-the-document-once— 每个客户端都会通过空状态检查,因此模板会写入两次
4. 文档生命周期与 React 绑定
react-stable-doc-and-provider— 在同一个 effect 中负责创建和销毁,因为 Strict Mode 会依次运行 setup、cleanup、setupreact-subscribe-via-usesyncexternalstore— 没有官方绑定,而且每次调用toJSON()都会返回一个新引用react-keep-yjs-client-only— provider 需要浏览器全局对象,而'use client'仍会在服务器上渲染
5. 撤销、快照与离线加载
hist-scope-undo-by-origin— 默认的撤销管理器会跟踪远程编辑,因此撤销时可能会还原同事的工作hist-snapshots-require-gc-disabled— 必须在构造时决定;否则恢复快照会抛出异常hist-wait-for-indexeddb-sync— 无法区分尚未加载的文档和空文档
6. Awareness 与光标
pres-awareness-is-ephemeral— 文档中的 presence 是永久且带版本的,也永远不会被清理pres-relative-positions-for-cursors— 只要有人在索引上方输入内容,该索引就不再指向同一个位置
7. 编辑器与表单集成
ui-do-not-drive-inputs-from-the-doc— 按规范,赋值给value会使光标跳到字段末尾ui-tiptap-collaboration-wiring— Tiptap v3 使用自己的分支,而content会在每次加载时重新初始化
如何使用
在需要做出相关决策时,阅读对应的参考文件。每条规则都会指出它要纠正的错误默认做法,然后展示规范做法;如果错误做法确实容易让人踩坑,还会对比错误与正确的写法。
排查问题而非编写代码时,请从症状入手:
| 症状 | 优先阅读 |
|---|---|
| 文档加载后为空白 | wire-rehydrate-to-uint8array、hist-wait-for-indexeddb-sync |
| 内容出现两次 | model-seed-the-document-once、ui-tiptap-collaboration-wiring |
| 某人的编辑被悄悄还原 | model-nested-types-not-plain-objects、hist-scope-undo-by-origin |
| 拖动时列表项重复 | model-yarray-has-no-move |
| 两个编辑器始终无法互相看到对方的内容 | host-vercel-lacks-instance-affinity |
| 用户编辑了其无权访问的字段 | host-authorize-rooms-not-updates |
| 协作者离开后,其头像仍然显示 | pres-awareness-is-ephemeral |
| 输入时光标跳到末尾 | ui-do-not-drive-inputs-from-the-doc |
| 无限渲染循环或快照错误 | react-subscribe-via-usesyncexternalstore |
| 在生产环境中正常运行,在开发环境中却出错 | react-stable-doc-and-provider |
参考文件
| 文件 | 描述 |
|---|---|
| references/_sections.md | 分类定义与排序 |
| assets/templates/_template.md | 新规则模板 |
| AGENTS.md | 自动生成的所有规则目录 |
| metadata.json | 版本与来源引用 |