tldraw 实时协作引擎解析:掌握 @tldraw/sync-core 的同步协议与实战集成
tldraw 实时协作引擎解析掌握 tldraw/sync-core 的同步协议与实战集成【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldrawtldraw/sync-core是 tldraw SDK 中负责实时协作与状态同步的底层引擎它以客户端-服务器 网络 diff模型为无限画布应用提供多人同时编辑能力。本文以 packages/sync-core/DOCS.md 为主线结合仓库内 TLSyncClient.ts、TLSyncRoom.ts、protocol.ts、diff.ts 等源码带你从架构原理到断线重连、冲突消解、持久化与边缘部署的完整实战。读完本文你将掌握sync-core 的客户端/服务端核心类与配置参数、同步协议的完整消息类型与 network diff 格式、连接状态机的监控与调试方法以及如何把实时协作能力接入 React、自建服务器与 Cloudflare Workers。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 中可见乐观更新随后被发送到服务器进行校验并分发到其他客户端。Tipsync-core 设计上可与任意 WebSocket 实现配合无论是简单的 Node.js 服务器还是边缘计算平台都能接入。从包配置看tldraw/sync-core当前版本为 5.4.0见 package.json运行环境要求 Node.js 22.12.0依赖tldraw/state、tldraw/store、tldraw/tlschema、tldraw/utils以及ws与nanoevents。所有核心类TLSyncClient、TLSyncRoom、TLSocketRoom、ClientWebSocketAdapter、diffRecord等均从 src/index.ts 统一导出。2. 核心概念2.1 客户端-服务器架构sync-core 采用服务器权威server-authoritative模型服务器是所有改动的唯一事实来源single source of truth。这保证了数据一致性同时保留了流畅的本地交互乐观更新Optimistic Updates本地改动立即生效UI 响应无延迟服务器校验Server Validation服务器校验你的改动并可能对其进行修正或拒绝冲突消解Conflict Resolution一旦发生冲突以服务器版本为准。在源码层面客户端TLSyncClient内部维护三份关键状态来支撑这个模型见 TLSyncClient.tspendingPushRequests已发出但尚未被服务器确认的 push 请求队列unsentChanges尚未发送的本地 diff 与 presence 缓存speculativeChanges本地推测性未确认改动。源码注释明确指出如果取出该 diff、求反并应用到 storestore 就会精确回到我们已知的服务器最新状态——这正是后续 rebase变基操作的基础。2.2 Rooms 与 Sessionsroom房间代表一个多人协作的文档空间// 服务端房间管理 const room new TLSyncRoom({ store: serverStore, roomId: drawing-room-123, }) // 每个连接的客户端在房间内创建一个 session room.handleSocketConnect(clientSocket, sessionMeta)每个客户端连接都会在房间内创建一个session会话用于跟踪该用户的连接状态、权限与 presence 信息。注意仓库中实际面向服务端的是更高层的TLSocketRoom封装了TLSyncRoom见 TLSocketRoom.ts它额外处理了消息分块重组、客户端超时清理、会话快照等职责。创建房间时可以指定storage同步存储默认InMemorySyncStorage也可用SQLiteSyncStorage等持久化实现与initialSnapshot二选一同时提供会抛出异常schemastore schema默认createTLSchema()clientTimeout客户端多久未通信即被断开log可选的warn/error日志器onSessionRemoved客户端断开会话移除时的回调onBeforeSendMessage/onAfterReceiveMessage消息收发钩子onCommittedChanges客户端 push 提交后回调可用于把文档改动投影到外部存储objectTypes/authorizeRecord对象存储通道与按类型的写入授权器详见后文对象存储通道。房间会自动处理会话生命周期、变更广播与失联客户端清理。TLSocketRoom还有一个值得注意的细节getNumActiveSessions()返回的活跃会话数与已连接 socket 数并不等价——socket 关闭后会话还会保留片刻以平滑网络抖动。2.3 网络 Diff 与同步sync-core 不传输整份文档状态而是使用网络 diffnetwork diff——一种只描述到底改了什么的紧凑表示// 更新某个 shape 位置的网络 diff 示例 const diff { shape:abc123: [ RecordOpType.Patch, { x: [ValueOpType.Put, 150], y: [ValueOpType.Put, 200], }, ], }这种设计把带宽消耗降到最低即使面对大型文档也能高效同步。从 diff.ts 源码可以看清 diff 的完整结构RecordOpType记录级操作Put整体写入新记录、Patch对现有记录打补丁、Remove删除记录ValueOpType字段级操作Put整体替换值、Delete删除属性、Append向数组或字符串末尾追加[type, value, offset]三元组、Patch对嵌套对象递归打补丁。NetworkDiff是一个以记录 id 为键、以 RecordOp 为值的对象。getNetworkDiff()负责把 store 内部可逆的RecordsDiffadded/updated/removed三段式转换为不可逆但极省流量的NetworkDiff从而只为传输而生。diffRecord()则针对 tldraw 记录做了专门优化将props与meta视为嵌套对象递归求差避免把整个 props 子树当普通值整体替换。值得一提的是数组与字符串的优化策略diff.ts当数组等长时只 diff 发生变化的索引超过 1/5 元素变化则退化为整体Put当数组长度不同且公共部分未变时使用Append操作字符串若为纯追加则使用Append携带追加片段——这对文本框持续输入的场景能大幅压缩流量。3. 基本用法3.1 搭建同步客户端要为 tldraw 应用启用同步需要三样东西一个 store、一个 WebSocket 适配器、一个 sync clientimport { 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) // 创建 sync client const syncClient new TLSyncClient({ store, socket, roomId: my-drawing-room, }) // 开始同步 syncClient.connect()连接建立后任何对 store 的改动都会自动与同一房间内的其他客户端同步。对照真实源码TLSyncClient.tsTLSyncClient的完整构造参数包括参数类型说明storeStoreR要同步的本地 tldraw store必填socketTLPersistentClientSocket与服务器通信的持久化 socket 适配器必填presenceSignalR \| null当前用户的 presence 数据响应式信号必填presenceModeSignalTLPresenceModepresence 共享模式solo不共享或full完全共享默认fullonLoad回调首次收到服务器消息、初始同步完成时触发onSyncError回调同步失败时触发携带错误 reasononCustomMessageReceived回调接收自定义应用消息onAfterConnect回调成功连入房间后触发参数含isReadonly与objectAccessdidCancel函数可选返回 true 时客户端自动关闭并清理3.2 监控连接状态sync client 通过响应式信号暴露状态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 始终反映真实连接状态。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 } })Tip务必优雅处理不兼容错误——它意味着客户端与服务器之间存在版本不匹配。3.4 深入了解同步协议与连接生命周期同步客户端与服务器之间传递的所有消息在 protocol.ts 中统一定义这是理解整个同步过程的关键客户端 → 服务器TLSocketClientSentEventconnect建立连接后的第一条消息携带connectRequestId、客户端序列化后的schema、protocolVersion与lastServerClock客户端已知的服务器时钟用于断线续传push推送文档 diff 与可选 presence携带clientClock每次 push 递增的计数器用于与服务器响应配对ping心跳探测服务器以pong应答。服务器 → 客户端TLSocketServerSentEventconnect握手成功携带hydrationTypewipe_all或wipe_presence、serverClock、isReadonly、协议版本与服务器 schemadata包含一个或多个patch他人改动与push_result对你 push 的应答commit提交、discard丢弃、或rebaseWithDiff携带 rebase 后的 diffpongping 应答custom自定义应用消息incompatibility_error协议版本不匹配源码注释标注该消息为 legacy新实现改为用 WebSocket close code 表达。当前同步协议版本号为8TLSYNC_PROTOCOL_VERSION 8可通过getTlsyncProtocolVersion()获取握手时用于保证客户端与服务器兼容。此外服务器还可以用WebSocket close code4099TLSyncErrorCloseEventCode终止连接并附带原因预定义原因包括见 TLSyncClient.tsNOT_FOUND房间不存在、FORBIDDEN无权限、NOT_AUTHENTICATED未认证、UNKNOWN_ERROR、CLIENT_TOO_OLD/SERVER_TOO_OLD协议版本过旧、INVALID_RECORD非法记录、RATE_LIMITED超出限流、ROOM_FULL房间已满。4. 进阶主题4.1 服务端房间管理服务端通过房间协调多个客户端会话import { TLSyncRoom } from tldraw/sync-core class CollaborationServer { private rooms new Mapstring, 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作为服务端入口它的handleSocketConnect接收{ sessionId, socket, isReadonly, objectAccess, meta }结构见 TLSocketRoom.ts其中sessionId通常取自浏览器 tab 的稳定标识以便断线后无缝恢复会话meta可用于携带userId、userName等业务信息。4.2 自定义 WebSocket 适配器sync-core 提供了开箱即用的ClientWebSocketAdapter你也可以针对特定需求实现自定义适配器import { TLPersistentClientSocket } from tldraw/sync-core class CustomSocketAdapter implements TLPersistentClientSocket { status atomTLPersistentClientSocketStatus(offline) sendMessage(message: any): void { // 你的自定义发送逻辑 this.customWebSocket.send(JSON.stringify(message)) } onReceiveMessage createNanoEventsany() onStatusChange createNanoEventsTLPersistentClientSocketStatus() restart(): void { // 你的重连逻辑 } }TLPersistentClientSocket接口TLSyncClient.ts要求实现四个成员connectionStatusonline | offline | error、sendMessage、onReceiveMessage订阅式返回清理函数、onStatusChange、restart与close。自定义适配器让你能接入现有 WebSocket 库或添加自定义认证与错误处理。4.3 内置适配器的断线重连机制仓库自带的ClientWebSocketAdapterClientWebSocketAdapter.ts值得深入理解。它有两个显著特点其一构造函数接收的是 URI 工厂函数而非固定字符串const adapter new ClientWebSocketAdapter(() ws://localhost:3000/sync) // 也支持异步每次连接尝试都会重新调用便于动态生成携带认证 token 的 URI const adapter new ClientWebSocketAdapter(async () { const token await fetchAuthToken() return wss://example.com/sync?token${token} })其二内置ReconnectManager智能重连采用指数退避exponential backoff策略前台活跃 tab 的延迟范围ACTIVE_MIN_DELAY 500ms至ACTIVE_MAX_DELAY 2000ms后台隐藏 tab 的延迟范围INACTIVE_MIN_DELAY 1000ms至INACTIVE_MAX_DELAY 5 分钟降低电量消耗与服务器压力每次失败重试延迟乘以DELAY_EXPONENT 1.5单次连接尝试超时ATTEMPT_TIMEOUT 1000ms防止连接卡在 CONNECTING 状态自动响应window的online/offline事件、visibilitychange以及navigator.connection变化。需要特别注意的是源码注释明确说明ClientWebSocketAdapter.ts浏览器 WebSocket API 不暴露协议级 ping/pong连接失效检测必须由上层实现。这正是TLSyncClient内部每PING_INTERVAL 5000ms发送一次ping、并以PONG_TIMEOUT 10000ms判定连接是否僵死的原因同时它会每 10 秒做一次健康检查若在2 × PING_INTERVAL内既无服务器交互又存在超时未应答的 ping就重置连接重新握手。4.4 冲突消解策略当多个用户同时编辑时可能产生冲突。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) })服务器按收到改动的顺序应用变更对冲突属性以后收到的改动为准。其底层算法在客户端表现为类 git 的 push / pull / rebase 模型源码注释见 TLSyncClient.ts本地改动作为乐观更新立即生效并被记为speculativeChanges收到服务器消息后rebase()流程会先撤销推测性改动 → 应用服务器 patch → 再把本地改动重新应用到服务器状态之上TLSyncClient.ts并据此生成新的 push 请求。若服务器返回discard则本地改动被丢弃若返回rebaseWithDiff则按服务器提供的 diff 重新落盘。applyNetworkDiff使用值级相等性isEqual判断避免对无实际变化的记录触发 store 监听器。仓库中的 TLSyncClientRebase.test.ts 与 syncFuzz.test.ts 对该流程做了大量验证后者通过随机操作序列对客户端-服务器同步进行模糊测试确保各种冲突与乱序场景下状态最终一致。4.5 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 的推送同样走 diff 优化getPresenceOpTLSyncClient.ts在已有 presence 时使用RecordOpType.PatchdiffRecord只发送变化字段首次则使用Put整体发送。presence 的推送频率还受presenceMode控制solo模式下网络同步帧率降为 1 FPS协作模式为 30 FPSSOLO_MODE_FPS/COLLABORATIVE_MODE_FPS见 TLSyncClient.ts。断线重连时客户端会清空所有 peer presence 数据resetConnection中移除全部 presence 记录因为服务器会在每次 connect 时全量下发。4.6 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 会尽可能尝试自动迁移迁移失败时发送不兼容错误对未知记录类型允许优雅降级。Tip尽量设计向后兼容的 schema 变更避免强制所有用户同时升级。从握手实现看sendConnectMessageTLSyncClient.ts客户端发送connect消息时会把store.schema.serialize()与protocolVersion一并提交服务器比对版本后决定接受、迁移或拒绝。仓库还内置了 upgradeDowngrade.test.ts专门验证 schema 升级与降级场景下的握手行为。4.7 对象存储通道Object Store Lanetldraw/sync-core5.x 引入了一个值得关注的机制对象存储通道。在TLSocketRoom中可以通过objectTypes指定一类记录如评论 comments走独立的对象通道而非文档通道见 TLSocketRoom.ts。这类记录的写入权限由会话级objectAccessread | write控制独立于文档通道的isReadonly——于是可以实现允许评论但不允许编辑文档或相反的组合权限protocol.ts。配合authorizeRecord按类型授权器服务端还可以在 create 时强制改写记录例如把评论的authorId强制为登录用户。5. 调试指南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这能精确揭示连接建立期间的消息序列以及可能出现的错误。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 消息从服务器返回。如果使用内置适配器还可以在浏览器控制台设置window.__tldraw_socket_debug true开启适配器自身的调试日志见 ClientWebSocketAdapter.ts。5.3 网络 Diff 检视理解正在同步的到底是什么变化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的实现位于 diff.ts它对props与meta做嵌套递归 diff因此你会看到props: [patch, { color: [put, blue] }]这样的层级结构。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)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) } })在服务端非致命问题通常通过TLSocketRoom的log选项warn/error输出致命错误则通过 close code4099关闭连接并携带TLSyncErrorCloseEventReason中的具体原因客户端可在onSyncError(reason)回调中按 reason 分支处理例如NOT_FOUND提示房间不存在、FORBIDDEN提示无权限、CLIENT_TOO_OLD提示升级客户端。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)Tip消息数过高或延迟过大通常意味着网络问题或低效的变更模式。可考虑对高频变化做批处理或优化你的 shape 更新逻辑。例如连续移动 shape 时TLSyncClient会以 30 FPS 的调度器节流发送 push 请求频繁的中间帧会被合并成一份 diff。6. 集成实战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] useStatestring(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 组件会在连接状态或同步数据变化时自动更新。注意react()返回的清理函数应作为useEffect的返回值确保组件卸载时取消订阅。6.2 自定义持久化将房间状态接入你已有的数据库或存储系统import { TLSyncRoom } from tldraw/sync-core class DatabasePersistenceAdapter { constructor( private db: Database, private roomId: string ) {} async loadRoom(): PromiseSerializedStore { 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): Promisevoid { 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内存存储含DEFAULT_INITIAL_SNAPSHOT、SQLiteSyncStorageSQLite 存储配合NodeSqliteWrapper或 Cloudflare 的DurableObjectSqliteSyncWrapper它们都实现了统一的TLSyncStorage接口见 src/index.ts并配有对应的 SQLiteSyncStorage.test.ts 与 InMemorySyncStorage.test.ts 验证。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), })在真实实现中推荐利用ClientWebSocketAdapter的 URI 工厂特性把 token 放进连接地址如wss://example.com/sync?token${token}每次重连都会重新执行工厂函数以获取新 token服务端在handleSocketConnect时通过isReadonly与objectAccess完成读写权限的细分控制。6.4 多房间应用在单个应用中管理多个协作文档class RoomManager { private rooms new Mapstring, 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 Workerssync-core 的轻量设计使其非常适合边缘计算平台// Cloudflare Worker 示例 export default { async fetch(request: Request, env: Env): PromiseResponse { 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 与边缘环境——这类环境往往难以维持传统长连接。Tip部署到边缘环境时需要权衡地理分布带来的低延迟与一致性潜在的脑裂 split-brain 场景之间的取舍。仓库中的 ServerSocketAdapter.ts 与TLSocketRoom.handleSocketMessage支持事件驱动式消息投递例如 Bun.serve 或 Cloudflare 的 WebSocket hibernation 模式中无法直接给 socket 挂监听器的场景配合getSessionSnapshot/handleSocketResume的会话快照机制TLSocketRoom.ts房间可以跨 Durable Object 休眠-唤醒周期恢复会话状态这正是 tldraw 官方 dotcom 与 sync-worker见 apps/dotcom/sync-worker生产环境的部署形态。7. 小结tldraw/sync-core为 tldraw 应用提供了完整的实时协作底座其核心设计可以归纳为四点服务器权威模型服务器是唯一事实来源配合乐观更新保证交互流畅紧凑的网络 diff记录级Put/Patch/Remove与字段级Put/Delete/Append/Patch组合让带宽消耗最小化类 git 的 push/pull/rebaseTLSyncClient通过撤销推测性改动、应用服务器 patch、再重放本地改动的方式自动消解冲突可插拔的适配器体系从浏览器ClientWebSocketAdapter到自定义 socket、从InMemorySyncStorage到 SQLite 持久化再到 Cloudflare Durable Object 的边缘部署各层均可替换。调试时善用status信号、onReceiveMessage、diffRecord与TLSyncErrorCloseEventReason组合定位问题生产环境中务必实现基于 ping 的失效检测或依赖TLSyncClient内置的 5 秒 ping / 10 秒超时机制并为不兼容错误与权限拒绝设计优雅的降级路径。【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考