rxdb
面向 Local-first 应用的 TypeScript 数据库核心。通过装饰器驱动的实体定义,自动生成类型安全的 Repository 与响应式查询 API,让你在浏览器中直接运行 SQLite,以接近原生 App 的方式构建离线优先、数据驱动的 Web 应用。
@aiao/rxdb 是引擎核心,与具体存储后端解耦 —— 通过适配器接入 wa-sqlite / PGlite / Supabase / sqliteai,通过框架绑定接入 Angular / React / Vue。
安装
pnpm add @aiao/rxdb rxjs
核心包不直接提供存储实现,需配合一个适配器使用,例如浏览器内 SQLite:
pnpm add @aiao/rxdb-adapter-wa-sqlite
核心能力
- 装饰器驱动实体:用
@Entity/@TreeEntity声明数据模型与字段元数据 - 类型安全 Repository:从实体自动派生 CRUD 与关系查询接口
- 响应式查询:查询结果以 RxJS Observable 形式推送,数据变更自动刷新
- 变更追踪与事务:内建 diff、变更事件与事务支持
- 事务执行器:
TransactionExecutor把「本事务内的写入」这条判据收敛到一处 ——adapter.transaction(tx => tx.getRepository(X).create(...))而不是adapter.transaction(async () => entity.save())(后者会落回队列并永久挂起) - 迁移执行器:
MigrationType.up(executor)形参;用户迁移里的写入必须经 executor 发出,否则无路可走 - 适配器无关:同一套模型可运行在不同存储后端之上
事务与迁移 API 速查
import { TransactionExecutor, MigrationType } from '@aiao/rxdb';
// 1. 事务 —— 持有 executor 才算「在本事务内」
await adapter.transaction(async executor => {
const repo = executor.getRepository(Todo);
await repo.create({ title: 'inside tx' });
// await todo.save() // ❌ 落回队列并永久挂起
});
// 2. 嵌套内层工作 —— 复用当前 executor,不开新事务
await adapter.transaction(async executor => {
await executor.run(async inner => {
await inner.getRepository(Todo).create({ title: 'nested' });
});
});
// 3. 合并远端变更到本事务
await adapter.transaction(async executor => {
await executor.mergeChanges(actions, localChanges, disableTriggers);
});
// 4. 迁移 —— 必须把 executor 交给用户
const migration: MigrationType = {
name: '001-init',
async up(executor) {
await executor.getRepository(Seed).create({ ... });
},
async down() {}
};
旧签名
tx => tx.execute(sql)与MigrationType.up()(无参)仍兼容 —— TS 允许形参更少。
快速开始
字段必须在 properties 里声明。TypeScript 的字段类型在编译后被擦除,装饰器无法从
title!: string 这样的声明推导出持久化属性 —— 只写类字段的实体可以正常赋值,但不会落库。
import { Entity, EntityBase, PropertyType, RxDB, SyncType } from '@aiao/rxdb';
@Entity({
name: 'Todo',
tableName: 'todo',
namespace: 'public',
properties: [
{ name: 'title', type: PropertyType.string, required: true },
`{ name: 'done', type: PropertyType.boolean }`
]
})
export class Todo extends EntityBase {
title!: string;
done!: boolean;
}
最小闭环 —— 注册适配器、连接、写入、查询:
import { firstValueFrom } from 'rxjs';
const rxdb = new RxDB({
dbName: 'demo',
entities: [Todo],
sync: `{ local: { adapter: 'sqlite' }, type: SyncType.None }`
});
rxdb.adapter('sqlite', db => createYourSqliteAdapter(db));
await rxdb.connect('sqlite');
const repository = rxdb.entityManager.getRepository(Todo);
await repository.create({ title: 'write docs', done: false });
// find() 返回活查询 Observable,数据变更会自动重新发射
const todos = await firstValueFrom(repository.find({ where: { combinator: 'and', rules: [] } }));
实体定义、查询与变更的完整用法见文档站。
可选能力:本地工作树与提交历史
@aiao/rxdb-plugin-working-tree 给库加上「未提交的改动」与「提交历史」两个一等概念:用户的编辑先落进工作树而不是直接改主数据,commit() 一次性提交成快照,restore() 把历史版本的内容搬回工作树。
未装这个插件的库零成本——十张系统表、写捕获、提交图编解码全部随包走,核心侧只留装卸口与两道转交门。
import { rxDBPluginWorkingTree } from '@aiao/rxdb-plugin-working-tree';
rxdb.use(rxDBPluginWorkingTree); // ← 必须在 connect() 之前
await rxdb.connect('sqlite');
await rxdb.workingTree.enable();
启用之后核心会在 rxdb_migration 里留下一行能力水位,没装对应插件的客户端再打开这个库时,核心拒绝连接,并把该装的包名原样报出来。这道守卫对第三方插件同样有效:包名是插件自己写进水位行的,核心不需要认识它。
启用前要知道的六件事(完整版见文档站的插件页):
- 提交能力是数据库级的显式开关——一次
enable()之后整个库的所有实体、所有分支都按工作树语义运行;从此所有访问这个库的客户端都必须装上插件,包括旧版本的、你控制不到的那些。v1 没有disable()。 - 工作树不是草稿缓存。 工作树装的是「已经写进数据库、还没提交成快照」的变更,参与事务、被查询读到;
@aiao/rxdb-plugin-workspace的草稿缓存装的是「还没保存」的编辑器 buffer,根本没进主库。两层各管一件事,不能合并。 - 恢复不是 checkout。
restore()把历史提交的内容作为新的未提交变更写回工作树:HEAD 不动、历史不删、工作树变脏,下一步是commit()或discard()。没有 detached HEAD,也没有checkout()。 - 历史会原样保留敏感旧值。 写进过某次提交的字段永久留在那次提交里,之后改掉、清空、删行都不会动到它;v1 没有任何公开 API 能把它从历史里抠掉。不要把不该留痕的东西写进启用了提交能力的库——需要 right-to-erasure 的字段不适合直接存在这里。
- 加密边界。 支持字段加密的后端上,提交、工作树与恢复会话里的加密字段仍以 versioned envelope 落盘,持久化路径不会先解密再写明文,错误与摘要也不带明文。但加密保护的是落盘的字节——解锁后的合法读取照常拿到旧值,所以它不消解第 4 条,第 4 条也不能替代它。
- 不改写历史。 没有 amend / rebase / squash,没有「修改提交信息」,也没有 auto-baseline。提交图损坏时守卫只把分支置为
corrupted_read_only并留下诊断,不动 HEAD、不删记录。
还有一条容易漏掉的:远端同步拉下来的变更和用户的编辑一样进工作树,在 status().byOrigin 里计为 origin = 'remote_sync' 且不豁免——它会让 clean 变成 false,并被下一次 commit() 一并提交(提交者是这次 commit() 的 authorId,v1 不伪造远端作者身份)。「同步之后工作树突然脏了」是正常行为,不是缺陷。
写捕获拦得住什么,拦不住什么
写捕获只覆盖经 adapter 的写路径与 adapter 公开的批量写方法。绕过 adapter 的外部数据库句柄——另一个进程直接打开同一个 SQLite 文件、另起一个 PGlite 实例、DevTools 里手写 SQL——拦不住,v1 也不承诺拦得住;这类写入不进工作树、不进历史、status() 看不见。
因此启用了提交能力的数据库有一条硬约束:业务表只能经 RxDB 写入。这句话不是免责声明的注脚:不假装拦得住比拦不住更重要——一道号称拦得住却拦不住的门禁,会让人把「没报错」当成「没被绕过」。
文档
- 仓库主页与路线图:https://github.com/aiao-io/rxdb
- API 参考、快速上手与框架集成指南见项目文档站
- 工作树与提交历史:
@aiao/rxdb-plugin-working-tree插件页
License
RxDB 核心包 - 响应式数据库客户端 提供实体管理、版本控制、查询管理等功能
Enumerations
| Enumeration | Description |
|---|---|
| OnDeleteAction | 外键约束操作枚举 定义 SQLite 外键约束的级联行为 |
| OnUpdateAction | 外键更新行为枚举 定义当父表主键被更新时的级联行为 |
| PropertyType | 实体属性类型枚举 定义实体属性支持的数据类型 |
| RelationKind | 实体关系类型枚举 定义实体之间可能的关系类型 |
| SyncStatus | 同步状态枚举 |
| SyncType | 同步类型枚举 定义不同的数据同步策略,控制本地数据和远程数据之间的同步方式 |
Classes
Interfaces
Type Aliases
Variables
Functions
| Function | Description |
|---|---|
| __decorateClass | - |
| applyExternalEntityUpdate | 把外部事件的增量 patch 合并进缓存实体,且不记成用户的本地修改。 |
| assertClaimedCapabilities | 未认领能力守卫 |
| assertOptionalNonNegativeSafeInteger | 校验可选的非负安全整数,不赋予缺省值任何领域语义。 |
| assertRxDBBackupCompatible | 判定归档能否恢复到目标配置。 |
| assertSingleActiveBranch | 运行期校验:必须恰好一行 active;不修复任何东西。 |
| assertSupportedRxDBSystemVersions | 断言库的版本不高于本进程支持的版本,否则拒绝打开。 |
| assertUsableBranchId | 校验分支 id 可用,不可用即抛。 |
| assertValidSystemContribution | 校验贡献的形状 |
| branchMaterializationPageFingerprint | 算一页快照 payload 的指纹——这是分页指纹的公开口径(FR-044)。 |
| buildOfflineWriteRepositoryRules | 按「离线可写但没有 changelog 端点的仓库」构造 OR 组,形状与 buildPushableRepositoryRules 完全一致。 |
| buildPushableRepositoryRules | 按「当前配置里有推送资格的仓库」构造 OR 组,同步记录只提供水位线。 |
| calculateOrderBy | 按 orderBy 给一批结果排序。 |
| canonicalMaterializationJson | 把一个 JSON 值折成键序无关的字符串——物化协议两端共用的规范形。 |
| capabilityWatermarkName | 把一次能力认领编码成迁移行名 |
| classifyBackupIoError | 把流 / 存储抛出的原始异常映射到稳定分类;已分类的原样返回。 |
| compactChanges | 压缩变更列表为 SwitchVersionActions(专用于 Push 场景) |
| computeRxDBSchemaFingerprint | 计算实体结构指纹。 |
| createEntitySyncResolver | 构造实体同步配置解析器。 |
| createSha256 | 创建增量 SHA-256 摘要器。 |
| declareTrustedWrite | 声明紧接着这一次写的意图 |
| decodeRxDBChangeEntityId | encodeRxDBChangeEntityId 的反序列化。无前缀视为"未被编码过", 直接返回原值(兼容老 RxDB 数据)。 |
| decodeRxDBChangePatch | encodeRxDBChangePatch 的逆运算;语义、列过滤规则完全对称。 |
| decodeRxDBEntityIdentity | encodeRxDBEntityIdentity 的反序列化。 |
| describeEntityFields | 生成一个实体的完整字段描述。 |
| deterministicStringify | 确定性的 JSON.stringify |
| diffMetadata | 比较远程元数据与本地元数据,确定同步动作 |
| encodeRxDBChangeEntityId | 把 RxDBEntityId 包成带有版本号的字符串。所有 id 一律包信封 —— string / number / bigint 都带前缀、版本号与类型 tag,靠 tag 在反序列化时区分。 |
| encodeRxDBChangePatch | 把实体的字段变更包(patch)按列类型重新编码,让它在落盘 / 跨网络时仍是合法 JSON。 |
| encodeRxDBEntityIdentity | 为 AAD(Additional Authenticated Data)和身份键编码实体标识。 |
| Entity | 实体装饰器 用于将类标记为 RxDB 实体,并处理元数据、代理和生命周期 |
| entityDefaultNow | 默认值函数里的「现在」。 |
| - | |
| - | |
| findCurrentSyncRecord | 读取某个 repository 在当前分支上的同步记录(只读,不创建) |
| formatEntityFieldValue | - |
| formatMetadataViolations | 把跨实体聚合后的违规渲染成单条异常消息。 |
| gateRawWrite | 判定一次 raw 写,放行时才执行语句 |
| getCurrentBranch | 取当前分支;没有激活分支时激活(或新建)main。 |
| getEntityColumnName | 将实体属性名或关系 ID 名转换为数据库列名。 |
| getEntityId | 获取实体 ID |
| getEntityMetadata | 获取实体元数据。 |
| getEntityMutations | 获取实体变更映射 |
| getEntityStatus | 获取实体状态。 |
| getEntitySync | 取实体生效的同步配置。 |
| getEntityType | 从实体元数据取回实体类。 |
| getFingerprintByEntities | 多个实体指纹计算 |
| getFingerprintByEntity | 单个实体指纹计算 |
| getFingerprintPrimitive | 值类型指纹计算 用于 count 等返回原始值的查询 |
| getLocalSystemRepositories | 取本地适配器上的系统表仓库。 |
| getOrCreateSyncRecord | 取出(或首次创建)某个 repository/branch 的同步水位记录 |
| getRemoteSystemRepositories | 取远端适配器上的系统表仓库。 |
| getRxDBBackupAuthDomain | 加密认证域:有实体声明加密列时为库名(与 keyring 的 namespace 同源),否则 null。 |
| getRxDBBackupSchemaFingerprint | 实体结构指纹。 |
| getRxDBChangeEntityIdQueryValues | 把一组 ID 拆成"用于 SQL IN (...) 列表"的字符串集合。 |
| getRxDBChangeKey | 构造业务实体的唯一键(用于 deletes/updates/inserts Map) |
| getRxDBEntityIdentityKey | 计算一个稳定可索引的"身份键"字符串:rxid1: + 编码字节的 hex。 |
| getRxDBSystemVersionState | 从迁移名集合里解出两个版本号。 |
| getSyncableRepositories | 获取需要同步的 repository 列表 |
| getSyncCapability | 读取某个同步类型的能力 |
| getSyncConfig | 获取实体的有效同步配置(支持全局配置回退) |
| getSyncType | 从 EntityMetadata 获取同步类型 |
| getSystemEntityIdentities | 全部系统表的身份(namespace:name,形如 rxdb:RxDBBranch) |
| getSystemEntityNames | 全部系统表的实体名(@Entity 上的 name,不带 namespace) |
| groupBySyncType | 按同步类型分组 repositories |
| hasRxDBBackupWebLocks | 当前环境是否提供 Web Locks。 |
| installWorkingTreeCapture | 在一个本地适配器实例上装好四个捕获挂载点 |
| isAdapterShutdownError | 判断错误是否由 adapter 关闭/断连引起。 |
| isCrossTabEvent | 判定事件是否来自其他 tab。 |
| isCurrentRxDBSystemVersion | 判断库是否已经停在当前水位,两个号都相等才算。 |
| isEntityInternalName | 判断字段名是否是内部保留字段 包括基类字段、私有字段和以下划线开头的字段 |
| isEntityMatchWhere | 判断实体是否匹配规则组 |
| isEntitySyncResolver | 判定入参是不是解析器(而不是一份数据库级 SyncOptions)。 |
| isNetworkError | 判断一个错误是否为网络故障(连不上远端),而非远端给出的业务结果。 |
| isNoSync | 检查 repository 是否完全不同步 |
| isRemoteNewer | 判定远端记录是否比本地新。 |
| isRepositorySyncEnabled | 判定仓库的同步开关是否处于启用状态 |
| isRuleGroup | 判断是否是 RuleGroup |
| isRxDBBackupError | 判断一个值是否为指定 code 的 RxDBBackupError。 |
| isRxDBEntity | 检查是否为 RxDB 实体。 |
| isStaleEntityEvent | 判断一条实体变更事件相对实体缓存是否陈旧。 |
| isStaleEntityRemoveEvent | 判断一条 DELETE 事件相对实体缓存是否陈旧。 |
| isSystemEntity | 判断一个实体类是不是 RxDB 注入的系统表 |
| isUniqueConstraintViolation | 判定错误是否为唯一/主键约束冲突。 |
| isWorkingTreeCaptureMountPoint | 判定一个写原语签名是否为捕获挂载点 |
| needsOfflineWrite | 检查 repository 在远端不可达时是否接受本地写(并在恢复连接后重放) |
| needsPull | 检查 repository 是否需要 pull |
| needsPush | 检查 repository 是否需要 push |
| normalizeCreateEntity | 规范化创建数据(过滤未赋值字段)。 |
| normalizeUpdateEntity | 规范化更新数据并过滤 readonly 字段。 |
| parseCapabilityWatermark | 从迁移行名里读回能力认领 |
| parseEntityFieldsDescriptor | 解析并校验来自网络或存储的字段描述。 |
| parseEntityFieldValue | 按字段类型把原始值规范化成实体侧的运行时表示。 |
| parseEntityRecordValues | 按实体元数据把一整条原始记录解码成实体侧的运行时值。 |
| parseRxDBBackupManifest | 校验并规范化归档 manifest。 |
| parseRxDBChangeKey | 解析实体键 |
| parseRxDBEntityIdentityKey | getRxDBEntityIdentityKey 的逆运算;不带前缀时视为"非身份键", 原样返回 —— 这样 parseRxDBEntityIdentityKey(decodeRxDBEntityIdentityKey(x)) === x 对任意字符串 x 都不抛错,调用方可以无条件来回转。 |
| parseUpdatedAt | 把 updatedAt 解析成毫秒时间点。 |
| prepareIncrementalUpdate | 为一批 UPDATE 事件准备增量合并上下文。 |
| queryNeedRefreshCreate | - |
| queryNeedRefreshRemove | - |
| queryNeedRefreshUpdate | - |
| quoteSqlIdentifier | 把标识符(表名、列名)包成双引号形式。 |
| registerSystemEntities | 登记插件贡献的系统表 |
| repositoryKey | 把仓库标识渲染成 namespace:entity(依赖图与错误消息共用的键格式) |
| resolve_current_branch | 解析「当前分支」:取激活分支,没有则激活 main,main 也不存在则建一个。 |
| resolvePullIneligibility | 判定某个 repository 当前能否拉取(syncType + RxDBSync.enabled 两道) |
| resolvePushIneligibility | 判定某个 repository 当前能否推送(syncType + RxDBSync.enabled 两道) |
| resolveSingleActiveBranch | 只给首次启用迁移用:读出唯一的 active 分支,零 active 时恢复到 main。 |
| runRxDBBackupWhenQueued | 在 adapter 的串行队列里执行备份任务,排队本身有时限、可取消。 |
| sha256Hex | 算一段字节的 SHA-256。 |
| sqlBooleanLiteral | 把布尔值拼成 SQL 布尔字面量。 |
| sqlIntegerLiteral | 把整数拼成 SQL 数值字面量。 |
| sqlStringLiteral | 把字符串拼成 SQL 字符串字面量。 |
| sqlTimestampLiteral | 把时刻拼成 SQL 字符串字面量(ISO 8601,UTC)。 |
| syncTypeIneligibility | 读取某个同步类型在指定方向上不具备能力的原因 |
| takeDeclaredWrite | 取出并清除这个作用域上的声明 |
| toEntitySyncResolver | 把历史签名里的 databaseSync 参数归一成解析器。 |
| transitionMetadata | 将元数据选项(metadataOptions)转换为实体元数据(metadata) |
| trustedCallsiteKey | 登记键:文件 + 符号 + 意图 |
| tryAcquireRxDBBackupLock | 立即尝试获取锁,拿不到就返回 null,不排队。 |
| tryGetEntityMetadata | 探测实体元数据:查不到返回 undefined,不抛错。 |
| tryGetEntityStatus | 探测实体状态:查不到返回 undefined,不抛错。 |
| uninstallWorkingTreeCapture | 卸载捕获挂载点,把五个写原语恢复到安装前的样子 |
| uuid | - |
| 按 EntityFieldConfig 校验字段值。 | |
| validateEntityMetadata | 校验单个实体的元数据声明,返回按 namespace/entity/field/rule 排序的全部违规。 |
| validateEntityMetadataSet | 跨实体聚合校验,并把全部违规排序后一次性返回。 |
| validateFieldValue | 按 EntityFieldDescriptor 校验单个字段值。 |