Skip to content

fix(rtc): pass once() handlers only the arguments they take, and let off() remove them - #869

Open
ANDROIDHASSAN wants to merge 3 commits into
livekit:mainfrom
ANDROIDHASSAN:fix/emitter-once-like-on
Open

ANDROIDHASSAN wants to merge 3 commits into
livekit:mainfrom
ANDROIDHASSAN:fix/emitter-once-like-on

Conversation

@ANDROIDHASSAN

@ANDROIDHASSAN ANDROIDHASSAN commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Two ways a once() handler behaved differently from an on() one:

  • emit() passes a handler only as many positional arguments as it takes (test_args). once() registered a *args wrapper instead of the callback, so the wrapper got every argument and passed them all on. A once handler that takes fewer than the event emits raised TypeError, which emit() re-raises:
    emitter.on("x", lambda a: ...)    # emit("x", 1, 2) calls it with 1
    emitter.once("x", lambda a: ...)  # emit("x", 1, 2) -> TypeError: <lambda>() takes 1 positional argument but 2 were given
  • off(event, callback) looked up callback itself, so it could not remove a handler added with once(event, callback): the handler still ran on the next emit.

once() now registers a private _OnceHandler that keeps a reference to the callback (recognised with isinstance, so an on() handler is never mistaken for one). emit() trims the arguments to that callback's signature, read when the event fires as it is for on() handlers (so a functools.partial changed after registration is handled the same way). off() matches the reference the way a dict key is matched: identity, then hash, then ==, so an equal bound method removes it and an unrelated callback's __eq__ is never called. A callback with no signature, such as a builtin, still receives every argument, as before.

Tests in tests/rtc/test_emitter.py:

  • test_once_passes_only_the_arguments_the_callback_takes: fails on main with the TypeError above.
  • test_off_removes_a_once_handler: a function and a bound method added with once are removed by off; fails on main (the handler runs).
  • test_once_runs_a_callback_without_a_signature: once(event, min) still runs and is removed.
  • test_once_trims_to_the_callback_signature_at_emit_time, test_off_does_not_compare_unrelated_once_callbacks and test_an_on_handler_with_a_once_callback_attribute_keeps_its_signature: the cases from the review below.

tests/rtc and livekit-rtc/tests here, without the FFI library: the same 17 failures and 17 errors before and after, from the missing native library; 131 passed before, 137 now. ruff check, ruff format --check (0.15.4) pass; mypy (1.19.1) on livekit-rtc reports the same 4 errors in jupyter.py before and after.

Not changed: on() rejects async callbacks, but once() passes it the sync wrapper, so once(event, async_fn) is still accepted and its coroutine is never awaited.

… remove them

once() registered a *args wrapper, so emit() passed it every argument and a
handler taking fewer raised TypeError (re-raised by emit), where on() drops
the extra ones. off(event, callback) also could not remove a handler added
with once(event, callback). The wrapper now carries the callback's signature
and a reference to it.
devin-ai-integration[bot]

This comment was marked as resolved.

…n off() like a dict key

emit() now takes the signature from the registered callback when it fires,
so a functools.partial changed after once() is trimmed like an on() handler.
off() compares hashes before calling ==, so removing an unrelated handler
never calls a once() callback's __eq__.
devin-ai-integration[bot]

This comment was marked as resolved.

once() now registers a private _OnceHandler, so emit() and off() recognise
it with isinstance. An on() handler that happens to carry a _once_callback
attribute keeps its own signature.

This branch has not been deployed

No deployments
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.

1 participant