From 05922f1bc7e81edb024070d372b15e3223f4dd03 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 24 Jun 2026 12:43:31 +0300 Subject: [PATCH] Fix media loading failures when retrying with a closed store `homeserverReachabilityPublisher` was driven directly by the sync service state so stopping sync on pause still reported .reachable. MediaProvider.loadImageRetryingOnReconnection would then retry against a closed store, fail, and bail out permanently because it treated the reachable state as a non-recoverable error. Make reachability a function of both the desired service state and the latest sync state, computed in one place: store the sync state in the listener and recompute reachability from it, reporting unreachable whenever the client isn't meant to be running. Recompute on suspend and on resume. --- .../Sources/Services/Client/ClientProxy.swift | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 11a52dc37..877a7459b 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -1100,6 +1100,8 @@ class ClientProxy: ClientProxyProtocol { MXLog.info("Starting sync") await syncService.start() + updateHomeserverReachability() + // If we are using OAuth we want to cache the account management URL in volatile memory on the SDK side. // To avoid the cache being invalidated while the app is backgrounded, we cache at every sync start. await cacheAccountURL() @@ -1108,6 +1110,8 @@ class ClientProxy: ClientProxyProtocol { // emit, and the SDK only re-enables queues when client.resume() runs (gated behind the flag). sendQueueStatusSubject.send(sendQueueStatusSubject.value) case .suspended: + updateHomeserverReachability() + MXLog.info("Stopping sync") await syncService.stop() MXLog.info("Sync stopped") @@ -1175,23 +1179,36 @@ class ClientProxy: ClientProxyProtocol { } } + /// The latest state reported by the sync service. Stored so reachability can be recomputed after a + /// resume, where the store reopens but the service may not emit a fresh state. + private var syncServiceState: SyncServiceState? + private func createSyncServiceStateObserver(_ syncService: SyncService) -> TaskHandle { syncService.state(listener: SDKListener.onMainActor { [weak self] state in guard let self else { return } MXLog.info("Received sync service update: \(state)") - switch state { - case .running, .terminated, .idle: - homeserverReachabilitySubject.send(.reachable) - case .offline: - homeserverReachabilitySubject.send(.unreachable) - case .error: + syncServiceState = state + + if case .error = state { restartServices() + } else { + updateHomeserverReachability() } }) } + private func updateHomeserverReachability() { + let reachability: NetworkMonitorReachability = if desiredServiceState == .running, syncServiceState != .offline { + .reachable + } else { + .unreachable + } + + homeserverReachabilitySubject.send(reachability) + } + private func createMediaPreviewConfigObserver() async -> TaskHandle? { do { return try await client.subscribeToMediaPreviewConfig(listener: SDKListener.onMainActor { [weak self] config in