From 70bd710e82fd3e9030f704947ed3030d35c2f800 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:44:23 +0400 Subject: [PATCH] chore(frontend): Remove old session class and some artifacts that are no longer needed (#4310) --- .../components/chat/ChatInterface.test.tsx | 29 +-- .../chat/ConfirmationButtons.test.tsx | 26 --- .../settings/AutocompleteCombobox.test.tsx | 82 ------- frontend/__tests__/services/session.test.ts | 41 ---- .../modals/settings/AutocompleteCombobox.tsx | 81 ------- .../modals/settings/SettingsForm.tsx | 184 --------------- .../modals/settings/SettingsModal.tsx | 202 ---------------- frontend/src/services/agentStateService.ts | 13 -- frontend/src/services/session.ts | 221 ------------------ 9 files changed, 1 insertion(+), 878 deletions(-) delete mode 100644 frontend/__tests__/components/chat/ConfirmationButtons.test.tsx delete mode 100644 frontend/__tests__/components/modals/settings/AutocompleteCombobox.test.tsx delete mode 100644 frontend/__tests__/services/session.test.ts delete mode 100644 frontend/src/components/modals/settings/AutocompleteCombobox.tsx delete mode 100644 frontend/src/components/modals/settings/SettingsForm.tsx delete mode 100644 frontend/src/components/modals/settings/SettingsModal.tsx delete mode 100644 frontend/src/services/session.ts diff --git a/frontend/__tests__/components/chat/ChatInterface.test.tsx b/frontend/__tests__/components/chat/ChatInterface.test.tsx index 9fb431e4c9..b95d656e34 100644 --- a/frontend/__tests__/components/chat/ChatInterface.test.tsx +++ b/frontend/__tests__/components/chat/ChatInterface.test.tsx @@ -1,10 +1,8 @@ import { screen, act } from "@testing-library/react"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import userEvent from "@testing-library/user-event"; import { renderWithProviders } from "test-utils"; import { createMemoryRouter, RouterProvider } from "react-router-dom"; -import Session from "#/services/session"; -import ActionType from "#/types/ActionType"; import { addAssistantMessage } from "#/state/chatSlice"; import AgentState from "#/types/AgentState"; import ChatInterface from "#/components/chat/ChatInterface"; @@ -34,9 +32,6 @@ HTMLElement.prototype.scrollTo = vi.fn().mockImplementation(() => {}); const TEST_TIMESTAMP = new Date().toISOString(); describe.skip("ChatInterface", () => { - const sessionSendSpy = vi.spyOn(Session, "send"); - vi.spyOn(Session, "isConnected").mockReturnValue(true); - // TODO: replace below with e.g. fake timers // https://vitest.dev/guide/mocking#timers // https://vitest.dev/api/vi.html#vi-usefaketimers @@ -66,19 +61,6 @@ describe.skip("ChatInterface", () => { }, }); - const userMessageEvent = { - action: ActionType.MESSAGE, - args: { - content: "my message", - images_urls: [], - timestamp: TEST_TIMESTAMP, - }, - }; - - afterEach(() => { - sessionSendSpy.mockClear(); - }); - it("should render empty message list and input", () => { renderWithProviders(); expect(screen.queryAllByTestId("article")).toHaveLength(0); @@ -125,10 +107,6 @@ describe.skip("ChatInterface", () => { const input = screen.getByRole("textbox"); await user.type(input, "my message"); await user.keyboard("{Enter}"); - - expect(sessionSendSpy).toHaveBeenCalledWith( - expect.toMatchMessageEvent(JSON.stringify(userMessageEvent)), - ); }); it("should send the user message as an event to the Session when the agent state is AWAITING_USER_INPUT", async () => { @@ -144,10 +122,6 @@ describe.skip("ChatInterface", () => { const input = screen.getByRole("textbox"); await user.type(input, "my message"); await user.keyboard("{Enter}"); - - expect(sessionSendSpy).toHaveBeenCalledWith( - expect.toMatchMessageEvent(JSON.stringify(userMessageEvent)), - ); }); it("should disable the user input if agent is not initialized", async () => { @@ -168,7 +142,6 @@ describe.skip("ChatInterface", () => { ); expect(submitButton).toBeDisabled(); - expect(sessionSendSpy).not.toHaveBeenCalled(); }); it.todo("test scroll-related behaviour"); diff --git a/frontend/__tests__/components/chat/ConfirmationButtons.test.tsx b/frontend/__tests__/components/chat/ConfirmationButtons.test.tsx deleted file mode 100644 index 7298a5ae48..0000000000 --- a/frontend/__tests__/components/chat/ConfirmationButtons.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { userEvent } from "@testing-library/user-event"; -import { render, screen } from "@testing-library/react"; -import AgentState from "#/types/AgentState"; -import { changeAgentState } from "#/services/agentStateService"; -import ConfirmationButtons from "#/components/chat/ConfirmationButtons"; - -describe("ConfirmationButtons", () => { - vi.mock("#/services/agentStateService", () => ({ - changeAgentState: vi.fn(), - })); - - it.skip("should change agent state appropriately on button click", async () => { - const user = userEvent.setup(); - render(); - - const confirmButton = screen.getByTestId("action-confirm-button"); - const rejectButton = screen.getByTestId("action-reject-button"); - - await user.click(confirmButton); - expect(changeAgentState).toHaveBeenCalledWith(AgentState.USER_CONFIRMED); - - await user.click(rejectButton); - expect(changeAgentState).toHaveBeenCalledWith(AgentState.USER_REJECTED); - }); -}); diff --git a/frontend/__tests__/components/modals/settings/AutocompleteCombobox.test.tsx b/frontend/__tests__/components/modals/settings/AutocompleteCombobox.test.tsx deleted file mode 100644 index 16ede4eff6..0000000000 --- a/frontend/__tests__/components/modals/settings/AutocompleteCombobox.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { act, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { describe, expect, it, vi } from "vitest"; -import { AutocompleteCombobox } from "#/components/modals/settings/AutocompleteCombobox"; - -const onChangeMock = vi.fn(); - -const renderComponent = () => - render( - , - ); - -describe("AutocompleteCombobox", () => { - it("should render a combobox with the default value", () => { - renderComponent(); - - const modelInput = screen.getByRole("combobox", { name: "model" }); - expect(modelInput).toHaveValue("model1"); - }); - - it("should open a dropdown with the available values", async () => { - renderComponent(); - - const modelInput = screen.getByRole("combobox", { name: "model" }); - - expect(screen.queryByText("model2")).not.toBeInTheDocument(); - expect(screen.queryByText("model3")).not.toBeInTheDocument(); - - await act(async () => { - await userEvent.click(modelInput); - }); - - expect(screen.getByText("model2")).toBeInTheDocument(); - expect(screen.getByText("model3")).toBeInTheDocument(); - }); - - it("should call the onChange handler when a new value is selected", async () => { - renderComponent(); - - const modelInput = screen.getByRole("combobox", { name: "model" }); - expect(modelInput).toHaveValue("model1"); - - await act(async () => { - await userEvent.click(modelInput); - }); - - const model2 = screen.getByText("model2"); - await act(async () => { - await userEvent.click(model2); - }); - - expect(onChangeMock).toHaveBeenCalledWith("model2"); - }); - - it("should set the input value to the default key if the default key is not in the list", () => { - render( - , - ); - - const modelInput = screen.getByRole("combobox", { name: "model" }); - - expect(modelInput).toHaveValue("m2"); - }); - - it.todo("should show a tooltip after 0.5 seconds of focus"); -}); diff --git a/frontend/__tests__/services/session.test.ts b/frontend/__tests__/services/session.test.ts deleted file mode 100644 index 8bbfa309c2..0000000000 --- a/frontend/__tests__/services/session.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import ActionType from "#/types/ActionType"; -import { Settings, saveSettings } from "../../src/services/settings"; -import Session from "../../src/services/session"; - -const sendSpy = vi.spyOn(Session, "send"); -// @ts-expect-error - spying on private function -const setupSpy = vi.spyOn(Session, "_setupSocket").mockImplementation(() => { - // @ts-expect-error - calling a private function - Session._initializeAgent(); -}); - -describe("startNewSession", () => { - afterEach(() => { - sendSpy.mockClear(); - setupSpy.mockClear(); - }); - - it("Should start a new session with the current settings", () => { - const settings: Settings = { - LLM_MODEL: "llm_value", - LLM_BASE_URL: "base_url", - AGENT: "agent_value", - LANGUAGE: "language_value", - LLM_API_KEY: "sk-...", - CONFIRMATION_MODE: true, - SECURITY_ANALYZER: "analyzer", - }; - - const event = { - action: ActionType.INIT, - args: settings, - }; - - saveSettings(settings); - Session.startNewSession(); - - expect(setupSpy).toHaveBeenCalledTimes(1); - expect(sendSpy).toHaveBeenCalledWith(JSON.stringify(event)); - }); -}); diff --git a/frontend/src/components/modals/settings/AutocompleteCombobox.tsx b/frontend/src/components/modals/settings/AutocompleteCombobox.tsx deleted file mode 100644 index 4c309e9990..0000000000 --- a/frontend/src/components/modals/settings/AutocompleteCombobox.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Autocomplete, AutocompleteItem, Tooltip } from "@nextui-org/react"; -import React from "react"; -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; - -type Label = "model" | "agent" | "language" | "securityanalyzer"; - -const LABELS: Record = { - model: I18nKey.CONFIGURATION$MODEL_SELECT_LABEL, - agent: I18nKey.CONFIGURATION$AGENT_SELECT_LABEL, - language: I18nKey.CONFIGURATION$LANGUAGE_SELECT_LABEL, - securityanalyzer: I18nKey.CONFIGURATION$SECURITY_SELECT_LABEL, -}; - -const PLACEHOLDERS: Record = { - model: I18nKey.CONFIGURATION$MODEL_SELECT_PLACEHOLDER, - agent: I18nKey.CONFIGURATION$AGENT_SELECT_PLACEHOLDER, - language: I18nKey.CONFIGURATION$LANGUAGE_SELECT_PLACEHOLDER, - securityanalyzer: I18nKey.CONFIGURATION$SECURITY_SELECT_PLACEHOLDER, -}; - -type AutocompleteItemType = { - value: string; - label: string; -}; - -interface AutocompleteComboboxProps { - ariaLabel: Label; - items: AutocompleteItemType[]; - defaultKey: string; - onChange: (key: string) => void; - tooltip: string; - allowCustomValue?: boolean; - disabled?: boolean; -} - -export function AutocompleteCombobox({ - ariaLabel, - items, - defaultKey, - onChange, - tooltip, - allowCustomValue = false, - disabled = false, -}: AutocompleteComboboxProps) { - const { t } = useTranslation(); - - return ( - - item.value === defaultKey)?.label || defaultKey - } - onInputChange={(val) => { - onChange(val); - }} - isDisabled={disabled} - allowsCustomValue={allowCustomValue} - > - {(item) => ( - {item.label} - )} - - - ); -} diff --git a/frontend/src/components/modals/settings/SettingsForm.tsx b/frontend/src/components/modals/settings/SettingsForm.tsx deleted file mode 100644 index 4841373918..0000000000 --- a/frontend/src/components/modals/settings/SettingsForm.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { Input, Switch, Tooltip, useDisclosure } from "@nextui-org/react"; -import React from "react"; -import { useTranslation } from "react-i18next"; -import { FaEye, FaEyeSlash } from "react-icons/fa"; -import { AvailableLanguages } from "../../../i18n"; -import { I18nKey } from "../../../i18n/declaration"; -import { AutocompleteCombobox } from "./AutocompleteCombobox"; -import { Settings } from "#/services/settings"; -import { organizeModelsAndProviders } from "#/utils/organizeModelsAndProviders"; -import { extractModelAndProvider } from "#/utils/extractModelAndProvider"; -import { ModelSelector } from "./ModelSelector"; - -interface SettingsFormProps { - settings: Settings; - models: string[]; - agents: string[]; - securityAnalyzers: string[]; - disabled: boolean; - - onModelChange: (model: string) => void; - onBaseURLChange: (baseURL: string) => void; - onAPIKeyChange: (apiKey: string) => void; - onAgentChange: (agent: string) => void; - onLanguageChange: (language: string) => void; - onConfirmationModeChange: (confirmationMode: boolean) => void; - onSecurityAnalyzerChange: (securityAnalyzer: string) => void; -} - -function SettingsForm({ - settings, - models, - agents, - securityAnalyzers, - disabled, - onModelChange, - onBaseURLChange, - onAPIKeyChange, - onAgentChange, - onLanguageChange, - onConfirmationModeChange, - onSecurityAnalyzerChange, -}: SettingsFormProps) { - const { t } = useTranslation(); - const { isOpen: isVisible, onOpenChange: onVisibleChange } = useDisclosure(); - const advancedAlreadyInUse = React.useMemo(() => { - const organizedModels = organizeModelsAndProviders(models); - const { provider, model } = extractModelAndProvider( - settings.LLM_MODEL || "", - ); - const isKnownModel = - provider in organizedModels && - organizedModels[provider].models.includes(model); - - return ( - !!settings.SECURITY_ANALYZER || - !!settings.CONFIRMATION_MODE || - !!settings.LLM_BASE_URL || - (!!settings.LLM_MODEL && !isKnownModel) - ); - }, [settings, models]); - const [enableAdvanced, setEnableAdvanced] = - React.useState(advancedAlreadyInUse); - - React.useEffect(() => { - setEnableAdvanced(advancedAlreadyInUse); - }, [advancedAlreadyInUse]); - - const handleAdvancedChange = (value: boolean) => { - setEnableAdvanced(value); - }; - - return ( - <> - - Advanced Options - - {enableAdvanced && ( - <> - - - - )} - {!enableAdvanced && ( - - )} - { - onAPIKeyChange(e.target.value); - }} - endContent={ - - } - /> - - {enableAdvanced && ( - ({ value: agent, label: agent }))} - defaultKey={settings.AGENT} - onChange={onAgentChange} - tooltip={t(I18nKey.SETTINGS$AGENT_TOOLTIP)} - /> - )} - {enableAdvanced && ( - ({ - value: securityAnalyzer, - label: securityAnalyzer, - }))} - defaultKey={settings.SECURITY_ANALYZER} - onChange={onSecurityAnalyzerChange} - tooltip={t(I18nKey.SETTINGS$SECURITY_ANALYZER)} - disabled={disabled} - /> - )} - {enableAdvanced && ( - - - {t(I18nKey.SETTINGS$CONFIRMATION_MODE)} - - - )} - - ); -} - -export default SettingsForm; diff --git a/frontend/src/components/modals/settings/SettingsModal.tsx b/frontend/src/components/modals/settings/SettingsModal.tsx deleted file mode 100644 index 393b4fd8d8..0000000000 --- a/frontend/src/components/modals/settings/SettingsModal.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { Spinner } from "@nextui-org/react"; -import i18next from "i18next"; -import React, { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { useSelector } from "react-redux"; -import { fetchSecurityAnalyzers } from "#/services/options"; -import { AvailableLanguages } from "#/i18n"; -import { I18nKey } from "#/i18n/declaration"; -import Session from "#/services/session"; -import { RootState } from "../../../store"; -import AgentState from "../../../types/AgentState"; -import { - Settings, - getSettings, - getDefaultSettings, - settingsAreUpToDate, - maybeMigrateSettings, - saveSettings, -} from "#/services/settings"; -import toast from "#/utils/toast"; -import BaseModal from "../base-modal/BaseModal"; -import SettingsForm from "./SettingsForm"; - -interface SettingsProps { - isOpen: boolean; - onOpenChange: (isOpen: boolean) => void; - models: string[]; - agents: string[]; -} - -const REQUIRED_SETTINGS = ["LLM_MODEL"]; - -function SettingsModal({ - isOpen, - onOpenChange, - models, - agents, -}: SettingsProps) { - const { t } = useTranslation(); - - const [securityAnalyzers, setSecurityAnalyzers] = React.useState( - [], - ); - const [settings, setSettings] = React.useState({} as Settings); - const [agentIsRunning, setAgentIsRunning] = React.useState(false); - const [loading, setLoading] = React.useState(true); - const { curAgentState } = useSelector((state: RootState) => state.agent); - - useEffect(() => { - maybeMigrateSettings(); - setSettings(getSettings()); - }, []); - - useEffect(() => { - const isRunning = - curAgentState === AgentState.RUNNING || - curAgentState === AgentState.PAUSED || - curAgentState === AgentState.AWAITING_USER_INPUT || - curAgentState === AgentState.AWAITING_USER_CONFIRMATION; - setAgentIsRunning(isRunning); - }, [curAgentState]); - - React.useEffect(() => { - (async () => { - try { - setSecurityAnalyzers(await fetchSecurityAnalyzers()); - } catch (error) { - toast.error("settings", t(I18nKey.CONFIGURATION$ERROR_FETCH_MODELS)); - } finally { - setLoading(false); - } - })(); - }, []); - - const handleModelChange = (model: string) => { - setSettings((prev) => ({ - ...prev, - LLM_MODEL: model, - })); - }; - - const handleBaseURLChange = (baseURL: string) => { - setSettings((prev) => ({ - ...prev, - LLM_BASE_URL: baseURL, - })); - }; - - const handleAgentChange = (agent: string) => { - setSettings((prev) => ({ ...prev, AGENT: agent })); - }; - - const handleLanguageChange = (language: string) => { - const key = - AvailableLanguages.find((lang) => lang.label === language)?.value || - language; - // The appropriate key is assigned when the user selects a language. - // Otherwise, their input is reflected in the inputValue field of the Autocomplete component. - setSettings((prev) => ({ ...prev, LANGUAGE: key })); - }; - - const handleAPIKeyChange = (key: string) => { - setSettings((prev) => ({ ...prev, LLM_API_KEY: key })); - }; - - const handleConfirmationModeChange = (confirmationMode: boolean) => { - setSettings((prev) => ({ ...prev, CONFIRMATION_MODE: confirmationMode })); - }; - - const handleSecurityAnalyzerChange = (securityAnalyzer: string) => { - setSettings((prev) => ({ - ...prev, - CONFIRMATION_MODE: true, - SECURITY_ANALYZER: securityAnalyzer, - })); - }; - - const handleResetSettings = () => { - setSettings(getDefaultSettings); - }; - - const handleSaveSettings = () => { - saveSettings(settings); - i18next.changeLanguage(settings.LANGUAGE); - Session.startNewSession(); - - localStorage.setItem( - `API_KEY_${settings.LLM_MODEL || models[0]}`, - settings.LLM_API_KEY, - ); - }; - - let subtitle = ""; - if (loading) { - subtitle = t(I18nKey.CONFIGURATION$AGENT_LOADING); - } else if (agentIsRunning) { - subtitle = t(I18nKey.CONFIGURATION$AGENT_RUNNING); - } else if (!settingsAreUpToDate()) { - subtitle = t(I18nKey.CONFIGURATION$SETTINGS_NEED_UPDATE_MESSAGE); - } - const saveIsDisabled = REQUIRED_SETTINGS.some( - (key) => !settings[key as keyof Settings], - ); - - return ( - { - setSettings(getSettings()); // reset settings from any changes - }, - isDisabled: !settingsAreUpToDate(), - closeAfterAction: true, - className: "bg-rose-600 rounded-lg", - }, - ] - } - > - {loading && } - {!loading && ( - - )} - - ); -} - -export default SettingsModal; diff --git a/frontend/src/services/agentStateService.ts b/frontend/src/services/agentStateService.ts index 5fdf1f8d24..ac194a8b10 100644 --- a/frontend/src/services/agentStateService.ts +++ b/frontend/src/services/agentStateService.ts @@ -1,21 +1,8 @@ import ActionType from "#/types/ActionType"; import AgentState from "#/types/AgentState"; -import Session from "./session"; - -const INIT_DELAY = 1000; export const generateAgentStateChangeEvent = (state: AgentState) => JSON.stringify({ action: ActionType.CHANGE_AGENT_STATE, args: { agent_state: state }, }); - -export function changeAgentState(state: AgentState): void { - const eventString = generateAgentStateChangeEvent(state); - Session.send(eventString); - if (state === AgentState.STOPPED) { - setTimeout(() => { - Session.startNewSession(); - }, INIT_DELAY); - } -} diff --git a/frontend/src/services/session.ts b/frontend/src/services/session.ts deleted file mode 100644 index 8e77a33cf2..0000000000 --- a/frontend/src/services/session.ts +++ /dev/null @@ -1,221 +0,0 @@ -import i18next from "i18next"; -import toast from "#/utils/toast"; -import { handleAssistantMessage } from "./actions"; -import { getToken, setToken, clearToken } from "./auth"; -import ActionType from "#/types/ActionType"; -import { getSettings } from "./settings"; -import { I18nKey } from "#/i18n/declaration"; - -const translate = (key: I18nKey) => i18next.t(key); - -// Define a type for the messages -type Message = { - action: ActionType; - args: Record; -}; - -class Session { - private static _socket: WebSocket | null = null; - - private static _latest_event_id: number = -1; - - private static _messageQueue: Message[] = []; - - public static _history: Record[] = []; - - // callbacks contain a list of callable functions - // event: function, like: - // open: [function1, function2] - // message: [function1, function2] - private static callbacks: { - [K in keyof WebSocketEventMap]: ((data: WebSocketEventMap[K]) => void)[]; - } = { - open: [], - message: [], - error: [], - close: [], - }; - - private static _connecting = false; - - private static _disconnecting = false; - - public static restoreOrStartNewSession() { - if (Session.isConnected()) { - Session.disconnect(); - } - Session._connect(); - } - - public static startNewSession() { - clearToken(); - Session.restoreOrStartNewSession(); - } - - private static _initializeAgent = () => { - const settings = getSettings(); - const event = { - action: ActionType.INIT, - args: { - ...settings, - }, - }; - const eventString = JSON.stringify(event); - Session.send(eventString); - }; - - private static _connect(): void { - if (Session.isConnected()) return; - Session._connecting = true; - - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - let wsURL = `${protocol}//${window.location.host}/ws`; - const token = getToken(); - if (token) { - wsURL += `?token=${token}`; - if (Session._latest_event_id !== -1) { - wsURL += `&latest_event_id=${Session._latest_event_id}`; - } - } - Session._socket = new WebSocket(wsURL); - Session._setupSocket(); - } - - private static _setupSocket(): void { - if (!Session._socket) { - throw new Error( - translate(I18nKey.SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE), - ); - } - Session._socket.onopen = (e) => { - toast.success("ws", translate(I18nKey.SESSION$SERVER_CONNECTED_MESSAGE)); - Session._connecting = false; - Session._initializeAgent(); - Session._flushQueue(); - Session.callbacks.open?.forEach((callback) => { - callback(e); - }); - }; - - Session._socket.onmessage = (e) => { - let data = null; - try { - data = JSON.parse(e.data); - Session._history.push(data); - } catch (err) { - toast.error( - "ws", - translate(I18nKey.SESSION$SESSION_HANDLING_ERROR_MESSAGE), - ); - return; - } - if (data.error && data.error_code === 401) { - Session._latest_event_id = -1; - clearToken(); - } else if (data.token) { - setToken(data.token); - } else { - if (data.id !== undefined) { - Session._latest_event_id = data.id; - } - handleAssistantMessage(data); - } - }; - - Session._socket.onerror = () => { - // TODO report error - toast.error( - "ws", - translate(I18nKey.SESSION$SESSION_CONNECTION_ERROR_MESSAGE), - ); - }; - - Session._socket.onclose = () => { - if (!Session._disconnecting) { - setTimeout(() => { - Session.restoreOrStartNewSession(); - }, 3000); // Reconnect after 3 seconds - } - Session._disconnecting = false; - }; - } - - static isConnected(): boolean { - return ( - Session._socket !== null && Session._socket.readyState === WebSocket.OPEN - ); - } - - static disconnect(): void { - Session._disconnecting = true; - if (Session._socket) { - Session._socket.close(); - } - Session._socket = null; - } - - private static _flushQueue(): void { - while (Session._messageQueue.length > 0) { - const message = Session._messageQueue.shift(); - if (message) { - setTimeout(() => Session.send(JSON.stringify(message)), 1000); - } - } - } - - static send(message: string): void { - const messageObject: Message = JSON.parse(message); - - if (Session._connecting) { - Session._messageQueue.push(messageObject); - return; - } - if (!Session.isConnected()) { - throw new Error( - translate(I18nKey.SESSION$SESSION_CONNECTION_ERROR_MESSAGE), - ); - } - - if (Session.isConnected()) { - Session._socket?.send(message); - Session._history.push(JSON.parse(message)); - } else { - toast.error( - "ws", - translate(I18nKey.SESSION$SESSION_CONNECTION_ERROR_MESSAGE), - ); - } - } - - static addEventListener( - event: string, - callback: (e: MessageEvent) => void, - ): void { - Session._socket?.addEventListener( - event as keyof WebSocketEventMap, - callback as ( - this: WebSocket, - ev: WebSocketEventMap[keyof WebSocketEventMap], - ) => never, - ); - } - - static removeEventListener( - event: string, - listener: (e: Event) => void, - ): void { - Session._socket?.removeEventListener(event, listener); - } - - static registerCallback( - event: K, - callbacks: ((data: WebSocketEventMap[K]) => void)[], - ): void { - if (Session.callbacks[event] === undefined) { - return; - } - Session.callbacks[event].push(...callbacks); - } -} - -export default Session;