reflex 0.8.14.post1__py3-none-any.whl → 0.8.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of reflex might be problematic. Click here for more details.
- reflex/.templates/web/utils/state.js +68 -8
- reflex/__init__.py +12 -7
- reflex/__init__.pyi +11 -3
- reflex/app.py +10 -7
- reflex/base.py +58 -33
- reflex/components/datadisplay/dataeditor.py +17 -2
- reflex/components/datadisplay/dataeditor.pyi +6 -2
- reflex/components/field.py +3 -1
- reflex/components/lucide/icon.py +2 -1
- reflex/components/lucide/icon.pyi +2 -1
- reflex/components/markdown/markdown.py +101 -27
- reflex/components/sonner/toast.py +3 -2
- reflex/components/sonner/toast.pyi +3 -2
- reflex/constants/base.py +5 -0
- reflex/constants/installer.py +3 -3
- reflex/environment.py +9 -1
- reflex/event.py +3 -0
- reflex/istate/manager/__init__.py +120 -0
- reflex/istate/manager/disk.py +210 -0
- reflex/istate/manager/memory.py +76 -0
- reflex/istate/{manager.py → manager/redis.py} +5 -372
- reflex/istate/proxy.py +35 -24
- reflex/model.py +534 -511
- reflex/plugins/tailwind_v4.py +2 -2
- reflex/reflex.py +16 -10
- reflex/state.py +35 -34
- reflex/testing.py +12 -14
- reflex/utils/build.py +11 -1
- reflex/utils/codespaces.py +30 -1
- reflex/utils/compat.py +51 -48
- reflex/utils/misc.py +2 -1
- reflex/utils/monitoring.py +1 -2
- reflex/utils/prerequisites.py +19 -4
- reflex/utils/processes.py +3 -1
- reflex/utils/redir.py +21 -37
- reflex/utils/serializers.py +21 -20
- reflex/utils/telemetry.py +0 -2
- reflex/utils/templates.py +4 -4
- reflex/utils/types.py +89 -90
- reflex/vars/base.py +108 -41
- reflex/vars/color.py +28 -8
- reflex/vars/datetime.py +6 -2
- reflex/vars/dep_tracking.py +2 -2
- reflex/vars/number.py +26 -0
- reflex/vars/object.py +51 -7
- reflex/vars/sequence.py +32 -1
- {reflex-0.8.14.post1.dist-info → reflex-0.8.15.dist-info}/METADATA +8 -3
- {reflex-0.8.14.post1.dist-info → reflex-0.8.15.dist-info}/RECORD +51 -48
- {reflex-0.8.14.post1.dist-info → reflex-0.8.15.dist-info}/WHEEL +0 -0
- {reflex-0.8.14.post1.dist-info → reflex-0.8.15.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.14.post1.dist-info → reflex-0.8.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import dataclasses
|
|
5
6
|
from typing import Any, Literal
|
|
6
7
|
|
|
7
|
-
from reflex.base import Base
|
|
8
8
|
from reflex.components.component import Component, ComponentNamespace
|
|
9
9
|
from reflex.components.lucide.icon import Icon
|
|
10
10
|
from reflex.components.props import NoExtrasAllowedProps
|
|
@@ -35,7 +35,8 @@ toast_ref = Var(
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
@dataclasses.dataclass
|
|
39
|
+
class ToastAction:
|
|
39
40
|
"""A toast action that render a button in the toast."""
|
|
40
41
|
|
|
41
42
|
label: str
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
# ------------------- DO NOT EDIT ----------------------
|
|
4
4
|
# This file was generated by `reflex/utils/pyi_generator.py`!
|
|
5
5
|
# ------------------------------------------------------
|
|
6
|
+
import dataclasses
|
|
6
7
|
from collections.abc import Mapping, Sequence
|
|
7
8
|
from typing import Any, Literal
|
|
8
9
|
|
|
9
|
-
from reflex.base import Base
|
|
10
10
|
from reflex.components.component import Component, ComponentNamespace
|
|
11
11
|
from reflex.components.core.breakpoints import Breakpoints
|
|
12
12
|
from reflex.components.lucide.icon import Icon
|
|
@@ -32,7 +32,8 @@ toast_ref = Var(
|
|
|
32
32
|
_var_data=VarData(imports={f"$/{Dirs.STATE_PATH}": [ImportVar(tag="refs")]}),
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
@dataclasses.dataclass
|
|
36
|
+
class ToastAction:
|
|
36
37
|
label: str
|
|
37
38
|
on_click: Any
|
|
38
39
|
|
reflex/constants/base.py
CHANGED
|
@@ -134,6 +134,11 @@ class Templates(SimpleNamespace):
|
|
|
134
134
|
# The reflex.build frontend host
|
|
135
135
|
REFLEX_BUILD_FRONTEND = "https://build.reflex.dev"
|
|
136
136
|
|
|
137
|
+
# The reflex.build frontend with referrer
|
|
138
|
+
REFLEX_BUILD_FRONTEND_WITH_REFERRER = (
|
|
139
|
+
f"{REFLEX_BUILD_FRONTEND}/?utm_source=reflex_cli"
|
|
140
|
+
)
|
|
141
|
+
|
|
137
142
|
class Dirs(SimpleNamespace):
|
|
138
143
|
"""Folders used by the template system of Reflex."""
|
|
139
144
|
|
reflex/constants/installer.py
CHANGED
|
@@ -87,7 +87,7 @@ def _determine_react_router_version() -> str:
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
def _determine_react_version() -> str:
|
|
90
|
-
default_version = "19.
|
|
90
|
+
default_version = "19.2.0"
|
|
91
91
|
if (version := os.getenv("REACT_VERSION")) and version != default_version:
|
|
92
92
|
from reflex.utils import console
|
|
93
93
|
|
|
@@ -143,11 +143,11 @@ class PackageJson(SimpleNamespace):
|
|
|
143
143
|
"postcss-import": "16.1.1",
|
|
144
144
|
"@react-router/dev": _react_router_version,
|
|
145
145
|
"@react-router/fs-routes": _react_router_version,
|
|
146
|
-
"vite": "npm:rolldown-vite@7.1.
|
|
146
|
+
"vite": "npm:rolldown-vite@7.1.16",
|
|
147
147
|
}
|
|
148
148
|
OVERRIDES = {
|
|
149
149
|
# This should always match the `react` version in DEPENDENCIES for recharts compatibility.
|
|
150
150
|
"react-is": _react_version,
|
|
151
151
|
"cookie": "1.0.2",
|
|
152
|
-
"vite": "npm:rolldown-vite@7.1.
|
|
152
|
+
"vite": "npm:rolldown-vite@7.1.16",
|
|
153
153
|
}
|
reflex/environment.py
CHANGED
|
@@ -24,6 +24,7 @@ from typing import (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
from reflex import constants
|
|
27
|
+
from reflex.constants.base import LogLevel
|
|
27
28
|
from reflex.plugins import Plugin
|
|
28
29
|
from reflex.utils.exceptions import EnvironmentVarValueError
|
|
29
30
|
from reflex.utils.types import GenericType, is_union, value_inside_optional
|
|
@@ -204,7 +205,8 @@ def interpret_env_var_value(
|
|
|
204
205
|
The interpreted value.
|
|
205
206
|
|
|
206
207
|
Raises:
|
|
207
|
-
ValueError: If the
|
|
208
|
+
ValueError: If the value is invalid.
|
|
209
|
+
EnvironmentVarValueError: If the value is invalid for the specific type.
|
|
208
210
|
"""
|
|
209
211
|
field_type = value_inside_optional(field_type)
|
|
210
212
|
|
|
@@ -218,6 +220,12 @@ def interpret_env_var_value(
|
|
|
218
220
|
return interpret_boolean_env(value, field_name)
|
|
219
221
|
if field_type is str:
|
|
220
222
|
return value
|
|
223
|
+
if field_type is LogLevel:
|
|
224
|
+
loglevel = LogLevel.from_string(value)
|
|
225
|
+
if loglevel is None:
|
|
226
|
+
msg = f"Invalid log level value: {value} for {field_name}"
|
|
227
|
+
raise EnvironmentVarValueError(msg)
|
|
228
|
+
return loglevel
|
|
221
229
|
if field_type is int:
|
|
222
230
|
return interpret_int_env(value, field_name)
|
|
223
231
|
if field_type is Path:
|
reflex/event.py
CHANGED
|
@@ -1866,6 +1866,9 @@ def fix_events(
|
|
|
1866
1866
|
# Fix the events created by the handler.
|
|
1867
1867
|
out = []
|
|
1868
1868
|
for e in events:
|
|
1869
|
+
if callable(e) and getattr(e, "__name__", "") == "<lambda>":
|
|
1870
|
+
# A lambda was returned, assume the user wants to call it with no args.
|
|
1871
|
+
e = e()
|
|
1869
1872
|
if isinstance(e, Event):
|
|
1870
1873
|
# If the event is already an event, append it to the list.
|
|
1871
1874
|
out.append(e)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""State manager for managing client states."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import dataclasses
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from reflex import constants
|
|
9
|
+
from reflex.config import get_config
|
|
10
|
+
from reflex.state import BaseState
|
|
11
|
+
from reflex.utils import console, prerequisites
|
|
12
|
+
from reflex.utils.exceptions import InvalidStateManagerModeError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclasses.dataclass
|
|
16
|
+
class StateManager(ABC):
|
|
17
|
+
"""A class to manage many client states."""
|
|
18
|
+
|
|
19
|
+
# The state class to use.
|
|
20
|
+
state: type[BaseState]
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def create(cls, state: type[BaseState]):
|
|
24
|
+
"""Create a new state manager.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
state: The state class to use.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
InvalidStateManagerModeError: If the state manager mode is invalid.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The state manager (either disk, memory or redis).
|
|
34
|
+
"""
|
|
35
|
+
config = get_config()
|
|
36
|
+
if prerequisites.parse_redis_url() is not None:
|
|
37
|
+
config.state_manager_mode = constants.StateManagerMode.REDIS
|
|
38
|
+
if config.state_manager_mode == constants.StateManagerMode.MEMORY:
|
|
39
|
+
from reflex.istate.manager.memory import StateManagerMemory
|
|
40
|
+
|
|
41
|
+
return StateManagerMemory(state=state)
|
|
42
|
+
if config.state_manager_mode == constants.StateManagerMode.DISK:
|
|
43
|
+
from reflex.istate.manager.disk import StateManagerDisk
|
|
44
|
+
|
|
45
|
+
return StateManagerDisk(state=state)
|
|
46
|
+
if config.state_manager_mode == constants.StateManagerMode.REDIS:
|
|
47
|
+
redis = prerequisites.get_redis()
|
|
48
|
+
if redis is not None:
|
|
49
|
+
from reflex.istate.manager.redis import StateManagerRedis
|
|
50
|
+
|
|
51
|
+
# make sure expiration values are obtained only from the config object on creation
|
|
52
|
+
return StateManagerRedis(
|
|
53
|
+
state=state,
|
|
54
|
+
redis=redis,
|
|
55
|
+
token_expiration=config.redis_token_expiration,
|
|
56
|
+
lock_expiration=config.redis_lock_expiration,
|
|
57
|
+
lock_warning_threshold=config.redis_lock_warning_threshold,
|
|
58
|
+
)
|
|
59
|
+
msg = f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
|
|
60
|
+
raise InvalidStateManagerModeError(msg)
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
async def get_state(self, token: str) -> BaseState:
|
|
64
|
+
"""Get the state for a token.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
token: The token to get the state for.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The state for the token.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
async def set_state(self, token: str, state: BaseState):
|
|
75
|
+
"""Set the state for a token.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
token: The token to set the state for.
|
|
79
|
+
state: The state to set.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
@contextlib.asynccontextmanager
|
|
84
|
+
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
85
|
+
"""Modify the state for a token while holding exclusive lock.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
token: The token to modify the state for.
|
|
89
|
+
|
|
90
|
+
Yields:
|
|
91
|
+
The state for the token.
|
|
92
|
+
"""
|
|
93
|
+
yield self.state()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _default_token_expiration() -> int:
|
|
97
|
+
"""Get the default token expiration time.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The default token expiration time.
|
|
101
|
+
"""
|
|
102
|
+
return get_config().redis_token_expiration
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def reset_disk_state_manager():
|
|
106
|
+
"""Reset the disk state manager."""
|
|
107
|
+
console.debug("Resetting disk state manager.")
|
|
108
|
+
states_directory = prerequisites.get_states_dir()
|
|
109
|
+
if states_directory.exists():
|
|
110
|
+
for path in states_directory.iterdir():
|
|
111
|
+
path.unlink()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_state_manager() -> StateManager:
|
|
115
|
+
"""Get the state manager for the app that is currently running.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
The state manager.
|
|
119
|
+
"""
|
|
120
|
+
return prerequisites.get_and_validate_app().app.state_manager
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""A state manager that stores states on disk."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import dataclasses
|
|
6
|
+
import functools
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from hashlib import md5
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
13
|
+
from reflex.istate.manager import StateManager, _default_token_expiration
|
|
14
|
+
from reflex.state import BaseState, _split_substate_key, _substate_key
|
|
15
|
+
from reflex.utils import path_ops, prerequisites
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclasses.dataclass
|
|
19
|
+
class StateManagerDisk(StateManager):
|
|
20
|
+
"""A state manager that stores states on disk."""
|
|
21
|
+
|
|
22
|
+
# The mapping of client ids to states.
|
|
23
|
+
states: dict[str, BaseState] = dataclasses.field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
# The mutex ensures the dict of mutexes is updated exclusively
|
|
26
|
+
_state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock())
|
|
27
|
+
|
|
28
|
+
# The dict of mutexes for each client
|
|
29
|
+
_states_locks: dict[str, asyncio.Lock] = dataclasses.field(
|
|
30
|
+
default_factory=dict,
|
|
31
|
+
init=False,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# The token expiration time (s).
|
|
35
|
+
token_expiration: int = dataclasses.field(default_factory=_default_token_expiration)
|
|
36
|
+
|
|
37
|
+
def __post_init__(self):
|
|
38
|
+
"""Create a new state manager."""
|
|
39
|
+
path_ops.mkdir(self.states_directory)
|
|
40
|
+
|
|
41
|
+
self._purge_expired_states()
|
|
42
|
+
|
|
43
|
+
@functools.cached_property
|
|
44
|
+
def states_directory(self) -> Path:
|
|
45
|
+
"""Get the states directory.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The states directory.
|
|
49
|
+
"""
|
|
50
|
+
return prerequisites.get_states_dir()
|
|
51
|
+
|
|
52
|
+
def _purge_expired_states(self):
|
|
53
|
+
"""Purge expired states from the disk."""
|
|
54
|
+
import time
|
|
55
|
+
|
|
56
|
+
for path in path_ops.ls(self.states_directory):
|
|
57
|
+
# check path is a pickle file
|
|
58
|
+
if path.suffix != ".pkl":
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
# load last edited field from file
|
|
62
|
+
last_edited = path.stat().st_mtime
|
|
63
|
+
|
|
64
|
+
# check if the file is older than the token expiration time
|
|
65
|
+
if time.time() - last_edited > self.token_expiration:
|
|
66
|
+
# remove the file
|
|
67
|
+
path.unlink()
|
|
68
|
+
|
|
69
|
+
def token_path(self, token: str) -> Path:
|
|
70
|
+
"""Get the path for a token.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
token: The token to get the path for.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The path for the token.
|
|
77
|
+
"""
|
|
78
|
+
return (
|
|
79
|
+
self.states_directory / f"{md5(token.encode()).hexdigest()}.pkl"
|
|
80
|
+
).absolute()
|
|
81
|
+
|
|
82
|
+
async def load_state(self, token: str) -> BaseState | None:
|
|
83
|
+
"""Load a state object based on the provided token.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
token: The token used to identify the state object.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The loaded state object or None.
|
|
90
|
+
"""
|
|
91
|
+
token_path = self.token_path(token)
|
|
92
|
+
|
|
93
|
+
if token_path.exists():
|
|
94
|
+
try:
|
|
95
|
+
with token_path.open(mode="rb") as file:
|
|
96
|
+
return BaseState._deserialize(fp=file)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
async def populate_substates(
|
|
102
|
+
self, client_token: str, state: BaseState, root_state: BaseState
|
|
103
|
+
):
|
|
104
|
+
"""Populate the substates of a state object.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
client_token: The client token.
|
|
108
|
+
state: The state object to populate.
|
|
109
|
+
root_state: The root state object.
|
|
110
|
+
"""
|
|
111
|
+
for substate in state.get_substates():
|
|
112
|
+
substate_token = _substate_key(client_token, substate)
|
|
113
|
+
|
|
114
|
+
fresh_instance = await root_state.get_state(substate)
|
|
115
|
+
instance = await self.load_state(substate_token)
|
|
116
|
+
if instance is not None:
|
|
117
|
+
# Ensure all substates exist, even if they weren't serialized previously.
|
|
118
|
+
instance.substates = fresh_instance.substates
|
|
119
|
+
else:
|
|
120
|
+
instance = fresh_instance
|
|
121
|
+
state.substates[substate.get_name()] = instance
|
|
122
|
+
instance.parent_state = state
|
|
123
|
+
|
|
124
|
+
await self.populate_substates(client_token, instance, root_state)
|
|
125
|
+
|
|
126
|
+
@override
|
|
127
|
+
async def get_state(
|
|
128
|
+
self,
|
|
129
|
+
token: str,
|
|
130
|
+
) -> BaseState:
|
|
131
|
+
"""Get the state for a token.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
token: The token to get the state for.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The state for the token.
|
|
138
|
+
"""
|
|
139
|
+
client_token = _split_substate_key(token)[0]
|
|
140
|
+
root_state = self.states.get(client_token)
|
|
141
|
+
if root_state is not None:
|
|
142
|
+
# Retrieved state from memory.
|
|
143
|
+
return root_state
|
|
144
|
+
|
|
145
|
+
# Deserialize root state from disk.
|
|
146
|
+
root_state = await self.load_state(_substate_key(client_token, self.state))
|
|
147
|
+
# Create a new root state tree with all substates instantiated.
|
|
148
|
+
fresh_root_state = self.state(_reflex_internal_init=True)
|
|
149
|
+
if root_state is None:
|
|
150
|
+
root_state = fresh_root_state
|
|
151
|
+
else:
|
|
152
|
+
# Ensure all substates exist, even if they were not serialized previously.
|
|
153
|
+
root_state.substates = fresh_root_state.substates
|
|
154
|
+
self.states[client_token] = root_state
|
|
155
|
+
await self.populate_substates(client_token, root_state, root_state)
|
|
156
|
+
return root_state
|
|
157
|
+
|
|
158
|
+
async def set_state_for_substate(self, client_token: str, substate: BaseState):
|
|
159
|
+
"""Set the state for a substate.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
client_token: The client token.
|
|
163
|
+
substate: The substate to set.
|
|
164
|
+
"""
|
|
165
|
+
substate_token = _substate_key(client_token, substate)
|
|
166
|
+
|
|
167
|
+
if substate._get_was_touched():
|
|
168
|
+
substate._was_touched = False # Reset the touched flag after serializing.
|
|
169
|
+
pickle_state = substate._serialize()
|
|
170
|
+
if pickle_state:
|
|
171
|
+
if not self.states_directory.exists():
|
|
172
|
+
self.states_directory.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
self.token_path(substate_token).write_bytes(pickle_state)
|
|
174
|
+
|
|
175
|
+
for substate_substate in substate.substates.values():
|
|
176
|
+
await self.set_state_for_substate(client_token, substate_substate)
|
|
177
|
+
|
|
178
|
+
@override
|
|
179
|
+
async def set_state(self, token: str, state: BaseState):
|
|
180
|
+
"""Set the state for a token.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
token: The token to set the state for.
|
|
184
|
+
state: The state to set.
|
|
185
|
+
"""
|
|
186
|
+
client_token, _ = _split_substate_key(token)
|
|
187
|
+
await self.set_state_for_substate(client_token, state)
|
|
188
|
+
|
|
189
|
+
@override
|
|
190
|
+
@contextlib.asynccontextmanager
|
|
191
|
+
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
192
|
+
"""Modify the state for a token while holding exclusive lock.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
token: The token to modify the state for.
|
|
196
|
+
|
|
197
|
+
Yields:
|
|
198
|
+
The state for the token.
|
|
199
|
+
"""
|
|
200
|
+
# Disk state manager ignores the substate suffix and always returns the top-level state.
|
|
201
|
+
client_token, _ = _split_substate_key(token)
|
|
202
|
+
if client_token not in self._states_locks:
|
|
203
|
+
async with self._state_manager_lock:
|
|
204
|
+
if client_token not in self._states_locks:
|
|
205
|
+
self._states_locks[client_token] = asyncio.Lock()
|
|
206
|
+
|
|
207
|
+
async with self._states_locks[client_token]:
|
|
208
|
+
state = await self.get_state(token)
|
|
209
|
+
yield state
|
|
210
|
+
await self.set_state(token, state)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""A state manager that stores states in memory."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import dataclasses
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
|
|
10
|
+
from reflex.istate.manager import StateManager
|
|
11
|
+
from reflex.state import BaseState, _split_substate_key
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass
|
|
15
|
+
class StateManagerMemory(StateManager):
|
|
16
|
+
"""A state manager that stores states in memory."""
|
|
17
|
+
|
|
18
|
+
# The mapping of client ids to states.
|
|
19
|
+
states: dict[str, BaseState] = dataclasses.field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
# The mutex ensures the dict of mutexes is updated exclusively
|
|
22
|
+
_state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock())
|
|
23
|
+
|
|
24
|
+
# The dict of mutexes for each client
|
|
25
|
+
_states_locks: dict[str, asyncio.Lock] = dataclasses.field(
|
|
26
|
+
default_factory=dict, init=False
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
async def get_state(self, token: str) -> BaseState:
|
|
31
|
+
"""Get the state for a token.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
token: The token to get the state for.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The state for the token.
|
|
38
|
+
"""
|
|
39
|
+
# Memory state manager ignores the substate suffix and always returns the top-level state.
|
|
40
|
+
token = _split_substate_key(token)[0]
|
|
41
|
+
if token not in self.states:
|
|
42
|
+
self.states[token] = self.state(_reflex_internal_init=True)
|
|
43
|
+
return self.states[token]
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
async def set_state(self, token: str, state: BaseState):
|
|
47
|
+
"""Set the state for a token.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
token: The token to set the state for.
|
|
51
|
+
state: The state to set.
|
|
52
|
+
"""
|
|
53
|
+
token = _split_substate_key(token)[0]
|
|
54
|
+
self.states[token] = state
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
@contextlib.asynccontextmanager
|
|
58
|
+
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
59
|
+
"""Modify the state for a token while holding exclusive lock.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
token: The token to modify the state for.
|
|
63
|
+
|
|
64
|
+
Yields:
|
|
65
|
+
The state for the token.
|
|
66
|
+
"""
|
|
67
|
+
# Memory state manager ignores the substate suffix and always returns the top-level state.
|
|
68
|
+
token = _split_substate_key(token)[0]
|
|
69
|
+
if token not in self._states_locks:
|
|
70
|
+
async with self._state_manager_lock:
|
|
71
|
+
if token not in self._states_locks:
|
|
72
|
+
self._states_locks[token] = asyncio.Lock()
|
|
73
|
+
|
|
74
|
+
async with self._states_locks[token]:
|
|
75
|
+
state = await self.get_state(token)
|
|
76
|
+
yield state
|