reflex 0.8.5a1__py3-none-any.whl → 0.8.6__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/vite.config.js → jinja/web/vite.config.js.jinja2} +11 -0
- reflex/.templates/web/utils/state.js +5 -0
- reflex/__init__.py +1 -0
- reflex/__init__.pyi +2 -0
- reflex/app.py +77 -13
- reflex/compiler/templates.py +3 -0
- reflex/compiler/utils.py +6 -12
- reflex/components/base/error_boundary.py +2 -0
- reflex/components/core/auto_scroll.py +14 -13
- reflex/components/datadisplay/dataeditor.py +3 -0
- reflex/components/datadisplay/dataeditor.pyi +2 -0
- reflex/components/el/__init__.pyi +4 -0
- reflex/components/el/elements/__init__.py +1 -0
- reflex/components/el/elements/__init__.pyi +5 -0
- reflex/components/el/elements/media.py +32 -0
- reflex/components/el/elements/media.pyi +261 -0
- reflex/components/lucide/icon.py +4 -1
- reflex/components/lucide/icon.pyi +4 -1
- reflex/components/sonner/toast.py +1 -1
- reflex/config.py +15 -0
- reflex/constants/base.py +7 -58
- reflex/environment.py +0 -10
- reflex/istate/manager.py +2 -1
- reflex/plugins/__init__.py +2 -0
- reflex/plugins/_screenshot.py +144 -0
- reflex/plugins/base.py +14 -1
- reflex/state.py +28 -6
- reflex/testing.py +11 -0
- reflex/utils/decorator.py +1 -0
- reflex/utils/monitoring.py +180 -0
- reflex/utils/prerequisites.py +17 -0
- reflex/utils/token_manager.py +215 -0
- {reflex-0.8.5a1.dist-info → reflex-0.8.6.dist-info}/METADATA +5 -2
- {reflex-0.8.5a1.dist-info → reflex-0.8.6.dist-info}/RECORD +37 -34
- {reflex-0.8.5a1.dist-info → reflex-0.8.6.dist-info}/WHEEL +0 -0
- {reflex-0.8.5a1.dist-info → reflex-0.8.6.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.5a1.dist-info → reflex-0.8.6.dist-info}/licenses/LICENSE +0 -0
reflex/plugins/base.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, ParamSpec, Protocol, TypedDict
|
|
|
7
7
|
from typing_extensions import Unpack
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
|
-
from reflex.app import UnevaluatedPage
|
|
10
|
+
from reflex.app import App, UnevaluatedPage
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class CommonContext(TypedDict):
|
|
@@ -44,6 +44,12 @@ class PreCompileContext(CommonContext):
|
|
|
44
44
|
unevaluated_pages: Sequence["UnevaluatedPage"]
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
class PostCompileContext(CommonContext):
|
|
48
|
+
"""Context for post-compile hooks."""
|
|
49
|
+
|
|
50
|
+
app: "App"
|
|
51
|
+
|
|
52
|
+
|
|
47
53
|
class Plugin:
|
|
48
54
|
"""Base class for all plugins."""
|
|
49
55
|
|
|
@@ -104,6 +110,13 @@ class Plugin:
|
|
|
104
110
|
context: The context for the plugin.
|
|
105
111
|
"""
|
|
106
112
|
|
|
113
|
+
def post_compile(self, **context: Unpack[PostCompileContext]) -> None:
|
|
114
|
+
"""Called after the compilation of the plugin.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
context: The context for the plugin.
|
|
118
|
+
"""
|
|
119
|
+
|
|
107
120
|
def __repr__(self):
|
|
108
121
|
"""Return a string representation of the plugin.
|
|
109
122
|
|
reflex/state.py
CHANGED
|
@@ -60,6 +60,7 @@ from reflex.utils.exceptions import (
|
|
|
60
60
|
)
|
|
61
61
|
from reflex.utils.exceptions import ImmutableStateError as ImmutableStateError
|
|
62
62
|
from reflex.utils.exec import is_testing_env
|
|
63
|
+
from reflex.utils.monitoring import is_pyleak_enabled, monitor_loopblocks
|
|
63
64
|
from reflex.utils.types import _isinstance, is_union, value_inside_optional
|
|
64
65
|
from reflex.vars import Field, VarData, field
|
|
65
66
|
from reflex.vars.base import (
|
|
@@ -508,7 +509,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
508
509
|
|
|
509
510
|
new_backend_vars = {
|
|
510
511
|
name: value
|
|
511
|
-
for name, value in cls.__dict__.items()
|
|
512
|
+
for name, value in list(cls.__dict__.items())
|
|
512
513
|
if types.is_backend_base_variable(name, cls)
|
|
513
514
|
}
|
|
514
515
|
# Add annotated backend vars that may not have a default value.
|
|
@@ -1784,7 +1785,11 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1784
1785
|
from reflex.utils import telemetry
|
|
1785
1786
|
|
|
1786
1787
|
# Get the function to process the event.
|
|
1787
|
-
|
|
1788
|
+
if is_pyleak_enabled():
|
|
1789
|
+
console.debug(f"Monitoring leaks for handler: {handler.fn.__qualname__}")
|
|
1790
|
+
fn = functools.partial(monitor_loopblocks(handler.fn), state)
|
|
1791
|
+
else:
|
|
1792
|
+
fn = functools.partial(handler.fn, state)
|
|
1788
1793
|
|
|
1789
1794
|
try:
|
|
1790
1795
|
type_hints = typing.get_type_hints(handler.fn)
|
|
@@ -2463,16 +2468,30 @@ class OnLoadInternalState(State):
|
|
|
2463
2468
|
This is a separate substate to avoid deserializing the entire state tree for every page navigation.
|
|
2464
2469
|
"""
|
|
2465
2470
|
|
|
2471
|
+
# Cannot properly annotate this as `App` due to circular import issues.
|
|
2472
|
+
_app_ref: ClassVar[Any] = None
|
|
2473
|
+
|
|
2466
2474
|
def on_load_internal(self) -> list[Event | EventSpec | event.EventCallback] | None:
|
|
2467
2475
|
"""Queue on_load handlers for the current page.
|
|
2468
2476
|
|
|
2469
2477
|
Returns:
|
|
2470
2478
|
The list of events to queue for on load handling.
|
|
2479
|
+
|
|
2480
|
+
Raises:
|
|
2481
|
+
TypeError: If the app reference is not of type App.
|
|
2471
2482
|
"""
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
)
|
|
2483
|
+
from reflex.app import App
|
|
2484
|
+
|
|
2485
|
+
app = type(self)._app_ref or prerequisites.get_and_validate_app().app
|
|
2486
|
+
if not isinstance(app, App):
|
|
2487
|
+
msg = (
|
|
2488
|
+
f"Expected app to be of type {App.__name__}, got {type(app).__name__}."
|
|
2489
|
+
)
|
|
2490
|
+
raise TypeError(msg)
|
|
2491
|
+
# Cache the app reference for subsequent calls.
|
|
2492
|
+
if type(self)._app_ref is None:
|
|
2493
|
+
type(self)._app_ref = app
|
|
2494
|
+
load_events = app.get_load_events(self.router._page.path)
|
|
2476
2495
|
if not load_events:
|
|
2477
2496
|
self.is_hydrated = True
|
|
2478
2497
|
return None # Fast path for navigation with no on_load events defined.
|
|
@@ -2646,6 +2665,9 @@ def reload_state_module(
|
|
|
2646
2665
|
state: Recursive argument for the state class to reload.
|
|
2647
2666
|
|
|
2648
2667
|
"""
|
|
2668
|
+
# Reset the _app_ref of OnLoadInternalState to avoid stale references.
|
|
2669
|
+
if state is OnLoadInternalState:
|
|
2670
|
+
state._app_ref = None
|
|
2649
2671
|
# Clean out all potentially dirty states of reloaded modules.
|
|
2650
2672
|
for pd_state in tuple(state._potentially_dirty_states):
|
|
2651
2673
|
with contextlib.suppress(ValueError):
|
reflex/testing.py
CHANGED
|
@@ -376,6 +376,17 @@ class AppHarness:
|
|
|
376
376
|
msg = "Failed to reset state manager."
|
|
377
377
|
raise RuntimeError(msg)
|
|
378
378
|
|
|
379
|
+
# Also reset the TokenManager to avoid loop affinity issues
|
|
380
|
+
if (
|
|
381
|
+
hasattr(self.app_instance, "event_namespace")
|
|
382
|
+
and self.app_instance.event_namespace is not None
|
|
383
|
+
and hasattr(self.app_instance.event_namespace, "_token_manager")
|
|
384
|
+
):
|
|
385
|
+
# Import here to avoid circular imports
|
|
386
|
+
from reflex.utils.token_manager import TokenManager
|
|
387
|
+
|
|
388
|
+
self.app_instance.event_namespace._token_manager = TokenManager.create()
|
|
389
|
+
|
|
379
390
|
def _start_frontend(self):
|
|
380
391
|
# Set up the frontend.
|
|
381
392
|
with chdir(self.app_path):
|
reflex/utils/decorator.py
CHANGED
|
@@ -76,6 +76,7 @@ def debug(f: Callable[P, T]) -> Callable[P, T]:
|
|
|
76
76
|
def _write_cached_procedure_file(payload: str, cache_file: Path, value: object):
|
|
77
77
|
import pickle
|
|
78
78
|
|
|
79
|
+
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
79
80
|
cache_file.write_bytes(pickle.dumps((payload, value)))
|
|
80
81
|
|
|
81
82
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""PyLeak integration for monitoring event loop blocking and resource leaks in Reflex applications."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
import threading
|
|
8
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
|
|
9
|
+
from typing import TypeVar, overload
|
|
10
|
+
|
|
11
|
+
from reflex.config import get_config
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from pyleak import no_event_loop_blocking, no_task_leaks, no_thread_leaks
|
|
15
|
+
from pyleak.base import LeakAction
|
|
16
|
+
|
|
17
|
+
PYLEAK_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
PYLEAK_AVAILABLE = False
|
|
20
|
+
no_event_loop_blocking = no_task_leaks = no_thread_leaks = None # pyright: ignore[reportAssignmentType]
|
|
21
|
+
LeakAction = None # pyright: ignore[reportAssignmentType]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Thread-local storage to track if monitoring is already active
|
|
25
|
+
_thread_local = threading.local()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_pyleak_enabled() -> bool:
|
|
29
|
+
"""Check if PyLeak monitoring is enabled and available.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if PyLeak monitoring is enabled in config and PyLeak is available.
|
|
33
|
+
"""
|
|
34
|
+
if not PYLEAK_AVAILABLE:
|
|
35
|
+
return False
|
|
36
|
+
config = get_config()
|
|
37
|
+
return config.enable_pyleak_monitoring
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@contextlib.contextmanager
|
|
41
|
+
def monitor_sync():
|
|
42
|
+
"""Sync context manager for PyLeak monitoring.
|
|
43
|
+
|
|
44
|
+
Yields:
|
|
45
|
+
None: Context for monitoring sync operations.
|
|
46
|
+
"""
|
|
47
|
+
if not is_pyleak_enabled():
|
|
48
|
+
yield
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
# Check if monitoring is already active in this thread
|
|
52
|
+
if getattr(_thread_local, "monitoring_active", False):
|
|
53
|
+
yield
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
config = get_config()
|
|
57
|
+
action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess]
|
|
58
|
+
|
|
59
|
+
# Mark monitoring as active
|
|
60
|
+
_thread_local.monitoring_active = True
|
|
61
|
+
try:
|
|
62
|
+
with contextlib.ExitStack() as stack:
|
|
63
|
+
# Thread leak detection has issues with background tasks (no_thread_leaks)
|
|
64
|
+
stack.enter_context(
|
|
65
|
+
no_event_loop_blocking( # pyright: ignore[reportOptionalCall]
|
|
66
|
+
action=action,
|
|
67
|
+
threshold=config.pyleak_blocking_threshold,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
yield
|
|
71
|
+
finally:
|
|
72
|
+
_thread_local.monitoring_active = False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@contextlib.asynccontextmanager
|
|
76
|
+
async def monitor_async():
|
|
77
|
+
"""Async context manager for PyLeak monitoring.
|
|
78
|
+
|
|
79
|
+
Yields:
|
|
80
|
+
None: Context for monitoring async operations.
|
|
81
|
+
"""
|
|
82
|
+
if not is_pyleak_enabled():
|
|
83
|
+
yield
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Check if monitoring is already active in this thread
|
|
87
|
+
if getattr(_thread_local, "monitoring_active", False):
|
|
88
|
+
yield
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
config = get_config()
|
|
92
|
+
action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess]
|
|
93
|
+
|
|
94
|
+
# Mark monitoring as active
|
|
95
|
+
_thread_local.monitoring_active = True
|
|
96
|
+
try:
|
|
97
|
+
async with contextlib.AsyncExitStack() as stack:
|
|
98
|
+
# Thread leak detection has issues with background tasks (no_thread_leaks)
|
|
99
|
+
# Re-add thread leak later.
|
|
100
|
+
|
|
101
|
+
# Block detection for event loops
|
|
102
|
+
stack.enter_context(
|
|
103
|
+
no_event_loop_blocking( # pyright: ignore[reportOptionalCall]
|
|
104
|
+
action=action,
|
|
105
|
+
threshold=config.pyleak_blocking_threshold,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
# Task leak detection has issues with background tasks (no_task_leaks)
|
|
109
|
+
|
|
110
|
+
yield
|
|
111
|
+
finally:
|
|
112
|
+
_thread_local.monitoring_active = False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
YieldType = TypeVar("YieldType")
|
|
116
|
+
SendType = TypeVar("SendType")
|
|
117
|
+
ReturnType = TypeVar("ReturnType")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@overload
|
|
121
|
+
def monitor_loopblocks(
|
|
122
|
+
func: Callable[..., AsyncGenerator[YieldType, ReturnType]],
|
|
123
|
+
) -> Callable[..., AsyncGenerator[YieldType, ReturnType]]: ...
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@overload
|
|
127
|
+
def monitor_loopblocks(
|
|
128
|
+
func: Callable[..., Generator[YieldType, SendType, ReturnType]],
|
|
129
|
+
) -> Callable[..., Generator[YieldType, SendType, ReturnType]]: ...
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@overload
|
|
133
|
+
def monitor_loopblocks(
|
|
134
|
+
func: Callable[..., Awaitable[ReturnType]],
|
|
135
|
+
) -> Callable[..., Awaitable[ReturnType]]: ...
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def monitor_loopblocks(func: Callable) -> Callable:
|
|
139
|
+
"""Framework decorator using the monitoring module's context manager.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
func: The function to be monitored for leaks.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Decorator function that applies PyLeak monitoring to sync/async functions.
|
|
146
|
+
"""
|
|
147
|
+
if inspect.isasyncgenfunction(func):
|
|
148
|
+
|
|
149
|
+
@functools.wraps(func)
|
|
150
|
+
async def async_gen_wrapper(*args, **kwargs):
|
|
151
|
+
async with monitor_async():
|
|
152
|
+
async for item in func(*args, **kwargs):
|
|
153
|
+
yield item
|
|
154
|
+
|
|
155
|
+
return async_gen_wrapper
|
|
156
|
+
|
|
157
|
+
if asyncio.iscoroutinefunction(func):
|
|
158
|
+
|
|
159
|
+
@functools.wraps(func)
|
|
160
|
+
async def async_wrapper(*args, **kwargs):
|
|
161
|
+
async with monitor_async():
|
|
162
|
+
return await func(*args, **kwargs)
|
|
163
|
+
|
|
164
|
+
return async_wrapper
|
|
165
|
+
|
|
166
|
+
if inspect.isgeneratorfunction(func):
|
|
167
|
+
|
|
168
|
+
@functools.wraps(func)
|
|
169
|
+
def gen_wrapper(*args, **kwargs):
|
|
170
|
+
with monitor_sync():
|
|
171
|
+
yield from func(*args, **kwargs)
|
|
172
|
+
|
|
173
|
+
return gen_wrapper
|
|
174
|
+
|
|
175
|
+
@functools.wraps(func)
|
|
176
|
+
def sync_wrapper(*args, **kwargs):
|
|
177
|
+
with monitor_sync():
|
|
178
|
+
return func(*args, **kwargs)
|
|
179
|
+
|
|
180
|
+
return sync_wrapper # pyright: ignore[reportReturnType]
|
reflex/utils/prerequisites.py
CHANGED
|
@@ -972,6 +972,9 @@ def initialize_web_directory():
|
|
|
972
972
|
console.debug("Initializing the react-router.config.js file.")
|
|
973
973
|
update_react_router_config()
|
|
974
974
|
|
|
975
|
+
console.debug("Initializing the vite.config.js file.")
|
|
976
|
+
initialize_vite_config()
|
|
977
|
+
|
|
975
978
|
console.debug("Initializing the reflex.json file.")
|
|
976
979
|
# Initialize the reflex json file.
|
|
977
980
|
init_reflex_json(project_hash=project_hash)
|
|
@@ -996,6 +999,20 @@ def initialize_package_json():
|
|
|
996
999
|
output_path.write_text(_compile_package_json())
|
|
997
1000
|
|
|
998
1001
|
|
|
1002
|
+
def _compile_vite_config(config: Config):
|
|
1003
|
+
# base must have exactly one trailing slash
|
|
1004
|
+
base = "/"
|
|
1005
|
+
if frontend_path := config.frontend_path.strip("/"):
|
|
1006
|
+
base += frontend_path + "/"
|
|
1007
|
+
return templates.VITE_CONFIG.render(base=base)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def initialize_vite_config():
|
|
1011
|
+
"""Render and write in .web the vite.config.js file using Reflex config."""
|
|
1012
|
+
vite_config_file_path = get_web_dir() / constants.ReactRouter.VITE_CONFIG_FILE
|
|
1013
|
+
vite_config_file_path.write_text(_compile_vite_config(get_config()))
|
|
1014
|
+
|
|
1015
|
+
|
|
999
1016
|
def initialize_bun_config():
|
|
1000
1017
|
"""Initialize the bun config file."""
|
|
1001
1018
|
bun_config_path = get_web_dir() / constants.Bun.CONFIG_PATH
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Token manager for handling client token to session ID mappings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from reflex.utils import console, prerequisites
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from redis.asyncio import Redis
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_new_token() -> str:
|
|
16
|
+
"""Generate a new unique token.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
A new UUID4 token string.
|
|
20
|
+
"""
|
|
21
|
+
return str(uuid.uuid4())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TokenManager(ABC):
|
|
25
|
+
"""Abstract base class for managing client token to session ID mappings."""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initialize the token manager with local dictionaries."""
|
|
29
|
+
# Keep a mapping between socket ID and client token.
|
|
30
|
+
self.token_to_sid: dict[str, str] = {}
|
|
31
|
+
# Keep a mapping between client token and socket ID.
|
|
32
|
+
self.sid_to_token: dict[str, str] = {}
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
async def link_token_to_sid(self, token: str, sid: str) -> str | None:
|
|
36
|
+
"""Link a token to a session ID.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
token: The client token.
|
|
40
|
+
sid: The Socket.IO session ID.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
New token if duplicate detected and new token generated, None otherwise.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
async def disconnect_token(self, token: str, sid: str) -> None:
|
|
48
|
+
"""Clean up token mapping when client disconnects.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
token: The client token.
|
|
52
|
+
sid: The Socket.IO session ID.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def create(cls) -> TokenManager:
|
|
57
|
+
"""Factory method to create appropriate TokenManager implementation.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
RedisTokenManager if Redis is available, LocalTokenManager otherwise.
|
|
61
|
+
"""
|
|
62
|
+
if prerequisites.check_redis_used():
|
|
63
|
+
redis_client = prerequisites.get_redis()
|
|
64
|
+
if redis_client is not None:
|
|
65
|
+
return RedisTokenManager(redis_client)
|
|
66
|
+
|
|
67
|
+
return LocalTokenManager()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LocalTokenManager(TokenManager):
|
|
71
|
+
"""Token manager using local in-memory dictionaries (single worker)."""
|
|
72
|
+
|
|
73
|
+
def __init__(self):
|
|
74
|
+
"""Initialize the local token manager."""
|
|
75
|
+
super().__init__()
|
|
76
|
+
|
|
77
|
+
async def link_token_to_sid(self, token: str, sid: str) -> str | None:
|
|
78
|
+
"""Link a token to a session ID.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
token: The client token.
|
|
82
|
+
sid: The Socket.IO session ID.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
New token if duplicate detected and new token generated, None otherwise.
|
|
86
|
+
"""
|
|
87
|
+
# Check if token is already mapped to a different SID (duplicate tab)
|
|
88
|
+
if token in self.token_to_sid and sid != self.token_to_sid.get(token):
|
|
89
|
+
new_token = _get_new_token()
|
|
90
|
+
self.token_to_sid[new_token] = sid
|
|
91
|
+
self.sid_to_token[sid] = new_token
|
|
92
|
+
return new_token
|
|
93
|
+
|
|
94
|
+
# Normal case - link token to SID
|
|
95
|
+
self.token_to_sid[token] = sid
|
|
96
|
+
self.sid_to_token[sid] = token
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
async def disconnect_token(self, token: str, sid: str) -> None:
|
|
100
|
+
"""Clean up token mapping when client disconnects.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
token: The client token.
|
|
104
|
+
sid: The Socket.IO session ID.
|
|
105
|
+
"""
|
|
106
|
+
# Clean up both mappings
|
|
107
|
+
self.token_to_sid.pop(token, None)
|
|
108
|
+
self.sid_to_token.pop(sid, None)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class RedisTokenManager(LocalTokenManager):
|
|
112
|
+
"""Token manager using Redis for distributed multi-worker support.
|
|
113
|
+
|
|
114
|
+
Inherits local dict logic from LocalTokenManager and adds Redis layer
|
|
115
|
+
for cross-worker duplicate detection.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, redis: Redis):
|
|
119
|
+
"""Initialize the Redis token manager.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
redis: The Redis client instance.
|
|
123
|
+
"""
|
|
124
|
+
# Initialize parent's local dicts
|
|
125
|
+
super().__init__()
|
|
126
|
+
|
|
127
|
+
self.redis = redis
|
|
128
|
+
|
|
129
|
+
# Get token expiration from config (default 1 hour)
|
|
130
|
+
from reflex.config import get_config
|
|
131
|
+
|
|
132
|
+
config = get_config()
|
|
133
|
+
self.token_expiration = config.redis_token_expiration
|
|
134
|
+
|
|
135
|
+
def _get_redis_key(self, token: str) -> str:
|
|
136
|
+
"""Get Redis key for token mapping.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
token: The client token.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Redis key following Reflex conventions: {token}_sid
|
|
143
|
+
"""
|
|
144
|
+
return f"{token}_sid"
|
|
145
|
+
|
|
146
|
+
async def link_token_to_sid(self, token: str, sid: str) -> str | None:
|
|
147
|
+
"""Link a token to a session ID with Redis-based duplicate detection.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
token: The client token.
|
|
151
|
+
sid: The Socket.IO session ID.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
New token if duplicate detected and new token generated, None otherwise.
|
|
155
|
+
"""
|
|
156
|
+
# Fast local check first (handles reconnections)
|
|
157
|
+
if token in self.token_to_sid and self.token_to_sid[token] == sid:
|
|
158
|
+
return None # Same token, same SID = reconnection, no Redis check needed
|
|
159
|
+
|
|
160
|
+
# Check Redis for cross-worker duplicates
|
|
161
|
+
redis_key = self._get_redis_key(token)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
token_exists_in_redis = await self.redis.exists(redis_key)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
console.error(f"Redis error checking token existence: {e}")
|
|
167
|
+
return await super().link_token_to_sid(token, sid)
|
|
168
|
+
|
|
169
|
+
if token_exists_in_redis:
|
|
170
|
+
# Duplicate exists somewhere - generate new token
|
|
171
|
+
new_token = _get_new_token()
|
|
172
|
+
new_redis_key = self._get_redis_key(new_token)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
# Store in Redis
|
|
176
|
+
await self.redis.set(new_redis_key, "1", ex=self.token_expiration)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
console.error(f"Redis error storing new token: {e}")
|
|
179
|
+
# Still update local dicts and continue
|
|
180
|
+
|
|
181
|
+
# Store in local dicts (always do this)
|
|
182
|
+
self.token_to_sid[new_token] = sid
|
|
183
|
+
self.sid_to_token[sid] = new_token
|
|
184
|
+
return new_token
|
|
185
|
+
|
|
186
|
+
# Normal case - store in both Redis and local dicts
|
|
187
|
+
try:
|
|
188
|
+
await self.redis.set(redis_key, "1", ex=self.token_expiration)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
console.error(f"Redis error storing token: {e}")
|
|
191
|
+
# Continue with local storage
|
|
192
|
+
|
|
193
|
+
# Store in local dicts (always do this)
|
|
194
|
+
self.token_to_sid[token] = sid
|
|
195
|
+
self.sid_to_token[sid] = token
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
async def disconnect_token(self, token: str, sid: str) -> None:
|
|
199
|
+
"""Clean up token mapping when client disconnects.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
token: The client token.
|
|
203
|
+
sid: The Socket.IO session ID.
|
|
204
|
+
"""
|
|
205
|
+
# Only clean up if we own it locally (fast ownership check)
|
|
206
|
+
if self.token_to_sid.get(token) == sid:
|
|
207
|
+
# Clean up Redis
|
|
208
|
+
redis_key = self._get_redis_key(token)
|
|
209
|
+
try:
|
|
210
|
+
await self.redis.delete(redis_key)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
console.error(f"Redis error deleting token: {e}")
|
|
213
|
+
|
|
214
|
+
# Clean up local dicts (always do this)
|
|
215
|
+
await super().disconnect_token(token, sid)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reflex
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.6
|
|
4
4
|
Summary: Web apps in pure Python.
|
|
5
5
|
Project-URL: homepage, https://reflex.dev
|
|
6
6
|
Project-URL: repository, https://github.com/reflex-dev/reflex
|
|
@@ -36,6 +36,8 @@ Requires-Dist: sqlmodel<0.1,>=0.0.24
|
|
|
36
36
|
Requires-Dist: starlette>=0.47.0
|
|
37
37
|
Requires-Dist: typing-extensions>=4.13.0
|
|
38
38
|
Requires-Dist: wrapt<2.0,>=1.17.0
|
|
39
|
+
Provides-Extra: monitoring
|
|
40
|
+
Requires-Dist: pyleak<1.0,>=0.1.14; extra == 'monitoring'
|
|
39
41
|
Description-Content-Type: text/markdown
|
|
40
42
|
|
|
41
43
|
<div align="center">
|
|
@@ -50,6 +52,7 @@ Description-Content-Type: text/markdown
|
|
|
50
52
|
[](https://reflex.dev/docs/getting-started/introduction)
|
|
51
53
|
[](https://pepy.tech/projects/reflex)
|
|
52
54
|
[](https://discord.gg/T5WSbC2YtQ)
|
|
55
|
+
[](https://x.com/getreflex)
|
|
53
56
|
|
|
54
57
|
</div>
|
|
55
58
|
|
|
@@ -242,7 +245,7 @@ def get_image(self):
|
|
|
242
245
|
|
|
243
246
|
Within the state, we define functions called event handlers that change the state vars. Event handlers are the way that we can modify the state in Reflex. They can be called in response to user actions, such as clicking a button or typing in a text box. These actions are called events.
|
|
244
247
|
|
|
245
|
-
Our DALL·E
|
|
248
|
+
Our DALL·E app has an event handler, `get_image` which gets this image from the OpenAI API. Using `yield` in the middle of an event handler will cause the UI to update. Otherwise the UI will update at the end of the event handler.
|
|
246
249
|
|
|
247
250
|
### **Routing**
|
|
248
251
|
|