fix(events): chained events inherit the originating event's router_data
The bug
An event yielded by a handler is built with no router_data:
# base_state_processor.py, chain_updates()
if fixed_events := Event.from_event_type(
_check_valid_yield(events, handler_name=handler_name), # no router_data
):
and _execute_event only refreshes the router when an event carries some:
router_data = event.router_data or {}
if router_data and state.router_data != router_data:
state.router_data = router_data
state.router = RouterData.from_router_data(router_data)
router/router_data live on the root state and are inherited by every substate, so a chained event resolves self.router and its dynamic route args against whichever view the last client-sent event left there — not against the view whose handler yielded it.
An on_load that yields its real loader is enough to hit this in production. The loader runs a moment later; any client event sent in the window where the browser's location ref still holds the previous page moves the shared root router under it. The loader then reads the old path, its route arg comes back "" on a page whose URL plainly names one, and since nothing re-runs an on_load once the args land, the wrong answer sticks for that page view. Downstream this showed up as a freshly created record 404-ing on its own detail page, intermittently, at a rate of roughly one CI run in fifty.
The fix
EventContext carries the routing data of the event being processed. It is bound in _process_event_queue_entry before the handler runs, and _execute_event reads it back from there instead of from the event.
enqueue forks the current context, so an event a handler yields inherits the producing view by construction — no threading through process_event/chain_updates, and it holds for chained-of-chained and for background handlers, which run inside the same task the context is set on.
A chained Event object is left untouched. That matters beyond tidiness: nothing stamps routing data onto an event that gets serialized to the client, so the request headers router_data carries cannot reach the wire.
Test
test_chained_event_keeps_originating_router_data drives the real processor: a background handler yields another handler, and a client event for a different route lands in the window. Before the fix the chained handler records /other|; after it records /item/abc|abc.
Verified failing before and passing after. Full unit suite green (7609 passed), under both fixed and random ordering, plus ruff, pyright and pre-commit.
Draft until a news/ fragment is added with this PR number.