跳到主要内容

客户端内部

客户端内部

客户端是运行时的浏览器端。本页介绍它内部发生的事情——本地副本、它可以自行回答哪些读取、写入所走的命令,以及多个标签页如何共享同一份拷贝。

本地副本

每个客户端在 IndexedDB 中维护一个按策略过滤的 PGlite 数据库,先依据租户自己的迁移完成置备,再由该主体可读的每个集合的分页快照填充。存储按租户与环境作键,因此同一浏览器登录两个工作区时绝不会把两者指向同一批行。编译为 sync: false 的集合是刻意的例外——session、account、verification 以及内部的 bolt_* 簿记表绝不复制,无论主体在别处被允许读取什么。

副本能回答什么

只有当副本能给出完全一致的答案时,读取才在本地完成。本地读取器直接引用服务端自己的 where 与 order by 编译器,而不是另写一份;凡是超出「集合 + 过滤 + 排序 + 限制」的查询它一律拒绝——关联展开、全文搜索、聚合与历史都交给服务器。无法识别的键同样被拒绝,因此日后新增的查询选项不会被悄悄当成另一个问题来回答。

写入路径

浏览器代码用 client.db.<collection>.mutate(values) 声明根图的期望状态。生成的输入类型会精确描述每一个可写的嵌套关联。服务器通过一条规范变更管线执行策略、审批、钩子、根与关联协调、历史、事件与审计;根记录与每一个已包含关联会原子协调。mutate 返回 Promise<void>,而集合的数值 pending 会计数仍在进行的写入。成功完成后,受影响的实时查询会被失效;应用代码自身从不调用 invalidate、refetch 或 revalidate。

  user action
        │
        ▼
  client.db.<collection>.mutate(values)
        │  collection.pending counts concurrent writes
        ▼
  server: policies → approvals → hooks → atomic graph reconciliation → audit
        │
   ┌────┴─────┐
   ▼          ▼
  committed  refused
   │          │
   │          ▼
   │     refuse() reason surfaces, nothing was written
   ▼
  drop this collection's cached answers, re-run the live
  queries reading it ──► Promise<void> resolves; pending decrements

一个副本,多个标签页

PGlite 通过 Web Locks API 在标签页之间选举领导者:一个标签页持有数据库,其余标签页把查询代理给它,因此所有标签页读到同一批行。只有领导者打开变更流,也只有它执行拉取,于是每个浏览器只有一条连接,而不是每个标签页一条。它应用完一批变更后,会通过 BroadcastChannel 广播集合名,其余标签页据此让缓存失效——这里不能用 Postgres 通道,因为 PGlite 的 worker 客户端共享同一个会话,一个客户端的 UNLISTEN 会禁用其它所有监听器。领导权发生转移时——也就是领导标签页被关闭——变更流会交给继承它的那个标签页。

只返回完成,不返回记录

mutate 从不返回记录。只写或行过滤策略可能允许调用方更改它无权读取的行,因此成功完成只能承诺变更已提交。实时查询拥有该调用方被授权可见的当前数据。

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