Skip to content
Merged
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
14 changes: 8 additions & 6 deletions reflex/istate/delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@
)


# True while a delta is built for dirty fields whose computed dependents the
# client already holds current, so the traversal neither invalidates nor
# evaluates any computed var.
_send_fields_only: ContextVar[bool] = ContextVar("_send_fields_only", default=False)
# Replay browser storage and cached dependents without blanket invalidation.
# Real assignments invalidate their caches eagerly. Uncached values are left
# to the normal event delta, avoiding an extra evaluation during hydration.
_replay_client_storage: ContextVar[bool] = ContextVar(
"_replay_client_storage", default=False
)


class _DeltaRecord(NamedTuple):
Expand Down Expand Up @@ -233,9 +235,9 @@ def build_delta(state: BaseState) -> Delta:
"""
delta = {}

if _send_fields_only.get():
if _replay_client_storage.get():
delta_vars = state.dirty_vars & state._frontend_var_names
delta_vars.difference_update(state.computed_vars)
delta_vars.difference_update(state._always_dirty_computed_vars)
Comment thread
masenf marked this conversation as resolved.
else:
state._mark_dirty_computed_vars()
delta_vars = state.dirty_vars & state._frontend_var_names
Expand Down
65 changes: 46 additions & 19 deletions reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@
from reflex.istate.delta import (
Delta,
DeltaMapping,
_replay_client_storage,
_resolve_delta,
_send_fields_only,
build_delta,
clean_state,
resolve_delta,
Expand Down Expand Up @@ -1862,16 +1862,25 @@ async def get_var_value(self, var: Var[VAR_TYPE]) -> VAR_TYPE:
return await value
return value

def _mark_dirty_computed_vars(self, var_names: Iterable[str] | None = None) -> None:
"""Invalidate the computed vars depending on changed vars of this state, transitively.
def _mark_dirty_computed_vars(
self, var_names: Iterable[str] | None = None, *, invalidate: bool = True
) -> None:
"""Mark the computed vars depending on changed vars of this state dirty, transitively.

Invalidates their cached values, so that the next read recomputes them.
Uncached and expired computed vars recompute on access, so they count as
changed along with any var.

Args:
var_names: The changed vars of this state; all its dirty vars if omitted.
invalidate: Whether the dependents' caches are invalidated. False marks
them for delivery only, leaving their caches and the uncached and
expired computed vars alone, so that an unchanged value is sent
as it is rather than recomputed.
"""
if self._always_dirty_computed_vars or self._interval_computed_var_names:
if invalidate and (
self._always_dirty_computed_vars or self._interval_computed_var_names
):
recomputed = (
self._always_dirty_computed_vars | self._expired_computed_vars()
)
Expand Down Expand Up @@ -1900,7 +1909,8 @@ def _mark_dirty_computed_vars(self, var_names: Iterable[str] | None = None) -> N
else:
target = state._get_root_state().get_substate(state_name.split("."))
target._mark_ancestors_dirty()
target.computed_vars[cvar_name].mark_dirty(instance=target)
if invalidate:
target.computed_vars[cvar_name].mark_dirty(instance=target)
target.dirty_vars.add(cvar_name)
if cvar_name in target._var_dependencies:
pending.append((target, cvar_name))
Expand Down Expand Up @@ -2443,9 +2453,9 @@ async def hydrate_and_load(
``get_delta`` and may not write browser storage, so a second delta
carries the browser-provided values again, as
``update_vars_internal``'s does: ``get_delta`` overrides see them
there, and the browser stores what they return. That delta holds only
those values, because the snapshot already sent the computed vars
derived from them. A page with on_load
there, and the browser stores what they return. Cached dependents are
included without invalidating their snapshot caches; real assignments
in an override invalidate those caches normally. A page with on_load
handlers then gets ``on_load_internal``, which only locks its leaf
substate.

Expand Down Expand Up @@ -2482,17 +2492,7 @@ async def hydrate_and_load(
# Follow-up corrections must be allowed to write browser storage.
self.dirty_vars.discard(constants.CompileVars.IS_HYDRATED)
if applied:
# Pass the browser's values through get_delta overrides. The
# snapshot already sent their dependents, computed from them.
for var_state, var_name in applied:
var_state.dirty_vars.add(var_name)
var_state._mark_ancestors_dirty()
token = _send_fields_only.set(True)
try:
delta = await self._get_resolved_delta()
finally:
_send_fields_only.reset(token)
self._clean()
delta = await _get_client_storage_delta(self, applied)
Comment thread
masenf marked this conversation as resolved.
await ctx.emit_delta(delta=delta)
if not RegistrationContext.get().app.get_load_events(self.rx_router_url.path):
self.is_hydrated = True
Expand All @@ -2504,6 +2504,33 @@ async def hydrate_and_load(
return [OnLoadInternalState.on_load_internal]


async def _get_client_storage_delta(
root: BaseState, applied: list[tuple[BaseState, str]]
) -> Delta:
"""Replay browser storage without discarding real override mutations.

Args:
root: The hydrated root state.
applied: Browser storage fields applied by this hydration.

Returns:
Override-filtered storage and cached dependent values.
"""
# Include cached storage dependents in the one public traversal, even when
# their branch is visited before the override changes the storage value.
# Mark delivery only: unchanged caches still hold the snapshot's values.
for state, name in applied:
state._mark_dirty_computed_vars((name,), invalidate=False)
state.dirty_vars.add(name)
state._mark_ancestors_dirty()
token = _replay_client_storage.set(True)
try:
return await root._get_resolved_delta()
finally:
_replay_client_storage.reset(token)
root._clean()
Comment thread
masenf marked this conversation as resolved.


T = TypeVar("T", bound=BaseState)


Expand Down
Loading
Loading