fix: enhance logic

This commit is contained in:
yassinedorbozgithub 2025-01-30 08:23:56 +01:00
parent b5d72761e4
commit 9cc2725674
7 changed files with 163 additions and 174 deletions

View File

@ -101,21 +101,18 @@ export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => {
setUser(user); setUser(user);
}; };
const isAuthenticated = !!user; const isAuthenticated = !!user;
const { subscribe } = useBroadcastChannel();
useEffect(() => { useEffect(() => {
const search = location.search; const search = location.search;
setSearch(search); setSearch(search);
setIsReady(true); setIsReady(true);
}, []);
const { subscribe } = useBroadcastChannel(); subscribe("logout", () => {
subscribe((message) => {
if (message.data === "logout") {
router.reload(); router.reload();
}
}); });
}, []);
if (!isReady || isLoading) return <Progress />; if (!isReady || isLoading) return <Progress />;

View File

@ -12,98 +12,123 @@ import {
ReactNode, ReactNode,
useContext, useContext,
useEffect, useEffect,
useMemo,
useRef, useRef,
} from "react"; } from "react";
import { useTabUuid } from "@/hooks/useTabUuid"; import { generateId } from "@/utils/generateId";
export enum EBCEvent {
LOGOUT = "logout",
}
type BroadcastChannelPayload = {
event: `${EBCEvent}`;
data?: string | number | boolean | Record<string, unknown> | undefined | null;
};
export type BroadcastChannelData = { export type BroadcastChannelData = {
tabId: string; tabId: string;
data: string | number | boolean | Record<string, unknown> | undefined | null; payload: BroadcastChannelPayload;
}; };
export interface IBroadcastChannelProps { export interface IBroadcastChannelProps {
channelName: string; channelName: string;
children?: ReactNode; children: ReactNode;
} }
export const BroadcastChannelContext = createContext<{ const getOrCreateTabId = () => {
subscribers: ((message: BroadcastChannelData) => void)[]; let storedTabId = sessionStorage.getItem("tab_uuid");
subscribe: (callback: (message: BroadcastChannelData) => void) => () => void;
postMessage: (message: BroadcastChannelData) => void; if (storedTabId) {
}>({ return storedTabId;
subscribers: [], }
storedTabId = generateId();
sessionStorage.setItem("tab_uuid", storedTabId);
return storedTabId;
};
interface IBroadcastChannelContext {
subscribe: (
event: `${EBCEvent}`,
callback: (message: BroadcastChannelData) => void,
) => () => void;
postMessage: (payload: BroadcastChannelPayload) => void;
}
export const BroadcastChannelContext = createContext<IBroadcastChannelContext>({
subscribe: () => () => {}, subscribe: () => () => {},
postMessage: () => {}, postMessage: () => {},
}); });
export const BroadcastChannelProvider: FC<IBroadcastChannelProps> = ({ export const BroadcastChannelProvider: FC<IBroadcastChannelProps> = ({
channelName,
children, children,
channelName,
}) => { }) => {
const channelRef = useRef<BroadcastChannel | null>(null); const channelRef = useRef<BroadcastChannel>(
const subscribersRef = useRef<Array<(message: BroadcastChannelData) => void>>( new BroadcastChannel(channelName),
[],
); );
const tabUuidRef = useTabUuid(); const subscribersRef = useRef<
Record<string, Array<(message: BroadcastChannelData) => void>>
>({});
const tabUuid = getOrCreateTabId();
useEffect(() => { useEffect(() => {
const channel = new BroadcastChannel(channelName); const handleMessage = ({ data }: MessageEvent<BroadcastChannelData>) => {
const { tabId, payload } = data;
channelRef.current = channel; if (tabId === tabUuid) {
return;
}
const handleMessage = (event: MessageEvent) => { subscribersRef.current[payload.event]?.forEach((callback) =>
const { tabId, data } = event.data; callback(data),
);
if (tabId === tabUuidRef.current) return;
subscribersRef.current.forEach((callback) => callback(data));
}; };
channel.addEventListener("message", handleMessage); channelRef.current.addEventListener("message", handleMessage);
return () => { return () => {
channel.removeEventListener("message", handleMessage); channelRef.current.removeEventListener("message", handleMessage);
channel.close(); channelRef.current.close();
}; };
}, [channelName, tabUuidRef]); }, []);
const postMessage = (message: BroadcastChannelData) => { const subscribe: IBroadcastChannelContext["subscribe"] = (
channelRef.current?.postMessage({ event,
tabId: tabUuidRef.current, callback,
data: message, ) => {
}); subscribersRef.current[event] ??= [];
}; subscribersRef.current[event]?.push(callback);
const subscribe = (callback: (message: BroadcastChannelData) => void) => {
subscribersRef.current.push(callback);
return () => { return () => {
const index = subscribersRef.current.indexOf(callback); const index = subscribersRef.current[event]?.indexOf(callback) ?? -1;
if (index !== -1) { if (index !== -1) {
subscribersRef.current.splice(index, 1); subscribersRef.current[event]?.splice(index, 1);
} }
}; };
}; };
const contextValue = useMemo( const postMessage: IBroadcastChannelContext["postMessage"] = (payload) => {
() => ({ channelRef.current?.postMessage({
subscribers: subscribersRef.current, tabId: tabUuid,
subscribe, payload,
postMessage, });
}), };
[],
);
return ( return (
<BroadcastChannelContext.Provider value={contextValue}> <BroadcastChannelContext.Provider
value={{
subscribe,
postMessage,
}}
>
{children} {children}
</BroadcastChannelContext.Provider> </BroadcastChannelContext.Provider>
); );
}; };
export default BroadcastChannelProvider;
export const useBroadcastChannel = () => { export const useBroadcastChannel = () => {
const context = useContext(BroadcastChannelContext); const context = useContext(BroadcastChannelContext);
@ -115,3 +140,5 @@ export const useBroadcastChannel = () => {
return context; return context;
}; };
export default BroadcastChannelProvider;

View File

@ -23,7 +23,6 @@ import { useSocket } from "@/websocket/socket-hooks";
import { useFind } from "../crud/useFind"; import { useFind } from "../crud/useFind";
import { useApiClient } from "../useApiClient"; import { useApiClient } from "../useApiClient";
import { CURRENT_USER_KEY, useAuth, useLogoutRedirection } from "../useAuth"; import { CURRENT_USER_KEY, useAuth, useLogoutRedirection } from "../useAuth";
import { useTabUuid } from "../useTabUuid";
import { useToast } from "../useToast"; import { useToast } from "../useToast";
import { useTranslate } from "../useTranslate"; import { useTranslate } from "../useTranslate";
@ -61,7 +60,6 @@ export const useLogout = (
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslate(); const { t } = useTranslate();
const { postMessage } = useBroadcastChannel(); const { postMessage } = useBroadcastChannel();
const uuid = useTabUuid();
return useMutation({ return useMutation({
...options, ...options,
@ -72,7 +70,7 @@ export const useLogout = (
}, },
onSuccess: async () => { onSuccess: async () => {
queryClient.removeQueries([CURRENT_USER_KEY]); queryClient.removeQueries([CURRENT_USER_KEY]);
postMessage({ data: "logout", tabId: uuid.current || "" }); postMessage({ event: "logout" });
await logoutRedirection(); await logoutRedirection();
toast.success(t("message.logout_success")); toast.success(t("message.logout_success"));
}, },

View File

@ -1,30 +0,0 @@
/*
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { useRef } from "react";
import { generateId } from "@/utils/generateId";
const getOrCreateTabId = () => {
let storedTabId = sessionStorage.getItem("tab_uuid");
if (storedTabId) {
return storedTabId;
}
storedTabId = generateId();
sessionStorage.setItem("tab_uuid", storedTabId);
return storedTabId;
};
export const useTabUuid = () => {
const tabUuidRef = useRef<string>(getOrCreateTabId());
return tabUuidRef;
};

View File

@ -1,30 +0,0 @@
/*
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { useRef } from "react";
import { generateId } from "../utils/generateId";
const getOrCreateTabId = () => {
let storedTabId = sessionStorage.getItem("tab_uuid");
if (storedTabId) {
return storedTabId;
}
storedTabId = generateId();
sessionStorage.setItem("tab_uuid", storedTabId);
return storedTabId;
};
export const useTabUuid = () => {
const tabUuidRef = useRef<string>(getOrCreateTabId());
return tabUuidRef;
};

View File

@ -12,99 +12,125 @@ import {
ReactNode, ReactNode,
useContext, useContext,
useEffect, useEffect,
useMemo,
useRef, useRef,
} from "react"; } from "react";
import { useTabUuid } from "../hooks/useTabUuid"; import { generateId } from "../utils/generateId";
export enum EBCEvent {
LOGOUT = "logout",
}
type BroadcastChannelPayload = {
event: `${EBCEvent}`;
data?: string | number | boolean | Record<string, unknown> | undefined | null;
};
export type BroadcastChannelData = { export type BroadcastChannelData = {
tabId: string; tabId: string;
data: string | number | boolean | Record<string, unknown> | undefined | null; payload: BroadcastChannelPayload;
}; };
export interface IBroadcastChannelProps { export interface IBroadcastChannelProps {
channelName: string; channelName: string;
children?: ReactNode; children: ReactNode;
} }
export const BroadcastChannelContext = createContext<{ const getOrCreateTabId = () => {
subscribers: ((message: BroadcastChannelData) => void)[]; let storedTabId = sessionStorage.getItem("tab_uuid");
subscribe: (callback: (message: BroadcastChannelData) => void) => () => void;
postMessage: (message: BroadcastChannelData) => void; if (storedTabId) {
}>({ return storedTabId;
subscribers: [], }
storedTabId = generateId();
sessionStorage.setItem("tab_uuid", storedTabId);
return storedTabId;
};
interface IBroadcastChannelContext {
subscribe: (
event: `${EBCEvent}`,
callback: (message: BroadcastChannelData) => void,
) => () => void;
postMessage: (payload: BroadcastChannelPayload) => void;
}
export const BroadcastChannelContext = createContext<IBroadcastChannelContext>({
subscribe: () => () => {}, subscribe: () => () => {},
postMessage: () => {}, postMessage: () => {},
}); });
export const BroadcastChannelProvider: FC<IBroadcastChannelProps> = ({ export const BroadcastChannelProvider: FC<IBroadcastChannelProps> = ({
channelName,
children, children,
channelName,
}) => { }) => {
const channelRef = useRef<BroadcastChannel | null>(null); const channelRef = useRef<BroadcastChannel>(
const subscribersRef = useRef<Array<(message: BroadcastChannelData) => void>>( new BroadcastChannel(channelName),
[],
); );
const tabUuidRef = useTabUuid(); const subscribersRef = useRef<
Record<string, Array<(message: BroadcastChannelData) => void>>
>({});
const tabUuid = getOrCreateTabId();
useEffect(() => { useEffect(() => {
const channel = new BroadcastChannel(channelName); const handleMessage = ({ data }: MessageEvent<BroadcastChannelData>) => {
const { tabId, payload } = data;
channelRef.current = channel; if (tabId === tabUuid) {
return;
}
const handleMessage = (event: MessageEvent) => { subscribersRef.current[payload.event]?.forEach((callback) =>
const { tabId, data } = event.data; callback(data),
);
if (tabId === tabUuidRef.current) return;
subscribersRef.current.forEach((callback) => callback(data));
}; };
channel.addEventListener("message", handleMessage); channelRef.current.addEventListener("message", handleMessage);
return () => { return () => {
channel.removeEventListener("message", handleMessage); channelRef.current.removeEventListener("message", handleMessage);
channel.close(); // eslint-disable-next-line react-hooks/exhaustive-deps
channelRef.current.close();
}; };
}, [channelName, tabUuidRef]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const postMessage = (message: BroadcastChannelData) => { const subscribe: IBroadcastChannelContext["subscribe"] = (
channelRef.current?.postMessage({ event,
tabId: tabUuidRef.current, callback,
data: message, ) => {
}); subscribersRef.current[event] ??= [];
}; subscribersRef.current[event]?.push(callback);
const subscribe = (callback: (message: BroadcastChannelData) => void) => {
subscribersRef.current.push(callback);
return () => { return () => {
const index = subscribersRef.current.indexOf(callback); const index = subscribersRef.current[event]?.indexOf(callback) ?? -1;
if (index !== -1) { if (index !== -1) {
subscribersRef.current.splice(index, 1); subscribersRef.current[event]?.splice(index, 1);
} }
}; };
}; };
const contextValue = useMemo( const postMessage: IBroadcastChannelContext["postMessage"] = (payload) => {
() => ({ channelRef.current?.postMessage({
subscribers: subscribersRef.current, tabId: tabUuid,
subscribe, payload,
postMessage, });
}), };
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
return ( return (
<BroadcastChannelContext.Provider value={contextValue}> <BroadcastChannelContext.Provider
value={{
subscribe,
postMessage,
}}
>
{children} {children}
</BroadcastChannelContext.Provider> </BroadcastChannelContext.Provider>
); );
}; };
export default BroadcastChannelProvider;
export const useBroadcastChannel = () => { export const useBroadcastChannel = () => {
const context = useContext(BroadcastChannelContext); const context = useContext(BroadcastChannelContext);
@ -116,3 +142,5 @@ export const useBroadcastChannel = () => {
return context; return context;
}; };
export default BroadcastChannelProvider;

View File

@ -385,6 +385,8 @@ const ChatProvider: React.FC<{
} }
}, [syncState, isOpen]); }, [syncState, isOpen]);
const { subscribe } = useBroadcastChannel();
useEffect(() => { useEffect(() => {
if (screen === "chat" && connectionState === ConnectionState.connected) { if (screen === "chat" && connectionState === ConnectionState.connected) {
handleSubscription(); handleSubscription();
@ -404,6 +406,10 @@ const ChatProvider: React.FC<{
socketCtx.socket.io.on("reconnect", reSubscribe); socketCtx.socket.io.on("reconnect", reSubscribe);
subscribe("logout", () => {
socketCtx.socket.disconnect();
});
return () => { return () => {
socketCtx.socket.io.off("reconnect", reSubscribe); socketCtx.socket.io.off("reconnect", reSubscribe);
}; };
@ -452,13 +458,6 @@ const ChatProvider: React.FC<{
setMessage, setMessage,
handleSubscription, handleSubscription,
}; };
const { subscribe } = useBroadcastChannel();
subscribe(({ data }) => {
if (data === "logout") {
socketCtx.socket.disconnect();
}
});
return ( return (
<ChatContext.Provider value={contextValue}>{children}</ChatContext.Provider> <ChatContext.Provider value={contextValue}>{children}</ChatContext.Provider>