-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add the ability to attach and detach a room
Based on the simplified requirements described in #19. This doesn’t include the emission of a room status change; will do that in a separate PR.
- Loading branch information
1 parent
0e9f703
commit c272c82
Showing
9 changed files
with
464 additions
and
9 deletions.
There are no files selected for viewing
30 changes: 30 additions & 0 deletions
30
Sources/AblyChat/AblyCocoaExtensions/Ably+Concurrency.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import Ably | ||
|
||
// This file contains extensions to ably-cocoa’s types, to make them easier to use in Swift concurrency. | ||
// TODO: remove once we improve this experience in ably-cocoa (https://github.com/ably/ably-cocoa/issues/1967) | ||
|
||
internal extension ARTRealtimeChannelProtocol { | ||
func attachAsync() async throws { | ||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in | ||
attach { error in | ||
if let error { | ||
continuation.resume(throwing: error) | ||
} else { | ||
continuation.resume() | ||
} | ||
} | ||
} | ||
} | ||
|
||
func detachAsync() async throws { | ||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in | ||
detach { error in | ||
if let error { | ||
continuation.resume(throwing: error) | ||
} else { | ||
continuation.resume() | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Ably | ||
|
||
// TODO: remove "@unchecked Sendable" once https://github.com/ably/ably-cocoa/issues/1962 done | ||
|
||
extension ARTRealtime: RealtimeClientProtocol, @unchecked Sendable {} | ||
|
||
extension ARTRealtimeChannels: RealtimeChannelsProtocol, @unchecked Sendable {} | ||
|
||
extension ARTRealtimeChannel: RealtimeChannelProtocol, @unchecked Sendable {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import Ably | ||
|
||
/// Expresses the requirements of the Ably realtime client that is supplied to the Chat SDK. | ||
/// | ||
/// The `ARTRealtime` class from the ably-cocoa SDK implements this protocol. | ||
public protocol RealtimeClientProtocol: ARTRealtimeProtocol, Sendable { | ||
associatedtype Channels: RealtimeChannelsProtocol | ||
|
||
// It’s not clear to me why ARTRealtimeProtocol doesn’t include this property. I briefly tried adding it but ran into compilation failures that it wasn’t immediately obvious how to fix. | ||
var channels: Channels { get } | ||
} | ||
|
||
/// Expresses the requirements of the object returned by ``RealtimeClientProtocol.channels``. | ||
public protocol RealtimeChannelsProtocol: ARTRealtimeChannelsProtocol, Sendable { | ||
associatedtype Channel: RealtimeChannelProtocol | ||
|
||
// It’s not clear to me why ARTRealtimeChannelsProtocol doesn’t include this property. | ||
func get(_ name: String) -> Channel | ||
} | ||
|
||
/// Expresses the requirements of the object returned by ``RealtimeChannelsProtocol.get(_:)``. | ||
public protocol RealtimeChannelProtocol: ARTRealtimeChannelProtocol, Sendable {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import Ably | ||
@testable import AblyChat | ||
import XCTest | ||
|
||
class DefaultRoomTests: XCTestCase { | ||
func test_attach_attachesAllChannels_andSucceedsIfAllSucceed() async throws { | ||
// Given: a DefaultRoom instance with ID "basketball", with a Realtime client for which `attach(_:)` completes successfully if called on the following channels: | ||
// | ||
// - basketball::$chat::$chatMessages | ||
// - basketball::$chat::$typingIndicators | ||
// - basketball::$chat::$reactions | ||
let channelsList = [ | ||
MockRealtimeChannel(name: "basketball::$chat::$chatMessages", attachResult: .success), | ||
MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", attachResult: .success), | ||
MockRealtimeChannel(name: "basketball::$chat::$reactions", attachResult: .success), | ||
] | ||
let channels = MockChannels(channels: channelsList) | ||
let realtime = MockRealtime.create(channels: channels) | ||
let room = DefaultRoom(realtime: realtime, roomID: "basketball", options: .init()) | ||
|
||
// 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 | ||
for channel in channelsList { | ||
XCTAssertTrue(channel.attachCallCounter.isNonZero) | ||
} | ||
} | ||
|
||
func test_attach_attachesAllChannels_andFailsIfOneFails() async throws { | ||
// Given: a DefaultRoom instance, with a Realtime client for which `attach(_:)` completes successfully if called on the following channels: | ||
// | ||
// - basketball::$chat::$chatMessages | ||
// - basketball::$chat::$typingIndicators | ||
// | ||
// and fails when called on channel basketball::$chat::$reactions | ||
let channelAttachError = ARTErrorInfo.createUnknownError() // arbitrary | ||
let channelsList = [ | ||
MockRealtimeChannel(name: "basketball::$chat::$chatMessages", attachResult: .success), | ||
MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", attachResult: .success), | ||
MockRealtimeChannel(name: "basketball::$chat::$reactions", attachResult: .failure(channelAttachError)), | ||
] | ||
let channels = MockChannels(channels: channelsList) | ||
let realtime = MockRealtime.create(channels: channels) | ||
let room = DefaultRoom(realtime: realtime, roomID: "basketball", options: .init()) | ||
|
||
// When: `attach` is called on the room | ||
let roomAttachError: Error? | ||
do { | ||
try await room.attach() | ||
roomAttachError = nil | ||
} catch { | ||
roomAttachError = error | ||
} | ||
|
||
// Then: the room `attach` call fails with the same error as the channel `attach(_:)` call | ||
let roomAttachErrorInfo = try XCTUnwrap(roomAttachError as? ARTErrorInfo) | ||
XCTAssertIdentical(roomAttachErrorInfo, channelAttachError) | ||
} | ||
|
||
func test_detach_detachesAllChannels_andSucceedsIfAllSucceed() async throws { | ||
// Given: a DefaultRoom instance with ID "basketball", with a Realtime client for which `detach(_:)` completes successfully if called on the following channels: | ||
// | ||
// - basketball::$chat::$chatMessages | ||
// - basketball::$chat::$typingIndicators | ||
// - basketball::$chat::$reactions | ||
let channelsList = [ | ||
MockRealtimeChannel(name: "basketball::$chat::$chatMessages", detachResult: .success), | ||
MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", detachResult: .success), | ||
MockRealtimeChannel(name: "basketball::$chat::$reactions", detachResult: .success), | ||
] | ||
let channels = MockChannels(channels: channelsList) | ||
let realtime = MockRealtime.create(channels: channels) | ||
let room = DefaultRoom(realtime: realtime, roomID: "basketball", options: .init()) | ||
|
||
// 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 | ||
for channel in channelsList { | ||
XCTAssertTrue(channel.detachCallCounter.isNonZero) | ||
} | ||
} | ||
|
||
func test_detach_detachesAllChannels_andFailsIfOneFails() async throws { | ||
// Given: a DefaultRoom instance, with a Realtime client for which `detach(_:)` completes successfully if called on the following channels: | ||
// | ||
// - basketball::$chat::$chatMessages | ||
// - basketball::$chat::$typingIndicators | ||
// | ||
// and fails when called on channel basketball::$chat::$reactions | ||
let channelDetachError = ARTErrorInfo.createUnknownError() // arbitrary | ||
let channelsList = [ | ||
MockRealtimeChannel(name: "basketball::$chat::$chatMessages", detachResult: .success), | ||
MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", detachResult: .success), | ||
MockRealtimeChannel(name: "basketball::$chat::$reactions", detachResult: .failure(channelDetachError)), | ||
] | ||
let channels = MockChannels(channels: channelsList) | ||
let realtime = MockRealtime.create(channels: channels) | ||
let room = DefaultRoom(realtime: realtime, roomID: "basketball", options: .init()) | ||
|
||
// When: `detach` is called on the room | ||
let roomDetachError: Error? | ||
do { | ||
try await room.detach() | ||
roomDetachError = nil | ||
} catch { | ||
roomDetachError = error | ||
} | ||
|
||
// Then: the room `detach` call fails with the same error as the channel `detach(_:)` call | ||
let roomDetachErrorInfo = try XCTUnwrap(roomDetachError as? ARTErrorInfo) | ||
XCTAssertIdentical(roomDetachErrorInfo, channelDetachError) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import Ably | ||
import AblyChat | ||
|
||
final class MockChannels: RealtimeChannelsProtocol, Sendable { | ||
private let channels: [MockRealtimeChannel] | ||
|
||
init(channels: [MockRealtimeChannel]) { | ||
self.channels = channels | ||
} | ||
|
||
func get(_ name: String) -> MockRealtimeChannel { | ||
guard let channel = (channels.first { $0.name == name }) else { | ||
fatalError("There is no mock channel with name \(name)") | ||
} | ||
|
||
return channel | ||
} | ||
|
||
func exists(_: String) -> Bool { | ||
fatalError("Not implemented") | ||
} | ||
|
||
func release(_: String, callback _: ARTCallback? = nil) { | ||
fatalError("Not implemented") | ||
} | ||
|
||
func release(_: String) { | ||
fatalError("Not implemented") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.