diff --git a/.pubnub.yml b/.pubnub.yml index d2aaf452..ce3f64f3 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,9 +1,14 @@ --- name: swift scm: github.com/pubnub/swift -version: "9.2.3" +version: "9.3.0" schema: 1 changelog: + - date: 2025-07-29 + version: 9.3.0 + changes: + - type: feature + text: "Add the ability to subscribe to presence channels only, without their main counterparts." - date: 2025-07-29 version: 9.2.3 changes: @@ -703,7 +708,7 @@ sdks: - distribution-type: source distribution-repository: GitHub release package-name: PubNub - location: https://github.com/pubnub/swift/archive/refs/tags/9.2.3.zip + location: https://github.com/pubnub/swift/archive/refs/tags/9.3.0.zip supported-platforms: supported-operating-systems: macOS: diff --git a/PubNub.xcodeproj/project.pbxproj b/PubNub.xcodeproj/project.pbxproj index 6d9b6b26..358d37e4 100644 --- a/PubNub.xcodeproj/project.pbxproj +++ b/PubNub.xcodeproj/project.pbxproj @@ -4031,7 +4031,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 9.2.3; + MARKETING_VERSION = 9.3.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -4082,7 +4082,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 9.2.3; + MARKETING_VERSION = 9.3.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -4190,7 +4190,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 9.2.3; + MARKETING_VERSION = 9.3.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -4243,7 +4243,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 9.2.3; + MARKETING_VERSION = 9.3.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -4364,7 +4364,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 9.2.3; + MARKETING_VERSION = 9.3.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -4416,7 +4416,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 9.2.3; + MARKETING_VERSION = 9.3.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -4896,7 +4896,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 9.2.3; + MARKETING_VERSION = 9.3.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; @@ -4939,7 +4939,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 9.2.3; + MARKETING_VERSION = 9.3.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; diff --git a/PubNubSwift.podspec b/PubNubSwift.podspec index 9914fa8a..3c45f621 100644 --- a/PubNubSwift.podspec +++ b/PubNubSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'PubNubSwift' - s.version = '9.2.3' + s.version = '9.3.0' s.homepage = 'https://github.com/pubnub/swift' s.documentation_url = 'https://www.pubnub.com/docs/swift-native/pubnub-swift-sdk' s.authors = { 'PubNub, Inc.' => 'support@pubnub.com' } diff --git a/Sources/PubNub/EventEngine/Presence/PresenceTransition.swift b/Sources/PubNub/EventEngine/Presence/PresenceTransition.swift index 46cb5668..52795907 100644 --- a/Sources/PubNub/EventEngine/Presence/PresenceTransition.swift +++ b/Sources/PubNub/EventEngine/Presence/PresenceTransition.swift @@ -23,10 +23,10 @@ class PresenceTransition: TransitionProtocol { func canTransition(from state: State, dueTo event: Event) -> Bool { switch event { - case .joined: - return true - case .left: - return true + case let .joined(channels, channelGroups): + return channels.count > 0 || channelGroups.count > 0 + case let .left(channels, channelGroups): + return channels.count > 0 || channelGroups.count > 0 case .heartbeatSuccess: return state is Presence.Heartbeating case .heartbeatFailed: diff --git a/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeInput.swift b/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeInput.swift index 96f2a6ff..3c88d906 100644 --- a/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeInput.swift +++ b/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeInput.swift @@ -10,171 +10,95 @@ import Foundation +/// A container for the current subscribed channels and channel groups struct SubscribeInput: Equatable { - private let channelEntries: [String: PubNubChannel] - private let groupEntries: [String: PubNubChannel] - - // swiftlint:disable:next large_tuple - typealias InsertingResult = ( - newInput: SubscribeInput, - insertedChannels: [PubNubChannel], - insertedGroups: [PubNubChannel] - ) - // swiftlint:disable:next large_tuple - typealias RemovingResult = ( - newInput: SubscribeInput, - removedChannels: [PubNubChannel], - removedGroups: [PubNubChannel] - ) - - init(channels: [PubNubChannel] = [], groups: [PubNubChannel] = []) { - self.channelEntries = channels.reduce(into: [String: PubNubChannel]()) { r, channel in - _ = r.insert(channel) - } - self.groupEntries = groups.reduce(into: [String: PubNubChannel]()) { r, channel in - _ = r.insert(channel) - } - } - - private init(channels: [String: PubNubChannel], groups: [String: PubNubChannel]) { - self.channelEntries = channels - self.groupEntries = groups - } - - var isEmpty: Bool { - channelEntries.isEmpty && groupEntries.isEmpty - } - - var channels: [PubNubChannel] { - Array(channelEntries.values) - } - - var groups: [PubNubChannel] { - Array(groupEntries.values) - } + private let subscribedChannels: Set + private let subscribedChannelGroups: Set - var subscribedChannelNames: [String] { - channelEntries.map { $0.key } + /// Result of comparing two SubscribeInput instances + struct Difference { + /// Items that were added (present in new but not in old) + let addedChannels: Set + /// Channels that were removed (present in old but not in new) + let removedChannels: Set + /// Channel groups that were added (present in new but not in old) + let addedChannelGroups: Set + /// Channel groups that were removed (present in old but not in new) + let removedChannelGroups: Set } - var subscribedGroupNames: [String] { - groupEntries.map { $0.key } + init(channels: Set = [], channelGroups: Set = []) { + self.subscribedChannels = channels + self.subscribedChannelGroups = channelGroups } - var allSubscribedChannelNames: [String] { - channelEntries.reduce(into: [String]()) { result, entry in - result.append(entry.value.id) - if entry.value.isPresenceSubscribed { - result.append(entry.value.presenceId) - } - } + init(channels: [String] = [], channelGroups: [String] = []) { + self.subscribedChannels = Set(channels) + self.subscribedChannelGroups = Set(channelGroups) } - var allSubscribedGroupNames: [String] { - groupEntries.reduce(into: [String]()) { result, entry in - result.append(entry.value.id) - if entry.value.isPresenceSubscribed { - result.append(entry.value.presenceId) - } - } + /// Whether the subscribe input is empty + /// + /// This is true if there are no subscribed channels or channel groups + var isEmpty: Bool { + subscribedChannels.isEmpty && subscribedChannelGroups.isEmpty } - var presenceSubscribedChannelNames: [String] { - channelEntries.compactMap { - if $0.value.isPresenceSubscribed { - return $0.value.id - } else { - return nil - } - } + /// Returns the names of all subscribed channels according to the `withPresence` parameter + /// + /// If `withPresence` is true, this list includes both regular and presence channel names + /// If `withPresence` is false, this list does not include presence channel names + func channelNames(withPresence: Bool) -> [String] { + withPresence ? subscribedChannels.allObjects : subscribedChannels.allObjects.filter { !$0.isPresenceChannelName } } - var presenceSubscribedGroupNames: [String] { - groupEntries.compactMap { - if $0.value.isPresenceSubscribed { - return $0.value.id - } else { - return nil - } - } + /// Returns the names of all subscribed channel groups according to the `withPresence` parameter + /// + /// If `withPresence` is true, this list includes both regular and presence channel group names + /// If `withPresence` is false, this list does not include presence channel group names + func channelGroupNames(withPresence: Bool) -> [String] { + withPresence ? subscribedChannelGroups.allObjects : subscribedChannelGroups.allObjects.filter { !$0.isPresenceChannelName } } + /// Total number of subscribed channels and channel groups var totalSubscribedCount: Int { - channelEntries.count + groupEntries.count + subscribedChannels.count + subscribedChannelGroups.count } - func adding( - channels: [PubNubChannel], - and groups: [PubNubChannel] - ) -> SubscribeInput.InsertingResult { - // Gets a copy of current channels and channel groups - var currentChannels = channelEntries - var currentGroups = groupEntries - - let insertedChannels = channels.filter { currentChannels.insert($0) } - let insertedGroups = groups.filter { currentGroups.insert($0) } - - return InsertingResult( - newInput: SubscribeInput(channels: currentChannels, groups: currentGroups), - insertedChannels: insertedChannels, - insertedGroups: insertedGroups + /// Adds the given channels and channel groups and returns a new input without modifying the current one + func adding(channels: Set, and channelGroups: Set) -> SubscribeInput { + SubscribeInput( + channels: channels.union(subscribedChannels), + channelGroups: channelGroups.union(subscribedChannelGroups) ) } - func removing( - mainChannels: [PubNubChannel], - presenceChannelsOnly: [PubNubChannel], - mainGroups: [PubNubChannel], - presenceGroupsOnly: [PubNubChannel] - ) -> SubscribeInput.RemovingResult { - // Gets a copy of current channels and channel groups - var currentChannels = channelEntries - var currentGroups = groupEntries - - let removedChannels = mainChannels.compactMap { - currentChannels.removeValue(forKey: $0.id) - } + presenceChannelsOnly.compactMap { - currentChannels.unsubscribePresence($0.id) - } - let removedGroups = mainGroups.compactMap { - currentGroups.removeValue(forKey: $0.id) - } + presenceGroupsOnly.compactMap { - currentGroups.unsubscribePresence($0.id) - } - return RemovingResult( - newInput: SubscribeInput(channels: currentChannels, groups: currentGroups), - removedChannels: removedChannels, - removedGroups: removedGroups + /// Removes the given channels and channel groups and returns a new input without modifying the current one + func removing(channels: Set, and channelGroups: Set) -> SubscribeInput { + SubscribeInput( + channels: subscribedChannels.subtracting(channels), + channelGroups: subscribedChannelGroups.subtracting(channelGroups) ) } - static func == (lhs: SubscribeInput, rhs: SubscribeInput) -> Bool { - let equalChannels = lhs.allSubscribedChannelNames.sorted(by: <) == rhs.allSubscribedChannelNames.sorted(by: <) - let equalGroups = lhs.allSubscribedGroupNames.sorted(by: <) == rhs.allSubscribedGroupNames.sorted(by: <) + /// Compares this input with another and returns the differences + func difference(from other: SubscribeInput) -> Difference { + let addedChannels = subscribedChannels.subtracting(other.subscribedChannels) + let removedChannels = other.subscribedChannels.subtracting(subscribedChannels) - return equalChannels && equalGroups - } -} + let addedGroups = subscribedChannelGroups.subtracting(other.subscribedChannelGroups) + let removedGroups = other.subscribedChannelGroups.subtracting(subscribedChannelGroups) -extension Dictionary where Key == String, Value == PubNubChannel { - // Inserts and returns the provided channel if that channel doesn't already exist - mutating func insert(_ channel: Value) -> Bool { - if let match = self[channel.id], match == channel { - return false - } - self[channel.id] = channel - return true + return Difference( + addedChannels: addedChannels, + removedChannels: removedChannels, + addedChannelGroups: addedGroups, + removedChannelGroups: removedGroups + ) } - // Updates current Dictionary with the new channel value unsubscribed from Presence. - // Returns the updated value if the corresponding entry matching the passed `id:` was found, otherwise `nil` - @discardableResult mutating func unsubscribePresence(_ id: String) -> Value? { - if let match = self[id], match.isPresenceSubscribed { - let updatedChannel = PubNubChannel(id: match.id, withPresence: false) - self[match.id] = updatedChannel - return updatedChannel - } - return nil + static func == (lhs: SubscribeInput, rhs: SubscribeInput) -> Bool { + lhs.subscribedChannels == rhs.subscribedChannels && lhs.subscribedChannelGroups == rhs.subscribedChannelGroups } } @@ -183,8 +107,8 @@ extension SubscribeInput: CustomStringConvertible { String.formattedDescription( self, arguments: [ - ("channels", allSubscribedChannelNames), - ("groups", allSubscribedGroupNames) + ("channels", channelNames(withPresence: true)), + ("groups", channelGroupNames(withPresence: true)) ] ) } diff --git a/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift b/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift index a36e76fa..44e01bdd 100644 --- a/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift +++ b/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift @@ -129,7 +129,7 @@ extension Subscribe.ReceiveFailedState { extension Subscribe { struct UnsubscribedState: SubscribeState { let cursor: SubscribeCursor = .init(timetoken: 0, region: 0) - let input: SubscribeInput = .init() + let input: SubscribeInput = .init(channels: [], channelGroups: []) let connectionStatus = ConnectionStatus.disconnected } } diff --git a/Sources/PubNub/EventEngine/Subscribe/SubscribeTransition.swift b/Sources/PubNub/EventEngine/Subscribe/SubscribeTransition.swift index e57e5726..070c3eac 100644 --- a/Sources/PubNub/EventEngine/Subscribe/SubscribeTransition.swift +++ b/Sources/PubNub/EventEngine/Subscribe/SubscribeTransition.swift @@ -62,8 +62,8 @@ class SubscribeTransition: TransitionProtocol { return [ .managed( .handshakeRequest( - channels: state.input.allSubscribedChannelNames, - groups: state.input.allSubscribedGroupNames + channels: state.input.channelNames(withPresence: true), + groups: state.input.channelGroupNames(withPresence: true) ) ) ] @@ -71,8 +71,8 @@ class SubscribeTransition: TransitionProtocol { return [ .managed( .receiveMessages( - channels: state.input.allSubscribedChannelNames, - groups: state.input.allSubscribedGroupNames, + channels: state.input.channelNames(withPresence: true), + groups: state.input.channelGroupNames(withPresence: true), cursor: state.cursor ) ) @@ -134,8 +134,8 @@ fileprivate extension SubscribeTransition { cursor: SubscribeCursor ) -> TransitionResult { let newInput = SubscribeInput( - channels: channels.map { PubNubChannel(channel: $0) }, - groups: groups.map { PubNubChannel(channel: $0) } + channels: Set(channels), + channelGroups: Set(groups) ) if newInput.isEmpty { @@ -146,8 +146,8 @@ fileprivate extension SubscribeTransition { .regular(.emitStatus(change: Subscribe.ConnectionStatusChange( oldStatus: state.connectionStatus, newStatus: .subscriptionChanged( - channels: newInput.subscribedChannelNames, - groups: newInput.subscribedGroupNames + channels: newInput.channelNames(withPresence: true), + groups: newInput.channelGroupNames(withPresence: true) ), error: nil ))) @@ -171,8 +171,8 @@ fileprivate extension SubscribeTransition { ) case is Subscribe.ReceivingState: let newStatus: ConnectionStatus = .subscriptionChanged( - channels: newInput.subscribedChannelNames, - groups: newInput.subscribedGroupNames + channels: newInput.channelNames(withPresence: true), + groups: newInput.channelGroupNames(withPresence: true) ) return TransitionResult( state: Subscribe.ReceivingState(input: newInput, cursor: cursor, connectionStatus: newStatus), diff --git a/Sources/PubNub/Events/New/Subscription.swift b/Sources/PubNub/Events/New/Subscription.swift index 2dde94ce..a1dd3780 100644 --- a/Sources/PubNub/Events/New/Subscription.swift +++ b/Sources/PubNub/Events/New/Subscription.swift @@ -158,7 +158,7 @@ extension Subscription: SubscribeCapable { let channels = subscriptionType == .channel ? [self] : [] let groups = subscriptionType == .channelGroup ? [self] : [] - pubnub.internalUnsubscribe(from: channels, and: groups, presenceOnly: false) + pubnub.internalUnsubscribe(from: channels, and: groups) } } @@ -211,7 +211,7 @@ extension Subscription: SubscribeMessagesReceiver { fileprivate func isMatchingEntityName(_ entityName: String, string: String) -> Bool { guard entityName.hasSuffix(".*") else { - return entityName == string + return entityName.trimmingPresenceChannelSuffix == string } if let firstIndex = entityName.lastIndex(of: "."), let secondIndex = string.lastIndex(of: ".") { return entityName.prefix(upTo: firstIndex) == string.prefix(upTo: secondIndex) diff --git a/Sources/PubNub/Events/New/SubscriptionSet.swift b/Sources/PubNub/Events/New/SubscriptionSet.swift index 3f8c2ff6..992987fb 100644 --- a/Sources/PubNub/Events/New/SubscriptionSet.swift +++ b/Sources/PubNub/Events/New/SubscriptionSet.swift @@ -31,14 +31,14 @@ public final class SubscriptionSet: EventListenerInterface, SubscriptionDisposab /// A unique identifier for the current `SubscriptionSet` public let uuid: UUID = UUID() /// Whether current subscription is disposed or not - public private(set) var isDisposed = false - // Internally holds a collection of child subscriptions - private(set) var currentSubscriptions: Set - // Stores additional listeners - private var listenersContainer: SubscriptionListenersContainer = .init() + public var isDisposed: Bool { isDisposedContainer.lockedRead { $0 } } + + let isDisposedContainer: Atomic = Atomic(false) + let currentSubscriptions: Atomic> + let listenersContainer: SubscriptionListenersContainer = .init() // Internally intercepts messages from the Subscribe loop - // and forwards them to the current `SubscriptionSet` + // and forwards them to the current instance lazy var adapter = BaseSubscriptionListenerAdapter( receiver: self, uuid: uuid, @@ -58,7 +58,7 @@ public final class SubscriptionSet: EventListenerInterface, SubscriptionDisposab ) { self.queue = queue self.options = SubscriptionOptions.empty() + options - self.currentSubscriptions = Set(entities.map { Subscription(queue: queue, entity: $0, options: options) }) + self.currentSubscriptions = Atomic(Set(entities.map { Subscription(queue: queue, entity: $0, options: options) })) } /// Initializes `SubscriptionSet` object with the specified parameters. @@ -74,7 +74,7 @@ public final class SubscriptionSet: EventListenerInterface, SubscriptionDisposab ) { self.queue = queue self.options = options - self.currentSubscriptions = Set(subscriptions) + self.currentSubscriptions = Atomic(Set(subscriptions)) } /// Adds `Subscription` to the existing set of subscriptions. @@ -82,7 +82,7 @@ public final class SubscriptionSet: EventListenerInterface, SubscriptionDisposab /// - Parameters: /// - subscription: `Subscription` to add public func add(subscription: Subscription) { - currentSubscriptions.insert(subscription) + currentSubscriptions.lockedWrite { $0.insert(subscription) } } /// Adds a collection of `Subscription` to the existing set of subscriptions. @@ -90,8 +90,10 @@ public final class SubscriptionSet: EventListenerInterface, SubscriptionDisposab /// - Parameters: /// - subscriptions: List of `Subscription` to add public func add(subscriptions: any Collection) { - subscriptions.forEach { - currentSubscriptions.insert($0) + currentSubscriptions.lockedWrite { + for subscription in subscriptions { + $0.insert(subscription) + } } } @@ -100,7 +102,7 @@ public final class SubscriptionSet: EventListenerInterface, SubscriptionDisposab /// - Parameters: /// - subscription: `Subscription` to remove public func remove(subscription: Subscription) { - currentSubscriptions.remove(subscription) + currentSubscriptions.lockedWrite { $0.remove(subscription) } } /// Removes a collection of `Subscription` from the existing set of subscriptions. @@ -108,8 +110,10 @@ public final class SubscriptionSet: EventListenerInterface, SubscriptionDisposab /// - Parameters: /// - subscriptions: Collection of `Subscription` to remove public func remove(subscriptions: any Collection) { - subscriptions.forEach { - currentSubscriptions.remove($0) + currentSubscriptions.lockedWrite { + for subscription in subscriptions { + $0.remove(subscription) + } } } @@ -118,12 +122,15 @@ public final class SubscriptionSet: EventListenerInterface, SubscriptionDisposab /// Use this method to create a new instance with the same configuration as the current `SubscriptionSet`. /// The clone is a separate instance that can be used independently. public func clone() -> SubscriptionSet { + let existingSubscriptions = currentSubscriptions.lockedRead { $0 } + let clonedSubscriptionSet = SubscriptionSet( queue: queue, - subscriptions: currentSubscriptions.map { $0.clone() }, + subscriptions: existingSubscriptions.map { $0.clone() }, options: options ) - if let pubnub = currentSubscriptions.first?.pubnub, pubnub.hasRegisteredAdapter(with: uuid) { + + if let pubnub = existingSubscriptions.first?.pubnub, pubnub.hasRegisteredAdapter(with: uuid) { pubnub.registerAdapter(clonedSubscriptionSet.adapter) } return clonedSubscriptionSet @@ -135,9 +142,9 @@ public final class SubscriptionSet: EventListenerInterface, SubscriptionDisposab /// Once disposed, the subscription interface cannot be restarted. public func dispose() { clearCallbacks() - currentSubscriptions.forEach { $0.dispose() } + currentSubscriptions.lockedRead { $0.forEach { $0.dispose() } } removeAllListeners() - isDisposed = true + isDisposedContainer.lockedWrite { $0 = true } } /// Adds additional subscription listener @@ -169,16 +176,19 @@ extension SubscriptionSet: SubscribeCapable { /// /// - Parameter timetoken: The timetoken to use for the subscriptions public func subscribe(with timetoken: Timetoken?) { - guard let pubnub = currentSubscriptions.first?.pubnub, !isDisposed else { + let existingSubscriptions = currentSubscriptions.lockedRead { $0 } + + guard let pubnub = existingSubscriptions.first?.pubnub, !isDisposed else { return } + pubnub.registerAdapter(adapter) - let channels = currentSubscriptions.filter { + let channels = existingSubscriptions.filter { $0.subscriptionType == .channel }.allObjects - let groups = currentSubscriptions.filter { + let groups = existingSubscriptions.filter { $0.subscriptionType == .channelGroup }.allObjects @@ -196,19 +206,21 @@ extension SubscriptionSet: SubscribeCapable { /// Use this method to gracefully end all subscriptions and stop receiving messages for all /// associated entities. After unsubscribing, the subscription set can be restarted if needed. public func unsubscribe() { - guard let pubnub = currentSubscriptions.first?.pubnub, !isDisposed else { + let existingSubscriptions = currentSubscriptions.lockedRead { $0 } + + guard let pubnub = existingSubscriptions.first?.pubnub, !isDisposed else { return } + pubnub.subscription.remove(adapter) pubnub.internalUnsubscribe( - from: currentSubscriptions.filter { $0.subscriptionType == .channel }, - and: currentSubscriptions.filter { $0.subscriptionType == .channelGroup }, - presenceOnly: false + from: existingSubscriptions.filter { $0.subscriptionType == .channel }, + and: existingSubscriptions.filter { $0.subscriptionType == .channelGroup } ) } } -// MARK: - SubscribeMessagePayloadReceiver +// MARK: - SubscribeMessagesReceiver extension SubscriptionSet: SubscribeMessagesReceiver { var subscriptionTopology: [SubscribableType: [String]] { @@ -216,7 +228,9 @@ extension SubscriptionSet: SubscribeMessagesReceiver { result[.channel] = [] result[.channelGroup] = [] - return currentSubscriptions.reduce(into: result, { accumulatedRes, current in + let existingSubscriptions = currentSubscriptions.lockedRead { $0 } + + return existingSubscriptions.reduce(into: result, { accumulatedRes, current in let currentRes = current.subscriptionTopology accumulatedRes[.channel]?.append(contentsOf: currentRes[.channel] ?? []) accumulatedRes[.channelGroup]?.append(contentsOf: currentRes[.channelGroup] ?? []) @@ -230,7 +244,9 @@ extension SubscriptionSet: SubscribeMessagesReceiver { // 3. Checks the events result received in the previous step against SubscriptionSet's options // 4. Emits filtered events from SubscriptionSet and to additional listeners attached @discardableResult func onPayloadsReceived(payloads: [SubscribeMessagePayload]) -> [PubNubEvent] { - currentSubscriptions.reduce(into: [PubNubEvent]()) { accumulatedRes, childSubscription in + let existingSubscriptions = currentSubscriptions.lockedRead { $0 } + + return existingSubscriptions.reduce(into: [PubNubEvent]()) { accumulatedRes, childSubscription in let events = payloads.compactMap { payload in childSubscription.event(from: payload) }.filter { diff --git a/Sources/PubNub/Extensions/String+PubNub.swift b/Sources/PubNub/Extensions/String+PubNub.swift index 9c8adad7..e6064aa0 100644 --- a/Sources/PubNub/Extensions/String+PubNub.swift +++ b/Sources/PubNub/Extensions/String+PubNub.swift @@ -12,17 +12,17 @@ import Foundation extension String { /// A channel name conforming to PubNub presence channel naming conventions - var presenceChannelName: String { + public var presenceChannelName: String { return "\(self)\(Constant.presenceChannelSuffix)" } /// If the `String` conforms to PubNub presence channel naming conventions - var isPresenceChannelName: Bool { + public var isPresenceChannelName: Bool { return hasSuffix(Constant.presenceChannelSuffix) } /// If the `String` conforms to PubNub presence channel naming conventions - var trimmingPresenceChannelSuffix: String { + public var trimmingPresenceChannelSuffix: String { if isPresenceChannelName { return String(dropLast(Constant.presenceChannelSuffix.count)) } diff --git a/Sources/PubNub/Helpers/Constants.swift b/Sources/PubNub/Helpers/Constants.swift index 0d2a3af7..84ee4037 100644 --- a/Sources/PubNub/Helpers/Constants.swift +++ b/Sources/PubNub/Helpers/Constants.swift @@ -57,7 +57,7 @@ public enum Constant { static let pubnubSwiftSDKName: String = "PubNubSwift" - static let pubnubSwiftSDKVersion: String = "9.2.3" + static let pubnubSwiftSDKVersion: String = "9.3.0" static let appBundleId: String = { if let info = Bundle.main.infoDictionary, diff --git a/Sources/PubNub/KMP/Wrappers/KMPSubscription.swift b/Sources/PubNub/KMP/Wrappers/KMPSubscription.swift index 9ffd6e55..ede44a9c 100644 --- a/Sources/PubNub/KMP/Wrappers/KMPSubscription.swift +++ b/Sources/PubNub/KMP/Wrappers/KMPSubscription.swift @@ -156,7 +156,7 @@ public class KMPSubscriptionSet: NSObject { @objc public func addListener(_ listener: KMPEventListener) { - let pubnub = subscriptionSet.currentSubscriptions.first?.entity.pubnub + let pubnub = subscriptionSet.currentSubscriptions.lockedRead { $0.first }?.entity.pubnub let eventListener = EventListener(uuid: listener.uuid) eventListener.onMessage = { diff --git a/Sources/PubNub/PubNub.swift b/Sources/PubNub/PubNub.swift index 6e11fa02..7c68d616 100644 --- a/Sources/PubNub/PubNub.swift +++ b/Sources/PubNub/PubNub.swift @@ -414,12 +414,29 @@ public extension PubNub { ), category: .pubNub ) + let finalChanelList = if withPresence { + channels + channels.map { $0.presenceChannelName } + } else { + channels + } + + let finalChannelGroupList = if withPresence { + channelGroups + channelGroups.map { $0.presenceChannelName } + } else { + channelGroups + } + + let channelSubscriptions = Set(finalChanelList).compactMap { + channel($0).subscription(queue: queue) + } + let channelGroupSubscriptions = Set(finalChannelGroupList).compactMap { + channelGroup($0).subscription(queue: queue) + } + subscription.subscribe( - to: channels, - and: channelGroups, - at: SubscribeCursor(timetoken: timetoken), - withPresence: withPresence, - using: self + to: channelSubscriptions, + and: channelGroupSubscriptions, + at: SubscribeCursor(timetoken: timetoken) ) } @@ -428,22 +445,19 @@ public extension PubNub { /// - Parameters: /// - from: List of channels to unsubscribe from /// - and: List of channel groups to unsubscribe from - /// - presenceOnly: If true, it only unsubscribes from presence events on the specified channels. - func unsubscribe(from channels: [String], and channelGroups: [String] = [], presenceOnly: Bool = false) { + func unsubscribe(from channels: [String], and channelGroups: [String] = []) { PubNub.log.debug( String.formattedDescription( "Executing unsubscribe", arguments: [ ("from", channels), - ("and", channelGroups), - ("presenceOnly", presenceOnly) + ("and", channelGroups) ] ), category: .pubNub ) subscription.unsubscribe( from: channels, - and: channelGroups, - presenceOnly: presenceOnly + and: channelGroups ) } @@ -544,13 +558,11 @@ extension PubNub { func internalUnsubscribe( from channels: [Subscription], - and groups: [Subscription], - presenceOnly: Bool + and groups: [Subscription] ) { subscription.internalUnsubscribe( from: channels, - and: groups, - presenceOnly: presenceOnly + and: groups ) } } diff --git a/Sources/PubNub/Subscription/Strategy/EventEngineSubscriptionSessionStrategy.swift b/Sources/PubNub/Subscription/Strategy/EventEngineSubscriptionSessionStrategy.swift index 89ccd1dc..2711dbf0 100644 --- a/Sources/PubNub/Subscription/Strategy/EventEngineSubscriptionSessionStrategy.swift +++ b/Sources/PubNub/Subscription/Strategy/EventEngineSubscriptionSessionStrategy.swift @@ -45,11 +45,11 @@ class EventEngineSubscriptionSessionStrategy: SubscriptionSessionStrategy { } var subscribedChannels: [String] { - subscribeEngine.state.input.subscribedChannelNames + subscribeEngine.state.input.channelNames(withPresence: true) } var subscribedChannelGroups: [String] { - subscribeEngine.state.input.subscribedGroupNames + subscribeEngine.state.input.channelGroupNames(withPresence: true) } var subscriptionCount: Int { @@ -101,98 +101,97 @@ class EventEngineSubscriptionSessionStrategy: SubscriptionSessionStrategy { private func onFilterExpressionChanged() { let currentState = subscribeEngine.state - let channels = currentState.input.allSubscribedChannelNames - let groups = currentState.input.allSubscribedGroupNames + let channels = currentState.input.channelNames(withPresence: true) + let groups = currentState.input.channelGroupNames(withPresence: true) sendSubscribeEvent(event: .subscriptionChanged(channels: channels, groups: groups)) } func subscribe( - to channels: [PubNubChannel], - and groups: [PubNubChannel], + to channels: [String], + and channelGroups: [String], at cursor: SubscribeCursor? ) { - let currentChannelsAndGroups = subscribeEngine.state.input - let insertionResult = currentChannelsAndGroups.adding(channels: channels, and: groups) - let newChannelsAndGroups = insertionResult.newInput + let currentInput = subscribeEngine.state.input + let newInput = currentInput.adding(channels: Set(channels), and: Set(channelGroups)) + let diff = newInput.difference(from: currentInput) if let cursor = cursor, cursor.timetoken != 0 { sendSubscribeEvent(event: .subscriptionRestored( - channels: newChannelsAndGroups.allSubscribedChannelNames, - groups: newChannelsAndGroups.allSubscribedGroupNames, + channels: newInput.channelNames(withPresence: true), + groups: newInput.channelGroupNames(withPresence: true), cursor: cursor )) sendPresenceEvent(event: .joined( - channels: newChannelsAndGroups.subscribedChannelNames, - groups: newChannelsAndGroups.subscribedGroupNames + channels: newInput.channelNames(withPresence: false), + groups: newInput.channelGroupNames(withPresence: false) )) - } else if newChannelsAndGroups != currentChannelsAndGroups { + } else if currentInput != newInput { sendSubscribeEvent(event: .subscriptionChanged( - channels: newChannelsAndGroups.allSubscribedChannelNames, - groups: newChannelsAndGroups.allSubscribedGroupNames + channels: newInput.channelNames(withPresence: true), + groups: newInput.channelGroupNames(withPresence: true) )) sendPresenceEvent(event: .joined( - channels: newChannelsAndGroups.subscribedChannelNames, - groups: newChannelsAndGroups.subscribedGroupNames + channels: newInput.channelNames(withPresence: false), + groups: newInput.channelGroupNames(withPresence: false) )) } else { // No unique channels or channel groups were provided. // There's no need to alter the Subscribe loop. } - if !insertionResult.insertedChannels.isEmpty || !insertionResult.insertedGroups.isEmpty { + + if !diff.addedChannels.isEmpty || !diff.addedChannelGroups.isEmpty { notify { $0.emit(subscribe: .subscriptionChanged( .subscribed( - channels: insertionResult.insertedChannels, - groups: insertionResult.insertedGroups + channels: diff.addedChannels.map { PubNubChannel(channel: $0) }.consolidated(), + groups: diff.removedChannels.map { PubNubChannel(channel: $0) }.consolidated() )) ) } } } - func unsubscribeFrom( - mainChannels: [PubNubChannel], - presenceChannelsOnly: [PubNubChannel], - mainGroups: [PubNubChannel], - presenceGroupsOnly: [PubNubChannel] + func unsubscribe( + from channels: [String], + and channelGroups: [String] ) { - // Retrieve the current list of subscribed channels and channel groups - let currentChannelsAndGroups = subscribeEngine.state.input - // Provides the outcome after updating the list of channels and channel groups - let removingResult = currentChannelsAndGroups.removing( - mainChannels: mainChannels, presenceChannelsOnly: presenceChannelsOnly, - mainGroups: mainGroups, presenceGroupsOnly: presenceGroupsOnly - ) + let currentInput = subscribeEngine.state.input + let newInput = currentInput.removing(channels: Set(channels), and: Set(channelGroups)) - // Exits if there are no differences for channels or channel groups - guard removingResult.newInput != currentChannelsAndGroups else { - return - } - if configuration.maintainPresenceState { - presenceStateContainer.removeState(forChannels: removingResult.removedChannels.map { $0.id }) - } - // Dispatch local event first to guarantee the expected order of events. - // An event indicating unsubscribing from channels and channel groups - // should be emitted before an event related to disconnecting - // from the Subscribe loop, assuming you unsubscribed from all channels - // and channel groups - notify { - $0.emit(subscribe: .subscriptionChanged( - .unsubscribed( - channels: removingResult.removedChannels, - groups: removingResult.removedGroups - )) - ) + if currentInput != newInput { + + let diff = newInput.difference(from: currentInput) + let removedMainChannels = diff.removedChannels.filter { !$0.isPresenceChannelName }.allObjects + let removedMainChannelGroups = diff.removedChannelGroups.filter { !$0.isPresenceChannelName }.allObjects + + // Dispatch local event first to guarantee the expected order of events. + // An event indicating unsubscribing from channels and channel groups + // should be emitted before an event related to disconnecting + // from the Subscribe loop, assuming you unsubscribed from all channels + // and channel groups + notify { + $0.emit(subscribe: .subscriptionChanged( + .unsubscribed( + channels: diff.removedChannels.map { PubNubChannel(channel: $0) }.consolidated(), + groups: diff.removedChannelGroups.map { PubNubChannel(channel: $0) }.consolidated() + )) + ) + } + + if configuration.maintainPresenceState { + presenceStateContainer.removeState(forChannels: removedMainChannels) + } + + sendSubscribeEvent(event: .subscriptionChanged( + channels: newInput.channelNames(withPresence: true), + groups: newInput.channelGroupNames(withPresence: true) + )) + sendPresenceEvent(event: .left( + channels: removedMainChannels, + groups: removedMainChannelGroups + )) } - sendSubscribeEvent(event: .subscriptionChanged( - channels: removingResult.newInput.allSubscribedChannelNames, - groups: removingResult.newInput.allSubscribedGroupNames - )) - sendPresenceEvent(event: .left( - channels: removingResult.removedChannels.map { $0.id }, - groups: removingResult.removedGroups.map { $0.id } - )) } func reconnect(at cursor: SubscribeCursor?) { @@ -215,8 +214,8 @@ class EventEngineSubscriptionSessionStrategy: SubscriptionSessionStrategy { notify { $0.emit(subscribe: .subscriptionChanged( .unsubscribed( - channels: currentInput.channels, - groups: currentInput.groups + channels: currentInput.channelNames(withPresence: true).map { PubNubChannel(channel: $0) }.consolidated(), + groups: currentInput.channelGroupNames(withPresence: true).map { PubNubChannel(channel: $0) }.consolidated() ) )) } diff --git a/Sources/PubNub/Subscription/Strategy/LegacySubscriptionSessionStrategy.swift b/Sources/PubNub/Subscription/Strategy/LegacySubscriptionSessionStrategy.swift index 69660e6a..858301c1 100644 --- a/Sources/PubNub/Subscription/Strategy/LegacySubscriptionSessionStrategy.swift +++ b/Sources/PubNub/Subscription/Strategy/LegacySubscriptionSessionStrategy.swift @@ -95,14 +95,14 @@ class LegacySubscriptionSessionStrategy: SubscriptionSessionStrategy { // MARK: - Subscription Loop func subscribe( - to channels: [PubNubChannel], - and groups: [PubNubChannel], + to channels: [String], + and channelGroups: [String], at cursor: SubscribeCursor? ) { let subscribeChange = internalState.lockedWrite { state -> SubscriptionChangeEvent in .subscribed( - channels: channels.filter { state.channels.insert($0) }, - groups: groups.filter { state.groups.insert($0) } + channels: channels.map { PubNubChannel(channel: $0) }.filter { state.channels.insert($0) }.consolidated(), + groups: channelGroups.map { PubNubChannel(channel: $0) }.filter { state.groups.insert($0) }.consolidated() ) } if subscribeChange.didChange { @@ -273,24 +273,24 @@ class LegacySubscriptionSessionStrategy: SubscriptionSessionStrategy { // MARK: - Unsubscribe - func unsubscribeFrom( - mainChannels: [PubNubChannel], - presenceChannelsOnly: [PubNubChannel], - mainGroups: [PubNubChannel], - presenceGroupsOnly: [PubNubChannel] - ) { + func unsubscribe(from channels: [String], and channelGroups: [String]) { let subscribeChange = internalState.lockedWrite { state -> SubscriptionChangeEvent in .unsubscribed( - channels: mainChannels.compactMap { - state.channels.removeValue(forKey: $0.id) - } + presenceChannelsOnly.compactMap { - state.channels.unsubscribePresence($0.id) - }, - groups: mainGroups.compactMap { - state.groups.removeValue(forKey: $0.id) - } + presenceGroupsOnly.compactMap { - state.groups.unsubscribePresence($0.id) - } + channels: channels.compactMap { + if $0.isPresenceChannelName { + return state.channels.unsubscribePresence($0) + } else { + return state.channels.removeValue(forKey: $0) + } + }.consolidated(), + + groups: channelGroups.compactMap { + if $0.isPresenceChannelName { + return state.groups.unsubscribePresence($0) + } else { + return state.groups.removeValue(forKey: $0) + } + }.consolidated() ) } if subscribeChange.didChange { @@ -313,7 +313,10 @@ class LegacySubscriptionSessionStrategy: SubscriptionSessionStrategy { let removedGroups = mutableState.groups mutableState.groups.removeAll(keepingCapacity: true) - return .unsubscribed(channels: removedChannels.map { $0.value }, groups: removedGroups.map { $0.value }) + return .unsubscribed( + channels: removedChannels.map { $0.value }.consolidated(), + groups: removedGroups.map { $0.value }.consolidated() + ) } if subscribeChange.didChange { diff --git a/Sources/PubNub/Subscription/Strategy/SubscriptionSessionStrategy.swift b/Sources/PubNub/Subscription/Strategy/SubscriptionSessionStrategy.swift index 7cc8b888..52bd7e22 100644 --- a/Sources/PubNub/Subscription/Strategy/SubscriptionSessionStrategy.swift +++ b/Sources/PubNub/Subscription/Strategy/SubscriptionSessionStrategy.swift @@ -21,17 +21,8 @@ protocol SubscriptionSessionStrategy: AnyObject { var filterExpression: String? { get set } var listeners: WeakSet { get set } - func subscribe( - to channels: [PubNubChannel], - and groups: [PubNubChannel], - at cursor: SubscribeCursor? - ) - func unsubscribeFrom( - mainChannels: [PubNubChannel], - presenceChannelsOnly: [PubNubChannel], - mainGroups: [PubNubChannel], - presenceGroupsOnly: [PubNubChannel] - ) + func subscribe(to channels: [String], and channelGroups: [String], at cursor: SubscribeCursor?) + func unsubscribe(from channels: [String], and channelGroups: [String]) func reconnect(at cursor: SubscribeCursor?) func disconnect() diff --git a/Sources/PubNub/Subscription/SubscriptionSession.swift b/Sources/PubNub/Subscription/SubscriptionSession.swift index 9d2ef441..df027365 100644 --- a/Sources/PubNub/Subscription/SubscriptionSession.swift +++ b/Sources/PubNub/Subscription/SubscriptionSession.swift @@ -71,8 +71,8 @@ class SubscriptionSession: EventListenerInterface, StatusListenerInterface { return statusListener }() - private var globalChannelSubscriptions: Atomic<[String: Subscription]> = Atomic([:]) - private var globalGroupSubscriptions: Atomic<[String: Subscription]> = Atomic([:]) + private let globalChannelSubscriptions: Atomic<[String: Subscription]> = Atomic([:]) + private let globalGroupSubscriptions: Atomic<[String: Subscription]> = Atomic([:]) private let strategy: any SubscriptionSessionStrategy init( @@ -110,24 +110,10 @@ class SubscriptionSession: EventListenerInterface, StatusListenerInterface { // MARK: - Subscription Loop func subscribe( - to channels: [String], - and groups: [String] = [], - at cursor: SubscribeCursor? = nil, - withPresence: Bool = false, - using pubnub: PubNub + to channelSubscriptions: [Subscription], + and channelGroupSubscriptions: [Subscription] = [], + at cursor: SubscribeCursor? = nil ) { - let channelSubscriptions = Set(channels).compactMap { - pubnub.channel($0).subscription( - queue: queue, - options: withPresence ? ReceivePresenceEvents() : SubscriptionOptions.empty() - ) - } - let channelGroupSubscriptions = Set(groups).compactMap { - pubnub.channelGroup($0).subscription( - queue: queue, - options: withPresence ? ReceivePresenceEvents() : SubscriptionOptions.empty() - ) - } internalSubscribe( with: channelSubscriptions, and: channelGroupSubscriptions, @@ -174,33 +160,35 @@ class SubscriptionSession: EventListenerInterface, StatusListenerInterface { func unsubscribe( from channels: [String], - and groups: [String] = [], - presenceOnly: Bool = false + and channelGroups: [String] = [] ) { - let channelNamesToUnsubscribe = channels.flatMap { - presenceOnly ? [$0.presenceChannelName] : [$0, $0.presenceChannelName] + let channelsToUnsubscribe = channels.flatMap { $0.isPresenceChannelName ? [$0] : [$0, $0.presenceChannelName] } + let channelGroupsToUnsubscribe = channelGroups.flatMap { $0.isPresenceChannelName ? [$0] : [$0, $0.presenceChannelName] } + + let matchingChannelsToUnsubscribe = globalChannelSubscriptions.lockedRead { + $0.compactMap { + channelsToUnsubscribe.contains($0.key) ? $0.value : nil + } } - let groupNamesToUnsubscribe = groups.flatMap { - presenceOnly ? [$0.presenceChannelName] : [$0, $0.presenceChannelName] + let matchingChannelGroupsToUnsubscribe = globalGroupSubscriptions.lockedRead { + $0.compactMap { + channelGroupsToUnsubscribe.contains($0.key) ? $0.value : nil + } } + internalUnsubscribe( - from: globalChannelSubscriptions.lockedRead { $0.compactMap { - channelNamesToUnsubscribe.contains($0.key) ? $0.value : nil - } }, - and: globalGroupSubscriptions.lockedRead { $0.compactMap { - groupNamesToUnsubscribe.contains($0.key) ? $0.value : nil - } }, - presenceOnly: presenceOnly + from: matchingChannelsToUnsubscribe, + and: matchingChannelGroupsToUnsubscribe ) globalChannelSubscriptions.lockedWrite { currentContainer in - channelNamesToUnsubscribe.forEach { + channelsToUnsubscribe.forEach { currentContainer.removeValue(forKey: $0) } } globalGroupSubscriptions.lockedWrite { currentContainer in - groupNamesToUnsubscribe.forEach { + channelGroupsToUnsubscribe.forEach { currentContainer.removeValue(forKey: $0) } } @@ -225,159 +213,77 @@ extension SubscriptionSession { add(adapter) } - // Maps the raw channel/channel group array to collections of PubNubChannel that should be subscribed to - // with and without Presence, respectively. - private typealias SubscribeRetrievalRes = ( - itemsWithPresenceIncluded: [PubNubChannel], - itemsWithoutPresence: [PubNubChannel] - ) - // Maps the raw channel/channel group array to collections of `PubNubChannel` that should be unsubscribed to. - private typealias UnsubscribeRetrievalRes = ( - presenceOnlyItems: [PubNubChannel], - mainItems: [PubNubChannel] - ) - // Composes final PubNubChannel lists the user should subscribe to // according to provided raw input and forwards the result to the underlying Subscription strategy. func internalSubscribe( with channels: [Subscription], - and groups: [Subscription], + and channelGroups: [Subscription], at timetoken: Timetoken? ) { - if channels.isEmpty, groups.isEmpty { + if channels.isEmpty, channelGroups.isEmpty { return } - - let extractingChannelsRes = retrieveItemsToSubscribe(from: channels) - let extractingGroupsRes = retrieveItemsToSubscribe(from: groups) - for channelSubscription in channels { registerAdapter(channelSubscription.adapter) } - for groupSubscription in groups { + for groupSubscription in channelGroups { registerAdapter(groupSubscription.adapter) } + strategy.subscribe( - to: extractingChannelsRes.itemsWithPresenceIncluded + extractingChannelsRes.itemsWithoutPresence, - and: extractingGroupsRes.itemsWithPresenceIncluded + extractingGroupsRes.itemsWithoutPresence, + to: channels.flatMap { $0.subscriptionNames }, + and: channelGroups.flatMap { $0.subscriptionNames }, at: SubscribeCursor(timetoken: timetoken) ) } - private func retrieveItemsToSubscribe(from subscriptions: [Subscription]) -> SubscribeRetrievalRes { - // Detects all Presence channels from provided String array and maps them into PubNubChannel - // containing the main channel name and the flag indicating the resulting PubNubChannel is subscribed - // with Presence. Note that Presence channels are supplementary to the main data channels. - // Therefore, subscribing to a Presence channel alone without its corresponding main channel is not supported. - let channelsWithPresenceIncluded = Set(subscriptions.flatMap { - $0.subscriptionNames - }.filter { - $0.isPresenceChannelName - }).map { - PubNubChannel(channel: $0) - } - - // Detects remaining main channel names without Presence enabled from provided input and ensuring - // there are no duplicates with the result received from the previous step - let channelsWithoutPresence = Set(subscriptions.flatMap { - $0.subscriptionNames - }.map { - $0.trimmingPresenceChannelSuffix - }).symmetricDifference(channelsWithPresenceIncluded.map { - $0.id - }).map { - PubNubChannel(id: $0, withPresence: false) - } - - return SubscribeRetrievalRes( - itemsWithPresenceIncluded: channelsWithPresenceIncluded, - itemsWithoutPresence: channelsWithoutPresence - ) - } - func internalUnsubscribe( from channels: [Subscription], - and channelGroups: [Subscription], - presenceOnly: Bool + and channelGroups: [Subscription] ) { - let extractingChannelsRes = extractItemsToUnsubscribe( - from: channels, - presenceItemsOnly: presenceOnly - ) - let extractingGroupsRes = extractItemsToUnsubscribe( - from: channelGroups, - presenceItemsOnly: presenceOnly - ) + let channelsToUnsubscribe = resolveItemsToUnsubscribe(from: channels) + let channelGroupsToUnsubscribe = resolveItemsToUnsubscribe(from: channelGroups) + for channelSubscription in channels { remove(channelSubscription.adapter) } for channelGroupSubscription in channelGroups { remove(channelGroupSubscription.adapter) } - strategy.unsubscribeFrom( - mainChannels: extractingChannelsRes.mainItems, - presenceChannelsOnly: extractingChannelsRes.presenceOnlyItems, - mainGroups: extractingGroupsRes.mainItems, - presenceGroupsOnly: extractingGroupsRes.presenceOnlyItems + + strategy.unsubscribe( + from: channelsToUnsubscribe, + and: channelGroupsToUnsubscribe ) } - // Returns an array of subscriptions, excluding the given subscription and the global listener, - // that subscribe to at least one name in common with the given subscription - func matchingSubscriptions(for subscription: Subscription, presenceOnly: Bool) -> [SubscribeMessagesReceiver] { - let allSubscriptions = strategy.listeners.compactMap { + // Returns a boolean indicating whether there are subscription objects that subscribe to at least one name + // in common with the given subscription + func hasOverlappingSubscriptions(for subscription: Subscription) -> Bool { + let remainingSubscriptions = strategy.listeners.allObjects.compactMap { $0 as? BaseSubscriptionListenerAdapter + }.filter { + // Exclude the subscription being checked and the internal global events listener + // since the global listener is not a user-triggered subscription + $0.uuid != subscription.uuid && $0.uuid != globalEventsListener.uuid } - let namesToFind = subscription.subscriptionNames.filter { - presenceOnly ? $0.isPresenceChannelName : true - } - return allSubscriptions.compactMap { - if $0.uuid != subscription.uuid && $0.uuid != globalEventsListener.uuid { - return $0.receiver - } else { - return nil - } + let matchingSubscriptions = remainingSubscriptions.compactMap { + $0.receiver }.filter { - !(Set($0.subscriptionTopology[subscription.subscriptionType] ?? []).isDisjoint(with: namesToFind)) + !Set($0.subscriptionTopology[subscription.subscriptionType] ?? []).isDisjoint(with: subscription.subscriptionNames) } - } - // Creates the final list of Presence channels/channel groups and main channels/channel groups - // the user should unsubscribe from according to the following rules: - // - // 1. Unsubscribing from the main channel happens if: - // * There are no references to its Presence equivalent from other subscriptions - // * There are no references to the main channel from other subscriptions - // 2. Unsubscribing from the Presence channel happens if: - // * There are no references to it from other subscriptions - private func extractItemsToUnsubscribe( - from subscriptions: [Subscription], - presenceItemsOnly: Bool - ) -> UnsubscribeRetrievalRes { - let presenceItems = Set(subscriptions.filter { - matchingSubscriptions(for: $0, presenceOnly: true).isEmpty - }.flatMap { - $0.subscriptionNames - }).filter { - $0.isPresenceChannelName - }.map { - PubNubChannel(channel: $0) - } + return !matchingSubscriptions.isEmpty + } - let channels = presenceItemsOnly ? [] : Set(subscriptions.filter { - matchingSubscriptions(for: $0, presenceOnly: false).isEmpty && matchingSubscriptions(for: $0, presenceOnly: true).isEmpty - }.flatMap { - $0.subscriptionNames - }).symmetricDifference(presenceItems.map { - $0.presenceId - }).map { - PubNubChannel(id: $0, withPresence: false) + private func resolveItemsToUnsubscribe(from subscriptions: [Subscription]) -> [String] { + return subscriptions.flatMap { + if !hasOverlappingSubscriptions(for: $0) { + return $0.subscriptionNames + } else { + return [] + } } - - return UnsubscribeRetrievalRes( - presenceOnlyItems: presenceItems, - mainItems: channels - ) } } diff --git a/Sources/PubNub/Subscription/SubscriptionState.swift b/Sources/PubNub/Subscription/SubscriptionState.swift index 677bae8d..717ab79a 100644 --- a/Sources/PubNub/Subscription/SubscriptionState.swift +++ b/Sources/PubNub/Subscription/SubscriptionState.swift @@ -76,14 +76,13 @@ public struct PubNubChannel: Hashable { /// If the channel is currently subscribed with presence public let isPresenceSubscribed: Bool - public init(id: String, withPresence: Bool = false) { + init(id: String, withPresence: Bool = false) { self.id = id presenceId = id.presenceChannelName isPresenceSubscribed = withPresence } - /// Detects if the string is a Presence channel name and sets the appropriate values - public init(channel: String) { + init(channel: String) { if channel.isPresenceChannelName { id = channel.trimmingPresenceChannelSuffix presenceId = channel @@ -96,26 +95,41 @@ public struct PubNubChannel: Hashable { } } -extension PubNubChannel: Codable { - enum CodingKeys: String, CodingKey { - case id - case presenceId - case isPresenceSubscribed +extension Array where Element == PubNubChannel { + /// Returns consolidated channels that merge duplicate channels with their presence counterparts + /// + /// This method groups PubNubChannel instances by their main channel ID, detecting duplicates + /// representing the same logical channel with different presence settings and merging them + /// into single consolidated instances. + func consolidated() -> [PubNubChannel] { + let consolidatedMap = self.reduce(into: [String: Bool]()) { result, channel in + /// Check if we've already processed this channel ID and if any previous instance had presence enabled + result[channel.id] = (result[channel.id] ?? false) || channel.isPresenceSubscribed + } + return consolidatedMap.map { channelId, hasPresence in + PubNubChannel(id: channelId, withPresence: hasPresence) + }.sorted { $0.id < $1.id } } +} - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - id = try container.decode(String.self, forKey: .id) - presenceId = try container.decode(String.self, forKey: .presenceId) - isPresenceSubscribed = try container.decode(Bool.self, forKey: .isPresenceSubscribed) +extension Dictionary where Key == String, Value == PubNubChannel { + // Inserts and returns the provided channel if that channel doesn't already exist + mutating func insert(_ channel: Value) -> Bool { + if let match = self[channel.id], match == channel { + return false + } + self[channel.id] = channel + return true } - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(id, forKey: .id) - try container.encode(presenceId, forKey: .presenceId) - try container.encode(isPresenceSubscribed, forKey: .isPresenceSubscribed) + // Updates current Dictionary with the new channel value unsubscribed from Presence. + // Returns the updated value if the corresponding entry matching the passed `id:` was found, otherwise `nil` + @discardableResult mutating func unsubscribePresence(_ id: String) -> Value? { + if let match = self[id], match.isPresenceSubscribed { + let updatedChannel = PubNubChannel(id: match.id, withPresence: false) + self[match.id] = updatedChannel + return updatedChannel + } + return nil } } diff --git a/Tests/PubNubTests/EventEngine/Subscribe/SubscribeInputTests.swift b/Tests/PubNubTests/EventEngine/Subscribe/SubscribeInputTests.swift index 42951785..97f79bf0 100644 --- a/Tests/PubNubTests/EventEngine/Subscribe/SubscribeInputTests.swift +++ b/Tests/PubNubTests/EventEngine/Subscribe/SubscribeInputTests.swift @@ -14,171 +14,143 @@ import XCTest class SubscribeInputTests: XCTestCase { func test_ChannelsWithoutPresence() { - let input = SubscribeInput(channels: [ - PubNubChannel(id: "first-channel"), - PubNubChannel(id: "second-channel") - ]) - - let expAllSubscribedChannelNames = ["first-channel", "second-channel"] - let expSubscribedChannelNames = ["first-channel", "second-channel"] + let input = SubscribeInput(channels: ["c1", "c2"]) + let expAllSubscribedChannelNames = ["c1", "c2"] + let expSubscribedChannelNames = ["c1", "c2"] - XCTAssertTrue(input.subscribedChannelNames.sorted(by: <).elementsEqual(expSubscribedChannelNames)) - XCTAssertTrue(input.allSubscribedChannelNames.sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) - XCTAssertTrue(input.subscribedGroupNames.isEmpty) - XCTAssertTrue(input.allSubscribedGroupNames.isEmpty) + XCTAssertTrue(input.channelNames(withPresence: true).sorted(by: <).elementsEqual(expSubscribedChannelNames)) + XCTAssertTrue(input.channelNames(withPresence: false).sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) + XCTAssertTrue(input.channelGroupNames(withPresence: true).isEmpty) + XCTAssertTrue(input.channelGroupNames(withPresence: false).isEmpty) } - func test_ChannelsWithPresence() { - let input = SubscribeInput(channels: [ - PubNubChannel(id: "first-channel", withPresence: true), - PubNubChannel(id: "second-channel") - ]) - - let expAllSubscribedChannelNames = ["first-channel", "first-channel-pnpres", "second-channel"] - let expSubscribedChannelNames = ["first-channel", "second-channel"] + func test_WithPresence() { + let input = SubscribeInput(channels: ["c1", "c1-pnpres", "c2"], channelGroups: ["g1", "g1-pnpres", "g2"]) + let expAllSubscribedChannelNames = ["c1", "c1-pnpres", "c2"] + let expSubscribedChannelNames = ["c1", "c2"] + let expAllSubscribedGroups = ["g1", "g1-pnpres", "g2"] + let expSubscribedGroups = ["g1", "g2"] - XCTAssertTrue(input.subscribedChannelNames.sorted(by: <).elementsEqual(expSubscribedChannelNames)) - XCTAssertTrue(input.allSubscribedChannelNames.sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) - XCTAssertTrue(input.subscribedGroupNames.isEmpty) - XCTAssertTrue(input.allSubscribedGroupNames.isEmpty) - } - - func test_ChannelGroups() { - let input = SubscribeInput( - channels: [ - PubNubChannel(id: "first-channel"), - PubNubChannel(id: "second-channel") - ], - groups: [ - PubNubChannel(channel: "group-1"), - PubNubChannel(channel: "group-2") - ] - ) - - let expAllSubscribedChannelNames = ["first-channel", "second-channel"] - let expSubscribedChannelNames = ["first-channel", "second-channel"] - let expAllSubscribedGroupNames = ["group-1", "group-2"] - let expSubscribedGroupNames = ["group-1", "group-2"] - - XCTAssertTrue(input.subscribedChannelNames.sorted(by: <).elementsEqual(expSubscribedChannelNames)) - XCTAssertTrue(input.allSubscribedChannelNames.sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) - XCTAssertTrue(input.subscribedGroupNames.sorted(by: <).elementsEqual(expSubscribedGroupNames)) - XCTAssertTrue(input.allSubscribedGroupNames.sorted(by: <).elementsEqual(expAllSubscribedGroupNames)) + XCTAssertTrue(input.channelNames(withPresence: false).sorted(by: <).elementsEqual(expSubscribedChannelNames)) + XCTAssertTrue(input.channelNames(withPresence: true).sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) + XCTAssertTrue(input.channelGroupNames(withPresence: false).sorted(by: <).elementsEqual(expSubscribedGroups)) + XCTAssertTrue(input.channelGroupNames(withPresence: true).sorted(by: <).elementsEqual(expAllSubscribedGroups)) } func test_addingInputContainsNoDuplicates() { - let input1 = SubscribeInput( + let input = SubscribeInput( channels: [ - PubNubChannel(id: "c1"), - PubNubChannel(id: "c2", withPresence: true) + "c1", + "c2", + "c2-pnpres" ], - groups: [ - PubNubChannel(id: "g1"), - PubNubChannel(id: "g2") + channelGroups: [ + "g1", + "g2" ] ) - let result = input1.adding(channels: [ - PubNubChannel(id: "c1"), - PubNubChannel(id: "c3", withPresence: true) + let newInput = input.adding(channels: [ + "c1", + "c3", + "c3-pnpres" ], and: [ - PubNubChannel(id: "g1"), - PubNubChannel(id: "g3") + "g1", + "g3" ]) + + let diff = newInput.difference(from: input) - let newInput = result.newInput let expAllSubscribedChannelNames = ["c1", "c2", "c2-pnpres", "c3", "c3-pnpres"] let expSubscribedChannelNames = ["c1", "c2", "c3"] let expAllSubscribedGroupNames = ["g1", "g2", "g3"] let expSubscribedGroupNames = ["g1", "g2", "g3"] - XCTAssertTrue(newInput.allSubscribedChannelNames.sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) - XCTAssertTrue(newInput.subscribedChannelNames.sorted(by: <).elementsEqual(expSubscribedChannelNames)) - XCTAssertTrue(newInput.subscribedGroupNames.sorted(by: <).elementsEqual(expSubscribedGroupNames)) - XCTAssertTrue(newInput.allSubscribedGroupNames.sorted(by: <).elementsEqual(expAllSubscribedGroupNames)) - XCTAssertTrue(result.insertedChannels == [PubNubChannel(id: "c3", withPresence: true)]) - XCTAssertTrue(result.insertedGroups == [PubNubChannel(id: "g3")]) + XCTAssertTrue(newInput.channelNames(withPresence: true).sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) + XCTAssertTrue(newInput.channelNames(withPresence: false).sorted(by: <).elementsEqual(expSubscribedChannelNames)) + XCTAssertTrue(newInput.channelGroupNames(withPresence: false).sorted(by: <).elementsEqual(expSubscribedGroupNames)) + XCTAssertTrue(newInput.channelGroupNames(withPresence: true).sorted(by: <).elementsEqual(expAllSubscribedGroupNames)) + XCTAssertTrue(diff.addedChannels == ["c3", "c3-pnpres"]) + XCTAssertTrue(diff.addedChannelGroups == ["g3"]) } func test_RemovingInput() { let input1 = SubscribeInput( channels: [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: true), - PubNubChannel(id: "c3", withPresence: true) + "c1", + "c2", + "c3" ], - groups: [ - PubNubChannel(id: "g1"), - PubNubChannel(id: "g2"), - PubNubChannel(id: "g3") + channelGroups: [ + "g1", + "g2", + "g3" ] ) - let result = input1.removing( - mainChannels: [PubNubChannel(id: "c1"), PubNubChannel(id: "c3")], - presenceChannelsOnly: [], - mainGroups: [PubNubChannel(id: "g1"), PubNubChannel(id: "g3")], - presenceGroupsOnly: [] + let newInput = input1.removing( + channels: ["c1", "c3"], + and: ["g1", "g3"] ) + + let diff = newInput.difference(from: input1) - let newInput = result.newInput - let expAllSubscribedChannelNames = ["c2", "c2-pnpres"] + let expAllSubscribedChannelNames = ["c2"] let expSubscribedChannelNames = ["c2"] let expAllSubscribedGroupNames = ["g2"] let expSubscribedGroupNames = ["g2"] - let expRemovedChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c3", withPresence: true) - ] - let expRemovedGroups = [ - PubNubChannel(id: "g1"), - PubNubChannel(id: "g3") - ] + let expRemovedChannels = Set(["c1", "c3"]) + let expRemovedGroups = Set(["g1", "g3"]) - XCTAssertTrue(newInput.allSubscribedChannelNames.sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) - XCTAssertTrue(newInput.subscribedChannelNames.sorted(by: <).elementsEqual(expSubscribedChannelNames)) - XCTAssertTrue(newInput.subscribedGroupNames.sorted(by: <).elementsEqual(expSubscribedGroupNames)) - XCTAssertTrue(newInput.allSubscribedGroupNames.sorted(by: <).elementsEqual(expAllSubscribedGroupNames)) - XCTAssertTrue(result.removedChannels == expRemovedChannels) - XCTAssertTrue(result.removedGroups == expRemovedGroups) + XCTAssertTrue(newInput.channelNames(withPresence: true).sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) + XCTAssertTrue(newInput.channelNames(withPresence: false).sorted(by: <).elementsEqual(expSubscribedChannelNames)) + XCTAssertTrue(newInput.channelGroupNames(withPresence: false).sorted(by: <).elementsEqual(expSubscribedGroupNames)) + XCTAssertTrue(newInput.channelGroupNames(withPresence: true).sorted(by: <).elementsEqual(expAllSubscribedGroupNames)) + XCTAssertTrue(diff.removedChannels == expRemovedChannels) + XCTAssertTrue(diff.removedChannelGroups == expRemovedGroups) } func test_RemovingInputWithPresenceOnly() { let input1 = SubscribeInput( channels: [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: true), - PubNubChannel(id: "c3", withPresence: true) + "c1", + "c1-pnpres", + "c2", + "c2-pnpres", + "c3", + "c3-pnpres" ], - groups: [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: true) + channelGroups: [ + "g1", + "g1-pnpres", + "g2", + "g2-pnpres", + "g3", + "g3-pnpres", + "g4", + "g4-pnpres" ] ) - let presenceChannelsToRemove = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c3", withPresence: true) + let presenceChannelsToRemove: Set = [ + "c1-pnpres", + "c3-pnpres" ] - let presenceGroupsToRemove = [ - PubNubChannel(id: "g1"), - PubNubChannel(id: "g3") + let presenceGroupsToRemove: Set = [ + "g1-pnpres", + "g3-pnpres" ] - let result = input1.removing( - mainChannels: [], - presenceChannelsOnly: presenceChannelsToRemove, - mainGroups: [], - presenceGroupsOnly: presenceGroupsToRemove + let newInput = input1.removing( + channels: presenceChannelsToRemove, + and: presenceGroupsToRemove ) - let newInput = result.newInput let expAllSubscribedChannelNames = ["c1", "c2", "c2-pnpres", "c3"] let expSubscribedChannelNames = ["c1", "c2", "c3"] - let expAllSubscribedGroupNames = ["g1", "g2", "g2-pnpres", "g3"] - let expSubscribedGroupNames = ["g1", "g2", "g3"] + let expAllSubscribedGroupNames = ["g1", "g2", "g2-pnpres", "g3", "g4", "g4-pnpres"] + let expSubscribedGroupNames = ["g1", "g2", "g3", "g4"] - XCTAssertTrue(newInput.allSubscribedChannelNames.sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) - XCTAssertTrue(newInput.subscribedChannelNames.sorted(by: <).elementsEqual(expSubscribedChannelNames)) - XCTAssertTrue(newInput.subscribedGroupNames.sorted(by: <).elementsEqual(expSubscribedGroupNames)) - XCTAssertTrue(newInput.allSubscribedGroupNames.sorted(by: <).elementsEqual(expAllSubscribedGroupNames)) + XCTAssertTrue(newInput.channelNames(withPresence: true).sorted(by: <).elementsEqual(expAllSubscribedChannelNames)) + XCTAssertTrue(newInput.channelNames(withPresence: false).sorted(by: <).elementsEqual(expSubscribedChannelNames)) + XCTAssertTrue(newInput.channelGroupNames(withPresence: false).sorted(by: <).elementsEqual(expSubscribedGroupNames)) + XCTAssertTrue(newInput.channelGroupNames(withPresence: true).sorted(by: <).elementsEqual(expAllSubscribedGroupNames)) } } diff --git a/Tests/PubNubTests/EventEngine/Subscribe/SubscribeTransitionTests.swift b/Tests/PubNubTests/EventEngine/Subscribe/SubscribeTransitionTests.swift index d4e38875..72976022 100644 --- a/Tests/PubNubTests/EventEngine/Subscribe/SubscribeTransitionTests.swift +++ b/Tests/PubNubTests/EventEngine/Subscribe/SubscribeTransitionTests.swift @@ -65,7 +65,7 @@ extension Subscribe.Event: Equatable { class SubscribeTransitionTests: XCTestCase { private let transition = SubscribeTransition() - private let input = SubscribeInput(channels: [PubNubChannel(channel: "test-channel")]) + private let input = SubscribeInput(channels: ["test-channel"]) // MARK: - Subscription Changed @@ -77,15 +77,10 @@ class SubscribeTransitionTests: XCTestCase { groups: ["g1", "g1-pnpres", "g2", "g2", "g2-pnpres", "g3"] ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3"] + let expectedInvocations: [EffectInvocation] = [ .managed(.handshakeRequest( channels: ["c1", "c1-pnpres", "c2"], @@ -93,7 +88,7 @@ class SubscribeTransitionTests: XCTestCase { )) ] let expectedState = Subscribe.HandshakingState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 0)! ) @@ -113,15 +108,10 @@ class SubscribeTransitionTests: XCTestCase { groups: ["g1", "g1-pnpres", "g2", "g2", "g2-pnpres", "g3"] ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3"] + let expectedInvocations: [EffectInvocation] = [ .managed(.handshakeRequest( channels: ["c1", "c1-pnpres", "c2"], @@ -130,7 +120,7 @@ class SubscribeTransitionTests: XCTestCase { ] let expectedState = Subscribe.HandshakingState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 0, region: 0) ) @@ -146,18 +136,12 @@ class SubscribeTransitionTests: XCTestCase { groups: ["g1", "g1-pnpres", "g2", "g2", "g2-pnpres", "g3"] ) ) - let expectedChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: false) - ] - let expectedGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: false) - ] + + let expectedChannels = ["c1", "c1-pnpres", "c2"] + let expectedGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3"] let expectedState = Subscribe.HandshakeStoppedState( - input: SubscribeInput(channels: expectedChannels,groups: expectedGroups), + input: SubscribeInput(channels: expectedChannels, channelGroups: expectedGroups), cursor: SubscribeCursor(timetoken: 0, region: 0) ) @@ -173,15 +157,10 @@ class SubscribeTransitionTests: XCTestCase { groups: ["g1", "g1-pnpres", "g2", "g2", "g2-pnpres", "g3"] ) ) - let expectedChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: false) - ] - let expectedGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: false) - ] + + let expectedChannels = ["c1", "c1-pnpres", "c2"] + let expectedGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3"] + let expectedInvocations: [EffectInvocation] = [ .cancel(.handshakeRequest), .managed(.handshakeRequest( @@ -191,7 +170,7 @@ class SubscribeTransitionTests: XCTestCase { ] let expectedState = Subscribe.HandshakingState( - input: SubscribeInput(channels: expectedChannels, groups: expectedGroups), + input: SubscribeInput(channels: expectedChannels, channelGroups: expectedGroups), cursor: SubscribeCursor(timetoken: 0, region: 0) ) @@ -201,8 +180,8 @@ class SubscribeTransitionTests: XCTestCase { func test_SubscriptionChangedForReceivingState() throws { let status: ConnectionStatus = .subscriptionChanged( - channels: input.subscribedChannelNames, - groups: input.subscribedGroupNames + channels: input.channelNames(withPresence: true), + groups: input.channelGroupNames(withPresence: true) ) let results = transition.transition( from: Subscribe.ReceivingState( @@ -214,19 +193,13 @@ class SubscribeTransitionTests: XCTestCase { groups: ["g1", "g1-pnpres", "g2", "g2", "g2-pnpres", "g3"] ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3"] let expectedNewStatus: ConnectionStatus = .subscriptionChanged( - channels: expChannels.map { $0.id }, - groups: expGroups.map { $0.id } + channels: expChannels, + groups: expGroups ) let expectedInvocations: [EffectInvocation] = [ .cancel(.receiveMessages), @@ -243,9 +216,9 @@ class SubscribeTransitionTests: XCTestCase { ] let expectedState = Subscribe.ReceivingState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 5001000, region: 22), - connectionStatus: .subscriptionChanged(channels: expChannels.map { $0.id }, groups: expGroups.map { $0.id }) + connectionStatus: .subscriptionChanged(channels: expChannels, groups: expGroups) ) XCTAssertTrue(results.state.isEqual(to: expectedState)) @@ -264,15 +237,10 @@ class SubscribeTransitionTests: XCTestCase { groups: ["g1", "g1-pnpres", "g2", "g2", "g2-pnpres", "g3"] ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3"] + let expectedInvocations: [EffectInvocation] = [ .managed(.handshakeRequest( channels: ["c1", "c1-pnpres", "c2"], @@ -280,7 +248,7 @@ class SubscribeTransitionTests: XCTestCase { )) ] let expectedState = Subscribe.HandshakingState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 500100900, region: 11) ) @@ -296,17 +264,12 @@ class SubscribeTransitionTests: XCTestCase { groups: ["g1", "g1-pnpres", "g2", "g2", "g2-pnpres", "g3"] ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3"] + let expectedState = Subscribe.ReceiveStoppedState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 500100900, region: 11) ) @@ -318,8 +281,8 @@ class SubscribeTransitionTests: XCTestCase { func test_SubscriptionRestoredForReceivingState() throws { let status: ConnectionStatus = .subscriptionChanged( - channels: input.subscribedChannelNames, - groups: input.subscribedGroupNames + channels: input.channelNames(withPresence: true), + groups: input.channelGroupNames(withPresence: true) ) let results = transition.transition( from: Subscribe.ReceivingState( @@ -332,21 +295,13 @@ class SubscribeTransitionTests: XCTestCase { cursor: SubscribeCursor(timetoken: 100, region: 55) ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: true), - PubNubChannel(id: "c3", withPresence: true), - PubNubChannel(id: "c4", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: true), - PubNubChannel(id: "g4", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2", "c2-pnpres", "c3", "c3-pnpres", "c4"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3", "g3-pnpres", "g4"] + let expectedNewStatus: ConnectionStatus = .subscriptionChanged( - channels: expChannels.map { $0.id }, - groups: expGroups.map { $0.id } + channels: expChannels, + groups: expGroups ) let expectedInvocations: [EffectInvocation] = [ .cancel(.receiveMessages), @@ -363,11 +318,11 @@ class SubscribeTransitionTests: XCTestCase { ] let expectedState = Subscribe.ReceivingState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 100, region: 55), connectionStatus: .subscriptionChanged( - channels: expChannels.map { $0.id }, - groups: expGroups.map { $0.id } + channels: expChannels, + groups: expGroups ) ) @@ -388,18 +343,9 @@ class SubscribeTransitionTests: XCTestCase { cursor: SubscribeCursor(timetoken: 100, region: 55) ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: true), - PubNubChannel(id: "c3", withPresence: true), - PubNubChannel(id: "c4", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: true), - PubNubChannel(id: "g4", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2", "c2-pnpres", "c3", "c3-pnpres", "c4"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3", "g3-pnpres", "g4"] let expectedInvocations: [EffectInvocation] = [ .managed(.handshakeRequest( @@ -408,7 +354,7 @@ class SubscribeTransitionTests: XCTestCase { )) ] let expectedState = Subscribe.HandshakingState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 100, region: 55) ) @@ -428,20 +374,12 @@ class SubscribeTransitionTests: XCTestCase { cursor: SubscribeCursor(timetoken: 100, region: 55) ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: true), - PubNubChannel(id: "c3", withPresence: true), - PubNubChannel(id: "c4", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: true), - PubNubChannel(id: "g4", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2", "c2-pnpres", "c3", "c3-pnpres", "c4"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3", "g3-pnpres", "g4"] + let expectedState = Subscribe.ReceiveStoppedState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 100, region: 55) ) @@ -458,18 +396,10 @@ class SubscribeTransitionTests: XCTestCase { cursor: SubscribeCursor(timetoken: 100, region: 55) ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: true), - PubNubChannel(id: "c3", withPresence: true), - PubNubChannel(id: "c4", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: true), - PubNubChannel(id: "g4", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2", "c2-pnpres", "c3", "c3-pnpres", "c4"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3", "g3-pnpres", "g4"] + let expectedInvocations: [EffectInvocation] = [ .cancel(.handshakeRequest), .managed(.handshakeRequest( @@ -478,7 +408,7 @@ class SubscribeTransitionTests: XCTestCase { )) ] let expectedState = Subscribe.HandshakingState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 100, region: 55) ) @@ -499,18 +429,10 @@ class SubscribeTransitionTests: XCTestCase { cursor: SubscribeCursor(timetoken: 100, region: 55) ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: true), - PubNubChannel(id: "c3", withPresence: true), - PubNubChannel(id: "c4", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: true), - PubNubChannel(id: "g4", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2", "c2-pnpres", "c3", "c3-pnpres", "c4"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3", "g3-pnpres", "g4"] + let expectedInvocations: [EffectInvocation] = [ .managed(.handshakeRequest( channels: ["c1", "c1-pnpres", "c2", "c2-pnpres", "c3", "c3-pnpres", "c4"], @@ -519,7 +441,7 @@ class SubscribeTransitionTests: XCTestCase { ] let expectedState = Subscribe.HandshakingState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 100, region: 55) ) @@ -536,20 +458,12 @@ class SubscribeTransitionTests: XCTestCase { cursor: SubscribeCursor(timetoken: 100, region: 55) ) ) - let expChannels = [ - PubNubChannel(id: "c1", withPresence: true), - PubNubChannel(id: "c2", withPresence: true), - PubNubChannel(id: "c3", withPresence: true), - PubNubChannel(id: "c4", withPresence: false) - ] - let expGroups = [ - PubNubChannel(id: "g1", withPresence: true), - PubNubChannel(id: "g2", withPresence: true), - PubNubChannel(id: "g3", withPresence: true), - PubNubChannel(id: "g4", withPresence: false) - ] + + let expChannels = ["c1", "c1-pnpres", "c2", "c2-pnpres", "c3", "c3-pnpres", "c4"] + let expGroups = ["g1", "g1-pnpres", "g2", "g2-pnpres", "g3", "g3-pnpres", "g4"] + let expectedState = Subscribe.HandshakeStoppedState( - input: SubscribeInput(channels: expChannels, groups: expGroups), + input: SubscribeInput(channels: expChannels, channelGroups: expGroups), cursor: SubscribeCursor(timetoken: 100, region: 55) ) @@ -575,8 +489,9 @@ class SubscribeTransitionTests: XCTestCase { newStatus: .connected, error: nil ))), - .managed(.receiveMessages(channels: input.allSubscribedChannelNames, - groups: input.allSubscribedGroupNames, + .managed(.receiveMessages( + channels: input.channelNames(withPresence: true), + groups: input.channelGroupNames(withPresence: true), cursor: cursor )) ] @@ -636,8 +551,8 @@ class SubscribeTransitionTests: XCTestCase { forCursor: SubscribeCursor(timetoken: 18002000, region: 123) )), .managed(.receiveMessages( - channels: input.allSubscribedChannelNames, - groups: input.allSubscribedGroupNames, + channels: input.channelNames(withPresence: true), + groups: input.channelGroupNames(withPresence: true), cursor: SubscribeCursor(timetoken: 18002000, region: 123) )) ] @@ -688,9 +603,11 @@ class SubscribeTransitionTests: XCTestCase { event: .reconnect(cursor: nil) ) let expectedInvocations: [EffectInvocation] = [ - .managed(.handshakeRequest( - channels: input.allSubscribedChannelNames, - groups: input.allSubscribedGroupNames) + .managed( + .handshakeRequest( + channels: input.channelNames(withPresence: true), + groups: input.channelGroupNames(withPresence: true) + ) ) ] let expectedState = Subscribe.HandshakingState( @@ -711,10 +628,12 @@ class SubscribeTransitionTests: XCTestCase { event: .reconnect(cursor: nil) ) let expectedInvocations: [EffectInvocation] = [ - .managed(.handshakeRequest( - channels: input.allSubscribedChannelNames, - groups: input.allSubscribedGroupNames - )) + .managed( + .handshakeRequest( + channels: input.channelNames(withPresence: true), + groups: input.channelGroupNames(withPresence: true) + ) + ) ] let expectedState = Subscribe.HandshakingState( input: input, @@ -734,10 +653,12 @@ class SubscribeTransitionTests: XCTestCase { event: .reconnect(cursor: nil) ) let expectedInvocations: [EffectInvocation] = [ - .managed(.handshakeRequest( - channels: input.allSubscribedChannelNames, - groups: input.allSubscribedGroupNames - )) + .managed( + .handshakeRequest( + channels: input.channelNames(withPresence: true), + groups: input.channelGroupNames(withPresence: true) + ) + ) ] let expectedState = Subscribe.HandshakingState( input: input, @@ -758,10 +679,12 @@ class SubscribeTransitionTests: XCTestCase { event: .reconnect(cursor: nil) ) let expectedInvocations: [EffectInvocation] = [ - .managed(.handshakeRequest( - channels: input.allSubscribedChannelNames, - groups: input.allSubscribedGroupNames - )) + .managed( + .handshakeRequest( + channels: input.channelNames(withPresence: true), + groups: input.channelGroupNames(withPresence: true) + ) + ) ] let expectedState = Subscribe.HandshakingState( input: input, diff --git a/Tests/PubNubTests/Integration/SubscriptionIntegrationTests.swift b/Tests/PubNubTests/Integration/SubscriptionIntegrationTests.swift index 2375f741..641627c0 100644 --- a/Tests/PubNubTests/Integration/SubscriptionIntegrationTests.swift +++ b/Tests/PubNubTests/Integration/SubscriptionIntegrationTests.swift @@ -375,4 +375,103 @@ class SubscriptionIntegrationTests: XCTestCase { wait(for: [expectation], timeout: 5.0) } + + func testSubscribingToPresenceChannelOnly() { + let presenceExpectation = XCTestExpectation(description: "Presence expectation") + presenceExpectation.assertForOverFulfill = true + presenceExpectation.expectedFulfillmentCount = 1 + + let messageExpectation = XCTestExpectation(description: "Message expectation") + messageExpectation.isInverted = true + + let mainChannelName = randomString() + let presenceChannelName = mainChannelName + "-pnpres" + + let pubnub = PubNub(configuration: presenceConfiguration()) + let subscription = pubnub.channel(presenceChannelName).subscription() + let anotherPubNub = PubNub(configuration: presenceConfiguration()) + + subscription.onPresence = { presenceEvent in + if case let .join(userIds) = presenceEvent.actions.first { + if userIds.count == 1 && userIds.first == anotherPubNub.configuration.userId { + presenceExpectation.fulfill() + } else { + XCTFail("Unexpected condition") + } + } else { + XCTFail("Unexpected condition") + } + } + subscription.onMessage = { _ in + messageExpectation.fulfill() + } + + pubnub.onConnectionStateChange = { [weak pubnub] newStatus in + if newStatus == .connected { + pubnub?.publish(channel: mainChannelName, message: "Some message") { _ in + anotherPubNub.subscribe(to: [mainChannelName]) + } + } + } + + subscription.subscribe() + + wait(for: [presenceExpectation, messageExpectation], timeout: 10.0) + } + + func testSubscribedChannels() { + let pubnub = PubNub(configuration: .init(from: testsBundle)) + let channelA = "A" + let channelB = "B" + + var firstSubscriptionToChannelA: Subscription? = pubnub.channel(channelA).subscription() + var secondSubscriptionToChannelA: Subscription? = pubnub.channel(channelA).subscription() + var subscriptionToChannelB: Subscription? = pubnub.channel(channelB).subscription() + + firstSubscriptionToChannelA?.subscribe() + secondSubscriptionToChannelA?.subscribe() + + XCTAssertEqual(pubnub.subscribedChannels, ["A"]) + subscriptionToChannelB?.subscribe() + XCTAssertEqual(pubnub.subscribedChannels.sorted(by: <), ["A", "B"]) + + firstSubscriptionToChannelA = nil + XCTAssertEqual(pubnub.subscribedChannels.sorted(by: <), ["A", "B"]) + secondSubscriptionToChannelA = nil + XCTAssertEqual(pubnub.subscribedChannels, ["B"]) + subscriptionToChannelB = nil + XCTAssertTrue(pubnub.subscribedChannels.isEmpty) + } + + func testUnsubscribingPresenceOnly() { + let pubnub = PubNub(configuration: .init(from: testsBundle)) + pubnub.subscribe(to: ["A"], withPresence: true) + XCTAssertEqual(pubnub.subscribedChannels.sorted(by: <), ["A", "A-pnpres"]) + pubnub.unsubscribe(from: ["A-pnpres"]) + XCTAssertEqual(pubnub.subscribedChannels, ["A"]) + } + + func testUnsubscribe() { + let pubnub = PubNub(configuration: .init(from: testsBundle)) + pubnub.subscribe(to: ["A"], withPresence: true) + XCTAssertEqual(pubnub.subscribedChannels.sorted(by: <), ["A", "A-pnpres"]) + pubnub.subscribe(to: ["B"], withPresence: true) + XCTAssertEqual(pubnub.subscribedChannels.sorted(by: <), ["A", "A-pnpres", "B", "B-pnpres"]) + + // Unsubscribing from the main channel. This should also unsubscribe from the presence channel + pubnub.unsubscribe(from: ["A"]) + // Ensuring backward compatibility and that the presence channel is unsubscribed along with the main channel + XCTAssertEqual(pubnub.subscribedChannels.sorted(by: <), ["B", "B-pnpres"]) + } +} + +private extension SubscriptionIntegrationTests { + func presenceConfiguration() -> PubNubConfiguration { + PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: randomString(), + durationUntilTimeout: 11 + ) + } } diff --git a/Tests/PubNubTests/Networking/Routers/SubscribeRouterTests.swift b/Tests/PubNubTests/Networking/Routers/SubscribeRouterTests.swift index 25348fe7..efd85186 100644 --- a/Tests/PubNubTests/Networking/Routers/SubscribeRouterTests.swift +++ b/Tests/PubNubTests/Networking/Routers/SubscribeRouterTests.swift @@ -243,7 +243,7 @@ extension SubscribeRouterTests { statusExpect.fulfill() } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -279,7 +279,7 @@ extension SubscribeRouterTests { statusExpect.fulfill() } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -313,7 +313,7 @@ extension SubscribeRouterTests { statusExpect.fulfill() } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -380,7 +380,7 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received") } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -432,7 +432,7 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received") } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -491,7 +491,7 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received") } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -543,7 +543,7 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received") } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -609,7 +609,7 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received \(event)") } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -669,7 +669,7 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received \(event)") } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -725,7 +725,7 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received") } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -777,7 +777,7 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received") } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -828,7 +828,7 @@ extension SubscribeRouterTests { statusExpect.fulfill() } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -878,7 +878,7 @@ extension SubscribeRouterTests { XCTFail("Unexpected event received \(event)") } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -927,7 +927,7 @@ extension SubscribeRouterTests { break } } - mockResult.subscriptionSession.subscribe(to: [testChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) defer { mockResult.listener.cancel() } @@ -975,7 +975,7 @@ extension SubscribeRouterTests { } } - mockResult.subscriptionSession.subscribe(to: [testChannel, otherChannel], using: pubnub) + mockResult.subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription(), pubnub.channel(otherChannel).subscription()]) XCTAssertTrue(mockResult.subscriptionSession.subscribedChannels.contains(testChannel)) XCTAssertTrue(mockResult.subscriptionSession.subscribedChannels.contains(otherChannel)) diff --git a/Tests/PubNubTests/Subscription/SubscriptionSessionTests.swift b/Tests/PubNubTests/Subscription/SubscriptionSessionTests.swift index c88464b0..384301ae 100644 --- a/Tests/PubNubTests/Subscription/SubscriptionSessionTests.swift +++ b/Tests/PubNubTests/Subscription/SubscriptionSessionTests.swift @@ -71,7 +71,7 @@ class SubscriptionSessionTests: XCTestCase { } } subscriptionSession.add(listener) - subscriptionSession.subscribe(to: [testChannel], using: pubnub) + subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()]) XCTAssertEqual(subscriptionSession.subscribedChannels, [testChannel]) defer { listener.cancel() } @@ -105,7 +105,7 @@ class SubscriptionSessionTests: XCTestCase { } } subscriptionSession.add(listener) - subscriptionSession.subscribe(to: [testChannel], at: SubscribeCursor(timetoken: 123456, region: 1), using: pubnub) + subscriptionSession.subscribe(to: [pubnub.channel(testChannel).subscription()], at: SubscribeCursor(timetoken: 123456, region: 1)) XCTAssertEqual(subscriptionSession.subscribedChannels, [testChannel]) defer { listener.cancel() }