@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,277 @@
|
|
|
1
|
+
import { RxDBBranch, RxDBError } from '@aiao/rxdb';
|
|
2
|
+
/**
|
|
3
|
+
* 分支切换步骤
|
|
4
|
+
*/
|
|
5
|
+
export interface SwitchBranchStep {
|
|
6
|
+
/**
|
|
7
|
+
* 分支对象
|
|
8
|
+
*/
|
|
9
|
+
branch: RxDBBranch;
|
|
10
|
+
/**
|
|
11
|
+
* 开始变更 ID
|
|
12
|
+
*/
|
|
13
|
+
fromChangeId: number;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 目标变更 ID
|
|
17
|
+
*/
|
|
18
|
+
toChangeId: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 查找分支切换路径的参数
|
|
23
|
+
*/
|
|
24
|
+
export interface FindBranchPathOptions {
|
|
25
|
+
/**
|
|
26
|
+
* 所需分支列表
|
|
27
|
+
*/
|
|
28
|
+
branches: RxDBBranch[];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 当前分支
|
|
32
|
+
*/
|
|
33
|
+
currentBranch: RxDBBranch;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 当前分支的变更 ID
|
|
37
|
+
*/
|
|
38
|
+
currentChangeId: number | null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 目标分支
|
|
42
|
+
*/
|
|
43
|
+
nextBranch: RxDBBranch;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 目标分支的变更 ID
|
|
47
|
+
*/
|
|
48
|
+
nextChangeId: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 计算出树的两个节点之间的切换路径
|
|
53
|
+
* 规则如下:
|
|
54
|
+
* 1, 如果两个树节点有共同的祖先节点,则路径为从当前节点到共同祖先节点,再到目标节点
|
|
55
|
+
* 2, 如果没有共同祖先节点,则路径为从当前节点到根节点,再到目标节点
|
|
56
|
+
* 3, 需要计算出路径内每个节点需要变更过程,也就是 fromChangeId -> toChangeId
|
|
57
|
+
* 4, 由于 changeId 在不同分支的变化都是是递增的, 所以如果逆向还原数据那么 fromChangeId 会大于 toChangeId, 正向应用数据则相反
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* 读取分支的分叉点变更 id。
|
|
61
|
+
*
|
|
62
|
+
* `fromChangeId === null` 的含义是**分叉于根**(这条分支不是从某条变更上长出来的)。
|
|
63
|
+
* 变更 id 从 1 开始递增,所以用 0 表示「根」既不会和任何真实变更撞上,又能让它
|
|
64
|
+
* 直接参与后面的区间运算。
|
|
65
|
+
*
|
|
66
|
+
* 抽成具名函数是为了让这个约定只存在一处:从前它以 `branch.fromChangeId ?? 0` 的形式
|
|
67
|
+
* 在本文件出现十余次,读的人无法判断哪一处是「表达根」、哪一处只是随手兜底。
|
|
68
|
+
*
|
|
69
|
+
* @param branch - 分支行
|
|
70
|
+
* @returns 分叉点变更 id,分叉于根时为 0
|
|
71
|
+
*/
|
|
72
|
+
const forkPointOf = (branch: RxDBBranch): number => branch.fromChangeId ?? 0;
|
|
73
|
+
|
|
74
|
+
export const find_switch_branch_step = (options: FindBranchPathOptions): SwitchBranchStep[] => {
|
|
75
|
+
const { branches, currentBranch, nextBranch } = options;
|
|
76
|
+
|
|
77
|
+
// 如果分支没有自己的变更记录,使用 fromChangeId 作为当前状态
|
|
78
|
+
// 否则使用该分支的最大 changeId
|
|
79
|
+
const currentChangeId = options.currentChangeId ?? forkPointOf(currentBranch);
|
|
80
|
+
const nextChangeId = options.nextChangeId ?? forkPointOf(nextBranch);
|
|
81
|
+
|
|
82
|
+
// 任意分支只要变更 id 相同,都无需切换数据
|
|
83
|
+
if (currentChangeId === nextChangeId) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 如果是同一个分支但变更 id 不同,直接在当前分支上移动
|
|
88
|
+
if (currentBranch.id === nextBranch.id) {
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
branch: currentBranch,
|
|
92
|
+
fromChangeId: currentChangeId,
|
|
93
|
+
toChangeId: nextChangeId
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 构建分支映射表
|
|
99
|
+
const branchMap = new Map<string, RxDBBranch>();
|
|
100
|
+
for (const branch of branches) {
|
|
101
|
+
branchMap.set(branch.id, branch);
|
|
102
|
+
}
|
|
103
|
+
|
|
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
|
+
// 获取当前分支和目标分支到根的路径
|
|
135
|
+
const currentPath = getPathToRoot(currentBranch);
|
|
136
|
+
const nextPath = getPathToRoot(nextBranch);
|
|
137
|
+
|
|
138
|
+
// 找到最近的共同祖先
|
|
139
|
+
let commonAncestor: RxDBBranch | null = null;
|
|
140
|
+
let commonAncestorIndex = -1;
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < currentPath.length; i++) {
|
|
143
|
+
const idx = nextPath.findIndex(b => b.id === currentPath[i].id);
|
|
144
|
+
if (idx !== -1) {
|
|
145
|
+
commonAncestor = currentPath[i];
|
|
146
|
+
commonAncestorIndex = i;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const steps: SwitchBranchStep[] = [];
|
|
152
|
+
|
|
153
|
+
if (commonAncestor) {
|
|
154
|
+
// 从当前分支回退到共同祖先
|
|
155
|
+
for (let i = 0; i < commonAncestorIndex; i++) {
|
|
156
|
+
const branch = currentPath[i];
|
|
157
|
+
const fromId = i === 0 ? currentChangeId : forkPointOf(currentPath[i - 1]);
|
|
158
|
+
const toId = forkPointOf(branch);
|
|
159
|
+
|
|
160
|
+
// 只有当需要实际变更时才添加步骤
|
|
161
|
+
if (fromId !== toId) {
|
|
162
|
+
steps.push({
|
|
163
|
+
branch,
|
|
164
|
+
fromChangeId: fromId,
|
|
165
|
+
toChangeId: toId
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 在共同祖先上可能也需要移动
|
|
171
|
+
const ancestorIndexInNext = nextPath.findIndex(b => b.id === commonAncestor!.id);
|
|
172
|
+
|
|
173
|
+
if (ancestorIndexInNext === 0) {
|
|
174
|
+
// 共同祖先就是目标分支,需要移动到目标 changeId
|
|
175
|
+
const ancestorFromId =
|
|
176
|
+
commonAncestorIndex === 0 ? currentChangeId : forkPointOf(currentPath[commonAncestorIndex - 1]);
|
|
177
|
+
const ancestorToId = nextChangeId;
|
|
178
|
+
|
|
179
|
+
if (ancestorFromId !== ancestorToId) {
|
|
180
|
+
steps.push({
|
|
181
|
+
branch: commonAncestor,
|
|
182
|
+
fromChangeId: ancestorFromId,
|
|
183
|
+
toChangeId: ancestorToId
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
} else if (ancestorIndexInNext > 0) {
|
|
187
|
+
// 需要从当前位置移动到目标分支的 fromChangeId
|
|
188
|
+
const ancestorFromId =
|
|
189
|
+
commonAncestorIndex === 0 ? currentChangeId : forkPointOf(currentPath[commonAncestorIndex - 1]);
|
|
190
|
+
const ancestorToId = forkPointOf(nextPath[ancestorIndexInNext - 1]);
|
|
191
|
+
|
|
192
|
+
if (ancestorFromId !== ancestorToId) {
|
|
193
|
+
steps.push({
|
|
194
|
+
branch: commonAncestor,
|
|
195
|
+
fromChangeId: ancestorFromId,
|
|
196
|
+
toChangeId: ancestorToId
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 从共同祖先前进到目标分支
|
|
202
|
+
const forwardPath = nextPath.slice(0, ancestorIndexInNext).reverse();
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < forwardPath.length; i++) {
|
|
205
|
+
const branch = forwardPath[i];
|
|
206
|
+
const fromId = forkPointOf(branch);
|
|
207
|
+
const toId = i === forwardPath.length - 1 ? nextChangeId : forkPointOf(forwardPath[i + 1]);
|
|
208
|
+
|
|
209
|
+
// 只有当需要实际变更时才添加步骤
|
|
210
|
+
if (fromId !== toId) {
|
|
211
|
+
steps.push({
|
|
212
|
+
branch,
|
|
213
|
+
fromChangeId: fromId,
|
|
214
|
+
toChangeId: toId
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
// 没有共同祖先,回退到当前分支的根,然后前进到目标分支
|
|
220
|
+
// 1. 回退当前分支的所有节点到根
|
|
221
|
+
for (let i = 0; i < currentPath.length; i++) {
|
|
222
|
+
const branch = currentPath[i];
|
|
223
|
+
const fromId = i === 0 ? currentChangeId : forkPointOf(currentPath[i - 1]);
|
|
224
|
+
const toId = forkPointOf(branch);
|
|
225
|
+
|
|
226
|
+
if (fromId !== toId) {
|
|
227
|
+
steps.push({
|
|
228
|
+
branch,
|
|
229
|
+
fromChangeId: fromId,
|
|
230
|
+
toChangeId: toId
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 2. 将当前树的根节点回退到 0
|
|
236
|
+
const currentRoot = currentPath[currentPath.length - 1];
|
|
237
|
+
const currentRootFromId = forkPointOf(currentRoot);
|
|
238
|
+
|
|
239
|
+
if (currentRootFromId !== 0) {
|
|
240
|
+
steps.push({
|
|
241
|
+
branch: currentRoot,
|
|
242
|
+
fromChangeId: currentRootFromId,
|
|
243
|
+
toChangeId: 0
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 3. 从目标树的根节点开始前进到目标分支
|
|
248
|
+
const nextRoot = nextPath[nextPath.length - 1];
|
|
249
|
+
const nextRootTargetId = nextPath.length > 1 ? forkPointOf(nextPath[nextPath.length - 2]) : nextChangeId;
|
|
250
|
+
|
|
251
|
+
if (nextRootTargetId !== 0) {
|
|
252
|
+
steps.push({
|
|
253
|
+
branch: nextRoot,
|
|
254
|
+
fromChangeId: 0,
|
|
255
|
+
toChangeId: nextRootTargetId
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 4. 前进目标树的所有子节点
|
|
260
|
+
const forwardPath = nextPath.slice(0, nextPath.length - 1).reverse();
|
|
261
|
+
for (let i = 0; i < forwardPath.length; i++) {
|
|
262
|
+
const branch = forwardPath[i];
|
|
263
|
+
const fromId = forkPointOf(branch);
|
|
264
|
+
const toId = i === forwardPath.length - 1 ? nextChangeId : forkPointOf(forwardPath[i + 1]);
|
|
265
|
+
|
|
266
|
+
if (fromId !== toId) {
|
|
267
|
+
steps.push({
|
|
268
|
+
branch,
|
|
269
|
+
fromChangeId: fromId,
|
|
270
|
+
toChangeId: toId
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return steps;
|
|
277
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 历史记录过滤与作用域键的纯函数集合
|
|
3
|
+
*
|
|
4
|
+
* 这些函数不持有任何状态,供 {@link HistoryManager} 与单元测试共用:
|
|
5
|
+
* - {@link filterHistoriesByScope}:按作用域裁切历史
|
|
6
|
+
* - {@link buildLastPushedMap}:合成每个仓库的「视同已推」水位线
|
|
7
|
+
* - {@link filterUndoableHistories}:过滤可撤销的历史
|
|
8
|
+
* - {@link getScopeKey}:生成作用域缓存键
|
|
9
|
+
*
|
|
10
|
+
* @module rxdb/version/history-filters
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getRxDBEntityIdentityKey, type HistoryItem, type HistoryScope } from '@aiao/rxdb';
|
|
14
|
+
import { generateHistoryDescription } from './history-item-builder.js';
|
|
15
|
+
/**
|
|
16
|
+
* 生成 repository 的缓存键(纯函数)
|
|
17
|
+
*
|
|
18
|
+
* @param repository - 带 namespace / entity 的仓储标识
|
|
19
|
+
* @returns 'namespace:entity' 形式的缓存键
|
|
20
|
+
*
|
|
21
|
+
* @internal 供 HistoryManager 与同文件的过滤函数使用
|
|
22
|
+
*/
|
|
23
|
+
export const getRepositoryKey = (repository: { namespace: string; entity: string }): string =>
|
|
24
|
+
`${repository.namespace}:${repository.entity}`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 合成「每个仓库推到哪了」的水位线映射:已提交的 `RxDBSync` 水位线与正在飞的认领取较大者。
|
|
28
|
+
*
|
|
29
|
+
* @param repoSyncs - 当前分支下的 `RxDBSync` 行
|
|
30
|
+
* @param inFlight - `PushInFlightRegistry.snapshot()`,`namespace:entity` -> 正在飞的最大 change id
|
|
31
|
+
* @returns `namespace:entity` -> 该仓库「视同已推」的最大 change id
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* `undoHistories$` 与 `fetchLatestHistories` 两条路径都要这份映射,此前各写一份循环。
|
|
35
|
+
* 两份必须逐字一致:一边认在飞、另一边不认,反应式的「可撤销」列表就会和真正执行撤销时
|
|
36
|
+
* 的判定分家 —— UI 上还亮着的那一项点下去什么也不会发生,或者更糟,真的撤销了一条
|
|
37
|
+
* 已经在飞的变更。
|
|
38
|
+
*
|
|
39
|
+
* 取最大值而不是「有在飞就用在飞」:上一轮推到 30、这一轮只在飞到 20 时,
|
|
40
|
+
* 水位线不许倒退。
|
|
41
|
+
*
|
|
42
|
+
* 在飞认领先进 map,因此**没有** `RxDBSync` 行的仓库也报得出来:`RxDBSync` 行由
|
|
43
|
+
* `planRepositoryPush` 现建,行还没落到本次查询的结果集里时,认领仍然必须生效。
|
|
44
|
+
*/
|
|
45
|
+
export function buildLastPushedMap(
|
|
46
|
+
repoSyncs: readonly { namespace: string; entity: string; lastPushedChangeId: number | null }[],
|
|
47
|
+
inFlight: ReadonlyMap<string, number>
|
|
48
|
+
): Map<string, number> {
|
|
49
|
+
const lastPushedMap = new Map(inFlight);
|
|
50
|
+
|
|
51
|
+
for (const repoSync of repoSyncs) {
|
|
52
|
+
if (repoSync.lastPushedChangeId === null) continue;
|
|
53
|
+
const key = getRepositoryKey(repoSync);
|
|
54
|
+
const claimed = lastPushedMap.get(key);
|
|
55
|
+
if (claimed !== undefined && claimed >= repoSync.lastPushedChangeId) continue;
|
|
56
|
+
lastPushedMap.set(key, repoSync.lastPushedChangeId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return lastPushedMap;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 按作用域过滤历史记录(纯函数)
|
|
64
|
+
*
|
|
65
|
+
* @param histories - 原始历史记录数组
|
|
66
|
+
* @param scope - 作用域配置
|
|
67
|
+
* @returns 过滤后的历史记录数组
|
|
68
|
+
*
|
|
69
|
+
* @internal 供 HistoryManager 和单元测试使用
|
|
70
|
+
*/
|
|
71
|
+
export function filterHistoriesByScope(histories: HistoryItem[], scope: HistoryScope): HistoryItem[] {
|
|
72
|
+
if (scope.type === 'database') {
|
|
73
|
+
return histories;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return histories
|
|
77
|
+
.map(history => {
|
|
78
|
+
const filtered_changes = history.changes.filter(change => {
|
|
79
|
+
if (scope.type === 'repository') {
|
|
80
|
+
return change.namespace === scope.namespace && change.entity === scope.entity;
|
|
81
|
+
}
|
|
82
|
+
// 实体作用域
|
|
83
|
+
return (
|
|
84
|
+
change.namespace === scope.namespace && change.entity === scope.entity && change.entityId === scope.entityId
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (filtered_changes.length === 0) return null;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...history,
|
|
92
|
+
changes: filtered_changes,
|
|
93
|
+
count: filtered_changes.length,
|
|
94
|
+
description: generateHistoryDescription(filtered_changes)
|
|
95
|
+
};
|
|
96
|
+
})
|
|
97
|
+
.filter((h): h is HistoryItem => h !== null);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 过滤出可撤销的历史记录(纯函数)
|
|
102
|
+
*
|
|
103
|
+
* @param histories - 原始历史记录数组
|
|
104
|
+
* @param lastPushedMap - namespace:entity -> lastPushedChangeId 的 repository 级水位线映射
|
|
105
|
+
* @param undoBoundaryChangeId - 同步清空后的永久撤销边界
|
|
106
|
+
* @param undoBoundaryCreatedAfter - 无数字 change id 时使用的严格时间边界
|
|
107
|
+
* @returns 可撤销的历史记录数组
|
|
108
|
+
*
|
|
109
|
+
* @remarks
|
|
110
|
+
* undoHistories$(UI 展示)与 undo()(实际执行)共用同一套规则,
|
|
111
|
+
* 保证 UI 上看不到的历史不会被 undo() 撤销:
|
|
112
|
+
* 1. history.reverted == false(按 updatedAt 水位合并持久态与本地态,未被撤销)
|
|
113
|
+
* 2. remoteId == null(只有本地创建的变更才能撤销)
|
|
114
|
+
* 3. id > 该 repository 的 lastPushedChangeId(未推送到远程;按 repository 独立判断)
|
|
115
|
+
* 4. id > undoBoundaryChangeId(同步清空前的历史永久不可撤销)
|
|
116
|
+
* 5. createdAt > undoBoundaryCreatedAfter(无数字 id 时保守隔离旧 session)
|
|
117
|
+
*
|
|
118
|
+
* @internal 供 HistoryManager 和单元测试使用
|
|
119
|
+
*/
|
|
120
|
+
export function filterUndoableHistories(
|
|
121
|
+
histories: HistoryItem[],
|
|
122
|
+
lastPushedMap: Map<string, number>,
|
|
123
|
+
undoBoundaryChangeId = 0,
|
|
124
|
+
undoBoundaryCreatedAfter: Date | null = null
|
|
125
|
+
): HistoryItem[] {
|
|
126
|
+
return histories.filter(history => {
|
|
127
|
+
if (history.reverted) return false;
|
|
128
|
+
|
|
129
|
+
return history.changes.every(change => {
|
|
130
|
+
const isLocal = change.remoteId == null;
|
|
131
|
+
const lastPushedId = lastPushedMap.get(getRepositoryKey(change));
|
|
132
|
+
const notPushed = lastPushedId == null || change.id > lastPushedId;
|
|
133
|
+
const afterUndoBoundary = change.id > undoBoundaryChangeId;
|
|
134
|
+
const afterUndoTimeBoundary =
|
|
135
|
+
undoBoundaryCreatedAfter === null || change.createdAt.getTime() > undoBoundaryCreatedAfter.getTime();
|
|
136
|
+
return isLocal && notPushed && afterUndoBoundary && afterUndoTimeBoundary;
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 生成作用域缓存键(纯函数)
|
|
143
|
+
*
|
|
144
|
+
* @param scope - 作用域配置
|
|
145
|
+
* @returns 缓存键字符串:'database' | 'namespace:entity' | 'namespace:entity:id'
|
|
146
|
+
*
|
|
147
|
+
* @internal 供 HistoryManager 和单元测试使用
|
|
148
|
+
*/
|
|
149
|
+
export function getScopeKey(scope: HistoryScope): string {
|
|
150
|
+
if (scope.type === 'database') {
|
|
151
|
+
return 'database';
|
|
152
|
+
}
|
|
153
|
+
if (scope.type === 'repository') {
|
|
154
|
+
return `${scope.namespace}:${scope.entity}`;
|
|
155
|
+
}
|
|
156
|
+
return `${scope.namespace}:${scope.entity}:${getRxDBEntityIdentityKey(scope.entityId)}`;
|
|
157
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { getRxDBEntityIdentityKey, HistoryItem, RxDBChange, UUID } from '@aiao/rxdb';
|
|
2
|
+
import { isNil } from '@aiao/utils';
|
|
3
|
+
/**
|
|
4
|
+
* 生成历史项的描述文本
|
|
5
|
+
*
|
|
6
|
+
* 规则:
|
|
7
|
+
* - 单条 INSERT/UPDATE/DELETE → 「创建 Foo」「更新 Foo」「删除 Foo」
|
|
8
|
+
* - 事务(多条变更)→ 「事务: 创建2条, 更新3条」
|
|
9
|
+
*/
|
|
10
|
+
export function generateHistoryDescription(changes: RxDBChange[]): string {
|
|
11
|
+
if (changes.length === 1) {
|
|
12
|
+
const change = changes[0];
|
|
13
|
+
const action =
|
|
14
|
+
change.type === 'INSERT' ? '创建'
|
|
15
|
+
: change.type === 'UPDATE' ? '更新'
|
|
16
|
+
: '删除';
|
|
17
|
+
return `${action} ${change.entity}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const operations: Record<string, number> = {};
|
|
21
|
+
for (const change of changes) {
|
|
22
|
+
const key = change.type.toLowerCase();
|
|
23
|
+
operations[key] = (operations[key] ?? 0) + 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parts: string[] = [];
|
|
27
|
+
if (operations['insert']) parts.push(`创建${operations['insert']}条`);
|
|
28
|
+
if (operations['update']) parts.push(`更新${operations['update']}条`);
|
|
29
|
+
if (operations['delete']) parts.push(`删除${operations['delete']}条`);
|
|
30
|
+
|
|
31
|
+
return `事务: ${parts.join(', ')}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 从一组同事务的变更构造单个 HistoryItem
|
|
36
|
+
*
|
|
37
|
+
* @param changes - 属于同一事务的变更记录(至少一条)
|
|
38
|
+
* @returns 结构化的历史项,包含描述、类型、状态等信息
|
|
39
|
+
*/
|
|
40
|
+
export function createHistoryItem(changes: RxDBChange[]): HistoryItem {
|
|
41
|
+
const first_change = changes[0]!;
|
|
42
|
+
const count = changes.length;
|
|
43
|
+
const transactionId = first_change.transactionId ?? null;
|
|
44
|
+
const createdAt = first_change.createdAt!;
|
|
45
|
+
const namespace = first_change.namespace;
|
|
46
|
+
const entity = first_change.entity;
|
|
47
|
+
const type = transactionId ? 'TRANSACTION' : first_change.type;
|
|
48
|
+
const fingerprint = [type, getRxDBEntityIdentityKey(first_change.entityId), createdAt.getTime()].join(':');
|
|
49
|
+
const reverted = changes.some(c => isNil(c.revertChangeId) === false);
|
|
50
|
+
const redoInvalidated = changes.some(c => isNil(c.redoInvalidatedAt) === false);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
transactionId,
|
|
54
|
+
changeId: changes.reduce((max, c) => (c.id > max ? c.id : max), changes[0].id),
|
|
55
|
+
fingerprint,
|
|
56
|
+
changes,
|
|
57
|
+
type,
|
|
58
|
+
count,
|
|
59
|
+
createdAt,
|
|
60
|
+
description: generateHistoryDescription(changes),
|
|
61
|
+
namespace,
|
|
62
|
+
entity,
|
|
63
|
+
reverted,
|
|
64
|
+
redoInvalidated
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 把按 id 降序排列的变更列表,转换为按事务分组的 HistoryItem 数组
|
|
70
|
+
*
|
|
71
|
+
* 转换逻辑:
|
|
72
|
+
* - 相同 transactionId 的连续变更合并为一个 HistoryItem
|
|
73
|
+
* - 无 transactionId 的变更单独成为一个 HistoryItem
|
|
74
|
+
*/
|
|
75
|
+
export function convertChangesToHistories(changes: RxDBChange[]): HistoryItem[] {
|
|
76
|
+
const histories: HistoryItem[] = [];
|
|
77
|
+
const next_transaction: RxDBChange[] = [];
|
|
78
|
+
let _transaction_id: UUID | undefined | null = undefined;
|
|
79
|
+
|
|
80
|
+
changes.forEach(d => {
|
|
81
|
+
if (d.transactionId && d.transactionId === _transaction_id) {
|
|
82
|
+
next_transaction.push(d);
|
|
83
|
+
} else {
|
|
84
|
+
_transaction_id = d.transactionId;
|
|
85
|
+
if (next_transaction.length) {
|
|
86
|
+
histories.push(createHistoryItem([...next_transaction]));
|
|
87
|
+
next_transaction.length = 0;
|
|
88
|
+
}
|
|
89
|
+
if (d.transactionId) {
|
|
90
|
+
next_transaction.push(d);
|
|
91
|
+
} else {
|
|
92
|
+
histories.push(createHistoryItem([d]));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (next_transaction.length) {
|
|
97
|
+
histories.push(createHistoryItem([...next_transaction]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return histories;
|
|
101
|
+
}
|