@aiao/rxdb-plugin-sync 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 +81 -2
- package/dist/SyncManager.d.ts +426 -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-materialization-source.d.ts +22 -0
- package/dist/branch-materialization-source.d.ts.map +1 -0
- package/dist/branch-utils.d.ts +40 -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 +3773 -0
- package/dist/plugin.d.ts +74 -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 +55 -0
- package/dist/pull-repository.d.ts.map +1 -0
- package/dist/pull-round.d.ts +41 -0
- package/dist/pull-round.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 +79 -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 +81 -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 +64 -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/package.json +60 -3
- package/src/SyncManager.ts +690 -0
- package/src/branch-change-id.ts +70 -0
- package/src/branch-materialization-source.ts +445 -0
- package/src/branch-utils.ts +74 -0
- package/src/bulk-sync.ts +349 -0
- package/src/cascade-blocking.ts +61 -0
- package/src/check-repository-updates.ts +143 -0
- package/src/cleanup-expired.ts +285 -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 +58 -0
- package/src/plugin.ts +132 -0
- package/src/pull-ancestor-changes.ts +61 -0
- package/src/pull-batch.ts +386 -0
- package/src/pull-conflict-utils.ts +284 -0
- package/src/pull-repository.ts +700 -0
- package/src/pull-round.ts +103 -0
- package/src/pull.ts +204 -0
- package/src/push-branch.ts +62 -0
- package/src/push-repository.ts +1541 -0
- package/src/push.ts +106 -0
- package/src/query-cache-outbox.ts +939 -0
- package/src/sync-branches.ts +276 -0
- package/src/sync-listeners.ts +309 -0
- package/src/sync-manager.utils.ts +48 -0
- package/src/sync-repository.ts +307 -0
- package/src/topological-sort.ts +246 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getEntityMetadata,
|
|
3
|
+
getSyncCapability,
|
|
4
|
+
getSyncType,
|
|
5
|
+
RepositorySyncBeginEvent,
|
|
6
|
+
RepositorySyncCompleteEvent,
|
|
7
|
+
RepositorySyncErrorEvent,
|
|
8
|
+
RxDBError,
|
|
9
|
+
RxDBPartialSyncError,
|
|
10
|
+
type SyncProgress,
|
|
11
|
+
type SyncRejection
|
|
12
|
+
} from '@aiao/rxdb';
|
|
13
|
+
import type { SyncManager } from './SyncManager.js';
|
|
14
|
+
import {
|
|
15
|
+
partialRepositoryProgressOf,
|
|
16
|
+
pullRepository,
|
|
17
|
+
type PullRepositoryOptions,
|
|
18
|
+
type PullRepositoryResult
|
|
19
|
+
} from './pull-repository.js';
|
|
20
|
+
import {
|
|
21
|
+
partialPushProgressOf,
|
|
22
|
+
pushRepository,
|
|
23
|
+
type PushRepositoryOptions,
|
|
24
|
+
type PushRepositoryResult
|
|
25
|
+
} from './push-repository.js';
|
|
26
|
+
/**
|
|
27
|
+
* @fileoverview 仓库级别的同步操作
|
|
28
|
+
*
|
|
29
|
+
* 将仓库的拉取(pull)和推送(push)操作组合在一起,保证正确的执行顺序。
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 仓库同步选项
|
|
34
|
+
*/
|
|
35
|
+
export interface SyncRepositoryOptions {
|
|
36
|
+
/**
|
|
37
|
+
* 同步方向
|
|
38
|
+
* @default 'sync'
|
|
39
|
+
*/
|
|
40
|
+
direction?: 'pull' | 'push' | 'sync';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 拉取(pull)选项
|
|
44
|
+
*/
|
|
45
|
+
pull?: PullRepositoryOptions;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 推送(push)选项
|
|
49
|
+
*/
|
|
50
|
+
push?: PushRepositoryOptions;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 仓库同步结果
|
|
55
|
+
*/
|
|
56
|
+
export interface SyncRepositoryResult extends SyncProgress {
|
|
57
|
+
/**
|
|
58
|
+
* 拉取结果
|
|
59
|
+
*/
|
|
60
|
+
pullResult: PullRepositoryResult;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 推送结果
|
|
64
|
+
*/
|
|
65
|
+
pushResult: PushRepositoryResult;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 跳过拉取时的占位结果(未发生任何拉取,进度两项都为 false)
|
|
70
|
+
*/
|
|
71
|
+
function emptyPullResult(namespace: string, entity: string): PullRepositoryResult {
|
|
72
|
+
return {
|
|
73
|
+
repository: { namespace, entity },
|
|
74
|
+
pulled: 0,
|
|
75
|
+
compacted: 0,
|
|
76
|
+
applied: 0,
|
|
77
|
+
hasMore: false,
|
|
78
|
+
conflictsResolved: 0,
|
|
79
|
+
conflictsDeferred: 0,
|
|
80
|
+
persistedProgress: false,
|
|
81
|
+
historyInvalidated: false,
|
|
82
|
+
failures: []
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 跳过推送时的占位结果
|
|
88
|
+
*/
|
|
89
|
+
function emptyPushResult(namespace: string, entity: string): PushRepositoryResult {
|
|
90
|
+
return {
|
|
91
|
+
repository: { namespace, entity },
|
|
92
|
+
pushed: 0,
|
|
93
|
+
rejected: 0,
|
|
94
|
+
failed: 0,
|
|
95
|
+
compacted: 0,
|
|
96
|
+
originalCount: 0,
|
|
97
|
+
failures: []
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 把仓库级 pull 的中断错误重新包装成**仓库同步**粒度的中断错误。
|
|
103
|
+
*
|
|
104
|
+
* `pullRepository` 抛出的 `RxDBPartialSyncError.result` 是 `PullRepositoryResult`,
|
|
105
|
+
* 而 `bulkSync` 会把它当成 `SyncRepositoryResult` 挂到 `result` 上给调用方读
|
|
106
|
+
* `result.pullResult` —— 不在这里转一道,那个字段永远是 undefined,
|
|
107
|
+
* 失败仓库的已提交进度照样丢。
|
|
108
|
+
*/
|
|
109
|
+
function rewrapPullFailure(error: unknown, namespace: string, entity: string): unknown {
|
|
110
|
+
if (!(error instanceof RxDBPartialSyncError)) return error;
|
|
111
|
+
const pullResult = partialRepositoryProgressOf(error);
|
|
112
|
+
if (!pullResult) return error;
|
|
113
|
+
const wrapped: SyncRepositoryResult = {
|
|
114
|
+
pullResult,
|
|
115
|
+
pushResult: emptyPushResult(namespace, entity),
|
|
116
|
+
persistedProgress: pullResult.persistedProgress,
|
|
117
|
+
historyInvalidated: pullResult.historyInvalidated
|
|
118
|
+
};
|
|
119
|
+
return new RxDBPartialSyncError<SyncRepositoryResult>(wrapped, error.cause);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 把仓库级 push 的失败重新包装成**仓库同步**粒度的中断错误。
|
|
124
|
+
*
|
|
125
|
+
* 两段进度都不能因为 push 抛错而消失:
|
|
126
|
+
*
|
|
127
|
+
* - pull 阶段已落库的部分(`pullResult.persistedProgress`);
|
|
128
|
+
* - push 阶段已发到远端、**不会回滚**的部分(`RxDBPartialSyncError.result.pushed > 0`)。
|
|
129
|
+
* 此前这里恒填 `emptyPushResult`,已经发出去的条目在聚合层看起来一条没推,
|
|
130
|
+
* 调用方据此重试就是重复推送。
|
|
131
|
+
*
|
|
132
|
+
* 两段都没有时原样抛:包一层空进度只会让调用方多剥一层。
|
|
133
|
+
*
|
|
134
|
+
* @param error - `pushRepository` 抛出的任意异常
|
|
135
|
+
* @param pullResult - 同一轮里 pull 阶段的结果,进度要一起交出去
|
|
136
|
+
* @param namespace - 实体命名空间
|
|
137
|
+
* @param entity - 实体名称
|
|
138
|
+
* @returns 待抛出的异常
|
|
139
|
+
*/
|
|
140
|
+
function rewrapPushFailure(
|
|
141
|
+
error: unknown,
|
|
142
|
+
pullResult: PullRepositoryResult,
|
|
143
|
+
namespace: string,
|
|
144
|
+
entity: string
|
|
145
|
+
): unknown {
|
|
146
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
147
|
+
const pushProgress = partialPushProgressOf(normalized);
|
|
148
|
+
const pushResult = pushProgress === undefined ? emptyPushResult(namespace, entity) : pushProgress;
|
|
149
|
+
const persistedProgress = pullResult.persistedProgress || pushResult.pushed > 0;
|
|
150
|
+
if (!persistedProgress) return normalized;
|
|
151
|
+
|
|
152
|
+
const partial: SyncRepositoryResult = {
|
|
153
|
+
pullResult,
|
|
154
|
+
pushResult,
|
|
155
|
+
persistedProgress,
|
|
156
|
+
historyInvalidated: pullResult.historyInvalidated
|
|
157
|
+
};
|
|
158
|
+
// 已经是 partial error 时取根因,避免调用方拿到「错误里套错误」
|
|
159
|
+
return new RxDBPartialSyncError<SyncRepositoryResult>(
|
|
160
|
+
partial,
|
|
161
|
+
normalized instanceof RxDBPartialSyncError ? normalized.cause : normalized
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 同步一个仓库(先拉取再推送)
|
|
167
|
+
*
|
|
168
|
+
* @param sm - SyncManager 实例
|
|
169
|
+
* @param namespace - 实体命名空间
|
|
170
|
+
* @param entity - 实体名称
|
|
171
|
+
* @param options - 同步选项
|
|
172
|
+
* @param rejectionSink - 推送被拒清单收集器,语义同 {@link pushRepository} 的同名参数
|
|
173
|
+
* @returns 同步结果
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* // 同步 Todo 仓库(pull + push)
|
|
178
|
+
* const result = await syncRepository(sm, 'public', 'Todo');
|
|
179
|
+
*
|
|
180
|
+
* // 使用自定义选项同步
|
|
181
|
+
* const result = await syncRepository(sm, 'public', 'Todo', {
|
|
182
|
+
* pull: { limit: 500, fetchAll: true },
|
|
183
|
+
* push: { batchSize: 100 }
|
|
184
|
+
* });
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export async function syncRepository(
|
|
188
|
+
sm: SyncManager,
|
|
189
|
+
namespace: string,
|
|
190
|
+
entity: string,
|
|
191
|
+
options?: SyncRepositoryOptions,
|
|
192
|
+
rejectionSink?: SyncRejection[]
|
|
193
|
+
): Promise<SyncRepositoryResult> {
|
|
194
|
+
const rxdb = sm.rxdb;
|
|
195
|
+
|
|
196
|
+
// 触发开始事件
|
|
197
|
+
const includeRelated = options?.pull?.includeRelated ?? options?.push?.includeRelated ?? true;
|
|
198
|
+
rxdb.dispatchEvent(new RepositorySyncBeginEvent('sync', namespace, entity, includeRelated));
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const result = await _syncRepositoryImpl(sm, namespace, entity, options, rejectionSink);
|
|
202
|
+
|
|
203
|
+
// 触发完成事件
|
|
204
|
+
// 两个子结果在 SyncRepositoryResult 上都是必填:跳过的方向由 emptyPullResult /
|
|
205
|
+
// emptyPushResult 交出各计数为 0 的占位。此前的 `?.x ?? 0` 是跑不到的分支,
|
|
206
|
+
// 真出现 undefined 时它会把「结果缺失」伪装成「同步了 0 条」。
|
|
207
|
+
const { pullResult, pushResult } = result;
|
|
208
|
+
const stats = {
|
|
209
|
+
pulled: pullResult.pulled,
|
|
210
|
+
pushed: pushResult.pushed,
|
|
211
|
+
compacted: pullResult.compacted + pushResult.compacted,
|
|
212
|
+
failed: pushResult.failed,
|
|
213
|
+
conflictsResolved: pullResult.conflictsResolved,
|
|
214
|
+
conflictsDeferred: pullResult.conflictsDeferred
|
|
215
|
+
};
|
|
216
|
+
rxdb.dispatchEvent(new RepositorySyncCompleteEvent('sync', namespace, entity, stats));
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
// 触发错误事件
|
|
221
|
+
rxdb.dispatchEvent(new RepositorySyncErrorEvent('sync', namespace, entity, error as Error));
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* syncRepository 的内部实现
|
|
228
|
+
*/
|
|
229
|
+
async function _syncRepositoryImpl(
|
|
230
|
+
sm: SyncManager,
|
|
231
|
+
namespace: string,
|
|
232
|
+
entity: string,
|
|
233
|
+
options?: SyncRepositoryOptions,
|
|
234
|
+
rejectionSink?: SyncRejection[]
|
|
235
|
+
): Promise<SyncRepositoryResult> {
|
|
236
|
+
// 验证仓库是否存在
|
|
237
|
+
const EntityType = sm.rxdb.config.entities.find(e => {
|
|
238
|
+
const meta = getEntityMetadata(e);
|
|
239
|
+
return meta.namespace === namespace && meta.name === entity;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!EntityType) {
|
|
243
|
+
throw new RxDBError(`Entity not found: ${namespace}:${entity}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const metadata = getEntityMetadata(EntityType);
|
|
247
|
+
|
|
248
|
+
// 检查同步类型(支持全局配置回退)
|
|
249
|
+
const syncType = getSyncType(metadata, sm.rxdb.entitySync);
|
|
250
|
+
if (syncType === 'none') {
|
|
251
|
+
throw new RxDBError(
|
|
252
|
+
`Cannot sync repository ${namespace}:${entity}: syncType is 'none'. ` +
|
|
253
|
+
`This entity is not configured for remote synchronization.`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 先执行拉取(确保我们拥有最新数据)
|
|
258
|
+
//
|
|
259
|
+
// 方向能力从 `getSyncCapability` 取,不再内联枚举 —— 此前这里漏了
|
|
260
|
+
// `querycache`(批量与单仓路径一直在拉它),同一个仓库走 sync 和走 pull 结果不一样。
|
|
261
|
+
// `RxDBSync.enabled` 不在这层判定:pullRepository / pushRepository 各自会拒,
|
|
262
|
+
// 在这里再判一次只会多读一遍系统表。
|
|
263
|
+
let pullResult: PullRepositoryResult;
|
|
264
|
+
const shouldPull =
|
|
265
|
+
(options?.direction === 'pull' || options?.direction === 'sync' || !options?.direction) &&
|
|
266
|
+
getSyncCapability(syncType).pull;
|
|
267
|
+
|
|
268
|
+
if (shouldPull) {
|
|
269
|
+
try {
|
|
270
|
+
pullResult = await pullRepository(sm, namespace, entity, options?.pull);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
throw rewrapPullFailure(error, namespace, entity);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
// 仅本地或仅推送:跳过拉取,创建占位结果
|
|
276
|
+
pullResult = emptyPullResult(namespace, entity);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 执行推送(发送本地变更)
|
|
280
|
+
//
|
|
281
|
+
// 'local' 不可推:它的定义是「SyncType.None + 只配了 local adapter、没有 remote」,
|
|
282
|
+
// 公开契约把它写成「只在本地」。把它算成可推等于把私有本地数据送出去,
|
|
283
|
+
// 而且这类仓库根本没有 remote adapter,推送本就无意义。fail-closed。
|
|
284
|
+
let pushResult: PushRepositoryResult;
|
|
285
|
+
const shouldPush =
|
|
286
|
+
(options?.direction === 'push' || options?.direction === 'sync' || !options?.direction) &&
|
|
287
|
+
getSyncCapability(syncType).push;
|
|
288
|
+
|
|
289
|
+
if (shouldPush) {
|
|
290
|
+
try {
|
|
291
|
+
pushResult = await pushRepository(sm, namespace, entity, options?.push, rejectionSink);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// pull 已落库的进度、push 已发到远端的进度,都不能因为这次抛错就消失
|
|
294
|
+
throw rewrapPushFailure(error, pullResult, namespace, entity);
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
// 仅远端或仅拉取:跳过推送,创建占位结果
|
|
298
|
+
pushResult = emptyPushResult(namespace, entity);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
pullResult,
|
|
303
|
+
pushResult,
|
|
304
|
+
persistedProgress: pullResult.persistedProgress || pushResult.pushed > 0,
|
|
305
|
+
historyInvalidated: pullResult.historyInvalidated
|
|
306
|
+
};
|
|
307
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 拓扑排序算法
|
|
3
|
+
*
|
|
4
|
+
* 提供两种排序方向:
|
|
5
|
+
* - Pull(父->子): 确保父实体先同步,外键引用的数据已存在
|
|
6
|
+
* - Push(子->父): 确保子实体先同步,避免外键约束错误
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { RxDBError } from '@aiao/rxdb';
|
|
10
|
+
import type { DependencyGraph, RepositoryIdentifier } from './dependency-graph.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 排序方向
|
|
14
|
+
*/
|
|
15
|
+
export type SortDirection = 'pull' | 'push';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 依赖图上的一条边。
|
|
19
|
+
*
|
|
20
|
+
* - `dependsOn`:本仓库**依赖**的仓库(父,外键指过去的那一端)
|
|
21
|
+
* - `requiredBy`:**依赖本仓库**的仓库(子,外键指过来的那一端)
|
|
22
|
+
*/
|
|
23
|
+
export type DependencyEdge = 'dependsOn' | 'requiredBy';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 排序方向沿依赖图上的哪条边展开。
|
|
27
|
+
*
|
|
28
|
+
* 这条对应关系是整个级联调度的枢纽,所以只在这里写一次:拓扑序沿哪条边建立,
|
|
29
|
+
* 「谁排在我前面」就是哪条边,级联的依赖闸门也必须看同一条边。
|
|
30
|
+
* 从前它以 `direction === 'pull' ? dep.dependsOn : dep.requiredBy` 的形式散在
|
|
31
|
+
* {@link topologicalSort} 里,闸门那一侧则各写各的,于是 DELETE 相位的闸门看错了边。
|
|
32
|
+
*/
|
|
33
|
+
export function dependencyEdgeOf(direction: SortDirection): DependencyEdge {
|
|
34
|
+
return direction === 'pull' ? 'dependsOn' : 'requiredBy';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 本相位该按哪个方向排序。
|
|
39
|
+
*
|
|
40
|
+
* DELETE 子先父后、INSERT/UPDATE 父先子后,理由见 {@link topologicalSortForAction}。
|
|
41
|
+
*/
|
|
42
|
+
function sortDirectionForAction(action: SortActionKind): SortDirection {
|
|
43
|
+
return action === 'DELETE' ? 'push' : 'pull';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 本相位的依赖闸门该看哪条边 —— 也就是「本相位里谁排在我前面」。
|
|
48
|
+
*
|
|
49
|
+
* DELETE 相位子先父后,父被子阻断,看 `requiredBy`;
|
|
50
|
+
* INSERT/UPDATE 相位父先子后,子被父阻断,看 `dependsOn`。
|
|
51
|
+
*
|
|
52
|
+
* 与 {@link topologicalSortForAction} 共用 {@link sortDirectionForAction},
|
|
53
|
+
* 保证「执行顺序」和「阻断关系」不可能各说各话。
|
|
54
|
+
*
|
|
55
|
+
* @param action - 本相位提交的变更类型
|
|
56
|
+
* @returns 该相位下判定阻断所依据的依赖边
|
|
57
|
+
*/
|
|
58
|
+
export function dependencyEdgeForAction(action: SortActionKind): DependencyEdge {
|
|
59
|
+
return dependencyEdgeOf(sortDirectionForAction(action));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 拓扑排序
|
|
64
|
+
*
|
|
65
|
+
* @param graph - 依赖图
|
|
66
|
+
* @param direction - 排序方向
|
|
67
|
+
* - 'pull': 父实体在前(User -> Todo -> Comment)
|
|
68
|
+
* - 'push': 子实体在前(Comment -> Todo -> User)
|
|
69
|
+
* @returns 排序后的 repository 列表
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* // Pull 顺序(父在前)
|
|
74
|
+
* const pullOrder = topologicalSort(graph, 'pull');
|
|
75
|
+
* // 返回: [User, Todo, Comment]
|
|
76
|
+
*
|
|
77
|
+
* // Push 顺序(子在前)
|
|
78
|
+
* const pushOrder = topologicalSort(graph, 'push');
|
|
79
|
+
* // 返回: [Comment, Todo, User]
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function topologicalSort(graph: DependencyGraph, direction: SortDirection): RepositoryIdentifier[] {
|
|
83
|
+
// Pull: 使用 dependsOn(父实体先)
|
|
84
|
+
// Push: 使用 requiredBy(子实体先)
|
|
85
|
+
const result: RepositoryIdentifier[] = [];
|
|
86
|
+
const visited = new Set<string>();
|
|
87
|
+
const tempMarked = new Set<string>();
|
|
88
|
+
|
|
89
|
+
function visit(key: string): void {
|
|
90
|
+
if (visited.has(key)) return;
|
|
91
|
+
|
|
92
|
+
if (tempMarked.has(key)) {
|
|
93
|
+
throw new RxDBError(`Circular dependency detected at ${key}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
tempMarked.add(key);
|
|
97
|
+
|
|
98
|
+
const dep = graph.get(key);
|
|
99
|
+
if (!dep) {
|
|
100
|
+
throw new RxDBError(`Repository ${key} not found in dependency graph`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 根据方向选择遍历的邻居(边的选择集中在 dependencyEdgeOf,闸门那一侧读同一份)
|
|
104
|
+
const neighbors = dep[dependencyEdgeOf(direction)];
|
|
105
|
+
|
|
106
|
+
for (const neighbor of neighbors) {
|
|
107
|
+
const neighborKey = `${neighbor.namespace}:${neighbor.entity}`;
|
|
108
|
+
// 跳过自引用(树形结构)
|
|
109
|
+
if (neighborKey === key) continue;
|
|
110
|
+
// 跳过悬挂依赖:关系可以指向没被注册进 config.entities 的实体(跨 namespace
|
|
111
|
+
// 部分注册、按需加载模块)。buildDependencyGraph 反向填充 requiredBy 时同样
|
|
112
|
+
// 是宽容的,这里不跳过就会让整个 pull/push 链路因一条无关关系而不可用。
|
|
113
|
+
if (!graph.has(neighborKey)) continue;
|
|
114
|
+
visit(neighborKey);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
tempMarked.delete(key);
|
|
118
|
+
visited.add(key);
|
|
119
|
+
|
|
120
|
+
// Pull: 父实体先访问,后添加 -> 父在前
|
|
121
|
+
// Push: 子实体先访问,后添加 -> 子在前
|
|
122
|
+
result.push(dep.repository);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 遍历所有节点
|
|
126
|
+
for (const key of graph.keys()) {
|
|
127
|
+
visit(key);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 拓扑排序(Pull 方向)
|
|
135
|
+
*
|
|
136
|
+
* 确保父实体在前,适用于 Pull 同步
|
|
137
|
+
*
|
|
138
|
+
* @param graph - 依赖图
|
|
139
|
+
* @returns 排序后的 repository 列表
|
|
140
|
+
*/
|
|
141
|
+
export function topologicalSortForPull(graph: DependencyGraph): RepositoryIdentifier[] {
|
|
142
|
+
return topologicalSort(graph, 'pull');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 拓扑排序(Push 方向)
|
|
147
|
+
*
|
|
148
|
+
* 确保子实体在前,适用于 Push 同步
|
|
149
|
+
*
|
|
150
|
+
* @param graph - 依赖图
|
|
151
|
+
* @returns 排序后的 repository 列表
|
|
152
|
+
*/
|
|
153
|
+
export function topologicalSortForPush(graph: DependencyGraph): RepositoryIdentifier[] {
|
|
154
|
+
return topologicalSort(graph, 'push');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 变更类型:决定安全的提交顺序
|
|
159
|
+
*/
|
|
160
|
+
export type SortActionKind = 'INSERT' | 'UPDATE' | 'DELETE';
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 按变更类型选排序方向
|
|
164
|
+
*
|
|
165
|
+
* 同一张依赖图,INSERT 和 DELETE 需要的顺序**正好相反**,「push 就是子→父」这个说法
|
|
166
|
+
* 只对 DELETE 成立:
|
|
167
|
+
*
|
|
168
|
+
* - `INSERT` / `UPDATE` → **父先**。子行的外键指向父行,父行不先落库就是悬空引用,FK 失败。
|
|
169
|
+
* - `DELETE` → **子先**。父行还被子行引用着,先删父同样 FK 失败。
|
|
170
|
+
*
|
|
171
|
+
* 所以一次级联推送不可能靠单一顺序扫完,必须分相位:DELETE 走一遍子→父,
|
|
172
|
+
* INSERT/UPDATE 再走一遍父→子。
|
|
173
|
+
*
|
|
174
|
+
* @param graph - 依赖图
|
|
175
|
+
* @param action - 本相位要提交的变更类型
|
|
176
|
+
* @returns 该类型下安全的提交顺序
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* topologicalSortForAction(graph, 'INSERT'); // [User, Todo, Comment]
|
|
181
|
+
* topologicalSortForAction(graph, 'DELETE'); // [Comment, Todo, User]
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export function topologicalSortForAction(graph: DependencyGraph, action: SortActionKind): RepositoryIdentifier[] {
|
|
185
|
+
return topologicalSort(graph, sortDirectionForAction(action));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 过滤并排序指定的 repository 列表
|
|
190
|
+
*
|
|
191
|
+
* 从依赖图中只选择指定的 repositories,并按拓扑顺序排序
|
|
192
|
+
*
|
|
193
|
+
* @param graph - 完整依赖图
|
|
194
|
+
* @param repositories - 要排序的 repository 列表
|
|
195
|
+
* @param direction - 排序方向
|
|
196
|
+
* @returns 排序后的 repository 列表
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* // 只同步 Todo 和 Comment
|
|
201
|
+
* const filtered = filterAndSort(
|
|
202
|
+
* graph,
|
|
203
|
+
* [
|
|
204
|
+
* { namespace: 'public', entity: 'Todo' },
|
|
205
|
+
* { namespace: 'public', entity: 'Comment' }
|
|
206
|
+
* ],
|
|
207
|
+
* 'pull'
|
|
208
|
+
* );
|
|
209
|
+
* // 返回: [Todo, Comment](按依赖顺序)
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export function filterAndSort(
|
|
213
|
+
graph: DependencyGraph,
|
|
214
|
+
repositories: RepositoryIdentifier[],
|
|
215
|
+
direction: SortDirection
|
|
216
|
+
): RepositoryIdentifier[] {
|
|
217
|
+
// 创建子图,只包含指定的 repositories
|
|
218
|
+
const subGraph: DependencyGraph = new Map();
|
|
219
|
+
const keys = new Set(repositories.map(r => `${r.namespace}:${r.entity}`));
|
|
220
|
+
|
|
221
|
+
for (const key of keys) {
|
|
222
|
+
const dep = graph.get(key);
|
|
223
|
+
if (!dep) {
|
|
224
|
+
throw new RxDBError(`Repository ${key} not found in dependency graph`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 过滤依赖,只保留在 keys 中的
|
|
228
|
+
const filteredDependsOn = dep.dependsOn.filter(parent => {
|
|
229
|
+
const parentKey = `${parent.namespace}:${parent.entity}`;
|
|
230
|
+
return keys.has(parentKey);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const filteredRequiredBy = dep.requiredBy.filter(child => {
|
|
234
|
+
const childKey = `${child.namespace}:${child.entity}`;
|
|
235
|
+
return keys.has(childKey);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
subGraph.set(key, {
|
|
239
|
+
repository: dep.repository,
|
|
240
|
+
dependsOn: filteredDependsOn,
|
|
241
|
+
requiredBy: filteredRequiredBy
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return topologicalSort(subGraph, direction);
|
|
246
|
+
}
|