@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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -2
  3. package/dist/SyncManager.d.ts +426 -0
  4. package/dist/SyncManager.d.ts.map +1 -0
  5. package/dist/branch-change-id.d.ts +31 -0
  6. package/dist/branch-change-id.d.ts.map +1 -0
  7. package/dist/branch-materialization-source.d.ts +22 -0
  8. package/dist/branch-materialization-source.d.ts.map +1 -0
  9. package/dist/branch-utils.d.ts +40 -0
  10. package/dist/branch-utils.d.ts.map +1 -0
  11. package/dist/bulk-sync.d.ts +117 -0
  12. package/dist/bulk-sync.d.ts.map +1 -0
  13. package/dist/cascade-blocking.d.ts +38 -0
  14. package/dist/cascade-blocking.d.ts.map +1 -0
  15. package/dist/check-repository-updates.d.ts +47 -0
  16. package/dist/check-repository-updates.d.ts.map +1 -0
  17. package/dist/cleanup-expired.d.ts +56 -0
  18. package/dist/cleanup-expired.d.ts.map +1 -0
  19. package/dist/dependency-graph.d.ts +128 -0
  20. package/dist/dependency-graph.d.ts.map +1 -0
  21. package/dist/get-all-repository-sync-status.d.ts +58 -0
  22. package/dist/get-all-repository-sync-status.d.ts.map +1 -0
  23. package/dist/get-repository-sync-status.d.ts +68 -0
  24. package/dist/get-repository-sync-status.d.ts.map +1 -0
  25. package/dist/index.d.ts +39 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +3773 -0
  28. package/dist/plugin.d.ts +74 -0
  29. package/dist/plugin.d.ts.map +1 -0
  30. package/dist/pull-ancestor-changes.d.ts +41 -0
  31. package/dist/pull-ancestor-changes.d.ts.map +1 -0
  32. package/dist/pull-batch.d.ts +16 -0
  33. package/dist/pull-batch.d.ts.map +1 -0
  34. package/dist/pull-conflict-utils.d.ts +43 -0
  35. package/dist/pull-conflict-utils.d.ts.map +1 -0
  36. package/dist/pull-repository.d.ts +55 -0
  37. package/dist/pull-repository.d.ts.map +1 -0
  38. package/dist/pull-round.d.ts +41 -0
  39. package/dist/pull-round.d.ts.map +1 -0
  40. package/dist/pull.d.ts +16 -0
  41. package/dist/pull.d.ts.map +1 -0
  42. package/dist/push-branch.d.ts +22 -0
  43. package/dist/push-branch.d.ts.map +1 -0
  44. package/dist/push-repository.d.ts +79 -0
  45. package/dist/push-repository.d.ts.map +1 -0
  46. package/dist/push.d.ts +14 -0
  47. package/dist/push.d.ts.map +1 -0
  48. package/dist/query-cache-outbox.d.ts +120 -0
  49. package/dist/query-cache-outbox.d.ts.map +1 -0
  50. package/dist/sync-branches.d.ts +81 -0
  51. package/dist/sync-branches.d.ts.map +1 -0
  52. package/dist/sync-listeners.d.ts +20 -0
  53. package/dist/sync-listeners.d.ts.map +1 -0
  54. package/dist/sync-manager.utils.d.ts +36 -0
  55. package/dist/sync-manager.utils.d.ts.map +1 -0
  56. package/dist/sync-repository.d.ts +64 -0
  57. package/dist/sync-repository.d.ts.map +1 -0
  58. package/dist/topological-sort.d.ts +126 -0
  59. package/dist/topological-sort.d.ts.map +1 -0
  60. package/package.json +60 -3
  61. package/src/SyncManager.ts +690 -0
  62. package/src/branch-change-id.ts +70 -0
  63. package/src/branch-materialization-source.ts +445 -0
  64. package/src/branch-utils.ts +74 -0
  65. package/src/bulk-sync.ts +349 -0
  66. package/src/cascade-blocking.ts +61 -0
  67. package/src/check-repository-updates.ts +143 -0
  68. package/src/cleanup-expired.ts +285 -0
  69. package/src/dependency-graph.ts +324 -0
  70. package/src/get-all-repository-sync-status.ts +115 -0
  71. package/src/get-repository-sync-status.ts +237 -0
  72. package/src/index.ts +58 -0
  73. package/src/plugin.ts +132 -0
  74. package/src/pull-ancestor-changes.ts +61 -0
  75. package/src/pull-batch.ts +386 -0
  76. package/src/pull-conflict-utils.ts +284 -0
  77. package/src/pull-repository.ts +700 -0
  78. package/src/pull-round.ts +103 -0
  79. package/src/pull.ts +204 -0
  80. package/src/push-branch.ts +62 -0
  81. package/src/push-repository.ts +1541 -0
  82. package/src/push.ts +106 -0
  83. package/src/query-cache-outbox.ts +939 -0
  84. package/src/sync-branches.ts +276 -0
  85. package/src/sync-listeners.ts +309 -0
  86. package/src/sync-manager.utils.ts +48 -0
  87. package/src/sync-repository.ts +307 -0
  88. package/src/topological-sort.ts +246 -0
