diff --git a/Sources/AblyChat/RoomLifecycleManager.swift b/Sources/AblyChat/RoomLifecycleManager.swift index 4d3ae5ce..42041609 100644 --- a/Sources/AblyChat/RoomLifecycleManager.swift +++ b/Sources/AblyChat/RoomLifecycleManager.swift @@ -58,9 +58,9 @@ internal actor RoomLifecycleManager { private struct ContributorAnnotations { private var storage: [Contributor.ID: ContributorAnnotation] - init(contributors: [Contributor]) { + init(contributors: [Contributor], pendingDiscontinuityEvents: [Contributor.ID: [ARTErrorInfo]]) { storage = contributors.reduce(into: [:]) { result, contributor in - result[contributor.id] = .init() + result[contributor.id] = .init(pendingDiscontinuityEvents: pendingDiscontinuityEvents[contributor.id] ?? []) } } @@ -100,6 +100,7 @@ internal actor RoomLifecycleManager { await self.init( current: nil, hasOperationInProgress: nil, + pendingDiscontinuityEvents: [:], contributors: contributors, logger: logger, clock: clock @@ -110,6 +111,7 @@ internal actor RoomLifecycleManager { internal init( testsOnly_current current: RoomLifecycle? = nil, testsOnly_hasOperationInProgress hasOperationInProgress: Bool? = nil, + testsOnly_pendingDiscontinuityEvents pendingDiscontinuityEvents: [Contributor.ID: [ARTErrorInfo]]? = nil, contributors: [Contributor], logger: InternalLogger, clock: SimpleClock @@ -117,6 +119,7 @@ internal actor RoomLifecycleManager { await self.init( current: current, hasOperationInProgress: hasOperationInProgress, + pendingDiscontinuityEvents: pendingDiscontinuityEvents, contributors: contributors, logger: logger, clock: clock @@ -127,6 +130,7 @@ internal actor RoomLifecycleManager { private init( current: RoomLifecycle?, hasOperationInProgress: Bool?, + pendingDiscontinuityEvents: [Contributor.ID: [ARTErrorInfo]]?, contributors: [Contributor], logger: InternalLogger, clock: SimpleClock @@ -134,7 +138,7 @@ internal actor RoomLifecycleManager { self.current = current ?? .initialized self.hasOperationInProgress = hasOperationInProgress ?? false self.contributors = contributors - contributorAnnotations = .init(contributors: contributors) + contributorAnnotations = .init(contributors: contributors, pendingDiscontinuityEvents: pendingDiscontinuityEvents ?? [:]) self.logger = logger self.clock = clock @@ -362,6 +366,23 @@ internal actor RoomLifecycleManager { // CHA-RL1g1 changeStatus(to: .attached) + + // CHA-RL1g2 + await emitPendingDiscontinuityEvents() + } + + /// Implements CHA-RL1g2’s emitting of pending discontinuity events. + private func emitPendingDiscontinuityEvents() async { + // Emit all pending discontinuity events + logger.log(message: "Emitting pending discontinuity events", level: .info) + for contributor in contributors { + for pendingDiscontinuityEvent in contributorAnnotations[contributor].pendingDiscontinuityEvents { + logger.log(message: "Emitting pending discontinuity event \(pendingDiscontinuityEvent) to contributor \(contributor)", level: .info) + await contributor.emitDiscontinuity(pendingDiscontinuityEvent) + } + } + + contributorAnnotations.clearPendingDiscontinuityEvents() } /// Implements CHA-RL1h5’s "detach all channels that are not in the FAILED state". diff --git a/Tests/AblyChatTests/RoomLifecycleManagerTests.swift b/Tests/AblyChatTests/RoomLifecycleManagerTests.swift index c7f93c2b..14804d27 100644 --- a/Tests/AblyChatTests/RoomLifecycleManagerTests.swift +++ b/Tests/AblyChatTests/RoomLifecycleManagerTests.swift @@ -30,12 +30,14 @@ struct RoomLifecycleManagerTests { private func createManager( forTestingWhatHappensWhenCurrentlyIn current: RoomLifecycle? = nil, forTestingWhatHappensWhenHasOperationInProgress hasOperationInProgress: Bool? = nil, + forTestingWhatHappensWhenHasPendingDiscontinuityEvents pendingDiscontinuityEvents: [MockRoomLifecycleContributor.ID: [ARTErrorInfo]]? = nil, contributors: [MockRoomLifecycleContributor] = [], clock: SimpleClock = MockSimpleClock() ) async -> RoomLifecycleManager { await .init( testsOnly_current: current, testsOnly_hasOperationInProgress: hasOperationInProgress, + testsOnly_pendingDiscontinuityEvents: pendingDiscontinuityEvents, contributors: contributors, logger: TestLogger(), clock: clock @@ -175,6 +177,40 @@ struct RoomLifecycleManagerTests { try #require(await manager.current == .attached) } + // @spec CHA-RL1g2 + @Test + func attach_uponSuccess_emitsPendingDiscontinuityEvents() async throws { + // Given: A RoomLifecycleManager, all of whose contributors’ calls to `attach` succeed + let contributors = (1 ... 3).map { _ in createContributor(attachBehavior: .complete(.success)) } + let pendingDiscontinuityEvents: [MockRoomLifecycleContributor.ID: [ARTErrorInfo]] = [ + contributors[1].id: [.init(domain: "SomeDomain", code: 123) /* arbitrary */ ], + contributors[2].id: [.init(domain: "SomeDomain", code: 456) /* arbitrary */ ], + ] + let manager = await createManager( + forTestingWhatHappensWhenHasPendingDiscontinuityEvents: pendingDiscontinuityEvents, + contributors: contributors + ) + + // When: `performAttachOperation()` is called on the lifecycle manager + try await manager.performAttachOperation() + + // Then: It: + // - emits all pending discontinuities to its contributors + // - clears all pending discontinuity events (TODO: I assume this is the intended behaviour, but confirm; have asked in https://github.com/ably/specification/pull/200/files#r1781917231) + for contributor in contributors { + let expectedPendingDiscontinuityEvents = pendingDiscontinuityEvents[contributor.id] ?? [] + let emitDiscontinuityArguments = await contributor.emitDiscontinuityArguments + try #require(emitDiscontinuityArguments.count == expectedPendingDiscontinuityEvents.count) + for (emitDiscontinuityArgument, expectedArgument) in zip(emitDiscontinuityArguments, expectedPendingDiscontinuityEvents) { + #expect(emitDiscontinuityArgument === expectedArgument) + } + } + + for contributor in contributors { + #expect(await manager.testsOnly_pendingDiscontinuityEvents(for: contributor).isEmpty) + } + } + // @spec CHA-RL1h2 // @specOneOf(1/2) CHA-RL1h1 - tests that an error gets thrown when channel attach fails due to entering SUSPENDED (TODO: but I don’t yet fully understand the meaning of CHA-RL1h1; outstanding question https://github.com/ably/specification/pull/200/files#r1765476610) // @specPartial CHA-RL1h3 - Have tested the failure of the operation and the error that’s thrown. Have not yet implemented the "enter the recovery loop" (TODO: https://github.com/ably-labs/ably-chat-swift/issues/50)