@aiao/rxdb-plugin-history 0.0.26 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  import { RxDBBranch, RxDBError } from '@aiao/rxdb';
2
2
  /**
3
3
  * 分支切换步骤
4
+ *
5
+ * @internal
4
6
  */
5
7
  export interface SwitchBranchStep {
6
8
  /**
@@ -48,14 +50,6 @@ export interface FindBranchPathOptions {
48
50
  nextChangeId: number | null;
49
51
  }
50
52
 
51
- /**
52
- * 计算出树的两个节点之间的切换路径
53
- * 规则如下:
54
- * 1, 如果两个树节点有共同的祖先节点,则路径为从当前节点到共同祖先节点,再到目标节点
55
- * 2, 如果没有共同祖先节点,则路径为从当前节点到根节点,再到目标节点
56
- * 3, 需要计算出路径内每个节点需要变更过程,也就是 fromChangeId -> toChangeId
57
- * 4, 由于 changeId 在不同分支的变化都是是递增的, 所以如果逆向还原数据那么 fromChangeId 会大于 toChangeId, 正向应用数据则相反
58
- */
59
53
  /**
60
54
  * 读取分支的分叉点变更 id。
61
55
  *
@@ -71,6 +65,78 @@ export interface FindBranchPathOptions {
71
65
  */
72
66
  const forkPointOf = (branch: RxDBBranch): number => branch.fromChangeId ?? 0;
73
67
 
68
+ /**
69
+ * 沿 `parentId` 从一条分支走到它所在树的根,返回途经的全部分支(含自己,根在最后)。
70
+ *
71
+ * 断链(`parentId` 指不到分支)与成环都是**持久化完整性错误**:调用方传进来的分支集合
72
+ * 是全量无过滤查询的结果,不存在「父分支被过滤掉了」这种正常情况。
73
+ * 曾经这两种情况都是 `break` —— 相当于把坏数据当成「已经到根」,继续算出一条通往
74
+ * 并不存在的根的完整 action 序列,调用方照单全收地写库,半条路径被应用而没有任何报错。
75
+ * 切分支失败远好过静默切错,因此这里必须终止且不产生任何 action。
76
+ *
77
+ * 从 {@link find_switch_branch_step} 的闭包里提出来成为模块级导出,是因为
78
+ * 「父链自不自洽」这一问的答案必须**全局只有一个**:`enable-migration.ts` 的
79
+ * 「分支可否完整物化」判定用的就是它(research.md R11)。留在闭包里够不着,
80
+ * 调用方只能另写一套遍历,于是迁移期与运行期会对同一个库给出两种答案。
81
+ *
82
+ * 它**够不到** {@link find_switch_branch_step} 的两处提前返回:变更 id 相同(含两边
83
+ * 都没有变更、都落在根上)时那个函数直接返回空步骤,根本走不到这里。所以要检查父链
84
+ * 完整性的调用方必须**自己**调它一次,不能指望 `find_switch_branch_step` 顺带检查。
85
+ *
86
+ * @param branch - 起点分支
87
+ * @param branchMap - 分支 id → 分支;必须是全量集合
88
+ * @returns 从 `branch` 到根的路径,`branch` 在第 0 位
89
+ * @throws {@link RxDBError} 父链成环或指向不存在的分支时
90
+ *
91
+ * @internal
92
+ */
93
+ export const find_branch_path_to_root = (
94
+ branch: RxDBBranch,
95
+ branchMap: ReadonlyMap<string, RxDBBranch>
96
+ ): RxDBBranch[] => {
97
+ const path: RxDBBranch[] = [branch];
98
+ const visited = new Set<string>([branch.id]);
99
+ let current = branch;
100
+ while (current.parentId) {
101
+ if (visited.has(current.parentId)) {
102
+ throw new RxDBError(
103
+ `Branch history is corrupt: cycle detected at branch '${current.id}' (parentId '${current.parentId}' already visited)`
104
+ );
105
+ }
106
+ const parent = branchMap.get(current.parentId);
107
+ if (!parent) {
108
+ throw new RxDBError(
109
+ `Branch history is corrupt: branch '${current.id}' references missing parent '${current.parentId}'`
110
+ );
111
+ }
112
+ visited.add(parent.id);
113
+ path.push(parent);
114
+ current = parent;
115
+ }
116
+ return path;
117
+ };
118
+
119
+ /**
120
+ * 算出树上两个节点之间的切换路径
121
+ *
122
+ * @param options - 起点分支、目标分支、两侧当前变更 id,以及**全量**分支集合
123
+ * @returns 按执行顺序排好的步骤;两侧停在同一个变更 id 时为空数组
124
+ * @throws {@link RxDBError} 父链成环或指向不存在的分支时(经 {@link find_branch_path_to_root})
125
+ *
126
+ * @remarks
127
+ * 规则四条:
128
+ *
129
+ * 1. 两个节点有共同祖先时,路径是「当前节点 → 共同祖先 → 目标节点」
130
+ * 2. 没有共同祖先时,路径是「当前节点 → 根 → 目标节点」
131
+ * 3. 路径里每个节点都要算出自己那一段的变更区间,即 `fromChangeId → toChangeId`
132
+ * 4. `changeId` 在每条分支上都递增,所以**逆向还原**的那些步骤 `fromChangeId > toChangeId`,
133
+ * 正向应用的则相反——方向由这两个数的大小关系表达,步骤上没有第二个方向字段
134
+ *
135
+ * 有两处**提前返回**会跳过父链遍历——变更 id 相同时返回空步骤,同分支不同变更 id 时返回
136
+ * 单步——因此本函数不是父链完整性的检查点,理由见 {@link find_branch_path_to_root}。
137
+ *
138
+ * @internal
139
+ */
74
140
  export const find_switch_branch_step = (options: FindBranchPathOptions): SwitchBranchStep[] => {
75
141
  const { branches, currentBranch, nextBranch } = options;
76
142
 
@@ -101,39 +167,9 @@ export const find_switch_branch_step = (options: FindBranchPathOptions): SwitchB
101
167
  branchMap.set(branch.id, branch);
102
168
  }
103
169
 
104
- // 获取从某个分支到根的路径
105
- //
106
- // 断链(parentId 指不到分支)与成环都是**持久化完整性错误**:调用方传进来的 `branches`
107
- // 是全量无过滤查询的结果,不存在「父分支被过滤掉了」这种正常情况。
108
- // 曾经这两种情况都是 `break` —— 相当于把坏数据当成「已经到根」,继续算出一条通往
109
- // 并不存在的根的完整 action 序列,调用方照单全收地写库,半条路径被应用而没有任何报错。
110
- // 切分支失败远好过静默切错,因此这里必须终止且不产生任何 action。
111
- const getPathToRoot = (branch: RxDBBranch): RxDBBranch[] => {
112
- const path: RxDBBranch[] = [branch];
113
- const visited = new Set<string>([branch.id]);
114
- let current = branch;
115
- while (current.parentId) {
116
- if (visited.has(current.parentId)) {
117
- throw new RxDBError(
118
- `Branch history is corrupt: cycle detected at branch '${current.id}' (parentId '${current.parentId}' already visited)`
119
- );
120
- }
121
- const parent = branchMap.get(current.parentId);
122
- if (!parent) {
123
- throw new RxDBError(
124
- `Branch history is corrupt: branch '${current.id}' references missing parent '${current.parentId}'`
125
- );
126
- }
127
- visited.add(parent.id);
128
- path.push(parent);
129
- current = parent;
130
- }
131
- return path;
132
- };
133
-
134
170
  // 获取当前分支和目标分支到根的路径
135
- const currentPath = getPathToRoot(currentBranch);
136
- const nextPath = getPathToRoot(nextBranch);
171
+ const currentPath = find_branch_path_to_root(currentBranch, branchMap);
172
+ const nextPath = find_branch_path_to_root(nextBranch, branchMap);
137
173
 
138
174
  // 找到最近的共同祖先
139
175
  let commonAncestor: RxDBBranch | null = null;
package/src/index.ts CHANGED
@@ -44,3 +44,18 @@ export { isIgnorableDetachedVersionEventError } from './detached-event-error.js'
44
44
  export { PushInFlightRegistry } from './push-inflight.js';
45
45
  export type { PushInFlightSession } from './push-inflight.js';
46
46
  export type { SyncHistoryBridge } from './sync-history-bridge.js';
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // 以下六项只为 `@aiao/rxdb-plugin-working-tree` 而导出,全部标了 `@internal`:
50
+ // 工作树插件的 commit 迁移(`commit/enable-migration.ts`)要在放行一条分支之前,
51
+ // 先按 `switchBranch` 的同一口径问两件事——「父链自不自洽」与「这条分支现在停在哪」;
52
+ // 它的物化屏障(`working-tree/materialize-branch.ts`)要在切换事务里按同一口径撤掉
53
+ // 来源分支的投影,手上只有事务执行器的仓库。另写一条同义查询,迟早会在「回滚标记算不算」
54
+ // 或「排序列是哪一个」上与切换路径分叉,表现为迁移放行了一条 `switchBranch` 走不通的分支
55
+ // (research.md R11),或物化后留下一截来源分支的数据。
56
+ // 两包之间只有 `dist` 一条路,没有深路径可走,所以从桶里出。应用代码不该引用它们。
57
+ // ---------------------------------------------------------------------------
58
+ export { find_branch_path_to_root, find_switch_branch_step } from './find-switch-branch-step.js';
59
+ export type { SwitchBranchStep } from './find-switch-branch-step.js';
60
+ export { compute_switch_branch_actions, get_branch_max_change } from './switch-branch-actions.js';
61
+ export type { SwitchBranchActionReaders } from './switch-branch-actions.js';
@@ -1,4 +1,12 @@
1
- import { getRxDBChangeKey, MergeBranchOptions, MergeBranchResult, RxDBError, SwitchVersionActions } from '@aiao/rxdb';
1
+ import {
2
+ declareTrustedWrite,
3
+ getRxDBChangeKey,
4
+ MergeBranchOptions,
5
+ MergeBranchResult,
6
+ RxDBError,
7
+ SwitchVersionActions,
8
+ TrustedWriteIntent
9
+ } from '@aiao/rxdb';
2
10
  import { VersionManager } from './VersionManager.js';
3
11
  import { remove_branch } from './remove-branch.js';
4
12
  import { get_switch_version_actions } from './switch-branch-actions.js';
@@ -122,6 +130,12 @@ export const merge_branch = async (
122
130
  } else if (change.type === 'DELETE') {
123
131
  singleActions.deletes.set(key, { patch: null, inversePatch: change.inversePatch! });
124
132
  }
133
+ // 声明写在循环体内:声明是取用即清除的,提到循环外只有第一条变更带得上身份。
134
+ declareTrustedWrite(executor, {
135
+ file: 'merge-branch.ts',
136
+ symbol: 'merge_branch',
137
+ intent: TrustedWriteIntent.merge_per_change
138
+ });
125
139
  await executor.mergeChanges(singleActions, undefined, false);
126
140
  }
