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

Implement CHA-RL5a1 #147

Merged
merged 1 commit into from
Nov 25, 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
43 changes: 24 additions & 19 deletions Sources/AblyChat/Room.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>
private nonisolated let realtime: RealtimeClient

private let lifecycleManager: any RoomLifecycleManager
private let channels: [RoomFeature: any RealtimeChannelProtocol]
private let channels: [any RealtimeChannelProtocol]

private let logger: InternalLogger

Expand All @@ -96,6 +96,7 @@ internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>
}
}

/// The features are returned in CHA-RC2e order.
static func fromRoomOptions(_ roomOptions: RoomOptions) -> [Self] {
var result: [Self] = [.messages]

Expand Down Expand Up @@ -133,8 +134,8 @@ internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>
let featuresWithOptions = RoomFeatureWithOptions.fromRoomOptions(options)

let featureChannelPartialDependencies = Self.createFeatureChannelPartialDependencies(roomID: roomID, featuresWithOptions: featuresWithOptions, realtime: realtime)
channels = featureChannelPartialDependencies.mapValues(\.channel)
let contributors = featureChannelPartialDependencies.values.map(\.contributor)
channels = featureChannelPartialDependencies.map(\.featureChannelPartialDependencies.channel)
let contributors = featureChannelPartialDependencies.map(\.featureChannelPartialDependencies.contributor)

lifecycleManager = await lifecycleManagerFactory.createManager(
contributors: contributors,
Expand Down Expand Up @@ -202,15 +203,19 @@ internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>
internal var contributor: DefaultRoomLifecycleContributor
}

/// The returned dictionary is guaranteed to have an entry for each element of `features`.
private static func createChannelsForFeaturesWithOptions(_ featuresWithOptions: [RoomFeatureWithOptions], roomID: String, realtime: RealtimeClient) -> [RoomFeature: RealtimeChannelProtocol] {
/// 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

// 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.

// CHA-RL5a1: This spec point requires us to implement a special behaviour to handle the fact that multiple contributors can share a channel. I have decided, instead, to make it so that each channel has precisely one lifecycle contributor. I think this is a simpler, functionally equivalent approach and have suggested it in https://github.com/ably/specification/issues/240.

let featuresGroupedByChannelName = Dictionary(grouping: featuresWithOptions) { $0.toRoomFeature.channelNameForRoomID(roomID) }

let pairsOfFeatureAndChannel = featuresGroupedByChannelName.flatMap { channelName, features in
return featuresGroupedByChannelName.map { channelName, features in
var channelOptions = RealtimeChannelOptions()

// channel setup for presence and occupancy
Expand All @@ -234,23 +239,23 @@ internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>
}

let channel = realtime.getChannel(channelName, opts: channelOptions)
return features.map { ($0.toRoomFeature, channel) }
}

return Dictionary(uniqueKeysWithValues: pairsOfFeatureAndChannel)
}
// 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
lawrence-forooghian marked this conversation as resolved.
Show resolved Hide resolved

private static func createFeatureChannelPartialDependencies(roomID: String, featuresWithOptions: [RoomFeatureWithOptions], realtime: RealtimeClient) -> [RoomFeature: FeatureChannelPartialDependencies] {
let channelsByFeature = createChannelsForFeaturesWithOptions(featuresWithOptions, roomID: roomID, realtime: realtime)
let contributor = DefaultRoomLifecycleContributor(channel: .init(underlyingChannel: channel), feature: contributorFeature)
let featureChannelPartialDependencies = FeatureChannelPartialDependencies(channel: channel, contributor: contributor)

return .init(uniqueKeysWithValues: channelsByFeature.map { feature, channel in
let contributor = DefaultRoomLifecycleContributor(channel: .init(underlyingChannel: channel), feature: feature)
return (feature, .init(channel: channel, contributor: contributor))
})
return (features.map(\.toRoomFeature), featureChannelPartialDependencies)
}
}

private static func createFeatureChannels(partialDependencies: [RoomFeature: FeatureChannelPartialDependencies], lifecycleManager: RoomLifecycleManager) -> [RoomFeature: DefaultFeatureChannel] {
partialDependencies.mapValues { partialDependencies in
private static func createFeatureChannels(partialDependencies: [(features: [RoomFeature], featureChannelPartialDependencies: FeatureChannelPartialDependencies)], lifecycleManager: RoomLifecycleManager) -> [RoomFeature: DefaultFeatureChannel] {
let pairsOfFeatureAndPartialDependencies = partialDependencies.flatMap { features, partialDependencies in
features.map { (feature: $0, partialDependencies: partialDependencies) }
lawrence-forooghian marked this conversation as resolved.
Show resolved Hide resolved
}

return Dictionary(uniqueKeysWithValues: pairsOfFeatureAndPartialDependencies).mapValues { partialDependencies in
lawrence-forooghian marked this conversation as resolved.
Show resolved Hide resolved
.init(
channel: partialDependencies.channel,
contributor: partialDependencies.contributor,
Expand Down Expand Up @@ -299,7 +304,7 @@ internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>
await lifecycleManager.performReleaseOperation()

// CHA-RL3h
for channel in channels.values {
for channel in channels {
realtime.channels.release(channel.name)
}
}
Expand Down
4 changes: 3 additions & 1 deletion Tests/AblyChatTests/DefaultRoomTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ struct DefaultRoomTests {
// @spec CHA-RC2c
// @spec CHA-RC2d
// @spec CHA-RC2f
// @spec CHA-RL5a1 - We implement this spec point by _not allowing multiple contributors to share a channel_; this is an approach that I’ve suggested in https://github.com/ably/specification/issues/240.
@Test
func fetchesChannelAndCreatesLifecycleContributorForEnabledFeatures() async throws {
// Given: a DefaultRoom instance, initialized with options that request that the room use a strict subset of the possible features
Expand All @@ -83,9 +84,10 @@ struct DefaultRoomTests {

// Then: It:
// - fetches the channel that corresponds to each feature requested by the room options, plus the messages feature
// - initializes the RoomLifecycleManager with a contributor for each fetched channel, and the feature assigned to each contributor is the feature, of the enabled features that correspond to that channel, which appears first in the CHA-RC2e list
// - initializes the RoomLifecycleManager with a contributor for each feature requested by the room options, plus the messages feature
let lifecycleManagerCreationArguments = try #require(await lifecycleManagerFactory.createManagerArguments.first)
let expectedFeatures: [RoomFeature] = [.messages, .presence, .reactions]
let expectedFeatures: [RoomFeature] = [.messages, .reactions] // i.e. since messages and presence share a channel, we create a single contributor for this channel and its assigned feature is messages
#expect(lifecycleManagerCreationArguments.contributors.count == expectedFeatures.count)
#expect(Set(lifecycleManagerCreationArguments.contributors.map(\.feature)) == Set(expectedFeatures))

Expand Down