@aiao/rxdb-plugin-sync 0.0.0-stage → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/dist/SyncManager.d.ts +412 -0
- package/dist/SyncManager.d.ts.map +1 -0
- package/dist/branch-change-id.d.ts +31 -0
- package/dist/branch-change-id.d.ts.map +1 -0
- package/dist/branch-utils.d.ts +8 -0
- package/dist/branch-utils.d.ts.map +1 -0
- package/dist/bulk-sync.d.ts +117 -0
- package/dist/bulk-sync.d.ts.map +1 -0
- package/dist/cascade-blocking.d.ts +38 -0
- package/dist/cascade-blocking.d.ts.map +1 -0
- package/dist/check-repository-updates.d.ts +47 -0
- package/dist/check-repository-updates.d.ts.map +1 -0
- package/dist/cleanup-expired.d.ts +56 -0
- package/dist/cleanup-expired.d.ts.map +1 -0
- package/dist/dependency-graph.d.ts +128 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/get-all-repository-sync-status.d.ts +58 -0
- package/dist/get-all-repository-sync-status.d.ts.map +1 -0
- package/dist/get-repository-sync-status.d.ts +68 -0
- package/dist/get-repository-sync-status.d.ts.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3273 -0
- package/dist/plugin.d.ts +70 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/pull-ancestor-changes.d.ts +41 -0
- package/dist/pull-ancestor-changes.d.ts.map +1 -0
- package/dist/pull-batch.d.ts +16 -0
- package/dist/pull-batch.d.ts.map +1 -0
- package/dist/pull-conflict-utils.d.ts +43 -0
- package/dist/pull-conflict-utils.d.ts.map +1 -0
- package/dist/pull-repository.d.ts +36 -0
- package/dist/pull-repository.d.ts.map +1 -0
- package/dist/pull.d.ts +16 -0
- package/dist/pull.d.ts.map +1 -0
- package/dist/push-branch.d.ts +22 -0
- package/dist/push-branch.d.ts.map +1 -0
- package/dist/push-repository.d.ts +63 -0
- package/dist/push-repository.d.ts.map +1 -0
- package/dist/push.d.ts +14 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/query-cache-outbox.d.ts +120 -0
- package/dist/query-cache-outbox.d.ts.map +1 -0
- package/dist/sync-branches.d.ts +35 -0
- package/dist/sync-branches.d.ts.map +1 -0
- package/dist/sync-listeners.d.ts +20 -0
- package/dist/sync-listeners.d.ts.map +1 -0
- package/dist/sync-manager.utils.d.ts +36 -0
- package/dist/sync-manager.utils.d.ts.map +1 -0
- package/dist/sync-repository.d.ts +63 -0
- package/dist/sync-repository.d.ts.map +1 -0
- package/dist/topological-sort.d.ts +126 -0
- package/dist/topological-sort.d.ts.map +1 -0
- package/dist/version.utils.d.ts +3 -0
- package/dist/version.utils.d.ts.map +1 -0
- package/package.json +58 -3
- package/src/SyncManager.ts +662 -0
- package/src/branch-change-id.ts +70 -0
- package/src/branch-utils.ts +37 -0
- package/src/bulk-sync.ts +338 -0
- package/src/cascade-blocking.ts +61 -0
- package/src/check-repository-updates.ts +143 -0
- package/src/cleanup-expired.ts +271 -0
- package/src/dependency-graph.ts +324 -0
- package/src/get-all-repository-sync-status.ts +115 -0
- package/src/get-repository-sync-status.ts +237 -0
- package/src/index.ts +53 -0
- package/src/plugin.ts +124 -0
- package/src/pull-ancestor-changes.ts +61 -0
- package/src/pull-batch.ts +410 -0
- package/src/pull-conflict-utils.ts +280 -0
- package/src/pull-repository.ts +701 -0
- package/src/pull.ts +204 -0
- package/src/push-branch.ts +61 -0
- package/src/push-repository.ts +1002 -0
- package/src/push.ts +104 -0
- package/src/query-cache-outbox.ts +866 -0
- package/src/sync-branches.ts +156 -0
- package/src/sync-listeners.ts +289 -0
- package/src/sync-manager.utils.ts +48 -0
- package/src/sync-repository.ts +302 -0
- package/src/topological-sort.ts +246 -0
- package/src/version.utils.ts +20 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 获取仓库同步状态
|
|
3
|
+
*
|
|
4
|
+
* 提供查询单个仓库同步状态的功能,包含 syncType、pushableCount、pullableCount 以及最后同步时间戳等信息。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getEntityMetadata,
|
|
9
|
+
getSyncCapability,
|
|
10
|
+
getSyncType,
|
|
11
|
+
isRepositorySyncEnabled,
|
|
12
|
+
needsPull,
|
|
13
|
+
needsPush,
|
|
14
|
+
type RepositoryIdentifier,
|
|
15
|
+
type RxDB,
|
|
16
|
+
RxDBChange,
|
|
17
|
+
type RxDBChangeRuleGroup,
|
|
18
|
+
RxDBSync,
|
|
19
|
+
type RxDBSyncRuleGroup
|
|
20
|
+
} from '@aiao/rxdb';
|
|
21
|
+
import { checkRepositoryUpdates } from './check-repository-updates.js';
|
|
22
|
+
import type { SyncManager } from './SyncManager.js';
|
|
23
|
+
// 唯一定义收敛到 `VersionManager.interface.ts`。
|
|
24
|
+
export type { RepositoryIdentifier };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 同步类型的取值
|
|
28
|
+
*/
|
|
29
|
+
export type SyncTypeValue = 'full' | 'filter' | 'querycache' | 'remote' | 'local' | 'none';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 仓库同步状态
|
|
33
|
+
*/
|
|
34
|
+
export interface RepositorySyncStatus {
|
|
35
|
+
/**
|
|
36
|
+
* 仓库标识
|
|
37
|
+
*/
|
|
38
|
+
repository: RepositoryIdentifier;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 分支 ID
|
|
42
|
+
*/
|
|
43
|
+
branchId: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 同步类型
|
|
47
|
+
*/
|
|
48
|
+
syncType: SyncTypeValue;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 是否启用同步
|
|
52
|
+
*/
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 最后一次推送的 changeId
|
|
57
|
+
*/
|
|
58
|
+
lastPushedChangeId: number | null;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 最后一次推送时间戳
|
|
62
|
+
*/
|
|
63
|
+
lastPushedAt: Date | null;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 最后一次拉取时间戳
|
|
67
|
+
*/
|
|
68
|
+
lastPulledAt: Date | null;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 最后一次拉取到的远端 changeId
|
|
72
|
+
*/
|
|
73
|
+
lastPullRemoteChangeId: number | null;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 待推送的变更数量
|
|
77
|
+
*/
|
|
78
|
+
pushableCount: number;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 可供拉取的变更数量
|
|
82
|
+
*/
|
|
83
|
+
pullableCount: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 计算仓库的待推送变更数量
|
|
88
|
+
*
|
|
89
|
+
* 逻辑:
|
|
90
|
+
* - 统计 RxDBChange 记录,满足以下条件:
|
|
91
|
+
* 1. 属于该实体(entity 字段匹配)
|
|
92
|
+
* 2. 属于当前分支
|
|
93
|
+
* 3. 不是远端产生的(remoteId 为 null)
|
|
94
|
+
* 4. 未被撤销(revertChangeId 为 null)
|
|
95
|
+
* 5. 比 lastPushedChangeId 新(如果存在)
|
|
96
|
+
*
|
|
97
|
+
* @param sm - 调用方持有的同步管理器实例
|
|
98
|
+
* @param namespace - 实体命名空间
|
|
99
|
+
* @param entity - 实体名称
|
|
100
|
+
* @param branchId - 当前分支 ID
|
|
101
|
+
* @param lastPushedChangeId - 最后一次推送的 changeId(若未推送则为 null)
|
|
102
|
+
* @returns 待推送的变更数量
|
|
103
|
+
*/
|
|
104
|
+
async function calculatePushableCount(
|
|
105
|
+
sm: SyncManager,
|
|
106
|
+
namespace: string,
|
|
107
|
+
entity: string,
|
|
108
|
+
branchId: string,
|
|
109
|
+
lastPushedChangeId: number | null
|
|
110
|
+
): Promise<number> {
|
|
111
|
+
const { adapter: localAdapter } = await sm.getLocalRepositories();
|
|
112
|
+
const changeRepo = localAdapter.getRepository(RxDBChange);
|
|
113
|
+
|
|
114
|
+
// 构建查询规则
|
|
115
|
+
const rules: RxDBChangeRuleGroup['rules'] = [
|
|
116
|
+
{ field: 'namespace', operator: '=', value: namespace },
|
|
117
|
+
{ field: 'entity', operator: '=', value: entity },
|
|
118
|
+
{ field: 'branchId', operator: '=', value: branchId },
|
|
119
|
+
{ field: 'remoteId', operator: '=', value: null },
|
|
120
|
+
{ field: 'revertChangeId', operator: '=', value: null }
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
// 如果存在 lastPushedChangeId,则添加过滤条件
|
|
124
|
+
if (lastPushedChangeId !== null) {
|
|
125
|
+
rules.push({ field: 'id', operator: '>', value: lastPushedChangeId });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result = await changeRepo.find({
|
|
129
|
+
where: {
|
|
130
|
+
combinator: 'and',
|
|
131
|
+
rules
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return result.length;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 计算仓库可拉取变更的数量
|
|
140
|
+
*
|
|
141
|
+
* 使用 checkRepositoryUpdates() 查询远端变更计数,避免下载实际数据。
|
|
142
|
+
*
|
|
143
|
+
* @param rxdb - RxDB 实例
|
|
144
|
+
* @param namespace - 实体命名空间
|
|
145
|
+
* @param entity - 实体名称
|
|
146
|
+
* @returns 可拉取的变更数量
|
|
147
|
+
*/
|
|
148
|
+
async function calculatePullableCount(rxdb: RxDB, namespace: string, entity: string): Promise<number> {
|
|
149
|
+
const result = await checkRepositoryUpdates(rxdb, namespace, entity);
|
|
150
|
+
return result.pendingCount;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 获取单个仓库的同步状态
|
|
155
|
+
*
|
|
156
|
+
* @param rxdb - RxDB 实例
|
|
157
|
+
* @param namespace - 实体命名空间
|
|
158
|
+
* @param entity - 实体名称
|
|
159
|
+
* @returns 仓库同步状态
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const status = await getRepositorySyncStatus(rxdb, 'public', 'Todo');
|
|
164
|
+
* console.log(`Sync type: ${status.syncType}`);
|
|
165
|
+
* console.log(`Pushable: ${status.pushableCount}, Pullable: ${status.pullableCount}`);
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export async function getRepositorySyncStatus(
|
|
169
|
+
rxdb: RxDB,
|
|
170
|
+
namespace: string,
|
|
171
|
+
entity: string
|
|
172
|
+
): Promise<RepositorySyncStatus> {
|
|
173
|
+
// 1. 查找实体类并获取元数据
|
|
174
|
+
const EntityClass = rxdb.config.entities.find(e => {
|
|
175
|
+
const meta = getEntityMetadata(e);
|
|
176
|
+
return meta.namespace === namespace && meta.name === entity;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!EntityClass) {
|
|
180
|
+
throw new Error(`Entity not found: ${namespace}.${entity}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const metadata = getEntityMetadata(EntityClass);
|
|
184
|
+
const syncType = getSyncType(metadata, rxdb.config.sync);
|
|
185
|
+
const syncCapability = getSyncCapability(syncType);
|
|
186
|
+
|
|
187
|
+
// 2. 获取当前分支
|
|
188
|
+
// 槽位只解析这一次:`rxdb.syncManager` 由插件作用域挂着,断连即被删除,
|
|
189
|
+
// 一次查询里反复重解析等于给自己留三个 `undefined` 的窗口。
|
|
190
|
+
const sm = rxdb.syncManager;
|
|
191
|
+
const branch = await sm.getCurrentBranch();
|
|
192
|
+
const branchId = branch.id;
|
|
193
|
+
|
|
194
|
+
// 3. 查询 RxDBSync 记录
|
|
195
|
+
const { adapter: localAdapter } = await sm.getLocalRepositories();
|
|
196
|
+
const repoSyncRepo = localAdapter.getRepository(RxDBSync);
|
|
197
|
+
const repoSyncId = `${namespace}:${entity}:${branchId}`;
|
|
198
|
+
|
|
199
|
+
const repoSyncWhere: RxDBSyncRuleGroup = {
|
|
200
|
+
combinator: 'and',
|
|
201
|
+
rules: [{ field: 'id', operator: '=', value: repoSyncId }]
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const repoSyncResults = await repoSyncRepo.find({ where: repoSyncWhere, limit: 1 });
|
|
205
|
+
|
|
206
|
+
const repoSync = repoSyncResults.length > 0 ? repoSyncResults[0] : null;
|
|
207
|
+
|
|
208
|
+
// 4. 计算 pushableCount 和 pullableCount
|
|
209
|
+
let pushableCount = 0;
|
|
210
|
+
let pullableCount = 0;
|
|
211
|
+
|
|
212
|
+
// 如果该实体需要推送,则计算 pushableCount(与 syncType 一致地传入全局 sync 回退)
|
|
213
|
+
if (needsPush(metadata, rxdb.config.sync)) {
|
|
214
|
+
pushableCount = await calculatePushableCount(sm, namespace, entity, branchId, repoSync?.lastPushedChangeId ?? null);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 如果该实体需要拉取,则计算 pullableCount(与 syncType 一致地传入全局 sync 回退)
|
|
218
|
+
if (needsPull(metadata, rxdb.config.sync)) {
|
|
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,53 @@
|
|
|
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
|
+
export * from './cleanup-expired.js';
|
|
35
|
+
// `SyncManager.getRepositoryDependencyGraph()` 的返回值。图的构建函数是内部实现。
|
|
36
|
+
export type { DependencyGraph } from './dependency-graph.js';
|
|
37
|
+
// `SyncManager.getAllRepositorySyncStatus()` 的可选过滤器。
|
|
38
|
+
export type { GetAllRepositorySyncStatusFilter } from './get-all-repository-sync-status.js';
|
|
39
|
+
// `getRepositorySyncStatus()` 的返回值。
|
|
40
|
+
export type { RepositorySyncStatus } from './get-repository-sync-status.js';
|
|
41
|
+
// QueryCache 出站队列的三个入口(US-025 阶段 D 从核心搬来)。前两个供应用与 DevTools
|
|
42
|
+
// 手动驱动一轮重放 / 读队列长度;`pendingQueryCacheWriteIds` 还兼作核心
|
|
43
|
+
// `QueryCacheOutboxProvider` 的实现,由本包的插件在 `install()` 里登记进宿主。
|
|
44
|
+
export {
|
|
45
|
+
countQueryCacheOutbox,
|
|
46
|
+
flushQueryCacheOutbox,
|
|
47
|
+
pendingQueryCacheWriteIds,
|
|
48
|
+
type QueryCacheOutboxFailure,
|
|
49
|
+
type QueryCacheOutboxResult
|
|
50
|
+
} from './query-cache-outbox.js';
|
|
51
|
+
export * from './sync-branches.js';
|
|
52
|
+
// 实例由插件装配,用户不自己 new,因此只转类型不转类。
|
|
53
|
+
export type { SyncManager } from './SyncManager.js';
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
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 { pendingQueryCacheWriteIds } from './query-cache-outbox.js';
|
|
11
|
+
import { SyncManager } from './SyncManager.js';
|
|
12
|
+
|
|
13
|
+
/** 本插件当前不接受任何选项 */
|
|
14
|
+
export type RxDBPluginSyncOptions = object;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 推拉同步 / 冲突 / 仓库依赖序插件。
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* **声明 `inject: ['plugin:history']`**,且只声明这一条:
|
|
21
|
+
*
|
|
22
|
+
* - 要历史插件,是因为一次往返结束时 undo 边界要作废、待拉计数要结算,而那些状态的主人
|
|
23
|
+
* 在历史侧。{@link SyncManager} 拿的是 `rxdb.versionManager.syncBridge` 这张窄接口,
|
|
24
|
+
* 没有它连构造都无从谈起。依赖缺失时宿主不装本插件、只告警一次,`rxdb.syncManager`
|
|
25
|
+
* 这个槽位于是不存在——没有历史就没有同步,这正是实情。
|
|
26
|
+
* - **不要 `adapter:remote`**:同步入口本身是按调用发起的,装配阶段一条适配器读写都不发;
|
|
27
|
+
* 而 {@link SyncManager.init} 挂的 `connected$` 自动回推订阅必须在第一次连接就位之前
|
|
28
|
+
* 就已经订阅上,声明适配器依赖会把安装推到引导链之后,第一次连接的那一跳回推就丢了。
|
|
29
|
+
*
|
|
30
|
+
* 五处宿主改动都登记在 `scope` 上,断开连接时由宿主逆序释放:
|
|
31
|
+
*
|
|
32
|
+
* 1. `rxdb.syncManager` 这个实例槽位 —— 释放时连同 `destroy()` 一起撤掉,不给下一个纪元
|
|
33
|
+
* 留一个指向已拆事件总线的管理器;
|
|
34
|
+
* 2. {@link RxDB.syncState} 上的「重算待拉数」跳板 —— 远端适配器在实时订阅恢复后按
|
|
35
|
+
* `syncState.requestPullableRefresh()` 发信号,它不认识 {@link SyncManager},接住这一跳
|
|
36
|
+
* 是本插件的活(US-025 阶段 D 之前这条绑定在历史插件上,随 `refreshPullableCount` 一起
|
|
37
|
+
* 搬过来)。没装插件时那个请求发进空里,正是「待拉数无人维护」的实情。
|
|
38
|
+
* 3. {@link RxDB.queryCacheOutbox} 这个出站队列槽 —— QueryCache 实体的读引擎要靠它把
|
|
39
|
+
* 「远端没返回」和「本地离线写过」区分开。这一条**没有**缺席形态:槽空着时核心在
|
|
40
|
+
* `connect()` 就抛 `RxDBMissingPluginError`,因为「当空集」等于把每条离线写都当孤儿删掉。
|
|
41
|
+
* 4. 宿主的 `online` / `offline` 监听 —— `ReachabilityMonitor` 只在有人 `watch()` 时才往
|
|
42
|
+
* `globalThis` 上挂,而这两个事件唯一的消费者是本包的同步监听器(`wakeup$` 驱动回推
|
|
43
|
+
* 重试)。不同步的库不该因为 `new RxDB()` 就永久多一对活过实例的监听器(US-025 D2)。
|
|
44
|
+
*/
|
|
45
|
+
export class RxDBPluginSync extends RxDBPluginBase implements IRxDBPlugin {
|
|
46
|
+
readonly lifecycle = 'scoped' as const;
|
|
47
|
+
readonly inject = ['plugin:history'] as const;
|
|
48
|
+
name: Uncapitalize<string> = 'sync';
|
|
49
|
+
|
|
50
|
+
install(scope: LifecycleScope) {
|
|
51
|
+
// `inject` 已经保证历史插件先装完,`versionManager` 槽位此刻一定在。
|
|
52
|
+
// 借的是 `syncBridge` 而不是管理器本身:面越窄,两个包越不会在演进中互相拽住。
|
|
53
|
+
const syncManager = new SyncManager(this.rxdb, this.rxdb.versionManager.syncBridge);
|
|
54
|
+
scope.acquire(() => () => syncManager.destroy(), 'sync:syncManager');
|
|
55
|
+
|
|
56
|
+
scope.acquire(() => {
|
|
57
|
+
Object.defineProperty(this.rxdb, 'syncManager', {
|
|
58
|
+
value: syncManager,
|
|
59
|
+
configurable: true,
|
|
60
|
+
enumerable: true
|
|
61
|
+
});
|
|
62
|
+
return () => void Reflect.deleteProperty(this.rxdb, 'syncManager');
|
|
63
|
+
}, 'sync:slot');
|
|
64
|
+
|
|
65
|
+
// 只登记 `pendingWriteIds` 这一个问题。`flushQueryCacheOutbox` / `countQueryCacheOutbox`
|
|
66
|
+
// 的调用者(同步监听器、DevTools 面板)都在本包里,核心不必认识它们。
|
|
67
|
+
// 传 scope:断开连接时这条注册跟着一起撤销,不留一个指向已拆纪元的队列。
|
|
68
|
+
this.rxdb.queryCacheOutbox(
|
|
69
|
+
{ pendingWriteIds: (namespace, entity) => pendingQueryCacheWriteIds(this.rxdb, namespace, entity) },
|
|
70
|
+
scope
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// 可达性监听:`watch()` 自己返回撤销函数,正好是 `acquire` 要的 setup 形状。
|
|
74
|
+
// 引用计数在监视器一侧,所以同一个宿主上多装几个消费者也不会互相摘掉对方的监听。
|
|
75
|
+
scope.acquire(() => this.rxdb.reachability.watch(), 'sync:reachability');
|
|
76
|
+
|
|
77
|
+
syncManager.init();
|
|
78
|
+
|
|
79
|
+
// 跳板约定「回调不得抛出」:这里即发即忘,没有调用方接得住错误,也没有位置重试。
|
|
80
|
+
// 重算失败只说明这一次的读数没刷新,下一次实时恢复还会再请求一次,不该炸掉
|
|
81
|
+
// 适配器的订阅恢复路径。
|
|
82
|
+
scope.acquire(
|
|
83
|
+
() =>
|
|
84
|
+
this.rxdb.syncState.bindPullableRefresh(() => {
|
|
85
|
+
void syncManager.refreshPullableCount().catch((error: unknown) => {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
console.warn(`[RxDB Sync] 重算待拉数失败:${message}。`);
|
|
88
|
+
});
|
|
89
|
+
}),
|
|
90
|
+
'sync:pullableRefresh'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
declare module '@aiao/rxdb' {
|
|
96
|
+
interface RxDB {
|
|
97
|
+
/**
|
|
98
|
+
* 推拉同步子系统。
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* 由 `@aiao/rxdb-plugin-sync` 在连接纪元内装配。**没装插件(或没装它依赖的
|
|
102
|
+
* `@aiao/rxdb-plugin-history`)时这个槽位不存在**:读它拿到 `undefined`,
|
|
103
|
+
* 而不是一个什么都不做的空壳——核心不做 fallback 兜底。
|
|
104
|
+
*/
|
|
105
|
+
syncManager: SyncManager;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 同步插件工厂。
|
|
111
|
+
*
|
|
112
|
+
* @param db - 宿主实例
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { rxDBPluginHistory } from '@aiao/rxdb-plugin-history';
|
|
117
|
+
* import { rxDBPluginSync } from '@aiao/rxdb-plugin-sync';
|
|
118
|
+
*
|
|
119
|
+
* // 顺序随意:宿主按 `inject` 拓扑排序,历史插件一定先装完。
|
|
120
|
+
* rxdb.use(rxDBPluginSync);
|
|
121
|
+
* rxdb.use(rxDBPluginHistory);
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
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
|
+
}
|