返回资讯中心
外部精选
软件工程
#开发者实时流

Milieu 将账户数据存入浏览器 SQLite,实现离线可用与后台同步

来源标题:From the server to SQLite to reactive Svelte state

Milieu 采用浏览器端 SQLite 数据库保存用户数据副本,使应用在无网络时仍可读写,并与服务器后台同步。首次打开时先在服务器上启动,同时在后台下载账户的 SQLite 数据库,下载完成后无感切换到本地副本。冲突处理以服务器为准,浏览器保留已确认数据和待处理改动的预判结果。

Hacker News 最新joeldrapper2026/11/7 · 7 天前30 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

我们希望 Milieu 无论你在哪里使用,都能感觉快速、可靠。你应该能在飞机上安排一天的事务,在地铁里查一个联系人,或者在断网时继续工作。即使网络连接良好,也没有理由让每一次交互都等待服务器。

因此,Milieu 会在你的浏览器中、紧挨着应用的地方保留一份数据副本,你的大部分操作读写的都是这份副本。你所做的更改会先保存在浏览器中,然后在后台与服务器同步。

但当你在新的浏览器中打开 Milieu 时,它还没有你的任何数据,而你也不应该盯着加载屏幕等待账户下载完成。

于是,我们让你先从服务器开始使用。你可以立刻上手,同时一个后台 worker 会下载你账户的 SQLite 数据库。一旦该数据库追上了你所见过和修改过的所有内容,你的标签页就会切换到它。你不会察觉到这次切换。从那时起,你的浏览器会在后台持续同步,即使连接中断也能继续工作。

服务器始终拥有最终决定权。你的浏览器保留服务器已确认的内容,再加上它对未决更改将产生何种结果的最佳猜测。每次同步时,它会获取服务器的最新状态,并在其之上重新推算这些猜测。

SQLite 在两端运行:在服务器进程内部,以及在你的浏览器中通过 WebAssembly 在 worker 里运行。在那里,一个同步引擎完成本文描述的所有工作。有三样东西将两端连接起来:相同的 SQL 查询、以可发送和重放的操作形式保存的你的更改,以及跟踪你的浏览器已同步到哪里的游标。我们接下来会逐一讨论。

如果你了解 Replicache 或 Zero,这种猜测方式看起来会很熟悉:未决更改被重放在服务器状态之上,而不是用 CRDT 进行合并。与 PowerSync 一样,你的浏览器运行真正的 SQLite。不同之处在于你从哪里开始。你的标签页不会等待本地副本就绪才让你工作;它一开始是一个普通的服务器渲染应用,并在会话进行中迁移到你的浏览器上,而你的屏幕永远不会倒退到过去的时间点。

你的首次页面加载是普通的服务器渲染。Svelte 的异步渲染会等待页面的查询完成,SvelteKit 将查询结果连同 HTML 一起发送。你的浏览器从这些数据中接管页面,因此无需下载你的账户或再次请求那些结果即可使用。

在你的浏览器能够自行应答之前,读取通过 SvelteKit remote queries 进行,写入通过 remote commands 进行。每当有内容发生变化时,服务器会发送提示,因此在其他地方所做的编辑会显示在你的屏幕上。

与此同时,你的浏览器会获取自己的一份账户副本。

服务器上的一个后台 worker 会构建一个 SQLite 文件,其中包含你有权查看的行和我们同步的列,索引已经建好。该文件还会记录它的游标:一个精确标记它来自哪个服务器状态的书签。行和游标从同一个快照中读取,因此它们描述的是同一时刻。

我们会压缩并缓存这些文件。你的浏览器可以从一个较旧的文件开始,并拉取自其游标以来发生的所有变化,因此登录不需要等待一个全新构建的文件。

在你的浏览器中,数据库及其 outbox(等待发送的更改队列)位于 Origin Private File System(OPFS)中。SQLite 通过同步访问句柄访问它们,这种方式很快但具有排他性,且仅在 dedicated worker 中可用。这意味着同一时间只有一个标签页的 worker 能持有这些文件。

一个 Web Lock 选出那个标签页,该标签页启动数据库 worker。一个 SharedWorker 将其他所有标签页连接到它。无论你打开多少个标签页,它们都共享一个数据库和一条同步流。

这种安排必须能够应对浏览器对标签页所做的任何操作。你可能会关闭持有文件的标签页,或者浏览器可能会冻结它或将其放入前进后退缓存。另一个标签页随后可以获取锁,它的新 worker 会等待旧 worker 释放文件后再打开它们。如果现有数据库无法打开,我们将其视为存储问题,而不是重新开始,因为该文件可能包含你尚未发送的工作。

我们用 Kysely 针对诸如 visible_todos 这样的关系编写一次 Milieu 的查询。在服务器上,这些关系将查询限制为你被允许查看的行。在你的浏览器中,它们就是本地表,本来就只保存你的数据。

这样一来,一份查询定义就能同时服务于首次渲染、远程读取和本地读取。查询永远不需要知道它在读取谁的数据,组件看到的始终只是行。

这两个数据库并不完全相同。你的浏览器持有服务器数据的一个选定切片,而服务器的处理程序还会检查权限并执行浏览器无法完成的操作。共享查询和领域逻辑减少了这些代码行内部的重复。

你的浏览器能接收的每一行都属于一个所有者账户。你的通讯录属于你;共享项目的待办事项、事件和讨论属于项目的所有者。每个所有者都有一个计数器。每当他们的某一行发生变化时,数据库触发器会递增计数器并用新值标记该行。在每个已同步的行上,sync_owner 表示它属于谁,owner_seq 保存该标记值。

你的浏览器为它能看到的每个所有者保留一个游标:一个为你,一个为每个与你共享内容的人。一次拉取会请求那些标记值超出这些游标的可见行。服务器从同一个快照中读取这些行和新的计数器,这一点很重要:如果你的游标越过了响应中遗漏的某个更改,你的浏览器将永远错过那个更改。

计数器在不信任任何人的时钟的情况下对更改进行排序,而每个所有者一个计数器意味着不相关的账户永远不会在一个全局序列中排队。

刚下载的文件还不能安全地读取。

假设文件包含一个标题为“Book train”的待办事项。在它下载期间,你通过一个 remote command 将其重命名为“Book train to Edinburgh”。如果你的标签页此时切换到该文件,旧标题就会重新出现。

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →