Skip to content

Commit

Permalink
Implement CHA-RL1g2
Browse files Browse the repository at this point in the history
Resolves #53.
  • Loading branch information
lawrence-forooghian committed Sep 30, 2024
1 parent 006b333 commit 1bd5475
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 1 deletion.
31 changes: 30 additions & 1 deletion Sources/AblyChat/RoomLifecycleManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ internal actor RoomLifecycleManager<Channel: RoomLifecycleContributorChannel> {
await self.init(
current: nil,
hasOperationInProgress: nil,
pendingDiscontinuityEvents: nil,
contributors: contributors,
logger: logger,
clock: clock
Expand All @@ -70,13 +71,16 @@ internal actor RoomLifecycleManager<Channel: RoomLifecycleContributorChannel> {
internal init(
testsOnly_current current: RoomLifecycle? = nil,
testsOnly_hasOperationInProgress hasOperationInProgress: Bool? = nil,
// In the same order as `contributors`
testsOnly_pendingDiscontinuityEvents pendingDiscontinuityEvents: [[PendingDiscontinuityEvent]]? = nil,
contributors: [Contributor],
logger: InternalLogger,
clock: SimpleClock
) async {
await self.init(
current: current,
hasOperationInProgress: hasOperationInProgress,
pendingDiscontinuityEvents: pendingDiscontinuityEvents,
contributors: contributors,
logger: logger,
clock: clock
Expand All @@ -87,14 +91,19 @@ internal actor RoomLifecycleManager<Channel: RoomLifecycleContributorChannel> {
private init(
current: RoomLifecycle?,
hasOperationInProgress: Bool?,
pendingDiscontinuityEvents: [[PendingDiscontinuityEvent]]?,
contributors: [Contributor],
logger: InternalLogger,
clock: SimpleClock
) async {
self.current = current ?? .initialized
self.hasOperationInProgress = hasOperationInProgress ?? false
if let pendingDiscontinuityEvents {
contributorAnnotations = pendingDiscontinuityEvents.map { .init(pendingDiscontinuityEvents: $0) }
} else {
contributorAnnotations = Array(repeating: .init(), count: contributors.count)
}
self.contributors = contributors
contributorAnnotations = Array(repeating: .init(), count: contributors.count)
self.logger = logger
self.clock = clock

Expand Down Expand Up @@ -317,6 +326,26 @@ internal actor RoomLifecycleManager<Channel: RoomLifecycleContributorChannel> {

// CHA-RL1g1
changeStatus(to: .attached)

// CHA-RL1g2
await emitPendingDiscontinuityEvents()
}

/// Implements CHA-RL1g2’s emitting of pending discontinuity events.
private func emitPendingDiscontinuityEvents() async {
// Emit all pending discontinuity events
logger.log(message: "Emitting pending discontinuity events", level: .info)
for (contributor, annotation) in zip(contributors, contributorAnnotations) {
for pendingDiscontinuityEvent in annotation.pendingDiscontinuityEvents {
logger.log(message: "Emitting pending discontinuity event \(pendingDiscontinuityEvent) to contributor \(contributor)", level: .info)
await contributor.channel.emitDiscontinuity(pendingDiscontinuityEvent.error)
}
}

// Clear all pending discontinuity events
for i in contributorAnnotations.indices {
contributorAnnotations[i].pendingDiscontinuityEvents = []
}
}

/// Implements CHA-RL1h5’s "detach all channels that are not in the FAILED state".
Expand Down
36 changes: 36 additions & 0 deletions Tests/AblyChatTests/RoomLifecycleManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ struct RoomLifecycleManagerTests {
private func createManager(
forTestingWhatHappensWhenCurrentlyIn current: RoomLifecycle? = nil,
forTestingWhatHappensWhenHasOperationInProgress hasOperationInProgress: Bool? = nil,
forTestingWhatHappensWhenHasPendingDiscontinuityEvents pendingDiscontinuityEvents: [[RoomLifecycleManager<MockRoomLifecycleContributorChannel>.PendingDiscontinuityEvent]]? = nil,
contributors: [RoomLifecycleManager<MockRoomLifecycleContributorChannel>.Contributor] = [],
clock: SimpleClock = MockSimpleClock()
) async -> RoomLifecycleManager<MockRoomLifecycleContributorChannel> {
await .init(
testsOnly_current: current,
testsOnly_hasOperationInProgress: hasOperationInProgress,
testsOnly_pendingDiscontinuityEvents: pendingDiscontinuityEvents,
contributors: contributors,
logger: TestLogger(),
clock: clock
Expand Down Expand Up @@ -167,6 +169,40 @@ struct RoomLifecycleManagerTests {
try #require(await manager.current == .attached)
}

// @spec CHA-RL1g2
@Test
func attach_uponSuccess_emitsPendingDiscontinuityEvents() async throws {
// Given: A RoomLifecycleManager, all of whose contributors’ calls to `attach` succeed
let contributors = (1 ... 3).map { _ in createContributor(attachBehavior: .complete(.success)) }
let pendingDiscontinuityEvents: [[RoomLifecycleManager<MockRoomLifecycleContributorChannel>.PendingDiscontinuityEvent]] = [
[],
[.init(error: .init(domain: "SomeDomain", code: 123) /* arbitrary */ )],
[.init(error: .init(domain: "SomeDomain", code: 456) /* arbitrary */ )],
]
let manager = await createManager(
forTestingWhatHappensWhenHasPendingDiscontinuityEvents: pendingDiscontinuityEvents,
contributors: contributors
)

// When: `performAttachOperation()` is called on the lifecycle manager
try await manager.performAttachOperation()

// Then: It:
// - emits all pending discontinuities to its contributors
// - clears all pending discontinuity events (TODO: I assume this is the intended behaviour, but confirm; have asked in https://github.com/ably/specification/pull/200/files#r1781917231)
for (contributor, expectedPendingDiscontinuityEvents) in zip(contributors, pendingDiscontinuityEvents) {
let emitDiscontinuityArguments = await contributor.channel.emitDiscontinuityArguments
try #require(emitDiscontinuityArguments.count == expectedPendingDiscontinuityEvents.count)
for (emitDiscontinuityArgument, expectedArgument) in zip(emitDiscontinuityArguments, expectedPendingDiscontinuityEvents.map(\.error)) {
#expect(emitDiscontinuityArgument === expectedArgument)
}
}

for i in contributors.indices {
#expect(await manager.testsOnly_pendingDiscontinuityEventsForContributor(at: i).isEmpty)
}
}

// @spec CHA-RL1h2
// @specOneOf(1/2) CHA-RL1h1 - tests that an error gets thrown when channel attach fails due to entering SUSPENDED (TODO: but I don’t yet fully understand the meaning of CHA-RL1h1; outstanding question https://github.com/ably/specification/pull/200/files#r1765476610)
// @specPartial CHA-RL1h3 - Have tested the failure of the operation and the error that’s thrown. Have not yet implemented the "enter the recovery loop" (TODO: https://github.com/ably-labs/ably-chat-swift/issues/50)
Expand Down

0 comments on commit 1bd5475

Please sign in to comment.