Skip to content

Commit

Permalink
- remove redundant code around channelWithProperties
Browse files Browse the repository at this point in the history
- move getChannel inside the ChatAPI to avoid needing both Realtime and ChatAPI when initialising DefaultMessages
  • Loading branch information
umair-ably committed Sep 30, 2024
1 parent 84af6a1 commit 12b49cf
Show file tree
Hide file tree
Showing 4 changed files with 18 additions and 31 deletions.
4 changes: 4 additions & 0 deletions Sources/AblyChat/ChatAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ public final class ChatAPI: Sendable {
self.realtime = realtime
}

internal func getChannel(_ messagesChannelName: String) -> any RealtimeChannelProtocol {
realtime.getChannel(messagesChannelName)
}

// (CHA-M6) Messages should be queryable from a paginated REST API.
public func getMessages(roomId: String, params: QueryOptions) async throws -> any PaginatedResult<Message> {
let endpoint = "/chat/v1/rooms/\(roomId)/messages"
Expand Down
40 changes: 12 additions & 28 deletions Sources/AblyChat/DefaultMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public struct MessageEventPayload: Sendable {
/**
* The type of the message event.
*/
internal let type: MessageEvents
internal let type: MessageEvent

/**
* The message that was received.
Expand All @@ -45,21 +45,19 @@ public final class DefaultMessages: Messages, HandlesDiscontinuity {
private let chatAPI: ChatAPI
private let clientID: String
private var listenerSubscriptionPoints: MessageListeners = [:]
private let realtime: RealtimeClient

public nonisolated init(chatAPI: ChatAPI, realtime: RealtimeClient, roomID: String, clientID: String) {
public nonisolated init(chatAPI: ChatAPI, roomID: String, clientID: String) {
self.chatAPI = chatAPI
self.roomID = roomID
self.clientID = clientID
self.realtime = realtime

// (CHA-M1) Chat messages for a Room are sent on a corresponding realtime channel <roomId>::$chat::$chatMessages. For example, if your room id is my-room then the messages channel will be my-room::$chat::$chatMessages.
let messagesChannelName = "\(roomID)::$chat::$chatMessages"
self.channel = realtime.getChannel(messagesChannelName)
self.channel = chatAPI.getChannel(messagesChannelName)

// Implicitly handles channel events and therefore listners within this class. Alternative is to explicitly call something like `DefaultMessages.start()` which makes the SDK more cumbersome to interact with. This class is useless without kicking off this flow so I think leaving it here is suitable.
Task {
await handleChannelEvents(roomId: roomID, realtime: realtime)
await handleChannelEvents(roomId: roomID)
}
}

Expand All @@ -71,7 +69,7 @@ public final class DefaultMessages: Messages, HandlesDiscontinuity {
// (CHA-M4c) When a realtime message with name set to message.created is received, it is translated into a message event, which contains a type field with the event type as well as a message field containing the Message Struct. This event is then broadcast to all subscribers.
// (CHA-M4d) If a realtime message with an unknown name is received, the SDK shall silently discard the message, though it may log at DEBUG or TRACE level.
// (CHA-M5d) Incoming realtime events that are malformed (unknown field should be ignored) shall not be emitted to subscribers.
channel.subscribe(MessageEvents.created.rawValue) { message in
channel.subscribe(MessageEvent.created.rawValue) { message in
Task {
guard let data = message.data as? [String: Any],
let text = data["text"] as? String else {
Expand Down Expand Up @@ -153,7 +151,7 @@ public final class DefaultMessages: Messages, HandlesDiscontinuity {
return try await chatAPI.getMessages(roomId: roomID, params: queryOptions)
}

private func handleChannelEvents(roomId: String, realtime: RealtimeClient) {
private func handleChannelEvents(roomId: String) {
// (CHA-M5c) If a channel leaves the ATTACHED state and then re-enters ATTACHED with resumed=false, then it must be assumed that messages have been missed. The subscription point of any subscribers must be reset to the attachSerial.
channel.on(.attached) { [self] stateChange in
Task {
Expand Down Expand Up @@ -200,11 +198,9 @@ public final class DefaultMessages: Messages, HandlesDiscontinuity {
}

private func resolveSubscriptionStart() async throws -> FromSerial {
let channelWithProperties = try await getChannelProperties()

// (CHA-M5a) If a subscription is added when the underlying realtime channel is ATTACHED, then the subscription point is the current channelSerial of the realtime channel.
if channelWithProperties.channel?.state == .attached {
if let channelSerial = channelWithProperties.properties.channelSerial {
if channel.state == .attached {
if let channelSerial = channel.properties.channelSerial {
return channelSerial
} else {
throw ARTErrorInfo.create(withCode: 40000, status: 400, message: "channel is attached, but channelSerial is not defined")
Expand All @@ -220,23 +216,11 @@ public final class DefaultMessages: Messages, HandlesDiscontinuity {
listenerSubscriptionPoints.removeValue(forKey: listener)
}

private func getChannelProperties() async throws -> (channel: (any RealtimeChannelProtocol)?, properties: (attachSerial: String?, channelSerial: String?)) {
// Return the channel with the properties as a tuple
let properties = (
attachSerial: channel.properties.attachSerial,
channelSerial: channel.properties.channelSerial
)

return (channel, properties)
}

// Always returns the attachSerial and not the channelSerial to also serve (CHA-M5c) - If a channel leaves the ATTACHED state and then re-enters ATTACHED with resumed=false, then it must be assumed that messages have been missed. The subscription point of any subscribers must be reset to the attachSerial.
private func subscribeAtChannelAttach() async throws -> String {
let channelWithProperties = try await getChannelProperties()

// If the state is already 'attached', return the attachSerial immediately
if channelWithProperties.channel?.state == .attached {
if let attachSerial = channelWithProperties.properties.attachSerial {
if channel.state == .attached {
if let attachSerial = channel.properties.attachSerial {
return attachSerial
} else {
throw ARTErrorInfo.create(withCode: 40000, status: 400, message: "Channel is attached, but attachSerial is not defined")
Expand All @@ -246,8 +230,8 @@ public final class DefaultMessages: Messages, HandlesDiscontinuity {
// (CHA-M5b) If a subscription is added when the underlying realtime channel is in any other state, then its subscription point becomes the attachSerial at the the point of channel attachment.
return try await withCheckedThrowingContinuation { continuation in
// Handle successful attachment
channelWithProperties.channel?.on(.attached) { _ in
if let attachSerial = channelWithProperties.properties.attachSerial {
channel.on(.attached) { [channel] _ in
if let attachSerial = channel.properties.attachSerial {
continuation.resume(returning: attachSerial)
} else {
continuation.resume(throwing: ARTErrorInfo.create(withCode: 40000, status: 400, message: "Channel is attached, but attachSerial is not defined"))
Expand Down
2 changes: 1 addition & 1 deletion Sources/AblyChat/Events.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
internal enum MessageEvents: String, Hashable {
internal enum MessageEvent: String {
case created = "message.created"
}
3 changes: 1 addition & 2 deletions Sources/AblyChat/Room.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,13 @@ internal actor DefaultRoom: Room {

self._messages = DefaultMessages(
chatAPI: chatAPI,
realtime: realtime,
roomID: roomID,
clientID: realtime.clientId ?? ""
)
}

public nonisolated var messages: any Messages {
self._messages
_messages
}

public nonisolated var presence: any Presence {
Expand Down

0 comments on commit 12b49cf

Please sign in to comment.