@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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -2
  3. package/dist/HistoryManager.d.ts +79 -0
  4. package/dist/HistoryManager.d.ts.map +1 -0
  5. package/dist/VersionManager.d.ts +233 -0
  6. package/dist/VersionManager.d.ts.map +1 -0
  7. package/dist/create-branch.d.ts +29 -0
  8. package/dist/create-branch.d.ts.map +1 -0
  9. package/dist/detached-event-error.d.ts +28 -0
  10. package/dist/detached-event-error.d.ts.map +1 -0
  11. package/dist/find-switch-branch-step.d.ts +45 -0
  12. package/dist/find-switch-branch-step.d.ts.map +1 -0
  13. package/dist/history-filters.d.ts +78 -0
  14. package/dist/history-filters.d.ts.map +1 -0
  15. package/dist/history-item-builder.d.ts +25 -0
  16. package/dist/history-item-builder.d.ts.map +1 -0
  17. package/dist/history-scope-api.d.ts +41 -0
  18. package/dist/history-scope-api.d.ts.map +1 -0
  19. package/dist/history-undo-session.types.d.ts +32 -0
  20. package/dist/history-undo-session.types.d.ts.map +1 -0
  21. package/dist/index.d.ts +35 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +1480 -0
  24. package/dist/merge-branch.d.ts +22 -0
  25. package/dist/merge-branch.d.ts.map +1 -0
  26. package/dist/plugin.d.ts +58 -0
  27. package/dist/plugin.d.ts.map +1 -0
  28. package/dist/pullable-count.d.ts +37 -0
  29. package/dist/pullable-count.d.ts.map +1 -0
  30. package/dist/push-inflight.d.ts +60 -0
  31. package/dist/push-inflight.d.ts.map +1 -0
  32. package/dist/redo-stack.d.ts +56 -0
  33. package/dist/redo-stack.d.ts.map +1 -0
  34. package/dist/remove-branch.d.ts +16 -0
  35. package/dist/remove-branch.d.ts.map +1 -0
  36. package/dist/restore-entity.d.ts +15 -0
  37. package/dist/restore-entity.d.ts.map +1 -0
  38. package/dist/scope-selection.d.ts +55 -0
  39. package/dist/scope-selection.d.ts.map +1 -0
  40. package/dist/switch-branch-actions.d.ts +16 -0
  41. package/dist/switch-branch-actions.d.ts.map +1 -0
  42. package/dist/sync-history-bridge.d.ts +87 -0
  43. package/dist/sync-history-bridge.d.ts.map +1 -0
  44. package/dist/undo-redo-apply.d.ts +47 -0
  45. package/dist/undo-redo-apply.d.ts.map +1 -0
  46. package/dist/version-manager.utils.d.ts +30 -0
  47. package/dist/version-manager.utils.d.ts.map +1 -0
  48. package/package.json +57 -3
  49. package/src/HistoryManager.ts +770 -0
  50. package/src/VersionManager.ts +451 -0
  51. package/src/create-branch.ts +154 -0
  52. package/src/detached-event-error.ts +43 -0
  53. package/src/find-switch-branch-step.ts +277 -0
  54. package/src/history-filters.ts +157 -0
  55. package/src/history-item-builder.ts +101 -0
  56. package/src/history-scope-api.ts +189 -0
  57. package/src/history-undo-session.types.ts +37 -0
  58. package/src/index.ts +46 -0
  59. package/src/merge-branch.ts +153 -0
  60. package/src/plugin.ts +93 -0
  61. package/src/pullable-count.ts +67 -0
  62. package/src/push-inflight.ts +91 -0
  63. package/src/redo-stack.ts +79 -0
  64. package/src/remove-branch.ts +89 -0
  65. package/src/restore-entity.ts +97 -0
  66. package/src/scope-selection.ts +132 -0
  67. package/src/switch-branch-actions.ts +238 -0
  68. package/src/sync-history-bridge.ts +131 -0
  69. package/src/undo-redo-apply.ts +260 -0
  70. 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
+ }