Skip to content

Commit b7958c9

Browse files
authored
ENG-7948: Fix hanging frontend and other reconnection woes (#5884)
* ENG-7948: Fix hanging frontend and other reconnection woes * Disable socketio automatic reconnection logic -- replace with manual reconnect logic * Try to keep the socketio socket allocated, even when it gets disconnected for better resource management. * Introduce new `socket.current.reconnect()` helper * Update query param token when reconnecting * Only allow one concurrent connection attempt * ensureSocketConnected only reconnects if the event loop is mounted -- this avoids issues during hot reload where some Event-emitting components may still have a reference to the _old_ `addEvents` and `ensureSocketConnected` was reconnecting the old socket. * Use `.off` and null the `socket` ref when cleaning up to free memory and avoid unwanted reconnections when the socket will be recreated. Also addresses the following ticket: ENG-7762: [reconnect] client is given new_token when wifi disconnects and reconnects This issue was the result of automatic reconnect logic and manual reconnect logic conflicting with each other and opening two sockets with the same token, which reflex reassigns the latter to a new token, and this is typically the socket that gets used in the app. * useRef for `mounted` to play nice in the react world
1 parent 8a2337b commit b7958c9

File tree

1 file changed

+68
-8
lines changed

1 file changed

+68
-8
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
}, []);

0 commit comments

Comments
 (0)