@tonbo/sqlite-ursula

中文文档
0.11.4发布于 2026/11/03Apache-2.0TypeScriptES Module零依赖构建来源已验证

SQLite通过一个Ursula流通过SQLite - Ursula -vfs扩展复制:提交是本地的,在后台复制,Pi Durable的官方SqliteStorage在@tonbo/ SQLite - Ursula / Pi中

中文使用文档
文档目录10 节
  1. 开放
  2. 令牌
  3. 控制输出
  4. 进口
  5. 错误
  6. π耐用
  7. SQL函数
  8. 平台
  9. SQLITE_URSULA_VFS
  10. 许可证

@tonbo / sqlite-ursula

通过复制SQLite数据库 厄休拉 流,适用于Node.js 22.19或更高版本。

import { open } from "@tonbo/sqlite-ursula";

const db = await open("https://streams.example.com/acme/notes");
db.exec("CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, body TEXT)");
db.prepare("INSERT INTO notes(body) VALUES (?)").run("hello"); // committed on this host
await db.synced(); // in the stream, and this process still owns it
res.end("saved");

唯一的规则: 在进程外的任何东西对您所写或所读的内容(响应、队列确认、对另一个服务的调用)起作用之前, await db.synced().

提交是本地的。扩展将其附加到它拥有的日志中,并在后台将日志复制到流中。 synced() 一旦流保存调用之前所做的每个提交,以及调用之后开始的往返旅行发现该进程仍然是所有者,就会进行解析。它永远不会阻塞事件循环,并且在等待过程中保持进程活动。脚本在退出之前等待它。

开发人员会发生什么
运行语句或COMMIT本地工作:SQLite,加上一次写入日志
等待 synced()写入时一个追加往返,空闲时一个leader读取
失去了流提交继续, synced() 等待,过去 maxPendingBytes 写入失败 backlog_full
被接管了语句和 synced() 扔 UrsulaResetError (fenced), on("reset") 侦听器运行
崩溃提交从未确认的流在下次打开时从日志返回并重新发送

开放

open(url, { dir, token, signal, maxPendingBytes, recreate, readOnly, ...sqlite }) 返回一个类型的承诺 UrsulaDatabase,一个 node:sqlite DatabaseSync 子类,所以毛毛雨,kysely和Pi的司机取它。剩下的选项是 DatabaseSync.

  • 进程中第一次打开的流将其缓存绑定在 dir (在默认情况下 <user cache>/sqlite-ursula/0.8),捕获它并声明流:最后到达流的打开者拥有它。稍后打开的进程返回同一绑定上的另一个连接,没有网络。并发打开共享1。工作线程调用 open 了。
  • token 是承载令牌请求,还是提供分发令牌:看到了吗 令牌.
  • 该开的时候放弃 signal 火灾,30后没有一个。
  • 多个连接共享一个绑定及其文件,因此node:sqlite很忙 timeout 默认为5000ms。通过 timeout 去改变它。
  • 当open不能保留的本地缓存保存数据时,它被移到一边。在丢失提交已确认的缓存的流的历史更改之后(恢复到较早的点),在缓存保存未记录确认的提交(脱机或在崩溃之前已确认)的任何历史更改之后,当它的db文件不是它自己的并且与 recreate: true 当溪流消失的时候。没有保存数据的缓存将被替换为没有移动的缓存。移动的缓存进入 salvage/ 在流的缓存目录中作为一个普通的SQLite数据库 manifest.json, status().salvage 列出它,然后open从流构建一个新的缓存。没有 recreate,在保存数据的缓存上丢失流失败 not_found.
  • db.close() 是同步的。一旦绑定的最后一个连接关闭,复制将在后台耗尽,绑定将退出。再打开一次 open().
  • readOnly: true 不要求任何东西,也不保护所有者,并写了失败 SQLITE_READONLY (8).在拥有流的进程中,它在所有者的绑定上返回一个只读连接,该连接看到所有者最近的提交,无论是否被确认,所以它 synced() 像主人一样等待。在任何其他进程中,它建立或捕获自己的读取器缓存(role: "reader", reader-<n>/ 在流的缓存目录中,每个读取器进程一个)在打开时读取流的尾部,并从那时开始读取流:它不跟随以后的提交,并且它的 synced() 立即解决。一条封闭的小溪也会打开。在另一个部署上构建的读取器缓存失败 wrong_cluster,就像所有者的缓存一样,然后构建一个空缓存。关闭进程的所有只读连接,然后重新打开以读取更新的提交。当流不可达时,它打开一个较早的读取器缓存,否则就等待,直到信号触发。丢失的流失败 not_found. readOnly 与 recreate 失败与 invalid_argument.

