Run the compatibility rehydrate under StateManagerRedis
Stack: 1 of 2. #2 stacks on this one — both touch BaseStateEventProcessor._rehydrate, so they are stacked rather than parallel. Merge this first.
Type of change
- Bug fix (non-breaking change which fixes an issue)
Changes To Core Features
BaseStateEventProcessor._rehydrate — the compatibility path that hydrates a state tree that arrived with no router_data, i.e. one whose token expired since the last event — gated on OnLoadInternalState being present in root_state.substates. StateManagerRedis materializes only the state classes needed to reach the event's own substate, and OnLoadInternalState is a sibling of those, so it is never in the fetched tree and the rehydrate always bailed.
The preamble in _execute_event sets router_data on the root anyway, so no later event rehydrated either. Once a token's state expired, the page the client was on never got its on_load back and is_hydrated stayed False until the client navigated or reloaded.
OnLoadInternalState is a substate of State at import, so the root type check on its own already covers what the guard was after — whether the app has the on-load machinery at all — and get_state fetches the substate when it is not already in the tree.
Tests
First commit adds the repro coverage this fix is measured against, and holds it as xfail(strict) on the redis parametrization; the second commit lands the fix and drops the xfail.
test_rehydrate_runs_on_load_for_the_incoming_events_route— two pages plus a 404 page; the loader that runs is the one for the route the event carried.test_rehydrate_after_expiry_does_not_reload_the_previous_route— the state settles on/page-a, is evicted, and the client's next event comes from/page-b.test_rehydrate_resolves_dynamic_route_args_of_the_incoming_event—/item/[item_id], where the loader is useless without the arg.
The first and last run against all three state managers, so the processor fixture now takes its manager from an indirectly parametrizable processor_state_manager fixture.
Two incidental notes for reviewers. The tests wait on the event's own future rather than processor.join(), which does not cover chained events — the queue can empty between an event finishing and the events it chained being put on it. And add_page with a dynamic route installs the arg as a computed var on the shared State class, which leaked into test_app.py's exact-delta assertions until the cleanup fixture was added, mirroring the one already there.
Full unit suite green, ruff and pyright clean.
🤖 Generated with Claude Code
https://claude.ai/code/session_0171CdXddfL4FTfSAg6WUmj6
Generated by Claude Code