tldraw.com 同步 Worker 详解:房间管理、快照路由与本地开发配置(sync-worker)
【免费下载链接】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 官方站点 tldraw.com 的后端核心是一个部署在 Cloudflare Workers 上的 Worker 应用,位于仓库的apps/dotcom/sync-worker目录。本文围绕该目录的 README 展开:先讲清这个 Worker 承担的房间创建/加入、只读房间、快照与历史等职责及其路由结构,再结合 wrangler.toml 说明它的 Durable Objects、R2 与 KV 绑定,最后完整落地 README 给出的本地开发流程——如何配置 Supabase 持久化(.dev.vars)并启动 workerd 本地调试,读者读完后可以直接在本地跑起这个多房间同步服务并理解每个配置项的来龙去脉。
一、sync-worker 是什么:tldraw.com 的房间管理服务
README 对它的定位一句话概括:
The dotcom-worker is used for room management on tldraw.com (e.g. creating & joining rooms, readonly rooms, snapshots).
即:它是 tldraw.com 上所有"房间级"请求的入口。在 package.json 中,这个包的名称是@tldraw/dotcom-worker,描述仍写作 "tldraw infinite canvas SDK (merge server)"——这是它在 tldraw 演进早期的名字,如今目录与职责已经聚焦为同步/房间管理后端。
从源码结构看,它构建在@tldraw/sync-core、@tldraw/sync-collaboration、@tldraw/store等 monorepo 内部包之上(见 package.json 的dependencies),并引入了@rocicorp/zero、pg、kysely、@supabase/supabase-js等运行时依赖,说明它同时处理 Zero 同步协议路由与 Postgres/Supabase 数据访问。
二、核心路由:创建房间、加入房间、只读房间与快照
Worker 的入口是 src/worker.ts,它用itty-router注册了全部 HTTP 路由,并通过@tldraw/worker-shared提供的createRouter、blockUnknownOrigins、CORS preflight 等公共能力做安全边界。与 README 所述职责直接对应的路由如下:
| 路由 | 处理函数 | 职责 |
|---|---|---|
POST /snapshots | createRoomSnapshot | 创建房间快照(createRoomSnapshot.ts) |
GET /snapshot/:roomId | getRoomSnapshot | 读取指定房间的快照 |
GET /r/:roomId(ROOM_PREFIX) | joinExistingRoom | 以读写模式加入已有房间 |
GET /readonly/:roomId(READ_ONLY_PREFIX) | joinExistingRoom | 以只读模式加入房间 |
GET /legacy/:roomId(READ_ONLY_LEGACY_PREFIX) | joinExistingRoom | 兼容旧的只读房间链接 |
GET /r/:roomId/history与/f/:roomId/history | getRoomHistory | 房间历史列表(含按时间戳取历史快照) |
GET /readonly-slug/:roomId | getReadonlySlug | 生成/解析只读 slug(配合 KV 双向映射) |
POST /app/tldr、POST /app/:userId/init | createFiles、initUser | 新版(file 体系)的文件创建与用户初始化 |
其中ROOM_OPEN_MODE(读写 / 只读 / 只读 legacy)与 slug 前缀常量ROOM_PREFIX、READ_ONLY_PREFIX、READ_ONLY_LEGACY_PREFIX、FILE_PREFIX都定义在共享包 packages/dotcom-shared 中,由worker.ts直接引入使用——这正是 README 所说"readonly rooms"能力的落点:同一个joinExistingRoom处理函数,靠路由前缀决定打开模式。
快照的读取路径还依赖 Supabase:getRoomSnapshot.ts 与 getSocialPreview.ts 都会先调用createSupabaseClient(env)读取房间数据(详见下节),这也是为什么本地开发必须正确配置SUPABASE_URL/SUPABASE_KEY才能看到完整行为。
三、Cloudflare 资源绑定:wrangler.toml 逐项解读
wrangler.toml 是这个 Worker 最完整的"架构文档"。关键部分:
基本项
main = "src/worker.ts",compatibility_date = "2026-03-24",开启nodejs_compat;[dev]段固定本地开发端口port = 8787、监听0.0.0.0——本地客户端 dev server 正是按这个端口对接/api请求的;[assets]把assets/目录(内含welcome-snapshot.json默认欢迎画板)以ASSETS绑定注入 Worker,run_worker_first = true保证目录本身不对外暴露,仅在播种欢迎画板时通过绑定读取。
Durable Objects:所有环境(dev / preview / staging / production)配置完全一致,绑定三个类:
[durable_objects] bindings = [ { name = "TLDR_DOC", class_name = "TLFileDurableObject" }, { name = "TL_FILE_EFFECTS", class_name = "TLFileEffectProcessor" }, { name = "TL_LOGGER", class_name = "TLLoggerDurableObject" }, ]对应的类实现就在本目录内:TLFileDurableObject.ts(文件/房间的持久状态,含 Zero 同步与 outbox)、TLFileEffectProcessor.ts(异步副作用处理)、TLLoggerDurableObject.ts。worker.ts 顶部还保留了TLDrawDurableObject、TLPostgresReplicator等 no-op stub 导出——这是 wrangler.toml 迁移历史的强制要求:staging/prod 的已应用迁移引用了这些旧类名,删除导出会直接破坏部署(文件内注释明确指向 issue #8124)。[[migrations]]从 v1 到 v12 的追加式历史完整记录了各 DO 类的创建/删除过程,注释还特别说明 preview 环境为何要跳过部分迁移(全新部署不允许删除从未创建过的类,会触发 CF 错误码 10074)。
R2 存储桶(按环境隔离,preview/staging 共享*-preview桶,production 用正式桶):
ROOMS:房间快照本体(rooms/rooms-preview);ROOMS_HISTORY_EPHEMERAL:房间历史;ROOM_SNAPSHOTS:快照桶;UPLOADS:用户上传文件;THUMBNAILS:画板缩略图/OG 图缓存,注释强调该桶不能配置过期生命周期规则;MCP_DATA_BUCKET:MCP 工具截图,因 key 携带内容版本会不断累积,反而需要过期规则。
KV 命名空间:SLUG_TO_READONLY_SLUG与READONLY_SLUG_TO_SLUG(只读房间 slug 双向映射,支撑上文/readonly-slug/:roomId路由)、SNAPSHOT_SLUG_TO_PARENT_SLUG(快照 slug 到父文件 slug 的映射)、FEATURE_FLAGS(功能开关,dev/preview 共享一个命名空间,staging/production 各自独立)。
限流与队列:四个ratelimit绑定(通用RATE_LIMITER600 次/60s,以及 MCP 截图的按账号 10/60s、按画板 2/60s、全局 20/60s 三级预算);QUEUE消费者max_retries = 10,production 还配置了死信队列tldraw-multiplayer-dlq,与 worker.ts 中QUEUE_FINAL_ATTEMPT = 11的注释相互印证(Queues 投递 max_retries+1 次后才进 DLQ)。
路由:staging 绑定staging.tldraw.com/api/*,production 绑定www.tldraw.com/api/*,另各加一条/.well-known/oauth-protected-resource/*路由用于 MCP 服务的 RFC 9728 元数据发现,以及*-sync.tldraw.xyz自定义域名。
四、本地开发:为 --local 模式启用数据库持久化(README 核心实操)
这是 README 唯一给出的操作步骤,原样保留并展开如下。
问题背景:env.SUPABASE_KEY与env.SUPABASE_URL两个值平时存放在 Cloudflare Workers 控制台(dashboard)中。但本地开发使用的是wrangler dev --local模式,它不会从 dashboard 读取这些变量。因此需要手动在工作目录创建.dev.vars文件提供这两个值——wrangler 的 local 模式会自动读取该文件并注入为环境变量。
README 原文中把该文件的位置写作
merge-server,这是历史目录名;当前仓库中对应目录为apps/dotcom/sync-worker。
操作步骤
在 apps/dotcom/sync-worker 目录下创建
.dev.vars文件,写入:SUPABASE_URL=<url> SUPABASE_KEY=<key>值来源于团队 Supabase 项目的 API 设置页(README 中给出的是当时项目的 dashboard 入口,具体以团队内部凭据为准;此文件属于本地机密,不应提交到版本库)。
启动本地 worker。package.json 的
devscript 已封装好:"dev": "yarn run -T tsx ../../../internal/scripts/workers/dev.ts --persist-to .wrangler/state-dev --var ASSET_UPLOAD_ORIGIN:http://localhost:8788 --var USER_CONTENT_URL:http://localhost:8789"即在仓库根目录执行
yarn workspace @tldraw/dotcom-worker dev(或进入该目录yarn dev)。
dev 脚本到底做了什么:internal/scripts/workers/dev.ts 是一个包装器(MiniflareMonitor类),它实际启动的是:
wrangler dev --env dev --test-scheduled --log-level info --var IS_LOCAL:true [附加参数]同时它会:
- 启动前探测
wrangler.toml中[dev].port(8787)是否被占用,若被占用则明确报错退出并提示用lsof -nP -iTCP:8787 -sTCP:LISTEN定位占用进程——因为 wrangler 默认会静默换端口,导致客户端 dev server 的/api代理打错地方; - 用一个进程锁保证多个 workerd 实例串行启动(早期 workerd 并发启动会互相冲突);
- 监控输出,检测到
Segmentation fault自动重启; - 附带一个
SizeReporter,实时打印 worker 产物的大致 minify 体积(check-bundle-sizescript 则把体积上限固定在 1,550,000 字节)。
为什么这两个变量重要——源码印证:src/utils/createSupabaseClient.ts 中,client 仅在env.SUPABASE_URL && env.SUPABASE_KEY都存在时才会创建:
export function createSupabaseClient(env: Environment) { return env.SUPABASE_URL && env.SUPABASE_KEY ? createClient(env.SUPABASE_URL, env.SUPABASE_KEY, { auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false }, }) : undefined }若缺失则返回undefined,依赖它的房间快照/社交预览等路由会退化为noSupabaseSorry()错误响应。所以"配了.dev.vars才能看到完整行为"不是文档惯例,而是代码路径决定的。该文件还解释了一个容易被忽视的细节:autoRefreshToken: false是刻意关闭的——Supabase JS 客户端默认会 arm 一个 30 秒setInterval,而 Durable Object 在存在任何定时器时无法休眠,会导致 TLFileDurableObject 每次唤醒后持续计费。这个 client 从不用于用户登录(只用 anon key 读取 legacy 房间数据),因此没有任何理由保留自动刷新。createSupabaseClient.test.ts 对这一行为做了单测覆盖。
此外,env.dev.vars(wrangler.toml)已经预置了本地开发所需的其它变量:Postgres 连接串(指向本地 6543/6432 端口)、TLDRAW_ENV = "development"、MULTIPLAYER_SERVER = "http://localhost:3000"、MCP 相关开关等;--persist-to .wrangler/state-dev让 Durable Objects 的本地状态落盘到.wrangler/state-dev,配合cleanscript(rm -rf .wrangler/state .wrangler/state-*)可随时清掉本地 DO 状态重新开始。
五、测试与验证
- 单测:目录内按
vitest组织,yarn test(watch)/test-ci(单次运行)即可跑;README 主题相关的行为有大量佐证用例,如 getRoomSnapshot、snapshotUtils.test.ts、getReadonlySlug、worker.exports.test.ts 等; - 路由前缀、打开模式(
ROOM_OPEN_MODE)等常量与 packages/dotcom-shared 共享,保证客户端与 Worker 两端一致。
六、许可与贡献
README 同时声明:本目录代码 Copyright (c) 2024-present tldraw Inc.,遵循 tldraw license;tldraw 名称与 logo 属商标,使用规范见 TRADEMARKS.md。发现问题可通过提交 issue 反馈。
小结
- sync-worker 是 tldraw.com 的房间管理后端:创建/加入房间、只读房间、快照与历史路由全部集中在 src/worker.ts 的 itty-router 中,持久状态由三个 Durable Objects 承载;
- wrangler.toml 以"追加式迁移 + 每环境独立 R2/KV/限流/路由"的方式管理 dev、preview、staging、production 四套部署;
- 本地开发的关键一步是为
wrangler dev --local补上.dev.vars中的SUPABASE_URL/SUPABASE_KEY,然后用仓库封装的 dev script 启动 workerd(端口 8787,状态持久化到.wrangler/state-dev)——这一流程在 createSupabaseClient.ts 与 dev.ts 中均有源码级印证。
【免费下载链接】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),仅供参考