Skip to content

Commit

Permalink
Merge pull request #211 from ably/use-wrapper-sdk-proxy
Browse files Browse the repository at this point in the history
[ECO-5018] Use wrapper SDK proxy to track Chat SDK usage
  • Loading branch information
lawrence-forooghian authored Feb 10, 2025
2 parents ec2a0b8 + 47752d3 commit 46c0e1f
Show file tree
Hide file tree
Showing 16 changed files with 126 additions and 136 deletions.
6 changes: 3 additions & 3 deletions AblyChat.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"originHash" : "77e4b2b661f9de38584f7d61ba62b95f5fe6ef751d01e208399bed3950246e2b",
"originHash" : "1642b4839120f35594f234f50e4692c898bd27f9ab378d75c8b8cc23e3955b51",
"pins" : [
{
"identity" : "ably-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ably/ably-cocoa",
"state" : {
"revision" : "35805d0e96a1df5b4dc0f6d82afe22ab753c15bd",
"version" : "1.2.37"
"revision" : "11f67876d3f2070ac976f639c4cc959d2995e1ca",
"version" : "1.2.38"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Example/AblyChatExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ private enum Environment: Equatable {
switch self {
case .mock:
return MockChatClient(
realtime: MockRealtime.create(),
realtime: MockRealtime(),
clientOptions: ClientOptions()
)
case let .live(key: key, clientId: clientId):
Expand Down
15 changes: 0 additions & 15 deletions Example/AblyChatExample/Mocks/MockRealtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -344,21 +344,6 @@ final class MockRealtime: NSObject, RealtimeClientProtocol, Sendable {
}
}

required init(options _: ARTClientOptions) {}

required init(key _: String) {}

required init(token _: String) {}

/**
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: "")
}

func time(_: @escaping ARTDateTimeCallback) {
fatalError("Not implemented")
}
Expand Down
6 changes: 3 additions & 3 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"originHash" : "8c7acde3826a921568cc75459ff2052ebec85d53398758c637099c1128f45e32",
"originHash" : "37fce824816ce268471c881861e4f5083c97a5b677b2060fdad5e31fab9cd658",
"pins" : [
{
"identity" : "ably-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ably/ably-cocoa",
"state" : {
"revision" : "35805d0e96a1df5b4dc0f6d82afe22ab753c15bd",
"version" : "1.2.37"
"revision" : "11f67876d3f2070ac976f639c4cc959d2995e1ca",
"version" : "1.2.38"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let package = Package(
dependencies: [
.package(
url: "https://github.com/ably/ably-cocoa",
from: "1.2.37"
from: "1.2.38"
),
.package(
url: "https://github.com/apple/swift-argument-parser",
Expand Down
6 changes: 5 additions & 1 deletion Sources/AblyChat/AblyCocoaExtensions/Ably+Dependencies.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import Ably

extension ARTRealtime: RealtimeClientProtocol {}
extension ARTRealtime: SuppliedRealtimeClientProtocol {}

extension ARTWrapperSDKProxyRealtime: RealtimeClientProtocol {}

extension ARTRealtimeChannels: RealtimeChannelsProtocol {}
extension ARTWrapperSDKProxyRealtimeChannels: RealtimeChannelsProtocol {}

extension ARTRealtimeChannel: RealtimeChannelProtocol {}
extension ARTWrapperSDKProxyRealtimeChannel: RealtimeChannelProtocol {}

extension ARTRealtimePresence: RealtimePresenceProtocol {}

Expand Down
9 changes: 6 additions & 3 deletions Sources/AblyChat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public typealias RealtimeClient = any RealtimeClientProtocol
* This is the core client for Ably chat. It provides access to chat rooms.
*/
public actor DefaultChatClient: ChatClient {
public let realtime: RealtimeClient
public nonisolated let realtime: RealtimeClient
public nonisolated let clientOptions: ClientOptions
public nonisolated let rooms: Rooms
private let logger: InternalLogger
Expand All @@ -60,9 +60,12 @@ public actor DefaultChatClient: ChatClient {
* - realtime: The Ably Realtime client.
* - clientOptions: The client options.
*/
public init(realtime: RealtimeClient, clientOptions: ClientOptions?) {
self.realtime = realtime
public init(realtime suppliedRealtime: any SuppliedRealtimeClientProtocol, clientOptions: ClientOptions?) {
self.realtime = suppliedRealtime
self.clientOptions = clientOptions ?? .init()

let realtime = suppliedRealtime.createWrapperSDKProxy(with: .init(agents: agents))

logger = DefaultInternalLogger(logHandler: self.clientOptions.logHandler, logLevel: self.clientOptions.logLevel)
let roomFactory = DefaultRoomFactory()
rooms = DefaultRooms(realtime: realtime, clientOptions: self.clientOptions, logger: logger, roomFactory: roomFactory)
Expand Down
50 changes: 16 additions & 34 deletions Sources/AblyChat/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ 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 {
public protocol SuppliedRealtimeClientProtocol: Sendable, RealtimeClientProtocol {
associatedtype ProxyClient: RealtimeClientProtocol

func createWrapperSDKProxy(with options: ARTWrapperSDKProxyOptions) -> ProxyClient
}

/// Expresses the requirements of the object returned by ``SuppliedRealtimeClientProtocol/createWrapperSDKProxy(with:)``.
public protocol RealtimeClientProtocol: ARTRealtimeInstanceMethodsProtocol, Sendable {
associatedtype Channels: RealtimeChannelsProtocol
associatedtype Connection: ConnectionProtocol

Expand Down Expand Up @@ -31,44 +38,19 @@ public protocol RealtimePresenceProtocol: ARTRealtimePresenceProtocol, Sendable
/// Expresses the requirements of the object returned by ``RealtimeClientProtocol/connection``.
public protocol ConnectionProtocol: ARTConnectionProtocol, Sendable {}

/// Like (a subset of) `ARTRealtimeChannelOptions` but with value semantics. (It’s unfortunate that `ARTRealtimeChannelOptions` doesn’t have a `-copy` method.)
internal struct RealtimeChannelOptions {
internal var modes: ARTChannelMode
internal var params: [String: String]?
internal var attachOnSubscribe: Bool

internal init() {
// Get our default values from ably-cocoa
let artRealtimeChannelOptions = ARTRealtimeChannelOptions()
modes = artRealtimeChannelOptions.modes
params = artRealtimeChannelOptions.params
attachOnSubscribe = artRealtimeChannelOptions.attachOnSubscribe
}

internal var toARTRealtimeChannelOptions: ARTRealtimeChannelOptions {
let result = ARTRealtimeChannelOptions()
result.modes = modes
result.params = params
result.attachOnSubscribe = attachOnSubscribe
return result
}
}

internal extension RealtimeClientProtocol {
// Function to get the channel with merged options
func getChannel(_ name: String, opts: RealtimeChannelOptions? = nil) -> any RealtimeChannelProtocol {
var resolvedOptions = opts ?? .init()

// Add in the default params
resolvedOptions.params = (resolvedOptions.params ?? [:]).merging(
defaultChannelParams
) { _, new
in new
// Function to get the channel with Chat's default options
func getChannel(_ name: String, opts: ARTRealtimeChannelOptions? = nil) -> any RealtimeChannelProtocol {
let resolvedOptions: ARTRealtimeChannelOptions = if let opts {
// swiftlint:disable:next force_cast
opts.copy() as! ARTRealtimeChannelOptions
} else {
.init()
}

// CHA-GP2a
resolvedOptions.attachOnSubscribe = false

return channels.get(name, options: resolvedOptions.toARTRealtimeChannelOptions)
return channels.get(name, options: resolvedOptions)
}
}
2 changes: 1 addition & 1 deletion Sources/AblyChat/Room.swift
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>
let featuresGroupedByChannelName = Dictionary(grouping: featuresWithOptions) { $0.toRoomFeature.channelNameForRoomID(roomID) }

let unorderedResult = featuresGroupedByChannelName.map { channelName, features in
var channelOptions = RealtimeChannelOptions()
let channelOptions = ARTRealtimeChannelOptions()

// channel setup for presence and occupancy
for feature in features {
Expand Down
4 changes: 1 addition & 3 deletions Sources/AblyChat/Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@ import Ably
// Version information
internal let version = "0.1.2"

internal let channelOptionsAgentString = "chat-swift/\(version)"

internal let defaultChannelParams = ["agent": channelOptionsAgentString]
internal let agents = ["chat-swift": version]
14 changes: 7 additions & 7 deletions Tests/AblyChatTests/ChatAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct ChatAPITests {
@Test
func sendMessage_whenSendMessageReturnsNoItems_throwsNoItemInResponse() async {
// Given
let realtime = MockRealtime.create {
let realtime = MockRealtime {
(MockHTTPPaginatedResponse.successSendMessageWithNoItems, nil)
}
let chatAPI = ChatAPI(realtime: realtime)
Expand All @@ -29,7 +29,7 @@ struct ChatAPITests {
@Test
func sendMessage_returnsMessage() async throws {
// Given
let realtime = MockRealtime.create {
let realtime = MockRealtime {
(MockHTTPPaginatedResponse.successSendMessage, nil)
}
let chatAPI = ChatAPI(realtime: realtime)
Expand All @@ -55,7 +55,7 @@ struct ChatAPITests {
@Test
func sendMessage_includesHeadersInBody() async throws {
// Given
let realtime = MockRealtime.create {
let realtime = MockRealtime {
(MockHTTPPaginatedResponse.successSendMessage, nil)
}
let chatAPI = ChatAPI(realtime: realtime)
Expand All @@ -78,7 +78,7 @@ struct ChatAPITests {
@Test
func sendMessage_includesMetadataInBody() async throws {
// Given
let realtime = MockRealtime.create {
let realtime = MockRealtime {
(MockHTTPPaginatedResponse.successSendMessage, nil)
}
let chatAPI = ChatAPI(realtime: realtime)
Expand All @@ -105,7 +105,7 @@ struct ChatAPITests {
func getMessages_whenGetMessagesReturnsNoItems_returnsEmptyPaginatedResult() async {
// Given
let paginatedResponse = MockHTTPPaginatedResponse.successGetMessagesWithNoItems
let realtime = MockRealtime.create {
let realtime = MockRealtime {
(paginatedResponse, nil)
}
let chatAPI = ChatAPI(realtime: realtime)
Expand All @@ -127,7 +127,7 @@ struct ChatAPITests {
func getMessages_whenGetMessagesReturnsItems_returnsItemsInPaginatedResult() async {
// Given
let paginatedResponse = MockHTTPPaginatedResponse.successGetMessagesWithItems
let realtime = MockRealtime.create {
let realtime = MockRealtime {
(paginatedResponse, nil)
}
let chatAPI = ChatAPI(realtime: realtime)
Expand Down Expand Up @@ -171,7 +171,7 @@ struct ChatAPITests {
// Given
let paginatedResponse = MockHTTPPaginatedResponse.successGetMessagesWithNoItems
let artError = ARTErrorInfo.create(withCode: 50000, message: "Internal server error")
let realtime = MockRealtime.create {
let realtime = MockRealtime {
(paginatedResponse, artError)
}
let chatAPI = ChatAPI(realtime: realtime)
Expand Down
32 changes: 28 additions & 4 deletions Tests/AblyChatTests/DefaultChatClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,49 @@ struct DefaultChatClientTests {
@Test
func init_withoutClientOptions() {
// Given: An instance of DefaultChatClient is created with nil clientOptions
let client = DefaultChatClient(realtime: MockRealtime.create(), clientOptions: nil)
let client = DefaultChatClient(
realtime: MockRealtime(createWrapperSDKProxyReturnValue: .init()),
clientOptions: nil
)

// Then: It uses the default client options
let defaultOptions = ClientOptions()
#expect(client.clientOptions.isEqualForTestPurposes(defaultOptions))
}

@Test
func test_realtime() {
// Given: An instance of DefaultChatClient
let realtime = MockRealtime(createWrapperSDKProxyReturnValue: .init())
let options = ClientOptions()
let client = DefaultChatClient(realtime: realtime, clientOptions: options)

// Then: Its `realtime` property returns the client that was passed to the initializer (i.e. as opposed to the proxy client created by `createWrapperSDKProxy(with:)`
#expect(client.realtime === realtime)
}

@Test
func createsWrapperSDKProxyRealtimeClientWithAgents() throws {
let realtime = MockRealtime(createWrapperSDKProxyReturnValue: .init())
let options = ClientOptions()
_ = DefaultChatClient(realtime: realtime, clientOptions: options)

#expect(realtime.createWrapperSDKProxyOptionsArgument?.agents == ["chat-swift": AblyChat.version])
}

@Test
func rooms() throws {
// Given: An instance of DefaultChatClient
let realtime = MockRealtime.create()
let proxyClient = MockRealtime()
let realtime = MockRealtime(createWrapperSDKProxyReturnValue: proxyClient)
let options = ClientOptions()
let client = DefaultChatClient(realtime: realtime, clientOptions: options)

// Then: Its `rooms` property returns an instance of DefaultRooms with the same realtime client and client options
// Then: Its `rooms` property returns an instance of DefaultRooms with the wrapper SDK proxy realtime client and same client options
let rooms = client.rooms

let defaultRooms = try #require(rooms as? DefaultRooms<DefaultRoomFactory>)
#expect(defaultRooms.testsOnly_realtime === realtime)
#expect(defaultRooms.testsOnly_realtime === proxyClient)
#expect(defaultRooms.clientOptions.isEqualForTestPurposes(options))
}
}
12 changes: 6 additions & 6 deletions Tests/AblyChatTests/DefaultMessagesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct DefaultMessagesTests {
// roomId and clientId values are arbitrary

// Given
let realtime = MockRealtime.create()
let realtime = MockRealtime()
let chatAPI = ChatAPI(realtime: realtime)
let channel = MockRealtimeChannel()
let featureChannel = MockFeatureChannel(channel: channel)
Expand All @@ -26,7 +26,7 @@ struct DefaultMessagesTests {
// Message response of succcess with no items, and roomId are arbitrary

// Given
let realtime = MockRealtime.create { (MockHTTPPaginatedResponse.successGetMessagesWithNoItems, nil) }
let realtime = MockRealtime { (MockHTTPPaginatedResponse.successGetMessagesWithNoItems, nil) }
let chatAPI = ChatAPI(realtime: realtime)
let channel = MockRealtimeChannel()
let featureChannel = MockFeatureChannel(channel: channel)
Expand All @@ -46,7 +46,7 @@ struct DefaultMessagesTests {
// all setup values here are arbitrary

// Given
let realtime = MockRealtime.create { (MockHTTPPaginatedResponse.successGetMessagesWithNoItems, nil) }
let realtime = MockRealtime { (MockHTTPPaginatedResponse.successGetMessagesWithNoItems, nil) }
let chatAPI = ChatAPI(realtime: realtime)
let channel = MockRealtimeChannel(
properties: .init(
Expand All @@ -72,7 +72,7 @@ struct DefaultMessagesTests {
@Test
func subscribe_extractsHeadersFromChannelMessage() async throws {
// Given
let realtime = MockRealtime.create()
let realtime = MockRealtime()
let chatAPI = ChatAPI(realtime: realtime)

let channel = MockRealtimeChannel(
Expand Down Expand Up @@ -106,7 +106,7 @@ struct DefaultMessagesTests {
@Test
func subscribe_extractsMetadataFromChannelMessage() async throws {
// Given
let realtime = MockRealtime.create()
let realtime = MockRealtime()
let chatAPI = ChatAPI(realtime: realtime)

let channel = MockRealtimeChannel(
Expand Down Expand Up @@ -140,7 +140,7 @@ struct DefaultMessagesTests {
@Test
func onDiscontinuity() async throws {
// Given: A DefaultMessages instance
let realtime = MockRealtime.create()
let realtime = MockRealtime()
let chatAPI = ChatAPI(realtime: realtime)
let channel = MockRealtimeChannel()
let featureChannel = MockFeatureChannel(channel: channel)
Expand Down
Loading

0 comments on commit 46c0e1f

Please sign in to comment.