improve starting server logic

This commit is contained in:
Ishaan Dey 2024-05-26 12:18:09 -07:00
parent 07d59cf01b
commit 010a4fec59
8 changed files with 542 additions and 513 deletions

View File

@ -3,6 +3,8 @@ CLERK_SECRET_KEY=
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
LIVEBLOCKS_SECRET_KEY= LIVEBLOCKS_SECRET_KEY=
VERCEL_ENV=development
AWS_ACCESS_KEY_ID= AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=

View File

@ -1,79 +1,75 @@
import Navbar from "@/components/editor/navbar"; import Navbar from "@/components/editor/navbar"
import { Room } from "@/components/editor/live/room"; import { Room } from "@/components/editor/live/room"
import { Sandbox, User, UsersToSandboxes } from "@/lib/types"; import { Sandbox, User, UsersToSandboxes } from "@/lib/types"
import { currentUser } from "@clerk/nextjs"; import { currentUser } from "@clerk/nextjs"
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation"
import Editor from "@/components/editor"; import Editor from "@/components/editor"
export const revalidate = 0; export const revalidate = 0
const getUserData = async (id: string) => { const getUserData = async (id: string) => {
const userRes = await fetch( const userRes = await fetch(
`https://database.ishaan1013.workers.dev/api/user?id=${id}` `https://database.ishaan1013.workers.dev/api/user?id=${id}`
); )
const userData: User = await userRes.json(); const userData: User = await userRes.json()
return userData; return userData
}; }
const getSandboxData = async (id: string) => { const getSandboxData = async (id: string) => {
const sandboxRes = await fetch( const sandboxRes = await fetch(
`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}` `https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`
); )
const sandboxData: Sandbox = await sandboxRes.json(); const sandboxData: Sandbox = await sandboxRes.json()
return sandboxData; return sandboxData
}; }
const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => { const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => {
if (!usersToSandboxes) { if (!usersToSandboxes) {
return []; return []
} }
const shared = await Promise.all( const shared = await Promise.all(
usersToSandboxes.map(async (user) => { usersToSandboxes.map(async (user) => {
const userRes = await fetch( const userRes = await fetch(
`https://database.ishaan1013.workers.dev/api/user?id=${user.userId}` `https://database.ishaan1013.workers.dev/api/user?id=${user.userId}`
); )
const userData: User = await userRes.json(); const userData: User = await userRes.json()
return { id: userData.id, name: userData.name }; return { id: userData.id, name: userData.name }
}) })
); )
return shared; return shared
}; }
export default async function CodePage({ params }: { params: { id: string } }) { export default async function CodePage({ params }: { params: { id: string } }) {
const user = await currentUser(); const user = await currentUser()
const sandboxId = params.id; const sandboxId = params.id
if (!user) { if (!user) {
redirect("/"); redirect("/")
} }
const userData = await getUserData(user.id); const userData = await getUserData(user.id)
const sandboxData = await getSandboxData(sandboxId); const sandboxData = await getSandboxData(sandboxId)
const shared = await getSharedUsers(sandboxData.usersToSandboxes); const shared = await getSharedUsers(sandboxData.usersToSandboxes)
const isOwner = sandboxData.userId === user.id; const isOwner = sandboxData.userId === user.id
const isSharedUser = shared.some((uts) => uts.id === user.id); const isSharedUser = shared.some((uts) => uts.id === user.id)
if (!isOwner && !isSharedUser) { if (!isOwner && !isSharedUser) {
return notFound(); return notFound()
} }
console.log("sandboxes: ", sandboxData); console.log("sandboxes: ", sandboxData)
return ( return (
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background"> <div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
<Room id={sandboxId}> <Room id={sandboxId}>
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} /> <Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
<div className="w-screen flex grow"> <div className="w-screen flex grow">
<Editor <Editor userData={userData} sandboxData={sandboxData} />
isOwner={isOwner}
userData={userData}
sandboxData={sandboxData}
/>
</div> </div>
</Room> </Room>
</div> </div>
); )
} }

View File

