rxdb-plugin-history
Implements: US-025 核心插件化外移
历史、撤销重做与分支。装上它,rxdb.versionManager 才存在。
核心 @aiao/rxdb 保留的是原语:三张系统表(RxDBBranch / RxDBChange / RxDBSync)、变更编解码、冲突模型、同步资格判定,以及 getCurrentBranch()。这些东西并不专属历史子系统——变更日志触发器直接写 branchId,RxDBChange.branch 会生成真实的 REFERENCES rxdb$rxdb_branch(id) 外键,整条响应式增量链路都跑在 rxdb_change 上。搬进本包的是消费者:读历史、撤销重做、建/切/合/删分支。
推拉同步的调度不在这里,它住在 @aiao/rxdb-plugin-sync:rxdb.syncManager.push() / .pull() / .sync()。那个插件 inject: ['plugin:history'],反向没有依赖——本包不认识同步包。
安装
pnpm add @aiao/rxdb @aiao/rxdb-plugin-history
使用
import { RxDB, SyncType } from '@aiao/rxdb';
import { rxDBPluginHistory } from '@aiao/rxdb-plugin-history';
const rxdb = new RxDB({
dbName: 'notes',
entities: [Note],
sync: `{ type: SyncType.Full, local: { adapter: 'wa-sqlite' }, remote: { adapter: 'supabase' } }`
});
// 传工厂函数本身,不要调用它;必须早于 connect()——connect() 内部就会调 init()
rxdb.use(rxDBPluginHistory).adapter('wa-sqlite' /* … */);
await rxdb.connect('wa-sqlite');
// 撤销重做
await rxdb.versionManager.history().undo();
await rxdb.versionManager.history(Note).redo(2);
// 分支
await rxdb.versionManager.createBranch('feature-a');
await rxdb.versionManager.switchBranch('feature-a');
await rxdb.versionManager.mergeBranch('feature-a');
use() 的时机是有意义的:use() 在 init() 之前调用则插件在 init() 时装上,之后调用则立即装上,而 connect() 内部会调 init()。写在 connect() 后面,第一批变更已经错过了历史订阅。
主要能力
| 分组 | 入口 |
|---|---|
| 历史 | history(scope?) → histories$ / undoHistories$ / redoHistories$ / count$ / undo() / redo() |
| 分支 | createBranch() / switchBranch() / mergeBranch() / removeBranch() |
| 计数 | pushableCount$ / pullableCount$ |
| 恢复 | restoreEntity() |
history() 的作用域按入参解析:无参 = 整库,传实体类 = 该仓储,传实例 = 该行。撤销一个跨作用域事务时抛 RxDBCrossScopeTransactionError,而不是只撤一半——半个事务比失败难查得多。
没装插件时
rxdb.versionManager 不存在(读到 undefined),核心不给一个什么都不做的空壳。这对下游是可见的契约而非退化路径:
- DevTools:
DevToolsRxDB.versionManager是可选成员,连接器据实报「分支能力不可用」,不会把它谎报成「一个分支都没有」。 - 同步状态枢纽:
SyncStateHub留在核心,但可推送计数由本插件经bindPushableCount()接上。没装插件,那个读数就是无人维护——这正是实情。待拉数归@aiao/rxdb-plugin-sync:它接住远端适配器发出的requestPullableRefresh()。 - 类型:
versionManager由本包的declare module '@aiao/rxdb'声明。只消费、不负责安装的模块(框架组件、共享 UI)应写import type {} from '@aiao/rxdb-plugin-history';——它把类型声明拉进编译单元,同时在 emit 时整句擦除,不产生运行时依赖。
连接纪元
插件声明 lifecycle: 'scoped',三处宿主改动全部登记在 install(scope) 收到的作用域上,disconnectAll() 时逆序释放,宿主不调用 destroy():VersionManager 实例本身(连同 destroy())、rxdb.versionManager 槽位、syncState.bindPushableCount() 订阅。重新 connect() 装的是一个全新的管理器,而不是复活上一纪元那个——后者指向的事件总线已经拆了。
本包不声明 inject,所以它在 init() 里就装完;@aiao/rxdb-plugin-sync 声明了 inject: ['plugin:history'],要等本包装好才轮到它,那个槽位得 await connect() 之后才在。
插件不声明 inject:VersionManager.init() 只挂监听与订阅,一条适配器读写都不发。声明 adapter:local 会把安装推到引导链之后,而分支流必须在第一条 rxdb_change 事件到达之前就订阅上。
@aiao/rxdb-plugin-history —— 历史、撤销重做与分支。
Remarks
核心 @aiao/rxdb 只保留原语:三张系统表(RxDBBranch / RxDBChange /
RxDBSync)、变更编解码、冲突模型与同步资格判定。原因是这些东西并不属于历史子系统 ——
变更日志触发器直接写 branchId,RxDBChange.branch 会生成真实的
REFERENCES rxdb$rxdb_branch(id) 外键,整条响应式增量链路都跑在 rxdb_change 上。
搬进本包的是消费者:读历史、撤销重做、建/切/合/删分支。
推拉同步的调度不在这里。US-025 阶段 D 之后它住在 @aiao/rxdb-plugin-sync:
rxdb.syncManager.push() / .pull() / .sync()。那个插件 inject: ['plugin:history'],
反向则没有依赖——本包不认识同步包。
Example
import { rxDBPluginHistory } from '@aiao/rxdb-plugin-history';
rxdb.use(rxDBPluginHistory);
await rxdb.connect();
// 撤销重做走作用域 API:无参 = 整库,传实体类 = 该仓储,传实例 = 该行
await rxdb.versionManager.history().undo();
Classes
| Class | Description |
|---|---|
| PushInFlightRegistry | 「哪些变更此刻正在飞往远端」的进程内登记处。 |
| RxDBCrossScopeTransactionError | 作用域 undo / redo 撞上跨作用域事务 |
| RxDBPluginHistory | 历史 / 撤销重做 / 分支插件。 |
Interfaces
| Interface | Description |
|---|---|
| PushInFlightSession | 一次 push 调用期间持有的认领句柄。 |
| SwitchBranchActionReaders | 算切换分支所需操作要用到的最小仓库能力。 |
| SwitchBranchStep | 分支切换步骤 |
| SyncHistoryBridge | 同步插件可见的历史侧能力。 |
| VersionManager | 版本管理器 |
Type Aliases
| Type Alias | Description |
|---|---|
| RxDBPluginHistoryOptions | 本插件当前不接受任何选项 |
Variables
| Variable | Description |
|---|---|
| rxDBPluginHistory | 历史插件工厂。 |
Functions
| Function | Description |
|---|---|
| compute_switch_branch_actions | 用给定的仓库算「从当前 active 分支切到 branchId」要落的增删改—— switch_branch_actions 的本体。 |
| find_branch_path_to_root | 沿 parentId 从一条分支走到它所在树的根,返回途经的全部分支(含自己,根在最后)。 |
| find_switch_branch_step | 算出树上两个节点之间的切换路径 |
| get_branch_max_change | 读一条分支当前的 tip:它自己那些未被回滚的变更里 id 最大的那条。 |
| isIgnorableDetachedVersionEventError | 判断游离事件任务的错误是否可忽略 |