Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECO-5128] Fetch each channel once #134

Merged
merged 3 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 44 additions & 21 deletions Sources/AblyChat/Room.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,34 +131,57 @@ internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>
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))
})
}
Expand Down
23 changes: 23 additions & 0 deletions Tests/AblyChatTests/DefaultRoomTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down