@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,451 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ENTITY_LOCAL_CREATE_EVENT,
|
|
3
|
+
EntityLocalCreatedEvent,
|
|
4
|
+
EntityType,
|
|
5
|
+
getCurrentBranch,
|
|
6
|
+
getLocalSystemRepositories,
|
|
7
|
+
getRemoteSystemRepositories,
|
|
8
|
+
HistoryScopeAPI,
|
|
9
|
+
MergeBranchBeginEvent,
|
|
10
|
+
MergeBranchCommitEvent,
|
|
11
|
+
MergeBranchFailedEvent,
|
|
12
|
+
MergeBranchOptions,
|
|
13
|
+
MergeBranchResult,
|
|
14
|
+
RestoreEntityOptions,
|
|
15
|
+
RxDB,
|
|
16
|
+
RxDBBranch,
|
|
17
|
+
RxDBError,
|
|
18
|
+
SwitchBranchBeginEvent,
|
|
19
|
+
SwitchBranchCommitEvent,
|
|
20
|
+
SwitchBranchRollbackEvent,
|
|
21
|
+
TRANSACTION_BEGIN,
|
|
22
|
+
TRANSACTION_COMMIT,
|
|
23
|
+
TRANSACTION_ROLLBACK
|
|
24
|
+
} from '@aiao/rxdb';
|
|
25
|
+
import { create_branch } from './create-branch.js';
|
|
26
|
+
import { isIgnorableDetachedVersionEventError } from './detached-event-error.js';
|
|
27
|
+
import { HistoryManager } from './HistoryManager.js';
|
|
28
|
+
import { merge_branch } from './merge-branch.js';
|
|
29
|
+
import { PushInFlightRegistry } from './push-inflight.js';
|
|
30
|
+
import { remove_branch } from './remove-branch.js';
|
|
31
|
+
import { restore_entity } from './restore-entity.js';
|
|
32
|
+
import { switch_branch_actions } from './switch-branch-actions.js';
|
|
33
|
+
import { createSyncHistoryBridge, type SyncHistoryBridge } from './sync-history-bridge.js';
|
|
34
|
+
import { getEarliestRecordAt, getRxDBChangeEventId } from './version-manager.utils.js';
|
|
35
|
+
/**
|
|
36
|
+
* 版本管理器
|
|
37
|
+
*
|
|
38
|
+
* 负责管理数据库的分支、撤销/重做与版本历史:
|
|
39
|
+
* - 分支管理(创建、删除、切换、合并)
|
|
40
|
+
* - 撤销/重做操作(数据库级别)
|
|
41
|
+
* - Redo 栈的自动失效(当有新操作时)
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* 推拉同步**不在**这里。US-025 阶段 D 之后它住在 `@aiao/rxdb-plugin-sync` 的
|
|
45
|
+
* `SyncManager` 上,两边只经 {@link VersionManager.syncBridge} 这一张窄接口相遇。
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
export class VersionManager {
|
|
49
|
+
#event_removers: Array<() => void> = [];
|
|
50
|
+
#transactionDepth = 0;
|
|
51
|
+
#transactionGeneration: number | null = null;
|
|
52
|
+
#transactionCleanupInitialized = false;
|
|
53
|
+
#historyManagerDestroyed = false;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 撤销/重做辅助类
|
|
57
|
+
* 管理历史记录和内存 redo 栈
|
|
58
|
+
*/
|
|
59
|
+
private historyManager: HistoryManager;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 「哪些变更此刻正在飞往远端」的登记处。
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* push 与 undo 唯一的会合点。push 在远端往返之前认领区间,undo 把认领区间当成已推 ——
|
|
66
|
+
* 没有它,往返窗口内的一次撤销会造成本地与远端永久分叉,见 {@link PushInFlightRegistry}。
|
|
67
|
+
*
|
|
68
|
+
* 挂在 `VersionManager` 上是因为两边都只经它相遇:`pushRepository(vm, …)` 直接拿到它,
|
|
69
|
+
* `HistoryManager` 经 `rxdb.versionManager` 拿到它。生命周期跟随实例,不跨实例共享。
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
readonly pushInFlight = new PushInFlightRegistry();
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 交给同步插件的那一小块历史能力。
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* 传闭包而不是 `this.historyManager`:`init()` 会在断连重连后重建管理器,
|
|
80
|
+
* 桥必须每次现取,见 {@link createSyncHistoryBridge}。
|
|
81
|
+
*
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
readonly syncBridge: SyncHistoryBridge = createSyncHistoryBridge(() => this.historyManager, this.pushInFlight);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 可 push 的变更数量流
|
|
88
|
+
*
|
|
89
|
+
* 实时追踪本地未推送的有效变更数量:
|
|
90
|
+
* - 排除已撤销的变更(revertChangeId != null)
|
|
91
|
+
* - 排除从远程 pull 来的变更(remoteId != null)
|
|
92
|
+
*/
|
|
93
|
+
get pushableCount$() {
|
|
94
|
+
return this.historyManager.pushableCount$;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 远程待 pull 的变更数量流
|
|
99
|
+
*
|
|
100
|
+
* 通过监听远程事件(INSERT/UPDATE/DELETE)累计:
|
|
101
|
+
* - 收到远程变更事件时计数+1
|
|
102
|
+
* - pull 完成后计数重置为 0
|
|
103
|
+
*
|
|
104
|
+
* 用于 UI 显示"有 N 条远程更新可拉取"
|
|
105
|
+
*/
|
|
106
|
+
get pullableCount$() {
|
|
107
|
+
return this.historyManager.pullableCount$;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param rxdb - 宿主实例
|
|
112
|
+
*
|
|
113
|
+
* @remarks
|
|
114
|
+
* 构造即建 {@link HistoryManager},而它在自己的构造器里就订阅了活跃分支流 ——
|
|
115
|
+
* 所以「造出来」本身已经是一次资源获取,调用方必须把 {@link VersionManager.destroy}
|
|
116
|
+
* 登记在作用域上,哪怕后面的 {@link VersionManager.init} 还没跑。
|
|
117
|
+
*/
|
|
118
|
+
constructor(public readonly rxdb: RxDB) {
|
|
119
|
+
this.historyManager = new HistoryManager(this.rxdb);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 挂上事务与本地写入的事件监听,开始记历史。
|
|
124
|
+
*
|
|
125
|
+
* @remarks
|
|
126
|
+
* 与 {@link VersionManager.destroy} 成对,可重入:上一轮 `destroy()` 过的
|
|
127
|
+
* `HistoryManager` 已经拆掉了订阅,这里重建一个而不是复用 —— 复用等于把新纪元的
|
|
128
|
+
* 变更喂给一条已 complete 的流。
|
|
129
|
+
*
|
|
130
|
+
* 不发任何适配器读写,因此可以(也必须)早于引导链跑:`HistoryManager` 的分支流
|
|
131
|
+
* 要赶在第一条 `rxdb_change` 事件之前订阅上,装晚了那一批变更就不进历史。
|
|
132
|
+
*/
|
|
133
|
+
init() {
|
|
134
|
+
if (this.#historyManagerDestroyed) {
|
|
135
|
+
this.historyManager = new HistoryManager(this.rxdb);
|
|
136
|
+
this.#historyManagerDestroyed = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const onTransactionBegin = () => {
|
|
140
|
+
if (!this.#transactionCleanupInitialized) {
|
|
141
|
+
this.#transactionCleanupInitialized = true;
|
|
142
|
+
const onTransactionCommit = () => {
|
|
143
|
+
if (this.#transactionDepth > 1) {
|
|
144
|
+
this.#transactionDepth -= 1;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.#transactionDepth = 0;
|
|
148
|
+
this.#transactionGeneration = null;
|
|
149
|
+
};
|
|
150
|
+
const onTransactionRollback = () => {
|
|
151
|
+
this.#transactionDepth = 0;
|
|
152
|
+
this.#transactionGeneration = null;
|
|
153
|
+
};
|
|
154
|
+
this.rxdb.addEventListener(TRANSACTION_COMMIT, onTransactionCommit);
|
|
155
|
+
this.rxdb.addEventListener(TRANSACTION_ROLLBACK, onTransactionRollback);
|
|
156
|
+
this.#event_removers.push(
|
|
157
|
+
() => this.rxdb.removeEventListener(TRANSACTION_COMMIT, onTransactionCommit),
|
|
158
|
+
() => this.rxdb.removeEventListener(TRANSACTION_ROLLBACK, onTransactionRollback)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (this.#transactionDepth === 0) {
|
|
162
|
+
this.#transactionGeneration = this.historyManager.undoSessionGeneration;
|
|
163
|
+
}
|
|
164
|
+
this.#transactionDepth += 1;
|
|
165
|
+
};
|
|
166
|
+
this.rxdb.addEventListener(TRANSACTION_BEGIN, onTransactionBegin);
|
|
167
|
+
this.#event_removers.push(() => this.rxdb.removeEventListener(TRANSACTION_BEGIN, onTransactionBegin));
|
|
168
|
+
|
|
169
|
+
// 监听 ENTITY_LOCAL_CREATE_EVENT 事件,自动失效 redo 栈(标准 undo/redo 语义)
|
|
170
|
+
// 当用户在 undo 后执行新操作时,所有可重做的历史项将被标记为失效
|
|
171
|
+
//
|
|
172
|
+
// 契约:本处**有意不过滤** `data.origin === 'cross-tab'`。
|
|
173
|
+
// 网关会给跨 tab 转发的实体打 `origin: 'cross-tab'`(见 `gateway/RxDBTabsGateway.ts`),
|
|
174
|
+
// `rxdb-plugin-workspace` 在 NEW 事件上读它来防广播成环;但 version 子系统**不读**。
|
|
175
|
+
// 理由:多 tab 共享同一份本地存储(同 dbName / OPFS),他 tab 的新写入对本 tab 而言
|
|
176
|
+
// 同样让 redo 栈失效、同样可能恢复 undo 边界——按本地写入处理才是正确语义。
|
|
177
|
+
const onLocalCreate = (event: EntityLocalCreatedEvent) => {
|
|
178
|
+
const rxdbChanges = event.entities?.filter(e => e.namespace === 'rxdb' && e.entity === 'RxDBChange') ?? [];
|
|
179
|
+
if (rxdbChanges.length > 0) {
|
|
180
|
+
// 只有在非 undo/redo 操作时才失效 redo 栈
|
|
181
|
+
if (!this.historyManager.isExecutingUndoRedo()) {
|
|
182
|
+
const changeIds = rxdbChanges.flatMap(change => {
|
|
183
|
+
const changeId = getRxDBChangeEventId(change);
|
|
184
|
+
return changeId === null ? [] : [changeId];
|
|
185
|
+
});
|
|
186
|
+
// 带上 id:变更表通知可能跨进程迟到(Tauri stdio 宿主),时间窗守卫挡不住,
|
|
187
|
+
// 由 invalidateRedoStack 按序列水位判定是否真是 undo 之后的新写入
|
|
188
|
+
this.#runDetachedEventTask(this.historyManager.invalidateRedoStack(changeIds), 'invalidateRedoStack');
|
|
189
|
+
const recordAt = getEarliestRecordAt(rxdbChanges);
|
|
190
|
+
if (this.#transactionGeneration !== null || recordAt !== null) {
|
|
191
|
+
this.historyManager.resetSyncCleared(changeIds, {
|
|
192
|
+
generation: this.#transactionGeneration,
|
|
193
|
+
recordAt
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
this.historyManager.resetSyncCleared(changeIds);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
this.rxdb.addEventListener(ENTITY_LOCAL_CREATE_EVENT, onLocalCreate);
|
|
202
|
+
this.#event_removers.push(() => this.rxdb.removeEventListener(ENTITY_LOCAL_CREATE_EVENT, onLocalCreate));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 摘掉全部事件监听与订阅,并销毁 {@link HistoryManager}。
|
|
207
|
+
*
|
|
208
|
+
* @remarks
|
|
209
|
+
* 幂等。事务深度与代次一并清零:拆卸可能发生在事务中途,留着计数会让下一个纪元
|
|
210
|
+
* 以为自己开局就在一笔未结的事务里。
|
|
211
|
+
*/
|
|
212
|
+
destroy() {
|
|
213
|
+
if (this.#historyManagerDestroyed) return;
|
|
214
|
+
this.#historyManagerDestroyed = true;
|
|
215
|
+
this.historyManager.destroy();
|
|
216
|
+
for (const remove of this.#event_removers) {
|
|
217
|
+
remove();
|
|
218
|
+
}
|
|
219
|
+
this.#event_removers.length = 0;
|
|
220
|
+
this.#transactionDepth = 0;
|
|
221
|
+
this.#transactionGeneration = null;
|
|
222
|
+
this.#transactionCleanupInitialized = false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 作废本连接积累的历史上下文:清空全部分支的 undo 会话与待拉计数。
|
|
227
|
+
*
|
|
228
|
+
* @remarks
|
|
229
|
+
* 供切库 / 重连这类「本地数据整体换了一份」的场景调用。清的是**全部**分支而不只是
|
|
230
|
+
* 当前分支 —— 换掉的是整个数据源,其它分支上那些 undo 条目指向的行同样已经不在了。
|
|
231
|
+
*/
|
|
232
|
+
resetSessionState() {
|
|
233
|
+
// 整个连接的历史上下文都作废了,不只是当前分支
|
|
234
|
+
this.historyManager.clearAllUndoHistory();
|
|
235
|
+
this.historyManager.resetPullableCount();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 创建新分支
|
|
240
|
+
* @param branchId 分支 id
|
|
241
|
+
* @param fromChangeId 从哪个 changeId 创建新分支
|
|
242
|
+
*/
|
|
243
|
+
async createBranch(branchId: string, fromChangeId?: number): Promise<InstanceType<typeof RxDBBranch>> {
|
|
244
|
+
return create_branch(this, branchId, fromChangeId);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 删除分支
|
|
249
|
+
* @param branchId 分支 id
|
|
250
|
+
*/
|
|
251
|
+
async removeBranch(branchId: string): Promise<void> {
|
|
252
|
+
return remove_branch(this, branchId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 切换到指定分支
|
|
257
|
+
*
|
|
258
|
+
* @param branchId - 目标分支 ID
|
|
259
|
+
*
|
|
260
|
+
* @remarks
|
|
261
|
+
* 切换分支后会自动清空 redo 栈,因为 redo 历史在新分支中不再有效。
|
|
262
|
+
* 如果目标分支与当前分支相同,则直接返回,避免不必要的操作。
|
|
263
|
+
*/
|
|
264
|
+
async switchBranch(branchId: string): Promise<void> {
|
|
265
|
+
const currentBranch = await this.getCurrentBranch();
|
|
266
|
+
// 若切换到相同分支则直接返回,避免不必要操作
|
|
267
|
+
if (currentBranch?.id === branchId) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
let switchCommitted = false;
|
|
271
|
+
try {
|
|
272
|
+
this.rxdb.dispatchEvent(new SwitchBranchBeginEvent(branchId));
|
|
273
|
+
const { adapter } = await this.getLocalRepositories();
|
|
274
|
+
const actions = await switch_branch_actions(this, branchId);
|
|
275
|
+
const result = await adapter.switchBranch({
|
|
276
|
+
branchId: branchId,
|
|
277
|
+
actions
|
|
278
|
+
});
|
|
279
|
+
// 适配器的 switchBranch 内部包了事务(见各适配器 version/switch_branch.ts),
|
|
280
|
+
// 走到这里说明已提交。此后任何失败都不该再发 Rollback —— 分支确实切过去了。
|
|
281
|
+
switchCommitted = true;
|
|
282
|
+
|
|
283
|
+
// 切换分支后清理状态:
|
|
284
|
+
// 1. 清空 redo 栈,因为 redo 历史在新分支中不再有效
|
|
285
|
+
// 2. 更新当前分支ID和历史起始时间为切换时刻
|
|
286
|
+
this.historyManager.clearRedoStack();
|
|
287
|
+
// undo session 是按分支存的,这里同步切视图。否则要等 current_branch$
|
|
288
|
+
// 这条响应式查询补发,切换后紧接着的 undo() 会撞上中途换 session 的竞态。
|
|
289
|
+
this.historyManager.setUndoBranch(branchId);
|
|
290
|
+
|
|
291
|
+
this.rxdb.dispatchEvent(new SwitchBranchCommitEvent(branchId));
|
|
292
|
+
return result;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (!switchCommitted) {
|
|
295
|
+
this.rxdb.dispatchEvent(new SwitchBranchRollbackEvent(branchId));
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 合并分支
|
|
303
|
+
*
|
|
304
|
+
* 将源分支的变更合并到当前激活分支(目标分支)。
|
|
305
|
+
*
|
|
306
|
+
* @param sourceBranchId - 源分支 ID
|
|
307
|
+
* @param options - 合并选项
|
|
308
|
+
* @returns 合并结果
|
|
309
|
+
*
|
|
310
|
+
* @remarks
|
|
311
|
+
* 支持两种策略:
|
|
312
|
+
* - `squash`(默认):将源分支所有变更压缩为最小操作集后一次性应用,目标分支只产生一组变更记录
|
|
313
|
+
* - `normal`:逐条应用源分支变更,每条变更在目标分支产生独立的变更记录(保留历史细节)
|
|
314
|
+
*
|
|
315
|
+
* 合并后会清空 undo/redo 历史,因为合并操作不可逆。
|
|
316
|
+
* 可选通过 `deleteSource: true` 在合并后删除源分支。
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```typescript
|
|
320
|
+
* // 压缩合并(默认)
|
|
321
|
+
* const result = await rxdb.versionManager.mergeBranch('feature-x');
|
|
322
|
+
* console.log(`Merged ${result.merged} changes`);
|
|
323
|
+
*
|
|
324
|
+
* // 普通合并
|
|
325
|
+
* const result = await rxdb.versionManager.mergeBranch('feature-x', {
|
|
326
|
+
* strategy: 'normal'
|
|
327
|
+
* });
|
|
328
|
+
*
|
|
329
|
+
* // 合并后删除源分支
|
|
330
|
+
* const result = await rxdb.versionManager.mergeBranch('feature-x', {
|
|
331
|
+
* deleteSource: true
|
|
332
|
+
* });
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
async mergeBranch(sourceBranchId: string, options?: MergeBranchOptions): Promise<MergeBranchResult> {
|
|
336
|
+
const currentBranch = await this.getCurrentBranch();
|
|
337
|
+
if (!currentBranch) {
|
|
338
|
+
throw new RxDBError('No active branch found');
|
|
339
|
+
}
|
|
340
|
+
const targetBranchId = currentBranch.id;
|
|
341
|
+
try {
|
|
342
|
+
this.rxdb.dispatchEvent(new MergeBranchBeginEvent(sourceBranchId, targetBranchId));
|
|
343
|
+
|
|
344
|
+
const result = await merge_branch(this, sourceBranchId, targetBranchId, options);
|
|
345
|
+
|
|
346
|
+
// 合并有变更时清空 undo/redo 历史
|
|
347
|
+
if (result.merged > 0) {
|
|
348
|
+
this.historyManager.clearUndoHistory();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.rxdb.dispatchEvent(new MergeBranchCommitEvent(sourceBranchId, targetBranchId));
|
|
352
|
+
return result;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
this.rxdb.dispatchEvent(new MergeBranchFailedEvent(sourceBranchId, targetBranchId));
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 获取特定作用域的历史记录 API
|
|
361
|
+
*
|
|
362
|
+
* @param options - 作用域选项:
|
|
363
|
+
* - 不传参数 → database 作用域(整个数据库)
|
|
364
|
+
* - EntityType 类 → repository 作用域(该实体的所有记录)
|
|
365
|
+
* - 实体实例 → entity 作用域(该实例的历史)
|
|
366
|
+
* @returns 统一的历史记录 API 接口
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```ts
|
|
370
|
+
* // 数据库级别
|
|
371
|
+
* const dbHistory = versionManager.history();
|
|
372
|
+
*
|
|
373
|
+
* // 仓库级别(所有 User 记录)
|
|
374
|
+
* const userHistory = versionManager.history(User);
|
|
375
|
+
*
|
|
376
|
+
* // 实体级别(单个用户)
|
|
377
|
+
* const entityHistory = versionManager.history(userInstance);
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
history<T extends EntityType>(options?: T | InstanceType<T>): HistoryScopeAPI {
|
|
381
|
+
return this.historyManager.history(options);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 恢复被删除的实体
|
|
386
|
+
*
|
|
387
|
+
* 根据 RxDBChange 记录中的 inversePatch 重新插入实体,
|
|
388
|
+
* 恢复操作本身会生成新的 RxDBChange 记录(可被 push 到远程)。
|
|
389
|
+
*
|
|
390
|
+
* @param entity - 被删除的实体实例(需要包含 metadata 信息)
|
|
391
|
+
* @param options - 恢复选项,包含 changeId(DELETE 类型的 RxDBChange 记录 ID)
|
|
392
|
+
* @returns 恢复后的实体实例
|
|
393
|
+
*/
|
|
394
|
+
async restoreEntity<T extends EntityType>(
|
|
395
|
+
entity: InstanceType<T>,
|
|
396
|
+
options: RestoreEntityOptions
|
|
397
|
+
): Promise<InstanceType<T>> {
|
|
398
|
+
return restore_entity(this, entity, options);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 取本地适配器上的系统表仓库。
|
|
403
|
+
*
|
|
404
|
+
* @returns `branchRepository` / `changeRepository` 与它们所属的本地适配器
|
|
405
|
+
*
|
|
406
|
+
* @remarks
|
|
407
|
+
* 每次都经 `localAdapter$` 重新解析,不缓存:适配器随连接纪元换,缓下来的那一份
|
|
408
|
+
* 会在重连之后指向已拆的旧实例。
|
|
409
|
+
*/
|
|
410
|
+
async getLocalRepositories() {
|
|
411
|
+
return getLocalSystemRepositories(this.rxdb);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 取远端适配器上的系统表仓库。
|
|
416
|
+
*
|
|
417
|
+
* @returns `branchRepository` / `changeRepository` 与它们所属的远端适配器
|
|
418
|
+
*
|
|
419
|
+
* @remarks
|
|
420
|
+
* 未配置远端时 `remoteAdapter$` 不发值,调用会一直挂着 —— 这是有意的:同步路径
|
|
421
|
+
* 本来就只在配了远端时才走,返回一个空壳只会把「没配远端」推迟到更深的地方才报。
|
|
422
|
+
*/
|
|
423
|
+
async getRemoteRepositories() {
|
|
424
|
+
return getRemoteSystemRepositories(this.rxdb);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 取当前分支;没有激活分支时激活(或新建)`main`。
|
|
429
|
+
*
|
|
430
|
+
* @remarks
|
|
431
|
+
* 分两段是有意的:
|
|
432
|
+
*
|
|
433
|
+
* - **热路径**(已有激活分支)不开事务。同步插件的远端事件链路里每条事件都要调它
|
|
434
|
+
* (`sync-listeners` 的 `filterByBranch`),把整段包进事务能修好并发,
|
|
435
|
+
* 但会让每次读都去抢并发度 1 的写队列槽位。
|
|
436
|
+
* - **冷路径**(查不到激活分支)才开事务,并在事务内**重做一遍检查**(双重检查)。
|
|
437
|
+
* 否则两个并发调用会双双走到 `create`,第二个撞主键报错。
|
|
438
|
+
*/
|
|
439
|
+
async getCurrentBranch() {
|
|
440
|
+
return getCurrentBranch(this.rxdb);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
#runDetachedEventTask(task: Promise<void>, label: string) {
|
|
444
|
+
void task.catch(error => {
|
|
445
|
+
if (isIgnorableDetachedVersionEventError(error)) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
console.error(`[VersionManager] ${label} failed:`, error);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type LocalRxDBBranchRepository,
|
|
3
|
+
type LocalRxDBChangeRepository,
|
|
4
|
+
resolve_current_branch,
|
|
5
|
+
RxDBBranch,
|
|
6
|
+
RxDBChange,
|
|
7
|
+
RxDBError
|
|
8
|
+
} from '@aiao/rxdb';
|
|
9
|
+
import { VersionManager } from './VersionManager.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 创建一个新分支
|
|
13
|
+
* @param version
|
|
14
|
+
* @param branchId
|
|
15
|
+
* @param fromChangeId
|
|
16
|
+
* @returns
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* 「查重 → 解析分叉点 → 写入」在一个事务窗口内完成。只把 `remove_branch`
|
|
20
|
+
* 包进事务是不够的:一个在删除之前就解析完父分支的 `create_branch`,仍会在删除之后
|
|
21
|
+
* 把子分支写进去,留下孤儿。两侧同处一个写队列窗口,「只能有一方成功」才成立。
|
|
22
|
+
*
|
|
23
|
+
* 远端 `branchExists` 那一趟**留在事务外**:它是网络往返,放进事务会让并发度 1 的
|
|
24
|
+
* 写队列被一次 RTT 堵住。它本来也只是尽力而为的预检,真正的互斥由本地主键约束兜底。
|
|
25
|
+
*/
|
|
26
|
+
export const create_branch = async (version: VersionManager, branchId: string, fromChangeId?: number) => {
|
|
27
|
+
const { branchRepository: queuedBranchRepository, adapter } = await version.getLocalRepositories();
|
|
28
|
+
// 检查分支 ID 是否存在(本地)
|
|
29
|
+
//
|
|
30
|
+
// 事务里还会再查一次。这一次是快速失败:它挡在远端往返之前,
|
|
31
|
+
// 保证「本地已存在」比「远端已存在」先报,错误信息的优先级不因加事务而改变。
|
|
32
|
+
const branchEntity = await queuedBranchRepository.find({
|
|
33
|
+
where: {
|
|
34
|
+
combinator: 'and',
|
|
35
|
+
rules: [{ field: 'id', operator: '=', value: branchId }]
|
|
36
|
+
},
|
|
37
|
+
limit: 1
|
|
38
|
+
});
|
|
39
|
+
if (branchEntity.length) throw new RxDBError(`Branch id (${branchId}) already exists`);
|
|
40
|
+
|
|
41
|
+
// 检查分支 ID 是否存在(远程)
|
|
42
|
+
const remoteAdapterName = version.rxdb.config.sync?.remote?.adapter;
|
|
43
|
+
if (remoteAdapterName) {
|
|
44
|
+
const { adapter: remoteAdapter } = await version.getRemoteRepositories();
|
|
45
|
+
if (remoteAdapter.branchExists) {
|
|
46
|
+
const existsOnRemote = await remoteAdapter.branchExists(branchId);
|
|
47
|
+
if (existsOnRemote) throw new RxDBError(`Branch id (${branchId}) already exists on remote`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 校验失败返回而不是就地抛出,理由同 `remove_branch`:不让适配器的事务包装层改写异常身份。
|
|
52
|
+
const result = await adapter.transaction(async executor => {
|
|
53
|
+
const branchRepository = executor.getRepository(RxDBBranch) as unknown as LocalRxDBBranchRepository;
|
|
54
|
+
const changeRepository = executor.getRepository(RxDBChange) as unknown as LocalRxDBChangeRepository;
|
|
55
|
+
|
|
56
|
+
const existed = await branchRepository.find({
|
|
57
|
+
where: {
|
|
58
|
+
combinator: 'and',
|
|
59
|
+
rules: [{ field: 'id', operator: '=', value: branchId }]
|
|
60
|
+
},
|
|
61
|
+
limit: 1
|
|
62
|
+
});
|
|
63
|
+
if (existed.length) return `Branch id (${branchId}) already exists`;
|
|
64
|
+
|
|
65
|
+
let fromBranch: InstanceType<typeof RxDBBranch> | undefined;
|
|
66
|
+
let fromChange: InstanceType<typeof RxDBChange> | undefined;
|
|
67
|
+
|
|
68
|
+
// 找出当前分支
|
|
69
|
+
if (fromChangeId) {
|
|
70
|
+
// 从 RxDBChange 表查找包含指定 changeId 的记录,获取其 branchId
|
|
71
|
+
const changeEntity = (
|
|
72
|
+
await changeRepository.find({
|
|
73
|
+
where: {
|
|
74
|
+
combinator: 'and',
|
|
75
|
+
rules: [{ field: 'id', operator: '=', value: fromChangeId }]
|
|
76
|
+
},
|
|
77
|
+
limit: 1
|
|
78
|
+
})
|
|
79
|
+
)[0];
|
|
80
|
+
if (!changeEntity) return `Change ID (${fromChangeId}) not found`;
|
|
81
|
+
|
|
82
|
+
// 保存找到的 change 作为 fromChange
|
|
83
|
+
fromChange = changeEntity;
|
|
84
|
+
|
|
85
|
+
// 通过 branchId 查找分支
|
|
86
|
+
fromBranch = (
|
|
87
|
+
await branchRepository.find({
|
|
88
|
+
where: {
|
|
89
|
+
combinator: 'and',
|
|
90
|
+
rules: [{ field: 'id', operator: '=', value: changeEntity.branchId! }]
|
|
91
|
+
},
|
|
92
|
+
limit: 1
|
|
93
|
+
})
|
|
94
|
+
)[0];
|
|
95
|
+
} else {
|
|
96
|
+
// 不能调 `version.getCurrentBranch()`:那一份走绑在适配器上的仓库,
|
|
97
|
+
// 会排在自己这个事务后面(队列并发度 1)而挂死。共用同一段逻辑,换成事务内的仓库。
|
|
98
|
+
fromBranch = await resolve_current_branch(branchRepository, version.rxdb);
|
|
99
|
+
fromChange = await get_current_branch_last_change(changeRepository, fromBranch.id);
|
|
100
|
+
}
|
|
101
|
+
if (!fromBranch) return 'Source branch not found';
|
|
102
|
+
|
|
103
|
+
// 创建新分支
|
|
104
|
+
//
|
|
105
|
+
// 用事务内仓库的 `create` 而非 `branch.save()`:后者经 entityManager 打到适配器上,
|
|
106
|
+
// 正是事务体内不能碰的那条队列。
|
|
107
|
+
const branch = version.rxdb.entityManager.instantiate(RxDBBranch);
|
|
108
|
+
branch.id = branchId;
|
|
109
|
+
branch.activated = false;
|
|
110
|
+
branch.local = true;
|
|
111
|
+
branch.remote = false;
|
|
112
|
+
branch.fromChangeId = fromChange?.id ?? null;
|
|
113
|
+
branch.parentId = fromBranch.id;
|
|
114
|
+
await branchRepository.create(branch);
|
|
115
|
+
return branch;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (typeof result === 'string') throw new RxDBError(result);
|
|
119
|
+
return result;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 获取分支最新一笔可作为分叉点的变更记录
|
|
124
|
+
*
|
|
125
|
+
* @param changeRepository - 变更仓库。由调用方决定它属于哪个事务 ——
|
|
126
|
+
* `create_branch` 传的是事务内的那一份,不能在这里自己去 `getLocalRepositories()`。
|
|
127
|
+
* @param currentBranchId - 分支 ID;不传则按「当前激活分支」查
|
|
128
|
+
*
|
|
129
|
+
* 独立导出而非内联,是为了被 `__tests__/version/create-branch.spec.ts` 单独测试。
|
|
130
|
+
*/
|
|
131
|
+
export const get_current_branch_last_change = async (
|
|
132
|
+
changeRepository: LocalRxDBChangeRepository,
|
|
133
|
+
currentBranchId?: string
|
|
134
|
+
) => {
|
|
135
|
+
return (
|
|
136
|
+
await changeRepository.find({
|
|
137
|
+
where: {
|
|
138
|
+
combinator: 'and',
|
|
139
|
+
rules: [
|
|
140
|
+
currentBranchId ?
|
|
141
|
+
{ field: 'branchId', operator: '=', value: currentBranchId }
|
|
142
|
+
: { field: 'branch.activated', operator: '=', value: true },
|
|
143
|
+
{
|
|
144
|
+
field: 'revertChangeId',
|
|
145
|
+
operator: '=',
|
|
146
|
+
value: null
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
orderBy: [{ field: 'id', sort: 'desc' }],
|
|
151
|
+
limit: 1
|
|
152
|
+
})
|
|
153
|
+
)[0];
|
|
154
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 游离事件任务的错误判据
|
|
3
|
+
*
|
|
4
|
+
* 事件监听器里发起的异步任务没有调用方接得住错误({@link VersionManager} 的
|
|
5
|
+
* `#runDetachedEventTask`、{@link setupSyncListeners} 的远端事件处理器都是即发即忘)。
|
|
6
|
+
* 连接正在拆除时这些任务必然撞上一批「适配器已经走了」的错误,它们是拆除过程的正常噪音,
|
|
7
|
+
* 不该打进控制台当故障看。
|
|
8
|
+
*
|
|
9
|
+
* 判据留在历史插件、由同步插件经 `@aiao/rxdb-plugin-history` 引用:同步插件本就
|
|
10
|
+
* `inject: ['plugin:history']`,这条 import 不新增依赖边;放进核心则要为一个纯谓词
|
|
11
|
+
* 再撑大一圈公共表面,与 US-025「核心只留原语」的方向相反。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isAdapterShutdownError } from '@aiao/rxdb';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 判断游离事件任务的错误是否可忽略
|
|
18
|
+
*
|
|
19
|
+
* @param error - 任务抛出的错误
|
|
20
|
+
* @returns 属于连接拆除噪音时返回 `true`
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* 包括:
|
|
24
|
+
* - errno 44 = ENODEV(Emscripten IDBFS 连接关闭)
|
|
25
|
+
* - name === 'AbortError'
|
|
26
|
+
* - adapter shutdown 错误(统一字符串匹配模式)
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export const isIgnorableDetachedVersionEventError = (error: unknown): boolean => {
|
|
31
|
+
if (!error || typeof error !== 'object') {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const name = 'name' in error ? String(error.name) : '';
|
|
36
|
+
const errno = 'errno' in error ? Number((error as { errno?: unknown }).errno) : undefined;
|
|
37
|
+
|
|
38
|
+
if (errno === 44 || name === 'AbortError') {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return isAdapterShutdownError(error);
|
|
43
|
+
};
|