diff --git a/Sources/AblyChat/DefaultMessages.swift b/Sources/AblyChat/DefaultMessages.swift index 03339058..84836681 100644 --- a/Sources/AblyChat/DefaultMessages.swift +++ b/Sources/AblyChat/DefaultMessages.swift @@ -234,10 +234,11 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities { case .failed, .suspended: // TODO: Revisit as part of https://github.com/ably-labs/ably-chat-swift/issues/32 logger.log(message: "Channel failed to attach", level: .error) + let errorCodeCase = ErrorCode.CaseThatImpliesFixedStatusCode.messagesAttachmentFailed nillableContinuation?.resume( throwing: ARTErrorInfo.create( - withCode: ErrorCode.messagesAttachmentFailed.rawValue, - status: ErrorCode.messagesAttachmentFailed.statusCode, + withCode: errorCodeCase.toNumericErrorCode.rawValue, + status: errorCodeCase.statusCode, message: "Channel failed to attach" ) ) diff --git a/Sources/AblyChat/Errors.swift b/Sources/AblyChat/Errors.swift index c955e181..ba036c46 100644 --- a/Sources/AblyChat/Errors.swift +++ b/Sources/AblyChat/Errors.swift @@ -11,8 +11,6 @@ public let errorDomain = "AblyChatErrorDomain" The error codes for errors in the ``errorDomain`` error domain. */ public enum ErrorCode: Int { - case nonspecific = 40000 - /// ``Rooms.get(roomID:options:)`` was called with a different set of room options than was used on a previous call. You must first release the existing room instance using ``Rooms.release(roomID:)``. /// /// TODO this code is a guess, revisit in https://github.com/ably-labs/ably-chat-swift/issues/32 @@ -36,30 +34,118 @@ public enum ErrorCode: Int { case roomInInvalidState = 102_107 + /// Has a case for each of the ``ErrorCode`` cases that imply a fixed status code. + internal enum CaseThatImpliesFixedStatusCode { + case inconsistentRoomOptions + case messagesAttachmentFailed + case presenceAttachmentFailed + case reactionsAttachmentFailed + case occupancyAttachmentFailed + case typingAttachmentFailed + case messagesDetachmentFailed + case presenceDetachmentFailed + case reactionsDetachmentFailed + case occupancyDetachmentFailed + case typingDetachmentFailed + case roomInFailedState + case roomIsReleasing + case roomIsReleased + + internal var toNumericErrorCode: ErrorCode { + switch self { + case .inconsistentRoomOptions: + .inconsistentRoomOptions + case .messagesAttachmentFailed: + .messagesAttachmentFailed + case .presenceAttachmentFailed: + .presenceAttachmentFailed + case .reactionsAttachmentFailed: + .reactionsAttachmentFailed + case .occupancyAttachmentFailed: + .occupancyAttachmentFailed + case .typingAttachmentFailed: + .typingAttachmentFailed + case .messagesDetachmentFailed: + .messagesDetachmentFailed + case .presenceDetachmentFailed: + .presenceDetachmentFailed + case .reactionsDetachmentFailed: + .reactionsDetachmentFailed + case .occupancyDetachmentFailed: + .occupancyDetachmentFailed + case .typingDetachmentFailed: + .typingDetachmentFailed + case .roomInFailedState: + .roomInFailedState + case .roomIsReleasing: + .roomIsReleasing + case .roomIsReleased: + .roomIsReleased + } + } + + /// The ``ARTErrorInfo.statusCode`` that should be returned for this error. + internal var statusCode: Int { + // These status codes are taken from the "Chat-specific Error Codes" section of the spec. + switch self { + case .inconsistentRoomOptions, + .roomInFailedState, + .roomIsReleasing, + .roomIsReleased: + 400 + case + .messagesAttachmentFailed, + .presenceAttachmentFailed, + .reactionsAttachmentFailed, + .occupancyAttachmentFailed, + .typingAttachmentFailed, + .messagesDetachmentFailed, + .presenceDetachmentFailed, + .reactionsDetachmentFailed, + .occupancyDetachmentFailed, + .typingDetachmentFailed: + 500 + } + } + } + + /// Has a case for each of the ``ErrorCode`` cases that do not imply a fixed status code. + internal enum CaseThatImpliesVariableStatusCode { + case roomInInvalidState + + internal var toNumericErrorCode: ErrorCode { + switch self { + case .roomInInvalidState: + .roomInInvalidState + } + } + } +} + +/** + * Represents a case of ``ErrorCode`` plus a status code. + */ +internal enum ErrorCodeAndStatusCode { + case fixedStatusCode(ErrorCode.CaseThatImpliesFixedStatusCode) + case variableStatusCode(ErrorCode.CaseThatImpliesVariableStatusCode, statusCode: Int) + + /// The ``ARTErrorInfo.code`` that should be returned for this error. + internal var code: ErrorCode { + switch self { + case let .fixedStatusCode(code): + code.toNumericErrorCode + case let .variableStatusCode(code, _): + code.toNumericErrorCode + } + } + /// The ``ARTErrorInfo.statusCode`` that should be returned for this error. internal var statusCode: Int { - // These status codes are taken from the "Chat-specific Error Codes" section of the spec. switch self { - case .nonspecific, - .inconsistentRoomOptions, - .roomInFailedState, - .roomIsReleasing, - .roomIsReleased: - 400 - case - .messagesAttachmentFailed, - .presenceAttachmentFailed, - .reactionsAttachmentFailed, - .occupancyAttachmentFailed, - .typingAttachmentFailed, - .messagesDetachmentFailed, - .presenceDetachmentFailed, - .reactionsDetachmentFailed, - .occupancyDetachmentFailed, - .typingDetachmentFailed, - // CHA-RL9c - .roomInInvalidState: - 500 + case let .fixedStatusCode(code): + code.statusCode + case let .variableStatusCode(_, statusCode): + statusCode } } } @@ -77,51 +163,50 @@ internal enum ChatError { case roomIsReleasing case roomIsReleased case presenceOperationRequiresRoomAttach(feature: RoomFeature) - case presenceOperationDisallowedForCurrentRoomStatus(feature: RoomFeature) - case roomInInvalidState(cause: ARTErrorInfo?) + case roomTransitionedToInvalidStateForPresenceOperation(cause: ARTErrorInfo?) - /// The ``ARTErrorInfo.code`` that should be returned for this error. - internal var code: ErrorCode { + internal var codeAndStatusCode: ErrorCodeAndStatusCode { switch self { case .inconsistentRoomOptions: - .inconsistentRoomOptions + .fixedStatusCode(.inconsistentRoomOptions) case let .attachmentFailed(feature, _): switch feature { case .messages: - .messagesAttachmentFailed + .fixedStatusCode(.messagesAttachmentFailed) case .occupancy: - .occupancyAttachmentFailed + .fixedStatusCode(.occupancyAttachmentFailed) case .presence: - .presenceAttachmentFailed + .fixedStatusCode(.presenceAttachmentFailed) case .reactions: - .reactionsAttachmentFailed + .fixedStatusCode(.reactionsAttachmentFailed) case .typing: - .typingAttachmentFailed + .fixedStatusCode(.typingAttachmentFailed) } case let .detachmentFailed(feature, _): switch feature { case .messages: - .messagesDetachmentFailed + .fixedStatusCode(.messagesDetachmentFailed) case .occupancy: - .occupancyDetachmentFailed + .fixedStatusCode(.occupancyDetachmentFailed) case .presence: - .presenceDetachmentFailed + .fixedStatusCode(.presenceDetachmentFailed) case .reactions: - .reactionsDetachmentFailed + .fixedStatusCode(.reactionsDetachmentFailed) case .typing: - .typingDetachmentFailed + .fixedStatusCode(.typingDetachmentFailed) } case .roomInFailedState: - .roomInFailedState + .fixedStatusCode(.roomInFailedState) case .roomIsReleasing: - .roomIsReleasing + .fixedStatusCode(.roomIsReleasing) case .roomIsReleased: - .roomIsReleased - case .roomInInvalidState: - .roomInInvalidState - case .presenceOperationRequiresRoomAttach, - .presenceOperationDisallowedForCurrentRoomStatus: - .nonspecific + .fixedStatusCode(.roomIsReleased) + case .roomTransitionedToInvalidStateForPresenceOperation: + // CHA-RL9c + .variableStatusCode(.roomInInvalidState, statusCode: 500) + case .presenceOperationRequiresRoomAttach: + // CHA-PR3h, CHA-PR10h, CHA-PR6h, CHA-T2g + .variableStatusCode(.roomInInvalidState, statusCode: 400) } } @@ -177,9 +262,7 @@ internal enum ChatError { "Cannot perform operation because the room is in a released state." case let .presenceOperationRequiresRoomAttach(feature): "To perform this \(Self.descriptionOfFeature(feature)) operation, you must first attach the room." - case let .presenceOperationDisallowedForCurrentRoomStatus(feature): - "This \(Self.descriptionOfFeature(feature)) operation can not be performed given the current room status." - case .roomInInvalidState: + case .roomTransitionedToInvalidStateForPresenceOperation: "The room operation failed because the room was in an invalid state." } } @@ -191,14 +274,13 @@ internal enum ChatError { underlyingError case let .detachmentFailed(_, underlyingError): underlyingError - case let .roomInInvalidState(cause): + case let .roomTransitionedToInvalidStateForPresenceOperation(cause): cause case .inconsistentRoomOptions, .roomInFailedState, .roomIsReleasing, .roomIsReleased, - .presenceOperationRequiresRoomAttach, - .presenceOperationDisallowedForCurrentRoomStatus: + .presenceOperationRequiresRoomAttach: nil } } @@ -208,7 +290,7 @@ internal extension ARTErrorInfo { convenience init(chatError: ChatError) { var userInfo: [String: Any] = [:] // TODO: copied and pasted from implementation of -[ARTErrorInfo createWithCode:status:message:requestId:] because there’s no way to pass domain; revisit in https://github.com/ably-labs/ably-chat-swift/issues/32. Also the ARTErrorInfoStatusCode variable in ably-cocoa is not public. - userInfo["ARTErrorInfoStatusCode"] = chatError.code.statusCode + userInfo["ARTErrorInfoStatusCode"] = chatError.codeAndStatusCode.statusCode userInfo[NSLocalizedDescriptionKey] = chatError.localizedDescription // TODO: This is kind of an implementation detail (that NSUnderlyingErrorKey is what populates `cause`); consider documenting in ably-cocoa as part of https://github.com/ably-labs/ably-chat-swift/issues/32. @@ -218,7 +300,7 @@ internal extension ARTErrorInfo { self.init( domain: errorDomain, - code: chatError.code.rawValue, + code: chatError.codeAndStatusCode.code.rawValue, userInfo: userInfo ) } diff --git a/Sources/AblyChat/RoomFeature.swift b/Sources/AblyChat/RoomFeature.swift index 5b9968f8..5383b8ff 100644 --- a/Sources/AblyChat/RoomFeature.swift +++ b/Sources/AblyChat/RoomFeature.swift @@ -41,12 +41,11 @@ internal protocol FeatureChannel: Sendable, EmitsDiscontinuities { /// Waits until we can perform presence operations on the contributors of this room without triggering an implicit attach. /// - /// Implements the checks described by CHA-PR3d, CHA-PR3e, CHA-PR3f, and CHA-PR3g (and similar ones described by other functionality that performs contributor presence operations). Namely: + /// Implements the checks described by CHA-PR3d, CHA-PR3e, and CHA-PR3h (and similar ones described by other functionality that performs contributor presence operations). Namely: /// - /// - CHA-RL9, which is invoked by CHA-PR3d, CHA-PR10d, CHA-PR6c, CHA-T2c: If the room is in the ATTACHING status, it waits for the next room status change. If the new status is ATTACHED, it returns. Else, it throws an `ARTErrorInfo` derived from ``ChatError.roomInInvalidState(cause:)``. - /// - CHA-PR3e, CHA-PR11e, CHA-PR6d, CHA-T2d: If the room is in the ATTACHED status, it returns immediately. - /// - CHA-PR3f, CHA-PR11f, CHA-PR6e, CHA-T2e: If the room is in the DETACHED status, it throws an `ARTErrorInfo` derived from ``ChatError.presenceOperationRequiresRoomAttach(feature:)``. - /// - // CHA-PR3g, CHA-PR11g, CHA-PR6f, CHA-T2f: If the room is in any other status, it throws an `ARTErrorInfo` derived from ``ChatError.presenceOperationDisallowedForCurrentRoomStatus(feature:)``. + /// - CHA-RL9, which is invoked by CHA-PR3d, CHA-PR10d, CHA-PR6c, CHA-T2c: If the room is in the ATTACHING status, it waits for the next room status change. If the new status is ATTACHED, it returns. Else, it throws an `ARTErrorInfo` derived from ``ChatError.roomTransitionedToInvalidStateForPresenceOperation(cause:)``. + /// - CHA-PR3e, CHA-PR10e, CHA-PR6d, CHA-T2d: If the room is in the ATTACHED status, it returns immediately. + /// - CHA-PR3h, CHA-PR10h, CHA-PR6h, CHA-T2g: If the room is in any other status, it throws an `ARTErrorInfo` derived from ``ChatError.presenceOperationRequiresRoomAttach(feature:)``. /// /// - Parameters: /// - requester: The room feature that wishes to perform a presence operation. This is only used for customising the message of the thrown error. diff --git a/Sources/AblyChat/RoomLifecycleManager.swift b/Sources/AblyChat/RoomLifecycleManager.swift index de05474e..8c61522e 100644 --- a/Sources/AblyChat/RoomLifecycleManager.swift +++ b/Sources/AblyChat/RoomLifecycleManager.swift @@ -1225,17 +1225,14 @@ internal actor DefaultRoomLifecycleManager Bool { +func isChatError(_ maybeError: (any Error)?, withCodeAndStatusCode codeAndStatusCode: AblyChat.ErrorCodeAndStatusCode, cause: ARTErrorInfo? = nil, message: String? = nil) -> Bool { guard let ablyError = maybeError as? ARTErrorInfo else { return false } return ablyError.domain == AblyChat.errorDomain as String - && ablyError.code == code.rawValue - && ablyError.statusCode == code.statusCode + && ablyError.code == codeAndStatusCode.code.rawValue + && ablyError.statusCode == codeAndStatusCode.statusCode && ablyError.cause == cause && { guard let message else {