@aiao/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'],反向没有依赖——本包不认识同步包。
安装
- npm
- Yarn
- pnpm
- Bun
npm install @aiao/rxdb @aiao/rxdb-plugin-history
yarn add @aiao/rxdb @aiao/rxdb-plugin-history
pnpm add @aiao/rxdb @aiao/rxdb-plugin-history
bun 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 事件到达之前就订阅上。
迁移
从 versionManager 直接挂在 core 里的版本升级,见历史与同步拆包。