From 2da60ff4e448eefb59a07826addf46fcbcbf411e Mon Sep 17 00:00:00 2001 From: Akhilesh Rangani Date: Fri, 23 Aug 2024 20:09:54 -0400 Subject: [PATCH] fix: only one socket connection via socketcontext --- frontend/app/layout.tsx | 7 +- frontend/components/editor/index.tsx | 79 ++++++++----------- frontend/components/editor/navbar/run.tsx | 2 +- .../components/editor/terminals/index.tsx | 5 +- frontend/context/SocketContext.tsx | 63 +++++++++++++++ frontend/context/TerminalContext.tsx | 39 +-------- 6 files changed, 111 insertions(+), 84 deletions(-) create mode 100644 frontend/context/SocketContext.tsx diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 79f8b5d..2ba77ac 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -7,7 +7,8 @@ import { ClerkProvider } from "@clerk/nextjs" import { Toaster } from "@/components/ui/sonner" import { Analytics } from "@vercel/analytics/react" import { TerminalProvider } from '@/context/TerminalContext'; -import { PreviewProvider } from "@/context/PreviewContext" +import { PreviewProvider } from "@/context/PreviewContext"; +import { SocketProvider } from '@/context/SocketContext' export const metadata: Metadata = { title: "Sandbox", @@ -15,7 +16,7 @@ export const metadata: Metadata = { } export default function RootLayout({ - children, + children }: Readonly<{ children: React.ReactNode }>) { @@ -29,11 +30,13 @@ export default function RootLayout({ forcedTheme="dark" disableTransitionOnChange > + {children} + diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 595c914..5d4340d 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -3,7 +3,6 @@ import { SetStateAction, useCallback, useEffect, useRef, useState } from "react" import monaco from "monaco-editor" import Editor, { BeforeMount, OnMount } from "@monaco-editor/react" -import { Socket, io } from "socket.io-client" import { toast } from "sonner" import { useClerk } from "@clerk/nextjs" @@ -32,7 +31,7 @@ import PreviewWindow from "./preview" import Terminals from "./terminals" import { ImperativePanelHandle } from "react-resizable-panels" import { PreviewProvider, usePreview } from '@/context/PreviewContext'; -import { useTerminal } from '@/context/TerminalContext'; +import { useSocket } from "@/context/SocketContext" export default function CodeEditor({ userData, @@ -41,24 +40,16 @@ export default function CodeEditor({ userData: User sandboxData: Sandbox }) { - const socketRef = useRef(null); - // Initialize socket connection if it doesn't exist - if (!socketRef.current) { - socketRef.current = io( - `${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userData.id}&sandboxId=${sandboxData.id}`, - { - timeout: 2000, - } - ); - } - - //Terminalcontext functionsand effects - const { setUserAndSandboxId } = useTerminal(); + //SocketContext functions and effects + const { socket, setUserAndSandboxId } = useSocket(); useEffect(() => { - setUserAndSandboxId(userData.id, sandboxData.id); - }, [userData.id, sandboxData.id, setUserAndSandboxId]); + // Check if socket is null, and initialize it by setting userId and sandboxId + if (!socket && userData.id && sandboxData.id) { + setUserAndSandboxId(userData.id, sandboxData.id); + } + }, [socket, userData.id, sandboxData.id, setUserAndSandboxId]); //Preview Button state const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) @@ -120,7 +111,7 @@ export default function CodeEditor({ const room = useRoom() const [provider, setProvider] = useState() const userInfo = useSelf((me) => me.info) - + // Liveblocks providers map to prevent reinitializing providers type ProviderData = { provider: LiveblocksProvider; @@ -333,9 +324,9 @@ export default function CodeEditor({ ); console.log(`Saving file...${activeFileId}`); console.log(`Saving file...${value}`); - socketRef.current?.emit("saveFile", activeFileId, value); + socket?.emit("saveFile", activeFileId, value); }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), - [socketRef] + [socket] ); useEffect(() => { @@ -432,9 +423,9 @@ export default function CodeEditor({ // Connection/disconnection effect useEffect(() => { - socketRef.current?.connect() + socket?.connect() return () => { - socketRef.current?.disconnect() + socket?.disconnect() } }, []) @@ -469,22 +460,22 @@ export default function CodeEditor({ }) } - socketRef.current?.on("connect", onConnect) - socketRef.current?.on("disconnect", onDisconnect) - socketRef.current?.on("loaded", onLoadedEvent) - socketRef.current?.on("error", onError) - socketRef.current?.on("terminalResponse", onTerminalResponse) - socketRef.current?.on("disableAccess", onDisableAccess) - socketRef.current?.on("previewURL", loadPreviewURL) + socket?.on("connect", onConnect) + socket?.on("disconnect", onDisconnect) + socket?.on("loaded", onLoadedEvent) + socket?.on("error", onError) + socket?.on("terminalResponse", onTerminalResponse) + socket?.on("disableAccess", onDisableAccess) + socket?.on("previewURL", loadPreviewURL) return () => { - socketRef.current?.off("connect", onConnect) - socketRef.current?.off("disconnect", onDisconnect) - socketRef.current?.off("loaded", onLoadedEvent) - socketRef.current?.off("error", onError) - socketRef.current?.off("terminalResponse", onTerminalResponse) - socketRef.current?.off("disableAccess", onDisableAccess) - socketRef.current?.off("previewURL", loadPreviewURL) + socket?.off("connect", onConnect) + socket?.off("disconnect", onDisconnect) + socket?.off("loaded", onLoadedEvent) + socket?.off("error", onError) + socket?.off("terminalResponse", onTerminalResponse) + socket?.off("disableAccess", onDisableAccess) + socket?.off("previewURL", loadPreviewURL) } // }, []); }, [terminals]) @@ -499,7 +490,7 @@ export default function CodeEditor({ // Debounced function to get file content const debouncedGetFile = useCallback( debounce((tabId, callback) => { - socketRef.current?.emit('getFile', tabId, callback); + socket?.emit('getFile', tabId, callback); }, 300), // 300ms debounce delay, adjust as needed [] ); @@ -603,7 +594,7 @@ export default function CodeEditor({ return false } - socketRef.current?.emit("renameFile", id, newName) + socket?.emit("renameFile", id, newName) setTabs((prev) => prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab)) ) @@ -612,7 +603,7 @@ export default function CodeEditor({ } const handleDeleteFile = (file: TFile) => { - socketRef.current?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => { + socket?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => { setFiles(response) }) closeTab(file.id) @@ -622,11 +613,11 @@ export default function CodeEditor({ setDeletingFolderId(folder.id) console.log("deleting folder", folder.id) - socketRef.current?.emit("getFolder", folder.id, (response: string[]) => + socket?.emit("getFolder", folder.id, (response: string[]) => closeTabs(response) ) - socketRef.current?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => { + socket?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => { setFiles(response) setDeletingFolderId("") }) @@ -654,7 +645,7 @@ export default function CodeEditor({ {generate.show && ai ? ( t.id === activeFileId)?.name ?? "", @@ -714,7 +705,7 @@ export default function CodeEditor({ handleRename={handleRename} handleDeleteFile={handleDeleteFile} handleDeleteFolder={handleDeleteFolder} - socket={socketRef.current} + socket={socket!} setFiles={setFiles} addNew={(name, type) => addNew(name, type, setFiles, sandboxData)} deletingFolderId={deletingFolderId} @@ -832,7 +823,7 @@ export default function CodeEditor({ open={() => { usePreview().previewPanelRef.current?.expand() setIsPreviewCollapsed(false) - } } collapsed={isPreviewCollapsed} src={previewURL} ref={previewWindowRef} /> + }} collapsed={isPreviewCollapsed} src={previewURL} ref={previewWindowRef} /> void; +} + +const SocketContext = createContext(undefined); + +export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [socket, setSocket] = useState(null); + const [userId, setUserId] = useState(null); + const [sandboxId, setSandboxId] = useState(null); + + useEffect(() => { + if (userId && sandboxId) { + console.log("Initializing socket connection..."); + const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`); + console.log("Socket instance:", newSocket); + setSocket(newSocket); + + newSocket.on('connect', () => { + console.log("Socket connected:", newSocket.id); + }); + + newSocket.on('disconnect', () => { + console.log("Socket disconnected"); + }); + + return () => { + console.log("Disconnecting socket..."); + newSocket.disconnect(); + }; + } + }, [userId, sandboxId]); + + const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => { + setUserId(newUserId); + setSandboxId(newSandboxId); + }; + + const value = { + socket, + setUserAndSandboxId, + }; + + return ( + + {children} + + ); +}; + +export const useSocket = (): SocketContextType => { + const context = useContext(SocketContext); + if (!context) { + throw new Error('useSocket must be used within a SocketProvider'); + } + return context; +}; diff --git a/frontend/context/TerminalContext.tsx b/frontend/context/TerminalContext.tsx index a329bec..cb1cc47 100644 --- a/frontend/context/TerminalContext.tsx +++ b/frontend/context/TerminalContext.tsx @@ -1,12 +1,11 @@ "use client"; -import React, { createContext, useContext, useState, useEffect } from 'react'; -import { io, Socket } from 'socket.io-client'; +import React, { createContext, useContext, useState } from 'react'; import { Terminal } from '@xterm/xterm'; import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal'; +import { useSocket } from '@/context/SocketContext'; interface TerminalContextType { - socket: Socket | null; terminals: { id: string; terminal: Terminal | null }[]; setTerminals: React.Dispatch>; activeTerminalId: string; @@ -15,41 +14,16 @@ interface TerminalContextType { setCreatingTerminal: React.Dispatch>; createNewTerminal: (command?: string) => Promise; closeTerminal: (id: string) => void; - setUserAndSandboxId: (userId: string, sandboxId: string) => void; deploy: (callback: () => void) => void; } const TerminalContext = createContext(undefined); export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [socket, setSocket] = useState(null); + const { socket } = useSocket(); const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]); const [activeTerminalId, setActiveTerminalId] = useState(''); const [creatingTerminal, setCreatingTerminal] = useState(false); - const [userId, setUserId] = useState(null); - const [sandboxId, setSandboxId] = useState(null); - - useEffect(() => { - if (userId && sandboxId) { - console.log("Initializing socket connection..."); - const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`); - console.log("Socket instance:", newSocket); - setSocket(newSocket); - - newSocket.on('connect', () => { - console.log("Socket connected:", newSocket.id); - }); - - newSocket.on('disconnect', () => { - console.log("Socket disconnected"); - }); - - return () => { - console.log("Disconnecting socket..."); - newSocket.disconnect(); - }; - } - }, [userId, sandboxId]); const createNewTerminal = async (command?: string): Promise => { if (!socket) return; @@ -85,11 +59,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil } }; - const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => { - setUserId(newUserId); - setSandboxId(newSandboxId); - }; - const deploy = (callback: () => void) => { if (!socket) console.error("Couldn't deploy: No socket"); console.log("Deploying...") @@ -99,7 +68,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil } const value = { - socket, terminals, setTerminals, activeTerminalId, @@ -108,7 +76,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil setCreatingTerminal, createNewTerminal, closeTerminal, - setUserAndSandboxId, deploy };