diff --git a/frontend/src/contexts/auth.context.tsx b/frontend/src/contexts/auth.context.tsx
index eca0700f..030c4269 100644
--- a/frontend/src/contexts/auth.context.tsx
+++ b/frontend/src/contexts/auth.context.tsx
@@ -101,21 +101,18 @@ export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => {
setUser(user);
};
const isAuthenticated = !!user;
+ const { subscribe } = useBroadcastChannel();
useEffect(() => {
const search = location.search;
setSearch(search);
setIsReady(true);
- }, []);
- const { subscribe } = useBroadcastChannel();
-
- subscribe((message) => {
- if (message.data === "logout") {
+ subscribe("logout", () => {
router.reload();
- }
- });
+ });
+ }, []);
if (!isReady || isLoading) return ;
diff --git a/frontend/src/contexts/broadcast-channel.context.tsx b/frontend/src/contexts/broadcast-channel.context.tsx
index e6b8e588..5363ab18 100644
--- a/frontend/src/contexts/broadcast-channel.context.tsx
+++ b/frontend/src/contexts/broadcast-channel.context.tsx
@@ -12,98 +12,123 @@ import {
ReactNode,
useContext,
useEffect,
- useMemo,
useRef,
} 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 | undefined | null;
+};
export type BroadcastChannelData = {
tabId: string;
- data: string | number | boolean | Record | undefined | null;
+ payload: BroadcastChannelPayload;
};
export interface IBroadcastChannelProps {
channelName: string;
- children?: ReactNode;
+ children: ReactNode;
}
-export const BroadcastChannelContext = createContext<{
- subscribers: ((message: BroadcastChannelData) => void)[];
- subscribe: (callback: (message: BroadcastChannelData) => void) => () => void;
- postMessage: (message: BroadcastChannelData) => void;
-}>({
- subscribers: [],
+const getOrCreateTabId = () => {
+ let storedTabId = sessionStorage.getItem("tab_uuid");
+
+ if (storedTabId) {
+ return storedTabId;
+ }
+
+ 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({
subscribe: () => () => {},
postMessage: () => {},
});
export const BroadcastChannelProvider: FC = ({
- channelName,
children,
+ channelName,
}) => {
- const channelRef = useRef(null);
- const subscribersRef = useRef void>>(
- [],
+ const channelRef = useRef(
+ new BroadcastChannel(channelName),
);
- const tabUuidRef = useTabUuid();
+ const subscribersRef = useRef<
+ Record void>>
+ >({});
+ const tabUuid = getOrCreateTabId();
useEffect(() => {
- const channel = new BroadcastChannel(channelName);
+ const handleMessage = ({ data }: MessageEvent) => {
+ const { tabId, payload } = data;
- channelRef.current = channel;
+ if (tabId === tabUuid) {
+ return;
+ }
- const handleMessage = (event: MessageEvent) => {
- const { tabId, data } = event.data;
-
- if (tabId === tabUuidRef.current) return;
-
- subscribersRef.current.forEach((callback) => callback(data));
+ subscribersRef.current[payload.event]?.forEach((callback) =>
+ callback(data),
+ );
};
- channel.addEventListener("message", handleMessage);
+ channelRef.current.addEventListener("message", handleMessage);
return () => {
- channel.removeEventListener("message", handleMessage);
- channel.close();
+ channelRef.current.removeEventListener("message", handleMessage);
+ channelRef.current.close();
};
- }, [channelName, tabUuidRef]);
+ }, []);
- const postMessage = (message: BroadcastChannelData) => {
- channelRef.current?.postMessage({
- tabId: tabUuidRef.current,
- data: message,
- });
- };
- const subscribe = (callback: (message: BroadcastChannelData) => void) => {
- subscribersRef.current.push(callback);
+ const subscribe: IBroadcastChannelContext["subscribe"] = (
+ event,
+ callback,
+ ) => {
+ subscribersRef.current[event] ??= [];
+ subscribersRef.current[event]?.push(callback);
return () => {
- const index = subscribersRef.current.indexOf(callback);
+ const index = subscribersRef.current[event]?.indexOf(callback) ?? -1;
if (index !== -1) {
- subscribersRef.current.splice(index, 1);
+ subscribersRef.current[event]?.splice(index, 1);
}
};
};
- const contextValue = useMemo(
- () => ({
- subscribers: subscribersRef.current,
- subscribe,
- postMessage,
- }),
- [],
- );
+ const postMessage: IBroadcastChannelContext["postMessage"] = (payload) => {
+ channelRef.current?.postMessage({
+ tabId: tabUuid,
+ payload,
+ });
+ };
return (
-
+
{children}
);
};
-export default BroadcastChannelProvider;
-
export const useBroadcastChannel = () => {
const context = useContext(BroadcastChannelContext);
@@ -115,3 +140,5 @@ export const useBroadcastChannel = () => {
return context;
};
+
+export default BroadcastChannelProvider;
diff --git a/frontend/src/hooks/entities/auth-hooks.ts b/frontend/src/hooks/entities/auth-hooks.ts
index 105718c9..e6df15b3 100755
--- a/frontend/src/hooks/entities/auth-hooks.ts
+++ b/frontend/src/hooks/entities/auth-hooks.ts
@@ -23,7 +23,6 @@ import { useSocket } from "@/websocket/socket-hooks";
import { useFind } from "../crud/useFind";
import { useApiClient } from "../useApiClient";
import { CURRENT_USER_KEY, useAuth, useLogoutRedirection } from "../useAuth";
-import { useTabUuid } from "../useTabUuid";
import { useToast } from "../useToast";
import { useTranslate } from "../useTranslate";
@@ -61,7 +60,6 @@ export const useLogout = (
const { toast } = useToast();
const { t } = useTranslate();
const { postMessage } = useBroadcastChannel();
- const uuid = useTabUuid();
return useMutation({
...options,
@@ -72,7 +70,7 @@ export const useLogout = (
},
onSuccess: async () => {
queryClient.removeQueries([CURRENT_USER_KEY]);
- postMessage({ data: "logout", tabId: uuid.current || "" });
+ postMessage({ event: "logout" });
await logoutRedirection();
toast.success(t("message.logout_success"));
},
diff --git a/frontend/src/hooks/useTabUuid.ts b/frontend/src/hooks/useTabUuid.ts
deleted file mode 100644
index 5f86619f..00000000
--- a/frontend/src/hooks/useTabUuid.ts
+++ /dev/null
@@ -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(getOrCreateTabId());
-
- return tabUuidRef;
-};
diff --git a/widget/src/hooks/useTabUuid.ts b/widget/src/hooks/useTabUuid.ts
deleted file mode 100644
index 3f0b4018..00000000
--- a/widget/src/hooks/useTabUuid.ts
+++ /dev/null
@@ -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(getOrCreateTabId());
-
- return tabUuidRef;
-};
diff --git a/widget/src/providers/BroadcastChannelProvider.tsx b/widget/src/providers/BroadcastChannelProvider.tsx
index f4e23d03..b900c1df 100644
--- a/widget/src/providers/BroadcastChannelProvider.tsx
+++ b/widget/src/providers/BroadcastChannelProvider.tsx
@@ -12,99 +12,125 @@ import {
ReactNode,
useContext,
useEffect,
- useMemo,
useRef,
} 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 | undefined | null;
+};
export type BroadcastChannelData = {
tabId: string;
- data: string | number | boolean | Record | undefined | null;
+ payload: BroadcastChannelPayload;
};
export interface IBroadcastChannelProps {
channelName: string;
- children?: ReactNode;
+ children: ReactNode;
}
-export const BroadcastChannelContext = createContext<{
- subscribers: ((message: BroadcastChannelData) => void)[];
- subscribe: (callback: (message: BroadcastChannelData) => void) => () => void;
- postMessage: (message: BroadcastChannelData) => void;
-}>({
- subscribers: [],
+const getOrCreateTabId = () => {
+ let storedTabId = sessionStorage.getItem("tab_uuid");
+
+ if (storedTabId) {
+ return storedTabId;
+ }
+
+ 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({
subscribe: () => () => {},
postMessage: () => {},
});
export const BroadcastChannelProvider: FC = ({
- channelName,
children,
+ channelName,
}) => {
- const channelRef = useRef(null);
- const subscribersRef = useRef void>>(
- [],
+ const channelRef = useRef(
+ new BroadcastChannel(channelName),
);
- const tabUuidRef = useTabUuid();
+ const subscribersRef = useRef<
+ Record void>>
+ >({});
+ const tabUuid = getOrCreateTabId();
useEffect(() => {
- const channel = new BroadcastChannel(channelName);
+ const handleMessage = ({ data }: MessageEvent) => {
+ const { tabId, payload } = data;
- channelRef.current = channel;
+ if (tabId === tabUuid) {
+ return;
+ }
- const handleMessage = (event: MessageEvent) => {
- const { tabId, data } = event.data;
-
- if (tabId === tabUuidRef.current) return;
-
- subscribersRef.current.forEach((callback) => callback(data));
+ subscribersRef.current[payload.event]?.forEach((callback) =>
+ callback(data),
+ );
};
- channel.addEventListener("message", handleMessage);
+ channelRef.current.addEventListener("message", handleMessage);
return () => {
- channel.removeEventListener("message", handleMessage);
- channel.close();
+ channelRef.current.removeEventListener("message", handleMessage);
+ // 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) => {
- channelRef.current?.postMessage({
- tabId: tabUuidRef.current,
- data: message,
- });
- };
- const subscribe = (callback: (message: BroadcastChannelData) => void) => {
- subscribersRef.current.push(callback);
+ const subscribe: IBroadcastChannelContext["subscribe"] = (
+ event,
+ callback,
+ ) => {
+ subscribersRef.current[event] ??= [];
+ subscribersRef.current[event]?.push(callback);
return () => {
- const index = subscribersRef.current.indexOf(callback);
+ const index = subscribersRef.current[event]?.indexOf(callback) ?? -1;
if (index !== -1) {
- subscribersRef.current.splice(index, 1);
+ subscribersRef.current[event]?.splice(index, 1);
}
};
};
- const contextValue = useMemo(
- () => ({
- subscribers: subscribersRef.current,
- subscribe,
- postMessage,
- }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [],
- );
+ const postMessage: IBroadcastChannelContext["postMessage"] = (payload) => {
+ channelRef.current?.postMessage({
+ tabId: tabUuid,
+ payload,
+ });
+ };
return (
-
+
{children}
);
};
-export default BroadcastChannelProvider;
-
export const useBroadcastChannel = () => {
const context = useContext(BroadcastChannelContext);
@@ -116,3 +142,5 @@ export const useBroadcastChannel = () => {
return context;
};
+
+export default BroadcastChannelProvider;
diff --git a/widget/src/providers/ChatProvider.tsx b/widget/src/providers/ChatProvider.tsx
index 9c4b952f..9a8ba246 100644
--- a/widget/src/providers/ChatProvider.tsx
+++ b/widget/src/providers/ChatProvider.tsx
@@ -385,6 +385,8 @@ const ChatProvider: React.FC<{
}
}, [syncState, isOpen]);
+ const { subscribe } = useBroadcastChannel();
+
useEffect(() => {
if (screen === "chat" && connectionState === ConnectionState.connected) {
handleSubscription();
@@ -404,6 +406,10 @@ const ChatProvider: React.FC<{
socketCtx.socket.io.on("reconnect", reSubscribe);
+ subscribe("logout", () => {
+ socketCtx.socket.disconnect();
+ });
+
return () => {
socketCtx.socket.io.off("reconnect", reSubscribe);
};
@@ -452,13 +458,6 @@ const ChatProvider: React.FC<{
setMessage,
handleSubscription,
};
- const { subscribe } = useBroadcastChannel();
-
- subscribe(({ data }) => {
- if (data === "logout") {
- socketCtx.socket.disconnect();
- }
- });
return (
{children}