fix: rehydrate state trees that expire during a background task
Summary
A background task releases the state lock while it runs. If the session expires in that time, async with self gets a recreated tree. The delta then carries only the handler's changes, and the browser keeps the values of the expired session (#7478).
A foreground event already recovers from this. When the rx.State root has empty router_data, _execute_event runs _rehydrate. This PR makes StateProxy.__aenter__ use the same signal:
- It restores the router from the event context.
- It calls
BaseStateEventProcessor._rehydrate, now astaticmethodbecause it did not useself.
The hydrate snapshot carries is_hydrated=false, so the frontend does not write client storage. The router, the dynamic route args and rx.LocalStorage/rx.Cookie values stay correct. The result is the same as a foreground event on main today.
Supersedes #7543. That PR tracks recreated states in every manager and adds a Redis manifest key. This PR needs no new tracking state, and it also covers a Redis tree that expired completely.
Closes #7478
Test plan
- New unit test
test_background_task_rehydrates_state_tree_expired_while_unlocked, for the memory and Redis managers. It fails onmainand passes with this change. tests/units/istate,tests/units/reflex_base,test_state.py,test_event.pyandtest_app.pypass. The istate and event processor tests also pass against a live Redis.- A manual run in a small app with a root state, a child state, a cross-state computed var, a
ComponentState, anrx.LocalStoragevar and a dynamic route. The background task sleeps longer than the token TTL.- Memory, full expiry: the expired values reset.
pid, the router,is_hydratedand the browser's localStorage are kept. - Redis, full expiry and partial expiry: the expired values reset, and
pidand localStorage are kept. - Control, no expiry: the background update sends no rehydrate snapshot.
- Memory, full expiry: the expired values reset.
Known limit, the same as the foreground path on main: on Redis, hydrate sends only the states loaded in the tree, so a sibling state that the task does not load keeps its old value.