Skip to content
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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ To check formatting and code quality, run `swift run BuildTool lint`. Run with `
- When defining a `struct` that is emitted by the public API of the library, make sure to define a public memberwise initializer so that users can create one to be emitted by their mocks. (There is no way to make Swift’s autogenerated memberwise initializer public, so you will need to write one yourself. In Xcode, you can do this by clicking at the start of the type declaration and doing Editor → Refactor → Generate Memberwise Initializer.)
- When writing code that implements behaviour specified by the Chat SDK features spec, add a comment that references the identifier of the relevant spec item.
- The SDK isolates all of its mutable state to the main actor. Stateful objects should be marked as `@MainActor`.
- Avoid the usage of existential types (`any FooProtocol`) in the public API; favour protocol associated types combined with opaque types (`some FooProtocol`).
- There are exceptions; for example, sometimes you will have to choose between making a concrete type generic or using an existential type, and in some of these situations the existential type will be more appropriate. One such example is `ChatClientOptions`'s `logHandler` property.

### Throwing errors

Expand Down
6 changes: 3 additions & 3 deletions Example/AblyChatExample/Mocks/Misc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ final class MockMessagesPaginatedResult: PaginatedResult {

var isLast: Bool { fatalError("Not implemented") }

var next: (any PaginatedResult<Item>)? { fatalError("Not implemented") }
var next: Self? { fatalError("Not implemented") }

var first: any PaginatedResult<Item> { fatalError("Not implemented") }
var first: Self { fatalError("Not implemented") }

var current: any PaginatedResult<Item> { fatalError("Not implemented") }
var current: Self { fatalError("Not implemented") }

init(clientID: String, roomName: String, numberOfMockMessages: Int = 3) {
self.clientID = clientID
Expand Down
53 changes: 24 additions & 29 deletions Example/AblyChatExample/Mocks/MockClients.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class MockChatClient: ChatClient {
let realtime = Realtime()
nonisolated let clientOptions: ChatClientOptions
nonisolated let rooms: MockRooms
nonisolated let connection: Connection
nonisolated let connection: MockConnection

init(clientOptions: ChatClientOptions?) {
self.clientOptions = clientOptions ?? .init()
Expand Down Expand Up @@ -53,11 +53,11 @@ class MockRoom: Room {

nonisolated let name: String
nonisolated let options: RoomOptions
nonisolated let messages: any Messages
nonisolated let presence: any Presence
nonisolated let reactions: any RoomReactions
nonisolated let typing: any Typing
nonisolated let occupancy: any Occupancy
nonisolated let messages: MockMessages
nonisolated let presence: MockPresence
nonisolated let reactions: MockRoomReactions
nonisolated let typing: MockTyping
nonisolated let occupancy: MockOccupancy

let channel = Channel()

Expand Down Expand Up @@ -88,7 +88,7 @@ class MockRoom: Room {
}

@discardableResult
func onStatusChange(_ callback: @escaping @MainActor (RoomStatusChange) -> Void) -> StatusSubscriptionProtocol {
func onStatusChange(_ callback: @escaping @MainActor (RoomStatusChange) -> Void) -> DefaultStatusSubscription {
var needNext = true
periodic(with: randomStatusInterval) { [weak self] in
guard let self else {
Expand All @@ -99,13 +99,13 @@ class MockRoom: Room {
}
return needNext
}
return StatusSubscription {
return DefaultStatusSubscription {
needNext = false
}
}

@discardableResult
func onDiscontinuity(_: @escaping @MainActor (DiscontinuityEvent) -> Void) -> StatusSubscriptionProtocol {
func onDiscontinuity(_: @escaping @MainActor (DiscontinuityEvent) -> Void) -> DefaultStatusSubscription {
fatalError("Not yet implemented")
}
}
Expand All @@ -114,22 +114,17 @@ class MockMessages: Messages {
let clientID: String
let roomName: String

var reactions: any MessageReactions
var reactions: MockMessageReactions

var mockReactions: MockMessageReactions {
// swiftlint:disable:next force_cast
reactions as! MockMessageReactions
}

private let mockSubscriptions = MockMessageSubscriptionStorage<ChatMessageEvent>()
private let mockSubscriptions = MockMessageSubscriptionStorage<ChatMessageEvent, MockMessagesPaginatedResult>()

init(clientID: String, roomName: String) {
self.clientID = clientID
self.roomName = roomName
reactions = MockMessageReactions(clientID: clientID, roomName: roomName)
}

func subscribe(_ callback: @escaping @MainActor (ChatMessageEvent) -> Void) -> MessageSubscriptionResponseProtocol {
func subscribe(_ callback: @escaping @MainActor (ChatMessageEvent) -> Void) -> some MessageSubscriptionResponseProtocol {
mockSubscriptions.create(
randomElement: {
let message = Message(
Expand All @@ -146,9 +141,9 @@ class MockMessages: Messages {
timestamp: Date(),
)
if byChance(30) { /* 30% of the messages will get the reaction */
self.mockReactions.messageSerials.append(message.serial)
self.reactions.messageSerials.append(message.serial)
}
self.mockReactions.clientIDs.insert(message.clientID)
self.reactions.clientIDs.insert(message.clientID)
return ChatMessageEvent(message: message)
},
previousMessages: { _ in
Expand All @@ -159,7 +154,7 @@ class MockMessages: Messages {
)
}

func history(options _: QueryOptions) async throws(ARTErrorInfo) -> any PaginatedResult<Message> {
func history(options _: QueryOptions) async throws(ARTErrorInfo) -> some PaginatedResult<Message> {
MockMessagesPaginatedResult(clientID: clientID, roomName: roomName)
}

Expand Down Expand Up @@ -282,7 +277,7 @@ class MockMessageReactions: MessageReactions {
)
}

func subscribe(_ callback: @escaping @MainActor @Sendable (MessageReactionSummaryEvent) -> Void) -> SubscriptionProtocol {
func subscribe(_ callback: @escaping @MainActor @Sendable (MessageReactionSummaryEvent) -> Void) -> MockSubscription {
mockSubscriptions.create(
randomElement: {
guard let senderClientID = self.clientIDs.randomElement(), let messageSerial = self.messageSerials.randomElement() else {
Expand All @@ -308,7 +303,7 @@ class MockMessageReactions: MessageReactions {
)
}

func subscribeRaw(_: @escaping @MainActor @Sendable (MessageReactionRawEvent) -> Void) -> SubscriptionProtocol {
func subscribeRaw(_: @escaping @MainActor @Sendable (MessageReactionRawEvent) -> Void) -> MockSubscription {
fatalError("Not implemented")
}
}
Expand Down Expand Up @@ -338,7 +333,7 @@ class MockRoomReactions: RoomReactions {
}

@discardableResult
func subscribe(_ callback: @escaping @MainActor (RoomReactionEvent) -> Void) -> SubscriptionProtocol {
func subscribe(_ callback: @escaping @MainActor (RoomReactionEvent) -> Void) -> some SubscriptionProtocol {
mockSubscriptions.create(
randomElement: {
let reaction = RoomReaction(
Expand Down Expand Up @@ -369,7 +364,7 @@ class MockTyping: Typing {
}

@discardableResult
func subscribe(_ callback: @escaping @MainActor (TypingSetEvent) -> Void) -> SubscriptionProtocol {
func subscribe(_ callback: @escaping @MainActor (TypingSetEvent) -> Void) -> some SubscriptionProtocol {
mockSubscriptions.create(
randomElement: {
TypingSetEvent(
Expand Down Expand Up @@ -422,7 +417,7 @@ class MockPresence: Presence {
self.roomName = roomName
}

private func createSubscription(callback: @escaping @MainActor (PresenceEvent) -> Void) -> SubscriptionProtocol {
private func createSubscription(callback: @escaping @MainActor (PresenceEvent) -> Void) -> MockSubscription {
mockSubscriptions.create(
randomElement: {
let member = PresenceMember(
Expand Down Expand Up @@ -536,11 +531,11 @@ class MockPresence: Presence {
)
}

func subscribe(event _: PresenceEventType, _ callback: @escaping @MainActor (PresenceEvent) -> Void) -> SubscriptionProtocol {
func subscribe(event _: PresenceEventType, _ callback: @escaping @MainActor (PresenceEvent) -> Void) -> MockSubscription {
createSubscription(callback: callback)
}

func subscribe(events _: [PresenceEventType], _ callback: @escaping @MainActor (PresenceEvent) -> Void) -> SubscriptionProtocol {
func subscribe(events _: [PresenceEventType], _ callback: @escaping @MainActor (PresenceEvent) -> Void) -> MockSubscription {
createSubscription(callback: callback)
}
}
Expand All @@ -557,7 +552,7 @@ class MockOccupancy: Occupancy {
}

@discardableResult
func subscribe(_ callback: @escaping @MainActor (OccupancyEvent) -> Void) -> SubscriptionProtocol {
func subscribe(_ callback: @escaping @MainActor (OccupancyEvent) -> Void) -> MockSubscription {
mockSubscriptions.create(
randomElement: {
let random = Int.random(in: 1 ... 10)
Expand Down Expand Up @@ -590,7 +585,7 @@ class MockConnection: Connection {
}

@discardableResult
func onStatusChange(_ callback: @escaping @MainActor (ConnectionStatusChange) -> Void) -> StatusSubscriptionProtocol {
func onStatusChange(_ callback: @escaping @MainActor (ConnectionStatusChange) -> Void) -> some StatusSubscriptionProtocol {
Comment thread
maratal marked this conversation as resolved.
mockSubscriptions.create(
randomElement: {
ConnectionStatusChange(
Expand Down
26 changes: 13 additions & 13 deletions Example/AblyChatExample/Mocks/MockSubscriptionStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import AblyChat
class MockSubscriptionStorage<Element: Sendable> {
@MainActor
private struct SubscriptionItem {
let subscription: SubscriptionProtocol
let subscription: MockSubscription
let callback: (Element) -> Void

init(
Expand Down Expand Up @@ -39,7 +39,7 @@ class MockSubscriptionStorage<Element: Sendable> {
randomElement: @escaping @MainActor @Sendable () -> Element?,
interval: @autoclosure @escaping @MainActor @Sendable () -> Double,
callback: @escaping @MainActor (Element) -> Void,
) -> SubscriptionProtocol {
) -> MockSubscription {
let id = UUID()
let subscriptionItem = SubscriptionItem(randomElement: randomElement, interval: interval, callback: callback) { [weak self] in
self?.subscriptionDidTerminate(id: id)
Expand All @@ -64,7 +64,7 @@ class MockSubscriptionStorage<Element: Sendable> {
class MockStatusSubscriptionStorage<Element: Sendable> {
@MainActor
private struct SubscriptionItem {
let subscription: StatusSubscriptionProtocol
let subscription: MockStatusSubscription
let callback: (Element) -> Void

init(
Expand Down Expand Up @@ -97,7 +97,7 @@ class MockStatusSubscriptionStorage<Element: Sendable> {
randomElement: @escaping @MainActor @Sendable () -> Element?,
interval: @autoclosure @escaping @MainActor @Sendable () -> Double,
callback: @escaping @MainActor (Element) -> Void,
) -> StatusSubscriptionProtocol {
) -> MockStatusSubscription {
let id = UUID()
let subscriptionItem = SubscriptionItem(randomElement: randomElement, interval: interval, callback: callback) { [weak self] in
self?.subscriptionDidTerminate(id: id)
Expand All @@ -119,15 +119,15 @@ class MockStatusSubscriptionStorage<Element: Sendable> {

// This is copied from `MockSubscriptionStorage`, but for `MessageSubscriptionResponse`.
@MainActor
class MockMessageSubscriptionStorage<Element: Sendable> {
class MockMessageSubscriptionStorage<Element: Sendable, PaginatedResult: AblyChat.PaginatedResult<Message>> {
@MainActor
private struct SubscriptionItem {
let subscription: MessageSubscriptionResponseProtocol
let subscription: MockMessageSubscriptionResponse<PaginatedResult>
let callback: (Element) -> Void

init(
randomElement: @escaping @MainActor @Sendable () -> Element,
previousMessages: @escaping @MainActor @Sendable (QueryOptions) async throws(ARTErrorInfo) -> any PaginatedResult<Message>,
previousMessages: @escaping @MainActor @Sendable (QueryOptions) async throws(ARTErrorInfo) -> PaginatedResult,
interval: @escaping @MainActor @Sendable () -> Double,
callback: @escaping @MainActor (Element) -> Void,
onTerminate: @escaping () -> Void,
Expand All @@ -152,10 +152,10 @@ class MockMessageSubscriptionStorage<Element: Sendable> {

func create(
randomElement: @escaping @MainActor @Sendable () -> Element,
previousMessages: @escaping @MainActor @Sendable (QueryOptions) async throws(ARTErrorInfo) -> any PaginatedResult<Message>,
previousMessages: @escaping @MainActor @Sendable (QueryOptions) async throws(ARTErrorInfo) -> PaginatedResult,
interval: @autoclosure @escaping @MainActor @Sendable () -> Double,
callback: @escaping @MainActor (Element) -> Void,
) -> MessageSubscriptionResponseProtocol {
) -> some MessageSubscriptionResponseProtocol {
let id = UUID()
let subscriptionItem = SubscriptionItem(
randomElement: randomElement,
Expand Down Expand Up @@ -204,11 +204,11 @@ struct MockStatusSubscription: StatusSubscriptionProtocol {
}
}

struct MockMessageSubscriptionResponse: MessageSubscriptionResponseProtocol {
struct MockMessageSubscriptionResponse<PaginatedResult: AblyChat.PaginatedResult<Message>>: MessageSubscriptionResponseProtocol {
private let _unsubscribe: () -> Void
private let previousMessages: @MainActor @Sendable (QueryOptions) async throws(ARTErrorInfo) -> any PaginatedResult<Message>
private let previousMessages: @MainActor @Sendable (QueryOptions) async throws(ARTErrorInfo) -> PaginatedResult

func historyBeforeSubscribe(_ params: QueryOptions) async throws(ARTErrorInfo) -> any PaginatedResult<Message> {
func historyBeforeSubscribe(_ params: QueryOptions) async throws(ARTErrorInfo) -> PaginatedResult {
try await previousMessages(params)
}

Expand All @@ -217,7 +217,7 @@ struct MockMessageSubscriptionResponse: MessageSubscriptionResponseProtocol {
}

init(
previousMessages: @escaping @MainActor @Sendable (QueryOptions) async throws(ARTErrorInfo) -> any PaginatedResult<Message>,
previousMessages: @escaping @MainActor @Sendable (QueryOptions) async throws(ARTErrorInfo) -> PaginatedResult,
unsubscribe: @MainActor @Sendable @escaping () -> Void,
) {
self.previousMessages = previousMessages
Expand Down
4 changes: 2 additions & 2 deletions Sources/AblyChat/ChatAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal final class ChatAPI {
}

// (CHA-M6) Messages should be queryable from a paginated REST API.
internal func getMessages(roomName: String, params: QueryOptions) async throws(InternalError) -> any PaginatedResult<Message> {
internal func getMessages(roomName: String, params: QueryOptions) async throws(InternalError) -> some PaginatedResult<Message> {
let endpoint = "\(apiVersionV4)/rooms/\(roomName)/messages"
return try await makePaginatedRequest(endpoint, params: params.asQueryItems())
}
Expand Down Expand Up @@ -242,7 +242,7 @@ internal final class ChatAPI {
private func makePaginatedRequest<Response: JSONDecodable & Sendable & Equatable>(
_ url: String,
params: [String: String]? = nil,
) async throws(InternalError) -> any PaginatedResult<Response> {
) async throws(InternalError) -> some PaginatedResult<Response> {
let paginatedResponse = try await realtime.request("GET", path: url, params: params, body: nil, headers: [:])
let jsonValues = paginatedResponse.items.map { JSONValue(ablyCocoaData: $0) }
let items = try jsonValues.map { jsonValue throws(InternalError) in
Expand Down
10 changes: 7 additions & 3 deletions Sources/AblyChat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Ably
@MainActor
public protocol ChatClient: AnyObject, Sendable {
associatedtype Realtime
associatedtype Connection: AblyChat.Connection
associatedtype Rooms: AblyChat.Rooms

/**
Expand All @@ -18,7 +19,7 @@ public protocol ChatClient: AnyObject, Sendable {
*
* - Returns: The connection object.
*/
nonisolated var connection: any Connection { get }
nonisolated var connection: Connection { get }

/**
* Returns the clientId of the current client.
Expand Down Expand Up @@ -70,7 +71,10 @@ public class DefaultChatClient: ChatClient {

// (CHA-CS1) Every chat client has a status, which describes the current status of the connection.
// (CHA-CS4) The chat client must allow its connection status to be observed by clients.
public let connection: any Connection
private let _connection: DefaultConnection
public var connection: some Connection {
_connection
}

/**
* Constructor for Chat
Expand Down Expand Up @@ -101,7 +105,7 @@ public class DefaultChatClient: ChatClient {
logger = DefaultInternalLogger(logHandler: self.clientOptions.logHandler, logLevel: self.clientOptions.logLevel)
let roomFactory = DefaultRoomFactory<InternalRealtimeClientAdapter<ARTWrapperSDKProxyRealtime>>()
_rooms = DefaultRooms(realtime: internalRealtime, clientOptions: self.clientOptions, logger: logger, roomFactory: roomFactory)
connection = DefaultConnection(realtime: internalRealtime)
_connection = DefaultConnection(realtime: internalRealtime)
}

public nonisolated var clientID: String {
Expand Down
4 changes: 3 additions & 1 deletion Sources/AblyChat/Connection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import Ably
*/
@MainActor
public protocol Connection: AnyObject, Sendable {
associatedtype StatusSubscription: StatusSubscriptionProtocol

/**
* The current status of the connection.
*/
Expand All @@ -25,7 +27,7 @@ public protocol Connection: AnyObject, Sendable {
* - Returns: A subscription that can be used to unsubscribe from ``ConnectionStatusChange`` events.
*/
@discardableResult
func onStatusChange(_ callback: @escaping @MainActor (ConnectionStatusChange) -> Void) -> any StatusSubscriptionProtocol
func onStatusChange(_ callback: @escaping @MainActor (ConnectionStatusChange) -> Void) -> StatusSubscription
}

/// `AsyncSequence` variant of `Connection` status changes.
Expand Down
4 changes: 2 additions & 2 deletions Sources/AblyChat/DefaultConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal final class DefaultConnection: Connection {

// (CHA-CS4d) Clients must be able to register a listener for connection status events and receive such events.
@discardableResult
internal func onStatusChange(_ callback: @escaping @MainActor (ConnectionStatusChange) -> Void) -> any StatusSubscriptionProtocol {
internal func onStatusChange(_ callback: @escaping @MainActor (ConnectionStatusChange) -> Void) -> some StatusSubscriptionProtocol {
// (CHA-CS5) The chat client must monitor the underlying realtime connection for connection status changes.
let eventListener = realtime.connection.on { [weak self] stateChange in
guard let self else {
Expand Down Expand Up @@ -72,7 +72,7 @@ internal final class DefaultConnection: Connection {
callback(statusChange)
}

return StatusSubscription { [weak self] in
return DefaultStatusSubscription { [weak self] in
guard let self else {
return
}
Expand Down
Loading