Skip to content

Commit

Permalink
Merge pull request #37 from ably-labs/19-emit-room-status-update
Browse files Browse the repository at this point in the history
Implement room status change upon attach or detach
  • Loading branch information
lawrence-forooghian authored Sep 5, 2024
2 parents 8ad7d8d + a63b22b commit 3b7a54e
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 7 deletions.
8 changes: 6 additions & 2 deletions Sources/AblyChat/Room.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ internal actor DefaultRoom: Room {
// Exposed for testing.
internal nonisolated let realtime: RealtimeClient

private let _status = DefaultRoomStatus()

internal init(realtime: RealtimeClient, roomID: String, options: RoomOptions) {
self.realtime = realtime
self.roomID = roomID
Expand All @@ -50,8 +52,8 @@ internal actor DefaultRoom: Room {
fatalError("Not yet implemented")
}

public nonisolated var status: any RoomStatus {
fatalError("Not yet implemented")
internal nonisolated var status: any RoomStatus {
_status
}

/// Fetches the channels that contribute to this room.
Expand All @@ -69,11 +71,13 @@ internal actor DefaultRoom: Room {
for channel in channels() {
try await channel.attachAsync()
}
await _status.transition(to: .attached)
}

public func detach() async throws {
for channel in channels() {
try await channel.detachAsync()
}
await _status.transition(to: .detached)
}
}
30 changes: 27 additions & 3 deletions Sources/AblyChat/RoomStatus.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Ably

public protocol RoomStatus: AnyObject, Sendable {
var current: RoomLifecycle { get }
var current: RoomLifecycle { get async }
// TODO: (https://github.com/ably-labs/ably-chat-swift/issues/12): consider how to avoid the need for an unwrap
var error: ARTErrorInfo? { get }
func onChange(bufferingPolicy: BufferingPolicy) -> Subscription<RoomStatusChange>
var error: ARTErrorInfo? { get async }
func onChange(bufferingPolicy: BufferingPolicy) async -> Subscription<RoomStatusChange>
}

public enum RoomLifecycle: Sendable {
Expand All @@ -31,3 +31,27 @@ public struct RoomStatusChange: Sendable {
self.error = error
}
}

internal actor DefaultRoomStatus: RoomStatus {
internal private(set) var current: RoomLifecycle = .initialized
// TODO: populate this (https://github.com/ably-labs/ably-chat-swift/issues/28)
internal private(set) var error: ARTErrorInfo?

// TODO: clean up old subscriptions (https://github.com/ably-labs/ably-chat-swift/issues/36)
private var subscriptions: [Subscription<RoomStatusChange>] = []

internal func onChange(bufferingPolicy: BufferingPolicy) -> Subscription<RoomStatusChange> {
let subscription: Subscription<RoomStatusChange> = .init(bufferingPolicy: bufferingPolicy)
subscriptions.append(subscription)
return subscription
}

/// Sets ``current`` to the given state, and emits a status change to all subscribers added via ``onChange(bufferingPolicy:)``.
internal func transition(to newState: RoomLifecycle) {
let statusChange = RoomStatusChange(current: newState, previous: current)
current = newState
for subscription in subscriptions {
subscription.emit(statusChange)
}
}
}
46 changes: 46 additions & 0 deletions Tests/AblyChatTests/DefaultRoomStatusTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@testable import AblyChat
import XCTest

class DefaultRoomStatusTests: XCTestCase {
func test_current_startsAsInitialized() async {
let status = DefaultRoomStatus()
let current = await status.current
XCTAssertEqual(current, .initialized)
}

func test_error_startsAsNil() async {
let status = DefaultRoomStatus()
let error = await status.error
XCTAssertNil(error)
}

func test_transition() async {
// Given: A RoomStatus
let status = DefaultRoomStatus()
let originalState = await status.current
let newState = RoomLifecycle.attached // arbitrary

let subscription1 = await status.onChange(bufferingPolicy: .unbounded)
let subscription2 = await status.onChange(bufferingPolicy: .unbounded)

async let statusChange1 = subscription1.first { $0.current == newState }
async let statusChange2 = subscription2.first { $0.current == newState }

// When: transition(to:) is called
await status.transition(to: newState)

// Then: It emits a status change to all subscribers added via onChange(bufferingPolicy:), and updates its `current` property to the new state
guard let statusChange1 = await statusChange1, let statusChange2 = await statusChange2 else {
XCTFail("Expected status changes to be emitted")
return
}

for statusChange in [statusChange1, statusChange2] {
XCTAssertEqual(statusChange.previous, originalState)
XCTAssertEqual(statusChange.current, newState)
}

let current = await status.current
XCTAssertEqual(current, .attached)
}
}
26 changes: 24 additions & 2 deletions Tests/AblyChatTests/DefaultRoomTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,24 @@ class DefaultRoomTests: XCTestCase {
let realtime = MockRealtime.create(channels: channels)
let room = DefaultRoom(realtime: realtime, roomID: "basketball", options: .init())

let subscription = await room.status.onChange(bufferingPolicy: .unbounded)
async let attachedStatusChange = subscription.first { $0.current == .attached }

// When: `attach` is called on the room
try await room.attach()

// Then: `attach(_:)` is called on each of the channels, and the room `attach` call succeeds
// Then: `attach(_:)` is called on each of the channels, the room `attach` call succeeds, and the room transitions to ATTACHED
for channel in channelsList {
XCTAssertTrue(channel.attachCallCounter.isNonZero)
}

guard let attachedStatusChange = await attachedStatusChange else {
XCTFail("Expected status change to ATTACHED but didn't get one")
return
}
let currentStatus = await room.status.current
XCTAssertEqual(currentStatus, .attached)
XCTAssertEqual(attachedStatusChange.current, .attached)
}

func test_attach_attachesAllChannels_andFailsIfOneFails() async throws {
Expand Down Expand Up @@ -73,13 +84,24 @@ class DefaultRoomTests: XCTestCase {
let realtime = MockRealtime.create(channels: channels)
let room = DefaultRoom(realtime: realtime, roomID: "basketball", options: .init())

let subscription = await room.status.onChange(bufferingPolicy: .unbounded)
async let detachedStatusChange = subscription.first { $0.current == .detached }

// When: `detach` is called on the room
try await room.detach()

// Then: `detach(_:)` is called on each of the channels, and the room `detach` call succeeds
// Then: `detach(_:)` is called on each of the channels, the room `detach` call succeeds, and the room transitions to DETACHED
for channel in channelsList {
XCTAssertTrue(channel.detachCallCounter.isNonZero)
}

guard let detachedStatusChange = await detachedStatusChange else {
XCTFail("Expected status change to DETACHED but didn't get one")
return
}
let currentStatus = await room.status.current
XCTAssertEqual(currentStatus, .detached)
XCTAssertEqual(detachedStatusChange.current, .detached)
}

func test_detach_detachesAllChannels_andFailsIfOneFails() async throws {
Expand Down

0 comments on commit 3b7a54e

Please sign in to comment.