Skip to content

Commit 4598bbd

Browse files
committed
Merge remote-tracking branch 'origin/main' into release/reflex-0.8.15
2 parents 8dd49c6 + ade1254 commit 4598bbd

File tree

4 files changed

+111
-15
lines changed

4 files changed

+111
-15
lines changed

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

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,14 @@ export const connect = async (
529529
navigate,
530530
params,
531531
) => {
532+
// Socket already allocated, just reconnect it if needed.
533+
if (socket.current) {
534+
if (!socket.current.connected) {
535+
socket.current.reconnect();
536+
}
537+
return;
538+
}
539+
532540
// Get backend URL object from the endpoint.
533541
const endpoint = getBackendURL(EVENTURL);
534542
const on_hydrated_queue = [];
@@ -540,7 +548,9 @@ export const connect = async (
540548
protocols: [reflexEnvironment.version],
541549
autoUnref: false,
542550
query: { token: getToken() },
551+
reconnection: false, // Reconnection will be handled manually.
543552
});
553+
socket.current.wait_connect = !socket.current.connected;
544554
// Ensure undefined fields in events are sent as null instead of removed
545555
socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v);
546556
socket.current.io.decoder.tryParse = (str) => {
@@ -550,6 +560,18 @@ export const connect = async (
550560
return false;
551561
}
552562
};
563+
// Set up a reconnect helper function
564+
socket.current.reconnect = () => {
565+
if (
566+
socket.current &&
567+
!socket.current.connected &&
568+
!socket.current.wait_connect
569+
) {
570+
socket.current.wait_connect = true;
571+
socket.current.io.opts.query = { token: getToken() }; // Update token for reconnect.
572+
socket.current.connect();
573+
}
574+
};
553575

