Repository navigation
Make event handlers awaitable and register them through @rx.event - #7673
FarhanAliRaza wants to merge 14 commits into
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 22 files
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
[High impact] The PR appears safe to merge; the previously reported quote example is fixed. SummaryMakes event handlers awaitable and registers them through
Reviews (7) · Last reviewed commit: "Fetch the quote outside the state lock i..." · Reviewed by Greptile |
Merging this PR will degrade performance by 4.18%2 warnings
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing |
…andlers at class creation A handler's non-event return value now resolves its EventFuture instead of raising TypeError; returned events still chain. @rx.event returns the EventHandler itself, bound to its state class by __set_name__, and a function without a state parameter registers as a stateless handler that runs without the state lock and outside the per-token FIFO. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
Awaiting a handler (or a spec with bound args) runs it in the active EventContext: inline on the locked state tree while the current task holds the token's state lock, which the context now records, or enqueued and awaited when no lock is held. Its yields emit deltas and chain as usual, and the handler's return value is the result of the await. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
Adds the docs, news fragments and integration scenarios for awaiting event handlers from foreground and background handlers. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
Raise the reflex-base floor to 0.10.2.dev0 for the new EventContext lock methods; keep the state lock held until inline handler runs started from the handler finish; accept generic annotations on stateless handlers; keep awaiting futures returned by bare handlers; tighten the docs and news fragments and add a breaking fragment for the decorator ordering change. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
309c05c to
daeb277
Compare
There was a problem hiding this comment.
All reported issues were addressed across 16 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
A background handler leaving `async with self` waits for detached inline runs; cancelling it during that wait must still exit the state manager's lock, and the pending inline runs are cancelled rather than left behind. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
A nested inline run on an already registered task no longer drops the outer run's registration, the release keeps draining runs started while it waits, and exceptions of gathered runs are logged instead of being retrieved silently. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
… hot path A task cancelled inside the locked block is not interrupted again while it waits for detached inline runs, so those are cancelled up front. The per-event lock marking uses a plain try/finally and a fast path when nothing ran inline. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
The CI pyright does not carry the `added` narrowing into `set.add`; keep the task only when it was registered here. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
Drops the unused holding_lock context manager: both callers now set and release the lock marker directly, which keeps the per-call overhead down on the shared-state fan-out path. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
|
On the CodSpeed check (not in the required set;
The current flag is route-argument parsing, which this PR does not touch. For the earlier Generated by Claude Code |
Both sides appended tests to tests/units/test_app.py and tests/units/test_state.py; keep both. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
_literal_var_for caches the literal class per exact value type, but a MutableProxy has the proxy's type whatever it wraps while isinstance sees the wrapped value. The first proxied value pinned every later proxy to its literal class, so LiteralVar.create(proxy_of_list) raised after LiteralVar.create(proxy_of_dict). Skip the cache when the value reports a __class__ other than its type. Claude-Session: https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
All Submissions:
Type of change
New Feature Submission:
Changes To Core Features:
What this does
Implements ENG-9162 (EventHandler redesign: awaitable semantics).
@rx.eventis the registration point. The decorator returns theEventHandleritself.EventHandler.__set_name__binds a method's handler to the state class whose body declares it, beforeBaseState.__init_subclass__collects the class's handlers andRegistrationContext.register_base_stateregisters them under the final class name, so local classes and hot reload behave as before. A mixin's handler is bound to the mixin and copied per concrete state (includingrx.ComponentState). Undecorated public methods are still wrapped as today. A decorator stacked above@rx.eventnow raisesTypeErrorat class creation (it would otherwise chain the handler forever).@rx.eventon a function without a state parameter registers a stateless handler (states=()) that runs with no state, no lock, and outside the per-token FIFO ofBaseStateEventProcessor.Handlers are awaitable.
EventHandlerandEventSpecimplement__await__; calling still only binds arguments (returns anEventSpec), soreturn rx.redirect(...)andyield State.x(1)are unchanged.execute_event_inlinedecides how anawaitruns fromEventContext.locked_root, which the processor,StateProxy.__aenter__/__aexit__andApp.modify_stateset while the token's state lock is held:async with self,app.modify_state)EventFutureasync with self, API route)EventFutureawaitedEventHandlerTypeError(would wait for the lock);yieldit insteadrx.redirect, …) or upload specEventHandlerTypeErrorEventContextEventHandlerTypeErrorReturn values. A handler's non-event return value resolves
awaitand theEventFuture(EventContext.enqueuenow returns the futures). Returned events still chain; a returned coroutine still raises. Yielded non-events still raise; streaming values to the frontend is ENG-13447.EventProcessor._execute_eventawaits only coroutines now, so a bare handler returning a spec no longer executes it.self.handler()on a state instance keeps calling the plain method as before; the new form isawait type(self).handler(x).Verification
tests/units/reflex_bench/drivers/test_browser.py, which needs a Playwright browser download the sandbox cannot fetch.ruff check,ruff format,pyright reflex testsandpre-commit run --all-filesare clean.tests/integration/test_event_chain.py(await inline, return value, callee's yield chained after the caller) andtests/integration/test_background_task.py(await through the queue, then inline insideasync with self). Selenium could not download chromedriver in the sandbox, so both apps were driven with Playwright against the installed Chromium instead; both pass. CI runs the Selenium versions.tests/benchmarks/test_event_processing.pypasses; repeated A/B runs of the per-event cases againstmainare within noise (for exampleprocess_event[counter]0.60/0.58 ms base vs 0.58/0.66 ms head).Review notes
EventSpec.__hash__is identity-based while the dataclass__eq__stays field-wise:asyncio.gatherhashes awaitables, and the boundVarargs make field-wise hashing and equality unusable.locked_rootis shared by reference with tasks a handler spawns, so agathered sibling runs inline instead of enqueuing behind its own parent. A task outliving the handler seesNoneand enqueues.CancelledErrorinto the caller.inspect.isawaitableon handler results and for code expectingrx.event(...)to return a plain function (the function is at.fn, markers still on it).await State.h()is typedAny; a return TypeVar would mean rewriting theEventCallback.__call__overloads.Follow-ups
EventSpecto skip theLiteralVarencode/decode round trip on inline awaits.closes #6223
https://claude.ai/code/session_01LwJUCVfbdzZayfSZMiP8GJ
Generated by Claude Code