UrsulaDatabase 增加了 name (流的路径,如 /acme/notes), boundPath (本地数据库文件的扩展绑定,其中 status().path 报告及 SQL函数 作为 path), synced({ signal }), status() 和 on("reset", listener) / off("reset", listener).

令牌

token 是一个字符串,还是一个提供者:一个返回的异步函数 { token, expiresAt },在那里 expiresAt 是一个 Date 或者纪元以来的毫秒数。然后,包在后台保持令牌保持新鲜,没有单独的刷新过程。

import { open, type TokenProvider, UrsulaUnavailableError } from "@tonbo/sqlite-ursula";

const token: TokenProvider = async ({ signal }) => {
  const r = await fetch(exchangeUrl, { method: "POST", headers: { authorization: `Bearer ${apiKey}` }, signal });
  if (r.status === 401) throw new UrsulaUnavailableError("unauthorized: the exchange refused the key", "unauthorized");
  if (!r.ok) throw new Error(`the exchange answered ${r.status}`);
  const { access_token, expires_in } = await r.json();
  return { token: access_token, expiresAt: Date.now() + expires_in * 1000 };
};

const db = await open("https://streams.example.com/acme/notes", { token });
  • 当包没有持有有效令牌时,在打开的第一个请求之前调用提供程序,在每个令牌生命周期的70%到80%之后再次调用,当请求被拒绝时再次调用 401。并发调用共享一个。 signal 当包停止等待答案时触发:30秒后,或者当使用字符串令牌打开时停止了提供者,或者没有数据库使用它。
  • 每个呼叫都是手动的 previous,当包不再接受它时,提供程序最后分发的令牌,与它分发的令牌完全相同:在计划刷新时的当前令牌,或服务器在执行了一个请求后拒绝的令牌 401. 它是 undefined 在第一次调用之前和提供者停止之后,除了open的绑定被拒绝的令牌,或者仍然绑定并阻塞的数据库被拒绝的令牌之外,其余的令牌都保留 previous 在一个停止过程中,直到提供者发出另一个或一个带有字符串令牌的open停止提供者。当 previous ,分发一个不同的令牌,并重新填充保存它的缓存。扩展使用相等的令牌表示没有更改,因此答案等于 previous,与没有周围空格的情况相比,将被视为失败的调用。返回失败的调用,并在失效时重试 previous 因此,最迟,将其令牌交还直到其过期的缓存将在其移交时再次被请求。从那个到期开始,只有失败,因为它计数,所以后退再次开始在1秒。永远不会改变的标记属于字符串 token 或在 URSULA_VFS_TOKEN_FILE,而不是在供应商。
  • 打开程序立即获得包持有的有效令牌,因此已经绑定的数据库上的连接不需要网络,并且在后台运行到期的刷新。过期令牌仅在提供程序失败时使用。否则,open等待飞行中的呼叫或新的呼叫,因为在长时间暂停该过程之后。
  • 哪个函数生成持有的令牌并不重要,因为进程携带一个令牌。带有另一个函数的open(包括内联箭头)共享持有的令牌和正在执行的调用,并为下一个调用运行的函数命名。
  • 抛出错误的提供程序 action 是 fix 永远失败了,不管它的类是什么:一个打开等待它的令牌拒绝,并且包停止调用提供程序,直到下一个 open() 有一个提供者。一个 UrsulaError 是拒绝本身。任何其他错误,例如无法扩展此包的类的库之一,都是错误 cause 一个 UrsulaUnavailableError unauthorized,行动 fix。所有其他错误都将使用后退重试,从1秒到30秒,而持有的令牌将继续服务。在1秒内到期的令牌也是如此,这使得没有时间刷新它。
  • 当一个空位 signal 在等待提供程序时触发,然后拒绝 UrsulaUnavailableError unavailable (行动 retry),其 cause 是提供程序的最后一个错误,或者是提供程序没有应答的信号原因。非可打印的ASCII符号,不带空格或 expiresAt 那不是一个时间,失败的开放与 UrsulaSetupError invalid_argument。也一样。 expiresAt 小于10^12,读取为epoch后的秒数(JWT的) exp)而不是毫秒。
  • 被拒绝的捆绑 401 失败 unauthorized 当它 signal 触发,就像使用字符串标记一样。扩展报告一个 401 只针对已经绑定的数据库,所以提供者只有在那时才知道:被拒绝的令牌被丢弃,调用开始,下一个打开等待新的令牌。当提供者的最后一次调用失败时,该失败是错误的 cause,它的作用是 retry 除非提供者永远失败了。
  • 被拒绝两次的绑定数据库请求将阻止复制 unauthorized,并立即调用提供程序。当设置新令牌时,扩展会立即再次发送被阻止的请求。当提供者交还被拒绝的令牌复制时,一旦报告拒绝的每个数据库被关闭和退役,每个提交都被确认,或者不再被阻止,包就会再次接收它。然后立即运行已计划的重试。
  • 打开后提供程序失败,令牌保持不变。一旦服务器拒绝, status().replication.condition 是 unauthorized,并继续提交,直到积压被填满。
  • 令牌是每个进程一个。命名令牌的最后一个打开设置它。带有字符串的打开将提供者停止在其线程上,而在字符串下等待它的打开继续进行。一个函数存在于一个JavaScript线程中,因此每个线程运行自己的提供程序,该提供程序在每次刷新时通过另一个线程设置字符串设置其令牌:为进程的线程提供相同类型的令牌。
  • 只有在open等待令牌时,提供程序的计时器才使进程保持活动状态。一旦它打开的线程没有绑定数据库,提供程序就会停止,无论打开的令牌是什么,因为它们都携带进程的令牌。
  • 普通字符串记号和 URSULA_VFS_TOKEN_FILE 像以前一样工作。没有令牌被记录。

