@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,115 @@
1
+ /**
2
+ * @fileoverview 获取所有仓库的同步状态
3
+ *
4
+ * 提供查询所有仓库同步状态的功能,支持按 syncType、启用状态和待处理变更进行过滤。
5
+ */
6
+
7
+ import { getEntityMetadata, type RxDB } from '@aiao/rxdb';
8
+ import {
9
+ getRepositorySyncStatus,
10
+ type RepositorySyncStatus,
11
+ type SyncTypeValue
12
+ } from './get-repository-sync-status.js';
13
+
14
+ /**
15
+ * getAllRepositorySyncStatus 的过滤选项
16
+ */
17
+ export interface GetAllRepositorySyncStatusFilter {
18
+ /**
19
+ * 按同步类型过滤
20
+ * 如果提供,则仅返回这些同步类型的仓库
21
+ */
22
+ syncType?: SyncTypeValue[];
23
+
24
+ /**
25
+ * 按启用状态过滤
26
+ * 如果为 true,则仅返回启用的仓库
27
+ * 如果为 false,则仅返回禁用的仓库
28
+ * 如果为 undefined,则返回全部
29
+ */
30
+ enabled?: boolean;
31
+
32
+ /**
33
+ * 按待处理变更过滤
34
+ * 如果为 true,则仅返回 pushableCount > 0 或 pullableCount > 0 的仓库
35
+ * 如果为 false,则仅返回没有待处理变更的仓库
36
+ * 如果为 undefined,则返回全部
37
+ */
38
+ hasPendingChanges?: boolean;
39
+ }
40
+
41
+ /**
42
+ * 获取所有仓库的同步状态
43
+ *
44
+ * 遍历所有注册实体并查询它们的同步状态,支持按 syncType、启用状态和待处理变更进行过滤。
45
+ *
46
+ * @param rxdb - RxDB 实例
47
+ * @param filter - 可选的过滤条件
48
+ * @returns 仓库同步状态数组
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * // 获取所有仓库状态
53
+ * const statuses = await getAllRepositorySyncStatus(rxdb);
54
+ *
55
+ * // 仅获取启用的仓库
56
+ * const statuses = await getAllRepositorySyncStatus(rxdb, {
57
+ * enabled: true
58
+ * });
59
+ *
60
+ * // 仅获取有待处理变更的仓库
61
+ * const statuses = await getAllRepositorySyncStatus(rxdb, {
62
+ * hasPendingChanges: true
63
+ * });
64
+ *
65
+ * // 仅获取 full/remote 同步类型的仓库
66
+ * const statuses = await getAllRepositorySyncStatus(rxdb, {
67
+ * syncType: ['full', 'remote']
68
+ * });
69
+ * ```
70
+ */
71
+ export async function getAllRepositorySyncStatus(
72
+ rxdb: RxDB,
73
+ filter?: GetAllRepositorySyncStatusFilter
74
+ ): Promise<RepositorySyncStatus[]> {
75
+ // 1. 从配置中获取所有实体
76
+ const entities = rxdb.config.entities;
77
+ const allStatuses: RepositorySyncStatus[] = [];
78
+
79
+ // 2. 查询每个实体的状态
80
+ for (const EntityClass of entities) {
81
+ const metadata = getEntityMetadata(EntityClass);
82
+ const namespace = metadata.namespace;
83
+ const entity = metadata.name;
84
+
85
+ // 获取该实体的状态
86
+ const status = await getRepositorySyncStatus(rxdb, namespace, entity);
87
+ allStatuses.push(status);
88
+ }
89
+
90
+ // 3. 应用过滤条件
91
+ let filteredStatuses = allStatuses;
92
+
93
+ // 按 syncType 过滤
94
+ if (filter?.syncType && filter.syncType.length > 0) {
95
+ filteredStatuses = filteredStatuses.filter(status => filter.syncType!.includes(status.syncType));
96
+ }
97
+
98
+ // 按 enabled 状态过滤
99
+ if (filter?.enabled !== undefined) {
100
+ filteredStatuses = filteredStatuses.filter(status => status.enabled === filter.enabled);
101
+ }
102
+
103
+ // 按 hasPendingChanges 过滤
104
+ if (filter?.hasPendingChanges !== undefined) {
105
+ if (filter.hasPendingChanges) {
106
+ // 仅返回有待处理变更的仓库
107
+ filteredStatuses = filteredStatuses.filter(status => status.pushableCount > 0 || status.pullableCount > 0);
108
+ } else {
109
+ // 仅返回没有待处理变更的仓库
110
+ filteredStatuses = filteredStatuses.filter(status => status.pushableCount === 0 && status.pullableCount === 0);
111
+ }
112
+ }
113
+
114
+ return filteredStatuses;
115
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @fileoverview 获取仓库同步状态
3
+ *
4
+ * 提供查询单个仓库同步状态的功能,包含 syncType、pushableCount、pullableCount 以及最后同步时间戳等信息。
5
+ */
6
+
7
+ import {
8
+ getEntityMetadata,
9
+ getSyncCapability,
10
+ getSyncType,
11
+ isRepositorySyncEnabled,
12
+ type RepositoryIdentifier,
13
+ type RxDB,
14
+ RxDBChange,
15
+ type RxDBChangeRuleGroup,
16
+ RxDBSync,
17
+ type RxDBSyncRuleGroup
18
+ } from '@aiao/rxdb';
19
+ import { checkRepositoryUpdates } from './check-repository-updates.js';
20
+ import type { SyncManager } from './SyncManager.js';
21
+ // 唯一定义收敛到 `VersionManager.interface.ts`。
22
+ export type { RepositoryIdentifier };
23
+
24
+ /**
25
+ * 同步类型的取值
26
+ */
27
+ export type SyncTypeValue = 'full' | 'filter' | 'querycache' | 'remote' | 'local' | 'none';
28
+
29
+ /**
30
+ * 仓库同步状态
31
+ */
32
+ export interface RepositorySyncStatus {
33
+ /**
34
+ * 仓库标识
35
+ */
36
+ repository: RepositoryIdentifier;
37
+
38
+ /**
39
+ * 分支 ID
40
+ */
41
+ branchId: string;
42
+
43
+ /**
44
+ * 同步类型
45
+ */
46
+ syncType: SyncTypeValue;
47
+
48
+ /**
49
+ * 是否启用同步
50
+ */
51
+ enabled: boolean;
52
+
53
+ /**
54
+ * 最后一次推送的 changeId
55
+ */
56
+ lastPushedChangeId: number | null;
57
+
58
+ /**
59
+ * 最后一次推送时间戳
60
+ */
61
+ lastPushedAt: Date | null;
62
+
63
+ /**
64
+ * 最后一次拉取时间戳
65
+ */
66
+ lastPulledAt: Date | null;
67
+
68
+ /**
69
+ * 最后一次拉取到的远端 changeId
70
+ */
71
+ lastPullRemoteChangeId: number | null;
72
+
73
+ /**
74
+ * 待推送的变更数量
75
+ */
76
+ pushableCount: number;
77
+
78
+ /**
79
+ * 可供拉取的变更数量
80
+ */
81
+ pullableCount: number;
82
+ }
83
+
84
+ /**
85
+ * 计算仓库的待推送变更数量
86
+ *
87
+ * 逻辑:
88
+ * - 统计 RxDBChange 记录,满足以下条件:
89
+ * 1. 属于该实体(entity 字段匹配)
90
+ * 2. 属于当前分支
91
+ * 3. 不是远端产生的(remoteId 为 null)
92
+ * 4. 未被撤销(revertChangeId 为 null)
93
+ * 5. 比 lastPushedChangeId 新(如果存在)
94
+ *
95
+ * @param sm - 调用方持有的同步管理器实例
96
+ * @param namespace - 实体命名空间
97
+ * @param entity - 实体名称
98
+ * @param branchId - 当前分支 ID
99
+ * @param lastPushedChangeId - 最后一次推送的 changeId(若未推送则为 null)
100
+ * @returns 待推送的变更数量
101
+ */
102
+ async function calculatePushableCount(
103
+ sm: SyncManager,
104
+ namespace: string,
105
+ entity: string,
106
+ branchId: string,
107
+ lastPushedChangeId: number | null
108
+ ): Promise<number> {
109
+ const { adapter: localAdapter } = await sm.getLocalRepositories();
110
+ const changeRepo = localAdapter.getRepository(RxDBChange);
111
+
112
+ // 构建查询规则
113
+ const rules: RxDBChangeRuleGroup['rules'] = [
114
+ { field: 'namespace', operator: '=', value: namespace },
115
+ { field: 'entity', operator: '=', value: entity },
116
+ { field: 'branchId', operator: '=', value: branchId },
117
+ { field: 'remoteId', operator: '=', value: null },
118
+ // 被拒变更(rejectedAt 非空)的 remoteId 也一直是 null,但它永远不会再被推送,
119
+ // 必须和 remoteId = null 一起排除,否则会被当成待推变更计进 pushableCount
120
+ { field: 'rejectedAt', operator: '=', value: null },
121
+ { field: 'revertChangeId', operator: '=', value: null }
122
+ ];
123
+
124
+ // 如果存在 lastPushedChangeId,则添加过滤条件
125
+ if (lastPushedChangeId !== null) {
126
+ rules.push({ field: 'id', operator: '>', value: lastPushedChangeId });
127
+ }
128
+
129
+ const result = await changeRepo.find({
130
+ where: {
131
+ combinator: 'and',
132
+ rules
133
+ }
134
+ });
135
+
136
+ return result.length;
137
+ }
138
+
139
+ /**
140
+ * 计算仓库可拉取变更的数量
141
+ *
142
+ * 使用 checkRepositoryUpdates() 查询远端变更计数,避免下载实际数据。
143
+ *
144
+ * @param rxdb - RxDB 实例
145
+ * @param namespace - 实体命名空间
146
+ * @param entity - 实体名称
147
+ * @returns 可拉取的变更数量
148
+ */
149
+ async function calculatePullableCount(rxdb: RxDB, namespace: string, entity: string): Promise<number> {
150
+ const result = await checkRepositoryUpdates(rxdb, namespace, entity);
151
+ return result.pendingCount;
152
+ }
153
+
154
+ /**
155
+ * 获取单个仓库的同步状态
156
+ *
157
+ * @param rxdb - RxDB 实例
158
+ * @param namespace - 实体命名空间
159
+ * @param entity - 实体名称
160
+ * @returns 仓库同步状态
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * const status = await getRepositorySyncStatus(rxdb, 'public', 'Todo');
165
+ * console.log(`Sync type: ${status.syncType}`);
166
+ * console.log(`Pushable: ${status.pushableCount}, Pullable: ${status.pullableCount}`);
167
+ * ```
168
+ */
169
+ export async function getRepositorySyncStatus(
170
+ rxdb: RxDB,
171
+ namespace: string,
172
+ entity: string
173
+ ): Promise<RepositorySyncStatus> {
174
+ // 1. 查找实体类并获取元数据
175
+ const EntityClass = rxdb.config.entities.find(e => {
176
+ const meta = getEntityMetadata(e);
177
+ return meta.namespace === namespace && meta.name === entity;
178
+ });
179
+
180
+ if (!EntityClass) {
181
+ throw new Error(`Entity not found: ${namespace}.${entity}`);
182
+ }
183
+
184
+ const metadata = getEntityMetadata(EntityClass);
185
+ const syncType = getSyncType(metadata, rxdb.entitySync);
186
+ const syncCapability = getSyncCapability(syncType);
187
+
188
+ // 2. 获取当前分支
189
+ // 槽位只解析这一次:`rxdb.syncManager` 由插件作用域挂着,断连即被删除,
190
+ // 一次查询里反复重解析等于给自己留三个 `undefined` 的窗口。
191
+ const sm = rxdb.syncManager;
192
+ const branch = await sm.getCurrentBranch();
193
+ const branchId = branch.id;
194
+
195
+ // 3. 查询 RxDBSync 记录
196
+ const { adapter: localAdapter } = await sm.getLocalRepositories();
197
+ const repoSyncRepo = localAdapter.getRepository(RxDBSync);
198
+ const repoSyncId = `${namespace}:${entity}:${branchId}`;
199
+
200
+ const repoSyncWhere: RxDBSyncRuleGroup = {
201
+ combinator: 'and',
202
+ rules: [{ field: 'id', operator: '=', value: repoSyncId }]
203
+ };
204
+
205
+ const repoSyncResults = await repoSyncRepo.find({ where: repoSyncWhere, limit: 1 });
206
+
207
+ const repoSync = repoSyncResults.length > 0 ? repoSyncResults[0] : null;
208
+
209
+ // 4. 计算 pushableCount 和 pullableCount
210
+ let pushableCount = 0;
211
+ let pullableCount = 0;
212
+
213
+ // 推拉能力取开头那一次解析的结果:中间隔着几个 await,重新解析可能与返回的 syncType 不一致
214
+ if (syncCapability.push) {
215
+ pushableCount = await calculatePushableCount(sm, namespace, entity, branchId, repoSync?.lastPushedChangeId ?? null);
216
+ }
217
+
218
+ if (syncCapability.pull) {
219
+ pullableCount = await calculatePullableCount(rxdb, namespace, entity);
220
+ }
221
+
222
+ // 5. 构建并返回状态
223
+ return {
224
+ repository: { namespace, entity },
225
+ branchId,
226
+ syncType,
227
+ // 口径与调度侧同源 —— 两个方向都不可同步的(`none` / `local`)恒为
228
+ // 关闭;有能力时才由 `RxDBSync.enabled` 说了算(记录还没懒创建 ⇒ 启用)
229
+ enabled: syncCapability.pull || syncCapability.push ? isRepositorySyncEnabled(repoSync) : false,
230
+ lastPushedChangeId: repoSync?.lastPushedChangeId ?? null,
231
+ lastPushedAt: repoSync?.lastPushedAt ?? null,
232
+ lastPulledAt: repoSync?.lastPulledAt ?? null,
233
+ lastPullRemoteChangeId: repoSync?.lastPullRemoteChangeId ?? null,
234
+ pushableCount,
235
+ pullableCount
236
+ };
237
+ }
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * `@aiao/rxdb-plugin-sync` —— 推拉同步、冲突处理与仓库依赖序。
4
+ *
5
+ * @remarks
6
+ * 核心 `@aiao/rxdb` 只保留**原语**:`RxDBSync` 水位表、变更编解码、冲突模型
7
+ * (`ConflictResolution` / `RxDBConflictError`)与同步资格判定。搬进本包的是**消费者**:
8
+ * 整库与单仓库两个粒度的 push / pull / sync、冲突落地、依赖拓扑序、过期清理与出站队列。
9
+ *
10
+ * US-025 阶段 D 之前这些入口挂在 `@aiao/rxdb-plugin-history` 的 `VersionManager` 上。
11
+ * 两件事的生命周期本就不同:撤销重做要在第一条 `rxdb_change` 之前就位,推拉同步则只在
12
+ * 配了远端时才有意义,所以它们各自成包。
13
+ *
14
+ * 本包 `inject: ['plugin:history']`,反向没有依赖——历史包不认识本包。这条单向耦合只走
15
+ * 一张窄接口:一次往返结束时 undo 边界要作废、待拉计数要结算,而那些状态的主人在历史侧。
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { rxDBPluginHistory } from '@aiao/rxdb-plugin-history';
20
+ * import { rxDBPluginSync } from '@aiao/rxdb-plugin-sync';
21
+ *
22
+ * rxdb.use(rxDBPluginHistory);
23
+ * rxdb.use(rxDBPluginSync);
24
+ * await rxdb.connect();
25
+ *
26
+ * await rxdb.syncManager.sync();
27
+ * ```
28
+ */
29
+ export * from './plugin.js';
30
+ // `SyncManager.bulkSync()` 的形参与返回值。
31
+ export type { BulkSyncOptions, BulkSyncResult } from './bulk-sync.js';
32
+ // `SyncManager.checkRepositoryUpdates()` 的返回值。函数本身是内部实现。
33
+ export type { CheckRepositoryUpdatesResult } from './check-repository-updates.js';
34
+ // `SyncManager.cleanupExpired()` 的形参与返回值。同名的自由函数是该方法的实现,
35
+ // 全仓只有 `SyncManager.ts:427` 与本包测试相对 import 它——转出去等于多一个能绕开
36
+ // `SyncManager` 自身前置校验的入口。
37
+ export type { CleanupExpiredOptions, CleanupExpiredResult } from './cleanup-expired.js';
38
+ // `SyncManager.getRepositoryDependencyGraph()` 的返回值。图的构建函数是内部实现。
39
+ export type { DependencyGraph } from './dependency-graph.js';
40
+ // `SyncManager.getAllRepositorySyncStatus()` 的可选过滤器。
41
+ export type { GetAllRepositorySyncStatusFilter } from './get-all-repository-sync-status.js';
42
+ // `getRepositorySyncStatus()` 的返回值。
43
+ export type { RepositorySyncStatus } from './get-repository-sync-status.js';
44
+ // QueryCache 出站队列的三个入口(US-025 阶段 D 从核心搬来)。前两个供应用与 DevTools
45
+ // 手动驱动一轮重放 / 读队列长度;`pendingQueryCacheWriteIds` 还兼作核心
46
+ // `QueryCacheOutboxProvider` 的实现,由本包的插件在 `install()` 里登记进宿主。
47
+ export {
48
+ countQueryCacheOutbox,
49
+ flushQueryCacheOutbox,
50
+ pendingQueryCacheWriteIds,
51
+ type QueryCacheOutboxFailure,
52
+ type QueryCacheOutboxResult
53
+ } from './query-cache-outbox.js';
54
+ // 同 `cleanup-expired`:只转 `SyncManager.syncBranches()` 的返回值(连同 `skipReasons` 的
55
+ // 元素类型),自由函数收回。
56
+ export type { SyncBranchSkipReason, SyncBranchesResult } from './sync-branches.js';
57
+ // 实例由插件装配,用户不自己 new,因此只转类型不转类。
58
+ export type { SyncManager } from './SyncManager.js';
package/src/plugin.ts ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @fileoverview RxDB 推拉同步插件
3
+ * 把 {@link SyncManager} 装进宿主的连接纪元,并接住核心同步状态枢纽的「重算待拉数」请求。
4
+ *
5
+ * @module rxdb-plugin-sync
6
+ */
7
+
8
+ import { IRxDBPlugin, Plugin, RxDB, RxDBPluginBase } from '@aiao/rxdb';
9
+ import type { LifecycleScope } from '@aiao/utils';
10
+ import { createSyncBranchMaterializationSource } from './branch-materialization-source.js';
11
+ import { pendingQueryCacheWriteIds } from './query-cache-outbox.js';
12
+ import { SyncManager } from './SyncManager.js';
13
+
14
+ /** 本插件当前不接受任何选项 */
15
+ export type RxDBPluginSyncOptions = object;
16
+
17
+ /**
18
+ * 推拉同步 / 冲突 / 仓库依赖序插件。
19
+ *
20
+ * @remarks
21
+ * **声明 `inject: ['plugin:history']`**,且只声明这一条:
22
+ *
23
+ * - 要历史插件,是因为一次往返结束时 undo 边界要作废、待拉计数要结算,而那些状态的主人
24
+ * 在历史侧。{@link SyncManager} 拿的是 `rxdb.versionManager.syncBridge` 这张窄接口,
25
+ * 没有它连构造都无从谈起。依赖缺失时宿主不装本插件、只告警一次,`rxdb.syncManager`
26
+ * 这个槽位于是不存在——没有历史就没有同步,这正是实情。
27
+ * - **不要 `adapter:remote`**:同步入口本身是按调用发起的,装配阶段一条适配器读写都不发;
28
+ * 而 {@link SyncManager.init} 挂的 `connected$` 自动回推订阅必须在第一次连接就位之前
29
+ * 就已经订阅上,声明适配器依赖会把安装推到引导链之后,第一次连接的那一跳回推就丢了。
30
+ *
31
+ * 六处宿主改动都登记在 `scope` 上,断开连接时由宿主逆序释放:
32
+ *
33
+ * 1. `rxdb.syncManager` 这个实例槽位 —— 释放时连同 `destroy()` 一起撤掉,不给下一个纪元
34
+ * 留一个指向已拆事件总线的管理器;
35
+ * 2. {@link RxDB.syncState} 上的「重算待拉数」跳板 —— 远端适配器在实时订阅恢复后按
36
+ * `syncState.requestPullableRefresh()` 发信号,它不认识 {@link SyncManager},接住这一跳
37
+ * 是本插件的活(US-025 阶段 D 之前这条绑定在历史插件上,随 `refreshPullableCount` 一起
38
+ * 搬过来)。没装插件时那个请求发进空里,正是「待拉数无人维护」的实情。
39
+ * 3. {@link RxDB.queryCacheOutbox} 这个出站队列槽 —— QueryCache 实体的读引擎要靠它把
40
+ * 「远端没返回」和「本地离线写过」区分开。这一条**没有**缺席形态:槽空着时核心在
41
+ * `connect()` 就抛 `RxDBMissingPluginError`,因为「当空集」等于把每条离线写都当孤儿删掉。
42
+ * 4. 宿主的 `online` / `offline` 监听 —— `ReachabilityMonitor` 只在有人 `watch()` 时才往
43
+ * `globalThis` 上挂,而这两个事件唯一的消费者是本包的同步监听器(`wakeup$` 驱动回推
44
+ * 重试)。不同步的库不该因为 `new RxDB()` 就永久多一对活过实例的监听器(US-025 D2)。
45
+ * 5. {@link RxDB.branchMaterializationSource} 这个分支物化来源槽 —— `syncBranches()` 拉下来的
46
+ * 分支只有元数据,工作树插件第一次切过去之前要按来源交出的分页快照物化。远端是本插件的,
47
+ * 来源于是也只能由本插件交;槽空着时那次切换稳定抛 `branch_not_materialized`,
48
+ * 不存在「没装同步也能物化」的形态。槽位至多一个来源,本插件登记即占住。
49
+ */
50
+ export class RxDBPluginSync extends RxDBPluginBase implements IRxDBPlugin {
51
+ readonly lifecycle = 'scoped' as const;
52
+ readonly inject = ['plugin:history'] as const;
53
+ name: Uncapitalize<string> = 'sync';
54
+
55
+ install(scope: LifecycleScope) {
56
+ // `inject` 已经保证历史插件先装完,`versionManager` 槽位此刻一定在。
57
+ // 借的是 `syncBridge` 而不是管理器本身:面越窄,两个包越不会在演进中互相拽住。
58
+ const syncManager = new SyncManager(this.rxdb, this.rxdb.versionManager.syncBridge);
59
+ scope.acquire(() => () => syncManager.destroy(), 'sync:syncManager');
60
+
61
+ scope.acquire(() => {
62
+ Object.defineProperty(this.rxdb, 'syncManager', {
63
+ value: syncManager,
64
+ configurable: true,
65
+ enumerable: true
66
+ });
67
+ return () => void Reflect.deleteProperty(this.rxdb, 'syncManager');
68
+ }, 'sync:slot');
69
+
70
+ // 只登记 `pendingWriteIds` 这一个问题。`flushQueryCacheOutbox` / `countQueryCacheOutbox`
71
+ // 的调用者(同步监听器、DevTools 面板)都在本包里,核心不必认识它们。
72
+ // 传 scope:断开连接时这条注册跟着一起撤销,不留一个指向已拆纪元的队列。
73
+ this.rxdb.queryCacheOutbox(
74
+ { pendingWriteIds: (namespace, entity) => pendingQueryCacheWriteIds(this.rxdb, namespace, entity) },
75
+ scope
76
+ );
77
+
78
+ // 传 scope:断开连接时撤销,下一个纪元登记的是指向新 syncManager 的那一个,不撞「至多一个」。
79
+ this.rxdb.branchMaterializationSource(createSyncBranchMaterializationSource(syncManager), scope);
80
+
81
+ // 可达性监听:`watch()` 自己返回撤销函数,正好是 `acquire` 要的 setup 形状。
82
+ // 引用计数在监视器一侧,所以同一个宿主上多装几个消费者也不会互相摘掉对方的监听。
83
+ scope.acquire(() => this.rxdb.reachability.watch(), 'sync:reachability');
84
+
85
+ syncManager.init();
86
+
87
+ // 跳板约定「回调不得抛出」:这里即发即忘,没有调用方接得住错误,也没有位置重试。
88
+ // 重算失败只说明这一次的读数没刷新,下一次实时恢复还会再请求一次,不该炸掉
89
+ // 适配器的订阅恢复路径。
90
+ scope.acquire(
91
+ () =>
92
+ this.rxdb.syncState.bindPullableRefresh(() => {
93
+ void syncManager.refreshPullableCount().catch((error: unknown) => {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ console.warn(`[RxDB Sync] 重算待拉数失败:${message}。`);
96
+ });
97
+ }),
98
+ 'sync:pullableRefresh'
99
+ );
100
+ }
101
+ }
102
+
103
+ declare module '@aiao/rxdb' {
104
+ interface RxDB {
105
+ /**
106
+ * 推拉同步子系统。
107
+ *
108
+ * @remarks
109
+ * 由 `@aiao/rxdb-plugin-sync` 在连接纪元内装配。**没装插件(或没装它依赖的
110
+ * `@aiao/rxdb-plugin-history`)时这个槽位不存在**:读它拿到 `undefined`,
111
+ * 而不是一个什么都不做的空壳——核心不做 fallback 兜底。
112
+ */
113
+ syncManager: SyncManager;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * 同步插件工厂。
119
+ *
120
+ * @param db - 宿主实例
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * import { rxDBPluginHistory } from '@aiao/rxdb-plugin-history';
125
+ * import { rxDBPluginSync } from '@aiao/rxdb-plugin-sync';
126
+ *
127
+ * // 顺序随意:宿主按 `inject` 拓扑排序,历史插件一定先装完。
128
+ * rxdb.use(rxDBPluginSync);
129
+ * rxdb.use(rxDBPluginHistory);
130
+ * ```
131
+ */
132
+ export const rxDBPluginSync: Plugin<RxDBPluginSyncOptions> = (db: RxDB) => new RxDBPluginSync(db);
@@ -0,0 +1,61 @@
1
+ import { type RemoteChange, type RuleGroup, type RxDBAdapterRemoteBase } from '@aiao/rxdb';
2
+ /**
3
+ * 一个仓库跨祖先分支拉取变更所需的全部参数。
4
+ */
5
+ export interface AncestorPullRequest {
6
+ /** 仓库命名空间 */
7
+ namespace: string;
8
+
9
+ /** 实体名 */
10
+ entity: string;
11
+
12
+ /** 水位线:只拉 id 大于它的变更 */
13
+ sinceId: number;
14
+
15
+ /** 本轮最多消费多少条 */
16
+ limit: number;
17
+
18
+ /** 行级过滤条件,`SyncType.Filter` 用;批量路径不支持,故为可选 */
19
+ filter?: RuleGroup;
20
+ }
21
+
22
+ /**
23
+ * 跨祖先分支拉取**单个仓库**的远端变更,合并成一条按 id 递增、长度不超过 `limit` 的序列。
24
+ *
25
+ * @param remoteAdapter - 远端适配器
26
+ * @param request - 仓库标识、水位线、批量大小与可选行级过滤
27
+ * @param branchIds - 当前分支及其全部祖先,见 `getAncestorBranchIds`
28
+ * @returns 合并、排序、截断后的变更序列
29
+ *
30
+ * @remarks
31
+ * **为什么必须逐祖先分支拉。** 分支是 patch 模型,不是快照模型:建分支时只写
32
+ * `parentId` + `fromChangeId`,一条变更都不复制。所以 feature 分支上可见的数据 =
33
+ * 父分支在分叉点之前的变更 + feature 自己的变更,而父分支那些记录**物理上仍然
34
+ * 归属 `branchId='main'`**。`pullChanges` 的 `branchId` 是精确匹配(见其接口契约),
35
+ * 只传当前分支就永远收不到别人推到 main 的更新 —— 且不会自愈:切回 main 时水位线
36
+ * 换成了 `${ns}:${entity}:main` 这条**另一个**记录,那段区间从此跳过。
37
+ *
38
+ * **为什么合并后必须全局排序再截断。** 每个分支各自取满 `limit` 条,直接拼接会让
39
+ * 某个分支的高 id 把共享水位线推过另一个分支尚未消费的低 id;那些变更此后都不再满足
40
+ * `id > lastPullRemoteChangeId`,被永久跳过。排序后只消费前 `limit` 条,其余留给下一轮 ——
41
+ * 调用方据 `length >= limit` 置 `hasMore` 继续循环。
42
+ *
43
+ * 单分支(在 main 上)时只发一次请求,与逐祖先的写法结果一致,不额外分支判断。
44
+ */
45
+ export async function pullAncestorBranchChanges(
46
+ remoteAdapter: RxDBAdapterRemoteBase,
47
+ request: AncestorPullRequest,
48
+ branchIds: readonly string[]
49
+ ): Promise<RemoteChange[]> {
50
+ const { namespace, entity, sinceId, limit, filter } = request;
51
+ const repositoryFilter = [`${namespace}:${entity}`];
52
+
53
+ const perBranch = await Promise.all(
54
+ branchIds.map(branchId => remoteAdapter.pullChanges(sinceId, limit, repositoryFilter, filter, branchId))
55
+ );
56
+
57
+ return perBranch
58
+ .flat()
59
+ .sort((left, right) => left.id - right.id)
60
+ .slice(0, limit);
61
+ }