Repository navigation
Explicit state and event id minification - #6100
benedikt-bartscher wants to merge 163 commits into
Conversation
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Merging this PR will improve performance by 5.62%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing |
|
TODO: best-effort-mode
a similar thing can be implemented for event handler id's edit: not that easy, as state names are baked into to many things (events, vars etc). we would need to defer init_subclass and possibly parts of @rx.var and @rx.event edit2: i have found a better approach with a minify.json which makes this obsolete |
|
Open for discussion: Should reflex reserve the first 5/10 state id's for internal states? Current reflex uses 0-3, but that might change in the future. edit: with sibling uniqueness we would just need to reserve the state id's on the first rx.State subclass level. edit2: i have found a better approach with a minify.json which makes this obsolete |
|
TODO: think about sibling uniqueness for states. currently using global uniqueness edit: adjusted in 6b4c5fa |
35ad1c6 to
1647c1e
Compare
|
Idea: store ids in a minify.json file
|
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.
There was a problem hiding this comment.
All reported issues were addressed across 11 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…ler spawning and minify test configs
There was a problem hiding this comment.
All reported issues were addressed across 10 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…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.
…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.
| state_cls_name = f"{cls.__name__}__{_state_key}" | ||
| # Report the key rather than letting the generated class name reach | ||
| # the sibling-shadowing guard, whose message names neither. | ||
| existing = getattr(reflex.istate.dynamic, state_cls_name, None) | ||
| if existing is not None: |
There was a problem hiding this comment.
Different component keys collide
Distinct valid _state_key values can produce the same state name. For example, cart_item and cartItem pass this exact-name check, but default_state_name converts both generated class names to snake case. With minification off—the default—the second create() raises the sibling-shadowing error and stops app compilation.
Check uniqueness using the final state name, or encode keys without losing their spelling.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
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.
There was a problem hiding this comment.
1 issue found across 2 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:394">
P2: This check compares only the leaf of each function’s `__qualname__`, so distinct same-named handlers from different modules or nested scopes bypass it and the later handler overwrites the first. Compare the module plus full qualname before replacing the registry entry.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| existing = self.event_handlers.get(full_name) | ||
| # An entry of another class is replaced, as on a module reload. | ||
| if existing is not None and existing.handler.state is handler.state: | ||
| existing_name = existing.handler.fn.__qualname__.rpartition(".")[2] |
There was a problem hiding this comment.
P2: This check compares only the leaf of each function’s __qualname__, so distinct same-named handlers from different modules or nested scopes bypass it and the later handler overwrites the first. Compare the module plus full qualname before replacing the registry entry.
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 394:
<comment>This check compares only the leaf of each function’s `__qualname__`, so distinct same-named handlers from different modules or nested scopes bypass it and the later handler overwrites the first. Compare the module plus full qualname before replacing the registry entry.</comment>
<file context>
@@ -380,10 +380,25 @@ def _register_event_handler(
+ existing = self.event_handlers.get(full_name)
+ # An entry of another class is replaced, as on a module reload.
+ if existing is not None and existing.handler.state is handler.state:
+ existing_name = existing.handler.fn.__qualname__.rpartition(".")[2]
+ name = handler.fn.__qualname__.rpartition(".")[2]
+ if existing_name != name:
</file context>
|
|
||
| # Build lookup: full_path -> minified_id (None if no entry). | ||
| path_to_id: dict[str, str | None] = {} | ||
| for s in collect_all_states(State): |
There was a problem hiding this comment.
Inspection misses separate state roots
minify list and minify lookup only walk the framework State tree, while init, sync, and validate cover every registered root. An app using an independent BaseState root, such as the existing App(_state=Base) test, therefore gets IDs that these inspection commands cannot show or resolve.
Use the same root inventory across the commands so developers can inspect all IDs the CLI assigns.
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.
There was a problem hiding this comment.
1 issue found across 2 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="reflex/minify.py">
<violation number="1" location="reflex/minify.py:736">
P2: This conversion reintroduces numeric aliasing while allocating new state IDs: a valid sibling ID `"aa"` reserves the unrelated wire ID `"a"`. Preserve raw IDs through the allocation check, or ignore noncanonical aliases when building numeric reservations, so sync can assign the free `"a"` slot.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
View guided diff | Re-trigger cubic
| siblings, parent = _state_id_scope(states, parent_key, set(state_paths)) | ||
| return _assign_next_ids( | ||
| state_paths, | ||
| {minified_name_to_int(i) for i in siblings}, |
There was a problem hiding this comment.
P2: This conversion reintroduces numeric aliasing while allocating new state IDs: a valid sibling ID "aa" reserves the unrelated wire ID "a". Preserve raw IDs through the allocation check, or ignore noncanonical aliases when building numeric reservations, so sync can assign the free "a" slot.
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 736:
<comment>This conversion reintroduces numeric aliasing while allocating new state IDs: a valid sibling ID `"aa"` reserves the unrelated wire ID `"a"`. Preserve raw IDs through the allocation check, or ignore noncanonical aliases when building numeric reservations, so sync can assign the free `"a"` slot.</comment>
<file context>
@@ -734,7 +732,11 @@ def _assign_state_ids(
return _assign_next_ids(
- state_paths, siblings, reassign_deleted, for_states=True, skip=parent
+ state_paths,
+ {minified_name_to_int(i) for i in siblings},
+ reassign_deleted,
+ for_states=True,
</file context>
…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.
| handlers = [name for name in holders if name in current.event_handlers] or ( | ||
| [part] if part in current.event_handlers else [] | ||
| ) |
There was a problem hiding this comment.
minify lookup hides a valid reading when a segment is both one live handler's Python name and another handler's ID. For example, with foo -> a and bar -> foo, looking up <state>.foo returns only bar. With event minification disabled, that same wire name calls foo.
This can send a developer debugging an event to the wrong handler. Since inspection accepts both modes without using the current flags, return both readings and explain the ambiguity instead of presenting one as certain.
Summary
Add state and event name minification with CLI management tools.
minify.jsonconfig file to map state/event names to short IDs (e.g.,myapp.state.AppState→"a")reflex minifyCLI command group for managing minificationCLI Commands
based on #6098