重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
real-time-collaboration-engine by erichowens/some_claude_skills
npx skills add https://github.com/erichowens/some_claude_skills --skill real-time-collaboration-engine专长于构建类 Google Docs 风格的协作编辑系统,涵盖 WebSocket、冲突解决和在线状态感知。
✅ 适用于 :
❌ 不适用于 :
Need real-time collaboration?
├── Text editing? → Operational Transform (OT)
├── JSON data structures? → CRDTs
├── Cursor tracking only? → Simple WebSocket + presence
├── Offline-first? → CRDTs (better offline merge)
└── No conflicts possible? → Basic broadcast
| 策略 | 最佳适用场景 | 复杂度 | 离线支持 |
|---|---|---|---|
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 操作转换 (OT) |
| 文本、有序序列 |
| 高 |
| 有限 |
| CRDTs | JSON 对象、集合 | 中 | 优秀 |
| 最后写入获胜 | 简单状态 | 低 | 基础 |
| 三路合并 | Git 风格编辑 | 高 | 良好 |
时间线 :
新手想法 : "立即发送每一个变更以获得实时感"
问题 : 网络被大量微小消息淹没,性能低下。
错误做法 :
// ❌ 每次击键都发送消息
function Editor() {
const handleChange = (text: string) => {
socket.emit('text-change', { text }); // 每次击键!
};
return <textarea onChange={(e) => handleChange(e.target.value)} />;
}
为何错误 : 100 WPM 打字速度 = 500 条消息/分钟 = 网络拥塞。
正确做法 :
// ✅ 每 200ms 批量处理变更
function Editor() {
const [pendingChanges, setPendingChanges] = useState<Change[]>([]);
useEffect(() => {
const interval = setInterval(() => {
if (pendingChanges.length > 0) {
socket.emit('text-batch', { changes: pendingChanges });
setPendingChanges([]);
}
}, 200);
return () => clearInterval(interval);
}, [pendingChanges]);
const handleChange = (change: Change) => {
setPendingChanges(prev => [...prev, change]);
};
return <textarea onChange={handleChange} />;
}
影响 : 500 条消息/分钟 → 5 条消息/秒 (减少 90%)。
问题 : 并发编辑导致数据丢失或损坏。
症状 : 用户看到自己的更改消失,文档变得不一致。
错误做法 :
// ❌ 最后写入获胜,覆盖并发更改
socket.on('text-change', ({ userId, text }) => {
setDocument(text); // 丢失并发编辑!
});
为何错误 : 如果用户 A 和 B 同时编辑,一个更改会丢失。
正确做法 (OT) :
// ✅ 文本的操作转换
import { TextOperation } from 'ot.js';
socket.on('operation', ({ userId, operation, revision }) => {
const transformed = transformOperation(
operation,
pendingOperations,
revision
);
applyOperation(transformed);
incrementRevision();
});
function transformOperation(
incoming: Operation,
pending: Operation[],
baseRevision: number
): Operation {
// 针对待处理操作转换传入操作
let transformed = incoming;
for (const op of pending) {
transformed = TextOperation.transform(transformed, op)[0];
}
return transformed;
}
正确做法 (CRDT) :
// ✅ JSON 对象的 CRDT
import * as Y from 'yjs';
const ydoc = new Y.Doc();
const ytext = ydoc.getText('document');
// 自动处理冲突
ytext.insert(0, 'Hello');
// 与对等节点同步
const provider = new WebsocketProvider('ws://localhost:1234', 'room', ydoc);
影响 : 并发编辑正确合并,无数据丢失。
问题 : 用户离线,丢失工作或看到过时状态。
错误做法 :
// ❌ 无离线处理
socket.on('disconnect', () => {
console.log('Disconnected'); // 就这样?!
});
为何错误 : 待处理更改丢失,无重连策略,用户体验差。
正确做法 :
// ✅ 离线时排队更改,重连时同步
const [isOnline, setIsOnline] = useState(true);
const [offlineQueue, setOfflineQueue] = useState<Change[]>([]);
socket.on('disconnect', () => {
setIsOnline(false);
showToast('离线 - 重新连接后将同步更改');
});
socket.on('connect', () => {
setIsOnline(true);
// 发送排队的更改
if (offlineQueue.length > 0) {
socket.emit('sync-offline-changes', { changes: offlineQueue });
setOfflineQueue([]);
}
});
const handleChange = (change: Change) => {
if (isOnline) {
socket.emit('change', change);
} else {
setOfflineQueue(prev => [...prev, change]);
}
};
时间线背景 :
问题 : 无服务器权威,客户端不同步。
错误做法 :
// ❌ 客户端直接相互广播
socket.on('peer-change', ({ userId, change }) => {
applyChange(change); // 无验证,无服务器状态
});
为何错误 : 恶意客户端可发送无效数据,无法从不同步状态恢复。
正确做法 :
// ✅ 服务器是真相来源
// 客户端
socket.emit('operation', { operation, clientRevision });
socket.on('ack', ({ serverRevision }) => {
if (serverRevision !== expectedRevision) {
// 检测到不同步,请求完整状态
socket.emit('request-full-state');
}
});
// 服务器
io.on('connection', (socket) => {
socket.on('operation', ({ operation, clientRevision }) => {
// 验证操作
if (!isValid(operation)) {
socket.emit('error', { message: 'Invalid operation' });
return;
}
// 应用到服务器状态
const serverRevision = applyOperation(operation);
// 广播给所有客户端
io.emit('operation', { operation, serverRevision });
});
});
影响 : 数据完整性得到保证,可从客户端错误中恢复。
问题 : 用户无法看到谁在编辑什么,导致编辑冲突。
症状 : 两人在不知情的情况下编辑同一部分。
错误做法 :
// ❌ 无其他用户感知
function Editor() {
return <textarea />; // 盲目操作!
}
正确做法 :
// ✅ 显示活跃用户和光标
import { usePresence } from './usePresence';
function Editor() {
const { users, updateCursor } = usePresence();
const handleCursorMove = (position: number) => {
socket.emit('cursor-move', { userId: myId, position });
};
return (
<div>
{/* 显示谁在线 */}
<UserList users={users} />
{/* 显示远程光标 */}
<EditorWithCursors
content={content}
cursors={users.map(u => u.cursor)}
onCursorMove={handleCursorMove}
/>
</div>
);
}
功能 :
import { io } from 'socket.io-client';
const socket = io('ws://localhost:3000', {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: Infinity,
transports: ['websocket', 'polling'] // 后备方案
});
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
socket.on('disconnect', (reason) => {
if (reason === 'io server disconnect') {
// 服务器断开连接,手动重连
socket.connect();
}
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
});
import { TextOperation } from 'ot.js';
class OTEditor {
private revision = 0;
private pendingOperations: TextOperation[] = [];
applyLocalOperation(op: TextOperation): void {
// 立即应用 (乐观更新)
this.applyToEditor(op);
// 发送到服务器
this.sendOperation(op);
// 存储为待处理
this.pendingOperations.push(op);
}
receiveRemoteOperation(op: TextOperation, serverRevision: number): void {
// 针对待处理操作进行转换
let transformed = op;
for (const pending of this.pendingOperations) {
[transformed, pending] = TextOperation.transform(transformed, pending);
}
// 应用转换后的操作
this.applyToEditor(transformed);
this.revision = serverRevision;
}
acknowledgeOperation(serverRevision: number): void {
// 从待处理中移除已确认的操作
this.pendingOperations.shift();
this.revision = serverRevision;
}
}
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
// 创建共享文档
const ydoc = new Y.Doc();
// 定义共享类型
const ytext = ydoc.getText('content');
const ymap = ydoc.getMap('metadata');
const yarray = ydoc.getArray('users');
// 连接到同步服务器
const provider = new WebsocketProvider(
'ws://localhost:1234',
'room-name',
ydoc
);
// 监听更改
ytext.observe(event => {
console.log('Text changed:', event.changes);
});
// 进行更改 (自动同步)
ytext.insert(0, 'Hello ');
ytext.insert(6, 'World!');
// 撤销/重做支持
const undoManager = new Y.UndoManager(ytext);
undoManager.undo();
undoManager.redo();
import { Awareness } from 'y-protocols/awareness';
const awareness = provider.awareness;
// 设置本地状态
awareness.setLocalState({
user: {
name: 'Alice',
color: '#ff0000',
cursor: { line: 10, ch: 5 }
}
});
// 监听更改
awareness.on('change', ({ added, updated, removed }) => {
// 使用用户光标/选择更新 UI
const states = awareness.getStates();
states.forEach((state, clientId) => {
if (clientId !== awareness.clientID) {
renderCursor(state.user.cursor, state.user.color);
}
});
});
class OptimisticEditor {
private optimisticChanges = new Map<string, Change>();
async applyChange(change: Change): Promise<void> {
const changeId = generateId();
// 立即应用 (乐观)
this.applyToUI(change);
this.optimisticChanges.set(changeId, change);
try {
// 发送到服务器
const result = await this.sendToServer(change);
// 成功 - 从乐观更新中移除
this.optimisticChanges.delete(changeId);
} catch (error) {
// 失败 - 回滚
this.rollback(changeId);
this.showError('Could not apply change');
}
}
private rollback(changeId: string): void {
const change = this.optimisticChanges.get(changeId);
if (change) {
this.revertInUI(change);
this.optimisticChanges.delete(changeId);
}
}
}
□ 带自动重连的 WebSocket 连接
□ 待处理更改的离线队列
□ 冲突解决策略 (OT 或 CRDT)
□ 服务器权威 (客户端无法导致不同步)
□ 在线状态感知 (光标、活跃用户)
□ 带回滚的乐观更新
□ 变更批处理 (非每次击键)
□ 大负载的消息压缩
□ 身份验证和授权
□ 速率限制 (防止垃圾信息)
□ 心跳/Ping-Pong 以检测死连接
□ 优雅降级 (如果 WebSocket 失败则回退到轮询)
| 场景 | 策略 |
|---|---|
| 文本编辑 (Google Docs) | ✅ 操作转换 |
| JSON 对象 (Figma) | ✅ CRDTs (Yjs, Automerge) |
| 简单光标共享 | ✅ 基础 WebSocket + 在线状态感知 |
| 聊天消息 | ✅ 简单仅追加 (无 OT/CRDT) |
| 视频时间线编辑 | ✅ 时间线用 CRDTs,文本用 OT |
| 只读仪表板 | ❌ 改用 Server-Sent Events |
/references/ot-vs-crdt.md - 冲突解决策略深度比较/references/websocket-scaling.md - 扩展到数百万并发连接/references/presence-patterns.md - 光标跟踪、用户感知、活动指示器scripts/collaboration_tester.ts - 模拟并发编辑,测试冲突解决scripts/latency_simulator.ts - 在高延迟/丢包下测试行为此技能指导 : 实时协作 | WebSocket 架构 | 操作转换 | CRDTs | 在线状态感知 | 冲突解决
每周安装次数
48
仓库
GitHub 星标数
82
首次出现
2026年1月24日
安全审计
安装于
cursor43
opencode42
codex41
gemini-cli41
github-copilot37
cline35
Expert in building Google Docs-style collaborative editing with WebSockets, conflict resolution, and presence awareness.
✅ Use for :
❌ NOT for :
Need real-time collaboration?
├── Text editing? → Operational Transform (OT)
├── JSON data structures? → CRDTs
├── Cursor tracking only? → Simple WebSocket + presence
├── Offline-first? → CRDTs (better offline merge)
└── No conflicts possible? → Basic broadcast
| Strategy | Best For | Complexity | Offline Support |
|---|---|---|---|
| Operational Transform (OT) | Text, ordered sequences | High | Limited |
| CRDTs | JSON objects, sets | Medium | Excellent |
| Last-Write-Wins | Simple state | Low | Basic |
| Three-Way Merge | Git-style editing | High | Good |
Timeline :
Novice thinking : "Send every change immediately for real-time feel"
Problem : Network floods with tiny messages, poor performance.
Wrong approach :
// ❌ Sends message on every keystroke
function Editor() {
const handleChange = (text: string) => {
socket.emit('text-change', { text }); // Every keystroke!
};
return <textarea onChange={(e) => handleChange(e.target.value)} />;
}
Why wrong : 100 WPM typing = 500 messages/minute = network congestion.
Correct approach :
// ✅ Batches changes every 200ms
function Editor() {
const [pendingChanges, setPendingChanges] = useState<Change[]>([]);
useEffect(() => {
const interval = setInterval(() => {
if (pendingChanges.length > 0) {
socket.emit('text-batch', { changes: pendingChanges });
setPendingChanges([]);
}
}, 200);
return () => clearInterval(interval);
}, [pendingChanges]);
const handleChange = (change: Change) => {
setPendingChanges(prev => [...prev, change]);
};
return <textarea onChange={handleChange} />;
}
Impact : 500 messages/minute → 5 messages/second (90% reduction).
Problem : Concurrent edits cause data loss or corruption.
Symptom : Users see their changes disappear, documents become inconsistent.
Wrong approach :
// ❌ Last write wins, overwrites concurrent changes
socket.on('text-change', ({ userId, text }) => {
setDocument(text); // Loses concurrent edits!
});
Why wrong : If User A and B edit simultaneously, one change is lost.
Correct approach (OT) :
// ✅ Operational Transform for text
import { TextOperation } from 'ot.js';
socket.on('operation', ({ userId, operation, revision }) => {
const transformed = transformOperation(
operation,
pendingOperations,
revision
);
applyOperation(transformed);
incrementRevision();
});
function transformOperation(
incoming: Operation,
pending: Operation[],
baseRevision: number
): Operation {
// Transform incoming against pending operations
let transformed = incoming;
for (const op of pending) {
transformed = TextOperation.transform(transformed, op)[0];
}
return transformed;
}
Correct approach (CRDT) :
// ✅ CRDT for JSON objects
import * as Y from 'yjs';
const ydoc = new Y.Doc();
const ytext = ydoc.getText('document');
// Automatically handles conflicts
ytext.insert(0, 'Hello');
// Sync with peers
const provider = new WebsocketProvider('ws://localhost:1234', 'room', ydoc);
Impact : Concurrent edits merge correctly, no data loss.
Problem : User goes offline, loses work or sees stale state.
Wrong approach :
// ❌ No offline handling
socket.on('disconnect', () => {
console.log('Disconnected'); // That's it?!
});
Why wrong : Pending changes lost, no reconnection strategy, bad UX.
Correct approach :
// ✅ Queue changes offline, sync on reconnect
const [isOnline, setIsOnline] = useState(true);
const [offlineQueue, setOfflineQueue] = useState<Change[]>([]);
socket.on('disconnect', () => {
setIsOnline(false);
showToast('Offline - changes will sync when reconnected');
});
socket.on('connect', () => {
setIsOnline(true);
// Send queued changes
if (offlineQueue.length > 0) {
socket.emit('sync-offline-changes', { changes: offlineQueue });
setOfflineQueue([]);
}
});
const handleChange = (change: Change) => {
if (isOnline) {
socket.emit('change', change);
} else {
setOfflineQueue(prev => [...prev, change]);
}
};
Timeline context :
Problem : No server authority, clients get out of sync.
Wrong approach :
// ❌ Clients broadcast to each other directly
socket.on('peer-change', ({ userId, change }) => {
applyChange(change); // No validation, no server state
});
Why wrong : Malicious client can send invalid data, no recovery from desync.
Correct approach :
// ✅ Server is source of truth
// Client
socket.emit('operation', { operation, clientRevision });
socket.on('ack', ({ serverRevision }) => {
if (serverRevision !== expectedRevision) {
// Desync detected, request full state
socket.emit('request-full-state');
}
});
// Server
io.on('connection', (socket) => {
socket.on('operation', ({ operation, clientRevision }) => {
// Validate operation
if (!isValid(operation)) {
socket.emit('error', { message: 'Invalid operation' });
return;
}
// Apply to server state
const serverRevision = applyOperation(operation);
// Broadcast to all clients
io.emit('operation', { operation, serverRevision });
});
});
Impact : Data integrity guaranteed, can recover from client bugs.
Problem : Users can't see who's editing what, causing edit conflicts.
Symptom : Two people editing same section unknowingly.
Wrong approach :
// ❌ No awareness of other users
function Editor() {
return <textarea />; // Flying blind!
}
Correct approach :
// ✅ Show active users and cursors
import { usePresence } from './usePresence';
function Editor() {
const { users, updateCursor } = usePresence();
const handleCursorMove = (position: number) => {
socket.emit('cursor-move', { userId: myId, position });
};
return (
<div>
{/* Show who's online */}
<UserList users={users} />
{/* Show remote cursors */}
<EditorWithCursors
content={content}
cursors={users.map(u => u.cursor)}
onCursorMove={handleCursorMove}
/>
</div>
);
}
Features :
import { io } from 'socket.io-client';
const socket = io('ws://localhost:3000', {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: Infinity,
transports: ['websocket', 'polling'] // Fallback
});
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
socket.on('disconnect', (reason) => {
if (reason === 'io server disconnect') {
// Server disconnected, manually reconnect
socket.connect();
}
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
});
import { TextOperation } from 'ot.js';
class OTEditor {
private revision = 0;
private pendingOperations: TextOperation[] = [];
applyLocalOperation(op: TextOperation): void {
// Apply immediately (optimistic update)
this.applyToEditor(op);
// Send to server
this.sendOperation(op);
// Store as pending
this.pendingOperations.push(op);
}
receiveRemoteOperation(op: TextOperation, serverRevision: number): void {
// Transform against pending operations
let transformed = op;
for (const pending of this.pendingOperations) {
[transformed, pending] = TextOperation.transform(transformed, pending);
}
// Apply transformed operation
this.applyToEditor(transformed);
this.revision = serverRevision;
}
acknowledgeOperation(serverRevision: number): void {
// Remove acknowledged operation from pending
this.pendingOperations.shift();
this.revision = serverRevision;
}
}
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
// Create shared document
const ydoc = new Y.Doc();
// Define shared types
const ytext = ydoc.getText('content');
const ymap = ydoc.getMap('metadata');
const yarray = ydoc.getArray('users');
// Connect to sync server
const provider = new WebsocketProvider(
'ws://localhost:1234',
'room-name',
ydoc
);
// Listen to changes
ytext.observe(event => {
console.log('Text changed:', event.changes);
});
// Make changes (automatically synced)
ytext.insert(0, 'Hello ');
ytext.insert(6, 'World!');
// Undo/redo support
const undoManager = new Y.UndoManager(ytext);
undoManager.undo();
undoManager.redo();
import { Awareness } from 'y-protocols/awareness';
const awareness = provider.awareness;
// Set local state
awareness.setLocalState({
user: {
name: 'Alice',
color: '#ff0000',
cursor: { line: 10, ch: 5 }
}
});
// Listen to changes
awareness.on('change', ({ added, updated, removed }) => {
// Update UI with user cursors/selections
const states = awareness.getStates();
states.forEach((state, clientId) => {
if (clientId !== awareness.clientID) {
renderCursor(state.user.cursor, state.user.color);
}
});
});
class OptimisticEditor {
private optimisticChanges = new Map<string, Change>();
async applyChange(change: Change): Promise<void> {
const changeId = generateId();
// Apply immediately (optimistic)
this.applyToUI(change);
this.optimisticChanges.set(changeId, change);
try {
// Send to server
const result = await this.sendToServer(change);
// Success - remove from optimistic
this.optimisticChanges.delete(changeId);
} catch (error) {
// Failed - rollback
this.rollback(changeId);
this.showError('Could not apply change');
}
}
private rollback(changeId: string): void {
const change = this.optimisticChanges.get(changeId);
if (change) {
this.revertInUI(change);
this.optimisticChanges.delete(changeId);
}
}
}
□ WebSocket connection with auto-reconnect
□ Offline queue for pending changes
□ Conflict resolution strategy (OT or CRDT)
□ Server authority (clients can't desync)
□ Presence awareness (cursors, active users)
□ Optimistic updates with rollback
□ Change batching (not per-keystroke)
□ Message compression for large payloads
□ Authentication and authorization
□ Rate limiting (prevent spam)
□ Heartbeat/ping-pong to detect dead connections
□ Graceful degradation (falls back to polling if WebSocket fails)
| Scenario | Strategy |
|---|---|
| Text editing (Google Docs) | ✅ Operational Transform |
| JSON objects (Figma) | ✅ CRDTs (Yjs, Automerge) |
| Simple cursor sharing | ✅ Basic WebSocket + presence |
| Chat messages | ✅ Simple append-only (no OT/CRDT) |
| Video timeline editing | ✅ CRDTs for timeline, OT for text |
| Read-only dashboards | ❌ Use Server-Sent Events instead |
/references/ot-vs-crdt.md - Deep comparison of conflict resolution strategies/references/websocket-scaling.md - Scaling to millions of concurrent connections/references/presence-patterns.md - Cursor tracking, user awareness, activity indicatorsscripts/collaboration_tester.ts - Simulate concurrent edits, test conflict resolutionscripts/latency_simulator.ts - Test behavior under high latency/packet lossThis skill guides : Real-time collaboration | WebSocket architecture | Operational Transform | CRDTs | Presence awareness | Conflict resolution
Weekly Installs
48
Repository
GitHub Stars
82
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
cursor43
opencode42
codex41
gemini-cli41
github-copilot37
cline35
Laravel架构模式指南:生产级开发模式与最佳实践
1,400 周安装