127
141
  });
@@ -146,7 +160,32 @@ export const merge_branch = async (
146
160
 
147
161
  // 应用变更到当前分支的实体表
148
162
  // disableTriggers=false:让数据库触发器自动生成目标分支的 RxDBChange 记录
149
- await adapter.mergeChanges(actions, undefined, false);
163
+ //
164
+ // 压缩只有一次写,本来不需要事务。开事务是为了**拿到一个执行器当声明的作用域**:
165
+ // 声明存在一个 WeakMap 里,每个作用域只存一条(`trusted-write-scope.ts`),而工作树的
166
+ // `interceptMergeChanges()` 是排队拿到事务之后才取声明的(`capture-hook.ts`)。绑在
167
+ // 适配器实例上时,两个并发的适配器级写会互相覆盖:先执行的取到后声明者的意图,
168
+ // 后执行的取不到声明被当作未知入口拒绝(`__tests__/trusted-write-concurrency.spec.ts`)。
169
+ // 执行器是「这一次写」独有的对象,于是并发与否都不会串台。
170
+ //
171
+ // 与逐条出口同一个符号、不同意图:登记表把它们分成两行,因为压缩与逐条的判定不同,
172
+ // 合成一行会让其中一条策略失去登记。
173
+ //
174
+ // 第二个参数(transactionLog)显式传 false:这一层只借执行器当声明作用域,不该把合并记成
175
+ // 一次事务化的历史条目。默认 true 会为本次事务启用一个 transactionId(SQLite 系后端按分支
176
+ // 重建全部触发器,PGlite 设一个事务局部变量),写出的每条 change 行都盖上它,
177
+ // `history-item-builder.ts` 的 `type = transactionId ? 'TRANSACTION' : first_change.type`
178
+ // 随即把 squash 产生的多条 change 折叠成一条 `'TRANSACTION'`——undo 粒度从「按实体」
179
+ // 变成「整次合并一起撤销」。传 false,历史记账与不包这层事务时一致(调用契约锁在
180
+ // `__tests__/merge-branch.spec.ts`,分组结果锁在 pglite 的 `version/merge_branch.spec.ts`)。
181
+ await adapter.transaction(async executor => {
182
+ declareTrustedWrite(executor, {
183
+ file: 'merge-branch.ts',
184
+ symbol: 'merge_branch',
185
+ intent: TrustedWriteIntent.merge_squash
186
+ });
187
+ await executor.mergeChanges(actions, undefined, false);
188
+ }, false);
150
189
  }