554576
function checkVisibility() {
555577
if (document.visibilityState === "visible") {
@@ -565,7 +587,7 @@ export const connect = async (
565587
);
566588
} else if (!socket.current.connected) {
567589
console.log("Socket is disconnected, attempting to reconnect ");
568-
socket.current.connect();
590+
socket.current.reconnect();
569591
} else {
570592
console.log("Socket is reconnected ");
571593
}
@@ -588,6 +610,7 @@ export const connect = async (
588610

589611
// Once the socket is open, hydrate the page.
590612
socket.current.on("connect", async () => {
613+
socket.current.wait_connect = false;
591614
setConnectErrors([]);
592615
window.addEventListener("pagehide", pagehideHandler);
593616
window.addEventListener("beforeunload", disconnectTrigger);
@@ -599,16 +622,33 @@ export const connect = async (
599622
});
600623

601624
socket.current.on("connect_error", (error) => {
602-
setConnectErrors((connectErrors) => [connectErrors.slice(-9), error]);
625+
socket.current.wait_connect = false;
626+
let n_connect_errors = 0;
627+
setConnectErrors((connectErrors) => {
628+
const new_errors = [...connectErrors.slice(-9), error];
629+
n_connect_errors = new_errors.length;
630+
return new_errors;
631+
});
632+
window.setTimeout(() => {
633+
if (socket.current && !socket.current.connected) {
634+
socket.current.reconnect();
635+
}
636+
}, 200 * n_connect_errors); // Incremental backoff
603637
});
604638

605639
// When the socket disconnects reset the event_processing flag
606-
socket.current.on("disconnect", () => {
607-
socket.current = null; // allow reconnect to occur automatically
640+
socket.current.on("disconnect", (reason, details) => {
641+
socket.current.wait_connect = false;
642+
const try_reconnect =
643+
reason !== "io server disconnect" && reason !== "io client disconnect";
608644
event_processing = false;
609645
window.removeEventListener("unload", disconnectTrigger);
610646
window.removeEventListener("beforeunload", disconnectTrigger);
611647
window.removeEventListener("pagehide", pagehideHandler);
648+
if (try_reconnect) {
649+
// Attempt to reconnect transient non-intentional disconnects.
650+
socket.current.reconnect();
651+
}
612652
});
613653

614654
// On each received message, queue the updates and events.
@@ -785,6 +825,7 @@ export const useEventLoop = (
785825
const [searchParams] = useSearchParams();
786826
const [connectErrors, setConnectErrors] = useState([]);
787827
const params = useRef(paramsR);
828+
const mounted = useRef(false);
788829

789830
useEffect(() => {
790831
const { "*": splat, ...remainingParams } = paramsR;
@@ -796,11 +837,16 @@ export const useEventLoop = (
796837
}, [paramsR]);
797838

798839
const ensureSocketConnected = useCallback(async () => {
840+
if (!mounted.current) {
841+
// During hot reload, some components may still have a reference to
842+
// addEvents, so avoid reconnecting the socket of an unmounted event loop.
843+
return;
844+
}
799845
// only use websockets if state is present and backend is not disabled (reflex cloud).
800846
if (
801847
Object.keys(initialState).length > 1 &&
802848
!isBackendDisabled() &&
803-
!socket.current
849+
!socket.current?.connected
804850
) {
805851
// Initialize the websocket connection.
806852
await connect(
@@ -813,13 +859,23 @@ export const useEventLoop = (
813859
() => params.current,
814860
);
815861
}
816-
}, [socket, dispatch, setConnectErrors, client_storage, navigate, params]);
862+
}, [
863+
socket,
864+
dispatch,
865+
setConnectErrors,
866+
client_storage,
867+
navigate,
868+
params,
869+
mounted,
870+
]);
817871

818872
// Function to add new events to the event queue.
819873
const addEvents = useCallback((events, args, event_actions) => {
820874
const _events = events.filter((e) => e !== undefined && e !== null);
821-
822-
ensureSocketConnected();
875+
if (!event_actions?.temporal) {
876+
// Reconnect socket if needed for non-temporal events.
877+
ensureSocketConnected();
878+
}
823879

824880
if (!(args instanceof Array)) {
825881
args = [args];
@@ -914,12 +970,16 @@ export const useEventLoop = (
914970
// Handle socket connect/disconnect.
915971
useEffect(() => {
916972
// Initialize the websocket connection.
973+
mounted.current = true;
917974
ensureSocketConnected();
918975

919976
// Cleanup function.
920977
return () => {
978+
mounted.current = false;
921979
if (socket.current) {
922980
socket.current.disconnect();
981+
socket.current.off();
982+
socket.current = null;
923983
}
924984
};
925985
}, []);

reflex/app.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1781,16 +1781,16 @@ async def process(
17811781
name=f"reflex_emit_reload|{event.name}|{time.time()}|{event.token}",
17821782
)
17831783
return
1784+
router_data[constants.RouteVar.PATH] = "/" + (
1785+
app.router(path) or "404"
1786+
if (path := router_data.get(constants.RouteVar.PATH))
1787+
else "404"
1788+
).removeprefix("/")
17841789
# re-assign only when the value is different
17851790
if state.router_data != router_data:
17861791
# assignment will recurse into substates and force recalculation of
17871792
# dependent ComputedVar (dynamic route variables)
17881793
state.router_data = router_data
1789-
router_data[constants.RouteVar.PATH] = "/" + (
1790-
app.router(path) or "404"
1791-
if (path := router_data.get(constants.RouteVar.PATH))
1792-
else "404"
1793-
).removeprefix("/")
17941794
state.router = RouterData.from_router_data(router_data)
17951795

17961796
# Preprocess the event.

reflex/utils/codespaces.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,40 @@ def redirect_script() -> str:
2626
const thisUrl = new URL(window.location.href);
2727
const params = new URLSearchParams(thisUrl.search)
2828
29+
function sameHostnameDifferentPort(one, two) {{
30+
const hostnameOne = one.hostname;
31+
const hostnameTwo = two.hostname;
32+
const partsOne = hostnameOne.split(".");
33+
const partsTwo = hostnameTwo.split(".");
34+
if (partsOne.length !== partsTwo.length) {{ return false; }}
35+
for (let i = 1; i < partsOne.length; i++) {{
36+
if (partsOne[i] !== partsTwo[i]) {{ return false; }}
37+
}}
38+
const uniqueNameOne = partsOne[0];
39+
const uniqueNameTwo = partsTwo[0];
40+
const uniqueNamePartsOne = uniqueNameOne.split("-");
41+
const uniqueNamePartsTwo = uniqueNameTwo.split("-");
42+
if (uniqueNamePartsOne.length !== uniqueNamePartsTwo.length) {{ return false; }}
43+
for (let i = 0; i < uniqueNamePartsOne.length - 1; i++) {{
44+
if (uniqueNamePartsOne[i] !== uniqueNamePartsTwo[i]) {{ return false; }}
45+
}}
46+
return true;
47+
}}
48+
2949
function doRedirect(url) {{
3050
if (!window.sessionStorage.getItem("authenticated_github_codespaces")) {{
3151
const a = document.createElement("a");
3252
if (params.has("redirect_to")) {{
33-
a.href = params.get("redirect_to")
53+
const redirect_to = new URL(params.get("redirect_to"));
54+
if (!sameHostnameDifferentPort(thisUrl, redirect_to)) {{
55+
console.warn("Reflex: Not redirecting to different hostname");
56+
return;
57+
}}
58+
if (!redirect_to.hostname.endsWith(".app.github.dev")) {{
59+
console.warn("Reflex: Not redirecting to non .app.github.dev hostname");
60+
return;
61+
}}
62+
a.href = redirect_to.href;
3463
}} else if (!window.location.href.startsWith(url)) {{
3564
a.href = url + `?redirect_to=${{window.location.href}}`
3665
}} else {{

reflex/utils/types.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,13 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
443443

444444
from reflex.model import Model
445445

446+
if find_spec("sqlmodel"):
447+
from sqlmodel import SQLModel
448+
449+
sqlmodel_types = (Model, SQLModel)
450+
else:
451+
sqlmodel_types = (Model,)
452+
446453
if isinstance(cls, type) and issubclass(cls, DeclarativeBase):
447454
insp = sqlalchemy.inspect(cls)
448455
if name in insp.columns:
@@ -486,7 +493,7 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
486493
elif (
487494
isinstance(cls, type)
488495
and not is_generic_alias(cls)
489-
and issubclass(cls, Model)
496+
and issubclass(cls, sqlmodel_types)
490497
):
491498
# Check in the annotations directly (for sqlmodel.Relationship)
492499
hints = get_type_hints(cls) # pyright: ignore [reportArgumentType]

0 commit comments

Comments
 (0)