同步引擎
同步引擎
Bolt 内置一个 原生同步引擎 ——不是插件、轮询循环,也不是第三方复制层。本页介绍它在底层如何工作:实时查询 Machine、guest 的 connect 与 extendPrefix、仅由宿主调用的 advance,以及承载它们的共享宿主核心。应用所编写的读写 API 参见 实时数据。
心智模型:你的查询是活的
把每一条实时查询想成服务器替你 持续作答 的一个悬而未决的问题——浏览器里没有第二份数据库。你的应用读取自己声明过的答案,通过普通集合命令写入,而任何人的提交一旦触及某个答案,引擎立刻将其纠正。
SERVER DATABASE (Postgres) THIS BROWSER
┌────────────────────────────────┐ ┌────────────────────────────┐
│ collections │ │ the sync Machine — │
│ policies · write contracts │ │ versioned prefixes this │
│ approvals · audit │ │ tab registered, plus an │
│ SyncChange capture │ │ in-memory write overlay │
└────────────────────────────────┘ └────────────────────────────┘
▲ ▲
│ client mutation pipeline │
└────────────────────────────────────────────┘
sync.connect registers prefixes; sync.extendPrefix grows them;
the host calls sync.advance after a commit and pumps apply
frames over the standing stream 两条流让答案保持真实。 下行: 每一次提交都以一帧 apply 推送——该提交产生的前缀增量、重置与写入结果一并送达。 上行: 你的写入以普通集合命令传输,由服务器提交或拒绝——服务器始终是权威。
在 Colony 上,这一机制的服务器端运行在你的租户运行时中,经由代理的 HTTP 传输;引擎本身是 Bolt 的关注点,在任何宿主上行为一致。
答案如何保持最新
服务器不会被定时轮询。每条已提交的变更会捕获 SyncChange 事实(链接与路由值,而不是仅按 id 唤醒)以及同一事务中的写入结果。没有 changelog 游标、outbox,也没有已持有 id 的摘要。一次提交会唤醒索引在变更集合下的计划,推送随即开始:
someone commits a mutation
│ SyncChange facts + write outcome in the SAME transaction
▼
COMMIT ──► the host awaits the lane; the guest sync.advance-s
affected prefixes against the commit's change list
│
▼
keyed delta ──► removeIds + put at a final index
— or a reset that re-registers
│
▼
event: apply { updates, resets, outcomes } one reducer event:
the Machine applies prefix deltas, settles the writes, the
UI updates — nothing re-runs by hand - 每一条增量都带着 版本栅栏 :仅当客户端保留的版本等于更新的 fromVersion,且 toVersion 是下一个整数时才会应用。否则连接会重启。
- 一次 重置 始终合法:浏览器丢弃该前缀并重新注册。计划键或权限不匹配、前缀损坏,或触及字节/行上限,都会重置——唤醒时不会改发一份完整答案补丁。
- 每份答案在发送前都会在 SQL 中按读取者的 策略范围 过滤,主体不可读的列则按直接读取所走的同一条规则做掩码——客户端永远收不到策略拒绝的行,而已持有的行在授权被撤销后,只能存活到下一次与策略相关的提交。
- 空闲的流不产生成本,因为没有任何东西依赖定时器:在有提交推过它之前,连接始终是安静的。
连接、重连与多标签页
FIRST CONNECT RECONNECT / RESET
───────────── ─────────────────
sync.connect registers every the same command re-registers
live prefix and returns rows prefixes the browser dropped
plus the plan the host files
│ │
sync.extendPrefix grows the a missed wake closes the
viewer's loaded prefix stream; there is no changelog
without bumping version to replay
│ │
the stream carries apply a frame that does not continue
frames: updates, resets, every fromVersion restarts
outcomes the link - sync.connect 携带每条查询的 key、input 与 requestedPrefix,以及已分离的键和仍在途的写入 id。它解析每个请求的前缀,返回行以及宿主将登记的计划。重连与重置使用同一条命令。
- sync.extendPrefix 在不提升版本的情况下增长查看者已加载的前缀。错过一次唤醒会关闭流;没有 changelog 可供重放。
- 每个浏览器配置文件共享一条 EventSource,跨标签页与工作区复用。宿主按 planKey 登记一份计划;多条连接可以作为查看者挂上,各自有不同的 loadedPrefix 长度。
- 重连会重新解析。每个作用域的注册表状态是 O(活跃计划);浏览器不持久保存任何内容。
bolt sync CLI ——在构建前生成 .norbital/ 类型与注册表的文件系统编译器同步。 工作区工作室预览同步 ——应用 DDL 并记录不可变发布产物的发布产物构建。参见 工作区工作室。
线上发生了什么
Colony 对外提供专用的同步路由。浏览器 POST /__bolt/sync/connect 与 /__bolt/sync/extend ;宿主把流保持打开在 /__bolt/sync/stream.
| 命令 | 用途 |
|---|---|
sync.connect | 浏览器(以及重连 / 重置)。解析每个请求的前缀,返回行以及宿主将登记的计划。 |
sync.extendPrefix | 浏览器,单调增长。在不提升版本的情况下,把行追加到查看者已加载的前缀之后。 |
sync.advance | 宿主在提交之后调用。按该提交的 SyncChange 列表重新求值已登记的前缀,返回更新或重置。浏览器从不调用它。 |
stream (SSE) | apply 帧:updates、resets 与写入 outcomes 在一个载荷里——一次提交,一次 reducer 事件。 |
未能接续每个已保留 fromVersion 的帧是协议错误,连接会重启。重置始终合法。完整协议记录在 公开的 Bolt 同步引擎文档 中。
共享宿主核心:注册表与泵
订阅状态活在宿主注册表里。SyncRegistry 与 SyncConnectionLane 是 bolt-protocol 中唯一可执行的共享核心,由云上的 Colony 与自托管的 bolt-server 直接使用。宿主只提供连接、作用域、guest 调用与断开映射适配器,不会各自重写登记、失效、排序或发送:
- 注册表 按 planKey 登记不透明的 guest 事实——版本、前缀键、保留字节、权限指纹、依赖与路由约束。多条连接可以作为查看者挂上,各自有不同的 loadedPrefix 长度。宿主从不解析主体、求值谓词或构造增量。
- 泵 在每个 (tenant, environment, releaseId) 的一条通道上串行化 connect、extendPrefix 与 committed。宿主等待通道接受;无法写入的接收端或 guest 失败会关闭不确定的连接。没有 changelog 可用来重放错过的唤醒。