控制输出

gate(db, work, { signal }) 运行 work 并且只接受它的结果一次 synced() 之后打的电话 work 解决解决。它将一条规则应用于任何对结果起作用的东西:一个fetch风格的处理程序 Response队列框架确认的作业处理器的承诺,您自己确认的消息。

import { gate, open } from "@tonbo/sqlite-ursula";

const db = await open("https://streams.example.com/acme/notes");

// A fetch-style handler: the status, the headers and every body chunk wait.
const handler = (request: Request) => gate(db, () => router.fetch(request), { signal: request.signal });

// A queue you acknowledge yourself.
for await (const message of subscription) {
  await gate(db, () => applyMessage(db, message)); // records message.id in the same transaction
  await message.ack(); // a crash before this redelivers, and applyMessage skips an id it has seen
}
  • db 是一个数据库或一个数据库数组。如果有几个,大门会同时等待所有的人。
  • 读取也会等待:结果可能已经读取了另一个调用的挂起提交,或者该进程即将被接管。
  • work的错误和它的结果一样等待,除了 UrsulaError,当库将其包装(作为 cause,或在 AggregateError):被拒绝的写没有提交任何内容,等待将报告重置。在等待期间的重置拒绝使用 UrsulaResetError,结果被丢弃。
  • 当 work 返回一个 Response 有了消息体,gate就会用一个相同的状态和报头进行解析,这些状态和报头的消息体只会传递一次每个数据块及其结束 synced() 读取数据块后进行的调用。一个调用覆盖了在前一个调用运行时读取的每个数据块,因此一个流每批支付一次往返,而不是每个数据块,并且gate最多提前读取64 KiB加上一个数据块。重置使主体出错。
  • signal 限制等待结果的时间,这对于 Response 它的状态和标题是:门拒绝与 signal.reason 并取消返回的主体,提交继续复制。它不会缩短一个已经开始的身体。取消主体来阻止它。
  • 一、 node:http 响应本身不是门控的:将其写在门控之后,如 res.end(await gate(db, () => save(req))).

