@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,285 @@
1
+ /**
2
+ * @fileoverview 过期数据清理
3
+ *
4
+ * 清理不再满足 Filter 条件的本地过期数据。
5
+ * 删除操作不会记录到 RxDBChange,因此不会同步到远程。
6
+ */
7
+
8
+ import {
9
+ declareTrustedWrite,
10
+ getEntityMetadata,
11
+ getRxDBChangeEntityIdQueryValues,
12
+ getRxDBEntityIdentityKey,
13
+ getSyncConfig,
14
+ getSyncType,
15
+ type OperatorName,
16
+ type Rule,
17
+ type RuleGroup,
18
+ RxDBChange,
19
+ type RxDBEntityId,
20
+ RxDBError,
21
+ type SwitchVersionActions,
22
+ TrustedWriteIntent
23
+ } from '@aiao/rxdb';
24
+ /**
25
+ * 仅要求「能拿到 RxDBChange 仓库并 find」的最小结构 —— 适配器与
26
+ * {@link TransactionExecutor} 都满足。声明成两者的联合类型会因泛型方法不可调用而失败。
27
+ */
28
+ interface RxDBChangeRepositorySource {
29
+ getRepository(EntityType: typeof RxDBChange): { find(options: never): Promise<unknown> };
30
+ }
31
+
32
+ import type { SyncManager } from './SyncManager.js';
33
+
34
+ /**
35
+ * 过期数据清理选项
36
+ */
37
+ export interface CleanupExpiredOptions {
38
+ /**
39
+ * 过滤条件(可选)
40
+ * 如果不提供,从实体元数据的 sync.remote.filter() 获取
41
+ */
42
+ filter?: RuleGroup;
43
+
44
+ /**
45
+ * 干跑模式,只返回将被删除的数量
46
+ * @default false
47
+ */
48
+ dryRun?: boolean;
49
+ }
50
+
51
+ /**
52
+ * 过期数据清理结果
53
+ */
54
+ export interface CleanupExpiredResult {
55
+ /**
56
+ * 被清理的记录数量
57
+ */
58
+ removed: number;
59
+
60
+ /**
61
+ * 被清理的实体 ID 列表(仅 dryRun 模式或实际删除时)
62
+ */
63
+ removedIds?: RxDBEntityId[];
64
+ }
65
+
66
+ /**
67
+ * 清理不再满足过滤条件的本地过期数据
68
+ *
69
+ * 注意: 删除操作不会记录到 RxDBChange,因此不会同步到远程
70
+ * 这适用于"只保留最近 N 天数据"的场景
71
+ *
72
+ * @param sm - SyncManager 实例
73
+ * @param namespace - 命名空间
74
+ * @param entityName - 实体名
75
+ * @param options - 清理选项
76
+ * @returns 清理结果
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
81
+ * const { removed } = await cleanupExpired(sm, 'public', 'Order', {
82
+ * filter: {
83
+ * combinator: 'and',
84
+ * rules: [{ field: 'updatedAt', operator: '>=', value: thirtyDaysAgo }]
85
+ * }
86
+ * });
87
+ * console.log(`Removed ${removed} expired records`);
88
+ * ```
89
+ */
90
+ export async function cleanupExpired(
91
+ sm: SyncManager,
92
+ namespace: string,
93
+ entityName: string,
94
+ options?: CleanupExpiredOptions
95
+ ): Promise<CleanupExpiredResult> {
96
+ const rxdb = sm.rxdb;
97
+ const { adapter: localAdapter } = await sm.getLocalRepositories();
98
+
99
+ // 验证实体存在
100
+ const EntityType = rxdb.config.entities.find(e => {
101
+ const meta = getEntityMetadata(e);
102
+ return meta.namespace === namespace && meta.name === entityName;
103
+ });
104
+
105
+ if (!EntityType) {
106
+ throw new RxDBError(`Entity not found: ${namespace}:${entityName}`);
107
+ }
108
+
109
+ const metadata = getEntityMetadata(EntityType);
110
+ const syncType = getSyncType(metadata, rxdb.entitySync);
111
+
112
+ // 获取 filter 条件
113
+ let filter = options?.filter;
114
+ if (!filter && syncType === 'filter') {
115
+ // 取生效配置而不是装饰器原值:实例覆盖可能换掉了 filter
116
+ const syncConfig = getSyncConfig(metadata, rxdb.entitySync) as { remote?: { filter?: () => RuleGroup } };
117
+ if (syncConfig?.remote?.filter) {
118
+ try {
119
+ filter = syncConfig.remote.filter();
120
+ } catch (error) {
121
+ throw new RxDBError(
122
+ `Filter function failed for ${namespace}:${entityName}: ${error instanceof Error ? error.message : String(error)}`
123
+ );
124
+ }
125
+ }
126
+ }
127
+
128
+ if (!filter) {
129
+ throw new RxDBError(
130
+ `No filter provided and entity ${namespace}:${entityName} does not have a Filter sync configuration.`
131
+ );
132
+ }
133
+
134
+ // 反转 filter 条件:找出不满足条件的记录
135
+ const invertedFilter = invertRuleGroup(filter);
136
+
137
+ /**
138
+ * 剔除仍有未推送变更的候选。
139
+ *
140
+ * 删除走 `mergeChanges(..., disableTriggers=true)`,**不产生 RxDBChange**。
141
+ * 若候选还有未推送的 INSERT/UPDATE,删掉就等于让那些变更永远推不出去,
142
+ * 远端则永久保留一条幽灵记录 —— 本地与远端从此分叉。这类候选一律跳过。
143
+ *
144
+ * `source` 必须由调用方传入,**不能在闭包里取**:本函数在事务外(dryRun)与事务内
145
+ * 各调用一次,两处的仓库归属不同 —— 事务内必须走 executor 的仓库,否则这次读会
146
+ * 重新排队并排在自己所在的事务后面(队列并发度 1),永久挂起。
147
+ */
148
+ const rejectUnpushed = async (
149
+ candidates: { id: RxDBEntityId }[],
150
+ source: RxDBChangeRepositorySource
151
+ ): Promise<{ id: RxDBEntityId }[]> => {
152
+ if (candidates.length === 0) return candidates;
153
+ const changeRepo = source.getRepository(RxDBChange);
154
+ const unpushed = await changeRepo.find({
155
+ where: {
156
+ combinator: 'and',
157
+ rules: [
158
+ { field: 'namespace', operator: '=', value: namespace },
159
+ { field: 'entity', operator: '=', value: entityName },
160
+ { field: 'remoteId', operator: '=', value: null },
161
+ // 被拒变更(rejectedAt 非空)的 remoteId 也一直是 null,但它永远不会再被推送,
162
+ // 必须和 remoteId = null 一起排除,否则会被误判成「还占着没推完」而继续
163
+ // 保护对应实体不被清理
164
+ { field: 'rejectedAt', operator: '=', value: null },
165
+ { field: 'revertChangeId', operator: '=', value: null },
166
+ {
167
+ field: 'entityId',
168
+ operator: 'in',
169
+ value: getRxDBChangeEntityIdQueryValues(candidates.map(record => record.id))
170
+ }
171
+ ]
172
+ }
173
+ } as never);
174
+ const blocked = new Set(
175
+ (unpushed as { entityId: RxDBEntityId }[]).map(change => getRxDBEntityIdentityKey(change.entityId))
176
+ );
177
+ return blocked.size === 0 ?
178
+ candidates
179
+ : candidates.filter(record => !blocked.has(getRxDBEntityIdentityKey(record.id)));
180
+ };
181
+
182
+ // dryRun 模式只报数、不删除;同样要扣除被保护的候选,否则报的数字是假的
183
+ if (options?.dryRun) {
184
+ const repo = localAdapter.getRepository(EntityType);
185
+ const preview = await rejectUnpushed(await repo.find({ where: invertedFilter }), localAdapter);
186
+ return {
187
+ removed: preview.length,
188
+ removedIds: preview.map(record => record.id)
189
+ };
190
+ }
191
+
192
+ // 复核 + 保护性检查 + 删除必须在同一本地事务内完成。
193
+ // 此前是「事务外查一次 → 按旧快照无条件删」:查询后若用户把记录更新回满足 filter,
194
+ // 仍会被删掉,是稳定可复现的数据破坏。
195
+ return localAdapter.transaction(async executor => {
196
+ // 仓库必须在事务体**内**经 executor 获取:事务外取到的句柄绑定在适配器上,
197
+ // 其读写会重新排队并排在自己这个事务后面
198
+ const repo = executor.getRepository(EntityType);
199
+ // 事务内重新查询,拿到的是最新快照(复核 filter)
200
+ const expiredRecords = await rejectUnpushed(await repo.find({ where: invertedFilter }), executor);
201
+ const removedIds = expiredRecords.map(record => record.id);
202
+
203
+ if (removedIds.length > 0) {
204
+ const deletes = new Map<string, { patch: null; inversePatch: null }>();
205
+ for (const record of expiredRecords) {
206
+ deletes.set(`${namespace}:${entityName}:${getRxDBEntityIdentityKey(record.id)}`, {
207
+ patch: null,
208
+ inversePatch: null
209
+ });
210
+ }
211
+ const actions: SwitchVersionActions = { deletes, updates: new Map(), inserts: new Map() };
212
+ // 使用 mergeChanges + disableTriggers 删除,避免生成 RxDBChange 记录
213
+ // 过期清理走 cleanup_expired 入口:删的是本地缓存副本,不是用户删的数据。
214
+ declareTrustedWrite(executor, {
215
+ file: 'cleanup-expired.ts',
216
+ symbol: 'cleanupExpired',
217
+ intent: TrustedWriteIntent.remote_sync
218
+ });
219
+ await executor.mergeChanges(actions, undefined, true);
220
+ }
221
+
222
+ return { removed: removedIds.length, removedIds };
223
+ });
224
+ }
225
+
226
+ /**
227
+ * 反转 RuleGroup 条件
228
+ *
229
+ * 将 filter 条件转换为"不满足条件"的查询
230
+ * 例如: updatedAt >= 30天前 → updatedAt < 30天前
231
+ */
232
+ function invertRuleGroup(rg: RuleGroup): RuleGroup {
233
+ // 简单实现:使用 NOT 逻辑
234
+ // 对于 AND 条件:NOT (A AND B) = NOT A OR NOT B
235
+ // 对于 OR 条件:NOT (A OR B) = NOT A AND NOT B
236
+ return {
237
+ combinator: rg.combinator === 'and' ? 'or' : 'and',
238
+ rules: rg.rules.map(rule => {
239
+ if ('combinator' in rule) {
240
+ // 嵌套 RuleGroup
241
+ return invertRuleGroup(rule);
242
+ }
243
+ // 单个 Rule:反转操作符
244
+ return invertRule(rule);
245
+ })
246
+ };
247
+ }
248
+
249
+ /**
250
+ * 反转单个 Rule 的操作符
251
+ */
252
+ function invertRule(rule: Rule): Rule {
253
+ const invertedOperators: Partial<Record<OperatorName, OperatorName>> = {
254
+ '=': '!=',
255
+ '!=': '=',
256
+ '<': '>=',
257
+ '<=': '>',
258
+ '>': '<=',
259
+ '>=': '<',
260
+ in: 'notIn',
261
+ notIn: 'in',
262
+ contains: 'notContains',
263
+ notContains: 'contains',
264
+ startsWith: 'notStartsWith',
265
+ notStartsWith: 'startsWith',
266
+ endsWith: 'notEndsWith',
267
+ notEndsWith: 'endsWith',
268
+ between: 'notBetween',
269
+ notBetween: 'between',
270
+ null: 'notNull',
271
+ notNull: 'null',
272
+ exists: 'notExists',
273
+ notExists: 'exists'
274
+ };
275
+
276
+ const invertedOperator = invertedOperators[rule.operator];
277
+ if (!invertedOperator) {
278
+ throw new RxDBError(`Cannot invert operator: ${rule.operator}`);
279
+ }
280
+
281
+ return {
282
+ ...rule,
283
+ operator: invertedOperator
284
+ } as Rule;
285
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * @fileoverview 实体依赖图构建与分析
3
+ *
4
+ * 基于 EntityMetadata 的 relations 构建依赖图,用于:
5
+ * - Pull: 父实体 -> 子实体(确保外键引用的数据已存在)
6
+ * - Push: 子实体 -> 父实体(确保外键约束不被违反)
7
+ */
8
+
9
+ import { RelationKind, type RepositoryIdentifier, RxDBError } from '@aiao/rxdb';
10
+ // 唯一定义收敛到 `VersionManager.interface.ts`。
11
+ interface DependencyGraphEntityRelation {
12
+ kind: RelationKind;
13
+ mappedEntity: string;
14
+ mappedNamespace?: string;
15
+ }
16
+
17
+ interface DependencyGraphEntityMetadata {
18
+ name: string;
19
+ namespace: string;
20
+ relations?: readonly DependencyGraphEntityRelation[];
21
+ }
22
+
23
+ export type { RepositoryIdentifier };
24
+
25
+ /**
26
+ * Repository 依赖关系
27
+ */
28
+ export interface RepositoryDependency {
29
+ /**
30
+ * 当前 repository
31
+ */
32
+ repository: RepositoryIdentifier;
33
+
34
+ /**
35
+ * 依赖的 repository(外键指向的实体)
36
+ * Pull 时必须先同步这些实体
37
+ */
38
+ dependsOn: RepositoryIdentifier[];
39
+
40
+ /**
41
+ * 被依赖的 repository(指向当前实体的外键)
42
+ * Push 时必须等待这些实体同步完成
43
+ */
44
+ requiredBy: RepositoryIdentifier[];
45
+ }
46
+
47
+ /**
48
+ * 依赖图
49
+ * 键:`{namespace}:{entity}`。
50
+ */
51
+ export type DependencyGraph = Map<string, RepositoryDependency>;
52
+
53
+ /**
54
+ * 构建依赖图
55
+ *
56
+ * @param entities - 实体元数据数组
57
+ * @returns 依赖图
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const entities = entityManager.getEntities();
62
+ * const graph = buildDependencyGraph(entities);
63
+ *
64
+ * // 查询 Todo 的依赖
65
+ * const todoDep = graph.get('public:Todo');
66
+ * console.log(todoDep.dependsOn); // [{ namespace: 'public', entity: 'User' }]
67
+ * ```
68
+ */
69
+ export function buildDependencyGraph(entities: readonly DependencyGraphEntityMetadata[]): DependencyGraph {
70
+ const graph: DependencyGraph = new Map();
71
+
72
+ // 第一遍:创建所有节点,分析直接依赖
73
+ for (const entity of entities) {
74
+ const key = `${entity.namespace}:${entity.name}`;
75
+ const dep: RepositoryDependency = {
76
+ repository: { namespace: entity.namespace, entity: entity.name },
77
+ dependsOn: [],
78
+ requiredBy: []
79
+ };
80
+
81
+ // 分析关系,找出外键依赖
82
+ if (entity.relations) {
83
+ for (const relation of entity.relations) {
84
+ // MANY_TO_ONE 和 ONE_TO_ONE 关系中,外键存储在当前实体侧
85
+ if (relation.kind === RelationKind.MANY_TO_ONE || relation.kind === RelationKind.ONE_TO_ONE) {
86
+ const mappedEntity = relation.mappedEntity;
87
+ const mappedNamespace = relation.mappedNamespace || entity.namespace;
88
+
89
+ dep.dependsOn.push({
90
+ namespace: mappedNamespace,
91
+ entity: mappedEntity
92
+ });
93
+ }
94
+ }
95
+ }
96
+
97
+ graph.set(key, dep);
98
+ }
99
+
100
+ // 第二遍:反向填充 requiredBy
101
+ for (const dep of graph.values()) {
102
+ for (const parent of dep.dependsOn) {
103
+ const parentDep = graph.get(`${parent.namespace}:${parent.entity}`);
104
+ if (parentDep) {
105
+ parentDep.requiredBy.push(dep.repository);
106
+ }
107
+ }
108
+ }
109
+
110
+ // 检测循环依赖
111
+ detectCycles(graph);
112
+
113
+ return graph;
114
+ }
115
+
116
+ /**
117
+ * 检测循环依赖
118
+ *
119
+ * 注意:自引用(self-reference)不算循环依赖,因为树形结构是合法的
120
+ * 例如:RxDBBranch -> RxDBBranch (parent-child 关系)
121
+ *
122
+ * @param graph - 依赖图
123
+ * @throws {RxDBError} 如果检测到循环依赖
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * // 抛出错误示例:
128
+ * // User -> Profile -> User (循环依赖)
129
+ * detectCycles(graph); // 抛出 RxDBError
130
+ *
131
+ * // 允许的情况:
132
+ * // RxDBBranch -> RxDBBranch (自引用,树形结构)
133
+ * ```
134
+ */
135
+ export function detectCycles(graph: DependencyGraph): void {
136
+ const visited = new Set<string>();
137
+ const recStack = new Set<string>();
138
+ const path: string[] = [];
139
+
140
+ function dfs(key: string): boolean {
141
+ visited.add(key);
142
+ recStack.add(key);
143
+ path.push(key);
144
+
145
+ const dep = graph.get(key);
146
+ if (!dep) {
147
+ // 悬挂依赖:回滚本次入栈再退出。漏掉回滚会把该 key 永久留在 recStack 里,
148
+ // 后续任何真实路径再指向它都会被判成循环依赖(误报)。
149
+ recStack.delete(key);
150
+ path.pop();
151
+ return false;
152
+ }
153
+
154
+ for (const parent of dep.dependsOn) {
155
+ const parentKey = `${parent.namespace}:${parent.entity}`;
156
+
157
+ // 跳过自引用(树形结构)
158
+ if (parentKey === key) {
159
+ continue;
160
+ }
161
+
162
+ if (!visited.has(parentKey)) {
163
+ if (dfs(parentKey)) return true;
164
+ } else if (recStack.has(parentKey)) {
165
+ // 找到循环依赖
166
+ const cycleStart = path.indexOf(parentKey);
167
+ const cycle = [...path.slice(cycleStart), parentKey];
168
+ throw new RxDBError(`Circular dependency detected: ${cycle.join(' -> ')}`);
169
+ }
170
+ }
171
+
172
+ recStack.delete(key);
173
+ path.pop();
174
+ return false;
175
+ }
176
+
177
+ for (const key of graph.keys()) {
178
+ if (!visited.has(key)) {
179
+ dfs(key);
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * 获取 repository 的所有依赖(包括间接依赖)
186
+ *
187
+ * 注意:自引用会被跳过,避免无限递归
188
+ *
189
+ * @param namespace - 命名空间
190
+ * @param entity - 实体名称
191
+ * @param graph - 依赖图
192
+ * @returns 所有依赖的 repository(拓扑排序,父在前)
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * // Comment -> Todo -> User
197
+ * const deps = getAllDependencies('public', 'Comment', graph);
198
+ * // 返回: [User, Todo, Comment]
199
+ * ```
200
+ */
201
+ export function getAllDependencies(namespace: string, entity: string, graph: DependencyGraph): RepositoryIdentifier[] {
202
+ const key = `${namespace}:${entity}`;
203
+ const dep = graph.get(key);
204
+ if (!dep) {
205
+ throw new RxDBError(`Repository ${key} not found in dependency graph`);
206
+ }
207
+
208
+ const result: RepositoryIdentifier[] = [];
209
+ const visited = new Set<string>();
210
+
211
+ function collect(currentKey: string): void {
212
+ if (visited.has(currentKey)) return;
213
+ visited.add(currentKey);
214
+
215
+ const currentDep = graph.get(currentKey);
216
+ if (!currentDep) return;
217
+
218
+ // 先递归收集依赖的实体
219
+ for (const parent of currentDep.dependsOn) {
220
+ const parentKey = `${parent.namespace}:${parent.entity}`;
221
+
222
+ // 跳过自引用(树形结构)
223
+ if (parentKey === currentKey) {
224
+ continue;
225
+ }
226
+
227
+ collect(parentKey);
228
+ }
229
+
230
+ // 再添加当前实体
231
+ result.push(currentDep.repository);
232
+ }
233
+
234
+ collect(key);
235
+ return result;
236
+ }
237
+
238
+ /**
239
+ * 获取 repository 的所有被依赖方(包括间接被依赖)
240
+ *
241
+ * 注意:自引用会被跳过,避免无限递归
242
+ *
243
+ * @param namespace - 命名空间
244
+ * @param entity - 实体名称
245
+ * @param graph - 依赖图
246
+ * @returns 所有被依赖的 repository(拓扑排序,子在前)
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * // User <- Todo <- Comment
251
+ * const requiredBy = getAllRequiredBy('public', 'User', graph);
252
+ * // 返回: [Comment, Todo, User]
253
+ * ```
254
+ */
255
+ export function getAllRequiredBy(namespace: string, entity: string, graph: DependencyGraph): RepositoryIdentifier[] {
256
+ const key = `${namespace}:${entity}`;
257
+ const dep = graph.get(key);
258
+ if (!dep) {
259
+ throw new RxDBError(`Repository ${key} not found in dependency graph`);
260
+ }
261
+
262
+ const result: RepositoryIdentifier[] = [];
263
+ const visited = new Set<string>();
264
+
265
+ function collect(currentKey: string): void {
266
+ if (visited.has(currentKey)) return;
267
+ visited.add(currentKey);
268
+
269
+ const currentDep = graph.get(currentKey);
270
+ if (!currentDep) return;
271
+
272
+ // 先递归收集被依赖的实体
273
+ for (const child of currentDep.requiredBy) {
274
+ const childKey = `${child.namespace}:${child.entity}`;
275
+
276
+ // 跳过自引用(树形结构)
277
+ if (childKey === currentKey) {
278
+ continue;
279
+ }
280
+
281
+ collect(childKey);
282
+ }
283
+
284
+ // 再添加当前实体
285
+ result.push(currentDep.repository);
286
+ }
287
+
288
+ collect(key);
289
+ return result;
290
+ }
291
+
292
+ /**
293
+ * 获取 repository 的直接父实体(一层依赖)
294
+ *
295
+ * @param namespace - 命名空间
296
+ * @param entity - 实体名称
297
+ * @param graph - 依赖图
298
+ * @returns 直接父实体列表
299
+ */
300
+ export function getDirectParents(namespace: string, entity: string, graph: DependencyGraph): RepositoryIdentifier[] {
301
+ const key = `${namespace}:${entity}`;
302
+ const dep = graph.get(key);
303
+ if (!dep) {
304
+ throw new RxDBError(`Repository ${key} not found in dependency graph`);
305
+ }
306
+ return dep.dependsOn;
307
+ }
308
+
309
+ /**
310
+ * 获取 repository 的直接子实体(一层被依赖)
311
+ *
312
+ * @param namespace - 命名空间
313
+ * @param entity - 实体名称
314
+ * @param graph - 依赖图
315
+ * @returns 直接子实体列表
316
+ */
317
+ export function getDirectChildren(namespace: string, entity: string, graph: DependencyGraph): RepositoryIdentifier[] {
318
+ const key = `${namespace}:${entity}`;
319
+ const dep = graph.get(key);
320
+ if (!dep) {
321
+ throw new RxDBError(`Repository ${key} not found in dependency graph`);
322
+ }
323
+ return dep.requiredBy;
324
+ }