Skip to content

[🚀 Feature]: BiDi: Add low-level helper method to subscribe/unsubscribe from events #14201

@Mr0grog

Description

@Mr0grog

Feature and motivation

I’ve started playing around with the BiDi support in the Ruby webdriver, and have noticed that subscribing to and unsubscribing from events is fairly complicated: you need to keep track of the event callbacks that have been added to the session (including by other actors) so you know whether to send a session.subscribe/session.unsubscribe command alongside adding/removing your callback.

For example, the Ruby LogHandler does this:

def remove_message_handler(id)
@bidi.remove_callback('log.entryAdded', id)
unsubscribe_log_entry if @log_entry_subscribed && @bidi.callbacks['log.entryAdded'].empty?
end
private
def subscribe_log_entry
@bidi.session.subscribe('log.entryAdded')
@log_entry_subscribed = true
end
def unsubscribe_log_entry
@bidi.session.unsubscribe('log.entryAdded')
@log_entry_subscribed = false
end

The Python source follows a similar pattern in Script:

def remove_console_message_handler(self, id):
self.conn.remove_callback(LogEntryAdded, id)
self._unsubscribe_from_log_entries()
remove_javascript_error_handler = remove_console_message_handler
def _subscribe_to_log_entries(self):
if not self.log_entry_subscribed:
self.conn.execute(session_subscribe(LogEntryAdded.event_class))
self.log_entry_subscribed = True
def _unsubscribe_from_log_entries(self):
if self.log_entry_subscribed and LogEntryAdded.event_class not in self.conn.callbacks:
self.conn.execute(session_unsubscribe(LogEntryAdded.event_class))
self.log_entry_subscribed = False

I don’t know if this is a pattern that has crept in or a standard design/set of interfaces that are expected to exist across all the languages. Either way, having the subscriber need to keep track of who else might be subscribed seems like a problem — it adds a bit of complexity everywhere events are used and it’s easy to mess up if a user needs to listen to events that aren’t currently handled by a higher-level wrapper.

It would be really helpful if there were a subscribe or listen_to_event or some similar method (plus the corresponding unsubscribe method) on the low-level BiDi API that adds your callback and calls session.subscribe/session.unsubscribe as appropriate, e.g:

module Selenium
  module WebDriver
    class BiDi
      # This matches what `Script` does, but I imagine a more robust version of
      # this might also want to take an optional list of contexts and
      # subscribe/unsubscribe only the appropriate contexts. Then the handling
      # of callbacks might also filter based on what context the event is
      # coming from, rather than just hooking your callback directly up to the
      # @ws instance.
      def add_callback(event, &block)
        session.subscribe(event) if callbacks[event].empty?
        @ws.add_callback(event, &block)
      end

      def remove_callback(event, id)
        @ws.remove_callback(event, id)
        session.subscribe(event) if callbacks[event].empty?
      end

(BiDi seems like the obvious place to put this level of abstraction in Ruby, but the Python library currently has nothing sitting in-between the higher-level APIs like Script and the actual websocket connection. I have not looked at the other language packages.)

Usage example

Currently, subscribing/unsubscribing to BiDi events requires code like (this is Ruby, but it’s similar in Python and maybe other languages):

# Subscribe to navigation events:
driver_instance.bidi.session.subscribe('browsingContext.fragmentNavigated')
callback_id = driver_instance.bidi.add_callback('browsingContext.fragmentNavigated') do |params|
  # Do something with the event.
  puts "Fragment Navigated: #{params}"
end

# Do some work...

# Unsubscribe:
driver_instance.bidi.remove_callback('browsingContext.fragmentNavigated', callback_id)
driver_instance.bidi.session.unsubscribe('browsingContext.fragmentNavigated') if driver_instance.bidi.callbacks['browsingContext.fragmentNavigated'].empty?

Ideally, subscribing/unsubscribing would be one-liners without any conditionals:

callback_id = driver_instance.bidi.subscribe('browsingContext.fragmentNavigated') do |params|
  # Do something with the event.
  puts "Fragment Navigated: #{params}"
end

# Do some work...

# Unsubscribe:
driver_instance.bidi.unsubscribe('browsingContext.fragmentNavigated', callback_id)

Even nicer if allows filtering by context, but maybe not as critical?

callback_id = driver_instance.bidi.subscribe('browsingContext.fragmentNavigated', contexts: [a_context]) do |params|
  # Do something with the event.
  puts "Fragment Navigated: #{params}"
end

# Do some work...

# Unsubscribe (from which context is handled for you based on callback_id):
driver_instance.bidi.unsubscribe('browsingContext.fragmentNavigated', callback_id)

Metadata

Metadata

Assignees

No one assigned

    Labels

    I-enhancementSomething could be betterJ-staleApplied to issues that become stale, and eventually closed.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions