@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,131 @@
1
+ /**
2
+ * @fileoverview 同步插件向历史插件借的那一小块面
3
+ *
4
+ * US-025 阶段 D 把推拉同步从 `@aiao/rxdb-plugin-history` 切到
5
+ * `@aiao/rxdb-plugin-sync` 之后,两边仍有一条**单向**耦合:一次 pull / push 结束时,
6
+ * undo 边界要作废、待拉计数要结算 —— 这些状态的主人是 {@link HistoryManager},
7
+ * 而触发时机只有同步侧知道。
8
+ *
9
+ * 因此不把 {@link HistoryManager} 整个交出去,只交这一张接口:
10
+ *
11
+ * - 方向是单向的。同步插件 `inject: ['plugin:history']`,历史插件对同步插件一无所知,
12
+ * `@aiao/rxdb-plugin-history` 的 `dependencies` 里也不会出现同步包 —— 否则两包互指。
13
+ * - 面是窄的。八个成员,全部围绕「一次同步往返结束后历史该怎么变」,不透出 undo 栈、
14
+ * redo 栈、作用域 API 这些同步侧根本不该碰的东西。
15
+ * - 结算口径留在历史侧。{@link SyncHistoryBridge.settlePull} 收 `PullOptions` /
16
+ * `PullResult` 原件,「这次算不算完整同步」由 `pullable-count.ts` 判定;
17
+ * 同步侧不复制一份判据,两边也就不会漂。
18
+ */
19
+
20
+ import type { PullOptions, PullResult } from '@aiao/rxdb';
21
+ import type { HistoryManager } from './HistoryManager.js';
22
+ import { isCompletePull } from './pullable-count.js';
23
+ import type { PushInFlightRegistry } from './push-inflight.js';
24
+
25
+ /**
26
+ * 同步插件可见的历史侧能力。
27
+ *
28
+ * @remarks
29
+ * 由 `VersionManager.syncBridge` 提供,`SyncManager` 在构造时接过去。
30
+ *
31
+ * @internal
32
+ */
33
+ export interface SyncHistoryBridge {
34
+ /**
35
+ * 「哪些变更此刻正在飞往远端」的登记处。
36
+ *
37
+ * @remarks
38
+ * push 与 undo 唯一的会合点,主人是 {@link VersionManager} —— undo 侧经
39
+ * `rxdb.versionManager.pushInFlight` 读它,push 侧经本桥认领区间。
40
+ * 两边读的必须是同一个实例,见 {@link PushInFlightRegistry}。
41
+ */
42
+ readonly pushInFlight: PushInFlightRegistry;
43
+
44
+ /**
45
+ * 在可重入同步上下文中执行:期间产生的本地写入不进 undo/redo 栈。
46
+ *
47
+ * @param fn - 同步动作
48
+ */
49
+ syncing<T>(fn: () => Promise<T>): Promise<T>;
50
+
51
+ /** 与远端合并之后清空当前分支的 undo/redo —— 合并前的内容已经 undo 不回去了 */
52
+ clearUndoHistory(): void;
53
+
54
+ /**
55
+ * pull 开始前签发结算令牌。
56
+ *
57
+ * @returns 令牌,交回 {@link settlePull} / {@link settleAbortedPull} /
58
+ * {@link reconcilePullableCount}
59
+ *
60
+ * @remarks
61
+ * 必须在真正发起 pull **之前**取:拉取期间到达的远端事件描述的是快照之后的新变更,
62
+ * 结算时要能看出来,否则「完整同步」的归零会把它们一起吞掉。
63
+ */
64
+ beginPullableSettlement(): number;
65
+
66
+ /**
67
+ * 按一次成功 pull 的实际覆盖范围结算待拉计数。
68
+ *
69
+ * @param token - {@link beginPullableSettlement} 签发的令牌
70
+ * @param options - 发起 pull 时的选项
71
+ * @param result - pull 的结果
72
+ */
73
+ settlePull(token: number, options: PullOptions | undefined, result: PullResult): void;
74
+
75
+ /**
76
+ * 按一次抛错 pull 已落库的部分结算待拉计数。
77
+ *
78
+ * @param token - {@link beginPullableSettlement} 签发的令牌
79
+ * @param pulled - 抛错前已经真实落库的条数,一条都没落时传 0
80
+ *
81
+ * @remarks
82
+ * 令牌必须收回,否则代次永远停在旧值,后续结算全被判成「并发」。
83
+ */
84
+ settleAbortedPull(token: number, pulled: number): void;
85
+
86
+ /**
87
+ * 用远端水位线校准待拉计数。
88
+ *
89
+ * @param token - {@link beginPullableSettlement} 签发的令牌
90
+ * @param count - 按各仓库持久化水位线算出的待拉总数
91
+ */
92
+ reconcilePullableCount(token: number, count: number): void;
93
+
94
+ /**
95
+ * 远端事件到达时累加待拉计数。
96
+ *
97
+ * @param count - 本次事件里属于当前激活分支的条数
98
+ */
99
+ incrementPullableCount(count: number): void;
100
+ }
101
+
102
+ /**
103
+ * 造一座桥。
104
+ *
105
+ * @param historyManager - 取当前 {@link HistoryManager} 的闭包
106
+ * @param pushInFlight - 飞行登记处
107
+ *
108
+ * @remarks
109
+ * 第一个参数是闭包而不是实例:`VersionManager` 在 `init()` 里会重建
110
+ * {@link HistoryManager}(上一纪元 destroy 过),桥若捏着旧实例,重连后所有结算
111
+ * 都会打进一个已经拆掉事件总线的管理器。
112
+ *
113
+ * @internal
114
+ */
115
+ export const createSyncHistoryBridge = (
116
+ historyManager: () => HistoryManager,
117
+ pushInFlight: PushInFlightRegistry
118
+ ): SyncHistoryBridge => ({
119
+ pushInFlight,
120
+ syncing: fn => historyManager().syncing(fn),
121
+ clearUndoHistory: () => historyManager().clearUndoHistory(),
122
+ beginPullableSettlement: () => historyManager().beginPullableSettlement(),
123
+ settlePull: (token, options, result) =>
124
+ historyManager().settlePullableCount(token, {
125
+ complete: isCompletePull(options, result),
126
+ pulled: result.pulled
127
+ }),
128
+ settleAbortedPull: (token, pulled) => historyManager().settlePullableCount(token, { complete: false, pulled }),
129
+ reconcilePullableCount: (token, count) => historyManager().reconcilePullableCount(token, count),
130
+ incrementPullableCount: count => historyManager().incrementPullableCount(count)
131
+ });
@@ -0,0 +1,260 @@
1
+ import {
2
+ buildPushableRepositoryRules,
3
+ type FindOptions,
4
+ type HistoryItem,
5
+ type IRepository,
6
+ isAdapterShutdownError,
7
+ type RxDB,
8
+ type RxDBAdapterLocalBase,
9
+ RxDBBranch,
10
+ RxDBChange,
11
+ type RxDBChangeRuleGroup,
12
+ RxDBSync,
13
+ type RxDBSyncOrderByField,
14
+ type RxDBSyncRuleGroup
15
+ } from '@aiao/rxdb';
16
+ import { BehaviorSubject, firstValueFrom, Subject } from 'rxjs';
17
+ import { buildLastPushedMap } from './history-filters.js';
18
+ import { convertChangesToHistories } from './history-item-builder.js';
19
+ import type { ActiveUndoSession, UndoBoundary } from './history-undo-session.types.js';
20
+ import { get_switch_version_actions } from './switch-branch-actions.js';
21
+ type LocalRxDBSyncRepository = IRepository<typeof RxDBSync> & {
22
+ find(options: FindOptions<typeof RxDBSync, RxDBSyncRuleGroup, RxDBSyncOrderByField>): Promise<RxDBSync[]>;
23
+ };
24
+
25
+ export const getLocalRxDBSyncRepository = (adapter: RxDBAdapterLocalBase): LocalRxDBSyncRepository =>
26
+ adapter.getRepository<typeof RxDBSync, LocalRxDBSyncRepository>(RxDBSync);
27
+
28
+ /**
29
+ * undo / redo / pushableCount 所需的可变状态。
30
+ *
31
+ * `#` 字段必须通过 getter/setter 关闭 captured 实例;Map / Subject 用类持有的同一份引用。
32
+ */
33
+ export interface UndoRedoApplyHost {
34
+ readonly rxdb: RxDB;
35
+ readonly destroyed: boolean;
36
+ isUndoRedoInProgress: boolean;
37
+ redoInvalidationFloor: number;
38
+ pushableGeneration: number;
39
+ readonly pushableCount$: BehaviorSubject<number>;
40
+ readonly pushableCountTrigger$: BehaviorSubject<number>;
41
+ readonly errors$: Subject<Error>;
42
+ getFirstConnectedAt(): Date;
43
+ isUndoSessionCurrent(session: ActiveUndoSession): boolean;
44
+ getNextRevertStateUpdatedAt(changes: RxDBChange[]): Date;
45
+ setRevertStateWatermarks(changes: RxDBChange[], reverted: boolean, updatedAt: Date): void;
46
+ pushToRedoStack(items: HistoryItem[]): void;
47
+ removeFromRedoStack(items: HistoryItem[]): HistoryItem[];
48
+ }
49
+
50
+ /** 绕过 Observable 缓存,直接查最新变更再转成历史项。 */
51
+ export async function fetchLatestHistories(
52
+ host: UndoRedoApplyHost,
53
+ undoBoundary: UndoBoundary
54
+ ): Promise<{ histories: HistoryItem[]; lastPushedMap: Map<string, number> }> {
55
+ const { changeRepository: adapter, branchRepository } = await host.rxdb.versionManager.getLocalRepositories();
56
+
57
+ const branches = await branchRepository.find({
58
+ where: {
59
+ combinator: 'and',
60
+ rules: [{ field: 'activated', operator: '=', value: true }]
61
+ }
62
+ });
63
+
64
+ if (branches.length === 0) {
65
+ return { histories: [], lastPushedMap: new Map() };
66
+ }
67
+
68
+ const branch = branches[0];
69
+
70
+ const localAdapter = await firstValueFrom(host.rxdb.localAdapter$);
71
+ const repoSyncRepo = getLocalRxDBSyncRepository(localAdapter);
72
+ const repoSyncs = await repoSyncRepo.find({
73
+ where: {
74
+ combinator: 'and',
75
+ rules: [{ field: 'branchId', operator: '=', value: branch.id }]
76
+ }
77
+ });
78
+
79
+ // 带上在飞认领:远端往返途中的变更视同已推,否则这段窗口里的 undo 会造成永久分叉
80
+ const lastPushedMap = buildLastPushedMap(repoSyncs, host.rxdb.versionManager.pushInFlight.snapshot());
81
+
82
+ const rules: RxDBChangeRuleGroup['rules'] = [
83
+ {
84
+ field: 'branchId',
85
+ operator: '=',
86
+ value: branch.id
87
+ },
88
+ {
89
+ field: 'createdAt',
90
+ operator: '>=',
91
+ value: host.getFirstConnectedAt()
92
+ }
93
+ ];
94
+ if (undoBoundary.changeId > 0) {
95
+ rules.push({ field: 'id', operator: '>', value: undoBoundary.changeId });
96
+ }
97
+ const allChanges = await adapter.find({
98
+ where: {
99
+ combinator: 'and',
100
+ rules
101
+ },
102
+ orderBy: [{ field: 'id', sort: 'desc' }]
103
+ });
104
+
105
+ return { histories: convertChangesToHistories(allChanges), lastPushedMap };
106
+ }
107
+
108
+ /**
109
+ * 应用 undo/redo:提取排序 → switchBranch actions → 元数据 → 身份检查 → 切分支 → 栈。
110
+ *
111
+ * undo 在 `adapter.switchBranch` 前必须做 session 对象身份检查。
112
+ */
113
+ export async function applyUndoRedoHistories(
114
+ host: UndoRedoApplyHost,
115
+ operation: 'undo' | 'redo',
116
+ histories: HistoryItem[],
117
+ undoSession?: ActiveUndoSession
118
+ ): Promise<void> {
119
+ host.isUndoRedoInProgress = true;
120
+
121
+ try {
122
+ const changes = histories
123
+ .flatMap(h => h.changes)
124
+ .sort((a, b) => (operation === 'undo' ? b.id - a.id : a.id - b.id));
125
+
126
+ const actions = get_switch_version_actions(changes, operation === 'redo');
127
+ const stateUpdatedAt = host.getNextRevertStateUpdatedAt(changes);
128
+
129
+ if (operation === 'undo') {
130
+ const { adapter } = await host.rxdb.versionManager.getLocalRepositories();
131
+ const seq = await adapter.getRxDBChangeSequence();
132
+ const revertChangedAt = new Date();
133
+
134
+ changes.forEach((change, index) => {
135
+ const changeKey = `rxdb:RxDBChange:${change.id}`;
136
+ const existingUpdate = actions.updates.get(changeKey) || { patch: {}, inversePatch: null };
137
+ actions.updates.set(changeKey, {
138
+ patch: {
139
+ ...existingUpdate.patch,
140
+ revertChangeId: seq + index + 1,
141
+ revertChangedAt,
142
+ updatedAt: stateUpdatedAt
143
+ },
144
+ inversePatch: existingUpdate.inversePatch
145
+ });
146
+ });
147
+ actions.updateRxDBChangeSequence = seq + changes.length;
148
+ host.redoInvalidationFloor = seq + changes.length;
149
+ } else {
150
+ changes.forEach(change => {
151
+ const changeKey = `rxdb:RxDBChange:${change.id}`;
152
+ const existingUpdate = actions.updates.get(changeKey) || { patch: {}, inversePatch: null };
153
+ actions.updates.set(changeKey, {
154
+ patch: {
155
+ ...existingUpdate.patch,
156
+ revertChangeId: null,
157
+ updatedAt: stateUpdatedAt
158
+ },
159
+ inversePatch: existingUpdate.inversePatch
160
+ });
161
+ });
162
+ }
163
+
164
+ const { adapter } = await host.rxdb.versionManager.getLocalRepositories();
165
+ if (operation === 'undo') {
166
+ if (undoSession === undefined || !host.isUndoSessionCurrent(undoSession)) return;
167
+ }
168
+ // 不传 branchId:回放只作用于当前分支。在事务外采样分支 id 会让这次写入在并发切换时
169
+ // 把 activated 与全部触发器倒回旧分支(见 SwitchBranchOptions.branchId)。
170
+ await adapter.switchBranch({ actions });
171
+
172
+ host.pushableCountTrigger$.next(Date.now());
173
+ host.setRevertStateWatermarks(changes, operation === 'undo', stateUpdatedAt);
174
+
175
+ if (operation === 'undo') {
176
+ host.pushToRedoStack(histories);
177
+ } else {
178
+ host.removeFromRedoStack(histories);
179
+ }
180
+ } finally {
181
+ host.isUndoRedoInProgress = false;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * 按 repository 水位线异步刷新 pushableCount。
187
+ *
188
+ * 只有仍是最新那次刷新才有资格改写计数。失败字符串 `'[#updatePushableCount] error'` 不得改。
189
+ */
190
+ export async function updatePushableCount(host: UndoRedoApplyHost): Promise<void> {
191
+ const generation = ++host.pushableGeneration;
192
+ const publish = (count: number): void => {
193
+ if (generation === host.pushableGeneration) {
194
+ host.pushableCount$.next(count);
195
+ }
196
+ };
197
+
198
+ try {
199
+ const connected = await firstValueFrom(host.rxdb.connected$);
200
+ if (!connected) {
201
+ publish(0);
202
+ return;
203
+ }
204
+
205
+ const localAdapter = await firstValueFrom(host.rxdb.localAdapter$);
206
+ const RxDBSyncRepo = getLocalRxDBSyncRepository(localAdapter);
207
+ const changeRepository = host.rxdb.entityManager.getRepository(RxDBChange);
208
+ const branch = await firstValueFrom(
209
+ host.rxdb.entityManager.getRepository(RxDBBranch).findOne({
210
+ where: {
211
+ combinator: 'and',
212
+ rules: [{ field: 'activated', operator: '=', value: true }]
213
+ }
214
+ })
215
+ );
216
+
217
+ if (!branch) {
218
+ publish(0);
219
+ return;
220
+ }
221
+
222
+ const repoSyncs = await RxDBSyncRepo.find({
223
+ where: {
224
+ combinator: 'and',
225
+ rules: [{ field: 'branchId', operator: '=', value: branch.id }]
226
+ }
227
+ });
228
+
229
+ const repoRules = buildPushableRepositoryRules(host.rxdb.config.entities, host.rxdb.config.sync, repoSyncs);
230
+ if (repoRules.length === 0) {
231
+ publish(0);
232
+ return;
233
+ }
234
+
235
+ const count = await firstValueFrom(
236
+ changeRepository.count({
237
+ where: {
238
+ combinator: 'and',
239
+ rules: [
240
+ { field: 'branchId', operator: '=', value: branch.id },
241
+ { field: 'revertChangeId', operator: '=', value: null },
242
+ { field: 'remoteId', operator: '=', value: null },
243
+ {
244
+ combinator: 'or',
245
+ rules: repoRules
246
+ }
247
+ ]
248
+ }
249
+ })
250
+ );
251
+
252
+ publish(count);
253
+ } catch (error) {
254
+ if (!host.destroyed && !isAdapterShutdownError(error)) {
255
+ console.error('[#updatePushableCount] error', error);
256
+ host.errors$.next(error instanceof Error ? error : new Error(String(error)));
257
+ }
258
+ publish(0);
259
+ }
260
+ }
@@ -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
+ };