@ -1,13 +1,13 @@
"use client"; "use client"
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion"
import Image from "next/image"; import Image from "next/image"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react"
import ProjectCardDropdown from "./dropdown"; import ProjectCardDropdown from "./dropdown"
import { Clock, Globe, Lock } from "lucide-react"; import { Clock, Globe, Lock } from "lucide-react"
import { Sandbox } from "@/lib/types"; import { Sandbox } from "@/lib/types"
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card"
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation"
export default function ProjectCard({ export default function ProjectCard({
children, children,
@ -16,33 +16,33 @@ export default function ProjectCard({
onDelete, onDelete,
deletingId, deletingId,
}: { }: {
children?: React.ReactNode; children?: React.ReactNode
sandbox: Sandbox; sandbox: Sandbox
onVisibilityChange: (sandbox: Sandbox) => void; onVisibilityChange: (sandbox: Sandbox) => void
onDelete: (sandbox: Sandbox) => void; onDelete: (sandbox: Sandbox) => void
deletingId: string; deletingId: string
}) { }) {
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false)
const [date, setDate] = useState<string>(); const [date, setDate] = useState<string>()
const router = useRouter(); const router = useRouter()
useEffect(() => { useEffect(() => {
const createdAt = new Date(sandbox.createdAt); const createdAt = new Date(sandbox.createdAt)
const now = new Date(); const now = new Date()
const diffInMinutes = Math.floor( const diffInMinutes = Math.floor(
(now.getTime() - createdAt.getTime()) / 60000 (now.getTime() - createdAt.getTime()) / 60000
); )
if (diffInMinutes < 1) { if (diffInMinutes < 1) {
setDate("Now"); setDate("Now")
} else if (diffInMinutes < 60) { } else if (diffInMinutes < 60) {
setDate(`${diffInMinutes}m ago`); setDate(`${diffInMinutes}m ago`)
} else if (diffInMinutes < 1440) { } else if (diffInMinutes < 1440) {
setDate(`${Math.floor(diffInMinutes / 60)}h ago`); setDate(`${Math.floor(diffInMinutes / 60)}h ago`)
} else { } else {
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`); setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
} }
}, [sandbox]); }, [sandbox])
return ( return (
<Card <Card
@ -52,7 +52,7 @@ export default function ProjectCard({
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
className={`group/canvas-card p-4 h-48 flex flex-col justify-between items-start hover:border-muted-foreground/50 relative overflow-hidden transition-all`} className={`group/canvas-card p-4 h-48 flex flex-col justify-between items-start hover:border-muted-foreground/50 relative overflow-hidden transition-all`}
> >
{/* <AnimatePresence> <AnimatePresence>
{hovered && ( {hovered && (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
@ -62,7 +62,7 @@ export default function ProjectCard({
{children} {children}
</motion.div> </motion.div>
)} )}
</AnimatePresence> */} </AnimatePresence>
<div className="space-x-2 flex items-center justify-start w-full z-10"> <div className="space-x-2 flex items-center justify-start w-full z-10">
<Image <Image
@ -101,5 +101,5 @@ export default function ProjectCard({
</div> </div>
</div> </div>
</Card> </Card>
); )
} }

View File

@ -1,110 +1,109 @@
"use client"; "use client"
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react"
import monaco from "monaco-editor"; import monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"; import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { io } from "socket.io-client"; import { io } from "socket.io-client"
import { toast } from "sonner"; import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs"; import { useClerk } from "@clerk/nextjs"
import * as Y from "yjs"; import * as Y from "yjs"
import LiveblocksProvider from "@liveblocks/yjs"; import LiveblocksProvider from "@liveblocks/yjs"
import { MonacoBinding } from "y-monaco"; import { MonacoBinding } from "y-monaco"
import { Awareness } from "y-protocols/awareness"; import { Awareness } from "y-protocols/awareness"
import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config"; import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config"
import { import {
ResizableHandle, ResizableHandle,
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable"; } from "@/components/ui/resizable"
import { FileJson, Loader2, TerminalSquare } from "lucide-react"; import { FileJson, Loader2, TerminalSquare } from "lucide-react"
import Tab from "../ui/tab"; import Tab from "../ui/tab"
import Sidebar from "./sidebar"; import Sidebar from "./sidebar"
import GenerateInput from "./generate"; import GenerateInput from "./generate"
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"; import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
import { addNew, processFileType, validateName } from "@/lib/utils"; import { addNew, processFileType, validateName } from "@/lib/utils"
import { Cursors } from "./live/cursors"; import { Cursors } from "./live/cursors"
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm"
import DisableAccessModal from "./live/disableModal"; import DisableAccessModal from "./live/disableModal"
import Loading from "./loading"; import Loading from "./loading"
import PreviewWindow from "./preview"; import PreviewWindow from "./preview"
import Terminals from "./terminals"; import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels"; import { ImperativePanelHandle } from "react-resizable-panels"
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
sandboxData, sandboxData,
ip, ip,
}: { }: {
userData: User; userData: User
sandboxData: Sandbox; sandboxData: Sandbox
ip: string; ip: string
}) { }) {
const socket = io( const socket = io(
// `ws://${sandboxData.id}.ws.ishaand.com?userId=${userData.id}&sandboxId=${sandboxData.id}`
`http://${ip}:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`, `http://${ip}:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`,
{ {
timeout: 2000, timeout: 2000,
} }
); )
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true); const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
const [disableAccess, setDisableAccess] = useState({ const [disableAccess, setDisableAccess] = useState({
isDisabled: false, isDisabled: false,
message: "", message: "",
}); })
// File state // File state
const [files, setFiles] = useState<(TFolder | TFile)[]>([]); const [files, setFiles] = useState<(TFolder | TFile)[]>([])
const [tabs, setTabs] = useState<TTab[]>([]); const [tabs, setTabs] = useState<TTab[]>([])
const [activeFileId, setActiveFileId] = useState<string>(""); const [activeFileId, setActiveFileId] = useState<string>("")
const [activeFileContent, setActiveFileContent] = useState(""); const [activeFileContent, setActiveFileContent] = useState("")
const [deletingFolderId, setDeletingFolderId] = useState(""); const [deletingFolderId, setDeletingFolderId] = useState("")
// Editor state // Editor state
const [editorLanguage, setEditorLanguage] = useState("plaintext"); const [editorLanguage, setEditorLanguage] = useState("plaintext")
const [cursorLine, setCursorLine] = useState(0); const [cursorLine, setCursorLine] = useState(0)
const [editorRef, setEditorRef] = const [editorRef, setEditorRef] =
useState<monaco.editor.IStandaloneCodeEditor>(); useState<monaco.editor.IStandaloneCodeEditor>()
// AI Copilot state // AI Copilot state
const [ai, setAi] = useState(false); const [ai, setAi] = useState(false)
const [generate, setGenerate] = useState<{ const [generate, setGenerate] = useState<{
show: boolean; show: boolean
id: string; id: string
line: number; line: number
widget: monaco.editor.IContentWidget | undefined; widget: monaco.editor.IContentWidget | undefined
pref: monaco.editor.ContentWidgetPositionPreference[]; pref: monaco.editor.ContentWidgetPositionPreference[]
width: number; width: number
}>({ show: false, line: 0, id: "", widget: undefined, pref: [], width: 0 }); }>({ show: false, line: 0, id: "", widget: undefined, pref: [], width: 0 })
const [decorations, setDecorations] = useState<{ const [decorations, setDecorations] = useState<{
options: monaco.editor.IModelDeltaDecoration[]; options: monaco.editor.IModelDeltaDecoration[]
instance: monaco.editor.IEditorDecorationsCollection | undefined; instance: monaco.editor.IEditorDecorationsCollection | undefined
}>({ options: [], instance: undefined }); }>({ options: [], instance: undefined })
// Terminal state // Terminal state
const [terminals, setTerminals] = useState< const [terminals, setTerminals] = useState<
{ {
id: string; id: string
terminal: Terminal | null; terminal: Terminal | null
}[] }[]
>([]); >([])
const isOwner = sandboxData.userId === userData.id; const isOwner = sandboxData.userId === userData.id
const clerk = useClerk(); const clerk = useClerk()
// Liveblocks hooks // Liveblocks hooks
const room = useRoom(); const room = useRoom()
const [provider, setProvider] = useState<TypedLiveblocksProvider>(); const [provider, setProvider] = useState<TypedLiveblocksProvider>()
// Refs for libraries / features // Refs for libraries / features
const editorContainerRef = useRef<HTMLDivElement>(null); const editorContainerRef = useRef<HTMLDivElement>(null)
const monacoRef = useRef<typeof monaco | null>(null); const monacoRef = useRef<typeof monaco | null>(null)
const generateRef = useRef<HTMLDivElement>(null); const generateRef = useRef<HTMLDivElement>(null)
const generateWidgetRef = useRef<HTMLDivElement>(null); const generateWidgetRef = useRef<HTMLDivElement>(null)
const previewPanelRef = useRef<ImperativePanelHandle>(null); const previewPanelRef = useRef<ImperativePanelHandle>(null)
const editorPanelRef = useRef<ImperativePanelHandle>(null); const editorPanelRef = useRef<ImperativePanelHandle>(null)
// Pre-mount editor keybindings // Pre-mount editor keybindings
const handleEditorWillMount: BeforeMount = (monaco) => { const handleEditorWillMount: BeforeMount = (monaco) => {
@ -113,21 +112,21 @@ export default function CodeEditor({
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG, keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG,
command: "null", command: "null",
}, },
]); ])
}; }
// Post-mount editor keybindings and actions // Post-mount editor keybindings and actions
const handleEditorMount: OnMount = (editor, monaco) => { const handleEditorMount: OnMount = (editor, monaco) => {
setEditorRef(editor); setEditorRef(editor)
monacoRef.current = monaco; monacoRef.current = monaco
editor.onDidChangeCursorPosition((e) => { editor.onDidChangeCursorPosition((e) => {
const { column, lineNumber } = e.position; const { column, lineNumber } = e.position
if (lineNumber === cursorLine) return; if (lineNumber === cursorLine) return
setCursorLine(lineNumber); setCursorLine(lineNumber)
const model = editor.getModel(); const model = editor.getModel()
const endColumn = model?.getLineContent(lineNumber).length || 0; const endColumn = model?.getLineContent(lineNumber).length || 0
setDecorations((prev) => { setDecorations((prev) => {
return { return {
@ -145,18 +144,18 @@ export default function CodeEditor({
}, },
}, },
], ],
}; }
}); })
}); })
editor.onDidBlurEditorText((e) => { editor.onDidBlurEditorText((e) => {
setDecorations((prev) => { setDecorations((prev) => {
return { return {
...prev, ...prev,
options: [], options: [],
}; }
}); })
}); })
editor.addAction({ editor.addAction({
id: "generate", id: "generate",
@ -170,11 +169,11 @@ export default function CodeEditor({
...prev, ...prev,
show: !prev.show, show: !prev.show,
pref: [monaco.editor.ContentWidgetPositionPreference.BELOW], pref: [monaco.editor.ContentWidgetPositionPreference.BELOW],
}; }
}); })
}, },
}); })
}; }
// Generate widget effect // Generate widget effect
useEffect(() => { useEffect(() => {
@ -183,32 +182,32 @@ export default function CodeEditor({
return { return {
...prev, ...prev,
show: false, show: false,
}; }
}); })
return; return
} }
if (generate.show) { if (generate.show) {
editorRef?.changeViewZones(function (changeAccessor) { editorRef?.changeViewZones(function (changeAccessor) {
if (!generateRef.current) return; if (!generateRef.current) return
const id = changeAccessor.addZone({ const id = changeAccessor.addZone({
afterLineNumber: cursorLine, afterLineNumber: cursorLine,
heightInLines: 3, heightInLines: 3,
domNode: generateRef.current, domNode: generateRef.current,
}); })
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id, line: cursorLine }; return { ...prev, id, line: cursorLine }
}); })
}); })
if (!generateWidgetRef.current) return; if (!generateWidgetRef.current) return
const widgetElement = generateWidgetRef.current; const widgetElement = generateWidgetRef.current
const contentWidget = { const contentWidget = {
getDomNode: () => { getDomNode: () => {
return widgetElement; return widgetElement
}, },
getId: () => { getId: () => {
return "generate.widget"; return "generate.widget"
}, },
getPosition: () => { getPosition: () => {
return { return {
@ -217,232 +216,232 @@ export default function CodeEditor({
column: 1, column: 1,
}, },
preference: generate.pref, preference: generate.pref,
}; }
}, },
}; }
// window width - sidebar width, times the percentage of the editor panel // window width - sidebar width, times the percentage of the editor panel
const width = editorPanelRef.current const width = editorPanelRef.current
? (editorPanelRef.current.getSize() / 100) * (window.innerWidth - 224) ? (editorPanelRef.current.getSize() / 100) * (window.innerWidth - 224)
: 400; //fallback : 400 //fallback
setGenerate((prev) => { setGenerate((prev) => {
return { return {
...prev, ...prev,
widget: contentWidget, widget: contentWidget,
width, width,
}; }
}); })
editorRef?.addContentWidget(contentWidget); editorRef?.addContentWidget(contentWidget)
if (generateRef.current && generateWidgetRef.current) { if (generateRef.current && generateWidgetRef.current) {
editorRef?.applyFontInfo(generateRef.current); editorRef?.applyFontInfo(generateRef.current)
editorRef?.applyFontInfo(generateWidgetRef.current); editorRef?.applyFontInfo(generateWidgetRef.current)
} }
} else { } else {
editorRef?.changeViewZones(function (changeAccessor) { editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id); changeAccessor.removeZone(generate.id)
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id: "" }; return { ...prev, id: "" }
}); })
}); })
if (!generate.widget) return; if (!generate.widget) return
editorRef?.removeContentWidget(generate.widget); editorRef?.removeContentWidget(generate.widget)
setGenerate((prev) => { setGenerate((prev) => {
return { return {
...prev, ...prev,
widget: undefined, widget: undefined,
}; }
}); })
} }
}, [generate.show]); }, [generate.show])
// Decorations effect for generate widget tips // Decorations effect for generate widget tips
useEffect(() => { useEffect(() => {
if (decorations.options.length === 0) { if (decorations.options.length === 0) {
decorations.instance?.clear(); decorations.instance?.clear()
} }
if (!ai) return; if (!ai) return
if (decorations.instance) { if (decorations.instance) {
decorations.instance.set(decorations.options); decorations.instance.set(decorations.options)
} else { } else {
const instance = editorRef?.createDecorationsCollection(); const instance = editorRef?.createDecorationsCollection()
instance?.set(decorations.options); instance?.set(decorations.options)
setDecorations((prev) => { setDecorations((prev) => {
return { return {
...prev, ...prev,
instance, instance,
}; }
}); })
} }
}, [decorations.options]); }, [decorations.options])
// Save file keybinding logic effect // Save file keybinding logic effect
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) { if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault(); e.preventDefault()
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeFileId ? { ...tab, saved: true } : tab tab.id === activeFileId ? { ...tab, saved: true } : tab
) )
); )
socket.emit("saveFile", activeFileId, editorRef?.getValue()); socket.emit("saveFile", activeFileId, editorRef?.getValue())
} }
}; }
document.addEventListener("keydown", down); document.addEventListener("keydown", down)
return () => { return () => {
document.removeEventListener("keydown", down); document.removeEventListener("keydown", down)
}; }
}, [tabs, activeFileId]); }, [tabs, activeFileId])
// Liveblocks live collaboration setup effect // Liveblocks live collaboration setup effect
useEffect(() => { useEffect(() => {
const tab = tabs.find((t) => t.id === activeFileId); const tab = tabs.find((t) => t.id === activeFileId)
const model = editorRef?.getModel(); const model = editorRef?.getModel()
if (!editorRef || !tab || !model) return; if (!editorRef || !tab || !model) return
const yDoc = new Y.Doc(); const yDoc = new Y.Doc()
const yText = yDoc.getText(tab.id); const yText = yDoc.getText(tab.id)
const yProvider: any = new LiveblocksProvider(room, yDoc); const yProvider: any = new LiveblocksProvider(room, yDoc)
const onSync = (isSynced: boolean) => { const onSync = (isSynced: boolean) => {
if (isSynced) { if (isSynced) {
const text = yText.toString(); const text = yText.toString()
if (text === "") { if (text === "") {
if (activeFileContent) { if (activeFileContent) {
yText.insert(0, activeFileContent); yText.insert(0, activeFileContent)
} else { } else {
setTimeout(() => { setTimeout(() => {
yText.insert(0, editorRef.getValue()); yText.insert(0, editorRef.getValue())
}, 0); }, 0)
} }
} }
} }
}; }
yProvider.on("sync", onSync); yProvider.on("sync", onSync)
setProvider(yProvider); setProvider(yProvider)
const binding = new MonacoBinding( const binding = new MonacoBinding(
yText, yText,
model, model,
new Set([editorRef]), new Set([editorRef]),
yProvider.awareness as Awareness yProvider.awareness as Awareness
); )
return () => { return () => {
yDoc.destroy(); yDoc.destroy()
yProvider.destroy(); yProvider.destroy()
binding.destroy(); binding.destroy()
yProvider.off("sync", onSync); yProvider.off("sync", onSync)
}; }
}, [editorRef, room, activeFileContent]); }, [editorRef, room, activeFileContent])
// Connection/disconnection effect // Connection/disconnection effect
useEffect(() => { useEffect(() => {
socket.connect(); socket.connect()
return () => { return () => {
socket.disconnect(); socket.disconnect()
}; }
}, []); }, [])
// Socket event listener effect // Socket event listener effect
useEffect(() => { useEffect(() => {
const onConnect = () => {}; const onConnect = () => {}
const onDisconnect = () => { const onDisconnect = () => {
setTerminals([]); setTerminals([])
}; }
const onLoadedEvent = (files: (TFolder | TFile)[]) => { const onLoadedEvent = (files: (TFolder | TFile)[]) => {
setFiles(files); setFiles(files)
}; }
const onRateLimit = (message: string) => { const onRateLimit = (message: string) => {
toast.error(message); toast.error(message)
}; }
const onTerminalResponse = (response: { id: string; data: string }) => { const onTerminalResponse = (response: { id: string; data: string }) => {
const term = terminals.find((t) => t.id === response.id); const term = terminals.find((t) => t.id === response.id)
if (term && term.terminal) { if (term && term.terminal) {
term.terminal.write(response.data); term.terminal.write(response.data)
} }
}; }
const onDisableAccess = (message: string) => { const onDisableAccess = (message: string) => {
if (!isOwner) if (!isOwner)
setDisableAccess({ setDisableAccess({
isDisabled: true, isDisabled: true,
message, message,
}); })
}; }
socket.on("connect", onConnect); socket.on("connect", onConnect)
socket.on("disconnect", onDisconnect); socket.on("disconnect", onDisconnect)
socket.on("loaded", onLoadedEvent); socket.on("loaded", onLoadedEvent)
socket.on("rateLimit", onRateLimit); socket.on("rateLimit", onRateLimit)
socket.on("terminalResponse", onTerminalResponse); socket.on("terminalResponse", onTerminalResponse)
socket.on("disableAccess", onDisableAccess); socket.on("disableAccess", onDisableAccess)
return () => { return () => {
socket.off("connect", onConnect); socket.off("connect", onConnect)
socket.off("disconnect", onDisconnect); socket.off("disconnect", onDisconnect)
socket.off("loaded", onLoadedEvent); socket.off("loaded", onLoadedEvent)
socket.off("rateLimit", onRateLimit); socket.off("rateLimit", onRateLimit)
socket.off("terminalResponse", onTerminalResponse); socket.off("terminalResponse", onTerminalResponse)
socket.off("disableAccess", onDisableAccess); socket.off("disableAccess", onDisableAccess)
}; }
// }, []); // }, []);
}, [terminals]); }, [terminals])
// Helper functions for tabs: // Helper functions for tabs:
// Select file and load content // Select file and load content
const selectFile = (tab: TTab) => { const selectFile = (tab: TTab) => {
if (tab.id === activeFileId) return; if (tab.id === activeFileId) return
setGenerate((prev) => { setGenerate((prev) => {
return { return {
...prev, ...prev,
show: false, show: false,
}; }
}); })
const exists = tabs.find((t) => t.id === tab.id); const exists = tabs.find((t) => t.id === tab.id)
setTabs((prev) => { setTabs((prev) => {
if (exists) { if (exists) {
setActiveFileId(exists.id); setActiveFileId(exists.id)
return prev; return prev
} }
return [...prev, tab]; return [...prev, tab]
}); })
socket.emit("getFile", tab.id, (response: string) => { socket.emit("getFile", tab.id, (response: string) => {
setActiveFileContent(response); setActiveFileContent(response)
}); })
setEditorLanguage(processFileType(tab.name)); setEditorLanguage(processFileType(tab.name))
setActiveFileId(tab.id); setActiveFileId(tab.id)
}; }
// Close tab and remove from tabs // Close tab and remove from tabs
const closeTab = (id: string) => { const closeTab = (id: string) => {
const numTabs = tabs.length; const numTabs = tabs.length
const index = tabs.findIndex((t) => t.id === id); const index = tabs.findIndex((t) => t.id === id)
console.log("closing tab", id, index); console.log("closing tab", id, index)
if (index === -1) return; if (index === -1) return
const nextId = const nextId =
activeFileId === id activeFileId === id
@ -451,49 +450,49 @@ export default function CodeEditor({
: index < numTabs - 1 : index < numTabs - 1
? tabs[index + 1].id ? tabs[index + 1].id
: tabs[index - 1].id : tabs[index - 1].id
: activeFileId; : activeFileId
setTabs((prev) => prev.filter((t) => t.id !== id)); setTabs((prev) => prev.filter((t) => t.id !== id))
if (!nextId) { if (!nextId) {
setActiveFileId(""); setActiveFileId("")
} else { } else {
const nextTab = tabs.find((t) => t.id === nextId); const nextTab = tabs.find((t) => t.id === nextId)
if (nextTab) { if (nextTab) {
selectFile(nextTab); selectFile(nextTab)
} }
} }
}; }
const closeTabs = (ids: string[]) => { const closeTabs = (ids: string[]) => {
const numTabs = tabs.length; const numTabs = tabs.length
if (numTabs === 0) return; if (numTabs === 0) return
const allIndexes = ids.map((id) => tabs.findIndex((t) => t.id === id)); const allIndexes = ids.map((id) => tabs.findIndex((t) => t.id === id))
const indexes = allIndexes.filter((index) => index !== -1); const indexes = allIndexes.filter((index) => index !== -1)
if (indexes.length === 0) return; if (indexes.length === 0) return
console.log("closing tabs", ids, indexes); console.log("closing tabs", ids, indexes)
const activeIndex = tabs.findIndex((t) => t.id === activeFileId); const activeIndex = tabs.findIndex((t) => t.id === activeFileId)
const newTabs = tabs.filter((t) => !ids.includes(t.id)); const newTabs = tabs.filter((t) => !ids.includes(t.id))
setTabs(newTabs); setTabs(newTabs)
if (indexes.length === numTabs) { if (indexes.length === numTabs) {
setActiveFileId(""); setActiveFileId("")
} else { } else {
const nextTab = const nextTab =
newTabs.length > activeIndex newTabs.length > activeIndex
? newTabs[activeIndex] ? newTabs[activeIndex]
: newTabs[newTabs.length - 1]; : newTabs[newTabs.length - 1]
if (nextTab) { if (nextTab) {
selectFile(nextTab); selectFile(nextTab)
} }
} }
}; }
const handleRename = ( const handleRename = (
id: string, id: string,
@ -501,40 +500,40 @@ export default function CodeEditor({
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => { ) => {
const valid = validateName(newName, oldName, type); const valid = validateName(newName, oldName, type)
if (!valid.status) { if (!valid.status) {
if (valid.message) toast.error("Invalid file name."); if (valid.message) toast.error("Invalid file name.")
return false; return false
} }
socket.emit("renameFile", id, newName); socket.emit("renameFile", id, newName)
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab)) prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
); )
return true; return true
}; }
const handleDeleteFile = (file: TFile) => { const handleDeleteFile = (file: TFile) => {
socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => { socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
setFiles(response); setFiles(response)
}); })
closeTab(file.id); closeTab(file.id)
}; }
const handleDeleteFolder = (folder: TFolder) => { const handleDeleteFolder = (folder: TFolder) => {
setDeletingFolderId(folder.id); setDeletingFolderId(folder.id)
console.log("deleting folder", folder.id); console.log("deleting folder", folder.id)
socket.emit("getFolder", folder.id, (response: string[]) => socket.emit("getFolder", folder.id, (response: string[]) =>
closeTabs(response) closeTabs(response)
); )
socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => { socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
setFiles(response); setFiles(response)
setDeletingFolderId(""); setDeletingFolderId("")
}); })
}; }
// On disabled access for shared users, show un-interactable loading placeholder + info modal // On disabled access for shared users, show un-interactable loading placeholder + info modal
if (disableAccess.isDisabled) if (disableAccess.isDisabled)
@ -547,7 +546,7 @@ export default function CodeEditor({
/> />
<Loading /> <Loading />
</> </>
); )
return ( return (
<> <>
@ -569,41 +568,41 @@ export default function CodeEditor({
}} }}
onExpand={() => { onExpand={() => {
editorRef?.changeViewZones(function (changeAccessor) { editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id); changeAccessor.removeZone(generate.id)
if (!generateRef.current) return; if (!generateRef.current) return
const id = changeAccessor.addZone({ const id = changeAccessor.addZone({
afterLineNumber: cursorLine, afterLineNumber: cursorLine,
heightInLines: 12, heightInLines: 12,
domNode: generateRef.current, domNode: generateRef.current,
}); })
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id }; return { ...prev, id }
}); })
}); })
}} }}
onAccept={(code: string) => { onAccept={(code: string) => {
const line = generate.line; const line = generate.line
setGenerate((prev) => { setGenerate((prev) => {
return { return {
...prev, ...prev,
show: !prev.show, show: !prev.show,
}; }
}); })
const file = editorRef?.getValue(); const file = editorRef?.getValue()
const lines = file?.split("\n") || []; const lines = file?.split("\n") || []
lines.splice(line - 1, 0, code); lines.splice(line - 1, 0, code)
const updatedFile = lines.join("\n"); const updatedFile = lines.join("\n")
editorRef?.setValue(updatedFile); editorRef?.setValue(updatedFile)
}} }}
onClose={() => { onClose={() => {
setGenerate((prev) => { setGenerate((prev) => {
return { return {
...prev, ...prev,
show: !prev.show, show: !prev.show,
}; }
}); })
}} }}
/> />
) : null} ) : null}
@ -643,7 +642,7 @@ export default function CodeEditor({
saved={tab.saved} saved={tab.saved}
selected={activeFileId === tab.id} selected={activeFileId === tab.id}
onClick={(e) => { onClick={(e) => {
selectFile(tab); selectFile(tab)
}} }}
onClose={() => closeTab(tab.id)} onClose={() => closeTab(tab.id)}
> >
@ -680,7 +679,7 @@ export default function CodeEditor({
? { ...tab, saved: true } ? { ...tab, saved: true }
: tab : tab
) )
); )
} else { } else {
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
@ -688,7 +687,7 @@ export default function CodeEditor({
? { ...tab, saved: false } ? { ...tab, saved: false }
: tab : tab
) )
); )
} }
}} }}
options={{ options={{
@ -732,8 +731,8 @@ export default function CodeEditor({
<PreviewWindow <PreviewWindow
collapsed={isPreviewCollapsed} collapsed={isPreviewCollapsed}
open={() => { open={() => {
previewPanelRef.current?.expand(); previewPanelRef.current?.expand()
setIsPreviewCollapsed(false); setIsPreviewCollapsed(false)
}} }}
ip={ip} ip={ip}
/> />
@ -761,5 +760,5 @@ export default function CodeEditor({
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
</> </>
); )
} }

View File

@ -1,13 +1,13 @@
"use client"; "use client"
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react"
import { Button } from "../ui/button"; import { Button } from "../ui/button"
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"; import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client"
import { Editor } from "@monaco-editor/react"; import { Editor } from "@monaco-editor/react"
import { User } from "@/lib/types"; import { User } from "@/lib/types"
import { toast } from "sonner"; import { toast } from "sonner"
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation"
// import monaco from "monaco-editor" // import monaco from "monaco-editor"
export default function GenerateInput({ export default function GenerateInput({
@ -20,54 +20,54 @@ export default function GenerateInput({
onAccept, onAccept,
onClose, onClose,
}: { }: {
user: User; user: User
socket: Socket; socket: Socket
width: number; width: number
data: { data: {
fileName: string; fileName: string
code: string; code: string
line: number; line: number
}; }
editor: { editor: {
language: string; language: string
}; }
onExpand: () => void; onExpand: () => void
onAccept: (code: string) => void; onAccept: (code: string) => void
onClose: () => void; onClose: () => void
}) { }) {
const pathname = usePathname(); const pathname = usePathname()
const router = useRouter(); const router = useRouter()
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null)
const [code, setCode] = useState(""); const [code, setCode] = useState("")
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false)
const [loading, setLoading] = useState({ const [loading, setLoading] = useState({
generate: false, generate: false,
regenerate: false, regenerate: false,
}); })
const [input, setInput] = useState(""); const [input, setInput] = useState("")
const [currentPrompt, setCurrentPrompt] = useState(""); const [currentPrompt, setCurrentPrompt] = useState("")
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
inputRef.current?.focus(); inputRef.current?.focus()
}, 100); }, 100)
}, [inputRef.current]); }, [inputRef.current])
const handleGenerate = async ({ const handleGenerate = async ({
regenerate = false, regenerate = false,
}: { }: {
regenerate?: boolean; regenerate?: boolean
}) => { }) => {
if (user.generations >= 10) { if (user.generations >= 10) {
toast.error( toast.error(
"You reached the maximum # of generations. Contact @ishaandey_ on X/Twitter to reset :)" "You reached the maximum # of generations. Contact @ishaandey_ on X/Twitter to reset :)"
); )
return; return
} }
setLoading({ generate: !regenerate, regenerate }); setLoading({ generate: !regenerate, regenerate })
setCurrentPrompt(input); setCurrentPrompt(input)
socket.emit( socket.emit(
"generateCode", "generateCode",
data.fileName, data.fileName,
@ -75,25 +75,25 @@ export default function GenerateInput({
data.line, data.line,
regenerate ? currentPrompt : input, regenerate ? currentPrompt : input,
(res: { response: string; success: boolean }) => { (res: { response: string; success: boolean }) => {
console.log("Generated code", res.response, res.success); console.log("Generated code", res.response, res.success)
// if (!res.success) { // if (!res.success) {
// toast.error("Failed to generate code."); // toast.error("Failed to generate code.");
// return; // return;
// } // }
setCode(res.response); setCode(res.response)
router.refresh(); router.refresh()
} }
); )
}; }
useEffect(() => { useEffect(() => {
if (code) { if (code) {
setExpanded(true); setExpanded(true)
onExpand(); onExpand()
setLoading({ generate: false, regenerate: false }); setLoading({ generate: false, regenerate: false })
} }
}, [code]); }, [code])
return ( return (
<div className="w-full pr-4 space-y-2"> <div className="w-full pr-4 space-y-2">
@ -105,7 +105,7 @@ export default function GenerateInput({
}} }}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
placeholder="Generate code with a prompt" placeholder="Generate code with a prompt"
className="h-8 w-full rounded-md border border-muted-foreground bg-transparent px-3 py-1 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" className="h-8 w-full rounded-md border border-muted-foreground bg-transparent px-3 py-1 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
/> />
@ -192,5 +192,5 @@ export default function GenerateInput({
</> </>
) : null} ) : null}
</div> </div>
); )
} }