大门没有覆盖的:

  • 影响内 work 在它返回之前,例如出站调用或处理程序中间的手动确认。 await db.synced() 在他们面前。
  • WebSocket帧、原始套接字、定时器、后台作业和子进程。
  • 只有一次的效果。在发布之后但在确认之前再次运行工作的崩溃,因此在与工作相同的事务中记录消息id和幂等键。
  • 任何超出 synced() 它本身的意思是:流持有提交,具有服务器提供的持久性。

进口

importSqlite(url, file, { token, timeoutMs }) 将一个普通的SQLite数据库文件导入到一个新的流中,作为流的第一个快照,然后返回 { offset, digest, pages, bytes }. 然后 open(url) 它和其他任何溪流一样。

  • token 是一个字符串,仅用于导入。import阻塞了它的线程,因此没有提供者可以在此期间刷新令牌:pass (await provider({ signal })).token.
  • 文件必须有4096字节的页面,没有 -wal 旁边没有热日记。 VACUUM INTO 写这样一个副本(之后) PRAGMA page_size = 4096 当数据库有其他页面时)。文件只被读取。
  • 流必须是缺失的,或者是空的。其他内容类型的流失败 stream_format还有其他的吗? stream_state,什么也没有发表。另一个发布行的服务器出现故障 protocol_mismatch的回答 Ursula-Protocol 和 Ursula-Cluster-Id 与 no_server_identity.
  • 导入运行时,流的打开失败 stream_state。在声明流离开拒绝打开和导入的流后失败的导入 stream_state 直到它被删除或重新创建。 UrsulaUnavailableError 从一个输入中说 fix 因为这个原因。
  • 超过请求限制的快照将分成几个部分,这需要服务器上的冷存储。如果没有,则压缩快照超过限制的文件将失败 invalid_argument,命名极限。
  • 调用阻塞它的线程,直到导入完成为止 timeoutMs (默认为10分钟)。

错误

包因扩展失败而抛出的每个错误都有一个 kind,一个 action (retry, reopen, stop 或者 fix)和a cause。分支 action。网络故障从不抛出from语句或 synced().

类抛出的类型
UrsulaWriteRefusedError写或提交,没有任何承诺backlog_full (后重试 synced()), unsupported (修复) too_large (修复:提交的页面占用超过 URSULA_VFS_MAX_COMMIT_BYTES,默认为128 MiB,因此拆分事务或提高限制,最多1 GiB)。
UrsulaResetError重置后的语句, synced(), reset 听众fenced, stale, rewound, deleted, local_damage, stream_state, internal
UrsulaUnavailableErroropen() 当它的信号首先触发时,也是在它等待令牌提供者(unavailable,行动 retry)最后一个条件(unreachable, unavailable, rate_limited, unauthorized, payment_required, forbidden),或 cancelled 当它还没有遇到任何人的时候
UrsulaSetupErroropen(), importSqlite(), synced() 或者 status() 一个已经退役的数据库invalid_url, invalid_argument, not_found, in_use, bound_elsewhere, local_setup, stream_format, no_server_identity, protocol_mismatch, wrong_cluster, stream_state, claim_races, not_bound, internal

