跳到主要内容

客户端内部

客户端内部

客户端是运行时的浏览器端:一个 Machine 归约器加上驱动。本页介绍它内部如何工作——实时查询如何通过同步 HTTP/SSE 会话保持最新、写入如何送达,以及为何服务器仍是唯一权威。浏览器中没有 PGlite 副本,也没有 IndexedDB 租户数据库。Web Locks 为该浏览器配置文件选举一条 EventSource 的所有者。

Machine

浏览器只在内存中持有当前查询答案。machine.ts 是纯归约器:带版本的前缀、待处理写入与链路状态。两个驱动搬运字节——一条配置文件级 SSE 流入,序列化的 HTTP 控制与写入请求流出。已挂载的实时前缀由 sync.connect 作答,再由 apply 帧保持最新。只有保留的版本等于更新的 fromVersion 时才应用前缀更新;否则链路重启。校验失败得到的是 failed,绝不是一份自信的过期答案。浏览器里没有第二份真相来源。

实时查询能回答什么

带连续 limit 的 findMany 与 findFirst 是实时的——在宿主登记一个前缀,此后由服务器推送。count、findGrouped、带 after 游标的分页以及语义搜索都是一次性的:通过传输回答一次,从不登记为实时。每一份答案都以服务器为权威:权限与不变量始终由服务器裁定。浏览器不编译本地 SQL,也不维护按策略过滤的副本。

写入路径

浏览器代码用 client.collection.<collection>.create(input)、.update(id, input) 或 .delete(id) 提交声明的输入。Machine 将其入队,并带上连接头作为一条 collections.write 命令推出。持久性是 memory——本标签页的队列——直到权威结算。每次调用立即以乐观行兑现;返回的句柄暴露 settlement、status 与 wait。project() 把待处理写入叠在持有的答案上,因此界面在同一帧更新。结算结果是 accepted、rebased、rejected 或 quarantined;在结果到达之前,任何东西都不被宣称已保存。应用代码自身从不调用 invalidate、refetch 或 revalidate。

  user action
        │
        ▼
  client.collection.<collection>.create(input)
        │  Machine queues the write (durability: memory)
        │  pending += 1; project() overlays the optimistic row
        ▼
  collections.write ──► server write pipeline
        │
        ▼
  apply frame: patches + settlement
  (accepted | rebased | rejected | quarantined)
        │
        ▼
  Machine step(); pending decrements

每个浏览器配置文件一条 EventSource

每个标签页拥有自己的 Machine 和内存写入队列。同一浏览器配置文件里的标签页与工作区共享一条 EventSource——Web Locks 选出所有者标签页,BroadcastChannel 把帧分发出去。没有 IndexedDB 租户数据库。关闭所有者标签页会把流交给该配置文件里另一个仍打开的标签页;新的配置文件才会打开一条新的 EventSource。

先乐观叠层,再结算

写入立即返回乐观行,好让界面绘制,那不是一份持久的服务器记录。在结算到达之前,持久性一直是 memory。只写或行过滤策略可能允许调用方更改它无权读取的行,因此由服务器重新求值的实时查询拥有该调用方被授权可见的当前数据。

编写面是 客户端 ;传输是 同步引擎 。