-
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.
Implement the ability to fetch a room
Part of #19. References to spec are based on [1] at commit aa7455d. The @preconcurrency imports of ably-cocoa are temporary and will be removed once [2] is done; created #31 for tracking. I’ve decided to, for now, throw ably-cocoa’s ARTErrorInfo for consistency with JS; created #32 to revisit this later. [1] ably/specification#200 [2] ably/ably-cocoa#1962
- Loading branch information
1 parent
bc06747
commit a1e16c6
Showing
8 changed files
with
268 additions
and
6 deletions.
There are no files selected for viewing
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,67 @@ | ||
import Ably | ||
|
||
/** | ||
The error domain used for the ``Ably.ARTErrorInfo`` error instances thrown by the Ably Chat SDK. | ||
See ``ErrorCode`` for the possible ``ARTErrorInfo.code`` values. | ||
*/ | ||
public let errorDomain: NSString = "AblyChatErrorDomain" | ||
|
||
/** | ||
The error codes for errors in the ``errorDomain`` error domain. | ||
*/ | ||
public enum ErrorCode: Int { | ||
/// ``Rooms.get(roomID:options:)`` was called with a different set of room options than was used on a previous call. You must first release the existing room instance using ``Rooms.release(roomID:)``. | ||
/// | ||
/// TODO this code is a guess, revisit in https://github.com/ably-labs/ably-chat-swift/issues/32 | ||
case inconsistentRoomOptions = 1 | ||
|
||
/// The ``ARTErrorInfo.statusCode`` that should be returned for this error. | ||
internal var statusCode: Int { | ||
// TODO: These are currently a guess, revisit in https://github.com/ably-labs/ably-chat-swift/issues/32 | ||
switch self { | ||
case .inconsistentRoomOptions: | ||
400 | ||
} | ||
} | ||
} | ||
|
||
/** | ||
The errors thrown by the Chat SDK. | ||
This type exists in addition to ``ErrorCode`` to allow us to attach metadata which can be incorporated into the error’s `localizedDescription`. | ||
*/ | ||
internal enum ChatError { | ||
case inconsistentRoomOptions(requested: RoomOptions, existing: RoomOptions) | ||
|
||
/// The ``ARTErrorInfo.code`` that should be returned for this error. | ||
internal var code: ErrorCode { | ||
switch self { | ||
case .inconsistentRoomOptions: | ||
.inconsistentRoomOptions | ||
} | ||
} | ||
|
||
/// The ``ARTErrorInfo.localizedDescription`` that should be returned for this error. | ||
internal var localizedDescription: String { | ||
switch self { | ||
case let .inconsistentRoomOptions(requested, existing): | ||
"Rooms.get(roomID:options:) was called with a different set of room options than was used on a previous call. You must first release the existing room instance using Rooms.release(roomID:). Requested options: \(requested), existing options: \(existing)" | ||
} | ||
} | ||
} | ||
|
||
internal extension ARTErrorInfo { | ||
convenience init(chatError: ChatError) { | ||
var userInfo: [String: Any] = [:] | ||
// TODO: copied and pasted from implementation of -[ARTErrorInfo createWithCode:status:message:requestId:] because there’s no way to pass domain; revisit in https://github.com/ably-labs/ably-chat-swift/issues/32. Also the ARTErrorInfoStatusCode variable in ably-cocoa is not public. | ||
userInfo["ARTErrorInfoStatusCode"] = chatError.code.statusCode | ||
userInfo[NSLocalizedDescriptionKey] = chatError.localizedDescription | ||
|
||
self.init( | ||
domain: errorDomain as String, | ||
code: chatError.code.rawValue, | ||
userInfo: userInfo | ||
) | ||
} | ||
} |
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
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 |
---|---|---|
@@ -1,5 +1,64 @@ | ||
@preconcurrency import Ably | ||
|
||
public protocol Rooms: AnyObject, Sendable { | ||
func get(roomID: String, options: RoomOptions) throws -> any Room | ||
func release(roomID: String) async throws | ||
var clientOptions: ClientOptions { get } | ||
} | ||
|
||
internal final class DefaultRooms: Rooms { | ||
private let realtime: ARTRealtimeProtocol | ||
private let storage = Storage() | ||
|
||
internal init(realtime: ARTRealtimeProtocol) { | ||
self.realtime = realtime | ||
} | ||
|
||
/// Thread-safe storage of the set of rooms. | ||
private class Storage: @unchecked Sendable { | ||
/// Mutex used to synchronize access to ``rooms``. | ||
private let lock = NSLock() | ||
|
||
/// The set of rooms, keyed by room ID. Only access whilst holding ``lock``. | ||
private var rooms: [String: DefaultRoom] = [:] | ||
|
||
/// If there is an existing room with the given ID, returns it. Else creates a new room with the given ID and options. | ||
public func getOrCreate(realtime: any ARTRealtimeProtocol, roomID: String, options: RoomOptions) -> DefaultRoom { | ||
let room: DefaultRoom | ||
lock.lock() | ||
// CHA-RC1b | ||
if let existingRoom = rooms[roomID] { | ||
room = existingRoom | ||
} else { | ||
room = DefaultRoom(realtime: realtime, roomID: roomID, options: options) | ||
rooms[roomID] = room | ||
} | ||
lock.unlock() | ||
return room | ||
} | ||
} | ||
|
||
internal func get(roomID: String, options: RoomOptions) throws -> any Room { | ||
let room = storage.getOrCreate( | ||
realtime: realtime, | ||
roomID: roomID, | ||
options: options | ||
) | ||
|
||
if room.options != options { | ||
throw ARTErrorInfo( | ||
chatError: .inconsistentRoomOptions(requested: options, existing: room.options) | ||
) | ||
} | ||
|
||
return room | ||
} | ||
|
||
internal func release(roomID _: String) async throws { | ||
fatalError("Not yet implemented") | ||
} | ||
|
||
internal var clientOptions: ClientOptions { | ||
fatalError("Not yet 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
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,64 @@ | ||
@testable import AblyChat | ||
import XCTest | ||
|
||
class DefaultRoomsTests: XCTestCase { | ||
// @spec CHA-RC1a | ||
func test_get_returnsRoomWithGivenID() throws { | ||
// Given: an instance of DefaultRooms | ||
let realtime = MockRealtime.create() | ||
let rooms = DefaultRooms(realtime: realtime) | ||
|
||
// When: get(roomID:options:) is called | ||
let roomID = "basketball" | ||
let options = RoomOptions() | ||
let room = try rooms.get(roomID: roomID, options: options) | ||
|
||
// Then: It returns a DefaultRoom instance that uses the same Realtime instance, with the given ID and options | ||
let defaultRoom = try XCTUnwrap(room as? DefaultRoom) | ||
XCTAssertIdentical(defaultRoom.realtime, realtime) | ||
XCTAssertEqual(defaultRoom.roomID, roomID) | ||
XCTAssertEqual(defaultRoom.options, options) | ||
} | ||
|
||
// @spec CHA-RC1b | ||
func test_get_returnsExistingRoomWithGivenID() throws { | ||
// Given: an instance of DefaultRooms, on which get(roomID:options:) has already been called with a given ID | ||
let realtime = MockRealtime.create() | ||
let rooms = DefaultRooms(realtime: realtime) | ||
|
||
let roomID = "basketball" | ||
let options = RoomOptions() | ||
let firstRoom = try rooms.get(roomID: roomID, options: options) | ||
|
||
// When: get(roomID:options:) is called with the same room ID | ||
let secondRoom = try rooms.get(roomID: roomID, options: options) | ||
|
||
// Then: It returns the same room object | ||
XCTAssertIdentical(secondRoom, firstRoom) | ||
} | ||
|
||
// @spec CHA-RC1c | ||
func test_get_throwsErrorWhenOptionsDoNotMatch() throws { | ||
// Given: an instance of DefaultRooms, on which get(roomID:options:) has already been called with a given ID and options | ||
let realtime = MockRealtime.create() | ||
let rooms = DefaultRooms(realtime: realtime) | ||
|
||
let roomID = "basketball" | ||
let options = RoomOptions() | ||
_ = try rooms.get(roomID: roomID, options: options) | ||
|
||
// When: get(roomID:options:) is called with the same ID but different options | ||
let differentOptions = RoomOptions(presence: .init(subscribe: false)) | ||
|
||
let caughtError: Error? | ||
do { | ||
_ = try rooms.get(roomID: roomID, options: differentOptions) | ||
caughtError = nil | ||
} catch { | ||
caughtError = error | ||
} | ||
|
||
// Then: It throws an inconsistentRoomOptions error | ||
try assertIsChatError(caughtError, withCode: .inconsistentRoomOptions) | ||
} | ||
} |
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,15 @@ | ||
import Ably | ||
@testable import AblyChat | ||
import XCTest | ||
|
||
/** | ||
Asserts that a given optional `Error` is an `ARTErrorInfo` in the chat error domain with a given code. | ||
*/ | ||
func assertIsChatError(_ maybeError: (any Error)?, withCode code: AblyChat.ErrorCode, file: StaticString = #filePath, line: UInt = #line) throws { | ||
let error = try XCTUnwrap(maybeError, "Expected an error", file: file, line: line) | ||
let ablyError = try XCTUnwrap(error as? ARTErrorInfo, "Expected an ARTErrorInfo", file: file, line: line) | ||
|
||
XCTAssertEqual(ablyError.domain, AblyChat.errorDomain as String, file: file, line: line) | ||
XCTAssertEqual(ablyError.code, code.rawValue, file: file, line: line) | ||
XCTAssertEqual(ablyError.statusCode, code.statusCode, file: file, line: line) | ||
} |
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