@aiao/rxdb-plugin-history 0.0.0-stage → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -2
- package/dist/HistoryManager.d.ts +87 -0
- package/dist/HistoryManager.d.ts.map +1 -0
- package/dist/VersionManager.d.ts +243 -0
- package/dist/VersionManager.d.ts.map +1 -0
- package/dist/create-branch.d.ts +35 -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 +94 -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 +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1565 -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 +56 -0
- package/dist/scope-selection.d.ts.map +1 -0
- package/dist/switch-branch-actions.d.ts +87 -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 +59 -3
- package/src/HistoryManager.ts +800 -0
- package/src/VersionManager.ts +617 -0
- package/src/create-branch.ts +178 -0
- package/src/detached-event-error.ts +43 -0
- package/src/find-switch-branch-step.ts +313 -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 +61 -0
- package/src/merge-branch.ts +192 -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 +100 -0
- package/src/restore-entity.ts +124 -0
- package/src/scope-selection.ts +133 -0
- package/src/switch-branch-actions.ts +319 -0
- package/src/sync-history-bridge.ts +131 -0
- package/src/undo-redo-apply.ts +270 -0
- package/src/version-manager.utils.ts +43 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import {
|
|
2
|
+
declareTrustedWrite,
|
|
3
|
+
EntityType,
|
|
4
|
+
HistoryItem,
|
|
5
|
+
HistoryScope,
|
|
6
|
+
HistoryScopeAPI,
|
|
7
|
+
isAdapterShutdownError,
|
|
8
|
+
REPOSITORY_SYNC_COMPLETE_EVENT,
|
|
9
|
+
RxDB,
|
|
10
|
+
RxDBBranch,
|
|
11
|
+
RxDBChange,
|
|
12
|
+
SKIP_BRANCH_SWITCH_PREPARE,
|
|
13
|
+
TrustedWriteIntent
|
|
14
|
+
} from '@aiao/rxdb';
|
|
15
|
+
import {
|
|
16
|
+
BehaviorSubject,
|
|
17
|
+
catchError,
|
|
18
|
+
combineLatest,
|
|
19
|
+
EMPTY,
|
|
20
|
+
filter,
|
|
21
|
+
firstValueFrom,
|
|
22
|
+
map,
|
|
23
|
+
NEVER,
|
|
24
|
+
Observable,
|
|
25
|
+
ReplaySubject,
|
|
26
|
+
shareReplay,
|
|
27
|
+
Subject,
|
|
28
|
+
Subscription,
|
|
29
|
+
switchMap,
|
|
30
|
+
takeUntil
|
|
31
|
+
} from 'rxjs';
|
|
32
|
+
import { buildLastPushedMap, filterUndoableHistories } from './history-filters.js';
|
|
33
|
+
import { convertChangesToHistories } from './history-item-builder.js';
|
|
34
|
+
import { createHistoryScopeApi, type HistoryScopeApiHost } from './history-scope-api.js';
|
|
35
|
+
import {
|
|
36
|
+
INITIAL_UNDO_BOUNDARY,
|
|
37
|
+
type ActiveUndoSession,
|
|
38
|
+
type UndoBoundary,
|
|
39
|
+
type UndoSession,
|
|
40
|
+
type UndoSessionEvent
|
|
41
|
+
} from './history-undo-session.types.js';
|
|
42
|
+
import { settledPullableCount } from './pullable-count.js';
|
|
43
|
+
import { RedoStack } from './redo-stack.js';
|
|
44
|
+
import { RxDBCrossScopeTransactionError, selectScopedHistories } from './scope-selection.js';
|
|
45
|
+
import { get_switch_version_actions } from './switch-branch-actions.js';
|
|
46
|
+
import {
|
|
47
|
+
applyUndoRedoHistories,
|
|
48
|
+
fetchLatestHistories,
|
|
49
|
+
getLocalRxDBSyncRepository,
|
|
50
|
+
updatePushableCount,
|
|
51
|
+
type UndoRedoApplyHost
|
|
52
|
+
} from './undo-redo-apply.js';
|
|
53
|
+
// 重新导出供测试和外部消费者使用(移动到独立文件后保持 API 兼容)
|
|
54
|
+
export { filterHistoriesByScope, filterUndoableHistories, getScopeKey } from './history-filters.js';
|
|
55
|
+
export { generateHistoryDescription } from './history-item-builder.js';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 取一批变更号里的最小值。
|
|
59
|
+
*
|
|
60
|
+
* @param changeIds - 已筛过的合法变更号,非空
|
|
61
|
+
* @returns 其中的最小值
|
|
62
|
+
*
|
|
63
|
+
* @remarks
|
|
64
|
+
* 不写 `Math.min(...changeIds)`:展开传参的实参个数受引擎栈帧限制(V8 约 6.5 万),
|
|
65
|
+
* 而这里的入参是一次批量写事件的**全部**变更号 —— 一次十万行的 bulk insert 就能把它
|
|
66
|
+
* 压成 `RangeError`,落在一条与批量大小相关、小数据量下永远复现不了的路径上。
|
|
67
|
+
*/
|
|
68
|
+
const minChangeId = (changeIds: readonly number[]): number =>
|
|
69
|
+
changeIds.reduce((min, changeId) => (changeId < min ? changeId : min), Number.POSITIVE_INFINITY);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 历史记录管理器:变更历史、内存 redo 栈、多作用域 undo/redo API。
|
|
73
|
+
*
|
|
74
|
+
* 数据流:`RxDBChange[] → #all_changes$ → histories$ → undoHistories$ / redoHistories$`
|
|
75
|
+
*/
|
|
76
|
+
export class HistoryManager {
|
|
77
|
+
#subscriptions: Subscription[] = [];
|
|
78
|
+
#destroyed = false;
|
|
79
|
+
#scopeHost!: HistoryScopeApiHost;
|
|
80
|
+
#applyHost!: UndoRedoApplyHost;
|
|
81
|
+
|
|
82
|
+
/** 销毁信号(ReplaySubject:迟到订阅者直接 complete,避免重拉活查询) */
|
|
83
|
+
#destroy$ = new ReplaySubject<void>(1);
|
|
84
|
+
|
|
85
|
+
/** undo / redo / invalidateRedoStack 串行锁 */
|
|
86
|
+
#operation_lock: Promise<unknown> = Promise.resolve();
|
|
87
|
+
|
|
88
|
+
/** 远程待 pull 计数,pull 结束后按 {@link settlePullableCount} 结算 */
|
|
89
|
+
#pullableCount$ = new BehaviorSubject<number>(0);
|
|
90
|
+
|
|
91
|
+
/** pullable 计数代次:结算令牌,用于判断 pull 期间是否有新事件 */
|
|
92
|
+
#pullableGeneration = 0;
|
|
93
|
+
|
|
94
|
+
/** 可 push 计数(repository 级 lastPushedChangeId 过滤) */
|
|
95
|
+
#pushableCount$ = new BehaviorSubject<number>(0);
|
|
96
|
+
|
|
97
|
+
/** pushable 刷新代次:只让最新那次刷新改写计数 */
|
|
98
|
+
#pushableGeneration = 0;
|
|
99
|
+
|
|
100
|
+
/** push/pull 完成后强制刷新 undoHistories$ */
|
|
101
|
+
#pushableCountTrigger$ = new BehaviorSubject<number>(0);
|
|
102
|
+
|
|
103
|
+
#revertStateWatermarks = new Map<number, { reverted: boolean; updatedAt: number }>();
|
|
104
|
+
#revertStateWatermarkTrigger$ = new BehaviorSubject<number>(0);
|
|
105
|
+
|
|
106
|
+
/** redo 失效判定水位:id ≤ 该值视为 undo 前迟到通知,不清栈 */
|
|
107
|
+
#redoInvalidationFloor = 0;
|
|
108
|
+
|
|
109
|
+
/** 当前分支 session 内变更流 */
|
|
110
|
+
#all_changes$!: Observable<RxDBChange[]>;
|
|
111
|
+
|
|
112
|
+
/** 首次连接时间:createdAt 早于此的变更不可 undo */
|
|
113
|
+
#firstConnectedAt: Date | null = null;
|
|
114
|
+
|
|
115
|
+
/** `firstConnectedAt` 不可用时的会话起点(构造时刻,必须定格) */
|
|
116
|
+
readonly #sessionStartedAt = new Date();
|
|
117
|
+
|
|
118
|
+
/** 按分支隔离的 undo session */
|
|
119
|
+
#undoSessions = new Map<string, UndoSession>();
|
|
120
|
+
|
|
121
|
+
/** 尚未访问的分支继承的 session 模板(仅 {@link clearAllUndoHistory} 写入) */
|
|
122
|
+
#undoSessionFloor: UndoSession | null = null;
|
|
123
|
+
|
|
124
|
+
/** 全局单调 session 代次(跨分支不复用) */
|
|
125
|
+
#undoSessionCounter = 0;
|
|
126
|
+
|
|
127
|
+
/** 当前分支 id;`null` 表示活跃分支还没解析出来 */
|
|
128
|
+
#currentUndoBranchId: string | null = null;
|
|
129
|
+
|
|
130
|
+
/** 当前分支 session 视图(真源 {@link #undoSessions}) */
|
|
131
|
+
#undoSession$ = new BehaviorSubject<UndoSession>({
|
|
132
|
+
generation: 0,
|
|
133
|
+
state: 'active',
|
|
134
|
+
boundary: INITIAL_UNDO_BOUNDARY
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
private isUndoRedoInProgress = false;
|
|
138
|
+
private isInvalidatingRedo = false;
|
|
139
|
+
/** 进行中的 pull/push 深度(可重入,避免并发同步提前清标志) */
|
|
140
|
+
private syncDepth = 0;
|
|
141
|
+
private readonly redoStack = new RedoStack();
|
|
142
|
+
private history_cache = new Map<string, HistoryScopeAPI>();
|
|
143
|
+
private history_ref_counts = new Map<string, number>();
|
|
144
|
+
histories$: Observable<HistoryItem[]>;
|
|
145
|
+
undoHistories$: Observable<HistoryItem[]>;
|
|
146
|
+
redoHistories$: Observable<HistoryItem[]>;
|
|
147
|
+
undoCount$: Observable<number>;
|
|
148
|
+
redoCount$: Observable<number>;
|
|
149
|
+
|
|
150
|
+
/** 内部错误流:派生流降级的同时把错误推给订阅者 */
|
|
151
|
+
readonly errors$ = new Subject<Error>();
|
|
152
|
+
pushableCount$: Observable<number>;
|
|
153
|
+
pullableCount$: Observable<number>;
|
|
154
|
+
|
|
155
|
+
count$: Observable<number>;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 当前撤销 session generation。
|
|
159
|
+
*
|
|
160
|
+
* @internal 供 VersionManager 在事务开始时捕获事件所属 session
|
|
161
|
+
*/
|
|
162
|
+
get undoSessionGeneration(): number {
|
|
163
|
+
return this.#undoSession$.value.generation;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
constructor(private rxdb: RxDB) {
|
|
167
|
+
// Host 必须在任何订阅之前建好:pushableCount 订阅会同步触发 #updatePushableCount。
|
|
168
|
+
this.#scopeHost = this.#createScopeHost();
|
|
169
|
+
this.#applyHost = this.#createApplyHost();
|
|
170
|
+
|
|
171
|
+
const branchRepository = rxdb.entityManager.getRepository(RxDBBranch);
|
|
172
|
+
const changeRepository = rxdb.entityManager.getRepository(RxDBChange);
|
|
173
|
+
// 连上之后才查:仓库活查询会经适配器的就绪门把实例连起来,构造期直接订阅等于 `init()` 之后插件
|
|
174
|
+
// 自己把库连上了,「连接前先恢复」之类必须对着未连接实例做的操作就永远拿不到空目标。
|
|
175
|
+
// 断开时交 NEVER:丢掉挂在已断适配器上的查询,undo session 停在最后一个分支上,重连再查。
|
|
176
|
+
const current_branch$ = this.rxdb.connected$.pipe(
|
|
177
|
+
switchMap(connected =>
|
|
178
|
+
connected ?
|
|
179
|
+
branchRepository.findOne({
|
|
180
|
+
where: {
|
|
181
|
+
combinator: 'and',
|
|
182
|
+
rules: [{ field: 'activated', operator: '=', value: true }]
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
: NEVER
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
this.#subscriptions.push(
|
|
190
|
+
current_branch$.pipe(takeUntil(this.#destroy$)).subscribe({
|
|
191
|
+
next: branch => this.#switchUndoSessionBranch(branch?.id ?? null),
|
|
192
|
+
error: error => this.#reportStreamError('undo session 分支跟随的活跃分支流已中断', error)
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
this.#all_changes$ = combineLatest({
|
|
197
|
+
connected: this.rxdb.connected$,
|
|
198
|
+
branch: current_branch$
|
|
199
|
+
}).pipe(
|
|
200
|
+
filter(({ connected, branch }) => connected && !!branch),
|
|
201
|
+
switchMap(({ branch }) => {
|
|
202
|
+
const firstConnectedAt = this.#getFirstConnectedAt();
|
|
203
|
+
return changeRepository.findAll({
|
|
204
|
+
where: {
|
|
205
|
+
combinator: 'and',
|
|
206
|
+
rules: [
|
|
207
|
+
{
|
|
208
|
+
field: 'branchId',
|
|
209
|
+
operator: '=',
|
|
210
|
+
value: branch!.id
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
field: 'createdAt',
|
|
214
|
+
operator: '>=',
|
|
215
|
+
value: firstConnectedAt
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
},
|
|
219
|
+
orderBy: [{ field: 'id', sort: 'desc' }]
|
|
220
|
+
});
|
|
221
|
+
}),
|
|
222
|
+
takeUntil(this.#destroy$)
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const persistedHistories$ = this.#all_changes$.pipe(
|
|
226
|
+
map(changes => convertChangesToHistories(changes)),
|
|
227
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
228
|
+
);
|
|
229
|
+
this.histories$ = combineLatest([persistedHistories$, this.#revertStateWatermarkTrigger$]).pipe(
|
|
230
|
+
map(([histories]) => this.#applyLocalRevertState(histories)),
|
|
231
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
this.count$ = this.histories$.pipe(
|
|
235
|
+
map(histories => histories.length),
|
|
236
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
this.undoHistories$ = combineLatest({
|
|
240
|
+
histories: this.histories$,
|
|
241
|
+
undoSession: this.#undoSession$,
|
|
242
|
+
trigger: this.#pushableCountTrigger$ // 监听 push/pull 完成事件
|
|
243
|
+
}).pipe(
|
|
244
|
+
switchMap(async ({ histories, undoSession }) => {
|
|
245
|
+
if (undoSession.state === 'cleared') {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const branch = await firstValueFrom(
|
|
251
|
+
this.rxdb.entityManager.getRepository(RxDBBranch).findOne({
|
|
252
|
+
where: {
|
|
253
|
+
combinator: 'and',
|
|
254
|
+
rules: [{ field: 'activated', operator: '=', value: true }]
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (!branch) return [];
|
|
260
|
+
|
|
261
|
+
const localAdapter = await firstValueFrom(this.rxdb.localAdapter$);
|
|
262
|
+
const RxDBSyncRepo = getLocalRxDBSyncRepository(localAdapter);
|
|
263
|
+
const repoSyncs = await RxDBSyncRepo.find({
|
|
264
|
+
where: {
|
|
265
|
+
combinator: 'and',
|
|
266
|
+
rules: [{ field: 'branchId', operator: '=', value: branch.id }]
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// 与 `fetchLatestHistories` 同源:反应式的可撤销列表必须和真正执行撤销时
|
|
271
|
+
// 的判定逐字一致,否则 UI 上还亮着的那一项点下去要么无效、要么撤掉一条在飞的变更
|
|
272
|
+
const lastPushedMap = buildLastPushedMap(repoSyncs, this.rxdb.versionManager.pushInFlight.snapshot());
|
|
273
|
+
|
|
274
|
+
return filterUndoableHistories(
|
|
275
|
+
histories,
|
|
276
|
+
lastPushedMap,
|
|
277
|
+
undoSession.boundary.changeId,
|
|
278
|
+
undoSession.boundary.createdAfter
|
|
279
|
+
);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
282
|
+
this.errors$.next(normalized);
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
}),
|
|
286
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
this.redoHistories$ = this.redoStack.items$;
|
|
290
|
+
|
|
291
|
+
this.undoCount$ = this.undoHistories$.pipe(
|
|
292
|
+
map(histories => histories.length),
|
|
293
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
294
|
+
);
|
|
295
|
+
this.redoCount$ = this.redoHistories$.pipe(
|
|
296
|
+
map(histories => histories.length),
|
|
297
|
+
shareReplay({ bufferSize: 1, refCount: true })
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
this.pushableCount$ = this.#pushableCount$.asObservable();
|
|
301
|
+
this.pullableCount$ = this.#pullableCount$.asObservable();
|
|
302
|
+
|
|
303
|
+
const pushableCountSub = combineLatest({
|
|
304
|
+
connected: this.rxdb.connected$,
|
|
305
|
+
branch: current_branch$
|
|
306
|
+
})
|
|
307
|
+
.pipe(
|
|
308
|
+
filter(({ connected, branch }) => connected && !!branch),
|
|
309
|
+
switchMap(({ branch }) => {
|
|
310
|
+
const baseRules = [
|
|
311
|
+
{ field: 'branchId' as const, operator: '=' as const, value: branch!.id },
|
|
312
|
+
{ field: 'revertChangeId' as const, operator: '=' as const, value: null },
|
|
313
|
+
{ field: 'remoteId' as const, operator: '=' as const, value: null },
|
|
314
|
+
// 被拒变更(rejectedAt 非空)的 remoteId 也一直是 null,但它永远不会再被
|
|
315
|
+
// 推送,必须和 remoteId = null 一起排除,否则无关的被拒变更一有风吹草动
|
|
316
|
+
// 就会白触发一次 pushableCount 重算
|
|
317
|
+
{ field: 'rejectedAt' as const, operator: '=' as const, value: null }
|
|
318
|
+
];
|
|
319
|
+
return changeRepository
|
|
320
|
+
.count({
|
|
321
|
+
where: { combinator: 'and', rules: baseRules }
|
|
322
|
+
})
|
|
323
|
+
.pipe(
|
|
324
|
+
// 降级而不终结:一次计数查询失败不该让整条 pushableCount 流停摆,
|
|
325
|
+
// 所以这里仍然返回 EMPTY。但**必须报出去** —— 从前是裸的
|
|
326
|
+
// `catchError(() => EMPTY)`,外层 subscribe 的 error 回调永远等不到,
|
|
327
|
+
// 计数悄悄停在旧值,UI 上「有 N 条待推送」再也不动,没有任何人知道。
|
|
328
|
+
catchError((error: unknown) => {
|
|
329
|
+
this.#reportStreamError('pushableCount 的变更计数查询', error);
|
|
330
|
+
return EMPTY;
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
})
|
|
334
|
+
)
|
|
335
|
+
.subscribe({
|
|
336
|
+
next: () => {
|
|
337
|
+
this.#updatePushableCount();
|
|
338
|
+
},
|
|
339
|
+
error: error => this.#reportStreamError('pushableCount 重算的活跃分支流已中断', error)
|
|
340
|
+
});
|
|
341
|
+
this.#subscriptions.push(pushableCountSub);
|
|
342
|
+
|
|
343
|
+
this.rxdb.addEventListener(REPOSITORY_SYNC_COMPLETE_EVENT, this.#onRepositorySyncComplete);
|
|
344
|
+
this.#updatePushableCount();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
destroy(): void {
|
|
348
|
+
this.#destroyed = true;
|
|
349
|
+
this.#destroy$.next();
|
|
350
|
+
this.#destroy$.complete();
|
|
351
|
+
for (const sub of this.#subscriptions) {
|
|
352
|
+
sub.unsubscribe();
|
|
353
|
+
}
|
|
354
|
+
this.#subscriptions.length = 0;
|
|
355
|
+
this.rxdb.removeEventListener(REPOSITORY_SYNC_COMPLETE_EVENT, this.#onRepositorySyncComplete);
|
|
356
|
+
this.#revertStateWatermarks.clear();
|
|
357
|
+
this.errors$.complete();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 增加远程待 pull 计数 */
|
|
361
|
+
incrementPullableCount(count: number): void {
|
|
362
|
+
this.#pullableGeneration++;
|
|
363
|
+
const newValue = this.#pullableCount$.value + count;
|
|
364
|
+
this.#pullableCount$.next(newValue);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** session 重置时清零 pullable;pull 结束结算走 {@link settlePullableCount} */
|
|
368
|
+
resetPullableCount(): void {
|
|
369
|
+
this.#pullableGeneration++;
|
|
370
|
+
this.#pullableCount$.next(0);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** pull 开始前签发结算令牌,必须在真正发起 pull 之前取 */
|
|
374
|
+
beginPullableSettlement(): number {
|
|
375
|
+
return this.#pullableGeneration;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** 用远端水位线校准待拉计数 */
|
|
379
|
+
reconcilePullableCount(token: number, count: number): void {
|
|
380
|
+
const nextCount = token === this.#pullableGeneration ? count : Math.max(this.#pullableCount$.value, count);
|
|
381
|
+
this.#pullableCount$.next(nextCount);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** 按一次 pull 的实际覆盖范围结算待拉计数 */
|
|
385
|
+
settlePullableCount(token: number, settlement: { complete: boolean; pulled: number }): void {
|
|
386
|
+
this.#pullableCount$.next(
|
|
387
|
+
settledPullableCount(this.#pullableCount$.value, {
|
|
388
|
+
complete: settlement.complete,
|
|
389
|
+
concurrent: token !== this.#pullableGeneration,
|
|
390
|
+
pulled: settlement.pulled
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** 推入 redo 栈(新项在前,超容量丢尾) */
|
|
396
|
+
pushToRedoStack(items: HistoryItem[]): void {
|
|
397
|
+
this.redoStack.push(items);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** 按身份从 redo 栈移除已应用项(作用域 redo 未必在栈顶) */
|
|
401
|
+
removeFromRedoStack(items: HistoryItem[]): HistoryItem[] {
|
|
402
|
+
return this.redoStack.remove(items);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** 清空 redo 栈 */
|
|
406
|
+
clearRedoStack(): void {
|
|
407
|
+
this.redoStack.clear();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** switchBranch 提交后同步切 undo session,避免 await 中间引用比对失配 */
|
|
411
|
+
setUndoBranch(branchId: string): void {
|
|
412
|
+
this.#switchUndoSessionBranch(branchId);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** 清空当前分支 undo/redo;连接级重置用 {@link clearAllUndoHistory} */
|
|
416
|
+
clearUndoHistory(): void {
|
|
417
|
+
this.#setUndoSession({
|
|
418
|
+
generation: this.#nextUndoSessionGeneration(),
|
|
419
|
+
state: 'cleared',
|
|
420
|
+
boundary: this.#undoSession$.value.boundary,
|
|
421
|
+
clearedAt: new Date()
|
|
422
|
+
});
|
|
423
|
+
this.clearRedoStack();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** 清空所有分支 undo/redo,并推进 {@link #undoSessionFloor} */
|
|
427
|
+
clearAllUndoHistory(): void {
|
|
428
|
+
const clearedAt = new Date();
|
|
429
|
+
const toCleared = (session: UndoSession): UndoSession => ({
|
|
430
|
+
generation: this.#nextUndoSessionGeneration(),
|
|
431
|
+
state: 'cleared',
|
|
432
|
+
boundary: session.boundary,
|
|
433
|
+
clearedAt
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
for (const [branchId, session] of this.#undoSessions) {
|
|
437
|
+
this.#undoSessions.set(branchId, toCleared(session));
|
|
438
|
+
}
|
|
439
|
+
this.#undoSessionFloor = toCleared(
|
|
440
|
+
this.#undoSessionFloor ?? { generation: 0, state: 'active', boundary: INITIAL_UNDO_BOUNDARY }
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const current = this.#currentUndoBranchId === null ? undefined : this.#undoSessions.get(this.#currentUndoBranchId);
|
|
444
|
+
this.#undoSession$.next(current ?? this.#undoSessionFloor);
|
|
445
|
+
this.clearRedoStack();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** 用属于当前 clear session 的本地写入事件恢复 undo */
|
|
449
|
+
resetSyncCleared(changeIds: readonly number[] = [], event?: UndoSessionEvent): void {
|
|
450
|
+
const session = this.#undoSession$.value;
|
|
451
|
+
if (session.state === 'active') return;
|
|
452
|
+
|
|
453
|
+
const validChangeIds = changeIds.filter(id => Number.isSafeInteger(id) && id > 0);
|
|
454
|
+
if (event === undefined) {
|
|
455
|
+
if (validChangeIds.length === 0) return;
|
|
456
|
+
const firstPostClearChangeId = minChangeId(validChangeIds);
|
|
457
|
+
this.#setUndoSession({
|
|
458
|
+
generation: session.generation,
|
|
459
|
+
state: 'active',
|
|
460
|
+
boundary: {
|
|
461
|
+
changeId: Math.max(session.boundary.changeId, firstPostClearChangeId - 1),
|
|
462
|
+
createdAfter: session.boundary.createdAfter
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const belongsToCurrentSession =
|
|
469
|
+
event.generation === null ?
|
|
470
|
+
event.recordAt !== null &&
|
|
471
|
+
Number.isFinite(event.recordAt.getTime()) &&
|
|
472
|
+
event.recordAt.getTime() > session.clearedAt.getTime()
|
|
473
|
+
: event.generation === session.generation;
|
|
474
|
+
if (!belongsToCurrentSession) return;
|
|
475
|
+
|
|
476
|
+
const firstPostClearChangeId = validChangeIds.length > 0 ? minChangeId(validChangeIds) : null;
|
|
477
|
+
const createdAfter =
|
|
478
|
+
(
|
|
479
|
+
firstPostClearChangeId === null &&
|
|
480
|
+
(session.boundary.createdAfter === null ||
|
|
481
|
+
session.boundary.createdAfter.getTime() < session.clearedAt.getTime())
|
|
482
|
+
) ?
|
|
483
|
+
session.clearedAt
|
|
484
|
+
: session.boundary.createdAfter;
|
|
485
|
+
|
|
486
|
+
this.#setUndoSession({
|
|
487
|
+
generation: session.generation,
|
|
488
|
+
state: 'active',
|
|
489
|
+
boundary: {
|
|
490
|
+
changeId:
|
|
491
|
+
firstPostClearChangeId === null ?
|
|
492
|
+
session.boundary.changeId
|
|
493
|
+
: Math.max(session.boundary.changeId, firstPostClearChangeId - 1),
|
|
494
|
+
createdAfter
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** 正在执行 undo/redo 或同步 */
|
|
500
|
+
isExecutingUndoRedo(): boolean {
|
|
501
|
+
return this.isUndoRedoInProgress || this.isInvalidatingRedo || this.syncDepth > 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** 在可重入同步上下文中执行:期间变更不影响 undo/redo 栈 */
|
|
505
|
+
async syncing<T>(fn: () => Promise<T>): Promise<T> {
|
|
506
|
+
this.syncDepth += 1;
|
|
507
|
+
try {
|
|
508
|
+
return await fn();
|
|
509
|
+
} finally {
|
|
510
|
+
this.syncDepth -= 1;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 使 redo 栈失效。全部 trigger id ≤ {@link #redoInvalidationFloor} 视为迟到通知,跳过。
|
|
516
|
+
*
|
|
517
|
+
* @remarks
|
|
518
|
+
* 这里**不查** `isUndoRedoInProgress` / `isInvalidatingRedo`:undo、redo 与本方法三个入口
|
|
519
|
+
* 全部排进 {@link HistoryManager.#runSerialized},两个标志又只在同一个序列化任务内部置起
|
|
520
|
+
* 再复位,因此轮到本任务时它们必然已复位——查了也是一条走不到的分支。下面那句
|
|
521
|
+
* `isInvalidatingRedo = true` 留着是给 {@link HistoryManager.isExecutingUndoRedo} 读的,
|
|
522
|
+
* 真正生效的守卫在调用方 `VersionManager` 的 `if (!historyManager.isExecutingUndoRedo())`,
|
|
523
|
+
* 它连 `syncDepth` 一起看,覆盖比这里严。
|
|
524
|
+
*
|
|
525
|
+
* @internal
|
|
526
|
+
*/
|
|
527
|
+
async invalidateRedoStack(triggerChangeIds?: readonly number[]): Promise<void> {
|
|
528
|
+
return this.#runSerialized(async () => {
|
|
529
|
+
if (triggerChangeIds?.length && triggerChangeIds.every(id => id <= this.#redoInvalidationFloor)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
this.isInvalidatingRedo = true;
|
|
534
|
+
try {
|
|
535
|
+
const redoHistories = await firstValueFrom(this.redoHistories$);
|
|
536
|
+
if (redoHistories.length === 0) return;
|
|
537
|
+
|
|
538
|
+
const redoableChanges = redoHistories.flatMap(h => h.changes);
|
|
539
|
+
const now = new Date();
|
|
540
|
+
const actions = get_switch_version_actions([], false);
|
|
541
|
+
const { adapter } = await this.rxdb.versionManager.getLocalRepositories();
|
|
542
|
+
|
|
543
|
+
redoableChanges.forEach(change => {
|
|
544
|
+
const changeKey = `rxdb:RxDBChange:${change.id}`;
|
|
545
|
+
actions.updates.set(changeKey, {
|
|
546
|
+
patch: {
|
|
547
|
+
redoInvalidatedAt: now
|
|
548
|
+
},
|
|
549
|
+
inversePatch: {
|
|
550
|
+
redoInvalidatedAt: null
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// 作废 redo 栈同样只重写投影(行 4)。这里与下面 undo-redo-apply 调的是同一个原语,
|
|
556
|
+
// 两者的结论相反,区别只在这条声明里。
|
|
557
|
+
declareTrustedWrite(adapter, {
|
|
558
|
+
file: 'HistoryManager.ts',
|
|
559
|
+
symbol: 'invalidateRedoStack',
|
|
560
|
+
intent: TrustedWriteIntent.redo_invalidation
|
|
561
|
+
});
|
|
562
|
+
// 不传 branchId:这里只想把 actions 套用在**当前**分支上。自己先查再传会留下一个
|
|
563
|
+
// 采样窗口——本方法是 detached 任务(变更通知还会被批处理 / 跨进程延迟),窗口内的一次
|
|
564
|
+
// 真实 switchBranch 会让这条调用把 activated 与全部触发器倒回旧分支。
|
|
565
|
+
// 同上:分支不换,没有前置条件可校验。
|
|
566
|
+
await adapter.switchBranch({ actions, prepare: SKIP_BRANCH_SWITCH_PREPARE });
|
|
567
|
+
|
|
568
|
+
this.clearRedoStack();
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.error('[HistoryManager] Failed to invalidate redo stack:', error);
|
|
571
|
+
throw error;
|
|
572
|
+
} finally {
|
|
573
|
+
this.isInvalidatingRedo = false;
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** 创建(或取出缓存的)作用域历史 API */
|
|
579
|
+
history<T extends EntityType>(options?: T | InstanceType<T> | HistoryScope): HistoryScopeAPI {
|
|
580
|
+
return createHistoryScopeApi(this.#scopeHost, options);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** 选出完整事务;跨作用域则抛 {@link RxDBCrossScopeTransactionError} */
|
|
584
|
+
#selectWholeTransactions(histories: HistoryItem[], scope: HistoryScope, step: number): HistoryItem[] {
|
|
585
|
+
const { crossScope, selected } = selectScopedHistories(histories, scope, step);
|
|
586
|
+
if (crossScope.length > 0) {
|
|
587
|
+
throw new RxDBCrossScopeTransactionError(scope, crossScope);
|
|
588
|
+
}
|
|
589
|
+
return selected;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** 分配一个全局唯一的 undo session 代次 */
|
|
593
|
+
#nextUndoSessionGeneration(): number {
|
|
594
|
+
return ++this.#undoSessionCounter;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** 写当前分支的 session:真源落在 {@link #undoSessions},视图同步跟上 */
|
|
598
|
+
#setUndoSession(session: UndoSession): void {
|
|
599
|
+
if (this.#currentUndoBranchId !== null) {
|
|
600
|
+
this.#undoSessions.set(this.#currentUndoBranchId, session);
|
|
601
|
+
}
|
|
602
|
+
this.#undoSession$.next(session);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* 落日志并推 `errors$`,不升级成 RxJS 未捕获异常。
|
|
607
|
+
*
|
|
608
|
+
* @remarks
|
|
609
|
+
* 销毁后与适配器停机错误一律静默:那两种情况下的报错是拆卸噪声,不是故障。
|
|
610
|
+
*
|
|
611
|
+
* 措辞由调用方给全(`source` 直接拼进日志),因为两类调用方的后果不同:
|
|
612
|
+
* 订阅上的 `error` 回调意味着那条流**已经终结**,而 `catchError` 里的降级
|
|
613
|
+
* 只是丢掉一次结果、流还活着。用一句写死的「已中断」会把后者说成前者。
|
|
614
|
+
*/
|
|
615
|
+
#reportStreamError(source: string, error: unknown): void {
|
|
616
|
+
if (this.#destroyed || isAdapterShutdownError(error)) return;
|
|
617
|
+
console.error(`[HistoryManager] ${source}:`, error);
|
|
618
|
+
this.errors$.next(error instanceof Error ? error : new Error(String(error)));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** 切到新活跃分支的 session;未解析时的 clear 归属第一个分支 */
|
|
622
|
+
#switchUndoSessionBranch(branchId: string | null): void {
|
|
623
|
+
if (branchId === this.#currentUndoBranchId) return;
|
|
624
|
+
|
|
625
|
+
if (this.#currentUndoBranchId === null && branchId !== null && !this.#undoSessions.has(branchId)) {
|
|
626
|
+
this.#undoSessions.set(branchId, this.#undoSession$.value);
|
|
627
|
+
}
|
|
628
|
+
this.#currentUndoBranchId = branchId;
|
|
629
|
+
if (branchId === null) return;
|
|
630
|
+
|
|
631
|
+
const existing = this.#undoSessions.get(branchId);
|
|
632
|
+
const session = existing ?? this.#inheritUndoSession();
|
|
633
|
+
this.#undoSessions.set(branchId, session);
|
|
634
|
+
this.#undoSession$.next(session);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** 首次访问某个分支时按 {@link #undoSessionFloor} 建它自己的 session */
|
|
638
|
+
#inheritUndoSession(): UndoSession {
|
|
639
|
+
const generation = this.#nextUndoSessionGeneration();
|
|
640
|
+
const floor = this.#undoSessionFloor;
|
|
641
|
+
return floor === null ? { generation, state: 'active', boundary: INITIAL_UNDO_BOUNDARY } : { ...floor, generation };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
#applyLocalRevertState(histories: HistoryItem[]): HistoryItem[] {
|
|
645
|
+
return histories.map(history => {
|
|
646
|
+
let reverted = false;
|
|
647
|
+
|
|
648
|
+
for (const change of history.changes) {
|
|
649
|
+
const persistedState = {
|
|
650
|
+
reverted: change.revertChangeId != null,
|
|
651
|
+
updatedAt: change.updatedAt.getTime()
|
|
652
|
+
};
|
|
653
|
+
const watermark = this.#revertStateWatermarks.get(change.id);
|
|
654
|
+
const currentState = watermark && watermark.updatedAt >= persistedState.updatedAt ? watermark : persistedState;
|
|
655
|
+
|
|
656
|
+
if (watermark && persistedState.updatedAt > watermark.updatedAt) {
|
|
657
|
+
this.#revertStateWatermarks.set(change.id, persistedState);
|
|
658
|
+
}
|
|
659
|
+
reverted ||= currentState.reverted;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return reverted === history.reverted ? history : { ...history, reverted };
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
#getNextRevertStateUpdatedAt(changes: RxDBChange[]): Date {
|
|
667
|
+
let updatedAt = Date.now();
|
|
668
|
+
|
|
669
|
+
for (const change of changes) {
|
|
670
|
+
const watermark = this.#revertStateWatermarks.get(change.id)?.updatedAt ?? 0;
|
|
671
|
+
updatedAt = Math.max(updatedAt, change.updatedAt.getTime() + 1, watermark + 1);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return new Date(updatedAt);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
#setRevertStateWatermarks(changes: RxDBChange[], reverted: boolean, updatedAt: Date): void {
|
|
678
|
+
for (const change of changes) {
|
|
679
|
+
this.#revertStateWatermarks.set(change.id, { reverted, updatedAt: updatedAt.getTime() });
|
|
680
|
+
}
|
|
681
|
+
this.#revertStateWatermarkTrigger$.next(this.#revertStateWatermarkTrigger$.value + 1);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** 串行执行 invalidateRedoStack / undo / redo */
|
|
685
|
+
#runSerialized<T>(task: () => Promise<T>): Promise<T> {
|
|
686
|
+
const result = this.#operation_lock.then(task, task);
|
|
687
|
+
this.#operation_lock = result.catch(() => undefined);
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
#getFirstConnectedAt(): Date {
|
|
692
|
+
if (!this.#firstConnectedAt) {
|
|
693
|
+
this.#firstConnectedAt = this.rxdb.firstConnectedAt ?? this.#sessionStartedAt;
|
|
694
|
+
}
|
|
695
|
+
return this.#firstConnectedAt;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
#isUndoSessionCurrent(session: ActiveUndoSession): boolean {
|
|
699
|
+
return this.#undoSession$.value === session;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async #fetch_latest_histories(
|
|
703
|
+
undoBoundary: UndoBoundary
|
|
704
|
+
): Promise<{ histories: HistoryItem[]; lastPushedMap: Map<string, number> }> {
|
|
705
|
+
return fetchLatestHistories(this.#applyHost, undoBoundary);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
readonly #onRepositorySyncComplete = () => {
|
|
709
|
+
this.#updatePushableCount();
|
|
710
|
+
this.#pushableCountTrigger$.next(Date.now());
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
async #apply_undo_redo_histories(
|
|
714
|
+
operation: 'undo' | 'redo',
|
|
715
|
+
histories: HistoryItem[],
|
|
716
|
+
undoSession?: ActiveUndoSession
|
|
717
|
+
): Promise<void> {
|
|
718
|
+
return applyUndoRedoHistories(this.#applyHost, operation, histories, undoSession);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async #updatePushableCount(): Promise<void> {
|
|
722
|
+
return updatePushableCount(this.#applyHost);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
#createScopeHost(): HistoryScopeApiHost {
|
|
726
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias -- 宿主 getter 需捕获 HistoryManager 私有字段
|
|
727
|
+
const manager = this;
|
|
728
|
+
return {
|
|
729
|
+
get history_cache() {
|
|
730
|
+
return manager.history_cache;
|
|
731
|
+
},
|
|
732
|
+
get history_ref_counts() {
|
|
733
|
+
return manager.history_ref_counts;
|
|
734
|
+
},
|
|
735
|
+
get histories$() {
|
|
736
|
+
return manager.histories$;
|
|
737
|
+
},
|
|
738
|
+
get undoHistories$() {
|
|
739
|
+
return manager.undoHistories$;
|
|
740
|
+
},
|
|
741
|
+
get redoHistories$() {
|
|
742
|
+
return manager.redoHistories$;
|
|
743
|
+
},
|
|
744
|
+
get undoSession$() {
|
|
745
|
+
return manager.#undoSession$;
|
|
746
|
+
},
|
|
747
|
+
runSerialized: task => manager.#runSerialized(task),
|
|
748
|
+
fetchLatestHistories: boundary => manager.#fetch_latest_histories(boundary),
|
|
749
|
+
isUndoSessionCurrent: session => manager.#isUndoSessionCurrent(session),
|
|
750
|
+
selectWholeTransactions: (h, s, n) => manager.#selectWholeTransactions(h, s, n),
|
|
751
|
+
applyUndoRedoHistories: (op, h, s) => manager.#apply_undo_redo_histories(op, h, s)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
#createApplyHost(): UndoRedoApplyHost {
|
|
756
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias -- 宿主 getter 需捕获 HistoryManager 私有字段
|
|
757
|
+
const manager = this;
|
|
758
|
+
return {
|
|
759
|
+
get rxdb() {
|
|
760
|
+
return manager.rxdb;
|
|
761
|
+
},
|
|
762
|
+
get destroyed() {
|
|
763
|
+
return manager.#destroyed;
|
|
764
|
+
},
|
|
765
|
+
get isUndoRedoInProgress() {
|
|
766
|
+
return manager.isUndoRedoInProgress;
|
|
767
|
+
},
|
|
768
|
+
set isUndoRedoInProgress(v) {
|
|
769
|
+
manager.isUndoRedoInProgress = v;
|
|
770
|
+
},
|
|
771
|
+
get redoInvalidationFloor() {
|
|
772
|
+
return manager.#redoInvalidationFloor;
|
|
773
|
+
},
|
|
774
|
+
set redoInvalidationFloor(v) {
|
|
775
|
+
manager.#redoInvalidationFloor = v;
|
|
776
|
+
},
|
|
777
|
+
get pushableGeneration() {
|
|
778
|
+
return manager.#pushableGeneration;
|
|
779
|
+
},
|
|
780
|
+
set pushableGeneration(v) {
|
|
781
|
+
manager.#pushableGeneration = v;
|
|
782
|
+
},
|
|
783
|
+
get pushableCount$() {
|
|
784
|
+
return manager.#pushableCount$;
|
|
785
|
+
},
|
|
786
|
+
get pushableCountTrigger$() {
|
|
787
|
+
return manager.#pushableCountTrigger$;
|
|
788
|
+
},
|
|
789
|
+
get errors$() {
|
|
790
|
+
return manager.errors$;
|
|
791
|
+
},
|
|
792
|
+
getFirstConnectedAt: () => manager.#getFirstConnectedAt(),
|
|
793
|
+
isUndoSessionCurrent: s => manager.#isUndoSessionCurrent(s),
|
|
794
|
+
getNextRevertStateUpdatedAt: c => manager.#getNextRevertStateUpdatedAt(c),
|
|
795
|
+
setRevertStateWatermarks: (c, r, d) => manager.#setRevertStateWatermarks(c, r, d),
|
|
796
|
+
pushToRedoStack: i => manager.pushToRedoStack(i),
|
|
797
|
+
removeFromRedoStack: i => manager.removeFromRedoStack(i)
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
}
|