diff --git a/Example/AblyChatExample/Mocks/MockRealtime.swift b/Example/AblyChatExample/Mocks/MockRealtime.swift index 11951d3..067e8f6 100644 --- a/Example/AblyChatExample/Mocks/MockRealtime.swift +++ b/Example/AblyChatExample/Mocks/MockRealtime.swift @@ -1,7 +1,8 @@ import Ably +import AblyChat -/// A mock implementation of `ARTRealtimeProtocol`. It only exists so that we can construct an instance of `DefaultChatClient` without needing to create a proper `ARTRealtime` instance (which we can’t yet do because we don’t have a method for inserting an API key into the example app). TODO remove this once we start building the example app -final class MockRealtime: NSObject, ARTRealtimeProtocol, Sendable { +/// A mock implementation of `RealtimeClientProtocol`. It only exists so that we can construct an instance of `DefaultChatClient` without needing to create a proper `ARTRealtime` instance (which we can’t yet do because we don’t have a method for inserting an API key into the example app). TODO remove this once we start building the example app +final class MockRealtime: NSObject, RealtimeClientProtocol, Sendable { var device: ARTLocalDevice { fatalError("Not implemented") } @@ -10,6 +11,168 @@ final class MockRealtime: NSObject, ARTRealtimeProtocol, Sendable { fatalError("Not implemented") } + let channels = Channels() + + final class Channels: RealtimeChannelsProtocol { + func get(_: String) -> Channel { + fatalError("Not implemented") + } + + func exists(_: String) -> Bool { + fatalError("Not implemented") + } + + func release(_: String, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func release(_: String) { + fatalError("Not implemented") + } + } + + final class Channel: RealtimeChannelProtocol { + var state: ARTRealtimeChannelState { + fatalError("Not implemented") + } + + var errorReason: ARTErrorInfo? { + fatalError("Not implemented") + } + + var options: ARTRealtimeChannelOptions? { + fatalError("Not implemented") + } + + func attach() { + fatalError("Not implemented") + } + + func attach(_: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func detach() { + fatalError("Not implemented") + } + + func detach(_: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func subscribe(_: @escaping ARTMessageCallback) -> ARTEventListener? { + fatalError("Not implemented") + } + + func subscribe(attachCallback _: ARTCallback?, callback _: @escaping ARTMessageCallback) -> ARTEventListener? { + fatalError("Not implemented") + } + + func subscribe(_: String, callback _: @escaping ARTMessageCallback) -> ARTEventListener? { + fatalError("Not implemented") + } + + func subscribe(_: String, onAttach _: ARTCallback?, callback _: @escaping ARTMessageCallback) -> ARTEventListener? { + fatalError("Not implemented") + } + + func unsubscribe() { + fatalError("Not implemented") + } + + func unsubscribe(_: ARTEventListener?) { + fatalError("Not implemented") + } + + func unsubscribe(_: String, listener _: ARTEventListener?) { + fatalError("Not implemented") + } + + func history(_: ARTRealtimeHistoryQuery?, callback _: @escaping ARTPaginatedMessagesCallback) throws { + fatalError("Not implemented") + } + + func setOptions(_: ARTRealtimeChannelOptions?, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func on(_: ARTChannelEvent, callback _: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener { + fatalError("Not implemented") + } + + func on(_: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener { + fatalError("Not implemented") + } + + func once(_: ARTChannelEvent, callback _: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener { + fatalError("Not implemented") + } + + func once(_: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener { + fatalError("Not implemented") + } + + func off(_: ARTChannelEvent, listener _: ARTEventListener) { + fatalError("Not implemented") + } + + func off(_: ARTEventListener) { + fatalError("Not implemented") + } + + func off() { + fatalError("Not implemented") + } + + var name: String { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, clientId _: String) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, clientId _: String, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, extras _: (any ARTJsonCompatible)?) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, extras _: (any ARTJsonCompatible)?, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, clientId _: String, extras _: (any ARTJsonCompatible)?) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, clientId _: String, extras _: (any ARTJsonCompatible)?, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func publish(_: [ARTMessage]) { + fatalError("Not implemented") + } + + func publish(_: [ARTMessage], callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func history(_: @escaping ARTPaginatedMessagesCallback) { + fatalError("Not implemented") + } + } + required init(options _: ARTClientOptions) {} required init(key _: String) {} diff --git a/Sources/AblyChat/AblyCocoaExtensions/Ably+Concurrency.swift b/Sources/AblyChat/AblyCocoaExtensions/Ably+Concurrency.swift new file mode 100644 index 0000000..05ebbb2 --- /dev/null +++ b/Sources/AblyChat/AblyCocoaExtensions/Ably+Concurrency.swift @@ -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) in + attach { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } + + func detachAsync() async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + detach { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } +} diff --git a/Sources/AblyChat/AblyCocoaExtensions/Ably+Dependencies.swift b/Sources/AblyChat/AblyCocoaExtensions/Ably+Dependencies.swift new file mode 100644 index 0000000..81eeee9 --- /dev/null +++ b/Sources/AblyChat/AblyCocoaExtensions/Ably+Dependencies.swift @@ -0,0 +1,18 @@ +import Ably + +// TODO: remove "@unchecked Sendable" once https://github.com/ably/ably-cocoa/issues/1962 done + +#if swift(>=6) + // This @retroactive is needed to silence the Swift 6 compiler error "extension declares a conformance of imported type 'ARTRealtimeChannels' to imported protocol 'Sendable'; this will not behave correctly if the owners of 'Ably' introduce this conformance in the future (…) add '@retroactive' to silence this warning". I don’t fully understand the implications of this but don’t really mind since both libraries are in our control. + extension ARTRealtime: RealtimeClientProtocol, @retroactive @unchecked Sendable {} + + extension ARTRealtimeChannels: RealtimeChannelsProtocol, @retroactive @unchecked Sendable {} + + extension ARTRealtimeChannel: RealtimeChannelProtocol, @retroactive @unchecked Sendable {} +#else + extension ARTRealtime: RealtimeClientProtocol, @unchecked Sendable {} + + extension ARTRealtimeChannels: RealtimeChannelsProtocol, @unchecked Sendable {} + + extension ARTRealtimeChannel: RealtimeChannelProtocol, @unchecked Sendable {} +#endif diff --git a/Sources/AblyChat/ChatClient.swift b/Sources/AblyChat/ChatClient.swift index 44ac63c..1ad57e5 100644 --- a/Sources/AblyChat/ChatClient.swift +++ b/Sources/AblyChat/ChatClient.swift @@ -8,7 +8,7 @@ public protocol ChatClient: AnyObject, Sendable { var clientOptions: ClientOptions { get } } -public typealias RealtimeClient = any(ARTRealtimeProtocol & Sendable) +public typealias RealtimeClient = any RealtimeClientProtocol public actor DefaultChatClient: ChatClient { public let realtime: RealtimeClient diff --git a/Sources/AblyChat/Dependencies.swift b/Sources/AblyChat/Dependencies.swift new file mode 100644 index 0000000..e0d1e18 --- /dev/null +++ b/Sources/AblyChat/Dependencies.swift @@ -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 (https://github.com/ably/ably-cocoa/issues/1968). + func get(_ name: String) -> Channel +} + +/// Expresses the requirements of the object returned by ``RealtimeChannelsProtocol.get(_:)``. +public protocol RealtimeChannelProtocol: ARTRealtimeChannelProtocol, Sendable {} diff --git a/Sources/AblyChat/Room.swift b/Sources/AblyChat/Room.swift index 5e55d17..e7f1f3c 100644 --- a/Sources/AblyChat/Room.swift +++ b/Sources/AblyChat/Room.swift @@ -54,11 +54,26 @@ internal actor DefaultRoom: Room { fatalError("Not yet implemented") } + /// Fetches the channels that contribute to this room. + private func channels() -> [any RealtimeChannelProtocol] { + [ + "chatMessages", + "typingIndicators", + "reactions", + ].map { suffix in + realtime.channels.get("\(roomID)::$chat::$\(suffix)") + } + } + public func attach() async throws { - fatalError("Not yet implemented") + for channel in channels() { + try await channel.attachAsync() + } } public func detach() async throws { - fatalError("Not yet implemented") + for channel in channels() { + try await channel.detachAsync() + } } } diff --git a/Sources/BuildTool/ProcessRunner.swift b/Sources/BuildTool/ProcessRunner.swift index cb47fbd..6219c01 100644 --- a/Sources/BuildTool/ProcessRunner.swift +++ b/Sources/BuildTool/ProcessRunner.swift @@ -7,7 +7,7 @@ enum ProcessRunner { // There’s probably a better way to implement these, which doesn’t involve having to use a separate dispatch queue. There’s a proposal for a Subprocess API coming up in Foundation which will marry Process with Swift concurrency. static func run(executableName: String, arguments: [String]) async throws { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in queue.async { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/env") diff --git a/Tests/AblyChatTests/DefaultRoomTests.swift b/Tests/AblyChatTests/DefaultRoomTests.swift new file mode 100644 index 0000000..624fdf2 --- /dev/null +++ b/Tests/AblyChatTests/DefaultRoomTests.swift @@ -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) + } +} diff --git a/Tests/AblyChatTests/Mocks/MockChannels.swift b/Tests/AblyChatTests/Mocks/MockChannels.swift new file mode 100644 index 0000000..6cbf82b --- /dev/null +++ b/Tests/AblyChatTests/Mocks/MockChannels.swift @@ -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") + } +} diff --git a/Tests/AblyChatTests/Mocks/MockRealtime.swift b/Tests/AblyChatTests/Mocks/MockRealtime.swift index d0f8c4a..e8c8277 100644 --- a/Tests/AblyChatTests/Mocks/MockRealtime.swift +++ b/Tests/AblyChatTests/Mocks/MockRealtime.swift @@ -1,8 +1,9 @@ import Ably +import AblyChat import Foundation -/// A mock implementation of `ARTRealtimeProtocol`. Copied from the class of the same name in the example app. We’ll figure out how to do mocking in tests properly in https://github.com/ably-labs/ably-chat-swift/issues/5. -final class MockRealtime: NSObject, ARTRealtimeProtocol, Sendable { +/// A mock implementation of `ARTRealtimeProtocol`. We’ll figure out how to do mocking in tests properly in https://github.com/ably-labs/ably-chat-swift/issues/5. +final class MockRealtime: NSObject, RealtimeClientProtocol, Sendable { var device: ARTLocalDevice { fatalError("Not implemented") } @@ -11,19 +12,31 @@ final class MockRealtime: NSObject, ARTRealtimeProtocol, Sendable { fatalError("Not implemented") } - required init(options _: ARTClientOptions) {} + required init(options _: ARTClientOptions) { + channels = .init(channels: []) + } + + required init(key _: String) { + channels = .init(channels: []) + } - required init(key _: String) {} + required init(token _: String) { + channels = .init(channels: []) + } + + init(channels: MockChannels = .init(channels: [])) { + self.channels = channels + } - required init(token _: String) {} + let channels: MockChannels /** Creates an instance of MockRealtime. This exists to give a convenient way to create an instance, because `init` is marked as unavailable in `ARTRealtimeProtocol`. */ - static func create() -> MockRealtime { - MockRealtime(key: "") + static func create(channels: MockChannels = MockChannels(channels: [])) -> MockRealtime { + MockRealtime(channels: channels) } func time(_: @escaping ARTDateTimeCallback) { diff --git a/Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift b/Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift new file mode 100644 index 0000000..f01f70b --- /dev/null +++ b/Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift @@ -0,0 +1,221 @@ +import Ably +import AblyChat + +final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol { + private let _name: String? + + init( + name: String? = nil, + attachResult: AttachOrDetachResult? = nil, + detachResult: AttachOrDetachResult? = nil + ) { + _name = name + self.attachResult = attachResult + self.detachResult = detachResult + } + + /// A threadsafe counter that starts at zero. + class Counter: @unchecked Sendable { + private var mutex = NSLock() + private var _value = 0 + + var value: Int { + let value: Int + mutex.lock() + value = _value + mutex.unlock() + return value + } + + func increment() { + mutex.lock() + _value += 1 + mutex.unlock() + } + + var isZero: Bool { + value == 0 + } + + var isNonZero: Bool { + value > 0 + } + } + + var state: ARTRealtimeChannelState { + fatalError("Not implemented") + } + + var errorReason: ARTErrorInfo? { + fatalError("Not implemented") + } + + var options: ARTRealtimeChannelOptions? { + fatalError("Not implemented") + } + + func attach() { + fatalError("Not implemented") + } + + enum AttachOrDetachResult { + case success + case failure(ARTErrorInfo) + + func performCallback(_ callback: ARTCallback?) { + switch self { + case .success: + callback?(nil) + case let .failure(error): + callback?(error) + } + } + } + + private let attachResult: AttachOrDetachResult? + + let attachCallCounter = Counter() + + func attach(_ callback: ARTCallback? = nil) { + attachCallCounter.increment() + + guard let attachResult else { + fatalError("attachResult must be set before attach is called") + } + + attachResult.performCallback(callback) + } + + private let detachResult: AttachOrDetachResult? + + let detachCallCounter = Counter() + + func detach() { + fatalError("Not implemented") + } + + func detach(_ callback: ARTCallback? = nil) { + detachCallCounter.increment() + + guard let detachResult else { + fatalError("detachResult must be set before detach is called") + } + + detachResult.performCallback(callback) + } + + func subscribe(_: @escaping ARTMessageCallback) -> ARTEventListener? { + fatalError("Not implemented") + } + + func subscribe(attachCallback _: ARTCallback?, callback _: @escaping ARTMessageCallback) -> ARTEventListener? { + fatalError("Not implemented") + } + + func subscribe(_: String, callback _: @escaping ARTMessageCallback) -> ARTEventListener? { + fatalError("Not implemented") + } + + func subscribe(_: String, onAttach _: ARTCallback?, callback _: @escaping ARTMessageCallback) -> ARTEventListener? { + fatalError("Not implemented") + } + + func unsubscribe() { + fatalError("Not implemented") + } + + func unsubscribe(_: ARTEventListener?) { + fatalError("Not implemented") + } + + func unsubscribe(_: String, listener _: ARTEventListener?) { + fatalError("Not implemented") + } + + func history(_: ARTRealtimeHistoryQuery?, callback _: @escaping ARTPaginatedMessagesCallback) throws { + fatalError("Not implemented") + } + + func setOptions(_: ARTRealtimeChannelOptions?, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func on(_: ARTChannelEvent, callback _: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener { + fatalError("Not implemented") + } + + func on(_: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener { + fatalError("Not implemented") + } + + func once(_: ARTChannelEvent, callback _: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener { + fatalError("Not implemented") + } + + func once(_: @escaping (ARTChannelStateChange) -> Void) -> ARTEventListener { + fatalError("Not implemented") + } + + func off(_: ARTChannelEvent, listener _: ARTEventListener) { + fatalError("Not implemented") + } + + func off(_: ARTEventListener) { + fatalError("Not implemented") + } + + func off() { + fatalError("Not implemented") + } + + var name: String { + guard let name = _name else { + fatalError("Channel name not set") + } + return name + } + + func publish(_: String?, data _: Any?) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, clientId _: String) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, clientId _: String, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, extras _: (any ARTJsonCompatible)?) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, extras _: (any ARTJsonCompatible)?, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, clientId _: String, extras _: (any ARTJsonCompatible)?) { + fatalError("Not implemented") + } + + func publish(_: String?, data _: Any?, clientId _: String, extras _: (any ARTJsonCompatible)?, callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func publish(_: [ARTMessage]) { + fatalError("Not implemented") + } + + func publish(_: [ARTMessage], callback _: ARTCallback? = nil) { + fatalError("Not implemented") + } + + func history(_: @escaping ARTPaginatedMessagesCallback) { + fatalError("Not implemented") + } +}