@@ -184,3 +184,106 @@ def test_on_rejects_async_mock() -> None:
184184
185185 with pytest .raises (ValueError , match = "Cannot register an async callback" ):
186186 emitter .on ("connected" , AsyncMock ())
187+
188+
189+ class _OrderedHandler :
190+ """A callable whose hash is fixed, so a set orders it independently of registration."""
191+
192+ def __init__ (self , name : str , hash_value : int , sink : list [str ]) -> None :
193+ self ._name = name
194+ self ._hash = hash_value
195+ self ._sink = sink
196+
197+ def __hash__ (self ) -> int :
198+ return self ._hash
199+
200+ def __eq__ (self , other : object ) -> bool :
201+ return self is other
202+
203+ def __call__ (self ) -> None :
204+ self ._sink .append (self ._name )
205+
206+
207+ def test_handlers_run_in_registration_order () -> None :
208+ # Handlers were kept in a set, so dispatch order was hash-derived. The hashes here are
209+ # picked so a set yields them in the opposite order to the one they were added in.
210+ emitter = EventEmitter [str ]()
211+ order : list [str ] = []
212+
213+ emitter .on ("event" , _OrderedHandler ("first" , 5 , order ))
214+ emitter .on ("event" , _OrderedHandler ("second" , 1 , order ))
215+
216+ emitter .emit ("event" )
217+ assert order == ["first" , "second" ]
218+
219+
220+ def test_a_mutating_handler_runs_before_a_peer_that_reads_it () -> None :
221+ # The livekit-agents case: one handler stamps a field onto the emitted object and a
222+ # user handler registered later reads it. Registration order has to decide.
223+ class Event :
224+ def __init__ (self ) -> None :
225+ self .speech_id : Any = None
226+
227+ class Stamp :
228+ def __hash__ (self ) -> int :
229+ return 5
230+
231+ def __eq__ (self , other : object ) -> bool :
232+ return self is other
233+
234+ def __call__ (self , ev : Event ) -> None :
235+ ev .speech_id = "speech_1"
236+
237+ class Read :
238+ def __init__ (self , sink : list [Any ]) -> None :
239+ self ._sink = sink
240+
241+ def __hash__ (self ) -> int :
242+ return 1
243+
244+ def __eq__ (self , other : object ) -> bool :
245+ return self is other
246+
247+ def __call__ (self , ev : Event ) -> None :
248+ self ._sink .append (ev .speech_id )
249+
250+ emitter = EventEmitter [str ]()
251+ seen : list [Any ] = []
252+ emitter .on ("metrics" , Stamp ())
253+ emitter .on ("metrics" , Read (seen ))
254+
255+ for _ in range (5 ):
256+ emitter .emit ("metrics" , Event ())
257+
258+ assert seen == ["speech_1" ] * 5
259+
260+
261+ def test_off_still_removes_a_handler () -> None :
262+ emitter = EventEmitter [str ]()
263+ calls : list [str ] = []
264+
265+ @emitter .on ("event" )
266+ def keep () -> None :
267+ calls .append ("keep" )
268+
269+ @emitter .on ("event" )
270+ def drop () -> None :
271+ calls .append ("drop" )
272+
273+ emitter .off ("event" , drop )
274+ emitter .off ("event" , drop ) # removing twice must not raise
275+ emitter .emit ("event" )
276+ assert calls == ["keep" ]
277+
278+
279+ def test_registering_the_same_handler_twice_keeps_one_entry () -> None :
280+ emitter = EventEmitter [str ]()
281+ calls : list [str ] = []
282+
283+ def handler () -> None :
284+ calls .append ("x" )
285+
286+ emitter .on ("event" , handler )
287+ emitter .on ("event" , handler )
288+ emitter .emit ("event" )
289+ assert calls == ["x" ]
0 commit comments