Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6841.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid leaking temporary router dirtiness into linked/shared state event deltas.
150 changes: 148 additions & 2 deletions reflex/istate/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import asyncio
import contextlib
from collections.abc import AsyncIterator
from typing import TypeVar
from typing import Any, TypeVar

from reflex_base.constants import ROUTER_DATA
from reflex_base.event import Event, get_hydrate_event
Expand Down Expand Up @@ -103,11 +103,157 @@ async def _patch_state(
linked_state.dirty_vars.update(linked_state.computed_vars)
linked_state._mark_dirty()
# Apply the updates into the existing state tree for rehydrate.
# For regular linked-state events this router dirtiness is temporary:
# it forces router-dependent computed vars to resolve for the patched
# tree, but should not leak into the event's final delta.
root_state = original_state._get_root_state()
dirty_state_snapshots: list[
tuple[
BaseState,
set[str],
set[str],
set[str],
dict[str, tuple[bool, Any, bool, Any]],
bool,
]
] = []
if not full_delta:
states_to_snapshot = [root_state]
while states_to_snapshot:
state = states_to_snapshot.pop()
# Interval and always-dirty computed vars are valid output from
# this refresh and must not be mistaken for temporary router dirtiness.
computed_vars_to_preserve = state._expired_computed_vars().union(
state._always_dirty_computed_vars
)
computed_var_snapshots = {
name: (
hasattr(state, computed_var._cache_attr),
getattr(state, computed_var._cache_attr, None),
hasattr(state, computed_var._last_updated_attr),
getattr(state, computed_var._last_updated_attr, None),
)
for name, computed_var in state.computed_vars.items()
}
dirty_state_snapshots.append((
state,
set(state.dirty_vars),
set(state.dirty_substates),
computed_vars_to_preserve,
computed_var_snapshots,
state._was_touched,
))
states_to_snapshot.extend(state.substates.values())
root_state.dirty_vars.add("router")
root_state.dirty_vars.add(ROUTER_DATA)
root_state._mark_dirty()
await root_state._get_resolved_delta()
router_dirty_snapshots: list[
tuple[
BaseState,
set[str],
set[str],
set[str],
set[str],
set[str],
dict[str, tuple[bool, Any, bool, Any]],
]
] = []
if not full_delta:
for (
state,
dirty_vars,
dirty_substates,
computed_vars_to_preserve,
computed_var_snapshots,
_,
) in dirty_state_snapshots:
router_dirty_snapshots.append((
state,
dirty_vars,
dirty_substates,
state.dirty_vars - dirty_vars - computed_vars_to_preserve,
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
state.dirty_substates - dirty_substates,
computed_vars_to_preserve,
computed_var_snapshots,
))
try:
await root_state._get_resolved_delta()
except BaseException:
if not full_delta:
for (
state,
dirty_vars,
dirty_substates,
_,
computed_var_snapshots,
was_touched,
) in dirty_state_snapshots:
state.dirty_vars = dirty_vars
state.dirty_substates = dirty_substates
Comment thread
greptile-apps[bot] marked this conversation as resolved.
state._was_touched = was_touched
for name, computed_var in state.computed_vars.items():
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
(
had_cache,
cached_value,
had_last_updated,
last_updated,
) = computed_var_snapshots[name]
if had_cache:
setattr(state, computed_var._cache_attr, cached_value)
else:
with contextlib.suppress(AttributeError):
delattr(state, computed_var._cache_attr)
if had_last_updated:
setattr(
state,
computed_var._last_updated_attr,
last_updated,
)
else:
with contextlib.suppress(AttributeError):
delattr(state, computed_var._last_updated_attr)
raise
else:
if not full_delta:
computed_refresh_states: list[BaseState] = []
for (
state,
dirty_vars,
dirty_substates,
router_dirty_vars,
_,
computed_vars_to_preserve,
computed_var_snapshots,
) in router_dirty_snapshots:
computed_vars_refreshed = set(computed_vars_to_preserve) | (
state.dirty_vars - router_dirty_vars
).intersection(state.computed_vars)
computed_vars_refreshed.update(
name
for name, computed_var in state.computed_vars.items()
if (
computed_var_snapshots[name][0],
computed_var_snapshots[name][2],
computed_var_snapshots[name][3],
)
!= (
hasattr(state, computed_var._cache_attr),
hasattr(state, computed_var._last_updated_attr),
getattr(state, computed_var._last_updated_attr, None),
)
and hasattr(state, computed_var._cache_attr)
)
if computed_vars_refreshed:
computed_refresh_states.append(state)
state.dirty_vars = (
dirty_vars | computed_vars_to_preserve | computed_vars_refreshed
)
state.dirty_substates = dirty_substates
for refreshed_state in computed_refresh_states:
state = refreshed_state
while state.parent_state is not None:
state.parent_state.dirty_substates.add(state.get_name())
state = state.parent_state
yield
finally:
original_parent_state.substates[state_name] = original_state
Expand Down
Loading
Loading