rxdb-adapter-pglite
RxDB 适配器,使用 PGlite 在浏览器中运行 PostgreSQL。
功能特性
- 本地优先: 在浏览器中通过 WebAssembly 运行完整 PostgreSQL
- 零服务器: 无需后端服务器,数据存储在本地
- PostgreSQL 兼容: 支持标准 PostgreSQL 语法和功能
- 响应式: 数据变化自动触发更新
何时使用
- 需要 PostgreSQL 特性(如 JSONB、tsvector 全文搜索、高级索引)
- 计划未来迁移到 PostgreSQL 后端
- 需要更强的 SQL 标准兼容性
- 应用需要复杂查询和事务支持
与其他适配器对比
| 特性 | PGlite | wa-sqlite | sqlite-wasm |
|---|---|---|---|
| 数据库引擎 | PostgreSQL | SQLite | SQLite |
| 运行资产 | 约 50.9 MB(未压缩 Worker) | ~500KB | ~800KB |
| 全文搜索 | tsvector | FTS5 | FTS5 |
| JSON 支持 | JSONB | JSON1 | JSON1 |
| 生态兼容 | PostgreSQL | SQLite | SQLite |
体积按当前构建产物口径记录:浏览器 Worker 未压缩约 50.9 MB,npm tarball 压缩后约 16.9 MB。实际首次传输量取决于部署端压缩,后续加载取决于浏览器缓存策略。
安装
npm install @aiao/rxdb-adapter-pglite
# 或
pnpm add @aiao/rxdb-adapter-pglite
使用
import { RxDB, SyncType } from '@aiao/rxdb';
import { RxDBAdapterPGlite } from '@aiao/rxdb-adapter-pglite';
const rxdb = new RxDB({
dbName: 'demo',
entities: [],
sync: {
type: SyncType.None,
local: `{ adapter: 'pglite' }`
}
});
rxdb.adapter('pglite', database => new RxDBAdapterPGlite(database, { store: 'memory' }));
await rxdb.connect('pglite');
// 应用退出时释放 Worker 和数据库资源
await rxdb.disconnect('pglite');
事务 API(C2 已落地第二步)
PGlite 适配器同 rxdb-adapter-sqlite-core 一致 —— transaction() / runInTransaction() 的回调收到 PGliteTransactionExecutor:
await adapter.transaction(async executor => {
const repo = executor.getRepository(Post);
await repo.create({ title: 'inside tx' });
await executor.mergeChanges(actions, localChanges, /* disableTriggers */ false);
});
PGliteTransactionExecutor不直接导出 —— 它是RxDBAdapterPGlite的内部产物;状态自持,绝不从驱动的tx.closed派生(该标志在失败路径上不翻转,逃逸出去的 tx 会以 autocommit 继续写)。外部代码通过@aiao/rxdb的TransactionExecutor接口与之交互。
备份与恢复
backup() 把整个数据库写成一个 .rxdb-backup 归档流;restorePGliteDatabase() 把归档恢复到一个空的、未连接的目标。两端都逐块流式处理,不会先把整个库读进内存。
外部文件不在备份范围内。 归档只包含数据库本身(
scope: { database: 'included', externalFiles: 'excluded' })。rxdb-plugin-storage等插件存放在数据库之外的文件需要另行备份。
import { restorePGliteDatabase, cleanupIncompletePGliteRestore } from '@aiao/rxdb-adapter-pglite';
// 备份:写入任意 WritableStream(例如 File System Access 的文件句柄)
const handle = await showSaveFilePicker({ suggestedName: 'demo.rxdb-backup' });
const result = await adapter.backup(await handle.createWritable());
console.log(result.sha256, result.bytes);
// 恢复到 IndexedDB:目标 RxDB 实例已注册 adapter,但尚未 connect
const file = await (await showOpenFilePicker())[0].getFile();
await restorePGliteDatabase(file.stream(), { rxdb: target, options: { store: 'idb' } });
await target.connect('pglite');
恢复到内存目标时,结果里的 database 句柄要交给 adapter 的 restoredDatabase 选项领取(只能领取一次,且只能由恢复时传入的那个 RxDB 实例、以同一组扩展领取,否则报 invalid_state):
const { database } = await restorePGliteDatabase(stream, { rxdb: target, options: { store: 'memory' } });
target.adapter('pglite', db => new RxDBAdapterPGlite(db, { store: 'memory', restoredDatabase: database }));
await target.connect('pglite');
| 存储 | 备份 | 恢复 |
|---|---|---|
memory | ✅ | ✅ |
idb | ✅ | ✅ |
其他(opfs-ahp://、file:// 等) | ❌ unsupported_combination | ❌ unsupported_combination |
要点:
- 一致性:备份落在一个已提交事务边界上,备份期间的写入排在其后;锁等待上限由
lockTimeoutMs(默认 30s)控制,超时抛lock_timeout。idb源库的每个连接只在打开时从 IndexedDB 读入一份内存视图,之后看不到别的连接的提交,所以备份要求本连接从打开到现在一直独占这份存储: 其他连接(同页面另一个实例、其他标签页或 Worker)此刻还开着,或本连接打开之后有别的连接来过——哪怕已经关掉——都报target_busy。 这时先关掉其他连接,再让备份所用的实例disconnect()后重新connect(),然后备份。 - 运行环境:
idb源库的备份靠navigator.locks.query()数出其他标签页 / Worker 里的连接,环境不提供时报unsupported_combination(navigator.locks.query)。 一致快照用到 PGlite 未在类型声明里公开的运行时内部件(两把互斥锁与 Emscripten 文件系统);升级 PGlite 后缺了任何一个,备份报unsupported_combination(pglite),不会做到一半才失败。 - 先校验后写入:manifest 在第一块里读出,引擎版本、schema 指纹、扩展、加密认证域不兼容时在写入目标之前拒绝(
incompatible_archive/auth_domain_mismatch);完整性(SHA-256)读到末尾才能确认,失败时已写的数据会被丢弃(corrupt_archive/truncated_archive)。 - 加密库:归档里只有密文和 keyring 元数据,不含口令与密钥;恢复后的库保持锁定,用原口令
unlock()。 - 中断:IndexedDB 目标在校验通过前不写 IndexedDB,并用持久标记记录「恢复进行中」。进程在恢复中途被杀后,连接与再次恢复都会报
restore_incomplete,调用cleanupIncompletePGliteRestore(target)清理后即可重新恢复;清理失败时报cleanup_pending。 - 独占:同一目标上并发的恢复只有一个能赢(
target_busy),恢复期间的连接尝试报restore_in_progress。
所有错误都是 RxDBBackupError,按 error.code 分支处理(isRxDBBackupError() 可做类型收窄)。
完整示例
参考 dev-rxdb-angular 中的集成示例。
@aiao/rxdb-adapter-pglite — RxDB 适配器(PGlite / WebAssembly PostgreSQL 后端)。
公开三层 API:
- 适配器与客户端:RxDBAdapterPGlite, PGliteClient
- 数据访问:通过 RxDBAdapterPGlite 获取的 Repository
- SQL 生成(纯函数):
create_tables_sql,generate_trigger_sql,notify_function_sql,以及 PG 原生 FTS 模块(fts/)
测试工具单独从 @aiao/rxdb-adapter-pglite/testing 子路径导入,避免污染运行时包。
Enumerations
| Enumeration | Description |
|---|---|
| PGliteChangeType | PGlite 变更事件类型枚举 对应 PostgreSQL 的 TG_OP (trigger operation) |
Classes
| Class | Description |
|---|---|
| PGliteClient | PGlite 客户端:封装 @electric-sql/pglite 实例,提供: - 统一的 query/exec/transaction API(对齐 IPGliteClient) - 系统表(rxdb_change/rxdb_branch/rxdb_migration)NOTIFY 监听 + 批量分发 (16ms trailing 防抖,另有 max-wait 与容量上限兜底,见 PGliteNotificationBatcher) - 安全的 disconnect(先 syncToFs 再 close,避免 IDBFS 关闭后回调抛错) - LiveQuery 支持(依赖 init 阶段注入的 live extension) |
| PGliteNotificationBatcher | 把裸 NOTIFY 聚合成 PGliteChangeEvent。 |
| PGliteRestoredDatabase | 恢复到内存目标后得到的一次性数据库句柄。 |
| PostgreSQLDialect | PostgreSQL 方言实现 |
| RxDBAdapterPGlite | RxDB PGlite 适配器 |
| RxdbAdapterPGliteError | PGlite 适配器错误类 |
| RxDBChangePipelineTimeoutError | PGlite 变更管道在时限内未空闲。 |
| RxDBQueryCacheRowContractError | QueryCache 拉取落地时,远端行不满足本地表列契约。 |
Interfaces
| Interface | Description |
|---|---|
| AdapterEncryptionFacade | 暴露在 adapter.encryption 上的开发者门面,内部转发给 Keyring。 如果数据库中没有实体声明加密列,所有方法都会抛 EncryptedConfigurationError(code: 'no_encrypted_columns')。 |
| EncryptionContext | 加密上下文,贯穿所有可能接触加密列的 PGlite 辅助函数。 当数据库没有加密列时 keyring 为 null,辅助函数走明文分支。 与 sqlite-core 的 EncryptionContext 结构保持一致。 |
| FtsField | - |
| FtsOptions | FTS DDL 生成选项。 |
| IPGliteClient | PGlite 客户端的最小公开契约。 |
| ISqlDialect | SQL 方言接口 定义数据库特定的 SQL 语法转换方法 |
| PGliteChangeEvent | PGlite 变更事件 触发器通过 NOTIFY 发送的数据库变更事件 |
| PGliteChangeEventSource | 变更事件源:能挂/摘 PGliteChangeType 监听的客户端。 |
| PGliteClientEvents | PGlite 客户端事件映射: 每个 PGliteChangeType 对应一个 PGliteChangeEvent, 用于 PGliteClient(基于 EventDispatcher)的 addEventListener 强类型推断。 |
| PGliteClientOptions | PGlite 客户端配置选项 扩展自 PGlite 原生配置 |
| PGliteEngineInfo | 引擎版本与数据格式兼容键。 |
| PGliteNotificationBatcherOptions | PGliteNotificationBatcher 的入参。 |
| PGliteNotifyPayload | PGlite 通知 payload 结构 从 NOTIFY 消息中解析的数据 |
| PGliteQueryable | 能执行只读查询的最小接口:PGlite 实例与 IPGliteClient 都满足。 |
| PGliteRestoreOptions | PGlite 恢复选项。 |
| PGliteRestoreResult | PGlite 恢复结果。 |
| PGliteRestoreTarget | 恢复目标:一个尚未连接的 RxDB 实例,加上它将来连接时使用的 adapter 选项。 |
| PgliteTableColumn | PGlite 数据库表列信息接口 包含 PostgreSQL information_schema.columns 视图的所有字段 |
| RxDBChangePipelineTimeoutDiagnostics | 变更管道超时诊断快照。 |
Type Aliases
| Type Alias | Description |
|---|---|
| ForeignKey | 外键约束信息类型 描述表之间的外键关系 |
| FtsArrayKind | 数组列的物理存储形态。 |
| PGliteBackupStorage | 数据目录所在的存储后端;只有这两种声明支持备份与恢复。 |
| PGliteDataDirItem | 数据目录快照里的一项:目录或文件的条目头,或紧随文件条目头的一块内容。 |
| PGliteRestoreStage | 恢复进行到的阶段,按顺序各触发一次。 |
Variables
| Variable | Description |
|---|---|
| ADAPTER_NAME | PGlite 适配器名称常量 |
| DEFAULT_FTS_ARRAY_KIND | 数组列的默认物理形态,与适配器的建表映射保持一致。 |
| DEFAULT_FTS_REGCONFIG | FTS 默认 PostgreSQL regconfig,决定 tokenizer / stopwords / stemmer。 |
| DEFAULT_NOTIFY_BATCH_TIMEOUT_MS | 默认的 trailing 防抖间隔(毫秒)。 |
| DEFAULT_NOTIFY_MAX_BATCH_WAIT_MS | 一个批量窗口从开启到必须冲刷的上限(毫秒)。 |
| DEFAULT_NOTIFY_MAX_PENDING_EVENTS | 单个窗口内累积事件的上限,超过即立即冲刷,形成背压。 |
| deserializeFromEnvelope | 按持久化时的属性类型把已认证明文字节还原为业务值。 |
| FTS_COLUMN | FTS 表的物理列名(tsvector 类型)。固定加在原表上,避免与业务列冲突。 |
| INVALID_QUERY_ERROR_CODE | 查询编译失败的错误码:where / orderBy 无法被翻译成 SQL。 |
| normalizeCreateEntity | 规范化创建数据(过滤未赋值字段)。 |
| normalizeUpdateEntity | 规范化更新数据并过滤 readonly 字段。 |
| PG_MAX_PARAMS | PostgreSQL 协议单次查询最大参数数量(int16)。 按列数将大批量数据切片,避免 INSERT ... VALUES (...) 超限。 |
| pgDialect | 默认导出 PostgreSQL 方言实例 |
| PGLITE_BACKUP_ENGINE | 归档里记录的引擎名。 |
| PGLITE_BACKUP_LOCK_TIMEOUT_MS | RxDBBackupOptions.lockTimeoutMs 的默认值。 |
| PGLITE_EXCLUDED_FILES | 不进归档的文件名(任何深度)。 |
| serializeForEnvelope | 按属性类型把单个非空业务值编码为信封明文字节。 |
Functions
| Function | Description |
|---|---|
| asPGliteChangeEventSource | 把客户端窄化为变更事件源;不具备该能力时返回 undefined。 |
| assertQueryCacheRowContract | 落地前校验远端行的列集,不合契约就 fail-fast。 |
| buildCreateFtsTableSql | 生成 PostgreSQL FTS 物理结构 DDL:在原表追加 _fts tsvector 列 + GIN 索引。 |
| buildFtsTriggersSql | 生成 PostgreSQL FTS 同步 trigger(函数 + trigger)。 |
| chunkByPgParamLimit | 按参数数量上限把批量数据分片。 |
| cleanupIncompletePGliteRestore | 清理一次没做完的恢复(页面在恢复中途关闭、或恢复失败后清理本身也失败)。 |
| create_tables_sql | 生成多张表的创建 SQL(多语句串) |
| create_tables_statements | 生成多张表的创建语句列表 |
| generate_trigger_sql | 生成 PostgreSQL 触发器 SQL |
| generateNotifyFunctionSQL | 生成 NOTIFY 触发器函数 SQL |
| generateNotifyInfrastructureSQL | 生成完整的 NOTIFY 基础设施 SQL |
| generateNotifyTriggerSQL | 为指定表创建 NOTIFY 触发器 |
| getEntityObjectFromResult | 从 PGlite 结果行获取实体对象数据 PGlite 返回行作为对象,主要用于类型转换 |
| getMonotonicUpdatedAt | 保证 updatedAt 单调递增: 同一毫秒内多次 update 时递增 1ms,避免同步端按 updatedAt 比较失效。 |
| getSqlValue | - |
| getSqlWithParams | 将 PostgreSQL 参数占位符($1、$2 等)替换为实际值 用于批量操作中无法使用参数化查询的情况 |
| getSwitchUpdatedAt | 计算分支切换(undo / redo)UPDATE 应写入的 updatedAt。 |
| getTableColumnIndexName | 获取表列索引名称 |
| getTableName | 拼出形如 "public"."users" 的完全限定表名(带双引号转义)。 |
| getTableNameByMetadata | 根据 EntityMetadata 得到完全限定表名,等价于 getTableName(metadata.tableName, metadata.namespace)。 |
| quoteIdentifier | 把标识符(schema 名、表名、列名)包成双引号形式。 |
| quoteLiteral | 把字符串拼成 SQL 字符串字面量。 |
| remove_all_triggers_sql | 生成删除所有实体触发器的 SQL |
| remove_trigger_sql | 生成删除单个实体触发器的 SQL |
| removeNotifyTriggerSQL | 移除表的 NOTIFY 触发器 |
| requiredQueryCacheColumns | 算出「远端行必须自带」的列:本地表上 NOT NULL 且建表时拿不到默认值的那些。 |
| resolvePGliteInitOptions | 把 PGliteClientOptions 规范化为 PGlite 构造函数能直接消费的形状。 |
| restorePGliteDatabase | 把 RxDBAdapterPGlite.backup 产出的归档恢复成一个新的 PGlite 数据库。 |
| rxDBColumnTypeToPGliteType | 将 RxDB 属性类型转换为 PGlite 数据类型 |
| rxDBColumnTypeToPGliteTypeIndexName | 获取属性的索引操作符 http://www.postgres.cn/docs/current/indexes-opclass.html |
| shouldUsePGliteWorker | 判断是否需要为 PGlite 启用 Web Worker。 |
| toPGliteEngineInfo | 由 SHOW server_version 的原文算出引擎信息。 |
| transformEntityValuePGliteToJs | 将实体对象中的所有值从 PGlite 格式转换为 JS 类型 主要用于 RxDBChange 表的 patch/inversePatch 字段 |
| transformEntityValueToSql | 将实体值转换为 SQL 兼容格式 |
| transformValueJsToPGlite | - |
| transformValuePGliteToJs | - |
| verifyPGliteRestored | 校验恢复出来的库确实是 manifest 声明的那个库,而不只是一堆摘要正确的字节。 |