一个 UrsulaSetupError 说 fix,除了 in_use 和 claim_races,上面说 retry, internal,上面写着 reopen。一个开放的最后条件是 wrong_cluster, no_server_identity 或者 protocol_mismatch 当它发出信号时抛出一个 UrsulaSetupError 是那种,不是那种 UrsulaUnavailableError。满足其中一个条件的绑定数据库将其报告为 status().replication.condition,无需重置,并在路由或服务器固定后继续运行。

一、 stream_state 重置(fix)意味着流不再匹配所有者所写的内容:另一个编写者附加到它,有人关闭了它,或者服务器丢失了它已确认的生产者状态。日志将保留未确认的提交,并且将拥有流的打开失败 stream_state 当水流保持这种状态时。

重置后,待定 synced() 电话先被拒绝,然后是 reset 侦听器运行一次。数据库的每个语句都会抛出重置,包括读操作,但它打开的读事务中的读操作除外,后者保留其快照。写入仍然失败。 open() 再次流,或停止时的动作是 stop。新的打开不需要等待旧数据库关闭:它在本地缓存的副本上工作,该副本包含旧数据库所做的每一次提交。当您不再需要旧数据库时,关闭它。

π耐用

在厄休拉流中持久化一个圆周率的线束,使用圆周率自己的习惯,只交换存储空间。 @tonbo/sqlite-ursula/pi Pi Durable的官方节点运行 SqliteStorage 在流上的URL:

import { Harness } from "@earendil-works/pi-durable";
import { openUrsulaPiStorage } from "@tonbo/sqlite-ursula/pi";

const storage = await openUrsulaPiStorage("https://streams.example.com/acme/agent", { closeTimeoutMs: 10_000 });
const harness = await Harness.open(storage, { models, registry }, context);
// ...
await harness.close(context); // closes the storage too

harness.close(context) 关闭线束打开的存储,在Pi 1.0到1.5上,因此存储不需要关闭它自己。没有 dir 它使用默认的缓存根。

每个事务和语句只有在 synced(),因此,一旦流持有Pi提交,它就会被解析。被拒绝的承诺 backlog_full 提交。它从1.2开始等待 synced() 并再次运行,因为Pi期望存储重试自己的瞬态失败:在Pi 1.5上,它将一组提交存储在一个中 Storage.commit 调用,整个调用以相同的批再次运行。在Pi 1.0和1.1上,它会抛出Pi StorageRejected,所以会议继续进行。重置会抛出 UrsulaResetError 它自己,所以会话停止。 close 等待到 closeTimeoutMs 用于 synced(). @earendil-works/pi-durable 是一个可选的对等依赖,从1.0.0到但不包括1.6.0,和 /pi 条目是唯一导入它的条目。

Pi 1.4在打开SQLite存储时会将其迁移到其模式版本2,而Pi 1.3或更早版本无法打开已迁移的SQLite存储。复制数据库也不例外:迁移与其他任何迁移一样是提交,因此一旦Pi 1.4或更新的所有者打开流,每个后续所有者(包括从流构建的新缓存)都需要Pi 1.4或更新的数据库。只将部署回滚到Pi 1.4之后的新流上。

openUrsulaPiStorage(url, options) 通过 dir, token, signal, maxPendingBytes 和 recreate 来 open,没有其他选择 open. walAutoCheckpointPages (SQLite的WAL自动检查点阈值) busyTimeoutMs (语句等待锁的时间)转到Pi的节点驱动程序,该驱动程序在数据库上打开自己的连接,并将Pi的默认值应用于您遗漏的那些。 closeTimeoutMs 默认是10秒。

SQL函数

