Skip to content

Commit 4c952be

Browse files
fix(rtc): emit events to handlers in registration order
1 parent 184d5a6 commit 4c952be

2 files changed

Lines changed: 112 additions & 6 deletions

File tree

livekit-rtc/livekit/rtc/event_emitter.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import inspect
22
import asyncio
3-
from typing import Any, Callable, Dict, Set, Optional, Generic, TypeVar
3+
from typing import Any, Callable, Dict, Optional, Generic, TypeVar
44

55
from .log import logger
66

@@ -12,7 +12,10 @@ def __init__(self) -> None:
1212
"""
1313
Initialize a new instance of EventEmitter.
1414
"""
15-
self._events: Dict[T_contra, Set[Callable]] = dict()
15+
# A dict keyed by callback is an insertion-ordered set: handlers run in the
16+
# order they were registered, so one that mutates the event still runs before
17+
# a peer that reads it.
18+
self._events: Dict[T_contra, Dict[Callable, None]] = dict()
1619

1720
def emit(self, event: T_contra, *args: Any) -> None:
1821
"""
@@ -36,7 +39,7 @@ def greet(name):
3639
```
3740
"""
3841
if event in self._events:
39-
callables = self._events[event].copy()
42+
callables = list(self._events[event])
4043
for callback in callables:
4144
try:
4245
sig = inspect.signature(callback)
@@ -163,8 +166,8 @@ def greet(name):
163166
)
164167

165168
if event not in self._events:
166-
self._events[event] = set()
167-
self._events[event].add(callback)
169+
self._events[event] = {}
170+
self._events[event][callback] = None
168171
return callback
169172
else:
170173

@@ -197,4 +200,4 @@ def greet(name):
197200
```
198201
"""
199202
if event in self._events:
200-
self._events[event].discard(callback)
203+
self._events[event].pop(callback, None)

tests/rtc/test_emitter.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,106 @@ def on_error_another() -> None:
102102
emitter.emit("error")
103103

104104
assert len(calls) == 2
105+
106+
107+
class _OrderedHandler:
108+
"""A callable whose hash is fixed, so a set orders it independently of registration."""
109+
110+
def __init__(self, name: str, hash_value: int, sink: list[str]) -> None:
111+
self._name = name
112+
self._hash = hash_value
113+
self._sink = sink
114+
115+
def __hash__(self) -> int:
116+
return self._hash
117+
118+
def __eq__(self, other: object) -> bool:
119+
return self is other
120+
121+
def __call__(self) -> None:
122+
self._sink.append(self._name)
123+
124+
125+
def test_handlers_run_in_registration_order() -> None:
126+
# Handlers were kept in a set, so dispatch order was hash-derived. The hashes here are
127+
# picked so a set yields them in the opposite order to the one they were added in.
128+
emitter = EventEmitter[str]()
129+
order: list[str] = []
130+
131+
emitter.on("event", _OrderedHandler("first", 5, order))
132+
emitter.on("event", _OrderedHandler("second", 1, order))
133+
134+
emitter.emit("event")
135+
assert order == ["first", "second"]
136+
137+
138+
def test_a_mutating_handler_runs_before_a_peer_that_reads_it() -> None:
139+
# The livekit-agents case: one handler stamps a field onto the emitted object and a
140+
# user handler registered later reads it. Registration order has to decide.
141+
class Event:
142+
def __init__(self) -> None:
143+
self.speech_id: Any = None
144+
145+
class Stamp:
146+
def __hash__(self) -> int:
147+
return 5
148+
149+
def __eq__(self, other: object) -> bool:
150+
return self is other
151+
152+
def __call__(self, ev: Event) -> None:
153+
ev.speech_id = "speech_1"
154+
155+
class Read:
156+
def __init__(self, sink: list[Any]) -> None:
157+
self._sink = sink
158+
159+
def __hash__(self) -> int:
160+
return 1
161+
162+
def __eq__(self, other: object) -> bool:
163+
return self is other
164+
165+
def __call__(self, ev: Event) -> None:
166+
self._sink.append(ev.speech_id)
167+
168+
emitter = EventEmitter[str]()
169+
seen: list[Any] = []
170+
emitter.on("metrics", Stamp())
171+
emitter.on("metrics", Read(seen))
172+
173+
for _ in range(5):
174+
emitter.emit("metrics", Event())
175+
176+
assert seen == ["speech_1"] * 5
177+
178+
179+
def test_off_still_removes_a_handler() -> None:
180+
emitter = EventEmitter[str]()
181+
calls: list[str] = []
182+
183+
@emitter.on("event")
184+
def keep() -> None:
185+
calls.append("keep")
186+
187+
@emitter.on("event")
188+
def drop() -> None:
189+
calls.append("drop")
190+
191+
emitter.off("event", drop)
192+
emitter.off("event", drop) # removing twice must not raise
193+
emitter.emit("event")
194+
assert calls == ["keep"]
195+
196+
197+
def test_registering_the_same_handler_twice_keeps_one_entry() -> None:
198+
emitter = EventEmitter[str]()
199+
calls: list[str] = []
200+
201+
def handler() -> None:
202+
calls.append("x")
203+
204+
emitter.on("event", handler)
205+
emitter.on("event", handler)
206+
emitter.emit("event")
207+
assert calls == ["x"]

0 commit comments

Comments
 (0)