使用 @tursodatabase/sync 在 JavaScript 中实现 Turso 本地数据库与云端双向同步
【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso
Turso(turso)是一个用 Rust 编写的 SQLite 兼容数据库,而@tursodatabase/sync是面向 JavaScript/TypeScript 生态的同步引擎包,用于将本地 Turso 数据库与 Turso Cloud 托管数据库进行双向同步。本文以 bindings/javascript/sync/README.md 为主线,结合该仓库中 sync 包的 TypeScript 源码(sync/packages/common、sync/packages/native、sync/packages/wasm)与 NAPI 类型定义,完整讲解安装、connect配置、pull/push/sync同步流程、高级选项(加密、transform、远程写、部分同步等)以及底层同步引擎的运作原理,帮助你快速上手并深入理解同步机制。
包定位与适用场景
@tursodatabase/sync的核心职责是将本地 Turso 数据库与 Turso Cloud 之间进行双向同步(原文:This package is for syncing local Turso databases to the Turso Cloud and back)。
在 Turso 的 JavaScript 生态中,它与其他包的分工如下(见 sync/README.md 与 native 包 README):
@tursodatabase/database:提供内存版 Turso 数据库,兼容 SQLite 查询语言与文件格式,直接运行在 Node.js 进程内,无网络开销;@tursodatabase/database-wasm:浏览器(WASM)环境的嵌入式数据库库;@tursodatabase/serverless:提供相同 API 的 serverless 驱动;@tursodatabase/sync:在上述本地数据库能力之上,增加与云端数据库的双向同步能力。
需要注意的是,@tursodatabase/sync连接的数据库由connect返回的对象,与@tursodatabase/database包的Database类拥有相同的函数,只是额外增加了几个用于同步的方法(pull、push、sync),这与源码中class Database extends DatabasePromise(见 promise.ts)的实现一致——同步数据库实例继承自通用的DatabasePromise基类。
从仓库状态看,Turso 数据库已在多家组织的生产环境中运行,但尚未达到 1.0 版本,官方在文档中明确建议保持备份。
安装
在 Node.js 项目中安装同步包:
npm install @tursodatabase/sync从 native 包 package.json 可以看到,该包是 NAPI-RS 原生模块(二进制名sync),并依赖两个配套包:
@tursodatabase/database-common:提供DatabasePromise、Transaction等公共基础类;@tursodatabase/sync-common:提供同步引擎协议、runner、retryFetch等公共逻辑。
支持的原生目标平台包括x86_64-unknown-linux-gnu、x86_64-pc-windows-msvc、aarch64-apple-darwin、aarch64-unknown-linux-gnu,即 Linux(x86/arm64)、macOS 与 Windows 均可用。
快速开始:同步一个 Turso Cloud 数据库
以下示例完整来自 sync/README.md 的 Getting Started 章节,用于将托管在 Turso Cloud 的数据库同步到本地:
import { connect } from '@tursodatabase/sync'; const db = await connect({ path: 'local.db', // path used as a prefix for local files created by sync-engine url: 'https://<db>.turso.io', // URL of the remote database: turso db show <db> authToken: '...', // auth token issued from the Turso Cloud: turso db tokens create <db> clientName: 'turso-sync-example' // arbitrary client name }); // db has same functions as Database class from @tursodatabase/database package but adds few more methods for sync: await db.pull(); // pull changes from the remote await db.push(); // push changes to the remote await db.sync(); // pull & push changes几点关键解读:
path是本地文件的路径前缀。正如 types.ts 中DatabaseOpts.path的注释所说明:同步数据库会以该前缀写出多个文件(例如local.db-info、local.db-wal等),不仅仅是单个数据库文件;url是远程数据库地址,可以通过turso db show <db>获取;注意它同时支持libsql://与https://两种协议前缀——在 run.ts 的normalizeUrl中会把libsql://或turso://统一归一化为https://后再发起请求;authToken通过turso db tokens create <db>签发;clientName是任意客户端名称,用于在服务端区分客户端;源码注释说明库会通过追加唯一后缀来保证clientId的唯一性。
创建连接后,db同时具备普通数据库能力(exec、prepare、transaction等)与三个同步方法。sync()等价于先pull()再push()。
核心同步 API 详解
pull():从远程拉取变更
await db.pull(); // 返回 true 表示拉取到了新变更pull()的语义与返回值的细节,可从 promise.ts 的实现确认:
- 若数据库是以无同步方式打开(未提供
url),调用会抛出sync is disabled as database was opened without sync support; - 内部先调用
engine.wait()等待远程产生新变更,再调用engine.apply(changes)将变更应用到本地; - 如果拉取到的变更集为空,返回
false;否则应用变更后返回true; - 如果设置了
longPollTimeoutMs,服务端会保持连接打开,直到数据库出现新变更或超时,可用于实时监听场景。
push():推送本地变更到远程
await db.push();push()将本地累积的未同步变更发送到远程(见 promise.ts)。如果设置了transform回调,则每条变更在发送前都会经过该回调处理(详见下文 transform 章节)。
sync():先拉后推
await db.sync(); // pull & push changessync()是pull()与push()的组合操作,适合在程序的关键节点(如启动时、写入一批数据后)做一次完整的双向同步。
checkpoint() 与 stats()
除了文档提到的三个同步方法,同步数据库还提供了(见 promise.ts):
await db.checkpoint():对本地数据库执行 WAL checkpoint;await db.stats():返回同步引擎统计信息,其结构定义在 types.ts 的DatabaseStats:
| 字段 | 含义 |
|---|---|
cdcOperations | 尚未发送到远程的本地变更(CDC 操作)数量 |
mainWalSize | 主 WAL 文件大小(字节) |
revertWalSize | 回滚 WAL 文件大小(字节) |
lastPullUnixTime | 最近一次成功 pull 的 Unix 时间戳 |
lastPushUnixTime | 最近一次成功 push 的 Unix 时间戳(可为 null) |
revision | 从远程拉取到的变更的不透明版本号(可作为 etag 使用,但不应解释其内容) |
networkSentBytes/networkReceivedBytes | 网络发送/接收的总字节数 |
connect 完整配置项(DatabaseOpts)
connect接受的配置对象类型为DatabaseOpts,完整定义在 types.ts。下表汇总了所有字段及其语义:
| 字段 | 类型 | 说明 |
|---|---|---|
path | string(必填) | 本地文件路径前缀,同步引擎会写入多个带此前缀的文件 |
url | string \| (() => string \| null) | 远程数据库地址。省略时为纯本地数据库;也支持传入函数,返回非空值时启用同步(延迟同步),此时其他参数(如加密)必须预先设置 |
authToken | string \| (() => Promise<string>) | 远程鉴权令牌;支持函数形式以按请求提供短期凭证 |
clientName | string | 任意客户端名,库会附加唯一后缀保证 clientId 唯一 |
remoteEncryption | EncryptionOpts | 云端数据库加密参数(若云端默认加密) |
transform | Transform | 每条变更发送到远程前的回调,可用于实现复杂冲突解决策略 |
longPollTimeoutMs | number | pull 操作的长轮询超时时间,不设置则无超时 |
tracing | 'error' \| 'warn' \| 'info' \| 'debug' \| 'trace' | 开启内部日志 |
experimental | ExperimentalFeature[] | 在本地数据库上启用的实验特性(如'views'、'index_method'、'vacuum'),与普通Database的experimental选项对应 |
remoteWritesExperimental | boolean | 实验性:写语句在远程服务器执行而非本地;每次写(或事务提交)后自动 pull 以保持读写一致性。需要url,且所有显式事务都走远程 |
pushOperationsThreshold | number | 单个 push HTTP 批次中打包的 CDC 操作数量上限;达到后按事务边界拆分,单个用户事务不会被拆分。默认(不设置)一次发送全部变更集 |
pullBytesThreshold | number | 引导(bootstrap)下载拆分为多个/pull-updatesHTTP 请求的字节数提示(使用server_pages_selector位图)。默认单次往返完成引导;仅影响引导阶段,增量 pull 不受影响;部分同步使用query策略时无效 |
logicalMvccPull | boolean | 增量 pull 的同步协议覆盖:默认自动探测远程协议(WAL 页流 vs MVCC 逻辑日志流)并持久化;true强制 MVCC 逻辑日志流,false强制页流。通常仅测试或作为逃生舱使用 |
fetch | typeof fetch | 同步引擎所有 HTTP 请求(push、pull、wait-for-changes)的 fetch 实现替换,可用于重试/退避(见retryFetch)、AbortSignal 超时、请求日志、测试 mock |
partialSyncExperimental | object | 实验性:部分同步配置(见下文) |
url 的延迟同步与动态鉴权
url与authToken都支持函数形式(见 types.ts),这在 promise.ts 中有完整实现:
- 当
url是函数时,本地数据库先创建,同步在 url 返回非空值时"开启";同步引擎每次 HTTP 请求时都会调用该函数获取最新地址(见 run.ts),若返回 null 则引擎被暂停(url is empty - sync is paused); - 当
authToken是函数时,每次请求都会调用它动态生成Authorization: Bearer <token>头,适合接入短期凭证刷新机制。
加密(EncryptionOpts)
remoteEncryption的EncryptionOpts定义在 types.ts:
interface EncryptionOpts { // base64 编码的加密密钥(根据算法必须是 16 或 32 字节) key: string, // 加密算法: // - aes256gcm, aes128gcm, chacha20poly1305: 预留 28 字节 // - aegis128l, aegis128x2, aegis128x4: 预留 32 字节 // - aegis256, aegis256x2, aegis256x4: 预留 48 字节 cipher: 'aes256gcm' | 'aes128gcm' | 'chacha20poly1305' | 'aegis128l' | 'aegis128x2' | 'aegis128x4' | 'aegis256' | 'aegis256x2' | 'aegis256x4' }当配置了remoteEncryption时,promise.ts 会在每次请求中额外附加两个 HTTP 头:x-turso-encryption-key与x-turso-encryption-cipher,同步引擎的 NAPI 构造参数也相应接受remoteEncryptionCipher与remoteEncryptionKey(见 index.d.ts)。
transform:发送前的变更改写
transform回调类型为Transform = (arg: DatabaseRowMutation) => DatabaseRowTransformResult(types.ts)。DatabaseRowMutation描述一条变更:
interface DatabaseRowMutation { changeTime: number; // 变更的 Unix 秒级时间戳 tableName: string; // 变更所属表名 id: number; // 变更行的 rowid changeType: 'insert' | 'update' | 'delete'; // 变更类型 before?: Record<string, any>; // 变更前的行数据 after?: Record<string, any>; // 变更后的行数据 updates?: Record<string, any>; // 变更中仅被更新的列 }返回值DatabaseRowTransformResult有三种可能(源码注释,见 types.ts):
{ operation: 'skip' }:完全忽略该变更,不发送也不应用;{ operation: 'rewrite', stmt: { sql, values } }:用提供的 SQL 语句替换该变更;null:保持变更原样。
底层处理在 run.ts 的Transform请求类型中:每条 mutation 的结果被映射为Keep/Skip/Rewrite三种内部指令传给同步引擎。
部分同步(partialSyncExperimental,实验性)
partialSyncExperimental: { // bootstrap 策略: // - prefix: 启动时先在本地加载前 N 字节 // - query: 加载指定 SQL 语句触及的页面 bootstrapStrategy: { kind: 'prefix', length: number } | { kind: 'query', query: string }, // 分段大小:让同步引擎按 segment_size 字节分批加载页面 // (例如以 128kb 加载第 1 页时,会加载 [1..32] 共 32 页) segmentSize?: number, // 预取可能很快会被访问的页面 prefetch?: boolean, }该配置在 promise.ts 中被转换为 NAPI 层的JsPartialSyncOpts(Prefix/Query两种 bootstrap 策略,见 index.d.ts)后传给同步引擎。
底层原理:同步引擎与协议循环
@tursodatabase/sync并非简单地"上传/下载整个文件",而是由一个 Rust 编写的同步引擎(SyncEngine,通过 NAPI-RS 暴露,见 index.d.ts 的SyncEngine类)以生成器(generator)协议驱动。理解这一点有助于把握connect、pull、push的真实开销与行为。
协议循环(runner / run)
同步引擎通过GeneratorHolder(connect()、wait()、push()、apply()、checkpoint()、stats()都返回该对象)以resumeAsync方式推进,每次推进可能产生四类请求(见 index.d.ts 的JsProtocolRequest):
Http:向远程服务器发起 HTTP 请求(方法、路径、请求头、请求体由引擎给出);FullRead:读取本地元数据文件;FullWrite:原子写入本地元数据文件;Transform:把一批 mutation 交给 JS 侧的transform回调处理。
这些请求由 run.ts 的process()逐类执行,runner()则维护请求队列并推进引擎的 I/O 循环(ioLoopAsync)。同步引擎的每个 HTTP 请求都会附加上文构造的鉴权/加密头。
并发控制:SyncEngineGuards
由于同步操作会与本地语句执行交错进行,run.ts 定义了SyncEngineGuards,用四把异步锁(waitLock、pushLock、pullLock、checkpointLock)串行化关键操作:
pull()走wait守卫(只持 waitLock),拉取到变更后再走apply守卫(四把锁全持有)来应用变更;push()走push守卫(持 push/pull/checkpoint 三把锁);checkpoint()走checkpoint守卫(四把锁全持有)。
这样保证 wait(长轮询)、push、apply、checkpoint 之间互不重叠,避免本地数据库文件状态在同步过程中被并发破坏。
原生 I/O 与内存 I/O
- Node.js 环境下使用
NodeIO(见 promise.ts):通过node:fs/promises读写文件,且写入采用"先写临时文件再 rename"的原子方式(${path}.tmp.${unix}.${nonce}),避免半写状态; - 若底层数据库是内存模式,则改用
memoryIO(),把元数据保存在Map中(run.ts)。
远程写(remoteWritesExperimental)
开启remoteWritesExperimental后,promise.ts 的exec/prepare会先通过classifySql(NAPI 层的Database.classifySql,见 index.d.ts)判断 SQL 类别(read/write/begin/commit/rollback):
- 读语句仍在本地执行;
- 写语句通过
RemoteWriter(remote-writer.ts)发送到远程,RemoteWriter内部复用@tursodatabase/serverless的Session,每次写(或事务提交)后自动pull()一次以保证读写一致性(read-your-writes); - 事务整体在远程执行:
BEGIN ...、COMMIT、ROLLBACK由execRemote自动识别并管理会话生命周期(见 remote-writer.ts)。
需要注意:transactionAsync目前不支持与remoteWritesExperimental同时使用(会抛出明确错误,见 promise.ts),此时应使用已标记 deprecated 的transaction()。
retryFetch:弹性网络传输
同步引擎的fetch可替换为 run.ts 提供的retryFetch(),为所有同步 HTTP 请求增加重试/退避能力:
- 重试条件:网络错误(DNS、连接重置、AbortError 等)、5xx 服务端响应、429 限流响应;
- 不重试:2xx、3xx 及其余 4xx(鉴权/请求错误重试无意义);
- 默认值:3 次尝试(初始 + 2 次重试)、初始延迟 500ms、退避倍率 2(延迟序列 500ms → 1000ms);可用
{ attempts, delayMs, backoff, fetch }定制。
示例:
import { connect } from '@tursodatabase/sync'; import { retryFetch } from '@tursodatabase/sync-common'; const db = await connect({ path: 'local.db', url: 'libsql://...', fetch: retryFetch(), // 默认参数 // fetch: retryFetch({ attempts: 5, delayMs: 1000 }), });运行与测试
同步包在仓库内的测试方式(见 native package.json 的 scripts)可以复现:
- 常规测试:
npm test,需要先构建本地同步服务器二进制(LOCAL_SYNC_SERVER=../../../../../target/debug/tursodb),并用 vitest 运行(排除 remote-write 测试); - 远程写测试:
npm run test:remote-write; - 测试用例可参考 promise.test.ts 与 remote-write.test.ts;
- 浏览器/WASM 场景对应 wasm 包(
@tursodatabase/database-wasm生态),其入口文件支持 Vite/Turbopack 的构建适配(见index-vite-dev-hack.ts、index-turbopack-hack.ts等)。
与周边包的搭配
- 纯本地内存数据库(无网络):
@tursodatabase/database,示例见 native README,支持内存库、文件库与transactionAsync事务; - 浏览器环境:
@tursodatabase/database-wasm,API 与 Node 版本一致; - 无状态 serverless 场景:
@tursodatabase/serverless; - 需要本地↔云端双向同步:
@tursodatabase/sync,即本文主题。
仓库中的可运行示例还可在 examples/javascript 下找到(如database-node、sync-node、sync-wasm-vite、concurrent-writes、encryption等),其中sync-node与sync-wasm-vite是与本文主题直接对应的端到端参考实现,建议结合阅读。
小结
@tursodatabase/sync提供了一条在 Node.js/浏览器中把本地 Turso 数据库与 Turso Cloud 保持双向同步的路径:通过connect一次配置path、url、authToken,即可获得与@tursodatabase/database完全一致的数据库 API,并叠加pull/push/sync三个同步方法。在此基础上,其配置体系还覆盖了长轮询监听、动态鉴权、延迟同步、云端加密、冲突改写(transform)、远程写、部分同步等进阶能力;底层则由 Rust 同步引擎以生成器协议驱动 HTTP/本地 I/O 请求循环,配合多把异步锁保证同步与本地执行的安全交错。使用时请留意remoteWritesExperimental与partialSyncExperimental等仍处于实验阶段的特性,并始终为生产数据保留备份。
【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考