# @tonbo/sqlite-ursula 中文文档

规范地址：https://npmcn.com/package/%40tonbo/sqlite-ursula/

版本：0\.11\.4
npm 元数据更新时间：2026-11-02T16:43:48.161Z
中文译文更新时间：2026-11-02T17:23:09.938Z
软件包许可：Apache-2\.0

本文由上游 README 机器翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40tonbo/sqlite-ursula
原文对照：https://npmcn.com/package/%40tonbo/sqlite-ursula/?lang=original
项目主页：https://ursula.tonbo.io/docs/examples/sqlite-vfs
源码仓库：https://github.com/tonbo-io/ursula

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：164 kB，25 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22\.19\.0
- 运行依赖：0 个
- peer 依赖：@earendil-works/pi-durable \>=1\.0\.0 \<1\.6\.0
- 首次发布：2026-10-07T03:25:13.834Z
- 最新版本发布：2026-11-02T16:43:47.482Z
- 已发布版本数：19（近 12 个月 19 个）

## 中文简介

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

## 安装

```sh
npm install @tonbo/sqlite-ursula
```

也可以使用：`pnpm add @tonbo/sqlite-ursula`、`yarn add @tonbo/sqlite-ursula`、`bun add @tonbo/sqlite-ursula`

## README

# @tonbo / sqlite-ursula

通过复制SQLite数据库 [厄休拉](https://ursula.tonbo.io/) 流，适用于Node.js 22.19或更高版本。

```ts
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` 是承载令牌请求，还是提供分发令牌：看到了吗 [令牌](https://npmcn.com/package/%40tonbo/sqlite-ursula/#readme-tokens).
-   该开的时候放弃 `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函数](https://npmcn.com/package/%40tonbo/sqlite-ursula/#readme-sql-functions) 作为 `path`)， `synced({ signal })`， `status()` 和 `on("reset", listener)` / `off("reset", listener)`.

## 令牌

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

```ts
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`队列框架确认的作业处理器的承诺，您自己确认的消息。

```ts
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` |
| `UrsulaUnavailableError` | `open()` 当它的信号首先触发时，也是在它等待令牌提供者(`unavailable`,行动 `retry`） | 最后一个条件(`unreachable`， `unavailable`， `rate_limited`， `unauthorized`， `payment_required`， `forbidden`),或 `cancelled` 当它还没有遇到任何人的时候 |
| `UrsulaSetupError` | `open()`， `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：

```ts
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）构建扩展：

```bash
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 是什么？

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 和提交记录判断。

