@aiao/rxdb-plugin-history 0.0.0-stage → 0.0.26
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.
- package/LICENSE +21 -0
- package/README.md +70 -2
- package/dist/HistoryManager.d.ts +79 -0
- package/dist/HistoryManager.d.ts.map +1 -0
- package/dist/VersionManager.d.ts +233 -0
- package/dist/VersionManager.d.ts.map +1 -0
- package/dist/create-branch.d.ts +29 -0
- package/dist/create-branch.d.ts.map +1 -0
- package/dist/detached-event-error.d.ts +28 -0
- package/dist/detached-event-error.d.ts.map +1 -0
- package/dist/find-switch-branch-step.d.ts +45 -0
- package/dist/find-switch-branch-step.d.ts.map +1 -0
- package/dist/history-filters.d.ts +78 -0
- package/dist/history-filters.d.ts.map +1 -0
- package/dist/history-item-builder.d.ts +25 -0
- package/dist/history-item-builder.d.ts.map +1 -0
- package/dist/history-scope-api.d.ts +41 -0
- package/dist/history-scope-api.d.ts.map +1 -0
- package/dist/history-undo-session.types.d.ts +32 -0
- package/dist/history-undo-session.types.d.ts.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1480 -0
- package/dist/merge-branch.d.ts +22 -0
- package/dist/merge-branch.d.ts.map +1 -0
- package/dist/plugin.d.ts +58 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/pullable-count.d.ts +37 -0
- package/dist/pullable-count.d.ts.map +1 -0
- package/dist/push-inflight.d.ts +60 -0
- package/dist/push-inflight.d.ts.map +1 -0
- package/dist/redo-stack.d.ts +56 -0
- package/dist/redo-stack.d.ts.map +1 -0
- package/dist/remove-branch.d.ts +16 -0
- package/dist/remove-branch.d.ts.map +1 -0
- package/dist/restore-entity.d.ts +15 -0
- package/dist/restore-entity.d.ts.map +1 -0
- package/dist/scope-selection.d.ts +55 -0
- package/dist/scope-selection.d.ts.map +1 -0
- package/dist/switch-branch-actions.d.ts +16 -0
- package/dist/switch-branch-actions.d.ts.map +1 -0
- package/dist/sync-history-bridge.d.ts +87 -0
- package/dist/sync-history-bridge.d.ts.map +1 -0
- package/dist/undo-redo-apply.d.ts +47 -0
- package/dist/undo-redo-apply.d.ts.map +1 -0
- package/dist/version-manager.utils.d.ts +30 -0
- package/dist/version-manager.utils.d.ts.map +1 -0
- package/package.json +57 -3
- package/src/HistoryManager.ts +770 -0
- package/src/VersionManager.ts +451 -0
- package/src/create-branch.ts +154 -0
- package/src/detached-event-error.ts +43 -0
- package/src/find-switch-branch-step.ts +277 -0
- package/src/history-filters.ts +157 -0
- package/src/history-item-builder.ts +101 -0
- package/src/history-scope-api.ts +189 -0
- package/src/history-undo-session.types.ts +37 -0
- package/src/index.ts +46 -0
- package/src/merge-branch.ts +153 -0
- package/src/plugin.ts +93 -0
- package/src/pullable-count.ts +67 -0
- package/src/push-inflight.ts +91 -0
- package/src/redo-stack.ts +79 -0
- package/src/remove-branch.ts +89 -0
- package/src/restore-entity.ts +97 -0
- package/src/scope-selection.ts +132 -0
- package/src/switch-branch-actions.ts +238 -0
- package/src/sync-history-bridge.ts +131 -0
- package/src/undo-redo-apply.ts +260 -0
- package/src/version-manager.utils.ts +43 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { HistoryItem } from '@aiao/rxdb';
|
|
2
|
+
import { BehaviorSubject, Observable } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Redo 栈容量上限(会话内)
|
|
6
|
+
*
|
|
7
|
+
* 超过则丢弃最旧的项(尾部),避免长会话下无界增长。
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_REDO_STACK_SIZE = 1000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 会话级 Redo 栈
|
|
13
|
+
*
|
|
14
|
+
* 从 HistoryManager 抽离的 redo 栈管理子模块:
|
|
15
|
+
* - 持有内存 `BehaviorSubject<HistoryItem[]>`(不持久化)
|
|
16
|
+
* - 新项总在头部(push 后第一个)
|
|
17
|
+
* - 容量上限 `MAX_REDO_STACK_SIZE`
|
|
18
|
+
*
|
|
19
|
+
* 设计理由:
|
|
20
|
+
* - HistoryManager 原本 1120 行,redo 栈状态 + 操作占 60+ 行且与 history 查询/undo 流程正交
|
|
21
|
+
* - 独立后可单测,并避免泄漏 BehaviorSubject 引用
|
|
22
|
+
*
|
|
23
|
+
* @internal 由 HistoryManager 持有;外部通过 HistoryManager 公共 API 访问
|
|
24
|
+
*/
|
|
25
|
+
export class RedoStack {
|
|
26
|
+
readonly #subject = new BehaviorSubject<HistoryItem[]>([]);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 当前 redo 栈的可观察流(新项在前)
|
|
30
|
+
*/
|
|
31
|
+
get items$(): Observable<HistoryItem[]> {
|
|
32
|
+
return this.#subject.asObservable();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 当前栈快照
|
|
37
|
+
*/
|
|
38
|
+
get value(): HistoryItem[] {
|
|
39
|
+
return this.#subject.value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 将一组历史项压入栈顶(items 在前)
|
|
44
|
+
*
|
|
45
|
+
* 超过 MAX_REDO_STACK_SIZE 时丢弃尾部最旧项。
|
|
46
|
+
*/
|
|
47
|
+
push(items: HistoryItem[]): void {
|
|
48
|
+
const merged = [...items, ...this.#subject.value];
|
|
49
|
+
const capped = merged.length > MAX_REDO_STACK_SIZE ? merged.slice(0, MAX_REDO_STACK_SIZE) : merged;
|
|
50
|
+
this.#subject.next(capped);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 按稳定身份(`fingerprint`)移除已应用的项。
|
|
55
|
+
*
|
|
56
|
+
* @param items - 已经 redo 完成、需要出栈的项
|
|
57
|
+
* @returns 实际被移除的项(按栈中原顺序);不在栈中的项被忽略
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* **不能用「移除 N 项」代替身份匹配**:作用域 redo 会先按 scope 筛出本作用域的项再应用,
|
|
61
|
+
* 这些项未必位于栈顶。按数量截栈顶会删掉栈头那些**未被 redo** 的项(它们从此丢失),
|
|
62
|
+
* 而真正已 redo 的项仍留在栈里可以被重复执行。
|
|
63
|
+
*/
|
|
64
|
+
remove(items: HistoryItem[]): HistoryItem[] {
|
|
65
|
+
const targets = new Set(items.map(item => item.fingerprint));
|
|
66
|
+
const current = this.#subject.value;
|
|
67
|
+
const removed = current.filter(item => targets.has(item.fingerprint));
|
|
68
|
+
if (removed.length === 0) return removed;
|
|
69
|
+
this.#subject.next(current.filter(item => !targets.has(item.fingerprint)));
|
|
70
|
+
return removed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 清空栈
|
|
75
|
+
*/
|
|
76
|
+
clear(): void {
|
|
77
|
+
this.#subject.next([]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type LocalRxDBBranchRepository,
|
|
3
|
+
type LocalRxDBChangeRepository,
|
|
4
|
+
RxDBBranch,
|
|
5
|
+
RxDBChange,
|
|
6
|
+
RxDBError
|
|
7
|
+
} from '@aiao/rxdb';
|
|
8
|
+
import { VersionManager } from './VersionManager.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 移除一个分支
|
|
12
|
+
* @param version
|
|
13
|
+
* @param branchId
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* 「查子分支 → 查 change → 删」整段必须在一个事务窗口内完成。
|
|
17
|
+
* 拆开之后,检查与删除之间新建的子分支会漏判,留下 `parentId` 指向不存在分支的孤儿,
|
|
18
|
+
* 而孤儿又会被切分支时的「父节点缺失即当作到根」静默吞掉,损坏无声。
|
|
19
|
+
*
|
|
20
|
+
* 事务体内一律用 `executor.getRepository(...)`:绑在适配器上的仓库会重新入队,
|
|
21
|
+
* 排在自己这个事务后面(队列并发度 1),永久挂起。
|
|
22
|
+
*/
|
|
23
|
+
export const remove_branch = async (version: VersionManager, branchId: string) => {
|
|
24
|
+
const { adapter } = await version.getLocalRepositories();
|
|
25
|
+
// 检查是否为主分支
|
|
26
|
+
if (branchId === 'main') {
|
|
27
|
+
throw new RxDBError(`Cannot remove main branch`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 校验失败**返回**而不是就地抛出:抛出会被适配器的事务包装层裹成适配器错误类型
|
|
31
|
+
// (`RxDBAdapterSqliteError` 等),调用方拿到的异常身份就跟着实现细节变了。
|
|
32
|
+
// 校验失败时本事务一行都没写,回不回滚无所谓,留到事务外抛更稳。
|
|
33
|
+
const rejection = await adapter.transaction(async executor => {
|
|
34
|
+
const branchRepository = executor.getRepository(RxDBBranch) as unknown as LocalRxDBBranchRepository;
|
|
35
|
+
const changeRepository = executor.getRepository(RxDBChange) as unknown as LocalRxDBChangeRepository;
|
|
36
|
+
|
|
37
|
+
// 检查分支是否存在
|
|
38
|
+
const branch = (
|
|
39
|
+
await branchRepository.find({
|
|
40
|
+
where: {
|
|
41
|
+
combinator: 'and',
|
|
42
|
+
rules: [{ field: 'id', operator: '=', value: branchId }]
|
|
43
|
+
},
|
|
44
|
+
limit: 1
|
|
45
|
+
})
|
|
46
|
+
)[0];
|
|
47
|
+
|
|
48
|
+
if (!branch) {
|
|
49
|
+
return `Branch '${branchId}' not found`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 检查分支是否为激活状态
|
|
53
|
+
if (branch.activated) {
|
|
54
|
+
return `Cannot remove active branch '${branchId}'. Switch to another branch first.`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 检查是否有子分支依赖于该分支
|
|
58
|
+
//
|
|
59
|
+
// 必须直接查 `parentId`:拿「父分支有哪些 change、子分支的 fromChangeId 是否落在其中」
|
|
60
|
+
// 反推拓扑有两个漏洞 —— 父分支一条 change 都没有时整段检查被跳过;子分支的 fromChangeId
|
|
61
|
+
// 指向的 change 若已被清理也会漏判。两者都留下 `parentId` 指向不存在分支的孤儿。
|
|
62
|
+
const childBranches = await branchRepository.find({
|
|
63
|
+
where: {
|
|
64
|
+
combinator: 'and',
|
|
65
|
+
rules: [
|
|
66
|
+
{ field: 'parentId', operator: '=', value: branchId },
|
|
67
|
+
{ field: 'id', operator: '!=', value: branchId }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
limit: 1
|
|
71
|
+
});
|
|
72
|
+
if (childBranches?.length) {
|
|
73
|
+
return `Cannot remove branch '${branchId}' because it has child branches`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const branchChanges = await changeRepository.find({
|
|
77
|
+
where: {
|
|
78
|
+
combinator: 'and',
|
|
79
|
+
rules: [{ field: 'branchId', operator: '=', value: branchId }]
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
await executor.removeMany([...branchChanges, branch]);
|
|
83
|
+
return undefined;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (rejection) {
|
|
87
|
+
throw new RxDBError(rejection);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 恢复被删除实体的纯逻辑
|
|
3
|
+
*
|
|
4
|
+
* 从 {@link VersionManager.restoreEntity} 抽出的独立实现,遵循
|
|
5
|
+
* `create_branch` / `merge_branch` 的「接收 VersionManager 实例」模式。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EntityType, getEntityMetadata, RestoreEntityOptions, RxDBError } from '@aiao/rxdb';
|
|
9
|
+
import { get_switch_version_actions } from './switch-branch-actions.js';
|
|
10
|
+
import type { VersionManager } from './VersionManager.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 恢复被删除的实体。
|
|
14
|
+
*
|
|
15
|
+
* 根据 RxDBChange 记录中的 inversePatch 重新插入实体,
|
|
16
|
+
* 恢复操作本身会生成新的 RxDBChange 记录(可被 push 到远程)。
|
|
17
|
+
*
|
|
18
|
+
* @param vm - 版本管理器实例
|
|
19
|
+
* @param entity - 被删除的实体实例(需要包含 metadata 信息)
|
|
20
|
+
* @param options - 恢复选项,包含 changeId(DELETE 类型的 RxDBChange 记录 ID)
|
|
21
|
+
* @returns 恢复后的实体实例
|
|
22
|
+
*/
|
|
23
|
+
export async function restore_entity<T extends EntityType>(
|
|
24
|
+
vm: VersionManager,
|
|
25
|
+
entity: InstanceType<T>,
|
|
26
|
+
options: RestoreEntityOptions
|
|
27
|
+
): Promise<InstanceType<T>> {
|
|
28
|
+
const { changeRepository, adapter } = await vm.getLocalRepositories();
|
|
29
|
+
|
|
30
|
+
const changes = await changeRepository.find({
|
|
31
|
+
where: {
|
|
32
|
+
combinator: 'and',
|
|
33
|
+
rules: [{ field: 'id', operator: '=', value: Number(options.changeId) }]
|
|
34
|
+
},
|
|
35
|
+
limit: 1
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (changes.length === 0) {
|
|
39
|
+
throw new RxDBError(`RxDBChange not found: ${options.changeId}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const change = changes[0];
|
|
43
|
+
|
|
44
|
+
if (change.type !== 'DELETE') {
|
|
45
|
+
throw new RxDBError(`Cannot restore from non-DELETE change (type=${change.type})`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!change.inversePatch) {
|
|
49
|
+
throw new RxDBError(`RxDBChange ${options.changeId} has no inversePatch`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const EntityType = entity.constructor as T;
|
|
53
|
+
const metadata = getEntityMetadata(EntityType);
|
|
54
|
+
const currentBranch = await vm.getCurrentBranch();
|
|
55
|
+
|
|
56
|
+
// 身份校验必须在写入之前:只校验 changeId/type/inversePatch 时,传 A 的实体配 B 的 changeId
|
|
57
|
+
// 会真的把 B 恢复出来,再用 A 的 constructor 去查 —— 返回 undefined,而返回类型声明是非空的
|
|
58
|
+
// InstanceType<T>。分支同理:别的分支的 change 不能应用到当前分支。
|
|
59
|
+
if (change.namespace !== metadata.namespace || change.entity !== metadata.name) {
|
|
60
|
+
throw new RxDBError(
|
|
61
|
+
`RxDBChange ${options.changeId} belongs to ${change.namespace}:${change.entity}, ` +
|
|
62
|
+
`not ${metadata.namespace}:${metadata.name}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (change.branchId != null && change.branchId !== currentBranch.id) {
|
|
66
|
+
throw new RxDBError(
|
|
67
|
+
`RxDBChange ${options.changeId} belongs to branch '${change.branchId}', current branch is '${currentBranch.id}'`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const actions = get_switch_version_actions([change], false);
|
|
72
|
+
// 不能走 switchBranch:各适配器的 switch_branch 第一步就是 remove_all_triggers_sql
|
|
73
|
+
// (见 sqlite-core / pglite 的 version/switch_branch.ts),恢复出来的行不会产生任何 change 行——
|
|
74
|
+
// 本函数的 TSDoc 说「恢复操作本身会生成新的 RxDBChange 记录(可被 push 到远程)」直接落空,
|
|
75
|
+
// 远端永远停在「已删除」,本地与远端静默分叉。
|
|
76
|
+
// 改走 mergeChanges(actions, undefined, false):与 merge_branch 的 squash 出口同路,
|
|
77
|
+
// disableTriggers=false 让数据库触发器照常记账。
|
|
78
|
+
await adapter.mergeChanges(actions, undefined, false);
|
|
79
|
+
|
|
80
|
+
const repo = adapter.getRepository(EntityType);
|
|
81
|
+
const restored = await repo.find({
|
|
82
|
+
where: {
|
|
83
|
+
combinator: 'and',
|
|
84
|
+
rules: [{ field: 'id', operator: '=', value: change.entityId }]
|
|
85
|
+
},
|
|
86
|
+
limit: 1
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 返回类型是非空的 InstanceType<T>,恢复不出行时必须抛错而不是让 undefined 冒充实体
|
|
90
|
+
if (!restored[0]) {
|
|
91
|
+
throw new RxDBError(
|
|
92
|
+
`Restore produced no row for ${metadata.namespace}:${metadata.name} id=${String(change.entityId)}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return restored[0] as InstanceType<T>;
|
|
97
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 作用域历史选择(纯函数)
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*
|
|
6
|
+
* `history(scope).undo()` 的选择口径与展示口径**不是同一件事**:
|
|
7
|
+
*
|
|
8
|
+
* - 展示(`filterHistoriesByScope`)可以把一个事务裁成「与你有关的那几条」,
|
|
9
|
+
* 用户在 User 的历史面板上看到的就该是 User 的部分;
|
|
10
|
+
* - 执行**不能**这么裁。事务是原子的,只回滚它的一半会留下一个从未存在过的中间态
|
|
11
|
+
* (订单被撤销、订单行还在)。
|
|
12
|
+
*
|
|
13
|
+
* 因此这里把「选哪几条」和「能不能整条应用」分开:`selectScopedHistories` 只返回
|
|
14
|
+
* **完整未裁剪**的历史项,凡是触及作用域但同时改了作用域外数据的事务,一律进
|
|
15
|
+
* `crossScope` 由调用方拒绝({@link RxDBCrossScopeTransactionError})。
|
|
16
|
+
*
|
|
17
|
+
* 为什么是「拒绝」而不是「连带撤销作用域外的部分」:作用域的契约是**只影响这个作用域**。
|
|
18
|
+
* 悄悄扩大影响面,调用方无从知道自己刚刚还回滚了别的仓库;而静默跳过则是点了撤销
|
|
19
|
+
* 什么都没发生。报错是唯一一个既不越权、又不骗人的选项,且可恢复——调用方改用
|
|
20
|
+
* `history()`(database 作用域)就能完整撤销这个事务。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { HistoryItem, HistoryScope, RxDBChange, RxDBError } from '@aiao/rxdb';
|
|
24
|
+
/** 判定作用域归属只需要这三个字段 */
|
|
25
|
+
type ScopedChange = Pick<RxDBChange, 'entity' | 'entityId' | 'namespace'>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 单条变更是否落在作用域内
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* entity 作用域用 `===` 比较 id,`0` / `''` 这类假值同样是合法 id——
|
|
32
|
+
* 判空必须用 `undefined` 而不是真值判断,否则 `entityId = 0` 会退化成 repository 作用域。
|
|
33
|
+
*/
|
|
34
|
+
export function isChangeInScope(change: ScopedChange, scope: HistoryScope): boolean {
|
|
35
|
+
if (scope.type === 'database') return true;
|
|
36
|
+
if (change.namespace !== scope.namespace || change.entity !== scope.entity) return false;
|
|
37
|
+
return scope.type === 'repository' || change.entityId === scope.entityId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 历史项是否**至少有一条**变更落在作用域内(决定它是否出现在该作用域的历史里) */
|
|
41
|
+
export function historyTouchesScope(history: HistoryItem, scope: HistoryScope): boolean {
|
|
42
|
+
return history.changes.some(change => isChangeInScope(change, scope));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 历史项是否**全部**变更都落在作用域内(决定它能否被该作用域整条应用) */
|
|
46
|
+
export function isHistoryWithinScope(history: HistoryItem, scope: HistoryScope): boolean {
|
|
47
|
+
return history.changes.every(change => isChangeInScope(change, scope));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** {@link selectScopedHistories} 的结果 */
|
|
51
|
+
export interface ScopedHistorySelection {
|
|
52
|
+
/** 触及作用域、但同时改了作用域外数据的事务:不可部分应用 */
|
|
53
|
+
crossScope: HistoryItem[];
|
|
54
|
+
/** 完全落在作用域内、可以整条应用的历史项(未被裁剪) */
|
|
55
|
+
selected: HistoryItem[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 在作用域内挑出接下来 `step` 条要 undo / redo 的历史项
|
|
60
|
+
*
|
|
61
|
+
* @param histories - 候选历史(已按可撤销 / 可重做过滤,最新在前)
|
|
62
|
+
* @param scope - 作用域
|
|
63
|
+
* @param step - 取几条;非正数取 0 条
|
|
64
|
+
*
|
|
65
|
+
* @remarks
|
|
66
|
+
* `step` 数的是「触及作用域的历史项」,越界事务同样占一个名额而不是被跳过——
|
|
67
|
+
* undo 必须后进先出,跳过它去撤更早的那条会把顺序弄乱。
|
|
68
|
+
*/
|
|
69
|
+
export function selectScopedHistories(
|
|
70
|
+
histories: HistoryItem[],
|
|
71
|
+
scope: HistoryScope,
|
|
72
|
+
step: number
|
|
73
|
+
): ScopedHistorySelection {
|
|
74
|
+
const window = histories.filter(history => historyTouchesScope(history, scope)).slice(0, Math.max(0, step));
|
|
75
|
+
const crossScope: HistoryItem[] = [];
|
|
76
|
+
const selected: HistoryItem[] = [];
|
|
77
|
+
|
|
78
|
+
for (const history of window) {
|
|
79
|
+
if (isHistoryWithinScope(history, scope)) {
|
|
80
|
+
selected.push(history);
|
|
81
|
+
} else {
|
|
82
|
+
crossScope.push(history);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { crossScope, selected };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const describeScope = (scope: HistoryScope): string => {
|
|
90
|
+
if (scope.type === 'database') return 'database';
|
|
91
|
+
if (scope.type === 'repository') return `${scope.namespace}:${scope.entity}`;
|
|
92
|
+
return `${scope.namespace}:${scope.entity}#${String(scope.entityId)}`;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const describeOutsiders = (histories: readonly HistoryItem[], scope: HistoryScope): string => {
|
|
96
|
+
const outsiders = new Set<string>();
|
|
97
|
+
for (const history of histories) {
|
|
98
|
+
for (const change of history.changes) {
|
|
99
|
+
if (isChangeInScope(change, scope)) continue;
|
|
100
|
+
outsiders.add(
|
|
101
|
+
scope.type === 'entity' ?
|
|
102
|
+
`${change.namespace}:${change.entity}#${String(change.entityId)}`
|
|
103
|
+
: `${change.namespace}:${change.entity}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return [...outsiders].join(', ');
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 作用域 undo / redo 撞上跨作用域事务
|
|
112
|
+
*
|
|
113
|
+
* @remarks
|
|
114
|
+
* 拿到本错误说明选中的历史项里有事务同时改了作用域内外的数据。作用域**不会**替你决定
|
|
115
|
+
* 是牺牲原子性还是越权撤销,改用不带参数的 `rxdb.versionManager.history()`
|
|
116
|
+
* (database 作用域)可以完整撤销这个事务。`historyManager` 是私有字段,够不到。
|
|
117
|
+
*/
|
|
118
|
+
export class RxDBCrossScopeTransactionError extends RxDBError {
|
|
119
|
+
constructor(
|
|
120
|
+
/** 发起 undo / redo 的作用域 */
|
|
121
|
+
readonly scope: HistoryScope,
|
|
122
|
+
/** 越界的历史项(完整未裁剪) */
|
|
123
|
+
readonly histories: readonly HistoryItem[]
|
|
124
|
+
) {
|
|
125
|
+
super(
|
|
126
|
+
`Scoped history operation would split a transaction: scope ${describeScope(scope)} also changes ` +
|
|
127
|
+
`${describeOutsiders(histories, scope)}. Use the database scope to apply the whole transaction.`
|
|
128
|
+
);
|
|
129
|
+
this.name = 'RxDBCrossScopeTransactionError';
|
|
130
|
+
Object.setPrototypeOf(this, RxDBCrossScopeTransactionError.prototype);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityStaticType,
|
|
3
|
+
getRxDBChangeKey,
|
|
4
|
+
LocalRxDBChangeRepository,
|
|
5
|
+
RxDBChange,
|
|
6
|
+
RxDBError,
|
|
7
|
+
SwitchVersionActions
|
|
8
|
+
} from '@aiao/rxdb';
|
|
9
|
+
import { find_switch_branch_step } from './find-switch-branch-step.js';
|
|
10
|
+
import { VersionManager } from './VersionManager.js';
|
|
11
|
+
/**
|
|
12
|
+
* 计算切换分支所需的操作
|
|
13
|
+
* @param version
|
|
14
|
+
* @param branchId 要切换到的分支ID
|
|
15
|
+
* @returns 返回切换分支所需的操作序列
|
|
16
|
+
*/
|
|
17
|
+
export const switch_branch_actions = async (version: VersionManager, branchId: string) => {
|
|
18
|
+
const { branchRepository, changeRepository } = await version.getLocalRepositories();
|
|
19
|
+
const current_branch = (
|
|
20
|
+
await branchRepository.find({
|
|
21
|
+
where: {
|
|
22
|
+
combinator: 'and',
|
|
23
|
+
rules: [{ field: 'activated', operator: '=', value: true }]
|
|
24
|
+
},
|
|
25
|
+
limit: 1
|
|
26
|
+
})
|
|
27
|
+
)[0];
|
|
28
|
+
if (!current_branch) throw new RxDBError('Current branch not found');
|
|
29
|
+
const all_branches = await branchRepository.find({
|
|
30
|
+
where: {
|
|
31
|
+
combinator: 'and',
|
|
32
|
+
rules: []
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const next_branch = all_branches.find(b => b.id === branchId);
|
|
36
|
+
if (!next_branch) throw new RxDBError(`Branch (${branchId}) not found`);
|
|
37
|
+
if (current_branch.id === next_branch.id) {
|
|
38
|
+
throw new RxDBError('Cannot switch to the same branch');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const currentChange = await get_branch_max_change(changeRepository, current_branch.id);
|
|
42
|
+
const nextChange = await get_branch_max_change(changeRepository, next_branch.id);
|
|
43
|
+
|
|
44
|
+
const steps = find_switch_branch_step({
|
|
45
|
+
branches: all_branches,
|
|
46
|
+
currentBranch: current_branch,
|
|
47
|
+
currentChangeId: currentChange ? currentChange.id : null,
|
|
48
|
+
nextBranch: next_branch,
|
|
49
|
+
nextChangeId: nextChange ? nextChange.id : null
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const actions: SwitchVersionActions = {
|
|
53
|
+
deletes: new Map(),
|
|
54
|
+
updates: new Map(),
|
|
55
|
+
inserts: new Map()
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < steps.length; i++) {
|
|
59
|
+
const { branch, fromChangeId, toChangeId } = steps[i];
|
|
60
|
+
const is_patch = fromChangeId < toChangeId;
|
|
61
|
+
const is_inverse_patch = fromChangeId > toChangeId;
|
|
62
|
+
|
|
63
|
+
// 构建查询条件:查询 fromChangeId 和 toChangeId 之间的变更
|
|
64
|
+
// 注意:区间是开区间 (min, max],不包含起点但包含终点
|
|
65
|
+
// 因为起点代表的状态已经存在,只需要应用之后的变更
|
|
66
|
+
const minId = Math.min(fromChangeId, toChangeId);
|
|
67
|
+
const maxId = Math.max(fromChangeId, toChangeId);
|
|
68
|
+
|
|
69
|
+
const options: EntityStaticType<typeof RxDBChange, 'findAllOptions'> = {
|
|
70
|
+
where: {
|
|
71
|
+
combinator: 'and',
|
|
72
|
+
rules: [
|
|
73
|
+
{ field: 'branchId', operator: '=', value: branch.id },
|
|
74
|
+
{ field: 'id', operator: '>', value: minId },
|
|
75
|
+
{ field: 'id', operator: '<=', value: maxId },
|
|
76
|
+
{ field: 'revertChangeId', operator: '=', value: null }
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 对于子分支,需要额外限制:不处理 branch.fromChangeId 及之前的变更
|
|
82
|
+
// 因为这些变更属于父分支的历史,不属于当前子分支的变更范围
|
|
83
|
+
// 注意:这个限制对 is_patch 和 is_inverse_patch 都适用
|
|
84
|
+
if (branch.fromChangeId) {
|
|
85
|
+
// 已经通过 minId > branch.fromChangeId 限制过了
|
|
86
|
+
// 所以这里不需要额外处理,minId 已经确保大于 branch.fromChangeId
|
|
87
|
+
// 但为了代码清晰,明确检查一下
|
|
88
|
+
if (minId < branch.fromChangeId) {
|
|
89
|
+
options.where.rules.push({ field: 'id', operator: '>', value: branch.fromChangeId });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const changes = await changeRepository.find(options);
|
|
93
|
+
if (is_inverse_patch) {
|
|
94
|
+
inverse_patch_all(changes, actions);
|
|
95
|
+
} else if (is_patch) {
|
|
96
|
+
patch_all(changes, actions);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return actions;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 应用所有补丁
|
|
104
|
+
* @param changes 需要计算的 changes
|
|
105
|
+
* @param actions 现有的操作结果
|
|
106
|
+
*/
|
|
107
|
+
const patch_all = (changes: RxDBChange[], actions?: SwitchVersionActions) =>
|
|
108
|
+
get_switch_version_actions(changes, true, actions);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 反向应用所有补丁
|
|
112
|
+
* @param changes 需要计算的 changes
|
|
113
|
+
* @param actions 现有的操作结果
|
|
114
|
+
*/
|
|
115
|
+
const inverse_patch_all = (changes: RxDBChange[], actions?: SwitchVersionActions) =>
|
|
116
|
+
get_switch_version_actions(changes, false, actions);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 计算切换版本所需的操作
|
|
120
|
+
* @param changes 需要计算的 changes
|
|
121
|
+
* @param options
|
|
122
|
+
*/
|
|
123
|
+
export const get_switch_version_actions = (
|
|
124
|
+
changes: RxDBChange[],
|
|
125
|
+
isPatch: boolean,
|
|
126
|
+
actions: SwitchVersionActions = {
|
|
127
|
+
deletes: new Map(),
|
|
128
|
+
updates: new Map(),
|
|
129
|
+
inserts: new Map()
|
|
130
|
+
}
|
|
131
|
+
): SwitchVersionActions => {
|
|
132
|
+
if (isPatch) {
|
|
133
|
+
const need_changes = [...changes].sort((a, b) => a.id - b.id);
|
|
134
|
+
for (const change of need_changes) {
|
|
135
|
+
const changeKey = getRxDBChangeKey(change);
|
|
136
|
+
switch (change.type) {
|
|
137
|
+
case 'INSERT':
|
|
138
|
+
// 浅拷贝:actions 不得持有 change.patch 的引用,否则后续 Object.assign 合并会原地
|
|
139
|
+
// 改写源 change(可能是 EntityManager 缓存实例),污染缓存
|
|
140
|
+
actions.inserts.set(changeKey, {
|
|
141
|
+
patch: { ...change.patch! },
|
|
142
|
+
inversePatch: null
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
case 'UPDATE':
|
|
146
|
+
{
|
|
147
|
+
const existing = actions.inserts.get(changeKey) || actions.updates.get(changeKey);
|
|
148
|
+
if (existing) {
|
|
149
|
+
// 合并补丁:正向补丁累加,反向补丁保持最早的
|
|
150
|
+
if (existing.patch && change.patch) {
|
|
151
|
+
Object.assign(existing.patch, change.patch);
|
|
152
|
+
}
|
|
153
|
+
if (existing.inversePatch === null && change.inversePatch) {
|
|
154
|
+
existing.inversePatch = { ...change.inversePatch };
|
|
155
|
+
} else if (existing.inversePatch && change.inversePatch) {
|
|
156
|
+
Object.assign(existing.inversePatch, change.inversePatch);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
actions.updates.set(changeKey, {
|
|
160
|
+
patch: { ...change.patch! },
|
|
161
|
+
inversePatch: { ...change.inversePatch! }
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
case 'DELETE':
|
|
167
|
+
actions.deletes.set(changeKey, {
|
|
168
|
+
patch: null,
|
|
169
|
+
inversePatch: { ...change.inversePatch! }
|
|
170
|
+
});
|
|
171
|
+
actions.inserts.delete(changeKey);
|
|
172
|
+
actions.updates.delete(changeKey);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
const need_changes = [...changes].sort((a, b) => b.id - a.id);
|
|
178
|
+
for (const change of need_changes) {
|
|
179
|
+
const changeKey = getRxDBChangeKey(change);
|
|
180
|
+
switch (change.type) {
|
|
181
|
+
case 'INSERT':
|
|
182
|
+
// 浅拷贝的理由同正向分支:actions 不得持有 change 的补丁引用,
|
|
183
|
+
// 否则后续 Object.assign 合并会原地改写源 change(可能是 EntityManager 缓存实例)
|
|
184
|
+
actions.deletes.set(changeKey, {
|
|
185
|
+
patch: null,
|
|
186
|
+
inversePatch: { ...change.patch! }
|
|
187
|
+
});
|
|
188
|
+
actions.inserts.delete(changeKey);
|
|
189
|
+
actions.updates.delete(changeKey);
|
|
190
|
+
break;
|
|
191
|
+
case 'UPDATE':
|
|
192
|
+
{
|
|
193
|
+
// 同时查 inserts:逆向按 id 降序处理,撤销 DELETE 会先落一条 inserts,
|
|
194
|
+
// 紧接着更早的 UPDATE 必须并进那条 inserts。只查 updates 会让同一实体
|
|
195
|
+
// 同时出现在 inserts 与 updates 两张表里。
|
|
196
|
+
const existing = actions.inserts.get(changeKey) || actions.updates.get(changeKey);
|
|
197
|
+
if (existing) {
|
|
198
|
+
// 反向合并:正向补丁用 inversePatch,反向补丁用 patch
|
|
199
|
+
if (existing.patch && change.inversePatch) {
|
|
200
|
+
Object.assign(existing.patch, change.inversePatch);
|
|
201
|
+
}
|
|
202
|
+
if (existing.inversePatch && change.patch) {
|
|
203
|
+
Object.assign(existing.inversePatch, change.patch);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
actions.updates.set(changeKey, {
|
|
207
|
+
patch: { ...change.inversePatch! },
|
|
208
|
+
inversePatch: { ...change.patch! }
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
case 'DELETE':
|
|
214
|
+
actions.inserts.set(changeKey, {
|
|
215
|
+
patch: { ...change.inversePatch! },
|
|
216
|
+
inversePatch: null
|
|
217
|
+
});
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return actions;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const get_branch_max_change = async (changeRepository: LocalRxDBChangeRepository, branchId: string) => {
|
|
226
|
+
const changes = await changeRepository.find({
|
|
227
|
+
where: {
|
|
228
|
+
combinator: 'and',
|
|
229
|
+
rules: [
|
|
230
|
+
{ field: 'branchId', operator: '=', value: branchId },
|
|
231
|
+
{ field: 'revertChangeId', operator: '=', value: null }
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
orderBy: [{ field: 'id', sort: 'desc' }],
|
|
235
|
+
limit: 1
|
|
236
|
+
});
|
|
237
|
+
return changes[0];
|
|
238
|
+
};
|