Skip to content

Commit 8819444

Browse files
committed
Merge remote-tracking branch 'origin/main' into extract-env-json-into-its-own-file
2 parents b79518e + 54e0727 commit 8819444

25 files changed

Lines changed: 1040 additions & 226 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
1111
[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex)
1212
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
13+
[![Twitter](https://img.shields.io/twitter/follow/getreflex)](https://x.com/getreflex)
1314

1415
</div>
1516

@@ -202,7 +203,7 @@ def get_image(self):
202203

203204
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.
204205

205-
Our DALL·E. app has an event handler, `get_image` to which get 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.
206+
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.
206207

207208
### **Routing**
208209

pyi_hashes.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"reflex/__init__.pyi": "e9f08e115b1e1dd89beabacade84a589",
2+
"reflex/__init__.pyi": "cc4f461d8244f0f372b7607eb1edd146",
33
"reflex/components/__init__.pyi": "ac05995852baa81062ba3d18fbc489fb",
44
"reflex/components/base/__init__.pyi": "16e47bf19e0d62835a605baa3d039c5a",
55
"reflex/components/base/app_wrap.pyi": "22e94feaa9fe675bcae51c412f5b67f1",
@@ -24,23 +24,23 @@
2424
"reflex/components/core/window_events.pyi": "76bf03a273a1fbbb3b333e10d5d08c30",
2525
"reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e",
2626
"reflex/components/datadisplay/code.pyi": "ec35c215a219c616ff38c30047d9b601",
27-
"reflex/components/datadisplay/dataeditor.pyi": "afc6df39dbbdbb5b209d4751c3e189b2",
27+
"reflex/components/datadisplay/dataeditor.pyi": "82c652f0679148d8431a0cf645686a50",
2828
"reflex/components/datadisplay/shiki_code_block.pyi": "1d53e75b6be0d3385a342e7b3011babd",
29-
"reflex/components/el/__init__.pyi": "00ded672c0336da6225036f56855b042",
29+
"reflex/components/el/__init__.pyi": "0adfd001a926a2a40aee94f6fa725ecc",
3030
"reflex/components/el/element.pyi": "c5974a92fbc310e42d0f6cfdd13472f4",
31-
"reflex/components/el/elements/__init__.pyi": "2e30624329b8b535dfd8969f95efdd25",
31+
"reflex/components/el/elements/__init__.pyi": "29512d7a6b29c6dc5ff68d3b31f26528",
3232
"reflex/components/el/elements/base.pyi": "7d1163c7221bb16691ce179646ce8515",
3333
"reflex/components/el/elements/forms.pyi": "a2099706131a8cf364b2bf9a6f958cdc",
3434
"reflex/components/el/elements/inline.pyi": "8bc2bbf8f3fd8bb9b5910c0e888e5386",
35-
"reflex/components/el/elements/media.pyi": "09c761bb3743e2351073ad81397e7d13",
35+
"reflex/components/el/elements/media.pyi": "66846a0c74fbe772811cd6577b2796d0",
3636
"reflex/components/el/elements/metadata.pyi": "bace81e70eaa42adbf50702c166dcd65",
3737
"reflex/components/el/elements/other.pyi": "a6615c3b7373d57f3bf5bf52fd96410f",
3838
"reflex/components/el/elements/scripts.pyi": "944fbe108254498fd34d9fbf744fc965",
3939
"reflex/components/el/elements/sectioning.pyi": "b47a74c17a5c55bf410c296d0b708b84",
4040
"reflex/components/el/elements/tables.pyi": "e3f299e59bb8ff87aa949c6b606551c9",
4141
"reflex/components/el/elements/typography.pyi": "b4ec4ffb448f7a9b5f94712404448d9e",
4242
"reflex/components/gridjs/datatable.pyi": "98a7e1b3f3b60cafcdfcd8879750ee42",
43-
"reflex/components/lucide/icon.pyi": "c1090e2c68a5d7653584dd74e8b8f1cf",
43+
"reflex/components/lucide/icon.pyi": "eb7929c0d708ddbbe8ed4849cd4aa5ce",
4444
"reflex/components/markdown/markdown.pyi": "2f84254a548e908020949564fc289339",
4545
"reflex/components/moment/moment.pyi": "93fa4c6009390fe9400197ab52735b84",
4646
"reflex/components/plotly/plotly.pyi": "4311a0aae2abcc9226abb6a273f96372",

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "reflex"
3-
version = "0.8.5dev1"
3+
version = "0.8.6dev1"
44
description = "Web apps in pure Python."
55
license.text = "Apache-2.0"
66
authors = [
@@ -227,7 +227,7 @@ fail_fast = true
227227

228228
[[tool.pre-commit.repos]]
229229
repo = "https://github.com/astral-sh/ruff-pre-commit"
230-
rev = "v0.12.5"
230+
rev = "v0.12.7"
231231
hooks = [
232232
{ id = "ruff-format", args = [
233233
"reflex",

reflex/.templates/web/vite.config.js renamed to reflex/.templates/jinja/web/vite.config.js.jinja2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function alwaysUseReactDomServerNode() {
2525
}
2626

2727
export default defineConfig((config) => ({
28+
base: "{{base}}",
2829
plugins: [
2930
alwaysUseReactDomServerNode(),
3031
reactRouter(),

reflex/.templates/web/utils/state.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ export const connect = async (
530530
transports: transports,
531531
protocols: [reflexEnvironment.version],
532532
autoUnref: false,
533+
query: { token: getToken() },
533534
});
534535
// Ensure undefined fields in events are sent as null instead of removed
535536
socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v);
@@ -601,6 +602,10 @@ export const connect = async (
601602
event_processing = false;
602603
queueEvents([...initialEvents(), event], socket, true, navigate, params);
603604
});
605+
socket.current.on("new_token", async (new_token) => {
606+
token = new_token;
607+
window.sessionStorage.setItem(TOKEN_KEY, new_token);
608+
});
604609

605610
document.addEventListener("visibilitychange", checkVisibility);
606611
};

reflex/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@
285285
"data_editor_theme",
286286
],
287287
"components.sonner.toast": ["toast"],
288+
"components.props": ["PropsBase"],
288289
"components.datadisplay.logo": ["logo"],
289290
"components.gridjs": ["data_table"],
290291
"components.moment": ["MomentDelta", "moment"],

reflex/app.py

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import json
1414
import sys
1515
import traceback
16+
import urllib.parse
1617
from collections.abc import (
1718
AsyncGenerator,
1819
AsyncIterator,
@@ -114,6 +115,7 @@
114115
)
115116
from reflex.utils.exec import get_compile_context, is_prod_mode, is_testing_env
116117
from reflex.utils.imports import ImportVar
118+
from reflex.utils.token_manager import TokenManager
117119
from reflex.utils.types import ASGIApp, Message, Receive, Scope, Send
118120

119121
if TYPE_CHECKING:
@@ -1958,12 +1960,6 @@ class EventNamespace(AsyncNamespace):
19581960
# The application object.
19591961
app: App
19601962

1961-
# Keep a mapping between socket ID and client token.
1962-
token_to_sid: dict[str, str]
1963-
1964-
# Keep a mapping between client token and socket ID.
1965-
sid_to_token: dict[str, str]
1966-
19671963
def __init__(self, namespace: str, app: App):
19681964
"""Initialize the event namespace.
19691965
@@ -1972,17 +1968,45 @@ def __init__(self, namespace: str, app: App):
19721968
app: The application object.
19731969
"""
19741970
super().__init__(namespace)
1975-
self.token_to_sid = {}
1976-
self.sid_to_token = {}
19771971
self.app = app
19781972

1979-
def on_connect(self, sid: str, environ: dict):
1973+
# Use TokenManager for distributed duplicate tab prevention
1974+
self._token_manager = TokenManager.create()
1975+
1976+
@property
1977+
def token_to_sid(self) -> dict[str, str]:
1978+
"""Get token to SID mapping for backward compatibility.
1979+
1980+
Returns:
1981+
The token to SID mapping dict.
1982+
"""
1983+
# For backward compatibility, expose the underlying dict
1984+
return self._token_manager.token_to_sid
1985+
1986+
@property
1987+
def sid_to_token(self) -> dict[str, str]:
1988+
"""Get SID to token mapping for backward compatibility.
1989+
1990+
Returns:
1991+
The SID to token mapping dict.
1992+
"""
1993+
# For backward compatibility, expose the underlying dict
1994+
return self._token_manager.sid_to_token
1995+
1996+
async def on_connect(self, sid: str, environ: dict):
19801997
"""Event for when the websocket is connected.
19811998
19821999
Args:
19832000
sid: The Socket.IO session id.
19842001
environ: The request information, including HTTP headers.
19852002
"""
2003+
query_params = urllib.parse.parse_qs(environ.get("QUERY_STRING", ""))
2004+
token_list = query_params.get("token", [])
2005+
if token_list:
2006+
await self.link_token_to_sid(sid, token_list[0])
2007+
else:
2008+
console.warn(f"No token provided in connection for session {sid}")
2009+
19862010
subprotocol = environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL")
19872011
if subprotocol and subprotocol != constants.Reflex.VERSION:
19882012
console.warn(
@@ -1995,9 +2019,18 @@ def on_disconnect(self, sid: str):
19952019
Args:
19962020
sid: The Socket.IO session id.
19972021
"""
1998-
disconnect_token = self.sid_to_token.pop(sid, None)
2022+
# Get token before cleaning up
2023+
disconnect_token = self.sid_to_token.get(sid)
19992024
if disconnect_token:
2000-
self.token_to_sid.pop(disconnect_token, None)
2025+
# Use async cleanup through token manager
2026+
task = asyncio.create_task(
2027+
self._token_manager.disconnect_token(disconnect_token, sid)
2028+
)
2029+
# Don't await to avoid blocking disconnect, but handle potential errors
2030+
task.add_done_callback(
2031+
lambda t: t.exception()
2032+
and console.error(f"Token cleanup error: {t.exception()}")
2033+
)
20012034

20022035
async def emit_update(self, update: StateUpdate, sid: str) -> None:
20032036
"""Emit an update to the client.
@@ -2055,8 +2088,13 @@ async def on_event(self, sid: str, data: Any):
20552088
msg = f"Failed to deserialize event data: {fields}."
20562089
raise exceptions.EventDeserializationError(msg) from ex
20572090

2058-
self.token_to_sid[event.token] = sid
2059-
self.sid_to_token[sid] = event.token
2091+
# Correct the token if it doesn't match what we expect for this SID
2092+
expected_token = self.sid_to_token.get(sid)
2093+
if expected_token and event.token != expected_token:
2094+
# Create new event with corrected token since Event is frozen
2095+
from dataclasses import replace
2096+
2097+
event = replace(event, token=expected_token)
20602098

20612099
# Get the event environment.
20622100
if self.app.sio is None:
@@ -2106,3 +2144,17 @@ async def on_ping(self, sid: str):
21062144
"""
21072145
# Emit the test event.
21082146
await self.emit(str(constants.SocketEvent.PING), "pong", to=sid)
2147+
2148+
async def link_token_to_sid(self, sid: str, token: str):
2149+
"""Link a token to a session id.
2150+
2151+
Args:
2152+
sid: The Socket.IO session id.
2153+
token: The client token.
2154+
"""
2155+
# Use TokenManager for duplicate detection and Redis support
2156+
new_token = await self._token_manager.link_token_to_sid(token, sid)
2157+
2158+
if new_token:
2159+
# Duplicate detected, emit new token to client
2160+
await self.emit("new_token", new_token, to=sid)

reflex/compiler/templates.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ def from_string(source: str) -> Template:
149149
# Code that generate the package json file
150150
PACKAGE_JSON = get_template("web/package.json.jinja2")
151151

152+
# Code that generate the vite.config.js file
153+
VITE_CONFIG = get_template("web/vite.config.js.jinja2")
154+
152155
# Template containing some macros used in the web pages.
153156
MACROS = get_template("web/pages/macros.js.jinja2")
154157

reflex/components/datadisplay/dataeditor.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ class DataEditor(NoSSRComponent):
197197
# Enables or disables the overlay shadow when scrolling vertically.
198198
fixed_shadow_y: Var[bool]
199199

200+
# Controls the presence of the fill indicator
201+
fill_handle: Var[bool]
202+
200203
# The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers.
201204
freeze_columns: Var[int]
202205

reflex/components/el/elements/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"linear_gradient",
7979
"radial_gradient",
8080
"defs",
81+
"marker",
8182
],
8283
"metadata": [
8384
"base",

0 commit comments

Comments
 (0)