151
190
 
152
191
  return toResult(merged, await doDeleteSource());
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type LocalRxDBBranchRepository,
3
3
  type LocalRxDBChangeRepository,
4
+ MAIN_BRANCH_ID,
4
5
  RxDBBranch,
5
6
  RxDBChange,
6
7
  RxDBError
@@ -23,7 +24,7 @@ import { VersionManager } from './VersionManager.js';
23
24
  export const remove_branch = async (version: VersionManager, branchId: string) => {
24
25
  const { adapter } = await version.getLocalRepositories();
25
26
  // 检查是否为主分支
26
- if (branchId === 'main') {
27
+ if (branchId === MAIN_BRANCH_ID) {
27
28
  throw new RxDBError(`Cannot remove main branch`);
28
29
  }
29
30
 
@@ -79,6 +80,16 @@ export const remove_branch = async (version: VersionManager, branchId: string) =
79
80
  rules: [{ field: 'branchId', operator: '=', value: branchId }]
80
81
  }
81
82
  });
83
+ // 贡献方挂在这条分支名下的行清在**这一个**事务里,且排在分支行自己被删之前:
84
+ // 分处两个事务的话,中间失败留下的是一条「分支没了、贡献行还在」的残留,而那些行按 id
85
+ // 挂靠——同名重建出来的新分支会逐字命中它们,带着上一条分支的 HEAD 与未提交条目出生。
86
+ //
87
+ // 串行而非 `Promise.all`,与 `create_branch` 同理:`executor` 是一条并发度为 1 的队列,
88
+ // 并行发起只会让删除顺序取决于各贡献方内部 await 的排布,出问题时复现不出来。
89
+ for (const contribution of version.rxdb.systemContributions) {
90
+ await contribution.removeBranchRows({ executor, branchId });
91
+ }
92
+
82
93
  await executor.removeMany([...branchChanges, branch]);
