refactor(catalog): syncStatus 收敛单一事实源,workflow 只留透传读面

协调器退掉 @Published 与 ObservableObject:syncStatus 存储与发布单点在
协调器(写后经 syncStatusChanges 通知,didChange 语义);CatalogWorkflow
的 runtimeSync 从第二份 @Published 拷贝改为 computed 透传 + 变化通道
透传。App 侧本就无视图消费 runtimeSync,目录观察者不再被每次提交徒劳
失效;测试的轮询读面不变。
This commit is contained in:
Qiu Yuzhou
2026-10-04 02:09:30 +08:00
parent f80018a260
commit 1f8ec2e2f2
2 changed files with 26 additions and 10 deletions
@@ -89,9 +89,13 @@ protocol CatalogRuntimeSyncing: AnyObject {
/// 或启动耗时阻塞。每次提交递增代次:旧代次自然结束,但不得覆盖最新代次的
/// 最终状态;没有自动重试,后续提交、显式激活或启动代理会发起新的同步尝试。
@MainActor
final class CatalogCommitCoordinator: ObservableObject {
/// 最新提交的运行时收敛阶段(结构化;呈现由上层决定)。
@Published private(set) var syncStatus: RuntimeSyncStatus = .idle
final class CatalogCommitCoordinator {
/// 最新提交的运行时收敛阶段(结构化;呈现由上层决定)。存储与发布单点
/// 在本类:写后经 `syncStatusChanges`(didChange 语义)通知;目录工作流
/// 只透传读面,不再挂第二份 `@Published`。
private(set) var syncStatus: RuntimeSyncStatus = .idle
/// 收敛阶段变化通道(didChange 语义,值即当前阶段)。
let syncStatusChanges = PassthroughSubject<RuntimeSyncStatus, Never>()
let fileStore: CatalogFileStore
private let runtime: CatalogRuntimeSyncing
@@ -159,18 +163,24 @@ final class CatalogCommitCoordinator: ObservableObject {
generation += 1
let generation = generation
guard runtime.hasActiveTarget else {
syncStatus = .idle
setSyncStatus(.idle)
return
}
syncStatus = .syncing(generation: generation)
setSyncStatus(.syncing(generation: generation))
Task { @MainActor [weak self] in
guard let self, self.generation == generation else { return }
let outcome = await self.runtime.converge(to: snapshot)
guard !Task.isCancelled, self.generation == generation else { return }
self.syncStatus = .finished(generation: generation, outcome: outcome)
self.setSyncStatus(.finished(generation: generation, outcome: outcome))
}
}
/// 写入与通知的唯一口径:先落值后发值(didChange 语义)。
private func setSyncStatus(_ status: RuntimeSyncStatus) {
syncStatus = status
syncStatusChanges.send(status)
}
/// 磁盘读取的容错口径与首次加载一致:损坏文档回退全新空文档,等待
/// 用户经由导入/设置路径修复。
private static func loadCommitted(
@@ -27,8 +27,16 @@ final class CatalogWorkflow: ObservableObject {
/// outcome;持久化目录只保存安全的 typed failure facts。
@Published private(set) var subscriptionRefreshFailures:
[NodeID: SubscriptionRefreshFailureResult] = [:]
/// 最近一次提交的运行时收敛阶段(与目录提交成功分离,story 38)。
@Published private(set) var runtimeSync: RuntimeSyncStatus
/// 最近一次提交的运行时收敛阶段(与目录提交成功分离,story 38)。存储与
/// 发布单点在提交协调器,此处只透传读面;需要实时更新的表面订阅
/// `runtimeSyncChanges`(`.onReceive` 消费),不经本类 objectWillChange——
/// App 侧目前无视图消费者,避免每次提交徒劳失效全部目录观察者。
var runtimeSync: RuntimeSyncStatus { dependencies.coordinator.syncStatus }
/// 收敛阶段变化通道(didChange 语义,值即当前阶段)。
var runtimeSyncChanges: AnyPublisher<RuntimeSyncStatus, Never> {
dependencies.coordinator.syncStatusChanges.eraseToAnyPublisher()
}
/// Legacy 快照发现与一次性完成标记。
@Published private(set) var legacyImportState = LegacyImportAvailability(
snapshotFound: false, completed: false)
@@ -53,8 +61,6 @@ final class CatalogWorkflow: ObservableObject {
subscriptions = Self.subscriptionSummaries(
coordinator.committedSubscriptions, catalog: coordinator.committedCatalog,
credentials: dependencies.credentials)
runtimeSync = coordinator.syncStatus
coordinator.$syncStatus.assign(to: &$runtimeSync)
refreshLegacyImportState()
}