diff --git a/ShadowsocksX-NG2/App/CatalogCommitCoordinator.swift b/ShadowsocksX-NG2/App/CatalogCommitCoordinator.swift index 5ba4c683..6300c5db 100644 --- a/ShadowsocksX-NG2/App/CatalogCommitCoordinator.swift +++ b/ShadowsocksX-NG2/App/CatalogCommitCoordinator.swift @@ -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() 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( diff --git a/ShadowsocksX-NG2/App/CatalogWorkflow/CatalogWorkflow.swift b/ShadowsocksX-NG2/App/CatalogWorkflow/CatalogWorkflow.swift index 8894e465..a5f57374 100644 --- a/ShadowsocksX-NG2/App/CatalogWorkflow/CatalogWorkflow.swift +++ b/ShadowsocksX-NG2/App/CatalogWorkflow/CatalogWorkflow.swift @@ -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 { + 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() }