RxDBSystemContribution
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:227
插件对系统层的贡献
Remarks
九个注册点缺一不可,各自防一种不会编译报错的事故:
- RxDBSystemContribution.entities 漏接 → 表建不出来,首次用到时抛一条读不出主语的错;
- RxDBSystemContribution.createInitialRows 没进同一次
createTables()→ 新库第一次 启动就缺行,而缺行的表看起来跟正常的空表一模一样; - RxDBSystemContribution.createMigrations 只接了既有库那一半 → 新库下次启动重跑
up()撞主键(见system/migrations/index.ts:新库不跑这条链,链里的名字由createMigrationWatermarks()直接写成已执行水位); - RxDBSystemContribution.bootstrapExisting 漏接 → 既有库连上了,能力却没在这条连接上 接通,此后每一次写都绕开本能力,一条错误都不会有;
- RxDBSystemContribution.writeBranchRows 漏接 → 新分支缺贡献行,而缺行的分支与 一条正常分支在形状上分辨不出来,要到下一次按 id 取那几行时才炸;
- RxDBSystemContribution.removeBranchRows 漏接 → 分支删了、贡献行留着,而留下来的行 按 id 挂靠,同名重建之后会被新分支逐字命中——一条刚建出来的分支于是带着上一条的 HEAD、上一条的未提交条目、上一条崩在半路的物化现场;
- RxDBSystemContribution.prepareBranchSwitch 漏接 →
switchBranch()收下了调用方的 前置条件却没人校验,一条错误都不会有:用户显式要求「工作树不干净就别切」,切换照样 发生,而那正是他刚刚说要避免的事;随切换而来的那些行(激活代际 +1)也一并不落, 于是main → feature → main走一个来回之后,走之前捕获的凭据仍然验得过。 - RxDBSystemContribution.takeOverBranchSwitch 漏接 → 普通切换会照常跑在一条本地还 没有历史的分支上:它没有 ref、没有工作树状态行,切过去之后投影是上一条分支的内容, 而用户看见的是一次成功的切换;
- RxDBSystemContribution.settleBranchSwitchFailure 漏接 → 失败诊断永远落不了盘。 本钩子是回滚之后唯一还能写库的时点:失败的判定发生在那次切换事务里,而那次事务 注定回滚,写在里面的诊断会跟着一起消失。
九个都是必填,没有一个带 ?。没有可写之物的贡献方写一个空实现——那是一句
「我确实不需要」的明示,而 ? 让「不需要」与「忘了」变成同一种东西。
RxDBSystemContribution.capability 与 RxDBSystemContribution.packageSpecifier 则是给「未认领能力守卫」用的:宿主把它们写成一条能力水位行,此后任何没装这个插件的 客户端打开该库都会被挡住并拿到该装的包名。
Properties
capability
readonly capability: Uncapitalize<string>;
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:234
能力名,与 IRxDBPlugin.name 同值
Remarks
水位行按它归因,因此不得含 :——行名是 : 分隔的,含冒号的能力名会让守卫把它切碎。
entities
readonly entities: readonly EntityType[];
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:249
贡献的系统实体,顺序即建表顺序
packageSpecifier
readonly packageSpecifier: string;
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:246
包说明符,如 @aiao/rxdb-plugin-working-tree;未认领时原样报给用户
version
readonly version: number;
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:243
能力版本,正整数
Remarks
写进水位行,但核心不比对它:能不能读旧版的表只有插件自己知道,核心插一脚的话, 插件每加一版都要等核心跟着放行。核心只回答「有没有人认领这个能力」。
Methods
bootstrapExisting()
bootstrapExisting(context): Promise<void>;
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:290
既有库引导末尾,把本能力接到这条连接上
Parameters
| Parameter | Type | Description |
|---|---|---|
context | RxDBSystemActivationContext | 已建表、已迁移、已 completeBootstrap() 的本地适配器 |
Returns
Promise<void>
接通完成;没有连接期动作的贡献方返回一个已决 promise
Remarks
只在既有库上调,新库一次都不调。 这不是优化,是语义本身:新库的全部贡献行都是
同一次 createTables() 刚由本进程写下的,它们的取值由 RxDBSystemContribution.createInitialRows
当场决定——「读回来看看是什么状态」在那里问的是自己一行之前写了什么。既有库不一样,
它的状态是别人留下的,只有读了才知道。
新库还多一条硬约束:那次建表是一次原子提交,在它之外再开一个事务会把「表与初始行同批」 这条不变量破掉。
调用点排在 reconcileEntityIndexes() 之后、适配器就绪置位之前。这个位置不可改:
就绪置位同时是 adapter:local 的判据,声明了该依赖的插件在它之后才会被安装——把本钩子
挪到那之后,就会出现一个「别的插件已经在写、本能力还没接通」的窗口,而那批写入不留痕迹。
createInitialRows()
createInitialRows(entityManager, context): readonly any[];
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:258
新库建表时随表一次写入的初始行
Parameters
| Parameter | Type | Description |
|---|---|---|
entityManager | EntityManager | 用来 instantiate() 行对象 |
context | RxDBSystemBootstrapContext | 建表时点已经存在的事实 |
Returns
readonly any[]
与 RxDBSystemContribution.entities 同批写入的行;没有初始行时返回空数组
createMigrations()
createMigrations(entityManager): MigrationType[];
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:269
既有库要跑的迁移
Parameters
| Parameter | Type | Description |
|---|---|---|
entityManager | EntityManager | 迁移用来 instantiate() 行对象 |
Returns
迁移列表;新库由 createMigrationWatermarks() 直接写成已执行水位,不执行 up()
prepareBranchSwitch()
prepareBranchSwitch(context): Promise<void>;
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:358
在切换事务内部校验本能力的前置条件,并落下本能力在一次切换中该落的行
Parameters
| Parameter | Type | Description |
|---|---|---|
context | RxDBBranchSwitchContext | 切换事务的执行器、当前/目标分支 id,以及调用方提出的前置条件 |
Returns
Promise<void>
做完;没有前置条件要查、也没有行要落的贡献方返回一个已决 promise
Throws
任何前置条件不成立;抛出即回滚整次切换
Remarks
跑在 adapter.switchBranch() 的写事务里,由适配器经
SwitchBranchOptions.prepare 在解析出目标分支之后、动第一行之前回调,
每次切换恰好一次。
它曾经排在那次调用之前、自己开一个只读事务,理由写的是「塞进去要让每个适配器各开一个
口子」。那个理由不成立:真正实现 SQL 切换的只有 rxdb-adapter-pglite 与
rxdb-adapter-sqlite-core 两处(sqlite 家族的五个后端共用后者),开的是两个口子,
而且开的方式是入参上多一个必填回调,不是每个适配器各写一遍校验。
代价那一侧则被低估了:两个事务之间有一个窗口,校验说「干净」,窗口里的一次写让它变脏,
切换照样完成。当时写着这个窗口「由 expectedActivationRevision 这类 CAS 字段自己兜住」——
而那个字段恰恰也是在只读事务里比的,它不是 CAS,只是一次读后比较,兜不住任何东西。
拿到的执行器可写,写下的行与这次切换同生共死。能写并不意味着该写:这里该落的只有 「一次切换本身必然带来的那些行」(例如激活代际 +1),业务数据不在此列。
名字里是 prepare 而不是 assert:它的职责从「只判断」扩成了「判断 + 落下随切换而来的行」,
而 assert* 读起来像一个不留痕迹的检查,会让下一个人把那一次写挪出去。
也不叫 canSwitchBranch:can* 读起来像返回 boolean,而「不能切」的原因
必须能带着分支 id、条目数、expected/actual revision 一起报给用户——那只有异常带得动。
removeBranchRows()
removeBranchRows(context): Promise<void>;
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:328
每移除一条分支时,在调用方的事务里清掉本能力挂在它名下的那几行
Parameters
| Parameter | Type | Description |
|---|---|---|
context | RxDBBranchRemovalContext | 调用方的事务执行器与即将被删的分支 id |
Returns
Promise<void>
清理完成;没有分支级行的贡献方返回一个已决 promise
Remarks
与 RxDBSystemContribution.writeBranchRows 严格对称:建行、删行由同一个贡献方 负责,因为「一条分支在本能力里占了哪几张表」这件事只有它知道。核心这边既不知道有几张表, 也不该在加第七张表时跟着改。
不指望外键级联替它做这件事。 级联要不要生效摊在六个后端各自的 PRAGMA foreign_keys
与建表路径上,而贡献方的表里完全可能有几张压根没有指向 rxdb_branch 的关系。一半靠约束、
一半靠代码的清理是两套要互相盯着的机制——remove_branch 对 RxDBChange 早已给出过答案:
显式删,尽管那张表同样挂着级联。
不碰 rxdb_branch 那一行,那是调用方最后自己删的。也不碰任何全库单例行:那些行不属于
任何分支,跟着分支一起删掉之后,整个库会在下一次读它们时永久失败。
settleBranchSwitchFailure()
settleBranchSwitchFailure(context): Promise<void>;
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:413
切换事务回滚之后,落下本次失败的持久诊断
Parameters
| Parameter | Type | Description |
|---|---|---|
context | RxDBBranchSwitchFailureContext | 当前/目标分支 id 与那个错误 |
Returns
Promise<void>
落完;没有诊断要落的贡献方返回一个已决 promise
Remarks
时点是它的全部:判定失败发生在切换事务里(RxDBSystemContribution.prepareBranchSwitch 抛出),而那次事务注定回滚——把诊断写在里面等于写完就没。回滚之后本函数才跑, 它自己开事务,写下的行留得住。
具体到工作树:assertCommitGraphIntact() 命中损坏时,CommitBranchRef.status 要落成
corrupted_read_only(FR-051)。不落的话每次重试都重新扫整张图,而「什么时候开始坏的」
这个诊断永远缺席。
不得抛出。 本函数跑在 catch 里,紧接着要把原始错误重新抛出去——从这里抛出的任何
东西都会顶替掉那个错误,于是用户拿到的是「落诊断时数据库忙」,而不是「目标分支的历史
重放不出来」。贡献方自己把失败包住:诊断是附加的,落不下来不改变这次切换已经失败
这件事,而它把原始错误盖掉才是真正的损失。
只在回滚路径上调。切换已经提交之后再失败(记账那几步)不调:那时 active 已经切过去了, 这不是一次「没切成」。
takeOverBranchSwitch()
takeOverBranchSwitch(context): Promise<RxDBBranchSwitchTakeover>;
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:388
在普通切换开始之前,问一句本次切换要不要由本能力整个接管
Parameters
| Parameter | Type | Description |
|---|---|---|
context | RxDBBranchSwitchTakeoverContext | 当前/目标分支 id 与调用方提出的前置条件;没有执行器,见 RxDBBranchSwitchTakeoverContext |
Returns
Promise<RxDBBranchSwitchTakeover>
'switched' 表示 active 已经由本贡献方切过去了;'not_applicable' 表示照常走普通路径
Throws
本次切换不该发生时;抛出即整次切换失败,且 active 仍停在原处
Remarks
存在的理由只有一个:有些切换的目标在本地还不是一条可切的分支。syncBranches() 收下的
远端分支只有 metadata,第一次切过去要先把远端快照拉下来物化成本地历史(FR-044/049),
而那条流水线的三段——预取、分页落库、提交屏障——没有一段能跑在
RxDBSystemContribution.prepareBranchSwitch 里:预取是网络 I/O,分页落库要求每页
各自可提交,而屏障必须与切 active 落在同一个事务里,只能由接管方自己发起那次切换。
于是分工是:接管方把这次切换整个做完(含自己调 adapter.switchBranch()、把屏障放进
它的 prepare),核心这边跳过普通路径,只补上与切换无关的记账(redo 栈、undo 视图、
SwitchBranchCommitEvent)。
第一个答 'switched' 的就是最后一个:核心问到它为止,后面的贡献方一个都不再问。
两个贡献方都接管等于 active 被切两次,而第二次看到的现场已经是第一次的结果。
预取与分页落库跑在切换事务之外,与那次切换不同生共死:接管失败时已落库的 staging 留给下一次续用(按 attempt 可续、可作废,见 FR-044),核心不会替它回滚;屏障那一段则随 切换事务一起回滚,active 仍停在原处。
名字里是 takeOver 而不是 beforeSwitch:before* 读起来像一个不改变主流程的前置钩子,
而这个钩子的返回值决定主流程还跑不跑。
writeBranchRows()
writeBranchRows(entityManager, context): Promise<void>;
Defined in: packages/rxdb/src/rxdb-plugin-system.ts:307
每新建一条分支时,在调用方的事务里写下本能力的那几行
Parameters
| Parameter | Type | Description |
|---|---|---|
entityManager | EntityManager | 用来 instantiate() 行对象 |
context | RxDBBranchCreationContext | 调用方的事务执行器与刚写下的分支 id |
Returns
Promise<void>
写入完成;没有分支级行的贡献方返回一个已决 promise
Remarks
与 RxDBSystemContribution.createInitialRows 的差别是时点,不是内容:那个是
每个库一次(建表那一刻),这个是每条分支一次(含建库时那条 main——但 main 走的是
createInitialRows,因为那时事务还没开给任何人)。
名字不叫 onBranchCreated:on* 读起来像一个可以订阅、可以失败、可以稍后补上的监听器,
而这里的行必须与分支行同生共死。抛错就让整条 create_branch 事务回滚,这是对的。