View File

@ -1,68 +1,51 @@
"use client"; "use client"
import dynamic from "next/dynamic"; import dynamic from "next/dynamic"
import Loading from "@/components/editor/loading"; import Loading from "@/components/editor/loading"
import { Sandbox, User } from "@/lib/types"; import { Sandbox, User } from "@/lib/types"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react"
import { toast } from "sonner"; import { toast } from "sonner"
import { getTaskIp, startServer } from "@/lib/actions"; import { getTaskIp, startServer } from "@/lib/actions"
import { checkServiceStatus } from "@/lib/utils"; import { checkServiceStatus, setupServer } from "@/lib/utils"
const CodeEditor = dynamic(() => import("@/components/editor/editor"), { const CodeEditor = dynamic(() => import("@/components/editor/editor"), {
ssr: false, ssr: false,
loading: () => <Loading />, loading: () => <Loading />,
}); })
export default function Editor({ export default function Editor({
isOwner,
userData, userData,
sandboxData, sandboxData,
}: { }: {
isOwner: boolean; userData: User
userData: User; sandboxData: Sandbox
sandboxData: Sandbox;
}) { }) {
const [isServiceRunning, setIsServiceRunning] = useState(false); const isDev = process.env.VERCEL_ENV === "development"
const [isDeploymentActive, setIsDeploymentActive] = useState(false);
const [taskIp, setTaskIp] = useState<string>(); const [isServiceRunning, setIsServiceRunning] = useState(false)
const [didFail, setDidFail] = useState(false); const [isDeploymentActive, setIsDeploymentActive] = useState(false)
const [taskIp, setTaskIp] = useState<string>()
const [didFail, setDidFail] = useState(false)
useEffect(() => { useEffect(() => {
if (!isOwner) { if (isDev) {
toast.error("You are not the owner of this sandbox. (TEMPORARY)"); setIsServiceRunning(true)
setDidFail(true); setIsDeploymentActive(true)
return; setTaskIp("localhost")
return
} }
startServer(sandboxData.id).then((response) => { setupServer({
if (!response.success) { sandboxId: sandboxData.id,
toast.error(response.message); setIsServiceRunning,
setDidFail(true); setIsDeploymentActive,
} else { setTaskIp,
setIsServiceRunning(true); setDidFail,
toast,
})
}, [])
checkServiceStatus(sandboxData.id) if (didFail) return <Loading didFail={didFail} />
.then(() => {
setIsDeploymentActive(true);
getTaskIp(sandboxData.id)
.then((ip) => {
setTaskIp(ip);
})
.catch(() => {
setDidFail(true);
toast.error("An error occurred while getting your server IP.");
});
})
.catch(() => {
toast.error("An error occurred while initializing your server.");
setDidFail(true);
});
}
});
}, []);
if (didFail) return <Loading didFail={didFail} />;
if (!isServiceRunning || !isDeploymentActive || !taskIp) if (!isServiceRunning || !isDeploymentActive || !taskIp)
return ( return (
<Loading <Loading
@ -75,9 +58,9 @@ export default function Editor({
: "Requesting your server creation..." : "Requesting your server creation..."
} }
/> />
); )
return ( return (
<CodeEditor ip={taskIp} userData={userData} sandboxData={sandboxData} /> <CodeEditor ip={taskIp} userData={userData} sandboxData={sandboxData} />
); )
} }