@@ -0,0 +1,700 @@
1
+ /**
2
+ * @fileoverview 仓库级别的拉取(pull)操作
3
+ *
4
+ * 在仓库(实体类型)级别提供细粒度的拉取控制,
5
+ * 支持通过依赖图分析的级联同步。
6
+ */
7
+
8
+ import {
9
+ compactChanges,
10
+ declareTrustedWrite,
11
+ type EntityType,
12
+ getEntityMetadata,
13
+ getOrCreateSyncRecord,
14
+ getSyncConfig,
15
+ getSyncType,
16
+ LWWConflictResolver,
17
+ type PullRepositoryOptions,
18
+ type PullRepositoryResult,
19
+ RemoteChange,
20
+ type RepositoryIdentifier,
21
+ repositoryKey,
22
+ RepositorySyncBeginEvent,
23
+ RepositorySyncCompleteEvent,
24
+ RepositorySyncErrorEvent,
25
+ type RepositorySyncType,
26
+ resolvePullIneligibility,
27
+ type RuleGroup,
28
+ RxDBChange,
29
+ RxDBDependencyFailedError,
30
+ RxDBError,
31
+ type RxDBEvent,
32
+ RxDBPartialSyncError,
33
+ RxDBSync,
34
+ type SyncFailure,
35
+ type SyncOptions,
36
+ TrustedWriteIntent
37
+ } from '@aiao/rxdb';
38
+ import type { SyncManager } from './SyncManager.js';
39
+ import { getAncestorBranchIds } from './branch-utils.js';
40
+ import { findBlockingDependency } from './cascade-blocking.js';
41
+ import { buildDependencyGraph, type DependencyGraph } from './dependency-graph.js';
42
+ import { pullAncestorBranchChanges } from './pull-ancestor-changes.js';
43
+ import {
44
+ countActions,
45
+ createConflictActionEntries,
46
+ createEmptyActions,
47
+ markLocalChangesSuperseded,
48
+ queryPendingLocalChanges,
49
+ resolveConflictsAndBuildActions
50
+ } from './pull-conflict-utils.js';
51
+ import { backfillOwnChangeRemoteIds, splitRemoteChangesByOrigin } from './pull-round.js';
52
+ import { topologicalSortForPull } from './topological-sort.js';
53
+
54
+ /**
55
+ * 验证 RuleGroup 结构是否有效
56
+ */
57
+ function isValidRuleGroup(rg: unknown): rg is RuleGroup {
58
+ if (!rg || typeof rg !== 'object') return false;
59
+ const obj = rg as Record<string, unknown>;
60
+ return (obj['combinator'] === 'and' || obj['combinator'] === 'or') && Array.isArray(obj['rules']);
61
+ }
62
+
63
+ // 类型单一来源在 VersionManager.interface.ts(对外导出的那一份);
64
+ // 这里只做转发,避免两份定义各自漂移。
65
+ export type { PullRepositoryOptions, PullRepositoryResult };
66
+
67
+ /**
68
+ * 仓库级「零进度」基线:跳过或失败前一条都没落库时用它填充计数字段
69
+ *
70
+ * 用函数而非共享常量:`failures` 是可变数组,共享同一份引用会让不同仓库的
71
+ * 结果互相串改。
72
+ */
73
+ function emptyRepositoryProgress(): Omit<PullRepositoryResult, 'repository'> {
74
+ return {
75
+ pulled: 0,
76
+ compacted: 0,
77
+ applied: 0,
78
+ hasMore: false,
79
+ conflictsResolved: 0,
80
+ conflictsDeferred: 0,
81
+ persistedProgress: false,
82
+ historyInvalidated: false,
83
+ failures: []
84
+ };
85
+ }
86
+
87
+ /**
88
+ * 从中断错误里取出仓库级的部分进度。
89
+ *
90
+ * 结构化校验而非直接断言类型:`RxDBPartialSyncError` 的 `result` 是泛型,
91
+ * 拿一个 `as PullRepositoryResult` 硬转会在别处抛出别的形状时静默出错。
92
+ *
93
+ * @internal
94
+ */
95
+ export function partialRepositoryProgressOf(error: Error): PullRepositoryResult | undefined {
96
+ if (!(error instanceof RxDBPartialSyncError)) return undefined;
97
+ const result: unknown = error.result;
98
+ if (!result || typeof result !== 'object') return undefined;
99
+ const candidate = result as Partial<PullRepositoryResult>;
100
+ return typeof candidate.applied === 'number' && typeof candidate.persistedProgress === 'boolean' ?
101
+ (candidate as PullRepositoryResult)
102
+ : undefined;
103
+ }
104
+
105
+ /**
106
+ * 默认拉取仓库选项
107
+ */
108
+ const DEFAULT_PULL_REPOSITORY_OPTIONS: Required<Omit<PullRepositoryOptions, 'filter' | 'conflictResolver'>> &
109
+ Pick<PullRepositoryOptions, 'filter' | 'conflictResolver'> = {
110
+ limit: 1000,
111
+ fetchAll: false,
112
+ includeRelated: true,
113
+ filter: undefined,
114
+ conflictResolver: undefined
115
+ };
116
+
117
+ /**
118
+ * 为单个仓库拉取变更
119
+ *
120
+ * @param sm - SyncManager 实例
121
+ * @param namespace - 实体命名空间
122
+ * @param entity - 实体名称
123
+ * @param options - 拉取选项
124
+ * @returns 拉取结果
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * // 在不进行级联的情况下拉取 Todo 仓库
129
+ * const result = await pullRepository(sm, 'public', 'Todo', {
130
+ * includeRelated: false
131
+ * });
132
+ *
133
+ * // 拉取 Todo 及其所有依赖项(如 User)
134
+ * const result = await pullRepository(sm, 'public', 'Todo', {
135
+ * includeRelated: true
136
+ * });
137
+ * ```
138
+ */
139
+ export async function pullRepository(
140
+ sm: SyncManager,
141
+ namespace: string,
142
+ entity: string,
143
+ options?: PullRepositoryOptions
144
+ ): Promise<PullRepositoryResult> {
145
+ const opts = { ...DEFAULT_PULL_REPOSITORY_OPTIONS, ...options };
146
+ const rxdb = sm.rxdb;
147
+
148
+ // 触发开始事件
149
+ rxdb.dispatchEvent(new RepositorySyncBeginEvent('pull', namespace, entity, opts.includeRelated));
150
+
151
+ try {
152
+ const result = await _pullRepositoryImpl(sm, namespace, entity, opts);
153
+
154
+ // 触发完成事件
155
+ rxdb.dispatchEvent(
156
+ new RepositorySyncCompleteEvent('pull', namespace, entity, {
157
+ pulled: result.pulled,
158
+ compacted: result.compacted,
159
+ conflictsResolved: result.conflictsResolved,
160
+ conflictsDeferred: result.conflictsDeferred
161
+ })
162
+ );
163
+
164
+ return result;
165
+ } catch (error) {
166
+ // 触发错误事件
167
+ rxdb.dispatchEvent(new RepositorySyncErrorEvent('pull', namespace, entity, error as Error));
168
+ throw error;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * pullRepository 的内部实现
174
+ */
175
+ async function _pullRepositoryImpl(
176
+ sm: SyncManager,
177
+ namespace: string,
178
+ entity: string,
179
+ opts: typeof DEFAULT_PULL_REPOSITORY_OPTIONS
180
+ ): Promise<PullRepositoryResult> {
181
+ // 验证仓库是否存在
182
+ const EntityType = sm.rxdb.config.entities.find(e => {
183
+ const meta = getEntityMetadata(e);
184
+ return meta.namespace === namespace && meta.name === entity;
185
+ });
186
+
187
+ if (!EntityType) {
188
+ throw new RxDBError(`Entity not found: ${namespace}:${entity}`);
189
+ }
190
+
191
+ const metadata = getEntityMetadata(EntityType);
192
+
193
+ // 检查同步类型(支持全局配置回退)。资格判定与级联路径共用 `resolvePullIneligibility`,
194
+ // 避免两条路径各写一份而漂移。
195
+ // 同一处叠加 `RxDBSync.enabled` —— 显式点名单个仓库时抛错而非静默跳过,
196
+ // 与 syncType 不合格时的行为一致;批量枚举路径(pullBatch)才是跳过。
197
+ const syncType = getSyncType(metadata, sm.rxdb.entitySync);
198
+ const ineligible = await resolvePullIneligibility(sm.rxdb, namespace, entity, syncType);
199
+
200
+ if (ineligible) {
201
+ throw new RxDBError(`Cannot pull repository ${namespace}:${entity}: ${ineligible}.`);
202
+ }
203
+
204
+ // 对于 Filter 同步类型,从配置中提取 filter 函数并执行
205
+ let effectiveFilter = opts.filter;
206
+ if (syncType === 'filter' && !effectiveFilter) {
207
+ // 取生效配置而不是装饰器原值:实例覆盖可能换掉了 filter
208
+ const syncConfig = getSyncConfig(metadata, sm.rxdb.entitySync) as {
209
+ type: string;
210
+ remote?: { filter?: () => RuleGroup };
211
+ };
212
+ if (syncConfig?.remote?.filter) {
213
+ // T022: filter 函数执行错误处理
214
+ try {
215
+ effectiveFilter = syncConfig.remote.filter();
216
+ } catch (error) {
217
+ throw new RxDBError(
218
+ `Filter function failed for ${namespace}:${entity}: ${error instanceof Error ? error.message : String(error)}`
219
+ );
220
+ }
221
+
222
+ // T023: RuleGroup 验证逻辑
223
+ if (effectiveFilter && !isValidRuleGroup(effectiveFilter)) {
224
+ throw new RxDBError(
225
+ `Invalid RuleGroup returned by filter function for ${namespace}:${entity}: ` +
226
+ `RuleGroup must have 'combinator' ('and' | 'or') and 'rules' array properties.`
227
+ );
228
+ }
229
+ }
230
+ }
231
+
232
+ // 创建带有 filter 的选项
233
+ const optsWithFilter = { ...opts, filter: effectiveFilter };
234
+
235
+ // 处理级联拉取
236
+ if (opts.includeRelated) {
237
+ return await pullWithCascade(sm, namespace, entity, optsWithFilter);
238
+ }
239
+
240
+ // 单仓库拉取(传递 filter)
241
+ return await pullSingleRepository(sm, namespace, entity, optsWithFilter);
242
+ }
243
+
244
+ /**
245
+ * 级联拉取(包含依赖)
246
+ *
247
+ * @internal
248
+ */
249
+ async function pullWithCascade(
250
+ sm: SyncManager,
251
+ namespace: string,
252
+ entity: string,
253
+ options: typeof DEFAULT_PULL_REPOSITORY_OPTIONS
254
+ ): Promise<PullRepositoryResult> {
255
+ // 构建依赖图
256
+ const entities = sm.rxdb.config.entities.map(e => getEntityMetadata(e));
257
+ const graph = buildDependencyGraph(entities);
258
+
259
+ // 预构建 entityMap 避免循环内重复查找
260
+ const entityMap = new Map<string, (typeof sm.rxdb.config.entities)[number]>(
261
+ sm.rxdb.config.entities.map(e => {
262
+ const m = getEntityMetadata(e);
263
+ return [`${m.namespace}:${m.name}`, e];
264
+ })
265
+ );
266
+
267
+ // 获取拓扑排序(顺序:父 -> 子)
268
+ const sortedRepos = topologicalSortForPull(graph);
269
+
270
+ // 查找目标仓库及其依赖项
271
+ const targetKey = `${namespace}:${entity}`;
272
+ const dep = graph.get(targetKey);
273
+
274
+ if (!dep) {
275
+ throw new RxDBError(`Repository ${namespace}:${entity} not found in dependency graph`);
276
+ }
277
+
278
+ // 收集所有需要拉取的仓库(目标 + 其依赖)
279
+ const reposToPull = new Set<string>([targetKey]);
280
+ const collectDependencies = (key: string): void => {
281
+ const d = graph.get(key);
282
+ if (!d) return;
283
+
284
+ for (const parent of d.dependsOn) {
285
+ const parentKey = `${parent.namespace}:${parent.entity}`;
286
+ if (!reposToPull.has(parentKey)) {
287
+ reposToPull.add(parentKey);
288
+ collectDependencies(parentKey);
289
+ }
290
+ }
291
+ };
292
+
293
+ collectDependencies(targetKey);
294
+
295
+ // 过滤排序列表,只包含需要拉取的仓库
296
+ const orderedRepos = sortedRepos.filter(repo => {
297
+ const key = `${repo.namespace}:${repo.entity}`;
298
+ return reposToPull.has(key);
299
+ });
300
+
301
+ // 按顺序拉取仓库并处理错误
302
+ const results: PullRepositoryResult[] = [];
303
+ const failures: SyncFailure[] = [];
304
+ const failedRepos = new Map<string, Error>();
305
+
306
+ for (const repo of orderedRepos) {
307
+ const result = await pullCascadeNode(sm, graph, entityMap, repo, options, failedRepos);
308
+ results.push(result);
309
+
310
+ // 按策略跳过(`skipped` 且 `success`)不算失败:既不进失败清单,也不阻断下游
311
+ if (result.error) {
312
+ failedRepos.set(repositoryKey(repo), result.error);
313
+ failures.push({ repository: repo, error: result.error });
314
+ }
315
+ }
316
+
317
+ // 返回目标仓库结果,并附带相关仓库的结果
318
+ const targetResult = results.find(r => r.repository.namespace === namespace && r.repository.entity === entity);
319
+ if (!targetResult) {
320
+ throw new RxDBError(`Internal error: target repository result not found`);
321
+ }
322
+
323
+ targetResult.relatedResults = results.filter(r => r !== targetResult);
324
+ // 目标仓的 `failures` 是整次级联的聚合(含目标仓自身);
325
+ // `relatedResults` 里每一项的 `failures` 只覆盖它自己那一次子调用
326
+ targetResult.failures = failures;
327
+
328
+ // 级联里依赖仓的落库与实体改写同样是本次 pull 的成果。
329
+ // 目标仓自己 applied=0 不代表整次调用没写过东西 —— 两个信号必须跨全部仓库取并集,
330
+ // 否则调用方(SyncManager)会漏清 undo 历史、或误判「什么都没发生」。
331
+ for (const related of targetResult.relatedResults) {
332
+ targetResult.persistedProgress ||= related.persistedProgress;
333
+ targetResult.historyInvalidated ||= related.historyInvalidated;
334
+ }
335
+
336
+ // 目标仓没同步成功就必须 reject —— 此前依赖失败时只把目标仓包成
337
+ // `skipped: 'Dependency failed'` 然后 **resolve**,调用方拿到的是一个成功的
338
+ // Promise,而目标仓一条都没拉。
339
+ //
340
+ // 根因取失败清单里的第一条(最上游那个真错误),而不是目标仓身上那层
341
+ // 「被依赖阻断」的合成错误。只有「一条失败且没有任何落库」时才裸抛:
342
+ // 其余情况必须连同聚合结果(含 relatedResults 与 failures)一起交出去,
343
+ // 否则依赖仓已提交的那部分会随着裸抛一起消失。
344
+ const [rootFailure] = failures;
345
+ if (!targetResult.success && rootFailure) {
346
+ if (!targetResult.persistedProgress && failures.length === 1) throw rootFailure.error;
347
+ throw new RxDBPartialSyncError<PullRepositoryResult>(targetResult, rootFailure.error);
348
+ }
349
+
350
+ return targetResult;
351
+ }
352
+
353
+ /**
354
+ * 级联拉取中的单个节点:先过依赖闸门,再过同步资格闸门,最后才真正拉取
355
+ *
356
+ * @internal
357
+ */
358
+ async function pullCascadeNode(
359
+ sm: SyncManager,
360
+ graph: DependencyGraph,
361
+ entityMap: ReadonlyMap<string, EntityType>,
362
+ repo: RepositoryIdentifier,
363
+ options: typeof DEFAULT_PULL_REPOSITORY_OPTIONS,
364
+ failedRepos: ReadonlyMap<string, Error>
365
+ ): Promise<PullRepositoryResult> {
366
+ const repoKey = repositoryKey(repo);
367
+
368
+ // pull 只有一趟、且是父先子后,阻断关系恒定沿 `dependsOn`:父拉不下来,
369
+ // 子的外键就没有可指向的行。push 侧分两个相位,边随相位翻转(见
370
+ // findBlockingDependency 的 @remarks)。
371
+ const blocked = findBlockingDependency(graph, repoKey, failedRepos, 'dependsOn');
372
+ if (blocked) {
373
+ return {
374
+ ...emptyRepositoryProgress(),
375
+ repository: repo,
376
+ success: false,
377
+ skipped: `dependency ${repositoryKey(blocked.dependency)} failed`,
378
+ error: new RxDBDependencyFailedError(repo, blocked.dependency, blocked.cause)
379
+ };
380
+ }
381
+
382
+ const EntityType = entityMap.get(repoKey);
383
+ // entityMap 与依赖图都由 `config.entities` 构建,节点必然能反查回实体类
384
+ if (!EntityType) {
385
+ throw new RxDBError(`Internal error: entity not found for cascade node ${repoKey}`);
386
+ }
387
+
388
+ const repoMetadata = getEntityMetadata(EntityType);
389
+ const repoSyncType = getSyncType(repoMetadata, sm.rxdb.entitySync);
390
+
391
+ // 级联节点必须走和单仓路径同一份资格校验,否则 `local` / `none`
392
+ // 的依赖仓会被拿去问远端要数据,绕过它自己声明的同步策略
393
+ // 关联仓被单独关掉时同样跳过 —— 级联不是绕开开关的后门
394
+ const ineligible = await resolvePullIneligibility(sm.rxdb, repo.namespace, repo.entity, repoSyncType);
395
+ if (ineligible) {
396
+ return { ...emptyRepositoryProgress(), repository: repo, success: true, skipped: ineligible };
397
+ }
398
+
399
+ try {
400
+ const result = await pullSingleRepository(sm, repo.namespace, repo.entity, {
401
+ ...options,
402
+ // T039: 为每个实体独立提取 filter
403
+ filter: resolveCascadeFilter(
404
+ repoKey,
405
+ getSyncConfig(repoMetadata, sm.rxdb.entitySync),
406
+ repoSyncType,
407
+ options.filter
408
+ ),
409
+ includeRelated: false // 防止递归级联
410
+ });
411
+ result.success = true;
412
+ return result;
413
+ } catch (error) {
414
+ // 失败的仓库可能已经提交了前几轮(`RxDBPartialSyncError` 带着这段进度),
415
+ // 写死 0 会把已落库的进度抹掉,也把根因藏进「错误里套错误」。
416
+ const normalized = error instanceof Error ? error : new Error(String(error));
417
+ const cause = normalized instanceof RxDBPartialSyncError ? normalized.cause : normalized;
418
+ return {
419
+ ...(partialRepositoryProgressOf(normalized) ?? emptyRepositoryProgress()),
420
+ repository: repo,
421
+ success: false,
422
+ error: cause,
423
+ failures: [{ repository: repo, error: cause }]
424
+ };
425
+ }
426
+ }
427
+
428
+ /**
429
+ * 解析级联节点自己的 filter
430
+ *
431
+ * @param repoKey - 仓库键(`namespace:entity`),用于错误消息
432
+ * @param repoSync - 该仓库的**生效**同步配置(`getSyncConfig(metadata, rxdb.entitySync)`);
433
+ * 不能传装饰器原值 `metadata.sync`:实例覆盖可能换掉了 filter,也可能把 Full 覆盖成 Filter
434
+ * @param repoSyncType - 该仓库的有效同步类型
435
+ * @param inheritedFilter - 调用方显式传入的 filter
436
+ * @returns 该仓库实际生效的 filter
437
+ * @throws {RxDBError} filter 函数抛错或返回非法 RuleGroup 时
438
+ *
439
+ * @remarks
440
+ * fail-closed:filter 表达的是租户/用户数据边界,抛错或返回非法值一律终止本次同步。
441
+ * 此前这里只 console.warn 再把 filter 置为 undefined —— 后续请求会拉取**全部**远端行,
442
+ * 把一个配置错误变成过量数据落地。与单仓路径保持同一口径。
443
+ *
444
+ * @internal
445
+ */
446
+ export function resolveCascadeFilter(
447
+ repoKey: string,
448
+ repoSync: SyncOptions | undefined,
449
+ repoSyncType: RepositorySyncType,
450
+ inheritedFilter: RuleGroup | undefined
451
+ ): RuleGroup | undefined {
452
+ // T040: Full 同步类型不使用 filter(防止父实体 filter 传播)
453
+ if (repoSyncType === 'full') return undefined;
454
+ if (repoSyncType !== 'filter') return inheritedFilter;
455
+
456
+ const filterFn = (repoSync as { remote?: { filter?: () => RuleGroup } } | undefined)?.remote?.filter;
457
+ if (!filterFn) return inheritedFilter;
458
+
459
+ let extractedFilter: RuleGroup;
460
+ try {
461
+ extractedFilter = filterFn();
462
+ } catch (error) {
463
+ throw new RxDBError(
464
+ `Filter function failed for ${repoKey}: ${error instanceof Error ? error.message : String(error)}`
465
+ );
466
+ }
467
+
468
+ if (!isValidRuleGroup(extractedFilter)) {
469
+ throw new RxDBError(
470
+ `Invalid RuleGroup returned by filter function for ${repoKey}: ` +
471
+ `RuleGroup must have 'combinator' ('and' | 'or') and 'rules' array properties.`
472
+ );
473
+ }
474
+
475
+ return extractedFilter;
476
+ }
477
+
478
+ /**
479
+ * 单仓库拉取(无级联)
480
+ *
481
+ * @internal
482
+ */
483
+ async function pullSingleRepository(
484
+ sm: SyncManager,
485
+ namespace: string,
486
+ entity: string,
487
+ options: typeof DEFAULT_PULL_REPOSITORY_OPTIONS
488
+ ): Promise<PullRepositoryResult> {
489
+ const rxdb = sm.rxdb;
490
+
491
+ // 验证远端适配器
492
+ const remoteAdapterName = rxdb.config.sync?.remote?.adapter;
493
+ if (!remoteAdapterName) {
494
+ throw new RxDBError('Remote adapter not configured.');
495
+ }
496
+
497
+ const { adapter: remoteAdapter } = await sm.getRemoteRepositories();
498
+ const { adapter: localAdapter } = await sm.getLocalRepositories();
499
+
500
+ // 获取当前分支。水位线记录仍按当前分支存(`${ns}:${entity}:${branch.id}`),
501
+ // 但拉取范围要覆盖整条祖先链 —— 两者是不同的东西,不能混为一谈。
502
+ const branch = await sm.getCurrentBranch();
503
+ const branchIds = await getAncestorBranchIds(sm, branch.id);
504
+
505
+ // 获取或创建 RxDBSync 记录
506
+ const repoSyncRepo = localAdapter.getRepository(RxDBSync);
507
+
508
+ const EntityType = sm.rxdb.config.entities.find(e => {
509
+ const meta = getEntityMetadata(e);
510
+ return meta.namespace === namespace && meta.name === entity;
511
+ });
512
+ const metadata = getEntityMetadata(EntityType!);
513
+ const syncType = getSyncType(metadata, sm.rxdb.entitySync);
514
+
515
+ let repoSync = await getOrCreateSyncRecord(
516
+ repoSyncRepo,
517
+ {
518
+ namespace,
519
+ entity,
520
+ branchId: branch.id,
521
+ syncType
522
+ },
523
+ () => rxdb.entityManager.instantiate(RxDBSync)
524
+ );
525
+
526
+ const lastPullRemoteChangeId: number | null = repoSync.lastPullRemoteChangeId;
527
+
528
+ // 汇总结果
529
+ let totalPulled = 0;
530
+ let totalCompacted = 0;
531
+ let totalApplied = 0;
532
+ let totalConflictsResolved = 0;
533
+ const totalConflictsDeferred = 0;
534
+ // 每提交一个轮次事务就置位:该轮的 remoteId 回填与水位线推进已经落库,
535
+ // 后续轮次失败时重试必须从新水位线继续,不能对外报「什么都没发生」。
536
+ let persistedProgress = false;
537
+ let hasMore: boolean;
538
+ let lastRemoteChange: RemoteChange | undefined;
539
+ const conflictResolver = options.conflictResolver ?? new LWWConflictResolver();
540
+
541
+ // 使用最新值确定起始 ID
542
+ let sinceId = lastPullRemoteChangeId ?? 0;
543
+
544
+ let localActions = createEmptyActions();
545
+
546
+ try {
547
+ do {
548
+ // 按仓库和分支过滤拉取变更(支持 filter 条件)
549
+ // 分支范围取整条祖先链,与 pullBatch / pushRepository 同口径:只传当前分支
550
+ // 会让父分支上的变更永远拉不到(分支是 patch 模型,父分支的记录物理上仍归属父分支)。
551
+ // 详见 pullAncestorBranchChanges 的 @remarks。
552
+ const remoteChanges = await pullAncestorBranchChanges(
553
+ remoteAdapter,
554
+ { namespace, entity, sinceId, limit: options.limit, filter: options.filter },
555
+ branchIds
556
+ );
557
+
558
+ totalPulled += remoteChanges.length;
559
+ hasMore = remoteChanges.length >= options.limit;
560
+ if (remoteChanges.length === 0) {
561
+ // 即使没有拉取到变更,也要更新 lastPulledAt 时间戳
562
+ // 这样可以正确记录同步尝试的时间
563
+ await repoSyncRepo.update(repoSync, {
564
+ lastPulledAt: new Date(),
565
+ updatedAt: new Date()
566
+ });
567
+ break;
568
+ }
569
+
570
+ // 记录最后一个远端变更
571
+ lastRemoteChange = remoteChanges[remoteChanges.length - 1];
572
+ sinceId = lastRemoteChange.id;
573
+
574
+ // 分离自己 push 上去的变更和他人的变更(与 pull-batch.ts 共用同一份实现)
575
+ const { ownChanges, otherChanges } = splitRemoteChangesByOrigin(remoteChanges, rxdb.context.clientId);
576
+
577
+ // remoteId 回填、实体应用、supersession 标记与水位线推进必须在**一个**事务里。
578
+ //
579
+ // 否则中途失败时前面几步已落库、水位线也可能已前移:重试会从新水位线继续,
580
+ // 中间那段既不重放也不告警,静默消失(对齐 pull-batch.ts 的既有事务化处理)。
581
+ //
582
+ // 注意:远端拉取(上面的 remoteAdapter.pullChanges)在事务之前就已完成,
583
+ // 事务里只有本地写入,不会把网络往返圈进事务窗口。
584
+ let roundApplied = 0;
585
+ await localAdapter.transaction(async executor => {
586
+ // 仓库必须在事务体**内**经 executor 获取:事务外的 `changeRepo` / `repoSyncRepo`
587
+ // 绑定在适配器上,其读写会重新排队并排在自己这个事务后面(队列并发度 1)。
588
+ const txChangeRepo = executor.getRepository(RxDBChange);
589
+ const txRepoSyncRepo = executor.getRepository(RxDBSync);
590
+ // 为自己 push 的变更更新本地 remoteId。此前这里把「只写没映射过的行」写成 JS 守卫,
591
+ // 而 pull-batch 写在 SQL 里——判据相同、写法不同,改一处漏一处不会红。现已共用。
592
+ await backfillOwnChangeRemoteIds(txChangeRepo, ownChanges);
593
+
594
+ // 只对他人的变更执行压缩和应用
595
+ if (otherChanges.length > 0) {
596
+ compactChanges(otherChanges, localActions);
597
+
598
+ const effectiveCount = countActions(localActions);
599
+ totalCompacted += otherChanges.length - effectiveCount;
600
+
601
+ const actionEntries = createConflictActionEntries(otherChanges, localActions);
602
+ const pendingLocalChanges = await queryPendingLocalChanges(
603
+ txChangeRepo,
604
+ namespace,
605
+ entity,
606
+ branch.id,
607
+ repoSync.lastPushedChangeId,
608
+ [...new Set(actionEntries.map(entry => entry.entityId))]
609
+ );
610
+
611
+ const { applyActions, conflictsResolved, localChangeSupersessions } = await resolveConflictsAndBuildActions(
612
+ actionEntries,
613
+ pendingLocalChanges,
614
+ {
615
+ changeRepo: txChangeRepo,
616
+ conflictResolver,
617
+ dispatchEvent: (event: RxDBEvent) => rxdb.dispatchEvent(event),
618
+ repoLabel: `${namespace}:${entity}`,
619
+ // 推迟 supersession 标记,改由下面 `mergeChanges` 之后自己写。pull-batch 不传这一项、
620
+ // 走的是解冲突时就地标记——两条路径由此分叉,但分叉是**有意的**:本文件的写全在
621
+ // 一个事务里,把「这条本地变更已被远端取代」写在实体合并之后,失败回滚时二者同进同退。
622
+ //
623
+ // 这个位置不会漏标:`localChangeSupersessions` 只在 KEEP_REMOTE 分支 push,而那一支
624
+ // 必定同时 `setAction`,于是非空的 supersession 蕴含 `applyCount > 0`,
625
+ // 下面那道 `if` 永远不会把它挡在外面。
626
+ deferLocalChangeSupersession: true,
627
+ localClientId: rxdb.context.clientId
628
+ }
629
+ );
630
+ totalConflictsResolved += conflictsResolved;
631
+
632
+ const applyCount = countActions(applyActions);
633
+ if (applyCount > 0) {
634
+ // 先将实体变更应用到本地数据库(在更新 RxDBSync 之前)
635
+ // disableTriggers=true 确保不生成本地 RxDBChange 记录
636
+ // 同 pull-batch:远端来的行进工作树,但不回流成一次可 push 的本地变更。
637
+ declareTrustedWrite(executor, {
638
+ file: 'pull-repository.ts',
639
+ symbol: 'pullSingleRepository',
640
+ intent: TrustedWriteIntent.remote_sync
641
+ });
642
+ await executor.mergeChanges(applyActions, undefined, true);
643
+ for (const supersession of localChangeSupersessions) {
644
+ await markLocalChangesSuperseded(txChangeRepo, supersession.localChanges, supersession.remoteId);
645
+ }
646
+ roundApplied = applyCount;
647
+ }
648
+
649
+ // 清除操作以便下一次迭代使用
650
+ localActions = createEmptyActions();
651
+ }
652
+
653
+ // 使用直接 SQL 更新 RxDBSync(避免实体缓存问题)
654
+ const lastChangeId = lastRemoteChange!.id;
655
+ repoSync = await txRepoSyncRepo.update(repoSync, {
656
+ lastPullRemoteChangeId: lastChangeId,
657
+ lastPulledAt: new Date(),
658
+ updatedAt: new Date()
659
+ });
660
+ });
661
+ persistedProgress = true;
662
+ totalApplied += roundApplied;
663
+ } while (hasMore && options.fetchAll);
664
+ } catch (error) {
665
+ // 跨多轮 fetchAll 时,前面几轮的事务已经真实提交;直接裸抛会让调用方
666
+ // 以为「什么都没发生」。一条都没落库时包装只会多剥一层,原始错误更有用
667
+ // (对齐 pull-batch.ts 的 isEmptyProgress/toPartialSyncError 约定)。
668
+ if (!persistedProgress) throw error;
669
+ throw new RxDBPartialSyncError<PullRepositoryResult>(
670
+ {
671
+ repository: { namespace, entity },
672
+ pulled: totalPulled,
673
+ compacted: totalCompacted,
674
+ applied: totalApplied,
675
+ hasMore: true,
676
+ conflictsResolved: totalConflictsResolved,
677
+ conflictsDeferred: totalConflictsDeferred,
678
+ persistedProgress: true,
679
+ historyInvalidated: totalApplied > 0,
680
+ failures: [
681
+ { repository: { namespace, entity }, error: error instanceof Error ? error : new Error(String(error)) }
682
+ ]
683
+ },
684
+ error instanceof Error ? error : new Error(String(error))
685
+ );
686
+ }
687
+
688
+ return {
689
+ repository: { namespace, entity },
690
+ pulled: totalPulled,
691
+ compacted: totalCompacted,
692
+ applied: totalApplied,
693
+ hasMore,
694
+ conflictsResolved: totalConflictsResolved,
695
+ conflictsDeferred: totalConflictsDeferred,
696
+ persistedProgress,
697
+ historyInvalidated: totalApplied > 0,
698
+ failures: []
699
+ };
700
+ }