其他主机在连接上调用扩展的SQL函数 loadUrsulaVfs() 返回: ursula_open, ursula_synced, ursula_events, ursula_cancel, ursula_status, ursula_stats, ursula_set_token, ursula_import(url, file, options_json, timeout_ms),他的 timeout_ms 必须是正的,然后呢 ursula_classify(message, errcode, path),它表示SQLite的消息和结果代码中的失败是什么 {kind, action, class, message, fix, request_id}:其 kind, action 和 class (write_refused, reset, unavailable 或者 setup(此包抛出的错误)、 message 一个主持人为它表演,然后 fix 和 request_id,这是 null 对于每次扩展失败。它返回 null 为SQLite自己的错误。 path 是语句运行的数据库,其状态命名语句的I/O错误,它本身没有说明什么。通过 NULL 查询SQL函数的错误。一个宿主重新打开 ursula_open 关闭后:一旦一个数据库退役,一个平原 new DatabaseSync(path) 它的道路被拒绝,直到 ursula_open 再次绑定。 ursula_stats(path) 消耗扩展保留的每次提交度量(字节、页面、日志和VFS中的时间),每次消耗之间每种最多8,192个,并报告它减少了什么。从这个包,叫它 db.boundPath 在连接上 loadUrsulaVfs() 的回报。

缓存的文件不是扩展名之外的SQLite文件。db文件保存了一个标记来代替SQLite的头魔术,它的WAL是 db-uwal。这个 sqlite3 Shell, node:sqlite没有扩展,并且在加载扩展之前打开的连接失败 SQLITE_NOTADB (file is not a database),什么也改变不了。要使用普通SQLite读取数据库,请从连接中将其导出 open() 返回, VACUUM INTO.

0.8服务器和流不读取0.7数据,0.8不读取0.7缓存。

平台

这个扩展是预先构建的:npm会为你的平台安装这个构建。 @tonbo/sqlite-ursula-<platform>,作为可选依赖项。它是预先构建的:

  • Linux x64、arm64, glibc 2.28及以上版本,如Debian 11、Ubuntu 20.04、Amazon Linux 2023、RHEL 8。
  • Linux x64带肌肉,如Alpine。
  • macOS 11或更高版本的苹果芯片。

在任何其他平台上,自己构建扩展并设置 SQLITE_URSULA_VFS。该扩展拒绝加载到3.50.4以上的SQLite中。

SQLITE_URSULA_VFS

当 SQLITE_URSULA_VFS 设置时,包将在该路径加载库,而不是在预构建的路径加载库。传递路径给 loadUrsulaVfs(path) 在其他调用做同样的事情之前。要从源代码(Rust)构建扩展:

git clone https://github.com/tonbo-io/ursula
cd ursula/clients/sqlite-vfs
cargo build --release
export SQLITE_URSULA_VFS=$PWD/target/release/libsqlite_ursula_vfs.so   # .dylib on macOS

许可证

apache - 2.0

版本与发布记录

@tonbo/sqlite-ursula 共发布 19 个版本,首次发布于 2026/10/07;过去 12 个月发布了 19 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.11.4latest2026/11/03今天
0.11.32026/10/304 天前
0.11.22026/10/295 天前
0.11.12026/10/286 天前
0.11.02026/10/277 天前
0.10.12026/10/259 天前

关于 @tonbo/sqlite-ursula 的常见问题

@tonbo/sqlite-ursula 是什么?

SQLite通过一个Ursula流通过SQLite - Ursula -vfs扩展复制:提交是本地的,在后台复制,Pi Durable的官方SqliteStorage在@tonbo/ SQLite - Ursula / Pi中

如何安装 @tonbo/sqlite-ursula?

在项目目录运行 npm install @tonbo/sqlite-ursula,或使用 pnpm add / yarn add / bun add。

@tonbo/sqlite-ursula 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@tonbo/sqlite-ursula 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@tonbo/sqlite-ursula 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.19.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@tonbo/sqlite-ursula 可以商用吗?

@tonbo/sqlite-ursula 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@tonbo/sqlite-ursula 还在维护吗?

最新版本 0.11.4 发布于 2026/11/03(今天);至今共发布 19 个版本,其中过去 12 个月发布了 19 个;首次发布于 2026/10/07。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。