View File

@ -1,21 +1,21 @@
"use server"; "use server"
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache"
import ecsClient, { ec2Client } from "./ecs"; import ecsClient, { ec2Client } from "./ecs"
import { import {
CreateServiceCommand, CreateServiceCommand,
DescribeClustersCommand, DescribeClustersCommand,
DescribeServicesCommand, DescribeServicesCommand,
DescribeTasksCommand, DescribeTasksCommand,
ListTasksCommand, ListTasksCommand,
} from "@aws-sdk/client-ecs"; } from "@aws-sdk/client-ecs"
import { DescribeNetworkInterfacesCommand } from "@aws-sdk/client-ec2"; import { DescribeNetworkInterfacesCommand } from "@aws-sdk/client-ec2"
export async function createSandbox(body: { export async function createSandbox(body: {
type: string; type: string
name: string; name: string
userId: string; userId: string
visibility: string; visibility: string
}) { }) {
const res = await fetch( const res = await fetch(
"https://database.ishaan1013.workers.dev/api/sandbox", "https://database.ishaan1013.workers.dev/api/sandbox",
@ -26,15 +26,15 @@ export async function createSandbox(body: {
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
} }
); )
return await res.text(); return await res.text()
} }
export async function updateSandbox(body: { export async function updateSandbox(body: {
id: string; id: string
name?: string; name?: string
visibility?: "public" | "private"; visibility?: "public" | "private"
}) { }) {
await fetch("https://database.ishaan1013.workers.dev/api/sandbox", { await fetch("https://database.ishaan1013.workers.dev/api/sandbox", {
method: "POST", method: "POST",
@ -42,17 +42,17 @@ export async function updateSandbox(body: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}); })
revalidatePath("/dashboard"); revalidatePath("/dashboard")
} }
export async function deleteSandbox(id: string) { export async function deleteSandbox(id: string) {
await fetch(`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`, { await fetch(`https://database.ishaan1013.workers.dev/api/sandbox?id=${id}`, {
method: "DELETE", method: "DELETE",
}); })
revalidatePath("/dashboard"); revalidatePath("/dashboard")
} }
export async function shareSandbox(sandboxId: string, email: string) { export async function shareSandbox(sandboxId: string, email: string) {
@ -65,15 +65,15 @@ export async function shareSandbox(sandboxId: string, email: string) {
}, },
body: JSON.stringify({ sandboxId, email }), body: JSON.stringify({ sandboxId, email }),
} }
); )
const text = await res.text(); const text = await res.text()
if (res.status !== 200) { if (res.status !== 200) {
return { success: false, message: text }; return { success: false, message: text }
} }
revalidatePath(`/code/${sandboxId}`); revalidatePath(`/code/${sandboxId}`)
return { success: true, message: "Shared successfully." }; return { success: true, message: "Shared successfully." }
} }
export async function unshareSandbox(sandboxId: string, userId: string) { export async function unshareSandbox(sandboxId: string, userId: string) {
@ -83,102 +83,102 @@ export async function unshareSandbox(sandboxId: string, userId: string) {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ sandboxId, userId }), body: JSON.stringify({ sandboxId, userId }),
}); })
revalidatePath(`/code/${sandboxId}`); revalidatePath(`/code/${sandboxId}`)
} }
export async function describeService(serviceName: string) { export async function describeService(serviceName: string) {
const command = new DescribeServicesCommand({ const command = new DescribeServicesCommand({
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!, cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
services: [serviceName], services: [serviceName],
}); })
return await ecsClient.send(command); return await ecsClient.send(command)
} }
export async function getTaskIp(serviceName: string) { export async function getTaskIp(serviceName: string) {
const listCommand = new ListTasksCommand({ const listCommand = new ListTasksCommand({
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!, cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
serviceName, serviceName,
}); })
const listResponse = await ecsClient.send(listCommand); const listResponse = await ecsClient.send(listCommand)
const taskArns = listResponse.taskArns; const taskArns = listResponse.taskArns
const describeCommand = new DescribeTasksCommand({ const describeCommand = new DescribeTasksCommand({
cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!, cluster: process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!,
tasks: taskArns, tasks: taskArns,
}); })
const describeResponse = await ecsClient.send(describeCommand); const describeResponse = await ecsClient.send(describeCommand)
const tasks = describeResponse.tasks; const tasks = describeResponse.tasks
const taskAttachment = tasks?.[0].attachments?.[0].details; const taskAttachment = tasks?.[0].attachments?.[0].details
if (!taskAttachment) { if (!taskAttachment) {
throw new Error("Task attachment not found"); throw new Error("Task attachment not found")
} }
const eni = taskAttachment.find( const eni = taskAttachment.find(
(detail) => detail.name === "networkInterfaceId" (detail) => detail.name === "networkInterfaceId"
)?.value; )?.value
if (!eni) { if (!eni) {
throw new Error("Network interface not found"); throw new Error("Network interface not found")
} }
const describeNetworkInterfacesCommand = new DescribeNetworkInterfacesCommand( const describeNetworkInterfacesCommand = new DescribeNetworkInterfacesCommand(
{ {
NetworkInterfaceIds: [eni], NetworkInterfaceIds: [eni],
} }
); )
const describeNetworkInterfacesResponse = await ec2Client.send( const describeNetworkInterfacesResponse = await ec2Client.send(
describeNetworkInterfacesCommand describeNetworkInterfacesCommand
); )
const ip = const ip =
describeNetworkInterfacesResponse.NetworkInterfaces?.[0].Association describeNetworkInterfacesResponse.NetworkInterfaces?.[0].Association
?.PublicIp; ?.PublicIp
if (!ip) { if (!ip) {
throw new Error("Public IP not found"); throw new Error("Public IP not found")
} }
return ip; return ip
} }
async function doesServiceExist(serviceName: string) { export async function doesServiceExist(serviceName: string) {
const response = await describeService(serviceName); const response = await describeService(serviceName)
const activeServices = response.services?.filter( const activeServices = response.services?.filter(
(service) => service.status === "ACTIVE" (service) => service.status === "ACTIVE"
); )
console.log("activeServices: ", activeServices); console.log("activeServices: ", activeServices)
return activeServices?.length === 1; return activeServices?.length === 1
} }
async function countServices() { async function countServices() {
const command = new DescribeClustersCommand({ const command = new DescribeClustersCommand({
clusters: [process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!], clusters: [process.env.NEXT_PUBLIC_AWS_ECS_CLUSTER!],
}); })
const response = await ecsClient.send(command); const response = await ecsClient.send(command)
return response.clusters?.[0].activeServicesCount!; return response.clusters?.[0].activeServicesCount!
} }
export async function startServer( export async function startServer(
serviceName: string serviceName: string
): Promise<{ success: boolean; message: string }> { ): Promise<{ success: boolean; message: string }> {
const serviceExists = await doesServiceExist(serviceName); const serviceExists = await doesServiceExist(serviceName)
if (serviceExists) { if (serviceExists) {
return { success: true, message: "" }; return { success: true, message: "" }
} }
const activeServices = await countServices(); const activeServices = await countServices()
if (activeServices >= 100) { if (activeServices >= 100) {
return { return {
success: false, success: false,
message: message:
"Too many servers are running! Please try again later or contact @ishaandey_ on Twitter/X.", "Too many servers are running! Please try again later or contact @ishaandey_ on Twitter/X.",
}; }
} }
const command = new CreateServiceCommand({ const command = new CreateServiceCommand({
@ -201,13 +201,13 @@ export async function startServer(
assignPublicIp: "ENABLED", assignPublicIp: "ENABLED",
}, },
}, },
}); })
try { try {
const response = await ecsClient.send(command); const response = await ecsClient.send(command)
console.log("started server:", response.service?.serviceName); console.log("started server:", response.service?.serviceName)
return { success: true, message: "" }; return { success: true, message: "" }
// store in workers kv: // store in workers kv:
// { // {
@ -223,6 +223,6 @@ export async function startServer(
return { return {
success: false, success: false,
message: `Error starting server: ${error.message}. Try again in a minute, or contact @ishaandey_ on Twitter/X if it still doesn't work.`, message: `Error starting server: ${error.message}. Try again in a minute, or contact @ishaandey_ on Twitter/X if it still doesn't work.`,
}; }
} }
} }

