diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index 1de3fc2638c..836e4848b89 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -1,11 +1,12 @@ """Compiler variables.""" import enum +import os from enum import Enum from types import SimpleNamespace from reflex.base import Base -from reflex.constants import Dirs +from reflex.constants import ENV_MODE_ENV_VAR, Dirs, Env from reflex.utils.imports import ImportVar # The prefix used to create setters for state vars. @@ -14,6 +15,9 @@ # The file used to specify no compilation. NOCOMPILE_FILE = "nocompile" +# The env var to toggle minification of states. +ENV_MINIFY_STATES = "REFLEX_MINIFY_STATES" + class Ext(SimpleNamespace): """Extension used in Reflex.""" @@ -30,6 +34,20 @@ class Ext(SimpleNamespace): EXE = ".exe" +def minify_states() -> bool: + """Whether to minify states. + + Returns: + True if states should be minified. + """ + env = os.environ.get(ENV_MINIFY_STATES, None) + if env is not None: + return env.lower() == "true" + + # minify states in prod by default + return os.environ.get(ENV_MODE_ENV_VAR, "") == Env.PROD.value + + class CompileVars(SimpleNamespace): """The variables used during compilation.""" @@ -61,18 +79,35 @@ class CompileVars(SimpleNamespace): CONNECT_ERROR = "connectErrors" # The name of the function for converting a dict to an event. TO_EVENT = "Event" + + # Whether to minify states. + MINIFY_STATES = minify_states() + + # The name of the OnLoadInternal state. + ON_LOAD_INTERNAL_STATE = ( + "l" if MINIFY_STATES else "reflex___state____on_load_internal_state" + ) # The name of the internal on_load event. - ON_LOAD_INTERNAL = "reflex___state____on_load_internal_state.on_load_internal" - # The name of the internal event to update generic state vars. - UPDATE_VARS_INTERNAL = ( - "reflex___state____update_vars_internal_state.update_vars_internal" + ON_LOAD_INTERNAL = f"{ON_LOAD_INTERNAL_STATE}.on_load_internal" + # The name of the UpdateVarsInternal state. + UPDATE_VARS_INTERNAL_STATE = ( + "u" if MINIFY_STATES else "reflex___state____update_vars_internal_state" ) + # The name of the internal event to update generic state vars. + UPDATE_VARS_INTERNAL = f"{UPDATE_VARS_INTERNAL_STATE}.update_vars_internal" # The name of the frontend event exception state - FRONTEND_EXCEPTION_STATE = "reflex___state____frontend_event_exception_state" + FRONTEND_EXCEPTION_STATE = ( + "e" if MINIFY_STATES else "reflex___state____frontend_event_exception_state" + ) # The full name of the frontend exception state FRONTEND_EXCEPTION_STATE_FULL = ( f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}" ) + INTERNAL_STATE_NAMES = { + ON_LOAD_INTERNAL_STATE, + UPDATE_VARS_INTERNAL_STATE, + FRONTEND_EXCEPTION_STATE, + } class PageNames(SimpleNamespace): diff --git a/reflex/state.py b/reflex/state.py index e2933604240..8f865e0a1dd 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -291,6 +291,62 @@ def __call__(self, *args: Any) -> EventSpec: return super().__call__(*args) +# Keep track of all state instances to calculate minified state names +state_count: int = 0 + +all_state_names: Set[str] = set() + + +def next_minified_state_name() -> str: + """Get the next minified state name. + + Returns: + The next minified state name. + + Raises: + RuntimeError: If the minified state name already exists. + """ + global state_count + global all_state_names + num = state_count + + # All possible chars for minified state name + chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_" + base = len(chars) + state_name = "" + + if num == 0: + state_name = chars[0] + + while num > 0: + state_name = chars[num % base] + state_name + num = num // base + + state_count += 1 + + if state_name in all_state_names: + raise RuntimeError(f"Minified state name {state_name} already exists") + all_state_names.add(state_name) + + return state_name + + +def generate_state_name() -> str: + """Generate a minified state name. + + Returns: + The minified state name. + + Raises: + ValueError: If no more minified state names are available + """ + while name := next_minified_state_name(): + if name in constants.CompileVars.INTERNAL_STATE_NAMES: + continue + return name + raise ValueError("No more minified state names available") + + class BaseState(Base, ABC, extra=pydantic.Extra.allow): """The state of the app.""" @@ -360,6 +416,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # A special event handler for setting base vars. setvar: ClassVar[EventHandler] + # Minified state name + _state_name: ClassVar[Optional[str]] = None + def __init__( self, *args, @@ -461,6 +520,10 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): if mixin: return + # Generate a minified state name by converting state count to string + if not cls._state_name or cls._state_name in all_state_names: + cls._state_name = generate_state_name() + # Validate the module name. cls._validate_module_name() @@ -780,7 +843,16 @@ def get_name(cls) -> str: Returns: The name of the state. + + Raises: + RuntimeError: If the state name is not set. """ + if constants.CompileVars.MINIFY_STATES: + if not cls._state_name: + raise RuntimeError( + "State name minification is enabled, but state name is not set." + ) + return cls._state_name module = cls.__module__.replace(".", "___") return format.to_snake_case(f"{module}___{cls.__name__}") @@ -1852,6 +1924,10 @@ class State(BaseState): class FrontendEventExceptionState(State): """Substate for handling frontend exceptions.""" + _state_name: ClassVar[Optional[str]] = ( + constants.CompileVars.FRONTEND_EXCEPTION_STATE + ) + def handle_frontend_exception(self, stack: str) -> None: """Handle frontend exceptions. @@ -1869,6 +1945,10 @@ def handle_frontend_exception(self, stack: str) -> None: class UpdateVarsInternalState(State): """Substate for handling internal state var updates.""" + _state_name: ClassVar[Optional[str]] = ( + constants.CompileVars.UPDATE_VARS_INTERNAL_STATE + ) + async def update_vars_internal(self, vars: dict[str, Any]) -> None: """Apply updates to fully qualified state vars. @@ -1894,6 +1974,8 @@ class OnLoadInternalState(State): This is a separate substate to avoid deserializing the entire state tree for every page navigation. """ + _state_name: ClassVar[Optional[str]] = constants.CompileVars.ON_LOAD_INTERNAL_STATE + def on_load_internal(self) -> list[Event | EventSpec] | None: """Queue on_load handlers for the current page. diff --git a/tests/test_minify_state.py b/tests/test_minify_state.py new file mode 100644 index 00000000000..e4dea43ef13 --- /dev/null +++ b/tests/test_minify_state.py @@ -0,0 +1,17 @@ +from typing import Set + +from reflex.state import all_state_names, next_minified_state_name + + +def test_next_minified_state_name(): + """Test that the next_minified_state_name function returns unique state names.""" + current_state_count = len(all_state_names) + state_names: Set[str] = set() + gen: int = 10000 + for _ in range(gen): + state_name = next_minified_state_name() + assert state_name not in state_names + state_names.add(state_name) + assert len(state_names) == gen + + assert len(all_state_names) == current_state_count + gen