2424import java .util .List ;
2525import java .util .Map ;
2626import java .util .Set ;
27+ import java .util .concurrent .ConcurrentHashMap ;
28+ import java .util .concurrent .CopyOnWriteArrayList ;
2729import java .util .function .Consumer ;
2830import java .util .logging .Logger ;
2931import org .openqa .selenium .WebDriverException ;
@@ -34,6 +36,7 @@ public class BiDi implements Closeable {
3436
3537 private final Duration timeout ;
3638 private final Connection connection ;
39+ private final Map <Event <?>, List <Long >> contextListenerIds = new ConcurrentHashMap <>();
3740
3841 /**
3942 * @deprecated Use constructor with timeout parameter: {@link #BiDi(Connection, Duration)}
@@ -100,11 +103,13 @@ public <X> long addListener(String browsingContextId, Event<X> event, Consumer<X
100103 "session.subscribe" ,
101104 Map .of ("contexts" , List .of (browsingContextId ), "events" , List .of (event .getMethod ()))));
102105
103- return connection .addListener (event , handler );
106+ long id = connection .addListener (event , handler );
107+ contextListenerIds .computeIfAbsent (event , k -> new CopyOnWriteArrayList <>()).add (id );
108+ return id ;
104109 }
105110
106111 public <X > long addListener (Set <String > browsingContextIds , Event <X > event , Consumer <X > handler ) {
107- Require .nonNull ("List of browsing context ids" , browsingContextIds );
112+ Require .nonEmpty ("List of browsing context ids" , browsingContextIds );
108113 Require .nonNull ("Event to listen for" , event );
109114 Require .nonNull ("Handler to call" , handler );
110115
@@ -113,7 +118,9 @@ public <X> long addListener(Set<String> browsingContextIds, Event<X> event, Cons
113118 "session.subscribe" ,
114119 Map .of ("contexts" , browsingContextIds , "events" , List .of (event .getMethod ()))));
115120
116- return connection .addListener (event , handler );
121+ long id = connection .addListener (event , handler );
122+ contextListenerIds .computeIfAbsent (event , k -> new CopyOnWriteArrayList <>()).add (id );
123+ return id ;
117124 }
118125
119126 public <X > void clearListener (Event <X > event ) {
@@ -127,6 +134,32 @@ public <X> void clearListener(Event<X> event) {
127134 }
128135 }
129136
137+ public <X > void clearListener (Set <String > browsingContextIds , Event <X > event ) {
138+ Require .nonEmpty ("List of browsing context ids" , browsingContextIds );
139+ Require .nonNull ("Event to listen for" , event );
140+
141+ // The browser throws an error if we try to unsubscribe an event that was not subscribed in the
142+ // first place
143+ if (!connection .isEventSubscribed (event )) {
144+ return ;
145+ }
146+
147+ List <Long > ids = contextListenerIds .remove (event );
148+ if (ids != null && !ids .isEmpty ()) {
149+ // Subscription was context-specific: unsubscribe only for those contexts.
150+ send (
151+ new Command <>(
152+ "session.unsubscribe" ,
153+ Map .of ("contexts" , browsingContextIds , "events" , List .of (event .getMethod ()))));
154+ ids .forEach (connection ::removeListener );
155+ } else {
156+ // Subscription was global: context-specific unsubscription is invalid per the BiDi protocol,
157+ // so fall back to a global unsubscription.
158+ send (new Command <>("session.unsubscribe" , Map .of ("events" , List .of (event .getMethod ()))));
159+ connection .clearListener (event );
160+ }
161+ }
162+
130163 public void removeListener (long id ) {
131164 connection .removeListener (id );
132165 }
0 commit comments