@aiao/rxdb-plugin-sync 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 +81 -2
- package/dist/SyncManager.d.ts +412 -0
- package/dist/SyncManager.d.ts.map +1 -0
- package/dist/branch-change-id.d.ts +31 -0
- package/dist/branch-change-id.d.ts.map +1 -0
- package/dist/branch-utils.d.ts +8 -0
- package/dist/branch-utils.d.ts.map +1 -0
- package/dist/bulk-sync.d.ts +117 -0
- package/dist/bulk-sync.d.ts.map +1 -0
- package/dist/cascade-blocking.d.ts +38 -0
- package/dist/cascade-blocking.d.ts.map +1 -0
- package/dist/check-repository-updates.d.ts +47 -0
- package/dist/check-repository-updates.d.ts.map +1 -0
- package/dist/cleanup-expired.d.ts +56 -0
- package/dist/cleanup-expired.d.ts.map +1 -0
- package/dist/dependency-graph.d.ts +128 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/get-all-repository-sync-status.d.ts +58 -0
- package/dist/get-all-repository-sync-status.d.ts.map +1 -0
- package/dist/get-repository-sync-status.d.ts +68 -0
- package/dist/get-repository-sync-status.d.ts.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3273 -0
- package/dist/plugin.d.ts +70 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/pull-ancestor-changes.d.ts +41 -0
- package/dist/pull-ancestor-changes.d.ts.map +1 -0
- package/dist/pull-batch.d.ts +16 -0
- package/dist/pull-batch.d.ts.map +1 -0
- package/dist/pull-conflict-utils.d.ts +43 -0
- package/dist/pull-conflict-utils.d.ts.map +1 -0
- package/dist/pull-repository.d.ts +36 -0
- package/dist/pull-repository.d.ts.map +1 -0
- package/dist/pull.d.ts +16 -0
- package/dist/pull.d.ts.map +1 -0
- package/dist/push-branch.d.ts +22 -0
- package/dist/push-branch.d.ts.map +1 -0
- package/dist/push-repository.d.ts +63 -0
- package/dist/push-repository.d.ts.map +1 -0
- package/dist/push.d.ts +14 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/query-cache-outbox.d.ts +120 -0
- package/dist/query-cache-outbox.d.ts.map +1 -0
- package/dist/sync-branches.d.ts +35 -0
- package/dist/sync-branches.d.ts.map +1 -0
- package/dist/sync-listeners.d.ts +20 -0
- package/dist/sync-listeners.d.ts.map +1 -0
- package/dist/sync-manager.utils.d.ts +36 -0
- package/dist/sync-manager.utils.d.ts.map +1 -0
- package/dist/sync-repository.d.ts +63 -0
- package/dist/sync-repository.d.ts.map +1 -0
- package/dist/topological-sort.d.ts +126 -0
- package/dist/topological-sort.d.ts.map +1 -0
- package/dist/version.utils.d.ts +3 -0
- package/dist/version.utils.d.ts.map +1 -0
- package/package.json +58 -3
- package/src/SyncManager.ts +662 -0
- package/src/branch-change-id.ts +70 -0
- package/src/branch-utils.ts +37 -0
- package/src/bulk-sync.ts +338 -0
- package/src/cascade-blocking.ts +61 -0
- package/src/check-repository-updates.ts +143 -0
- package/src/cleanup-expired.ts +271 -0
- package/src/dependency-graph.ts +324 -0
- package/src/get-all-repository-sync-status.ts +115 -0
- package/src/get-repository-sync-status.ts +237 -0
- package/src/index.ts +53 -0
- package/src/plugin.ts +124 -0
- package/src/pull-ancestor-changes.ts +61 -0
- package/src/pull-batch.ts +410 -0
- package/src/pull-conflict-utils.ts +280 -0
- package/src/pull-repository.ts +701 -0
- package/src/pull.ts +204 -0
- package/src/push-branch.ts +61 -0
- package/src/push-repository.ts +1002 -0
- package/src/push.ts +104 -0
- package/src/query-cache-outbox.ts +866 -0
- package/src/sync-branches.ts +156 -0
- package/src/sync-listeners.ts +289 -0
- package/src/sync-manager.utils.ts +48 -0
- package/src/sync-repository.ts +302 -0
- package/src/topological-sort.ts +246 -0
- package/src/version.utils.ts +20 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCurrentBranch,
|
|
3
|
+
getEntityMetadata,
|
|
4
|
+
getLocalSystemRepositories,
|
|
5
|
+
getRemoteSystemRepositories,
|
|
6
|
+
PullOptions,
|
|
7
|
+
PullResult,
|
|
8
|
+
PushOptions,
|
|
9
|
+
PushResult,
|
|
10
|
+
RxDB,
|
|
11
|
+
RxDBPartialSyncError,
|
|
12
|
+
SyncResult
|
|
13
|
+
} from '@aiao/rxdb';
|
|
14
|
+
import type { PushInFlightRegistry, SyncHistoryBridge } from '@aiao/rxdb-plugin-history';
|
|
15
|
+
import { Subscription } from 'rxjs';
|
|
16
|
+
import { bulkSync, type BulkSyncOptions, type BulkSyncResult } from './bulk-sync.js';
|
|
17
|
+
import { checkRepositoryUpdates, type CheckRepositoryUpdatesResult } from './check-repository-updates.js';
|
|
18
|
+
import { cleanupExpired, type CleanupExpiredOptions, type CleanupExpiredResult } from './cleanup-expired.js';
|
|
19
|
+
import { buildDependencyGraph, type DependencyGraph, type RepositoryIdentifier } from './dependency-graph.js';
|
|
20
|
+
import { getAllRepositorySyncStatus, type GetAllRepositorySyncStatusFilter } from './get-all-repository-sync-status.js';
|
|
21
|
+
import { getRepositorySyncStatus, type RepositorySyncStatus } from './get-repository-sync-status.js';
|
|
22
|
+
import { pullRepository, type PullRepositoryOptions, type PullRepositoryResult } from './pull-repository.js';
|
|
23
|
+
import { pull } from './pull.js';
|
|
24
|
+
import { pushRepository, type PushRepositoryOptions, type PushRepositoryResult } from './push-repository.js';
|
|
25
|
+
import { push } from './push.js';
|
|
26
|
+
import { syncBranches, type SyncBranchesResult } from './sync-branches.js';
|
|
27
|
+
import { setupSyncListeners } from './sync-listeners.js';
|
|
28
|
+
import { hasSyncedData, partialResultOf, partialSyncInvalidatesHistory } from './sync-manager.utils.js';
|
|
29
|
+
import { syncRepository, type SyncRepositoryOptions, type SyncRepositoryResult } from './sync-repository.js';
|
|
30
|
+
import { topologicalSort, type SortDirection } from './topological-sort.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 同步管理器
|
|
34
|
+
*
|
|
35
|
+
* 负责整库与单仓库两个粒度的推拉同步:
|
|
36
|
+
* - `pull` / `push` / `sync`:整库
|
|
37
|
+
* - `pullRepository` / `pushRepository` / `syncRepository` / `bulkSync`:按仓库
|
|
38
|
+
* - `getRepositorySyncStatus` / `checkRepositoryUpdates` / `cleanupExpired`:状态与清理
|
|
39
|
+
* - `getRepositoryDependencyGraph` / `getRepositorySyncOrder`:依赖图与同步顺序
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* US-025 阶段 D 之前这些入口挂在 `@aiao/rxdb-plugin-history` 的 `VersionManager` 上。
|
|
43
|
+
* 切开的理由是两件事的生命周期本就不同:撤销重做要在第一条 `rxdb_change` 之前就位,
|
|
44
|
+
* 推拉同步则只在配了远端时才有意义。
|
|
45
|
+
*
|
|
46
|
+
* 切开之后仍有一条**单向**耦合:一次同步往返结束时 undo 边界要作废、待拉计数要结算,
|
|
47
|
+
* 而那些状态的主人是历史侧。它经 {@link SyncHistoryBridge} 这一张窄接口相遇 ——
|
|
48
|
+
* 本插件 `inject: ['plugin:history']`,历史插件则对本包一无所知。
|
|
49
|
+
*/
|
|
50
|
+
export class SyncManager {
|
|
51
|
+
#event_removers: Array<() => void> = [];
|
|
52
|
+
#subscriptions: Subscription[] = [];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 「哪些变更此刻正在飞往远端」的登记处。
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* push 与 undo 唯一的会合点,实例的主人在历史侧(`rxdb.versionManager.pushInFlight`)。
|
|
59
|
+
* 两边读的必须是同一个,见 `PushInFlightRegistry`:push 在远端往返之前认领区间,
|
|
60
|
+
* undo 把认领区间当成已推 —— 各持一份,往返窗口内的一次撤销就会造成永久分叉。
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
get pushInFlight(): PushInFlightRegistry {
|
|
65
|
+
return this.history.pushInFlight;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param rxdb - 宿主实例
|
|
70
|
+
* @param history - 历史侧借来的那一小块面,由 `rxdb.versionManager.syncBridge` 提供
|
|
71
|
+
*/
|
|
72
|
+
constructor(
|
|
73
|
+
public readonly rxdb: RxDB,
|
|
74
|
+
readonly history: SyncHistoryBridge
|
|
75
|
+
) {}
|
|
76
|
+
|
|
77
|
+
/** 装上 connected$ 自动回推与远端事件计数两条链路 */
|
|
78
|
+
init() {
|
|
79
|
+
const { subscriptions, removers } = setupSyncListeners(this);
|
|
80
|
+
this.#subscriptions.push(...subscriptions);
|
|
81
|
+
this.#event_removers.push(...removers);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 拆掉 {@link init} 装上的全部监听与订阅 */
|
|
85
|
+
destroy() {
|
|
86
|
+
for (const remove of this.#event_removers) {
|
|
87
|
+
remove();
|
|
88
|
+
}
|
|
89
|
+
this.#event_removers.length = 0;
|
|
90
|
+
for (const sub of this.#subscriptions) {
|
|
91
|
+
sub.unsubscribe();
|
|
92
|
+
}
|
|
93
|
+
this.#subscriptions.length = 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 取本地适配器上的系统表仓库 */
|
|
97
|
+
async getLocalRepositories() {
|
|
98
|
+
return getLocalSystemRepositories(this.rxdb);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 取远端适配器上的系统表仓库 */
|
|
102
|
+
async getRemoteRepositories() {
|
|
103
|
+
return getRemoteSystemRepositories(this.rxdb);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 取当前分支;没有激活分支时激活(或新建)`main`。
|
|
108
|
+
*
|
|
109
|
+
* @remarks
|
|
110
|
+
* 同步链路里每条远端事件都要调它(`sync-listeners` 的 `filterByBranch`),
|
|
111
|
+
* 热路径不开事务,见 `getCurrentBranch`。
|
|
112
|
+
*/
|
|
113
|
+
async getCurrentBranch() {
|
|
114
|
+
return getCurrentBranch(this.rxdb);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 从远程同步所有分支信息到本地
|
|
119
|
+
*
|
|
120
|
+
* 远程新分支 → 在本地创建(local: false, remote: true)
|
|
121
|
+
* 本地已有的远程分支 → 更新 remote 标记为 true
|
|
122
|
+
* 纯本地分支 → 不受影响
|
|
123
|
+
*
|
|
124
|
+
* @returns 同步结果
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const result = await rxdb.syncManager.syncBranches();
|
|
129
|
+
* console.log(`新增 ${result.created},更新 ${result.updated}`);
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
async syncBranches(): Promise<SyncBranchesResult> {
|
|
133
|
+
return syncBranches(this);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 从远程拉取变更并应用到本地数据库
|
|
138
|
+
*
|
|
139
|
+
* 当 autoSync=false 时,会先应用 Realtime 缓存的变更,再拉取远程变更。
|
|
140
|
+
*
|
|
141
|
+
* @param options - 可选配置
|
|
142
|
+
* @returns 拉取结果
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* // 基本用法
|
|
147
|
+
* const result = await rxdb.syncManager.pull();
|
|
148
|
+
* console.log(`Pulled ${result.pulled} changes`);
|
|
149
|
+
*
|
|
150
|
+
* // 拉取所有数据
|
|
151
|
+
* const result = await rxdb.syncManager.pull({ fetchAll: true });
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
async pull(options?: PullOptions): Promise<PullResult> {
|
|
155
|
+
let result: PullResult;
|
|
156
|
+
try {
|
|
157
|
+
result = await this.history.syncing(() => this.#pullAndSettle(options));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// repositoryFilter 逐仓拉取时,前面的仓库可能已经真实落库
|
|
160
|
+
// 随后某个仓库失败会抛 RxDBPartialSyncError。此前这里直接 rethrow,
|
|
161
|
+
// undo 边界从未按已提交的部分推进,用户仍能 undo 到「合并前」的内容,
|
|
162
|
+
// 与已落库的远端数据产生分叉。
|
|
163
|
+
if (partialSyncInvalidatesHistory(error)) {
|
|
164
|
+
this.history.clearUndoHistory();
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 远端变更改写了本地实体数据时才清空 undo/redo 历史:已与远程合并,无法 undo 合并前的内容。
|
|
170
|
+
// 判据是 historyInvalidated 而非 pulled —— 拉回来的变更可能被压缩全部抵消,
|
|
171
|
+
// 一条实体数据都没动,此时历史边界仍然有效。
|
|
172
|
+
if (result.historyInvalidated) {
|
|
173
|
+
this.history.clearUndoHistory();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 将本地未同步的变更推送到远程数据库
|
|
181
|
+
*
|
|
182
|
+
* 推送时会自动:
|
|
183
|
+
* 1. 查询 lastPushedChangeId 之后的新变更
|
|
184
|
+
* 2. 过滤已撤销的变更(revertChangeId != null)
|
|
185
|
+
* 3. 压缩变更(INSERT→DELETE 丢弃,INSERT→UPDATE* 合并为 INSERT)
|
|
186
|
+
* 4. 批量推送到远程
|
|
187
|
+
* 5. 更新 lastPushedChangeId 和 lastPushedAt
|
|
188
|
+
*
|
|
189
|
+
* @param options - 可选配置
|
|
190
|
+
* @returns 推送结果
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* // 基本用法
|
|
195
|
+
* const result = await rxdb.syncManager.push();
|
|
196
|
+
* console.log(`Pushed ${result.pushed} changes`);
|
|
197
|
+
*
|
|
198
|
+
* // 自定义批量大小
|
|
199
|
+
* const result = await rxdb.syncManager.push({ batchSize: 500 });
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
async push(options?: PushOptions): Promise<PushResult> {
|
|
203
|
+
const result = await this.history.syncing(() => push(this, options));
|
|
204
|
+
|
|
205
|
+
// 当 push 有数据变更时,清空 undo/redo 历史
|
|
206
|
+
// 因为已与远程合并,无法 undo 合并前的内容
|
|
207
|
+
if (result.pushed > 0) {
|
|
208
|
+
this.history.clearUndoHistory();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 执行完整的同步操作(先 pull 再 push)
|
|
216
|
+
*
|
|
217
|
+
* 推荐在重连后使用此方法,确保:
|
|
218
|
+
* 1. 先获取远程最新变更(避免覆盖他人数据)
|
|
219
|
+
* 2. 再推送本地变更
|
|
220
|
+
*
|
|
221
|
+
* @param options - 可选配置
|
|
222
|
+
* @returns 同步结果(包含 pull 和 push 结果)
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```typescript
|
|
226
|
+
* // 基本用法
|
|
227
|
+
* const result = await rxdb.syncManager.sync();
|
|
228
|
+
* console.log(`Pulled ${result.pullResult.pulled}, Pushed ${result.pushResult.pushed}`);
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
async sync(options?: { pull?: PullOptions; push?: PushOptions }): Promise<SyncResult> {
|
|
232
|
+
// pull 成功、push 才抛错时,这段进度不随返回值出来 —— 提到闭包外,catch 里才看得见
|
|
233
|
+
// 「已经合并进来的那部分」。否则远端变更已落库,undo 边界却原地不动。
|
|
234
|
+
let settledPull: PullResult | undefined;
|
|
235
|
+
|
|
236
|
+
let result: SyncResult;
|
|
237
|
+
try {
|
|
238
|
+
result = await this.history.syncing(async () => {
|
|
239
|
+
// 先 pull 再 push
|
|
240
|
+
settledPull = await this.#pullAndSettle(options?.pull);
|
|
241
|
+
const pushResult = await push(this, options?.push);
|
|
242
|
+
|
|
243
|
+
return { pullResult: settledPull, pushResult };
|
|
244
|
+
});
|
|
245
|
+
} catch (error) {
|
|
246
|
+
// 部分成功同样要推进 undo 边界,进度落在两处之一:pull 中途失败时挂在
|
|
247
|
+
// RxDBPartialSyncError.result 上,pull 已结算而 push 失败时只存在于 settledPull。
|
|
248
|
+
if (settledPull?.historyInvalidated === true || partialSyncInvalidatesHistory(error)) {
|
|
249
|
+
this.history.clearUndoHistory();
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 当 pull 改写了实体数据、或 push 有上行时,清空 undo/redo 历史
|
|
255
|
+
// 因为已与远程合并,无法 undo 合并前的内容
|
|
256
|
+
if (result.pullResult.historyInvalidated || result.pushResult.pushed > 0) {
|
|
257
|
+
this.history.clearUndoHistory();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 拉取指定 Repository 的远程变更
|
|
265
|
+
*
|
|
266
|
+
* 提供实体类型级别的精细同步控制,支持级联同步以自动拉取关联实体。
|
|
267
|
+
*
|
|
268
|
+
* @param namespace - 实体命名空间(如 "public")
|
|
269
|
+
* @param entity - 实体名称(如 "Todo")
|
|
270
|
+
* @param options - 拉取选项
|
|
271
|
+
* @returns 拉取结果
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* // 拉取 Todo 并级联拉取依赖
|
|
276
|
+
* const result = await rxdb.syncManager.pullRepository('public', 'Todo', {
|
|
277
|
+
* includeRelated: true // 默认:若 Todo 有外键则自动拉取 User
|
|
278
|
+
* });
|
|
279
|
+
*
|
|
280
|
+
* // 不级联拉取
|
|
281
|
+
* const result = await rxdb.syncManager.pullRepository('public', 'Todo', {
|
|
282
|
+
* includeRelated: false // 仅拉取 Todo,可能引发外键错误
|
|
283
|
+
* });
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
async pullRepository(
|
|
287
|
+
namespace: string,
|
|
288
|
+
entity: string,
|
|
289
|
+
options?: PullRepositoryOptions
|
|
290
|
+
): Promise<PullRepositoryResult> {
|
|
291
|
+
let result: PullRepositoryResult;
|
|
292
|
+
try {
|
|
293
|
+
result = await this.history.syncing(() => pullRepository(this, namespace, entity, options));
|
|
294
|
+
} catch (error) {
|
|
295
|
+
// fetchAll 多轮拉取中途失败时,前面几轮的事务已经真实提交
|
|
296
|
+
// 会抛 RxDBPartialSyncError 而非裸错误。此前这里直接 rethrow,undo 边界
|
|
297
|
+
// 从未按已提交的部分推进,用户仍能 undo 到「合并前」的内容。
|
|
298
|
+
if (partialSyncInvalidatesHistory(error)) {
|
|
299
|
+
this.history.clearUndoHistory();
|
|
300
|
+
}
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 有实体数据被改写时清空 undo/redo 历史(级联依赖仓的改写也算在内)
|
|
305
|
+
if (result.historyInvalidated) {
|
|
306
|
+
this.history.clearUndoHistory();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 推送指定 Repository 的本地变更
|
|
314
|
+
*
|
|
315
|
+
* 提供实体类型级别的精细同步控制,支持级联同步以自动推送依赖实体。
|
|
316
|
+
*
|
|
317
|
+
* @param namespace - 实体命名空间(如 "public")
|
|
318
|
+
* @param entity - 实体名称(如 "User")
|
|
319
|
+
* @param options - 推送选项
|
|
320
|
+
* @returns 推送结果
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* // 推送 User 并级联推送依赖方
|
|
325
|
+
* const result = await rxdb.syncManager.pushRepository('public', 'User', {
|
|
326
|
+
* includeRelated: true // 默认:若 Post 引用 User 则自动推送 Post
|
|
327
|
+
* });
|
|
328
|
+
*
|
|
329
|
+
* // 不级联推送
|
|
330
|
+
* const result = await rxdb.syncManager.pushRepository('public', 'User', {
|
|
331
|
+
* includeRelated: false // 仅推送 User,依赖数据可能不完整
|
|
332
|
+
* });
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
async pushRepository(
|
|
336
|
+
namespace: string,
|
|
337
|
+
entity: string,
|
|
338
|
+
options?: PushRepositoryOptions
|
|
339
|
+
): Promise<PushRepositoryResult> {
|
|
340
|
+
const result = await this.history.syncing(() => pushRepository(this, namespace, entity, options));
|
|
341
|
+
|
|
342
|
+
// 有数据变更时清空 undo/redo 历史
|
|
343
|
+
if (result.pushed > 0) {
|
|
344
|
+
this.history.clearUndoHistory();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 同步指定 Repository(先 pull 再 push)
|
|
352
|
+
*
|
|
353
|
+
* 将单个 Repository 的 pull 和 push 合并为一次操作,保证正确的执行顺序。
|
|
354
|
+
* 推荐在需要确保特定实体类型数据一致性时使用。
|
|
355
|
+
*
|
|
356
|
+
* @param namespace - 实体命名空间(如 "public")
|
|
357
|
+
* @param entity - 实体名称(如 "Todo")
|
|
358
|
+
* @param options - 同步选项(分别配置 pull 和 push)
|
|
359
|
+
* @returns 同步结果(包含 pull 和 push 结果)
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```typescript
|
|
363
|
+
* // 基本用法
|
|
364
|
+
* const result = await rxdb.syncManager.syncRepository('public', 'Todo');
|
|
365
|
+
* console.log(`Pulled ${result.pullResult.pulled}, Pushed ${result.pushResult.pushed}`);
|
|
366
|
+
*
|
|
367
|
+
* // 自定义 pull 和 push 选项
|
|
368
|
+
* const result = await rxdb.syncManager.syncRepository('public', 'Todo', {
|
|
369
|
+
* pull: { limit: 500, fetchAll: true },
|
|
370
|
+
* push: { batchSize: 100 }
|
|
371
|
+
* });
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
async syncRepository(
|
|
375
|
+
namespace: string,
|
|
376
|
+
entity: string,
|
|
377
|
+
options?: SyncRepositoryOptions
|
|
378
|
+
): Promise<SyncRepositoryResult> {
|
|
379
|
+
let result: SyncRepositoryResult;
|
|
380
|
+
try {
|
|
381
|
+
result = await this.history.syncing(() => syncRepository(this, namespace, entity, options));
|
|
382
|
+
} catch (error) {
|
|
383
|
+
// 与 pullRepository 同口径:pull 落库或 push 上行之后失败,都会包成
|
|
384
|
+
// RxDBPartialSyncError 抛出,那部分数据已经与远端合并、undo 不回去了。
|
|
385
|
+
if (partialSyncInvalidatesHistory(error)) {
|
|
386
|
+
this.history.clearUndoHistory();
|
|
387
|
+
}
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 有数据变更时清空 undo/redo 历史
|
|
392
|
+
if (result.pullResult.historyInvalidated || result.pushResult.pushed > 0) {
|
|
393
|
+
this.history.clearUndoHistory();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 清理不再满足过滤条件的本地过期数据
|
|
401
|
+
*
|
|
402
|
+
* 用于 SyncType.Filter 场景,删除不满足 filter 条件的本地数据
|
|
403
|
+
* 例如:清理超过 30 天的订单数据
|
|
404
|
+
*
|
|
405
|
+
* @param namespace - 实体命名空间
|
|
406
|
+
* @param entity - 实体名称
|
|
407
|
+
* @param options - 清理选项
|
|
408
|
+
* @returns 清理结果
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```ts
|
|
412
|
+
* const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
413
|
+
* const { removed } = await rxdb.syncManager.cleanupExpired('public', 'Order', {
|
|
414
|
+
* filter: {
|
|
415
|
+
* combinator: 'and',
|
|
416
|
+
* rules: [{ field: 'updatedAt', operator: '>=', value: thirtyDaysAgo }]
|
|
417
|
+
* }
|
|
418
|
+
* });
|
|
419
|
+
* console.log(`Removed ${removed} expired records`);
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
422
|
+
async cleanupExpired(
|
|
423
|
+
namespace: string,
|
|
424
|
+
entity: string,
|
|
425
|
+
options?: CleanupExpiredOptions
|
|
426
|
+
): Promise<CleanupExpiredResult> {
|
|
427
|
+
return cleanupExpired(this, namespace, entity, options);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 检查远程是否有更新,不下载数据
|
|
432
|
+
*
|
|
433
|
+
* 仅查询远程有多少新变更,不实际拉取数据。
|
|
434
|
+
* 适用于显示「有 N 条远程更新」提示,节省带宽和时间。
|
|
435
|
+
*
|
|
436
|
+
* @param namespace - 实体命名空间(如 "public")
|
|
437
|
+
* @param entity - 实体名称(如 "Todo")
|
|
438
|
+
* @returns 更新检查结果
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```typescript
|
|
442
|
+
* // 检查 Todo 是否有远程更新
|
|
443
|
+
* const result = await rxdb.syncManager.checkRepositoryUpdates('public', 'Todo');
|
|
444
|
+
* if (result.hasUpdates) {
|
|
445
|
+
* console.log(`有 ${result.pendingCount} 条更新可拉取`);
|
|
446
|
+
* // 用户点击「更新」按钮后再调用 pullRepository()
|
|
447
|
+
* }
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
async checkRepositoryUpdates(namespace: string, entity: string): Promise<CheckRepositoryUpdatesResult> {
|
|
451
|
+
return checkRepositoryUpdates(this.rxdb, namespace, entity);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 获取单个 Repository 的同步状态
|
|
456
|
+
*
|
|
457
|
+
* 返回完整的同步状态信息,包括:
|
|
458
|
+
* - syncType(full/remote/local/disabled)
|
|
459
|
+
* - pushableCount(待推送的本地变更数)
|
|
460
|
+
* - pullableCount(可拉取的远程变更数)
|
|
461
|
+
* - 最近同步时间戳
|
|
462
|
+
*
|
|
463
|
+
* @param namespace - 实体命名空间(如 "public")
|
|
464
|
+
* @param entity - 实体名称(如 "Todo")
|
|
465
|
+
* @returns Repository 同步状态
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* ```typescript
|
|
469
|
+
* const status = await rxdb.syncManager.getRepositorySyncStatus('public', 'Todo');
|
|
470
|
+
* console.log(`同步类型: ${status.syncType}`);
|
|
471
|
+
* console.log(`待推送: ${status.pushableCount},可拉取: ${status.pullableCount}`);
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
async getRepositorySyncStatus(namespace: string, entity: string): Promise<RepositorySyncStatus> {
|
|
475
|
+
return getRepositorySyncStatus(this.rxdb, namespace, entity);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 获取所有 Repository 的同步状态
|
|
480
|
+
*
|
|
481
|
+
* 返回所有已注册实体的状态,支持可选过滤。
|
|
482
|
+
*
|
|
483
|
+
* @param filter - 可选过滤条件
|
|
484
|
+
* @returns Repository 同步状态数组
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```typescript
|
|
488
|
+
* // 获取全部状态
|
|
489
|
+
* const statuses = await rxdb.syncManager.getAllRepositorySyncStatus();
|
|
490
|
+
*
|
|
491
|
+
* // 仅获取有待处理变更的 Repository
|
|
492
|
+
* const pending = await rxdb.syncManager.getAllRepositorySyncStatus({
|
|
493
|
+
* hasPendingChanges: true
|
|
494
|
+
* });
|
|
495
|
+
*
|
|
496
|
+
* // 仅获取已启用的全量同步 Repository
|
|
497
|
+
* const fullSync = await rxdb.syncManager.getAllRepositorySyncStatus({
|
|
498
|
+
* syncType: ['full'],
|
|
499
|
+
* enabled: true
|
|
500
|
+
* });
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
async getAllRepositorySyncStatus(filter?: GetAllRepositorySyncStatusFilter): Promise<RepositorySyncStatus[]> {
|
|
504
|
+
return getAllRepositorySyncStatus(this.rxdb, filter);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* 按各仓库持久化的远端水位线重新计算待拉变更数
|
|
509
|
+
*
|
|
510
|
+
* @returns 当前远端待拉变更总数
|
|
511
|
+
*
|
|
512
|
+
* @remarks
|
|
513
|
+
* 远程适配器在实时订阅恢复后调用。若刷新期间又收到实时事件,保留两者中的较大值,
|
|
514
|
+
* 避免把查询快照之后到达的通知覆盖掉。
|
|
515
|
+
*/
|
|
516
|
+
async refreshPullableCount(): Promise<number> {
|
|
517
|
+
const token = this.history.beginPullableSettlement();
|
|
518
|
+
const statuses = await getAllRepositorySyncStatus(this.rxdb);
|
|
519
|
+
const count = statuses.reduce((total, status) => total + (status.enabled ? status.pullableCount : 0), 0);
|
|
520
|
+
this.history.reconcilePullableCount(token, count);
|
|
521
|
+
return count;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 批量同步多个 Repository
|
|
526
|
+
*
|
|
527
|
+
* 在单次操作中同步多个 Repository,支持顺序或并发执行。
|
|
528
|
+
*
|
|
529
|
+
* @param options - 批量同步选项
|
|
530
|
+
* @returns 批量同步结果(包含成功/失败计数)
|
|
531
|
+
*
|
|
532
|
+
* @example
|
|
533
|
+
* ```typescript
|
|
534
|
+
* // 顺序同步所有已启用的 Repository
|
|
535
|
+
* const result = await rxdb.syncManager.bulkSync();
|
|
536
|
+
* console.log(`成功: ${result.succeeded},失败: ${result.failed}`);
|
|
537
|
+
*
|
|
538
|
+
* // 同步指定 Repository
|
|
539
|
+
* const result = await rxdb.syncManager.bulkSync({
|
|
540
|
+
* repositories: [
|
|
541
|
+
* { namespace: 'public', entity: 'Todo' },
|
|
542
|
+
* { namespace: 'public', entity: 'User' }
|
|
543
|
+
* ]
|
|
544
|
+
* });
|
|
545
|
+
*
|
|
546
|
+
* // 并发模式仅 pull
|
|
547
|
+
* const result = await rxdb.syncManager.bulkSync({
|
|
548
|
+
* concurrent: true,
|
|
549
|
+
* concurrency: 3,
|
|
550
|
+
* push: false
|
|
551
|
+
* });
|
|
552
|
+
*
|
|
553
|
+
* // 检查每项结果
|
|
554
|
+
* for (const item of result.results) {
|
|
555
|
+
* if (item.success) {
|
|
556
|
+
* console.log(`${item.repository.entity}: 已拉取 ${item.result?.pullResult.pulled ?? 0} 条`);
|
|
557
|
+
* } else {
|
|
558
|
+
* console.error(`${item.repository.entity}: ${item.error.message}`);
|
|
559
|
+
* }
|
|
560
|
+
* }
|
|
561
|
+
* ```
|
|
562
|
+
*/
|
|
563
|
+
async bulkSync(options?: BulkSyncOptions): Promise<BulkSyncResult> {
|
|
564
|
+
const result = await this.history.syncing(() => bulkSync(this.rxdb, options));
|
|
565
|
+
|
|
566
|
+
// 与 pull/push/sync 保持一致:任一仓库产生数据变更时清空 undo/redo 历史,
|
|
567
|
+
// 因为已与远程合并,无法 undo 合并前的内容。
|
|
568
|
+
//
|
|
569
|
+
// 失败项同样要算:仓库在失败前可能已经提交了部分进度,它藏在
|
|
570
|
+
// `item.error` 的 `RxDBPartialSyncError.result` 里。只看 `item.result` 会漏掉这部分——
|
|
571
|
+
// 远端数据已落库,用户却仍能 undo 回同步前状态,重新制造本地/远端分叉
|
|
572
|
+
const hasChanges = result.results.some(
|
|
573
|
+
item => hasSyncedData(item.result) || hasSyncedData(partialResultOf(item.error))
|
|
574
|
+
);
|
|
575
|
+
if (hasChanges) {
|
|
576
|
+
this.history.clearUndoHistory();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* 获取所有 Repository 的依赖图
|
|
584
|
+
*
|
|
585
|
+
* 分析实体关系(MANY_TO_ONE、ONE_TO_ONE)以构建依赖图,
|
|
586
|
+
* 展示各 Repository 之间的依赖关系。
|
|
587
|
+
*
|
|
588
|
+
* @returns 包含依赖关系的依赖图
|
|
589
|
+
*
|
|
590
|
+
* @example
|
|
591
|
+
* ```typescript
|
|
592
|
+
* const graph = rxdb.syncManager.getRepositoryDependencyGraph();
|
|
593
|
+
*
|
|
594
|
+
* // 遍历依赖关系
|
|
595
|
+
* for (const [key, dep] of graph) {
|
|
596
|
+
* console.log(`${key} 依赖:`, dep.dependsOn);
|
|
597
|
+
* console.log(`${key} 被依赖:`, dep.requiredBy);
|
|
598
|
+
* }
|
|
599
|
+
* ```
|
|
600
|
+
*/
|
|
601
|
+
getRepositoryDependencyGraph(): DependencyGraph {
|
|
602
|
+
const entities = this.rxdb.config.entities.map(EntityClass => getEntityMetadata(EntityClass));
|
|
603
|
+
return buildDependencyGraph(entities);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* 根据依赖关系获取 Repository 的同步顺序
|
|
608
|
+
*
|
|
609
|
+
* 通过拓扑排序确定基于依赖关系的正确同步顺序。
|
|
610
|
+
*
|
|
611
|
+
* @param direction - 排序方向:'pull'(父节点优先)或 'push'(子节点优先)
|
|
612
|
+
* @returns 有序的 Repository 列表
|
|
613
|
+
*
|
|
614
|
+
* @example
|
|
615
|
+
* ```typescript
|
|
616
|
+
* // 获取 pull 顺序(父节点优先)
|
|
617
|
+
* const pullOrder = rxdb.syncManager.getRepositorySyncOrder('pull');
|
|
618
|
+
* // [User, Todo, Comment]
|
|
619
|
+
*
|
|
620
|
+
* // 获取 push 顺序(子节点优先)
|
|
621
|
+
* const pushOrder = rxdb.syncManager.getRepositorySyncOrder('push');
|
|
622
|
+
* // [Comment, Todo, User]
|
|
623
|
+
* ```
|
|
624
|
+
*/
|
|
625
|
+
getRepositorySyncOrder(direction: SortDirection): RepositoryIdentifier[] {
|
|
626
|
+
const graph = this.getRepositoryDependencyGraph();
|
|
627
|
+
return topologicalSort(graph, direction);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 执行一次 pull,并按它的实际覆盖范围结算「远端待拉」计数
|
|
632
|
+
*
|
|
633
|
+
* @param options - 拉取选项
|
|
634
|
+
* @returns 拉取结果
|
|
635
|
+
*
|
|
636
|
+
* @remarks
|
|
637
|
+
* {@link SyncManager.pull} 和 {@link SyncManager.sync} 都要走这里。此前 `pull()` 无条件 `resetPullableCount()`
|
|
638
|
+
* ——分页、逐仓过滤、有仓库失败时同样归零,界面显示「已经拉干净了」而远端还有一堆;
|
|
639
|
+
* `sync` 则压根不结算,同步完计数原地不动。两条路径的口径互相矛盾。
|
|
640
|
+
*
|
|
641
|
+
* 令牌在发起 pull 之前签发:拉取期间到达的远端事件描述的是快照之后的新变更,
|
|
642
|
+
* 结算时必须能看出来,否则「完整同步」的归零会把它们一起吞掉。
|
|
643
|
+
*/
|
|
644
|
+
async #pullAndSettle(options?: PullOptions): Promise<PullResult> {
|
|
645
|
+
const token = this.history.beginPullableSettlement();
|
|
646
|
+
let result: PullResult;
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
result = await pull(this, options);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
// 部分成功也以异常形式抛出(RxDBPartialSyncError),已落库的那部分照实扣掉;
|
|
652
|
+
// 其余异常一条都没拉到,扣 0 —— 但令牌仍要收回,否则代次永远停在旧值。
|
|
653
|
+
const partial = error instanceof RxDBPartialSyncError ? (error.result as PullResult) : undefined;
|
|
654
|
+
this.history.settleAbortedPull(token, partial ? partial.pulled : 0);
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
this.history.settlePull(token, options, result);
|
|
659
|
+
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
662
|
+
}
|