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 room status change upon attach or detach #37

Merged
merged 1 commit into from
Sep 5, 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
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> {
maratal marked this conversation as resolved.
Show resolved Hide resolved
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