tldraw Sync 深度指南:用 @tldraw/sync-core 构建实时协作画布应用
【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw
@tldraw/sync-core是 tldraw SDK 中负责实时协作与状态同步的核心包,它为多人同时在同一个无限画布上编辑提供了底层的客户端-服务器同步协议,涵盖网络可靠性、冲突解决与分布式数据一致性。本指南围绕该包随发布版本附带的 DOCS.md 展开,并结合仓库源码,带你从零搭建一个可运行的协作应用:理解服务器权威模型、房间与会话机制、网络差异(diff)协议,掌握客户端接入、服务端房间管理、presence 实时光标、schema 迁移、断线重连与调试排障的完整实战路径。
1. Sync-core 是什么:协作引擎的定位
Sync-core是驱动 tldraw 实时协作的引擎。它让多个用户能同时编辑同一份画布文档:任一用户产生的改动会自动同步给所有已连接用户,同时优雅地处理网络中断与编辑冲突。
使用它的基本方式是将客户端连接到 sync room(同步房间),每个房间负责管理一份文档的共享状态:
import { TLSyncClient } from '@tldraw/sync-core' // 连接到一个协作房间 const syncClient = new TLSyncClient({ store: myTldrawStore, socket: myWebSocketAdapter, roomId: 'drawing-room-123', }) syncClient.connect() // 现在 myTldrawStore 的所有改动都会与其他用户同步当你在本地更新 store 时,改动会立即反映在 UI 上(乐观更新),随后被发送到服务器进行校验,再由服务器分发给其他客户端。在源码层面,这个"立即生效、随后推送"的流程由 TLSyncClient.ts 中的push方法实现:本地用户改动通过store.listen(..., { source: 'user', scope: 'document' })被捕获,先累积到speculativeChanges(推测性改动),再以push消息发给服务器。
提示:Sync-core 被设计为可与任何 WebSocket 实现协作,因此既适合简单的 Node.js 服务器,也适合 Cloudflare Workers 等边缘计算平台。实际上 TLSyncRoom.ts 在构造时会断言运行环境必须支持原生
structuredClone(Cloudflare Workers 或 Node 18+),这正是"跨平台"设计的一处源码证据。
2. 核心概念
2.1 客户端-服务器架构
Sync-core 采用**服务器权威(server-authoritative)**模型:服务器是所有改动的唯一权威来源。这样既保证了数据一致性,又保留了响应式的本地交互:
- 乐观更新:本地改动立即生效,UI 无需等待网络往返;
- 服务器校验:服务器会校验甚至修改你的改动(例如通过 authorizer 强制把评论的
authorId写成当前登录用户); - 冲突解决:发生冲突时,以服务器版本为准。
客户端的同步过程遵循类似 git 的 push/pull/rebase 模型(见 TLSyncClient.ts 的类注释):
- Push:本地改动以 diff 操作形式发送给服务器;
- Pull:接收服务器下发的变化并应用到本地;
- Rebase:冲突时先撤销本地改动,应用服务器改动,再把本地改动重新应用上去。
2.2 房间(Rooms)与会话(Sessions)
一个room代表一个多人协作的文档空间:
// 服务端房间管理 const room = new TLSyncRoom({ store: serverStore, roomId: 'drawing-room-123', }) // 每个连接的客户端都会创建一个会话 room.handleSocketConnect(clientSocket, sessionMeta)每条客户端连接都会在房间内创建一个session(会话),用于追踪该用户的连接状态、权限与 presence 信息。从源码看,会话有完整的生命周期状态机(定义于 RoomSession.ts):
| 状态 | 含义 | 超时/清理条件 |
|---|---|---|
AwaitingConnectMessage | 连接已建立,等待客户端发送connect握手消息 | 超过SESSION_START_WAIT_TIME(10000ms)未握手即移除 |
Connected | 已完成握手,正常同步中 | 超过SESSION_IDLE_TIMEOUT(20000ms)无交互或 socket 关闭则取消 |
AwaitingRemoval | 已取消连接,等待清理 | 超过SESSION_REMOVAL_WAIT_TIME(5000ms)后移除并广播session_removed |
这些超时常量均可在 RoomSession.ts 中查到:SESSION_START_WAIT_TIME = 10000、SESSION_REMOVAL_WAIT_TIME = 5000、SESSION_IDLE_TIMEOUT = 20000(毫秒)。TLSyncRoom通过节流后的pruneSessions(每 1000ms 节流一次)周期性清理空闲会话;当房间内最后一个会话移除时,还会触发room_became_empty事件,便于宿主回收房间资源。
2.3 网络差异(Network Diffs)与同步
Sync-core 不会发送整个文档状态,而是发送网络差异(network diff)——一种紧凑的"到底改了什么"的表示:
// 更新某个 shape 位置的网络差异示例 const diff = { 'shape:abc123': [ RecordOpType.Patch, { x: [ValueOpType.Put, 150], y: [ValueOpType.Put, 200], }, ], }这种方式最小化带宽占用,即使是大文档也能高效同步。从源码看,网络差异由三种记录操作组成(diff.ts):
RecordOpType.Put('put'):完整放入一条记录(新增或整体替换);RecordOpType.Patch('patch'):仅修改记录的若干字段,值操作又分为ValueOpType.Put(设置新值)与ValueOpType.Append(字符串追加等);RecordOpType.Remove('remove'):删除一条记录。
本地可逆的RecordsDiff(含 added/updated/removed 三张表)会经getNetworkDiff转换为不可逆但更紧凑的NetworkDiff再上线传输。服务器侧收到push后,会在事务内把改动写入存储,并计算出"实际落库的差异"回传给客户端确认(commit)或要求重放(rebase),详见 TLSyncRoom.ts 的handlePushRequest。
3. 基本用法
3.1 搭建同步客户端
要为你的 tldraw 应用开启同步,需要三个组件:store、WebSocket 适配器、同步客户端:
import { createTLStore } from '@tldraw/store' import { createTLSchema } from '@tldraw/tlschema' import { TLSyncClient, ClientWebSocketAdapter } from '@tldraw/sync-core' // 创建你的 tldraw store const store = createTLStore({ schema: createTLSchema(), }) // 创建 WebSocket 连接 const socket = new ClientWebSocketAdapter('ws://localhost:3000/sync') // 创建同步客户端 const syncClient = new TLSyncClient({ store, socket, roomId: 'my-drawing-room', }) // 开始同步 syncClient.connect()连接成功后,store 的任何改动都会自动与同房间的其他客户端同步。需要说明的是,实际源码中ClientWebSocketAdapter的构造函数接收的是一个返回 URI 的函数(getUri: () => Promise<string> | string),而不是字符串本身(见 ClientWebSocketAdapter.ts),这样每次重连都会重新调用,便于动态拼接带鉴权 token 的地址。而TLSyncClient的完整构造参数(TLSyncClient.ts)还包括presence(presence 数据的响应式信号)、presenceMode('solo'或'full')、onLoad(首次同步完成回调)、onSyncError(同步失败回调)、onCustomMessageReceived(自定义消息处理器)与onAfterConnect(连接后回调,携带isReadonly与objectAccess)。
3.2 监控连接状态
同步客户端提供响应式的状态信息:
import { react } from '@tldraw/state' // 响应连接状态变化 react('connection status', () => { const status = syncClient.status.get() switch (status) { case 'offline': console.log('No network connection') break case 'connecting': console.log('Connecting to server...') break case 'online': console.log('Connected and synchronized') break } })status信号会随网络状况自动更新,让 UI 实时反映连接状态。在底层,socket 状态由TLPersistentClientSocketStatus定义,实际取值是'online' | 'offline' | 'error'(TLSyncClient.ts),此外TLSocketStatusChangeEvent在error时还携带reason字段描述错误原因。
3.3 处理连接事件
你可以监听特定的同步事件来实现自定义行为:
syncClient.onReceiveMessage((message) => { switch (message.type) { case 'connect': console.log('Successfully connected to room') break case 'incompatibility-error': console.log('Client version incompatible with server') break } })提示:始终优雅地处理不兼容错误——它们意味着客户端与服务器之间出现了版本不匹配。
值得注意的是,现代 tldraw sync 协议(当前为协议版本 8,见 protocol.ts 的TLSYNC_PROTOCOL_VERSION)已弃用incompatibility_error消息,改为通过WebSocket close code 4099 + 关闭原因来传达致命错误(TLSyncClient.ts)。服务器可关闭连接的原因包括:
| 关闭原因 | 含义 |
|---|---|
NOT_FOUND | 房间或资源不存在 |
FORBIDDEN | 用户缺少访问权限 |
NOT_AUTHENTICATED | 未认证或认证无效 |
UNKNOWN_ERROR | 意外的服务器错误 |
CLIENT_TOO_OLD | 客户端协议版本过旧 |
SERVER_TOO_OLD | 服务器协议版本过旧 |
INVALID_RECORD | 客户端发送了无效或损坏的记录 |
RATE_LIMITED | 客户端触发限流 |
ROOM_FULL | 房间已达最大容量 |
客户端可通过onSyncError(reason)回调收到这些原因字符串,按需给用户展示"房间不存在""无权限""请升级应用"等提示。
4. 高级主题
4.1 服务端房间管理
在服务端,你管理协调多个客户端会话的房间:
import { TLSyncRoom } from '@tldraw/sync-core' class CollaborationServer { private rooms = new Map<string, TLSyncRoom>() getOrCreateRoom(roomId: string) { if (!this.rooms.has(roomId)) { const room = new TLSyncRoom({ store: this.createRoomStore(), roomId, // 可选:持久化适配器 persistenceAdapter: this.createPersistenceAdapter(roomId), }) this.rooms.set(roomId, room) } return this.rooms.get(roomId)! } handleClientConnection(socket: WebSocket, roomId: string) { const room = this.getOrCreateRoom(roomId) room.handleSocketConnect(socket, { sessionId: generateSessionId(), userId: extractUserId(socket), isReadonly: checkPermissions(socket), }) } }房间会自动处理会话生命周期、广播变更、清理断开的客户端。在实际代码中,面向业务集成的高层封装是TLSocketRoom(TLSocketRoom.ts),它内部包含一个TLSyncRoom,并提供了更贴近实际 WebSocket 服务器的 API:
handleSocketConnect({ sessionId, socket, meta, isReadonly, objectAccess }):接入新连接并自动挂接消息/关闭事件处理;handleMessage(sessionId, message)/handleClose(sessionId):转发客户端消息与关闭事件;getSnapshot()/loadSnapshot(snapshot):读取与恢复房间完整快照(含时钟、文档、墓碑记录);sendCustomMessage(sessionId, data):向单个客户端发送应用自定义消息;onSessionRemoved回调:客户端断开时通知宿主(常用于"房间空了就关闭回收");clientTimeout:会话空闲超时配置,默认 20000ms。
文档状态本身通过**存储层(storage)**管理,仓库提供了多种实现:InMemorySyncStorage.ts(内存存储,含默认初始快照)、SQLiteSyncStorage.ts(SQLite 持久化)以及面向 Cloudflare Durable Object 的 DurableObjectSqliteSyncWrapper.ts。
4.2 自定义 WebSocket 适配器
虽然 sync-core 提供了ClientWebSocketAdapter,你也可以为特定需求实现自定义适配器:
import { TLPersistentClientSocket } from '@tldraw/sync-core' class CustomSocketAdapter implements TLPersistentClientSocket { status = atom<TLPersistentClientSocketStatus>('offline') sendMessage(message: any): void { // 你的自定义发送逻辑 this.customWebSocket.send(JSON.stringify(message)) } onReceiveMessage = createNanoEvents<any>() onStatusChange = createNanoEvents<TLPersistentClientSocketStatus>() restart(): void { // 你的重连逻辑 } }自定义适配器让你能集成现有的 WebSocket 库,或添加自定义的鉴权与错误处理。需要满足的契约接口TLPersistentClientSocket定义在 TLSyncClient.ts:需要提供connectionStatus('online' | 'offline' | 'error')、sendMessage、onReceiveMessage(订阅函数,返回退订函数)、onStatusChange与restart()/close()。
4.3 冲突解决策略
当多个用户同时编辑时可能产生冲突。Sync-core 的服务器权威模型会自动解决:
// 客户端 A 将 shape 移动到 x: 100 store.update('shape:abc', (shape) => ({ ...shape, x: 100 })) // 与此同时,客户端 B 将同一个 shape 移动到 x: 200 // 服务器收到两个改动并决定最终状态 // 所有客户端都会收到服务器的权威版本 react('shape changes', () => { const shape = store.get('shape:abc') // 最终位置以服务器决定为准 console.log('Final position:', shape?.x) })服务器按收到改动的顺序应用它们,冲突属性上后到的改动优先。更精确地说,服务器的处理结果是三类push_result之一(protocol.ts):
action: 'commit':改动按原样提交,客户端确认成功;action: 'discard':改动被丢弃(例如只读会话的写入被跳过),客户端以服务器状态为准自我纠正;action: { rebaseWithDiff }:服务器的实际落库结果与客户端推送不同,携带权威 diff 让客户端重放。
客户端rebase的实现逻辑在 TLSyncClient.ts:先撤销本地推测性改动,再应用服务器 diff,最后把仍待确认的本地改动重新应用并打包成新的 push。
4.4 实时 Presence 与光标
Sync-core 支持光标位置等实时 presence 信息:
// 客户端发送 presence 更新 syncClient.updatePresence({ cursor: { x: 150, y: 200 }, selection: ['shape:abc123'], userName: 'Alice', }) // 其他客户端接收 presence 更新 syncClient.onPresenceUpdate((presenceUpdates) => { for (const [sessionId, presence] of presenceUpdates) { updateLiveCursor(sessionId, presence.cursor) updateUserSelection(sessionId, presence.selection) } })presence 更新是瞬态的——它们不会持久化到存储,只对当前已连接用户可见。在源码层面,presence 通过presence响应式信号(Signal<R | null>)驱动:客户端在react('pushPresence', ...)中监听该信号,把最新 presence 打包成Put/Patch操作随 push 消息发送(TLSyncClient.ts)。presence 有独立的发送节奏:presenceMode为'solo'时同步帧率降到1 FPS,为'full'时是30 FPS(SOLO_MODE_FPS与COLLABORATIVE_MODE_FPS常量)。服务器把 presence 记录存放在独立的PresenceStore中,会话断开时自动删除并广播移除。
4.5 Schema 演化与迁移
当应用的数据 schema 变化时,sync-core 会在各客户端之间协调迁移:
const schema = createTLSchema({ // 你的 shape 定义 shapes: { myShape: MyShapeUtil, }, }) // 客户端在连接时会发送它的 schema 版本 const syncClient = new TLSyncClient({ store: createTLStore({ schema }), socket, roomId: 'room-123', })如果客户端与服务器的 schema 版本不匹配,sync-core 会:
- 尽可能尝试自动迁移;
- 迁移失败时发送不兼容错误;
- 对未知记录类型允许优雅降级。
提示:设计 schema 变更时尽量向后兼容,避免强制所有用户同时升级。
握手时的版本协商逻辑在 TLSyncRoom.ts 的handleConnectRequest中:客户端在connect消息里携带protocolVersion、序列化 schema 与lastServerClock;服务器会校验协议版本(版本 5 视为 6、6/7 会向上兼容处理),并通过getMigrationsSince判断客户端 schema 是否可迁移。若客户端版本过旧且无可用的向下迁移,则关闭连接并给出CLIENT_TOO_OLD原因。对已连接的旧版本客户端,服务器在广播 diff 前会用migrateDiffOrRejectSession把记录向下迁移到该客户端的 schema 版本(TLSyncRoom.ts),保证新旧客户端共存。
5. 调试(Debugging)
Sync-core 提供了多种工具来理解与调试协作应用中的同步行为。
5.1 连接诊断
监控详细的连接生命周期:
import { TLSyncClient } from '@tldraw/sync-core' const syncClient = new TLSyncClient({ /* ... */ }) // 开启详细日志 syncClient.onReceiveMessage((message) => { console.log('Received:', message.type, message) }) syncClient.onStatusChange((status, previous) => { console.log(`Status: ${previous} → ${status}`) }) // 发起连接 syncClient.connect() // 输出展示了完整的握手过程: // Status: offline → connecting // Received: connect { hydrationType: 'wipe_all', ... } // Status: connecting → online这能揭示连接建立期间的消息序列以及可能出现的任何错误。握手消息的关键字段包括hydrationType('wipe_all'表示服务器历史不足需全量重灌,'wipe_presence'表示仅清空 presence)、connectRequestId(用于匹配旧连接请求)、serverClock(服务器逻辑时钟,客户端以此作为增量拉取的游标)。
5.2 消息流分析
追踪所有同步消息以理解数据流:
// 记录所有出站消息 const originalSend = syncClient.socket.sendMessage syncClient.socket.sendMessage = (message) => { console.log('Sending:', message.type, message) originalSend.call(syncClient.socket, message) } // 做出改动时的输出示例: // Sending: push { diff: { "shape:abc123": [2, { x: [1, 150] }] } } // Received: data { diff: { "shape:abc123": [2, { x: [1, 150] }] } }这展示了本地改动如何变成 push 消息,又从服务器以 data 消息返回。另外,浏览器环境下TLSyncClient构造时会把自己挂到window.tlsync(TLSyncClient.ts),在 DevTools 控制台可直接访问当前同步客户端的内部状态;而ClientWebSocketAdapter也支持通过设置window.__tldraw_socket_debug开启 socket 层的带时间戳日志。
5.3 网络差异检视
理解正在同步的到底是什么变更:
import { diffRecord } from '@tldraw/sync-core' // 监听 store 变化并查看其 diff 表示 const unsubscribe = store.listen( (entry) => { if (entry.changes.length > 0) { for (const change of entry.changes) { console.log('Change type:', change.source) console.log('Record diff:', change) // 详细 diff 分析 if (change.type === 'update') { const diff = diffRecord(change.prev, change.record) console.log('Network diff would be:', diff) } } } }, { source: 'user' } ) // 输出示例: // Change type: user // Record diff: { type: 'update', id: 'shape:abc123', ... } // Network diff would be: { x: [1, 150], y: [1, 200] }其中diffRecord与getNetworkDiff均由 diff.ts 导出,配套的单元测试见 diff.test.ts 与 recordDiff.test.ts,可作为理解 diff 语义的参考。
5.4 会话与房间调试
在服务端检视房间与会话状态:
class DebuggableRoom extends TLSyncRoom { debugSessions() { console.log(`Room ${this.roomId} has ${this.getNumActiveConnections()} connections:`) for (const [sessionId, session] of this.sessions) { console.log( ` ${sessionId}: ${session.state} (${session.isReadonly ? 'readonly' : 'read-write'})` ) } } debugLastChange() { console.log('Last document change:', this.documentState.clock) console.log('Store has', Object.keys(this.store.serialize()).length, 'records') } } // 开发期间使用 const room = new DebuggableRoom({ /* ... */ }) setInterval(() => room.debugSessions(), 5000)TLSyncRoom.sessions是一个以sessionId为键的Map<string, RoomSession>,每个 session 都包含state、isReadonly、objectAccess、meta(宿主自定义的会话元数据)、lastInteractionTime等字段,可直接遍历检视。
5.5 错误诊断
处理并调试常见的同步错误:
syncClient.onReceiveMessage((message) => { switch (message.type) { case 'incompatibility-error': console.error('Schema mismatch:', { clientSchema: message.clientSchema, serverSchema: message.serverSchema, reason: message.reason, }) break case 'error': console.error('Sync error:', message.error) // 常见原因: // - 房间不存在(检查 roomId) // - 权限不足(检查认证) // - 无效的记录数据(检查 schema 校验) break } }) // 网络层调试 syncClient.socket.onStatusChange((status) => { if (status === 'offline') { console.log('Connection lost - check network and server health') // 尝试手动重连 setTimeout(() => { syncClient.socket.restart() }, 1000) } })现代实现中,致命错误通过close code 4099传达(TLSyncErrorCloseEventCode),onSyncError(reason)会收到TLSyncErrorCloseEventReason中列出的原因字符串。注意重连并不总是需要手动触发:ClientWebSocketAdapter内置的ReconnectManager(ClientWebSocketAdapter.ts)会自动处理带指数退避的重连,并监听online、visibilitychange、navigator.connection等事件作为"可重连提示"。
5.6 性能监控
追踪同步性能指标:
class SyncProfiler { private messageCount = 0 private bytesTransferred = 0 private roundTripTimes: number[] = [] profile(syncClient: TLSyncClient) { const startTime = Date.now() syncClient.onReceiveMessage((message) => { this.messageCount++ this.bytesTransferred += JSON.stringify(message).length // 通过 ping/pong 追踪延迟 if (message.type === 'pong') { const roundTrip = Date.now() - message.sentAt this.roundTripTimes.push(roundTrip) } }) // 周期性上报 setInterval(() => { const avgLatency = this.roundTripTimes.length > 0 ? this.roundTripTimes.reduce((a, b) => a + b, 0) / this.roundTripTimes.length : 0 console.log('Sync Performance:', { uptime: Date.now() - startTime, messages: this.messageCount, bytesTransferred: this.bytesTransferred, avgLatencyMs: avgLatency, }) this.roundTripTimes = [] // 重置下个周期 }, 30000) } } new SyncProfiler().profile(syncClient)提示:过高的消息数或延迟通常意味着网络问题或低效的变更模式。可以考虑批处理高频变更或优化 shape 更新逻辑。
关于心跳机制,客户端在在线期间每 5000ms 发送一次ping(PING_INTERVAL),并额外运行一个健康检查循环(每 10000ms 一次):如果距上次服务器交互超过PING_INTERVAL * 2(10 秒)且存在超过PONG_TIMEOUT(10 秒)未应答的 ping,就判定连接失效并调用resetConnection()重启连接([TLSyncClient.ts](https://link.gitcode.com/i/82a1f3969ccb8c74a3bc6a543aa99eae#L287-L294, L608-L662))。服务器对每个ping回复pong并刷新该会话的lastInteractionTime。
6. 集成(Integration)
6.1 React 集成
Sync-core 通过 store 的响应式信号与 React 应用无缝集成:
import { useEditor } from '@tldraw/editor' import { react } from '@tldraw/state' import { useEffect, useState } from 'react' function CollaborationStatusBadge() { const editor = useEditor() const [status, setStatus] = useState<string>('offline') useEffect(() => { if (!editor.store.syncClient) return return react('sync status', () => { setStatus(editor.store.syncClient.status.get()) }) }, [editor]) return ( <div className={`status-badge ${status}`}> {status === 'online' ? '🟢 Connected' : '🔴 Offline'} </div> ) }Sync-core 的响应式特性意味着你的 React 组件会在连接状态或同步数据变化时自动更新。
6.2 自定义持久化
与现有数据库或存储系统集成:
import { TLSyncRoom } from '@tldraw/sync-core' class DatabasePersistenceAdapter { constructor( private db: Database, private roomId: string ) {} async loadRoom(): Promise<SerializedStore> { const roomData = await this.db.query('SELECT document_state FROM rooms WHERE id = ?', [ this.roomId, ]) return JSON.parse(roomData.document_state) } async saveRoom(serializedStore: SerializedStore): Promise<void> { await this.db.query('UPDATE rooms SET document_state = ?, updated_at = NOW() WHERE id = ?', [ JSON.stringify(serializedStore), this.roomId, ]) } } const room = new TLSyncRoom({ store: createTLStore({ schema }), roomId: 'room-123', persistenceAdapter: new DatabasePersistenceAdapter(myDatabase, 'room-123'), })这样房间既能持久化到任意存储后端,又能保持实时同步。在实际源码中,持久化通常通过两个途径实现:一是直接使用内置存储层(InMemorySyncStorage/SQLiteSyncStorage/ Durable Object 包装器),并通过TLSocketRoom.getSnapshot()/loadSnapshot()完成备份与恢复;二是监听onCommittedChanges回调(每次客户端推送提交后触发,携带文档 diff 与documentClock),把关心的记录类型投影到外部存储。
6.3 认证与授权
通过扩展 WebSocket 适配器实现自定义认证:
class AuthenticatedSocketAdapter extends ClientWebSocketAdapter { constructor( url: string, private authToken: string ) { super(url) } protected connect(): void { this.ws = new WebSocket(this.url, [], { headers: { Authorization: `Bearer ${this.authToken}`, }, }) this.setupEventHandlers() } } // 服务端认证 room.handleSocketConnect(socket, { sessionId: generateSessionId(), userId: extractUserFromToken(authToken), isReadonly: !hasEditPermission(authToken, roomId), })服务端还有更细粒度的授权手段:TLSocketRoom的authorizeRecord选项允许为指定记录类型注册 per-record authorizer(TLSyncRoom.ts)。authorizer 在 create/update/delete 时被调用,可以否决写入(返回null),也可以在 create 时改写记录(例如把authorId强制设为当前登录用户,防止冒名发布)。它运行在提交事务内、必须同步且不执行 I/O;抛出的异常会被捕获并当作拒绝处理(fail closed)。此外,objectTypes选项可以把评论等对象类记录放入独立的 object 车道,用objectAccess('read' | 'write')替代isReadonly进行门控,从而支持"只读画布但可评论"等权限组合。
6.4 多房间应用
在单个应用中管理多个协作文档:
class RoomManager { private rooms = new Map<string, TLSyncClient>() joinRoom(roomId: string): TLSyncClient { if (this.rooms.has(roomId)) { return this.rooms.get(roomId)! } const store = createTLStore({ schema: mySchema }) const socket = new ClientWebSocketAdapter(`ws://localhost:3000/rooms/${roomId}`) const syncClient = new TLSyncClient({ store, socket, roomId }) this.rooms.set(roomId, syncClient) syncClient.connect() return syncClient } leaveRoom(roomId: string): void { const client = this.rooms.get(roomId) if (client) { client.disconnect() this.rooms.delete(roomId) } } } const roomManager = new RoomManager() const drawingRoom = roomManager.joinRoom('drawing-123') const presentationRoom = roomManager.joinRoom('slides-456')6.5 边缘计算与 Cloudflare Workers
Sync-core 对边缘计算平台支持良好:
// Cloudflare Worker 示例 export default { async fetch(request: Request, env: Env): Promise<Response> { if (request.headers.get('Upgrade') !== 'websocket') { return new Response('Expected websocket', { status: 426 }) } const { 0: client, 1: server } = new WebSocketPair() const roomId = new URL(request.url).pathname.split('/').pop() const room = this.getOrCreateRoom(roomId, env) room.handleSocketConnect(server, { sessionId: crypto.randomUUID(), // 从请求头或认证中提取用户信息 }) return new Response(null, { status: 101, webSocket: client, }) }, }Sync-core 的轻量特性使其适合 serverless 与边缘环境。仓库中与之配套的现成实现包括:
- sync-worker:基于 Cloudflare Durable Object 的生产级同步服务,其中的
DurableObjectSqliteSyncWrapper支持在 Durable Object 休眠(hibernation)后恢复房间状态; - sync-cloudflare 模板:可直接运行的 Cloudflare 同步示例;
- simple-server-example 与 socketio-server-example:Node.js 场景的参考实现。
提示:部署到边缘环境时,需要权衡地理分布(更低延迟)与一致性(可能出现 split-brain 场景)之间的取舍。
7. 版本、许可与生态
@tldraw/sync-core当前版本为 5.4.0(见 package.json),依赖@tldraw/state、@tldraw/store、@tldraw/tlschema、@tldraw/utils、nanoevents与ws,Node.js 运行环境要求>= 22.12.0,React 为 peer dependency(^18.2.0 || ^19.2.1)。它在 tldraw SDK 中归属 Sync 产品线(stableId: tldraw:sync)。
其配套测试非常完整,可以作为深入理解协议行为的入口:
- TLSyncClient.test.ts 与 TLSyncClientRebase.test.ts:客户端同步与 rebase 行为;
- TLSyncRoom.test.ts 与 TLSocketRoom.test.ts:房间与会话管理;
- protocol.test.ts、diff.test.ts:协议消息与 diff 语义;
- syncFuzz.test.ts 与 upgradeDowngrade.test.ts:随机模糊测试与 schema 升降级测试;
- storageContractSuite.ts:各存储实现的契约一致性测试。
8. 小结
@tldraw/sync-core为 tldraw 应用提供了完整的实时协作基础设施。围绕"服务器权威 + 乐观更新 + 网络差异 + push/pull/rebase"这条主线,你可以快速搭建从单房间画布到多房间、多租户、边缘部署的协作系统。需要重点掌握的能力包括:理解TLSyncClient/TLSocketRoom的职责划分、用ClientWebSocketAdapter处理断线重连与指数退避、通过 presence 实现实时光标与选区、利用 schema 迁移与协议版本协商保持客户端共存,以及借助协议调试工具与性能监控定位同步问题。深入阅读 DOCS.md 与本文引用的源码文件,即可在实战中灵活定制自己的同步方案。
【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考