Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a new Listeners API #153

Merged
merged 12 commits into from
Feb 21, 2024
Merged

Adding a new Listeners API #153

merged 12 commits into from
Feb 21, 2024

Conversation

jguz-pubnub
Copy link
Contributor

@jguz-pubnub jguz-pubnub commented Feb 13, 2024

feat(listeners): adding the new Listeners API
feat(subscribe & presence): enabling EventEngine by default

Copy link
Contributor

@parfeon parfeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly looks good, but there is a question on ability to have multiple listeners on Subscription, SubscriptionSet and PubNub client. For example, one Subscription instance passed in one view between components in it — there is a chance that multiple components would like to have their listener on the same event type.

Comment on lines 123 to 127
channels: [PubNubChannel],
presenceChannelsOnly: [PubNubChannel],
groups: [PubNubChannel],
presenceGroupsOnly: [PubNubChannel]
) -> SubscribeInput.RemovingResult {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you subscribe solely on presence channels with the latest setup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I preserved existing behaviors in SDK:

  1. You cannot subscribe to Presence channel without main channel
  2. You can unsubscribe from Presence channel only and still keep main channel

The helper method you highlighted is responsible for returning the new channels and groups due to unsubscribing. I can change its signature to be more meaningful:

func removing(
  mainChannels: [PubNubChannel]
  presenceOnlyChannels: [PubNubChannel]
  mainGroups: [PubNubChannel],
  presenceOnlyGroups: [PubNubChannel]
)

/// A closure to be called when the connection status changes.
var onConnectionStateChange: ((ConnectionStatus) -> Void)? { get set }
/// A closure to be called when a subscription error occurs.
var onSubscribeError: ((PubNubError) -> Void)? { get set }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this one here for some form of backward compatibility? If there were an error, then depending on from the current state, we would receive different connection statuses: connection error or unexpected disconnect.

Copy link
Contributor Author

@jguz-pubnub jguz-pubnub Feb 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, that's because of the old implementation. You're right, it should be enough to read an underlying error from ConnectionStatus enum cases like connectionError and disconnectedUnexpectedly and getting rid of onSubscribeError closure you pointed out. So what I would suggest is improving existing ConnectionStatus like this:

enum ConnectionStatus {
  case connectionError(PubNubError)
  case disconnectedUnexpectedly(PubNubError)
}

The reason I haven't done it yes is that there's disconnectedUnexpectedly case introduced a long time ago before EE. Now it's time to change it even if it might be a breaking change. I will run it by the team.

Comment on lines 194 to 197
let pattern = "^" + self + "$"
let predicate = NSPredicate(format: "SELF MATCHES %@", pattern)

return predicate.evaluate(with: string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NSPredicate is a powerful tool, but in a way how it used may affect performance compared to regular compare as on line 192.

Comment on lines 291 to 294
channels: [PubNubChannel],
presenceChannelsOnly: [PubNubChannel],
groups: [PubNubChannel],
presenceGroupsOnly: [PubNubChannel]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to subscribe on presence channels only with the current setup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rules are still the same as I described in this comment. It's a helper method only I can always rename. In general, unsubscribing still works like that:

  1. If you unsubscribe from main channel, you should also unsubscribe from its Presence channel
  2. If you unsubscribe from Presence channel only, you should still keep main channel in the Subscribe loop

I noticed it's easier to keep these lists in separate parameters when performing next computations.

Comment on lines +135 to +161
let channelSubscriptions = channels.map {
channel($0).subscription(
queue: queue,
options: withPresence ? ReceivePresenceEvents() : SubscriptionOptions.empty()
)
}
let channelGroupSubscriptions = groups.map {
channelGroup($0).subscription(
queue: queue,
options: withPresence ? ReceivePresenceEvents() : SubscriptionOptions.empty()
)
}
internalSubscribe(
with: channelSubscriptions,
and: channelGroupSubscriptions,
at: cursor?.timetoken
)
channelSubscriptions.forEach { subscription in
subscription.subscriptionNames.flatMap { $0 }.forEach {
globalChannelSubscriptions[$0] = subscription
}
}
channelGroupSubscriptions.forEach { subscription in
subscription.subscriptionNames.flatMap { $0 }.forEach {
globalGroupSubscriptions[$0] = subscription
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand, this one is for PubNub client instance itself?
If subscribing to multiple entities, then it will be more reasonable to create SubscriptionSet with Subscriptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran it by the team. You're right that this should be kept in SubscriptionSet under the hood. Unfortunately, Swift SDK is the only one that provides functionality like unsubscribing from Presence channels only. And this is what prevents us from using SubscriptionSet for global subscriptions. We have no API/method for SubscriptionSet to unsubscribe from Presence channel only. I could deprecate this method or even remove withPresence parameter, but this decision requires PM input. Can we leave it as it is and fix it in case of further decisions?

jguz-pubnub and others added 5 commits February 18, 2024 21:08
* Removed onSubscribeError closure
* Added associaded type for .connectionError and .disconnectedUnexpectedly
* Modifying clone methods for Subscription and SubscriptionSet
* Minor names changing for helper functions
@jguz-pubnub
Copy link
Contributor Author

jguz-pubnub commented Feb 19, 2024

Regarding your comment here, I slightly modified clone() methods for Subscription and SubscriptionSet. Let's suppose that the user would like to pass the same Subscription object to another screen, but have different handling for things like onMessage, onPresence, etc, and not to override any settings from the parent listeners it derives from. Here's the example and it prints "Parent" and "Child" since these two Subscriptions below are independent:

let sub = pubnub.channel("someChannel").subscription() 
sub.onMessage = { _ in
  print("Parent")
}
sub.subscribe()

let screen = SomeScreen(subscription: sub.clone()) // <==== CLONING TAKES PLACE
self.present(screen, animated: true)

class SomeScreen: UIViewController {
  let subscription: Subscription
  
  func viewDidLoad() {
    super.viewDidLoad()
    subscription.onMessage = { _ in
       print("Child")
    } 
  }
}

The clone() method automatically registers its cloned instance in a container that tracks active subscriptions for the given channel, assuming that someone has already called .subscribe() on a parent subscription before, so now it's active. This means that the user doesn't have to call subscription.subscribe() inside the viewDidLoad() method.

Copy link
Contributor

@parfeon parfeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some naming questions and one about whether proper type used in method signature or not.

About multiple listeners of the same type on a single Subscription / SubscriptionSet is possible only through clone() utilization, and impossible to have multiple onMessage listeners on the same instance.

Comment on lines +194 to +196
if let firstIndex = entityName.lastIndex(of: "."), let secondIndex = string.lastIndex(of: ".") {
return entityName.prefix(upTo: firstIndex) == string.prefix(upTo: secondIndex)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the "original" NSPredicate basically matched the same what is done on the line #192?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it should. It compares two String up to the last occurrence of ., assuming both contain the . character. Let's suppose these scenarios:

  1. The message came from channel.item.x and the underlying entity's subscription is channel.item.*:
    • According to the line 195, I will compare channel.item with channel.item
  2. The message came from a.b.c and the underlying entity's subscription is channel.item.*
    • I will compare a.b with channel.item 🔴

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I finally got it.
I was totally fine with extension for String - it looked better.

I probably would use payload.subscription ?? payload.channel to match both channel and channelGroup subscription type. If a subscription has been created for wildcard subscription, then the subscription field will hold the same name (if an event for one of the channels from it) and will be no need for pattern match (just plain ==). The only thing which may require some attention is presence channels / channel group names.

Copy link
Contributor

@techwritermat techwritermat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enable event engine please :)

@jguz-pubnub
Copy link
Contributor Author

@pubnub-release-bot release as 7.0.0

@jguz-pubnub jguz-pubnub merged commit d5c5083 into master Feb 21, 2024
10 checks passed
@jguz-pubnub jguz-pubnub deleted the feat/new-listeners branch February 21, 2024 10:49
@pubnub-release-bot
Copy link

🚀 Release successfully completed 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants