diff --git a/Sources/AblyChat/Room.swift b/Sources/AblyChat/Room.swift index e0c724b2..49189ff2 100644 --- a/Sources/AblyChat/Room.swift +++ b/Sources/AblyChat/Room.swift @@ -131,34 +131,57 @@ internal actor DefaultRoom internal var contributor: DefaultRoomLifecycleContributor } - private static func createFeatureChannelPartialDependencies(roomID: String, roomOptions: RoomOptions, realtime: RealtimeClient) -> [RoomFeature: FeatureChannelPartialDependencies] { - .init(uniqueKeysWithValues: [ - RoomFeature.messages, - RoomFeature.reactions, - RoomFeature.presence, - RoomFeature.occupancy, - ].map { feature in - var channelOptions = RealtimeChannelOptions() + /// The returned dictionary is guaranteed to have an entry for each element of `features`. + private static func createChannelsForFeatures(_ features: [RoomFeature], roomID: String, roomOptions _: RoomOptions, realtime: RealtimeClient) -> [RoomFeature: RealtimeChannelProtocol] { + // CHA-RC3a - // channel setup for presence and occupancy - if feature == .presence { - let channelOptions = ARTRealtimeChannelOptions() - let presenceOptions = roomOptions.presence + // Multiple features can share a realtime channel. We fetch each realtime channel exactly once, merging the channel options for the various features that use this channel. - if presenceOptions?.enter ?? false { - channelOptions.modes.insert(.presence) - } + let featuresGroupedByChannelName = Dictionary(grouping: features) { $0.channelNameForRoomID(roomID) } - if presenceOptions?.subscribe ?? false { - channelOptions.modes.insert(.presenceSubscribe) + let pairsOfFeatureAndChannel = featuresGroupedByChannelName.flatMap { channelName, features in + var channelOptions = RealtimeChannelOptions() + + // channel setup for presence and occupancy + for feature in features { + if feature == .presence { + // TODO: Restore this code once we understand weird Realtime behaviour and spec points (https://github.com/ably-labs/ably-chat-swift/issues/133) + /* + let presenceOptions = roomOptions.presence + + if presenceOptions?.enter ?? false { + channelOptions.modes.insert(.presence) + } + + if presenceOptions?.subscribe ?? false { + channelOptions.modes.insert(.presenceSubscribe) + } + */ + } else if feature == .occupancy { + var params: [String: String] = channelOptions.params ?? [:] + params["occupancy"] = "metrics" + channelOptions.params = params } - } else if feature == .occupancy { - channelOptions.params = ["occupancy": "metrics"] } - let channel = realtime.getChannel(feature.channelNameForRoomID(roomID), opts: channelOptions) - let contributor = DefaultRoomLifecycleContributor(channel: .init(underlyingChannel: channel), feature: feature) + let channel = realtime.getChannel(channelName, opts: channelOptions) + return features.map { ($0, channel) } + } + + return Dictionary(uniqueKeysWithValues: pairsOfFeatureAndChannel) + } + private static func createFeatureChannelPartialDependencies(roomID: String, roomOptions: RoomOptions, realtime: RealtimeClient) -> [RoomFeature: FeatureChannelPartialDependencies] { + let features: [RoomFeature] = [ + .messages, + .reactions, + .presence, + .occupancy, + ] + let channelsByFeature = createChannelsForFeatures(features, roomID: roomID, roomOptions: roomOptions, realtime: realtime) + + return .init(uniqueKeysWithValues: channelsByFeature.map { feature, channel in + let contributor = DefaultRoomLifecycleContributor(channel: .init(underlyingChannel: channel), feature: feature) return (feature, .init(channel: channel, contributor: contributor)) }) } diff --git a/Tests/AblyChatTests/DefaultRoomTests.swift b/Tests/AblyChatTests/DefaultRoomTests.swift index c3f85e84..ca89167e 100644 --- a/Tests/AblyChatTests/DefaultRoomTests.swift +++ b/Tests/AblyChatTests/DefaultRoomTests.swift @@ -23,6 +23,29 @@ struct DefaultRoomTests { #expect(channelsGetArguments.allSatisfy { $0.options.attachOnSubscribe == false }) } + // @spec CHA-RC3a + @Test + func fetchesEachChannelOnce() async throws { + // Given: A DefaultRoom instance, configured to use presence and occupancy + let channelsList = [ + MockRealtimeChannel(name: "basketball::$chat::$chatMessages", attachResult: .success), + MockRealtimeChannel(name: "basketball::$chat::$reactions", attachResult: .success), + ] + let channels = MockChannels(channels: channelsList) + let realtime = MockRealtime.create(channels: channels) + let roomOptions = RoomOptions(presence: PresenceOptions(), occupancy: OccupancyOptions()) + _ = try await DefaultRoom(realtime: realtime, chatAPI: ChatAPI(realtime: realtime), roomID: "basketball", options: roomOptions, logger: TestLogger(), lifecycleManagerFactory: MockRoomLifecycleManagerFactory()) + + // Then: It fetches the …$chatMessages channel (which is used by messages, presence, and occupancy) only once, and the options with which it does so are the result of merging the options used by the presence feature and those used by the occupancy feature + let channelsGetArguments = channels.getArguments + #expect(channelsGetArguments.map(\.name).sorted() == ["basketball::$chat::$chatMessages", "basketball::$chat::$reactions"]) + + let chatMessagesChannelGetOptions = try #require(channelsGetArguments.first { $0.name == "basketball::$chat::$chatMessages" }?.options) + #expect(chatMessagesChannelGetOptions.params?["occupancy"] == "metrics") + // TODO: Restore this code once we understand weird Realtime behaviour and spec points (https://github.com/ably-labs/ably-chat-swift/issues/133) +// #expect(chatMessagesChannelGetOptions.modes == [.presence, .presenceSubscribe]) + } + // MARK: - Features // @spec CHA-M1