diff --git a/Sources/AblyChat/Room.swift b/Sources/AblyChat/Room.swift index 3e91370..cf1c6bf 100644 --- a/Sources/AblyChat/Room.swift +++ b/Sources/AblyChat/Room.swift @@ -106,7 +106,6 @@ internal actor DefaultRoom } } - /// The features are returned in CHA-RC2e order. static func fromRoomOptions(_ roomOptions: RoomOptions) -> [Self] { var result: [Self] = [.messages] @@ -214,8 +213,6 @@ internal actor DefaultRoom } /// Each feature in `featuresWithOptions` is guaranteed to appear in the `features` member of precisely one of the returned array’s values. - /// - /// The elements of `featuresWithOptions` must be in CHA-RC2e order. private static func createFeatureChannelPartialDependencies(roomID: String, featuresWithOptions: [RoomFeatureWithOptions], realtime: RealtimeClient) -> [(features: [RoomFeature], featureChannelPartialDependencies: FeatureChannelPartialDependencies)] { // CHA-RC3a @@ -225,7 +222,7 @@ internal actor DefaultRoom let featuresGroupedByChannelName = Dictionary(grouping: featuresWithOptions) { $0.toRoomFeature.channelNameForRoomID(roomID) } - return featuresGroupedByChannelName.map { channelName, features in + let unorderedResult = featuresGroupedByChannelName.map { channelName, features in var channelOptions = RealtimeChannelOptions() // channel setup for presence and occupancy @@ -251,13 +248,16 @@ internal actor DefaultRoom let channel = realtime.getChannel(channelName, opts: channelOptions) // Give the contributor the first of the enabled features that correspond to this channel, using CHA-RC2e ordering. This will determine which feature is used for atttachment and detachment errors. - let contributorFeature = features[0].toRoomFeature + let contributorFeature = features.map(\.toRoomFeature).sorted { RoomFeature.areInPrecedenceListOrder($0, $1) }[0] let contributor = DefaultRoomLifecycleContributor(channel: .init(underlyingChannel: channel), feature: contributorFeature) let featureChannelPartialDependencies = FeatureChannelPartialDependencies(channel: channel, contributor: contributor) return (features.map(\.toRoomFeature), featureChannelPartialDependencies) } + + // Sort the result in CHA-RC2e order + return unorderedResult.sorted { RoomFeature.areInPrecedenceListOrder($0.1.contributor.feature, $1.1.contributor.feature) } } private static func createFeatureChannels(partialDependencies: [(features: [RoomFeature], featureChannelPartialDependencies: FeatureChannelPartialDependencies)], lifecycleManager: RoomLifecycleManager) -> [RoomFeature: DefaultFeatureChannel] { diff --git a/Sources/AblyChat/RoomFeature.swift b/Sources/AblyChat/RoomFeature.swift index 6d7c5bb..5a81432 100644 --- a/Sources/AblyChat/RoomFeature.swift +++ b/Sources/AblyChat/RoomFeature.swift @@ -1,12 +1,13 @@ import Ably /// The features offered by a chat room. -internal enum RoomFeature { +internal enum RoomFeature: CaseIterable { + // This list MUST be kept in the same order as the list in CHA-RC2e, in order for the implementation of `areInPrecedenceListOrder` to work. case messages case presence + case typing case reactions case occupancy - case typing internal func channelNameForRoomID(_ roomID: String) -> String { "\(roomID)::$chat::$\(channelNameSuffix)" @@ -27,6 +28,14 @@ internal enum RoomFeature { "typingIndicators" } } + + /// Returns a `Bool` indicating whether `first` and `second` are in the same order as the list given in CHA-RC2e. + internal static func areInPrecedenceListOrder(_ first: Self, _ second: Self) -> Bool { + let allCases = Self.allCases + let indexOfFirst = allCases.firstIndex(of: first)! + let indexOfSecond = allCases.firstIndex(of: second)! + return indexOfFirst < indexOfSecond + } } /// Provides all of the channel-related functionality that a room feature (e.g. an implementation of ``Messages``) needs. diff --git a/Tests/AblyChatTests/DefaultRoomTests.swift b/Tests/AblyChatTests/DefaultRoomTests.swift index 5a6d9b4..8eed24a 100644 --- a/Tests/AblyChatTests/DefaultRoomTests.swift +++ b/Tests/AblyChatTests/DefaultRoomTests.swift @@ -100,6 +100,30 @@ struct DefaultRoomTests { #expect(Set(channelsGetArguments.map(\.name)) == Set(expectedFetchedChannelNames)) } + // @spec CHA-RC2e + // @spec CHA-RL10 + @Test + func lifecycleContributorOrder() async throws { + // Given: a DefaultRoom, instance, with all room features enabled + let channelsList = [ + MockRealtimeChannel(name: "basketball::$chat::$chatMessages"), + MockRealtimeChannel(name: "basketball::$chat::$reactions"), + MockRealtimeChannel(name: "basketball::$chat::$typingIndicators"), + ] + let channels = MockChannels(channels: channelsList) + let realtime = MockRealtime.create(channels: channels) + let lifecycleManagerFactory = MockRoomLifecycleManagerFactory() + _ = try await DefaultRoom(realtime: realtime, chatAPI: ChatAPI(realtime: realtime), roomID: "basketball", options: .allFeaturesEnabled, logger: TestLogger(), lifecycleManagerFactory: lifecycleManagerFactory) + + // Then: The array of contributors with which it initializes the RoomLifecycleManager are in the same order as the following list: + // + // messages, presence, typing, reactions, occupancy + // + // (note that we do not say that it is the _same_ list, because we combine multiple features into a single contributor) + let lifecycleManagerCreationArguments = try #require(await lifecycleManagerFactory.createManagerArguments.first) + #expect(lifecycleManagerCreationArguments.contributors.map(\.feature) == [.messages, .typing, .reactions]) + } + // @specUntested CHA-RC2b - We chose to implement this failure with an idiomatic fatalError instead of throwing, but we can’t test this. // This is just a basic sense check to make sure the room getters are working as expected, since we don’t have unit tests for some of the features at the moment.