@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,270 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPushableRepositoryRules,
|
|
3
|
+
declareTrustedWrite,
|
|
4
|
+
type FindOptions,
|
|
5
|
+
type HistoryItem,
|
|
6
|
+
type IRepository,
|
|
7
|
+
isAdapterShutdownError,
|
|
8
|
+
type RxDB,
|
|
9
|
+
type RxDBAdapterLocalBase,
|
|
10
|
+
RxDBBranch,
|
|
11
|
+
RxDBChange,
|
|
12
|
+
type RxDBChangeRuleGroup,
|
|
13
|
+
RxDBSync,
|
|
14
|
+
type RxDBSyncOrderByField,
|
|
15
|
+
type RxDBSyncRuleGroup,
|
|
16
|
+
SKIP_BRANCH_SWITCH_PREPARE,
|
|
17
|
+
TrustedWriteIntent
|
|
18
|
+
} from '@aiao/rxdb';
|
|
19
|
+
import { BehaviorSubject, firstValueFrom, Subject } from 'rxjs';
|
|
20
|
+
import { buildLastPushedMap } from './history-filters.js';
|
|
21
|
+
import { convertChangesToHistories } from './history-item-builder.js';
|
|
22
|
+
import type { ActiveUndoSession, UndoBoundary } from './history-undo-session.types.js';
|
|
23
|
+
import { get_switch_version_actions } from './switch-branch-actions.js';
|
|
24
|
+
type LocalRxDBSyncRepository = IRepository<typeof RxDBSync> & {
|
|
25
|
+
find(options: FindOptions<typeof RxDBSync, RxDBSyncRuleGroup, RxDBSyncOrderByField>): Promise<RxDBSync[]>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getLocalRxDBSyncRepository = (adapter: RxDBAdapterLocalBase): LocalRxDBSyncRepository =>
|
|
29
|
+
adapter.getRepository<typeof RxDBSync, LocalRxDBSyncRepository>(RxDBSync);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* undo / redo / pushableCount 所需的可变状态。
|
|
33
|
+
*
|
|
34
|
+
* `#` 字段必须通过 getter/setter 关闭 captured 实例;Map / Subject 用类持有的同一份引用。
|
|
35
|
+
*/
|
|
36
|
+
export interface UndoRedoApplyHost {
|
|
37
|
+
readonly rxdb: RxDB;
|
|
38
|
+
readonly destroyed: boolean;
|
|
39
|
+
isUndoRedoInProgress: boolean;
|
|
40
|
+
redoInvalidationFloor: number;
|
|
41
|
+
pushableGeneration: number;
|
|
42
|
+
readonly pushableCount$: BehaviorSubject<number>;
|
|
43
|
+
readonly pushableCountTrigger$: BehaviorSubject<number>;
|
|
44
|
+
readonly errors$: Subject<Error>;
|
|
45
|
+
getFirstConnectedAt(): Date;
|
|
46
|
+
isUndoSessionCurrent(session: ActiveUndoSession): boolean;
|
|
47
|
+
getNextRevertStateUpdatedAt(changes: RxDBChange[]): Date;
|
|
48
|
+
setRevertStateWatermarks(changes: RxDBChange[], reverted: boolean, updatedAt: Date): void;
|
|
49
|
+
pushToRedoStack(items: HistoryItem[]): void;
|
|
50
|
+
removeFromRedoStack(items: HistoryItem[]): HistoryItem[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 绕过 Observable 缓存,直接查最新变更再转成历史项。 */
|
|
54
|
+
export async function fetchLatestHistories(
|
|
55
|
+
host: UndoRedoApplyHost,
|
|
56
|
+
undoBoundary: UndoBoundary
|
|
57
|
+
): Promise<{ histories: HistoryItem[]; lastPushedMap: Map<string, number> }> {
|
|
58
|
+
const { changeRepository: adapter, branchRepository } = await host.rxdb.versionManager.getLocalRepositories();
|
|
59
|
+
|
|
60
|
+
const branches = await branchRepository.find({
|
|
61
|
+
where: {
|
|
62
|
+
combinator: 'and',
|
|
63
|
+
rules: [{ field: 'activated', operator: '=', value: true }]
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (branches.length === 0) {
|
|
68
|
+
return { histories: [], lastPushedMap: new Map() };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const branch = branches[0];
|
|
72
|
+
|
|
73
|
+
const localAdapter = await firstValueFrom(host.rxdb.localAdapter$);
|
|
74
|
+
const repoSyncRepo = getLocalRxDBSyncRepository(localAdapter);
|
|
75
|
+
const repoSyncs = await repoSyncRepo.find({
|
|
76
|
+
where: {
|
|
77
|
+
combinator: 'and',
|
|
78
|
+
rules: [{ field: 'branchId', operator: '=', value: branch.id }]
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// 带上在飞认领:远端往返途中的变更视同已推,否则这段窗口里的 undo 会造成永久分叉
|
|
83
|
+
const lastPushedMap = buildLastPushedMap(repoSyncs, host.rxdb.versionManager.pushInFlight.snapshot());
|
|
84
|
+
|
|
85
|
+
const rules: RxDBChangeRuleGroup['rules'] = [
|
|
86
|
+
{
|
|
87
|
+
field: 'branchId',
|
|
88
|
+
operator: '=',
|
|
89
|
+
value: branch.id
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
field: 'createdAt',
|
|
93
|
+
operator: '>=',
|
|
94
|
+
value: host.getFirstConnectedAt()
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
if (undoBoundary.changeId > 0) {
|
|
98
|
+
rules.push({ field: 'id', operator: '>', value: undoBoundary.changeId });
|
|
99
|
+
}
|
|
100
|
+
const allChanges = await adapter.find({
|
|
101
|
+
where: {
|
|
102
|
+
combinator: 'and',
|
|
103
|
+
rules
|
|
104
|
+
},
|
|
105
|
+
orderBy: [{ field: 'id', sort: 'desc' }]
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return { histories: convertChangesToHistories(allChanges), lastPushedMap };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 应用 undo/redo:提取排序 → switchBranch actions → 元数据 → 身份检查 → 切分支 → 栈。
|
|
113
|
+
*
|
|
114
|
+
* undo 在 `adapter.switchBranch` 前必须做 session 对象身份检查。
|
|
115
|
+
*/
|
|
116
|
+
export async function applyUndoRedoHistories(
|
|
117
|
+
host: UndoRedoApplyHost,
|
|
118
|
+
operation: 'undo' | 'redo',
|
|
119
|
+
histories: HistoryItem[],
|
|
120
|
+
undoSession?: ActiveUndoSession
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
host.isUndoRedoInProgress = true;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const changes = histories
|
|
126
|
+
.flatMap(h => h.changes)
|
|
127
|
+
.sort((a, b) => (operation === 'undo' ? b.id - a.id : a.id - b.id));
|
|
128
|
+
|
|
129
|
+
const actions = get_switch_version_actions(changes, operation === 'redo');
|
|
130
|
+
const stateUpdatedAt = host.getNextRevertStateUpdatedAt(changes);
|
|
131
|
+
|
|
132
|
+
if (operation === 'undo') {
|
|
133
|
+
const { adapter } = await host.rxdb.versionManager.getLocalRepositories();
|
|
134
|
+
const seq = await adapter.getRxDBChangeSequence();
|
|
135
|
+
const revertChangedAt = new Date();
|
|
136
|
+
|
|
137
|
+
changes.forEach((change, index) => {
|
|
138
|
+
const changeKey = `rxdb:RxDBChange:${change.id}`;
|
|
139
|
+
const existingUpdate = actions.updates.get(changeKey) || { patch: {}, inversePatch: null };
|
|
140
|
+
actions.updates.set(changeKey, {
|
|
141
|
+
patch: {
|
|
142
|
+
...existingUpdate.patch,
|
|
143
|
+
revertChangeId: seq + index + 1,
|
|
144
|
+
revertChangedAt,
|
|
145
|
+
updatedAt: stateUpdatedAt
|
|
146
|
+
},
|
|
147
|
+
inversePatch: existingUpdate.inversePatch
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
actions.updateRxDBChangeSequence = seq + changes.length;
|
|
151
|
+
host.redoInvalidationFloor = seq + changes.length;
|
|
152
|
+
} else {
|
|
153
|
+
changes.forEach(change => {
|
|
154
|
+
const changeKey = `rxdb:RxDBChange:${change.id}`;
|
|
155
|
+
const existingUpdate = actions.updates.get(changeKey) || { patch: {}, inversePatch: null };
|
|
156
|
+
actions.updates.set(changeKey, {
|
|
157
|
+
patch: {
|
|
158
|
+
...existingUpdate.patch,
|
|
159
|
+
revertChangeId: null,
|
|
160
|
+
updatedAt: stateUpdatedAt
|
|
161
|
+
},
|
|
162
|
+
inversePatch: existingUpdate.inversePatch
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const { adapter } = await host.rxdb.versionManager.getLocalRepositories();
|
|
168
|
+
if (operation === 'undo') {
|
|
169
|
+
if (undoSession === undefined || !host.isUndoSessionCurrent(undoSession)) return;
|
|
170
|
+
}
|
|
171
|
+
// undo/redo 是用户可感知的编辑,矩阵行 6 要求它**产生**单元(origin='undo_redo')。
|
|
172
|
+
declareTrustedWrite(adapter, {
|
|
173
|
+
file: 'undo-redo-apply.ts',
|
|
174
|
+
symbol: 'applyUndoRedoHistories',
|
|
175
|
+
intent: TrustedWriteIntent.undo_redo
|
|
176
|
+
});
|
|
177
|
+
// 不传 branchId:回放只作用于当前分支。在事务外采样分支 id 会让这次写入在并发切换时
|
|
178
|
+
// 把 activated 与全部触发器倒回旧分支(见 SwitchBranchOptions.branchId)。
|
|
179
|
+
// 分支一动不动,所以没有前置条件可校验——这里的豁免是一句「我确实不需要」,不是漏传。
|
|
180
|
+
await adapter.switchBranch({ actions, prepare: SKIP_BRANCH_SWITCH_PREPARE });
|
|
181
|
+
|
|
182
|
+
host.pushableCountTrigger$.next(Date.now());
|
|
183
|
+
host.setRevertStateWatermarks(changes, operation === 'undo', stateUpdatedAt);
|
|
184
|
+
|
|
185
|
+
if (operation === 'undo') {
|
|
186
|
+
host.pushToRedoStack(histories);
|
|
187
|
+
} else {
|
|
188
|
+
host.removeFromRedoStack(histories);
|
|
189
|
+
}
|
|
190
|
+
} finally {
|
|
191
|
+
host.isUndoRedoInProgress = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 按 repository 水位线异步刷新 pushableCount。
|
|
197
|
+
*
|
|
198
|
+
* 只有仍是最新那次刷新才有资格改写计数。失败字符串 `'[#updatePushableCount] error'` 不得改。
|
|
199
|
+
*/
|
|
200
|
+
export async function updatePushableCount(host: UndoRedoApplyHost): Promise<void> {
|
|
201
|
+
const generation = ++host.pushableGeneration;
|
|
202
|
+
const publish = (count: number): void => {
|
|
203
|
+
if (generation === host.pushableGeneration) {
|
|
204
|
+
host.pushableCount$.next(count);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const connected = await firstValueFrom(host.rxdb.connected$);
|
|
210
|
+
if (!connected) {
|
|
211
|
+
publish(0);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const localAdapter = await firstValueFrom(host.rxdb.localAdapter$);
|
|
216
|
+
const RxDBSyncRepo = getLocalRxDBSyncRepository(localAdapter);
|
|
217
|
+
const changeRepository = host.rxdb.entityManager.getRepository(RxDBChange);
|
|
218
|
+
const branch = await firstValueFrom(
|
|
219
|
+
host.rxdb.entityManager.getRepository(RxDBBranch).findOne({
|
|
220
|
+
where: {
|
|
221
|
+
combinator: 'and',
|
|
222
|
+
rules: [{ field: 'activated', operator: '=', value: true }]
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (!branch) {
|
|
228
|
+
publish(0);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const repoSyncs = await RxDBSyncRepo.find({
|
|
233
|
+
where: {
|
|
234
|
+
combinator: 'and',
|
|
235
|
+
rules: [{ field: 'branchId', operator: '=', value: branch.id }]
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const repoRules = buildPushableRepositoryRules(host.rxdb.config.entities, host.rxdb.entitySync, repoSyncs);
|
|
240
|
+
if (repoRules.length === 0) {
|
|
241
|
+
publish(0);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const count = await firstValueFrom(
|
|
246
|
+
changeRepository.count({
|
|
247
|
+
where: {
|
|
248
|
+
combinator: 'and',
|
|
249
|
+
rules: [
|
|
250
|
+
{ field: 'branchId', operator: '=', value: branch.id },
|
|
251
|
+
{ field: 'revertChangeId', operator: '=', value: null },
|
|
252
|
+
{ field: 'remoteId', operator: '=', value: null },
|
|
253
|
+
{
|
|
254
|
+
combinator: 'or',
|
|
255
|
+
rules: repoRules
|
|
256
|
+
}
|
|
257
|
+
]
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
publish(count);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (!host.destroyed && !isAdapterShutdownError(error)) {
|
|
265
|
+
console.error('[#updatePushableCount] error', error);
|
|
266
|
+
host.errors$.next(error instanceof Error ? error : new Error(String(error)));
|
|
267
|
+
}
|
|
268
|
+
publish(0);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 版本管理器的纯函数工具集
|
|
3
|
+
*
|
|
4
|
+
* 从 {@link VersionManager} 抽出的无状态辅助函数,供变更事件处理流程复用。
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* 同步侧那两个(`hasSyncedData` / `partialResultOf`)随 US-025 阶段 D 去了
|
|
8
|
+
* `@aiao/rxdb-plugin-sync` 的 `sync-manager.utils.ts`:它们的形参是
|
|
9
|
+
* `SyncRepositoryResult`,留在这里会让历史包反向依赖同步包。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const getPositiveSafeInteger = (value: unknown): number | null =>
|
|
13
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 从变更事件里提取数字 id。
|
|
17
|
+
*
|
|
18
|
+
* @param change - 变更事件,优先取 `change.id`,缺失时回退到 `change.patch.id`
|
|
19
|
+
* @returns 正整数 id,无法提取时返回 null
|
|
20
|
+
*/
|
|
21
|
+
export const getRxDBChangeEventId = (change: { id: unknown; patch?: unknown }): number | null => {
|
|
22
|
+
const eventId = getPositiveSafeInteger(change.id);
|
|
23
|
+
if (eventId !== null) return eventId;
|
|
24
|
+
if (typeof change.patch !== 'object' || change.patch === null || !('id' in change.patch)) return null;
|
|
25
|
+
return getPositiveSafeInteger(change.patch.id);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 取一组变更里最早的 `recordAt` 时间。
|
|
30
|
+
*
|
|
31
|
+
* @param changes - 变更数组
|
|
32
|
+
* @returns 最早的有效 `recordAt`,没有有效记录时返回 null
|
|
33
|
+
*/
|
|
34
|
+
export const getEarliestRecordAt = (changes: readonly { recordAt?: unknown }[]): Date | null => {
|
|
35
|
+
let earliest: Date | null = null;
|
|
36
|
+
for (const change of changes) {
|
|
37
|
+
if (!(change.recordAt instanceof Date) || !Number.isFinite(change.recordAt.getTime())) continue;
|
|
38
|
+
if (earliest === null || change.recordAt.getTime() < earliest.getTime()) {
|
|
39
|
+
earliest = change.recordAt;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return earliest;
|
|
43
|
+
};
|