diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index dc54e5bbd..1fd958aed 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -1285,8 +1285,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg return } - // State-only (no HTTP): configure the background-refresh sync to carry set_presence=offline so it never - // reuses the client's foreground sync presence and marks the user online or idle. + // Configure the background-refresh sync to carry set_presence=offline so it doesn't mark the + // user online or idle. Note: If already online/idle then setting offline shouldn't override that. _ = await userSession.clientProxy.configurePresence(.offline, sendImmediately: false) await resumeClientServices() diff --git a/ElementX/Sources/Services/Presence/PresenceService.swift b/ElementX/Sources/Services/Presence/PresenceService.swift index 178367130..3f76c24c8 100644 --- a/ElementX/Sources/Services/Presence/PresenceService.swift +++ b/ElementX/Sources/Services/Presence/PresenceService.swift @@ -100,10 +100,9 @@ final class PresenceService { return } - let clientProxy = clientProxy sendTask = Task { [weak self, clientProxy] in while !Task.isCancelled { - guard let presence = await self?.nextPendingPresence() else { + guard let presence = self?.nextPendingPresence() else { break } @@ -113,10 +112,10 @@ final class PresenceService { break } - await self?.finishSending(presence) + self?.finishSending(presence) } - await self?.finishSendTask() + self?.finishSendTask() } } diff --git a/UnitTests/Sources/PresenceServiceTests.swift b/UnitTests/Sources/PresenceServiceTests.swift index 5bd3f2f6e..292e2888a 100644 --- a/UnitTests/Sources/PresenceServiceTests.swift +++ b/UnitTests/Sources/PresenceServiceTests.swift @@ -175,12 +175,12 @@ struct PresenceServiceTests { func newerTransitionWinsWhenAnEarlierSendIsInFlight() async throws { appSettings.sharePresence = true - let onlineStarted = PassthroughSubject() - let gate = AsyncStream.makeStream() - clientProxy.configurePresenceSendImmediatelyClosure = { [sendsSubject, onlineStarted] presence, sendImmediately in + let (onlineStartedStream, onlineStartedContinuation) = AsyncStream.makeStream() + let (resumeOnlineSendStream, resumeOnlineSendContinuation) = AsyncStream.makeStream() + clientProxy.configurePresenceSendImmediatelyClosure = { [sendsSubject] presence, sendImmediately in if presence == .online { - onlineStarted.send(()) - var iterator = gate.stream.makeAsyncIterator() + onlineStartedContinuation.yield() + var iterator = resumeOnlineSendStream.makeAsyncIterator() _ = await iterator.next() } @@ -188,15 +188,15 @@ struct PresenceServiceTests { return .success(()) } - let onlineStartedDeferred = deferFulfillment(onlineStarted) { _ in true } + let onlineStartedDeferred = deferFulfillment(onlineStartedStream) { _ in true } let service = makeService(initialApplicationState: .active) try await onlineStartedDeferred.fulfill() let unavailableDeferred = deferFulfillment(sendsSubject) { $0.presence == .unavailable } notificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil) - gate.continuation.yield() - gate.continuation.finish() + resumeOnlineSendContinuation.yield() + resumeOnlineSendContinuation.finish() try await unavailableDeferred.fulfill() let applied = clientProxy.configurePresenceSendImmediatelyReceivedInvocations.map(\.presence) @@ -208,47 +208,29 @@ struct PresenceServiceTests { func deinitCancelsPendingSend() async throws { appSettings.sharePresence = true - let onlineStarted = AsyncStream.makeStream(of: Void.self) - let onlineCancelled = AsyncStream.makeStream(of: Void.self) - let onlineCompleted = AsyncStream.makeStream(of: Void.self) - let gate = AsyncStream.makeStream(of: Void.self) - let gateContinuation = gate.continuation - let onlineCancelledContinuation = onlineCancelled.continuation - let onlineCompletedContinuation = onlineCompleted.continuation - + let (onlineStartedStream, onlineStartedContinuation) = AsyncStream.makeStream(of: Void.self) + let (onlineFinishedStream, onlineFinishedContinuation) = AsyncStream.makeStream(of: Void.self) clientProxy.configurePresenceSendImmediatelyClosure = { presence, _ in - guard presence == .online else { - return .success(()) + if presence == .online { + onlineStartedContinuation.yield() + try? await Task.sleep(for: .seconds(3600)) // Interrupted by the service's deinit cancelling the send task. + onlineFinishedContinuation.yield() } - onlineStarted.continuation.yield() - let result: Result = await withTaskCancellationHandler { - var iterator = gate.stream.makeAsyncIterator() - _ = await iterator.next() - return .success(()) - } onCancel: { - gateContinuation.finish() - onlineCancelledContinuation.yield() - onlineCancelledContinuation.finish() - } - onlineCompletedContinuation.yield() - onlineCompletedContinuation.finish() - - return result + return .success(()) } - let onlineStartedDeferred = deferFulfillment(onlineStarted.stream) { _ in true } + let onlineStartedDeferred = deferFulfillment(onlineStartedStream) { _ in true } var service: PresenceService? = makeService(initialApplicationState: .active) #expect(service != nil) try await onlineStartedDeferred.fulfill() + // Queue an unavailable send behind the in-flight online one. notificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil) - let onlineCancelledDeferred = deferFulfillment(onlineCancelled.stream) { _ in true } - let onlineCompletedDeferred = deferFulfillment(onlineCompleted.stream) { _ in true } + let onlineFinishedDeferred = deferFulfillment(onlineFinishedStream) { _ in true } service = nil - try await onlineCancelledDeferred.fulfill() - try await onlineCompletedDeferred.fulfill() + try await onlineFinishedDeferred.fulfill() let applied = clientProxy.configurePresenceSendImmediatelyReceivedInvocations.map(\.presence) #expect(applied == [.online], "Expected deinit to cancel the queued unavailable send, got \(applied)")