From b069867a76b83bac66cb70f9f24b54cc3371d626 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 30 Sep 2024 14:12:44 -0300 Subject: [PATCH] Implement CHA-RL1g2 Resolves #53. --- Sources/AblyChat/RoomLifecycleManager.swift | 31 +++++++++++++++- .../RoomLifecycleManagerTests.swift | 36 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/Sources/AblyChat/RoomLifecycleManager.swift b/Sources/AblyChat/RoomLifecycleManager.swift index a4615f60..82298141 100644 --- a/Sources/AblyChat/RoomLifecycleManager.swift +++ b/Sources/AblyChat/RoomLifecycleManager.swift @@ -67,6 +67,7 @@ internal actor RoomLifecycleManager { await self.init( current: nil, hasOperationInProgress: nil, + pendingDiscontinuityEvents: nil, contributors: contributors, logger: logger, clock: clock @@ -77,6 +78,8 @@ internal actor RoomLifecycleManager { internal init( testsOnly_current current: RoomLifecycle? = nil, testsOnly_hasOperationInProgress hasOperationInProgress: Bool? = nil, + // In the same order as `contributors` + testsOnly_pendingDiscontinuityEvents pendingDiscontinuityEvents: [[ARTErrorInfo]]? = nil, contributors: [Contributor], logger: InternalLogger, clock: SimpleClock @@ -84,6 +87,7 @@ internal actor RoomLifecycleManager { await self.init( current: current, hasOperationInProgress: hasOperationInProgress, + pendingDiscontinuityEvents: pendingDiscontinuityEvents, contributors: contributors, logger: logger, clock: clock @@ -94,14 +98,19 @@ internal actor RoomLifecycleManager { private init( current: RoomLifecycle?, hasOperationInProgress: Bool?, + pendingDiscontinuityEvents: [[ARTErrorInfo]]?, contributors: [Contributor], logger: InternalLogger, clock: SimpleClock ) async { self.current = current ?? .initialized self.hasOperationInProgress = hasOperationInProgress ?? false + if let pendingDiscontinuityEvents { + contributorAnnotations = pendingDiscontinuityEvents.map { .init(pendingDiscontinuityEvents: $0) } + } else { + contributorAnnotations = Array(repeating: .init(), count: contributors.count) + } self.contributors = contributors - contributorAnnotations = Array(repeating: .init(), count: contributors.count) self.logger = logger self.clock = clock @@ -330,6 +339,26 @@ 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, annotation) in zip(contributors, contributorAnnotations) { + for pendingDiscontinuityEvent in annotation.pendingDiscontinuityEvents { + logger.log(message: "Emitting pending discontinuity event \(pendingDiscontinuityEvent) to contributor \(contributor)", level: .info) + await contributor.emitDiscontinuity(pendingDiscontinuityEvent) + } + } + + // Clear all pending discontinuity events + for i in contributorAnnotations.indices { + contributorAnnotations[i].pendingDiscontinuityEvents = [] + } } /// 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 fab135f5..8e77c895 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: [[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: [[ARTErrorInfo]] = [ + [], + [.init(domain: "SomeDomain", code: 123) /* arbitrary */ ], + [.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, expectedPendingDiscontinuityEvents) in zip(contributors, pendingDiscontinuityEvents) { + let emitDiscontinuityArguments = await contributor.emitDiscontinuityArguments + try #require(emitDiscontinuityArguments.count == expectedPendingDiscontinuityEvents.count) + for (emitDiscontinuityArgument, expectedArgument) in zip(emitDiscontinuityArguments, expectedPendingDiscontinuityEvents) { + #expect(emitDiscontinuityArgument === expectedArgument) + } + } + + for i in contributors.indices { + #expect(await manager.testsOnly_pendingDiscontinuityEventsForContributor(at: i).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)