@@ -182,10 +182,12 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
182182 internal enum Status : Equatable {
183183 case initialized
184184 case attachingDueToAttachOperation( attachOperationID: UUID )
185+ case attachingDueToRetryOperation( retryOperationID: UUID )
185186 case attachingDueToContributorStateChange( error: ARTErrorInfo ? )
186187 case attached
187188 case detaching( detachOperationID: UUID )
188189 case detached
190+ case detachedDueToRetryOperation( retryOperationID: UUID )
189191 case suspendedAwaitingStartOfRetryOperation( error: ARTErrorInfo )
190192 case suspended( retryOperationID: UUID , error: ARTErrorInfo )
191193 case failed( error: ARTErrorInfo )
@@ -198,13 +200,15 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
198200 . initialized
199201 case . attachingDueToAttachOperation:
200202 . attaching( error: nil )
203+ case . attachingDueToRetryOperation:
204+ . attaching( error: nil )
201205 case let . attachingDueToContributorStateChange( error: error) :
202206 . attaching( error: error)
203207 case . attached:
204208 . attached
205209 case . detaching:
206210 . detaching
207- case . detached:
211+ case . detached, . detachedDueToRetryOperation :
208212 . detached
209213 case let . suspendedAwaitingStartOfRetryOperation( error) :
210214 . suspended( error: error)
@@ -223,8 +227,12 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
223227 switch self {
224228 case let . attachingDueToAttachOperation( attachOperationID) :
225229 attachOperationID
230+ case let . attachingDueToRetryOperation( retryOperationID) :
231+ retryOperationID
226232 case let . detaching( detachOperationID) :
227233 detachOperationID
234+ case let . detachedDueToRetryOperation( retryOperationID) :
235+ retryOperationID
228236 case let . releasing( releaseOperationID) :
229237 releaseOperationID
230238 case let . suspended( retryOperationID, _) :
@@ -321,8 +329,12 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
321329 logger. log ( message: " Transitioning from \( status) to \( new) " , level: . info)
322330 let previous = status
323331 status = new
324- let statusChange = RoomStatusChange ( current: status. toRoomStatus, previous: previous. toRoomStatus)
325- emitStatusChange ( statusChange)
332+
333+ // Avoid a double-emit of room status when changing from `.suspendedAwaitingStartOfRetryOperation` to `.suspended`.
334+ if new. toRoomStatus != previous. toRoomStatus {
335+ let statusChange = RoomStatusChange ( current: status. toRoomStatus, previous: previous. toRoomStatus)
336+ emitStatusChange ( statusChange)
337+ }
326338 }
327339
328340 private func emitStatusChange( _ change: RoomStatusChange ) {
@@ -709,7 +721,7 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
709721 case . released:
710722 // CHA-RL1c
711723 throw ARTErrorInfo ( chatError: . roomIsReleased)
712- case . initialized, . suspendedAwaitingStartOfRetryOperation, . suspended, . attachingDueToAttachOperation, . attachingDueToContributorStateChange, . detached, . detaching, . failed:
724+ case . initialized, . suspendedAwaitingStartOfRetryOperation, . suspended, . attachingDueToAttachOperation, . attachingDueToRetryOperation , . attachingDueToContributorStateChange, . detached, . detachedDueToRetryOperation , . detaching, . failed:
713725 break
714726 }
715727
@@ -825,7 +837,7 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
825837
826838 private func bodyOfDetachOperation( operationID: UUID ) async throws ( ARTErrorInfo) {
827839 switch status {
828- case . detached:
840+ case . detached, . detachedDueToRetryOperation :
829841 // CHA-RL2a
830842 return
831843 case . releasing:
@@ -837,22 +849,38 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
837849 case . failed:
838850 // CHA-RL2d
839851 throw ARTErrorInfo ( chatError: . roomInFailedState)
840- case . initialized, . suspendedAwaitingStartOfRetryOperation, . suspended, . attachingDueToAttachOperation, . attachingDueToContributorStateChange, . attached, . detaching:
852+ case . initialized, . suspendedAwaitingStartOfRetryOperation, . suspended, . attachingDueToAttachOperation, . attachingDueToRetryOperation , . attachingDueToContributorStateChange, . attached, . detaching:
841853 break
842854 }
843855
844856 // CHA-RL2e
845857 clearTransientDisconnectTimeouts ( )
846858 changeStatus ( to: . detaching( detachOperationID: operationID) )
847859
848- try await performDetachmentCycle ( )
860+ try await performDetachmentCycle ( trigger: . detachOperation)
861+ }
862+
863+ /// Describes the reason a CHA-RL2f detachment cycle is being performed.
864+ private enum DetachmentCycleTrigger {
865+ case detachOperation
866+ case retryOperation( retryOperationID: UUID , triggeringContributor: Contributor )
867+
868+ /// Given a CHA-RL2f detachment cycle triggered by this trigger, returns the DETACHED status to which the room should transition per CHA-RL2g.
869+ var detachedStatus : Status {
870+ switch self {
871+ case . detachOperation:
872+ . detached
873+ case let . retryOperation( retryOperationID, _) :
874+ . detachedDueToRetryOperation( retryOperationID: retryOperationID)
875+ }
876+ }
849877 }
850878
851879 /// Performs the “CHA-RL2f detachment cycle”, to use the terminology of CHA-RL5a.
852- private func performDetachmentCycle( ) async throws ( ARTErrorInfo) {
880+ private func performDetachmentCycle( trigger : DetachmentCycleTrigger ) async throws ( ARTErrorInfo) {
853881 // CHA-RL2f
854882 var firstDetachError : ARTErrorInfo ?
855- for contributor in contributors {
883+ for contributor in contributorsForDetachmentCycle ( trigger : trigger ) {
856884 logger. log ( message: " Detaching contributor \( contributor) " , level: . info)
857885 do {
858886 try await contributor. channel. detach ( )
@@ -904,7 +932,19 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
904932 }
905933
906934 // CHA-RL2g
907- changeStatus ( to: . detached)
935+ changeStatus ( to: trigger. detachedStatus)
936+ }
937+
938+ /// Returns the contributors that should be detached in a CHA-RL2f detachment cycle.
939+ private func contributorsForDetachmentCycle( trigger: DetachmentCycleTrigger ) -> [ Contributor ] {
940+ switch trigger {
941+ case . detachOperation:
942+ // CHA-RL2f
943+ contributors
944+ case let . retryOperation( _, triggeringContributor) :
945+ // CHA-RL5a
946+ contributors. filter { $0. id != triggeringContributor. id }
947+ }
908948 }
909949
910950 // MARK: - RELEASE operation
@@ -934,7 +974,7 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
934974 case . released:
935975 // CHA-RL3a
936976 return
937- case . detached:
977+ case . detached, . detachedDueToRetryOperation :
938978 // CHA-RL3b
939979 changeStatus ( to: . released)
940980 return
@@ -943,7 +983,7 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
943983 // See note on waitForCompletionOfOperationWithID for the current need for this force try
944984 // swiftlint:disable:next force_try
945985 return try ! await waitForCompletionOfOperationWithID ( releaseOperationID, requester: . anotherOperation( operationID: operationID) )
946- case . initialized, . attached, . attachingDueToAttachOperation, . attachingDueToContributorStateChange, . detaching, . suspendedAwaitingStartOfRetryOperation, . suspended, . failed:
986+ case . initialized, . attached, . attachingDueToAttachOperation, . attachingDueToRetryOperation , . attachingDueToContributorStateChange, . detaching, . suspendedAwaitingStartOfRetryOperation, . suspended, . failed:
947987 break
948988 }
949989
@@ -982,6 +1022,108 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
9821022 changeStatus ( to: . released)
9831023 }
9841024
1025+ // MARK: - RETRY operation
1026+
1027+ /// Implements CHA-RL5’s RETRY operation.
1028+ ///
1029+ /// - Parameters:
1030+ /// - forcedOperationID: Allows tests to force the operation to have a given ID. In combination with the ``testsOnly_subscribeToOperationWaitEvents`` API, this allows tests to verify that one test-initiated operation is waiting for another test-initiated operation.
1031+ /// - triggeringContributor: This is, in the language of CHA-RL5a, “the channel that became SUSPENDED”.
1032+ internal func performRetryOperation( testsOnly_forcingOperationID forcedOperationID: UUID ? = nil , triggeredByContributor triggeringContributor: Contributor , errorForSuspendedStatus: ARTErrorInfo ) async {
1033+ // See note on performAnOperation for the current need for this force try
1034+ // swiftlint:disable:next force_try
1035+ try ! await performAnOperation ( forcingOperationID: forcedOperationID) { operationID in
1036+ await bodyOfRetryOperation (
1037+ operationID: operationID,
1038+ triggeredByContributor: triggeringContributor,
1039+ errorForSuspendedStatus: errorForSuspendedStatus
1040+ )
1041+ }
1042+ }
1043+
1044+ private func bodyOfRetryOperation(
1045+ operationID: UUID ,
1046+ triggeredByContributor triggeringContributor: Contributor ,
1047+ errorForSuspendedStatus: ARTErrorInfo
1048+ ) async {
1049+ changeStatus ( to: . suspended( retryOperationID: operationID, error: errorForSuspendedStatus) )
1050+
1051+ // CHA-RL5a
1052+ do {
1053+ try await performDetachmentCycle (
1054+ trigger: . retryOperation(
1055+ retryOperationID: operationID,
1056+ triggeringContributor: triggeringContributor
1057+ )
1058+ )
1059+ } catch {
1060+ logger. log ( message: " RETRY’s detachment cycle failed with error \( error) . Ending RETRY. " , level: . debug)
1061+ return
1062+ }
1063+
1064+ // CHA-RL5d
1065+ do {
1066+ try await waitForContributorThatTriggeredRetryToBecomeAttached ( triggeringContributor)
1067+ } catch {
1068+ // CHA-RL5e
1069+ logger. log ( message: " RETRY’s waiting for triggering contributor to attach failed with error \( error) . Ending RETRY. " , level: . debug)
1070+ return
1071+ }
1072+
1073+ // CHA-RL5f
1074+ changeStatus ( to: . attachingDueToRetryOperation( retryOperationID: operationID) )
1075+ do {
1076+ try await performAttachmentCycle ( )
1077+ } catch {
1078+ logger. log ( message: " RETRY’s attachment cycle failed with error \( error) . Ending RETRY. " , level: . debug)
1079+ return
1080+ }
1081+ }
1082+
1083+ /// Performs CHA-RL5d’s “the room waits until the original channel that caused the retry loop naturally enters the ATTACHED state”.
1084+ ///
1085+ /// Throws an error if the room enters the FAILED status, which is considered terminal by the RETRY operation.
1086+ private func waitForContributorThatTriggeredRetryToBecomeAttached( _ triggeringContributor: Contributor ) async throws {
1087+ logger. log ( message: " RETRY waiting for \( triggeringContributor) to enter ATTACHED " , level: . debug)
1088+
1089+ let handleState = { [ self ] ( state: ARTRealtimeChannelState , associatedError: ARTErrorInfo ? ) in
1090+ switch state {
1091+ // CHA-RL5d
1092+ case . attached:
1093+ logger. log ( message: " RETRY completed waiting for \( triggeringContributor) to enter ATTACHED " , level: . debug)
1094+ return true
1095+ // CHA-RL5e
1096+ case . failed:
1097+ guard let associatedError else {
1098+ preconditionFailure ( " Contributor entered FAILED but there’s no associated error " )
1099+ }
1100+ logger. log ( message: " RETRY failed waiting for \( triggeringContributor) to enter ATTACHED, since it entered FAILED with error \( associatedError) " , level: . debug)
1101+
1102+ changeStatus ( to: . failed( error: associatedError) )
1103+ throw associatedError
1104+ case . attaching, . detached, . detaching, . initialized, . suspended:
1105+ return false
1106+ @unknown default :
1107+ return false
1108+ }
1109+ }
1110+
1111+ // Check whether the contributor is already in one of the states that we’re going to wait for. CHA-RL5d doesn’t make this check explicit but it seems like the right thing to do (asked in https://github.com/ably/specification/issues/221).
1112+ // TODO: this assumes that if you fetch a channel’s `state` and then its `errorReason`, they will both refer to the same channel state; this may not be true due to threading, address in https://github.com/ably-labs/ably-chat-swift/issues/49
1113+ if try await handleState ( triggeringContributor. channel. state, triggeringContributor. channel. errorReason) {
1114+ return
1115+ }
1116+
1117+ // TODO: this assumes that if you check a channel’s state, and it’s x, and you then immediately add a state listener, you’ll definitely find out if the channel changes to a state other than x; this may not be true due to threading, address in https://github.com/ably-labs/ably-chat-swift/issues/49
1118+ for await stateChange in await triggeringContributor. channel. subscribeToState ( ) {
1119+ // (I prefer this way of writing it, in this case)
1120+ // swiftlint:disable:next for_where
1121+ if try handleState ( stateChange. current, stateChange. reason) {
1122+ return
1123+ }
1124+ }
1125+ }
1126+
9851127 // MARK: - Waiting to be able to perform presence operations
9861128
9871129 internal func waitToBeAbleToPerformPresenceOperations( requestedByFeature requester: RoomFeature ) async throws ( ARTErrorInfo) {
@@ -992,10 +1134,13 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
9921134 case . attachingDueToContributorStateChange:
9931135 // TODO: Spec doesn’t say what to do in this situation; asked in https://github.com/ably/specification/issues/228
9941136 fatalError ( " waitToBeAbleToPerformPresenceOperations doesn’t currently handle attachingDueToContributorStateChange " )
1137+ case . attachingDueToRetryOperation:
1138+ // TODO: Spec doesn’t say what to do in this situation; asked in https://github.com/ably/specification/issues/228
1139+ fatalError ( " waitToBeAbleToPerformPresenceOperations doesn’t currently handle attachingDueToRetryOperation " )
9951140 case . attached:
9961141 // CHA-PR3e, CHA-PR11e, CHA-PR6d, CHA-T2d
9971142 break
998- case . detached:
1143+ case . detached, . detachedDueToRetryOperation :
9991144 // CHA-PR3f, CHA-PR11f, CHA-PR6e, CHA-T2e
10001145 throw . init( chatError: . presenceOperationRequiresRoomAttach( feature: requester) )
10011146 case . detaching,
0 commit comments