83
94
  return undefined;
84
95
  });
@@ -5,7 +5,14 @@
5
5
  * `create_branch` / `merge_branch` 的「接收 VersionManager 实例」模式。
6
6
  */
7
7
 
8
- import { EntityType, getEntityMetadata, RestoreEntityOptions, RxDBError } from '@aiao/rxdb';
8
+ import {
9
+ declareTrustedWrite,
10
+ EntityType,
11
+ getEntityMetadata,
12
+ RestoreEntityOptions,
13
+ RxDBError,
14
+ TrustedWriteIntent
15
+ } from '@aiao/rxdb';
9
16
  import { get_switch_version_actions } from './switch-branch-actions.js';
10
17
  import type { VersionManager } from './VersionManager.js';
11
18
 
@@ -75,7 +82,27 @@ export async function restore_entity<T extends EntityType>(
75
82
  // 远端永远停在「已删除」,本地与远端静默分叉。
76
83
  // 改走 mergeChanges(actions, undefined, false):与 merge_branch 的 squash 出口同路,
77
84
  // disableTriggers=false 让数据库触发器照常记账。
78
- await adapter.mergeChanges(actions, undefined, false);
85
+ // 恢复是重算领域状态,不是一次新的用户编辑。
86
+ //
87
+ // 只有一次写,本来不需要事务。开事务是为了**拿到一个执行器当声明的作用域**:声明存在一个
88
+ // WeakMap 里,每个作用域只存一条(`trusted-write-scope.ts`),而工作树的
89
+ // `interceptMergeChanges()` 是排队拿到事务之后才取声明的(`capture-hook.ts`)。绑在适配器
90
+ // 实例上时,一次恢复与一次并发的压缩合并会互相覆盖:先执行的取到后声明者的意图,
91
+ // 后执行的取不到声明被当作未知入口拒绝(`merge-branch.ts` 的压缩出口同理,两边的并发用例在
92
+ // `__tests__/trusted-write-concurrency.spec.ts`)。执行器是「这一次写」独有的对象。
93
+ //
94
+ // 第二个参数(transactionLog)显式传 false,理由同 `merge-branch.ts` 的压缩出口:这一层只借
95
+ // 执行器当声明作用域。默认 true 会给这次写出的 change 行盖上 transactionId,
96
+ // `history-item-builder.ts` 于是把这条恢复显示成 `'TRANSACTION'` 而不是它自己的类型(如
97
+ // INSERT)——TSDoc 承诺的是一条普通的 RxDBChange 记录,不是被特殊分组的事务记录。
98
+ await adapter.transaction(async executor => {
99
+ declareTrustedWrite(executor, {
100
+ file: 'restore-entity.ts',
101
+ symbol: 'restore_entity',
102
+ intent: TrustedWriteIntent.restore_entity
103
+ });
104
+ await executor.mergeChanges(actions, undefined, false);
105
+ }, false);
79
106
 
80
107
  const repo = adapter.getRepository(EntityType);
81
108
  const restored = await repo.find({
@@ -112,8 +112,9 @@ const describeOutsiders = (histories: readonly HistoryItem[], scope: HistoryScop
112
112
  *
113
113
  * @remarks
114
114
  * 拿到本错误说明选中的历史项里有事务同时改了作用域内外的数据。作用域**不会**替你决定
115
- * 是牺牲原子性还是越权撤销,改用不带参数的 `rxdb.versionManager.history()`
116
- * (database 作用域)可以完整撤销这个事务。`historyManager` 是私有字段,够不到。
115
+ * 是牺牲原子性还是越权撤销:要完整撤销这个事务,改用不带参数的
116
+ * `rxdb.versionManager.history()`(database 作用域)重新取一次历史再 undo。
117
+ * `VersionManager.historyManager` 是实现细节,不是绕开本错误的出口。
117
118
  */
118
119
  export class RxDBCrossScopeTransactionError extends RxDBError {
119
120
  constructor(
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  EntityStaticType,
3
3
  getRxDBChangeKey,
4
- LocalRxDBChangeRepository,
4
+ RxDBBranch,
5
5
  RxDBChange,
6
6
  RxDBError,
7
7
  SwitchVersionActions
@@ -14,8 +14,53 @@ import { VersionManager } from './VersionManager.js';
14
14
  * @param branchId 要切换到的分支ID
15
15
  * @returns 返回切换分支所需的操作序列
16
16
  */
17
- export const switch_branch_actions = async (version: VersionManager, branchId: string) => {
18
- const { branchRepository, changeRepository } = await version.getLocalRepositories();
17
+ export const switch_branch_actions = async (version: VersionManager, branchId: string) =>
18
+ compute_switch_branch_actions(await version.getLocalRepositories(), branchId);
19
+
20
+ /**
21
+ * 算切换分支所需操作要用到的最小仓库能力。
22
+ *
23
+ * @remarks
24
+ * 与 {@link BranchChangeReader} 同一个理由写成结构化接口:本地仓库与事务执行器给出的
25
+ * `IRepository` 都接得住,两边的 `find` 入参本就是同一个类型。
26
+ */
27
+ export interface SwitchBranchActionReaders {
28
+ /** 读分支行 */
29
+ readonly branchRepository: {
30
+ /**
31
+ * 按条件查分支
32
+ *
33
+ * @param options - 查询选项
34
+ * @returns 命中的分支行
35
+ */
36
+ find(options: EntityStaticType<typeof RxDBBranch, 'findOptions'>): Promise<RxDBBranch[]>;
37
+ };
38
+
39
+ /** 读变更行;见 {@link BranchChangeReader} */
40
+ readonly changeRepository: BranchChangeReader;
41
+ }
42
+
43
+ /**
44
+ * 用给定的仓库算「从当前 active 分支切到 `branchId`」要落的增删改——
45
+ * {@link switch_branch_actions} 的本体。
46
+ *
47
+ * @param readers - 见 {@link SwitchBranchActionReaders}
48
+ * @param branchId - 要切换到的分支 id
49
+ * @returns 切换分支所需的操作序列
50
+ *
51
+ * @remarks
52
+ * 拆出来是给 `@aiao/rxdb-plugin-working-tree` 的物化屏障用的:屏障要先把来源分支的投影撤掉,
53
+ * 再铺目标分支的快照,而它跑在 `switchBranch` 的 `prepare` 里,手上只有事务执行器的仓库——
54
+ * 绑在适配器上的本地仓库会在同一条连接上等屏障自己那笔事务,死锁。另写一份同义计算,
55
+ * 迟早会在「回滚标记算不算」上与普通切换分叉。
56
+ *
57
+ * @internal
58
+ */
59
+ export const compute_switch_branch_actions = async (
60
+ readers: SwitchBranchActionReaders,
61
+ branchId: string
62
+ ): Promise<SwitchVersionActions> => {
63
+ const { branchRepository, changeRepository } = readers;
19
64
  const current_branch = (
20
65
  await branchRepository.find({
21
66
  where: {
@@ -222,7 +267,43 @@ export const get_switch_version_actions = (
222
267
  return actions;
223
268
  };
224
269
 
225
- const get_branch_max_change = async (changeRepository: LocalRxDBChangeRepository, branchId: string) => {
270
+ /**
271
+ * 读一条分支 tip 所需要的最小仓库能力。
272
+ *
273
+ * @remarks
274
+ * 写成结构化的最小接口而不是直接用本地仓库类型,是为了让 {@link get_branch_max_change}
275
+ * 同时接得住事务执行器给出的 `IRepository<typeof RxDBChange>`——`enable-migration.ts`
276
+ * 跑在事务里,拿不到本地仓库。两边的 `find` 入参本就是同一个类型
277
+ * (`RxDBChangeStaticTypes['findOptions']`),所以这不是放宽,只是把已有的共同点写出来。
278
+ */
279
+ export interface BranchChangeReader {
280
+ /**
281
+ * 按条件查变更
282
+ *
283
+ * @param options - 查询选项
284
+ * @returns 命中的变更行
285
+ */
286
+ find(options: EntityStaticType<typeof RxDBChange, 'findOptions'>): Promise<RxDBChange[]>;
287
+ }
288
+
289
+ /**
290
+ * 读一条分支当前的 tip:它自己那些未被回滚的变更里 id 最大的那条。
291
+ *
292
+ * @param changeRepository - 见 {@link BranchChangeReader}
293
+ * @param branchId - 分支 id
294
+ * @returns tip 变更行;该分支一条自有变更都没有时为 `undefined`(调用方据此退回分叉点)
295
+ *
296
+ * @remarks
297
+ * 导出是给 `enable-migration.ts` 复用的:「一条分支现在停在哪」这一问必须全局只有一个
298
+ * 答案(research.md R11)。迁移期另写一条同义查询,迟早会在「回滚标记算不算」
299
+ * 或「排序列是哪一个」上与切换路径分叉,表现为迁移放行了一条 `switchBranch` 走不通的分支。
300
+ *
301
+ * @internal
302
+ */
303
+ export const get_branch_max_change = async (
304
+ changeRepository: BranchChangeReader,
305
+ branchId: string
306
+ ): Promise<RxDBChange | undefined> => {
226
307
  const changes = await changeRepository.find({
227
308
  where: {
228
309
  combinator: 'and',
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  buildPushableRepositoryRules,
3
+ declareTrustedWrite,
3
4
  type FindOptions,
4
5
  type HistoryItem,
5
6
  type IRepository,
@@ -11,7 +12,9 @@ import {
11
12
  type RxDBChangeRuleGroup,
12
13
  RxDBSync,
13
14
  type RxDBSyncOrderByField,
14
- type RxDBSyncRuleGroup
15
+ type RxDBSyncRuleGroup,
16
+ SKIP_BRANCH_SWITCH_PREPARE,
17
+ TrustedWriteIntent
15
18
  } from '@aiao/rxdb';
16
19
  import { BehaviorSubject, firstValueFrom, Subject } from 'rxjs';
17
20
  import { buildLastPushedMap } from './history-filters.js';
@@ -165,9 +168,16 @@ export async function applyUndoRedoHistories(
165
168
  if (operation === 'undo') {
166
169
  if (undoSession === undefined || !host.isUndoSessionCurrent(undoSession)) return;
167
170
  }
171
+ // undo/redo 是用户可感知的编辑,矩阵行 6 要求它**产生**单元(origin='undo_redo')。
172
+ declareTrustedWrite(adapter, {
173
+ file: 'undo-redo-apply.ts',
174
+ symbol: 'applyUndoRedoHistories',
175
+ intent: TrustedWriteIntent.undo_redo
176
+ });
168
177
  // 不传 branchId:回放只作用于当前分支。在事务外采样分支 id 会让这次写入在并发切换时
169
178
  // 把 activated 与全部触发器倒回旧分支(见 SwitchBranchOptions.branchId)。
170
- await adapter.switchBranch({ actions });
179
+ // 分支一动不动,所以没有前置条件可校验——这里的豁免是一句「我确实不需要」,不是漏传。
180
+ await adapter.switchBranch({ actions, prepare: SKIP_BRANCH_SWITCH_PREPARE });
171
181
 
172
182
  host.pushableCountTrigger$.next(Date.now());
173
183
  host.setRevertStateWatermarks(changes, operation === 'undo', stateUpdatedAt);
@@ -226,7 +236,7 @@ export async function updatePushableCount(host: UndoRedoApplyHost): Promise<void
226
236
  }
227
237
  });
228
238
 
229
- const repoRules = buildPushableRepositoryRules(host.rxdb.config.entities, host.rxdb.config.sync, repoSyncs);
239
+ const repoRules = buildPushableRepositoryRules(host.rxdb.config.entities, host.rxdb.entitySync, repoSyncs);
230
240
  if (repoRules.length === 0) {
231
241
  publish(0);
232
242
  return;