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.
This commit is contained in:
Stefan Ceriu
2026-06-24 12:43:31 +03:00
parent 605ccc6be7
commit 05922f1bc7
@@ -1100,6 +1100,8 @@ class ClientProxy: ClientProxyProtocol {
MXLog.info("Starting sync") MXLog.info("Starting sync")
await syncService.start() await syncService.start()
updateHomeserverReachability()
// If we are using OAuth we want to cache the account management URL in volatile memory on the SDK side. // 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. // To avoid the cache being invalidated while the app is backgrounded, we cache at every sync start.
await cacheAccountURL() 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). // emit, and the SDK only re-enables queues when client.resume() runs (gated behind the flag).
sendQueueStatusSubject.send(sendQueueStatusSubject.value) sendQueueStatusSubject.send(sendQueueStatusSubject.value)
case .suspended: case .suspended:
updateHomeserverReachability()
MXLog.info("Stopping sync") MXLog.info("Stopping sync")
await syncService.stop() await syncService.stop()
MXLog.info("Sync stopped") 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 { private func createSyncServiceStateObserver(_ syncService: SyncService) -> TaskHandle {
syncService.state(listener: SDKListener.onMainActor { [weak self] state in syncService.state(listener: SDKListener.onMainActor { [weak self] state in
guard let self else { return } guard let self else { return }
MXLog.info("Received sync service update: \(state)") MXLog.info("Received sync service update: \(state)")
switch state { syncServiceState = state
case .running, .terminated, .idle:
homeserverReachabilitySubject.send(.reachable) if case .error = state {
case .offline:
homeserverReachabilitySubject.send(.unreachable)
case .error:
restartServices() 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? { private func createMediaPreviewConfigObserver() async -> TaskHandle? {
do { do {
return try await client.subscribeToMediaPreviewConfig(listener: SDKListener.onMainActor { [weak self] config in return try await client.subscribeToMediaPreviewConfig(listener: SDKListener.onMainActor { [weak self] config in