View File

@ -1,22 +1,27 @@
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from "clsx"
// import { toast } from "sonner" // import { toast } from "sonner"
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge"
import { Sandbox, TFile, TFolder } from "./types"; import { Sandbox, TFile, TFolder } from "./types"
import { Service } from "@aws-sdk/client-ecs"; import { Service } from "@aws-sdk/client-ecs"
import { describeService } from "./actions"; import {
describeService,
doesServiceExist,
getTaskIp,
startServer,
} from "./actions"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs))
} }
export function processFileType(file: string) { export function processFileType(file: string) {
const ending = file.split(".").pop(); const ending = file.split(".").pop()
if (ending === "ts" || ending === "tsx") return "typescript"; if (ending === "ts" || ending === "tsx") return "typescript"
if (ending === "js" || ending === "jsx") return "javascript"; if (ending === "js" || ending === "jsx") return "javascript"
if (ending) return ending; if (ending) return ending
return "plaintext"; return "plaintext"
} }
export function validateName( export function validateName(
@ -25,7 +30,7 @@ export function validateName(
type: "file" | "folder" type: "file" | "folder"
) { ) {
if (newName === oldName || newName.length === 0) { if (newName === oldName || newName.length === 0) {
return { status: false, message: "" }; return { status: false, message: "" }
} }
if ( if (
newName.includes("/") || newName.includes("/") ||
@ -34,9 +39,9 @@ export function validateName(
(type === "file" && !newName.includes(".")) || (type === "file" && !newName.includes(".")) ||
(type === "folder" && newName.includes(".")) (type === "folder" && newName.includes("."))
) { ) {
return { status: false, message: "Invalid file name." }; return { status: false, message: "Invalid file name." }
} }
return { status: true, message: "" }; return { status: true, message: "" }
} }
export function addNew( export function addNew(
@ -49,9 +54,9 @@ export function addNew(
setFiles((prev) => [ setFiles((prev) => [
...prev, ...prev,
{ id: `projects/${sandboxData.id}/${name}`, name, type: "file" }, { id: `projects/${sandboxData.id}/${name}`, name, type: "file" },
]); ])
} else { } else {
console.log("adding folder"); console.log("adding folder")
setFiles((prev) => [ setFiles((prev) => [
...prev, ...prev,
{ {
@ -60,48 +65,92 @@ export function addNew(
type: "folder", type: "folder",
children: [], children: [],
}, },
]); ])
} }
} }
export function checkServiceStatus(serviceName: string): Promise<Service> { export function checkServiceStatus(serviceName: string): Promise<Service> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let tries = 0; let tries = 0
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
tries++; tries++
if (tries > 40) { if (tries > 40) {
clearInterval(interval); clearInterval(interval)
reject(new Error("Timed out.")); reject(new Error("Timed out."))
} }
const response = await describeService(serviceName); const response = await describeService(serviceName)
const activeServices = response.services?.filter( const activeServices = response.services?.filter(
(service) => service.status === "ACTIVE" (service) => service.status === "ACTIVE"
); )
console.log("Checking activeServices status", activeServices); console.log("Checking activeServices status", activeServices)
if (activeServices?.length === 1) { if (activeServices?.length === 1) {
const service = activeServices?.[0]; const service = activeServices?.[0]
if ( if (
service.runningCount === service.desiredCount && service.runningCount === service.desiredCount &&
service.deployments?.length === 1 service.deployments?.length === 1
) { ) {
if (service.deployments[0].rolloutState === "COMPLETED") { if (service.deployments[0].rolloutState === "COMPLETED") {
clearInterval(interval); clearInterval(interval)
resolve(service); resolve(service)
} else if (service.deployments[0].rolloutState === "FAILED") { } else if (service.deployments[0].rolloutState === "FAILED") {
clearInterval(interval); clearInterval(interval)
reject(new Error("Deployment failed.")); reject(new Error("Deployment failed."))
} }
} }
} }
} catch (error: any) { } catch (error: any) {
clearInterval(interval); clearInterval(interval)
reject(error); reject(error)
} }
}, 3000); }, 3000)
}); })
}
export async function setupServer({
sandboxId,
setIsServiceRunning,
setIsDeploymentActive,
setTaskIp,
setDidFail,
toast,
}: {
sandboxId: string
setIsServiceRunning: React.Dispatch<React.SetStateAction<boolean>>
setIsDeploymentActive: React.Dispatch<React.SetStateAction<boolean>>
setTaskIp: React.Dispatch<React.SetStateAction<string | undefined>>
setDidFail: React.Dispatch<React.SetStateAction<boolean>>
toast: any
}) {
const doesExist = await doesServiceExist(sandboxId)
if (!doesExist) {
const response = await startServer(sandboxId)
if (!response.success) {
toast.error(response.message)
setDidFail(true)
return
}
}
setIsServiceRunning(true)
try {
if (!doesExist) {
await checkServiceStatus(sandboxId)
}
setIsDeploymentActive(true)
const taskIp = await getTaskIp(sandboxId)
setTaskIp(taskIp)
} catch (error) {
toast.error("An error occurred while initializing your server.")
setDidFail(true)
}
} }