Repository navigation
Minify state var names and allow installing a name resolver after states exist - #7337
benedikt-bartscher wants to merge 202 commits into
Conversation
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
When a parent and child state have the same minified name, substate resolution can fail because the leading segment is stripped incorrectly. This change adds a flag to skip stripping only on the initial recursive call, ensuring correct resolution even in name collision scenarios.
…into late-bound-wire-names
…into late-bound-wire-names
…inification - state.js: keep the main_state_name prefix check in isStateful, with main's non-string event name guard and nested event normalization. - vars/base.py, tests: keep both sides' imports and new tests. - docs/api-reference/cli.md: keep compile and minify, drop the deleted component command. - test_context_js_template.py: context_template no longer takes state_name.
…into late-bound-wire-names - tests/benchmarks/test_shared_state.py: _always_dirty_substates holds state classes, so the shared-state harness adds SharedStateBaseInternal itself.
Ported upstream changes into the resolver-aware code: - Context template: hydrate_and_load (reflex-dev#7064) gets a resolved wire name in InternalNames (replacing hydrate), alongside upstream's state schema and initial state hashes. state.js reads app.hydrate_and_load and app.update_vars_internal instead of hardcoded default names, and the handshake query (token + scheme) is built for the warm-up socket too. - State reads use upstream's useStateContext (reflex-dev#7441), keyed by the state's resolved full name, into the branch's prefixed local. - Upstream made unknown substates non-fatal (reflex-dev#7435) and dropped backend_state_mismatch; the fatal minification scheme mismatch keeps its own flag (backend_scheme_mismatch) gating events, updates and reconnects. - The frontend state schema hash (reflex-dev#7553) hashes resolver-independent names; the scheme digest already covers the wire names. - The processor's hydrate/hydrate_and_load/on_load_internal lookups cache handlers, not names, so they follow a resolver installed later. - BaseStateToken._state_key and client storage application (_apply_client_storage_vars) use default names and wire var keys. - get_substate keeps upstream's loop but only lets the first segment name the state itself; get_name/get_full_name stay function-wide caches so a resolver install clears every class, and also clears _get_substate_path. - on_connect sets up per-SID state before upstream's new first await. - pyStrAt avoids Array.prototype.at, which the state.js tests forbid.
…inification Ported upstream changes into the resolver-aware code: - Context template: hydrate_and_load (reflex-dev#7064) gets a resolved wire name in InternalEventNames (replacing hydrate), alongside upstream's state schema and initial state hashes. state.js reads app.hydrate_and_load and app.update_vars_internal instead of hardcoded default names, and the handshake query (token + scheme) is built for the warm-up socket too. - State reads use upstream's useStateContext (reflex-dev#7441), keyed by the state's resolved full name, into the prefixed local. - Upstream made unknown substates non-fatal (reflex-dev#7435) and dropped backend_state_mismatch; the fatal minification scheme mismatch keeps its own flag (backend_scheme_mismatch) gating events, updates and reconnects. - The frontend state schema hash (reflex-dev#7553) hashes resolver-independent names; the scheme digest already covers the wire names. - The processor's hydrate/hydrate_and_load/on_load_internal lookups cache handlers, not names, so they follow a resolver installed later. - BaseStateToken._state_key keys by the resolver-independent name. - get_substate keeps upstream's loop but only lets the first segment name the state itself; get_name/get_full_name stay function-wide caches so a resolver install clears every class, and also clears _get_substate_path. - on_connect sets up per-SID state before upstream's new first await. - pyStrAt avoids Array.prototype.at, which the state.js tests forbid.
…into late-bound-wire-names Brings in reflex-dev#6100's merge of upstream/main. Its conflicts here are the reflex-dev#6100 spellings of code this branch already resolved (InternalEventNames vs InternalNames, the hook's state name, tests of state/event-only minification), so this branch's versions are kept and the tree is unchanged.
A handler with no minify.json entry keeps its Python name, which can be the id another handler of its state was given, e.g. when it is added before 'reflex minify sync' runs. Registering it overwrote the other, so an event for one ran the other. Registration now raises when two handlers of a state resolve to the same name. refresh_keys rebuilt the substate sets from base_states, which still holds a state a module reload removed, so installing a resolver after such a reload put the removed state back into the tree. It now re-keys the substate sets as they are.
…into late-bound-wire-names
sync compared decoded ids, so a hand-edited "aa" counted as a collision with "a" and got rehomed; validate and the wire compare the strings.
…inification The reconnect back-off from upstream counts a disconnect before the connect handler runs as a failed attempt. A fatal scheme mismatch disconnects in exactly that window, since socket.io replays it before "connect", so the retry appended a non-fatal error and the connect handler then discarded the out-of-date notice. A client-initiated disconnect no longer counts as a failed attempt; the state.js runtime test mock now replays buffered events and reports client disconnects like socket.io-client to cover it. The hydration toast test now locates the error threshold through has_too_many_connection_errors, which this branch extends with the fatal check.
_assign_next_ids compared decoded ids, so a held "aa" kept "a" from being handed out to a new state, event, or duplicate. It now compares the id strings, as validate and the wire do.
A segment that is one handler's Python name and another handler's id resolved only to the id holder. lookup now lists both and warns which one applies with REFLEX_MINIFY_EVENTS on or off.
_sync_member_ids now passes the raw id strings to _assign_next_ids, so new vars get the string-compared allocation along with events. minify lookup's loop over events and vars keeps the live id holders and adds the member a segment names when that is a different one, warning which reading applies with REFLEX_MINIFY_EVENTS or REFLEX_MINIFY_VARS on or off. Its cross-kind warning counts each kind once, since several members of one kind are already warned about. The lookup test gains a var case.
| if references: | ||
| for match in _DEFAULT_NAME_RUN.finditer(code): | ||
| prefix = "" | ||
| for segment in match.group().split("."): | ||
| prefix = f"{prefix}.{segment}" if prefix else segment | ||
| if prefix in references: | ||
| found.setdefault(prefix, (references[prefix], path)) | ||
| break | ||
| for match in STATE_MEMBER_READ.finditer(code): |
There was a problem hiding this comment.
Old events survive compilation
Events built under an earlier minified scheme can pass raise_for_stale_names unchanged. A module-level LiteralEventVar can store a.b.c; after the handler's ID changes to d, the default-name scan cannot match a.b.c, and the state-local scan does not inspect event strings. Compilation succeeds with an event the backend no longer registers—or now assigns to another handler.
The bundle carries the current scheme digest, so the handshake does not catch this. Track resolved event names when they are built, or rebuild these event Vars when installing the resolver.
Upstream's browser-storage type fix (reflex-dev#7603) widened the field's plain types where _init_var also bound the field's Var. Here _bind_var does the binding, so the widening stays in _init_var, which runs once per field, and the Var binding is left to _bind_var.
A handler name now counts as a stale default reference only when no handler of that state currently goes by it as its wire name.
32edd5e to
bbdce68
Compare
Record the handlers each event name is handed out for and reject a compiled ReflexEvent whose name none of them goes by any more, so an event built under a previous minify.json can't reach a vanished or reassigned handler. Registry lookups compute names without recording them, so a reassigned id doesn't count as handed out.
There was a problem hiding this comment.
2 issues found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/reflex-base/src/reflex_base/registry.py">
<violation number="1" location="packages/reflex-base/src/reflex_base/registry.py:76">
P2: This drops empty resolver segments, so registry keys diverge from the event names emitted by `format_event_handler` for a valid custom resolver. Preserve the formatter’s `handler.state is None` branch to keep inbound event dispatch consistent.</violation>
</file>
<file name="reflex/minify.py">
<violation number="1" location="reflex/minify.py:766">
P2: This live-name check can accept an event for a handler removed by a state reload because it consults the old issuer's resolver mapping without checking the current event registry. Verify that each issuer still has the recorded handler registered before returning it as live; otherwise compilation can emit an event the backend no longer handles.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| """ | ||
| from reflex_base.utils.format import get_event_handler_parts | ||
|
|
||
| return ".".join(filter(None, get_event_handler_parts(handler))) |
There was a problem hiding this comment.
P2: This drops empty resolver segments, so registry keys diverge from the event names emitted by format_event_handler for a valid custom resolver. Preserve the formatter’s handler.state is None branch to keep inbound event dispatch consistent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/reflex-base/src/reflex_base/registry.py, line 76:
<comment>This drops empty resolver segments, so registry keys diverge from the event names emitted by `format_event_handler` for a valid custom resolver. Preserve the formatter’s `handler.state is None` branch to keep inbound event dispatch consistent.</comment>
<file context>
@@ -59,6 +59,23 @@ def _rekey(
+ """
+ from reflex_base.utils.format import get_event_handler_parts
+
+ return ".".join(filter(None, get_event_handler_parts(handler)))
+
+
</file context>
| return ".".join(filter(None, get_event_handler_parts(handler))) | |
| state, name = get_event_handler_parts(handler) | |
| return name if handler.state is None else f"{state}.{name}" |
| for state_cls, handlers in issuers.items(): | ||
| state_name = state_cls.get_full_name() | ||
| for handler in handlers: | ||
| if f"{state_name}.{ctx.get_handler_name(state_cls, handler)}" == name: |
There was a problem hiding this comment.
P2: This live-name check can accept an event for a handler removed by a state reload because it consults the old issuer's resolver mapping without checking the current event registry. Verify that each issuer still has the recorded handler registered before returning it as live; otherwise compilation can emit an event the backend no longer handles.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At reflex/minify.py, line 766:
<comment>This live-name check can accept an event for a handler removed by a state reload because it consults the old issuer's resolver mapping without checking the current event registry. Verify that each issuer still has the recorded handler registered before returning it as live; otherwise compilation can emit an event the backend no longer handles.</comment>
<file context>
@@ -735,17 +735,53 @@ def _renamed_default_references() -> dict[str, str]:
+ for state_cls, handlers in issuers.items():
+ state_name = state_cls.get_full_name()
+ for handler in handlers:
+ if f"{state_name}.{ctx.get_handler_name(state_cls, handler)}" == name:
+ return None
+ labels.append(f"event handler {get_state_full_path(state_cls)}.{handler}")
</file context>
Stacked on #6100 — the diff includes its commits until that merges. The changes specific to this PR are in fb6992b and the merges of
explicit-event-id-minificationthat follow it.What this adds
Var name minification.
REFLEX_MINIFY_VARS=1shortens state var keys on the wire using a new optionalvarssection inminify.json(still schema version 1; older files load with no vars). A minified key is just its id, so a delta goes from{"a.b": {"items_rx_state_": []}}to{"a.b": {"c": []}}.reflex minify init/sync/validate/list/lookupcover vars, and the scheme digest includes them. Var ids that every JS object already has (constructor,__proto__, …) or that end in_rx_state_are rejected.A name resolver can be installed after states exist.
RegistrationContext.set_name_resolverrebuilds the Vars every registered state holds. Backend dependency tracking andVarData.field_dependenciesnow key on classes and the resolver-independent default name, so they never go stale. This lets in-processAppHarnesstests run fully minified. A Var built from state Vars before the install (e.g. a module-level component imported beforeminify.jsonwas loaded) is not fixed; compiling fails with a message naming each stale reference instead of shipping a broken frontend.One place builds wire keys.
format_var_key/format_state_var(which reads through the prefixed localformat_state_localfrom #6100) replace the ad-hocname + FIELD_MARKERconcatenations;update_vars_internalmaps keys back through a per-state lookup, andstate.jsreads the hydrated flag's key from the compiled context instead of hardcoding it.Notes
setvarstill sends the Python var name: it dispatches through the class hierarchy, where a per-state id would be ambiguous.REFLEX_PERF_MODE's check for stringified vars keys on the field marker, so it doesn't see minified vars.is_hydratedare minified too; nothing in the frontend reads them by name anymore.NameResolver.resolve_var_name,reflex_base.utils.format.format_var_key/format_state_var.Testing
update_vars_internalwith minified keys, and the CLI.tests/integration/test_minification.pynow runs in-process with framework states and vars minified.ComponentState, cookie/localStorage persistence across tabs, dynamic routes; websocket frames carry no var or state names.🤖 Generated with Claude Code