diff --git a/frontend/components/RijuEditor.js b/frontend/components/RijuEditor.js index b0f5467..72b2ecd 100644 --- a/frontend/components/RijuEditor.js +++ b/frontend/components/RijuEditor.js @@ -9,6 +9,7 @@ import React from "react"; import { createMessageConnection } from "vscode-jsonrpc"; import RijuMessageReader from "../services/RijuMessageReader"; import RijuMessageWriter from "../services/RijuMessageWriter"; +import { SocketManager } from "../services/WS"; import { EventEmitter } from "../utils/EventEmitter"; let clientDisposable = null; @@ -20,11 +21,11 @@ const RijuEditor = (props) => { const monacoRef = React.useRef(); React.useEffect(() => { - EventEmitter.subscribe("lspStarted", (data) => { - const { message, socket } = data; - initLSP(socket, message, monacoRef.current, editorRef.current); + const token1 = EventEmitter.subscribe("lspStarted", (data) => { + const { message } = data; + initLSP(message, monacoRef.current, editorRef.current); }); - EventEmitter.subscribe("lspStopped", () => { + const token2 = EventEmitter.subscribe("lspStopped", () => { if (clientDisposable) { clientDisposable.dispose(); clientDisposable = null; @@ -34,9 +35,11 @@ const RijuEditor = (props) => { servicesDisposable = null; } }); + () => EventEmitter.unsubcribe(token1, token2); }, []); - const initLSP = (socket, message, monaco, editor) => { + const initLSP = (message, monaco, editor) => { + const socket = SocketManager.socket; const services = MonacoServices.create(editor, { rootUri: `file://${message.root}`, }); diff --git a/frontend/components/RijuTerminal.js b/frontend/components/RijuTerminal.js index afbda0e..2726947 100644 --- a/frontend/components/RijuTerminal.js +++ b/frontend/components/RijuTerminal.js @@ -19,11 +19,11 @@ function RijuTerminal() { term.write("Connecting to server..."); window.addEventListener("resize", () => fitAddon.fit()); - EventEmitter.subscribe("resize", () => { + const token1 = EventEmitter.subscribe("resize", () => { const event = new Event("resize"); window.dispatchEvent(event); }); - EventEmitter.subscribe("terminal", (payload) => { + const token2 = EventEmitter.subscribe("terminal", (payload) => { if (!payload) return; const { type, data } = payload; switch (type) { @@ -41,6 +41,8 @@ function RijuTerminal() { term.onData((data) => { EventEmitter.dispatch("send", { event: "terminalInput", input: data }); }); + + () => EventEmitter.unsubcribe(token1, token2); }, []); return ( diff --git a/frontend/pages/editor/[lang].js b/frontend/pages/editor/[lang].js index b32b04f..29a77f2 100644 --- a/frontend/pages/editor/[lang].js +++ b/frontend/pages/editor/[lang].js @@ -24,6 +24,7 @@ import React, { useEffect, useRef, useState } from "react"; import Layouts from "../../components/Layouts"; import langs from "../../assets/langs.json"; import { EventEmitter } from "../../utils/EventEmitter"; +import { SocketManager } from "../../services/WS"; ansi.rgb = { green: "#00FD61", }; @@ -42,7 +43,7 @@ const CodeRunner = (props) => { const router = useRouter(); const { langConfig } = props; const editorRef = useRef(null); - const [config, setConfig] = useState(langConfig); + const [config] = useState(langConfig); const [mounted, setMounted] = useState(false); const [isRunning, setRunning] = useState(false); const [isFormatting, setFormatting] = useState(false); @@ -56,179 +57,162 @@ const CodeRunner = (props) => { EventEmitter.dispatch("terminal", { type, data }); } - function connect() { - serviceLogBuffers = {}; - serviceLogLines = {}; - setStatus("connecting"); - const socket = new WebSocket( - // (document.location.protocol === "http:" ? "ws://" : "wss://") + - "wss://" + - "riju.codes" + - `/api/v1/ws?lang=${encodeURIComponent(config.id)}` - ); - socket.addEventListener("open", () => { - console.log("Successfully connected to server playground"); - setStatus("connected"); - }); - EventEmitter.subscribe("send", (payload) => { - if (DEBUG) { - console.log("SEND:", payload); - } - if (socket) { - socket.send(JSON.stringify(payload)); - } - }); - socket.addEventListener("message", async (event) => { - let message; - try { - message = JSON.parse(event.data); - } catch (err) { - console.error("Malformed message from server:", event.data); + const handleWsOpen = (event) => { + setStatus("connected"); + }; + + const handleWsMessage = (event) => { + let message; + try { + message = JSON.parse(event.data); + } catch (err) { + console.error("Malformed message from server:", event.data); + return; + } + if ( + DEBUG && + message && + message.event !== "lspOutput" && + message.event !== "serviceLog" + ) { + console.log("RECEIVE:", message); + } + if (message && message.event && message.event !== "error") { + // retryDelayMs = initialRetryDelayMs; + } + switch (message && message.event) { + case "terminalClear": + // term.reset(); + sendToTerminal("terminalClear"); return; - } - if ( - DEBUG && - message && - message.event !== "lspOutput" && - message.event !== "serviceLog" - ) { - console.log("RECEIVE:", message); - } - if (message && message.event && message.event !== "error") { - // retryDelayMs = initialRetryDelayMs; - } - switch (message && message.event) { - case "terminalClear": - // term.reset(); - sendToTerminal("terminalClear"); - return; - case "terminalOutput": - if (typeof message.output !== "string") { - console.error("Unexpected message from server:", message); - return; - } - sendToTerminal("terminalOutput", ansi.white(message.output)); - setRunning(false); - return; - case "formattedCode": - setFormatting(false); - if ( - typeof message.code !== "string" || - typeof message.originalCode !== "string" - ) { - console.error("Unexpected message from server:", message); - return; - } - if (editorRef.current?.getValue() === message.originalCode) { - editorRef.current?.setValue(message.code); - } - return; - case "lspStopped": - setIsLspRequested(false); - setLspStarted(false); - EventEmitter.dispatch("lspStopped"); - break; - case "lspStarted": - setLspStarted(true); - setIsLspRequested(false); - if (typeof message.root !== "string") { - console.error("Unexpected message from server:", message); - return; - } - - EventEmitter.dispatch("lspStarted", { message, socket }); - - return; - case "lspOutput": - // Should be handled by RijuMessageReader - return; - case "serviceLog": - if ( - typeof message.service !== "string" || - typeof message.output !== "string" - ) { - console.error("Unexpected message from server:", message); - return; - } - let buffer = serviceLogBuffers[message.service] || ""; - let lines = serviceLogLines[message.service] || []; - buffer += message.output; - while (buffer.includes("\n")) { - const idx = buffer.indexOf("\n"); - const line = buffer.slice(0, idx); - buffer = buffer.slice(idx + 1); - lines.push(line); - if (DEBUG) { - console.log(`${message.service.toUpperCase()} || ${line}`); - } - } - serviceLogBuffers[message.service] = buffer; - serviceLogLines[message.service] = lines; - return; - case "serviceFailed": - if ( - typeof message.service !== "string" || - typeof message.error !== "string" - ) { - console.error("Unexpected message from server:", message); - return; - } - switch (message.service) { - case "formatter": - setFormatting(false); - // showError({ - // message: "Could not prettify code!", - // data: serviceLogLines["formatter"].join("\n"), - // }); - break; - case "lsp": - setLspStarted(false); - setIsLspRequested(false); - EventEmitter.dispatch("lspStopped"); - break; - case "terminal": - sendToTerminal( - "terminalOutput", - ansi.red(`\r\n[${message.error}]`) - ); - break; - } - return; - case "langConfig": - console.log("Lang Config", message); - // We could use this message instead of hardcoding the - // language config into the HTML page returned from the - // server, but for now we just ignore it. - return; - default: + case "terminalOutput": + if (typeof message.output !== "string") { console.error("Unexpected message from server:", message); - } - }); - socket.addEventListener("close", (event) => { - if (event.wasClean) { - console.log("Connection closed cleanly"); - } else { - console.error("Connection died"); - } - EventEmitter.dispatch("lspStopped"); - setRunning(false); - setLspStarted(false); - setIsLspRequested(false); - setStatus("idle"); - }); + return; + } + sendToTerminal("terminalOutput", ansi.white(message.output)); + setRunning(false); + return; + case "formattedCode": + setFormatting(false); + if ( + typeof message.code !== "string" || + typeof message.originalCode !== "string" + ) { + console.error("Unexpected message from server:", message); + return; + } + if (editorRef.current?.getValue() === message.originalCode) { + editorRef.current?.setValue(message.code); + } + return; + case "lspStopped": + setIsLspRequested(false); + setLspStarted(false); + EventEmitter.dispatch("lspStopped"); + break; + case "lspStarted": + setLspStarted(true); + setIsLspRequested(false); + if (typeof message.root !== "string") { + console.error("Unexpected message from server:", message); + return; + } - return socket; - } + EventEmitter.dispatch("lspStarted", { message }); + + return; + case "lspOutput": + // Should be handled by RijuMessageReader + return; + case "serviceLog": + if ( + typeof message.service !== "string" || + typeof message.output !== "string" + ) { + console.error("Unexpected message from server:", message); + return; + } + let buffer = serviceLogBuffers[message.service] || ""; + let lines = serviceLogLines[message.service] || []; + buffer += message.output; + while (buffer.includes("\n")) { + const idx = buffer.indexOf("\n"); + const line = buffer.slice(0, idx); + buffer = buffer.slice(idx + 1); + lines.push(line); + if (DEBUG) { + console.log(`${message.service.toUpperCase()} || ${line}`); + } + } + serviceLogBuffers[message.service] = buffer; + serviceLogLines[message.service] = lines; + return; + case "serviceFailed": + if ( + typeof message.service !== "string" || + typeof message.error !== "string" + ) { + console.error("Unexpected message from server:", message); + return; + } + switch (message.service) { + case "formatter": + setFormatting(false); + // showError({ + // message: "Could not prettify code!", + // data: serviceLogLines["formatter"].join("\n"), + // }); + break; + case "lsp": + setLspStarted(false); + setIsLspRequested(false); + EventEmitter.dispatch("lspStopped"); + break; + case "terminal": + sendToTerminal( + "terminalOutput", + ansi.red(`\r\n[${message.error}]`) + ); + break; + } + return; + case "langConfig": + console.log("Lang Config", message); + // We could use this message instead of hardcoding the + // language config into the HTML page returned from the + // server, but for now we just ignore it. + return; + default: + console.error("Unexpected message from server:", message); + } + }; + + const handleWsClose = (event) => { + if (event.wasClean) { + console.log("Connection closed cleanly"); + } else { + console.error("Connection died"); + } + EventEmitter.dispatch("lspStopped"); + setRunning(false); + setLspStarted(false); + setIsLspRequested(false); + setStatus("idle"); + }; useEffect(() => { if (!config || !mounted) return; - const socket = connect(); - return () => socket && socket.close(); + serviceLogBuffers = {}; + serviceLogLines = {}; + setStatus("connecting"); + SocketManager.connect(config, handleWsOpen, handleWsMessage, handleWsClose); + return () => SocketManager.disconnect(); }, [config, mounted]); function showValue() { setRunning(true); - EventEmitter.dispatch("send", { + SocketManager.send({ event: "runCode", code: editorRef.current.getValue(), }); @@ -238,7 +222,7 @@ const CodeRunner = (props) => { setFormatting(true); serviceLogBuffers["formatter"] = ""; serviceLogLines["formatter"] = []; - EventEmitter.dispatch("send", { + SocketManager.send({ event: "formatCode", code: editorRef.current.getValue(), }); @@ -261,21 +245,28 @@ const CodeRunner = (props) => { const handleLspClick = () => { setIsLspRequested(true); if (isLspStarted) { - EventEmitter.dispatch("send", { + SocketManager.send({ event: "lspStop", }); } else { - EventEmitter.dispatch("send", { + SocketManager.send({ event: "lspStart", }); } }; const handleChange = () => { - if (status != "idle") { + if (SocketManager.isConnected) { return; } else { - connect(); + if (!SocketManager.isConnected) { + SocketManager.connect( + config, + handleWsOpen, + handleWsMessage, + handleWsClose + ); + } } }; @@ -285,7 +276,6 @@ const CodeRunner = (props) => { const es = document.querySelectorAll(".split .panel"); for (const e of es) { e.removeAttribute("style"); - e.removeAttribute("style"); } setSplitType(value); }; diff --git a/frontend/services/WS.js b/frontend/services/WS.js new file mode 100644 index 0000000..1f10cec --- /dev/null +++ b/frontend/services/WS.js @@ -0,0 +1,46 @@ +const isFn = (callback) => { + return callback && typeof callback == "function"; +}; + +const createSocket = (url) => { + const socket = new WebSocket(url); + return socket; +}; + +export const SocketManager = { + socket: null, + isConnected: false, + connect: function (config, onOpen, onMessage, onClose) { + let url = + "wss://" + + "riju.codes" + + `/api/v1/ws?lang=${encodeURIComponent(config.id)}`; + this.socket = createSocket(url); + this.socket.addEventListener("open", () => { + console.log("Successfully connected to server playground"); + this.isConnected = true; + if (isFn(onOpen)) onOpen(); + }); + this.socket.addEventListener("message", async (event) => { + if (isFn(onMessage)) onMessage(event); + }); + this.socket.addEventListener("close", (event) => { + if (isFn(onClose)) onClose(event); + this.isConnected = false; + }); + }, + disconnect: function () { + if (this.socket) { + if (this.socket.readyState == WebSocket.OPEN) { + this.socket.close(); + } + } + }, + send: function (data) { + if (this.socket) { + if (this.socket.readyState == WebSocket.OPEN) { + this.socket.send(JSON.stringify(data)); + } + } + }, +}; diff --git a/frontend/utils/EventEmitter.js b/frontend/utils/EventEmitter.js index da4b88b..3749d61 100644 --- a/frontend/utils/EventEmitter.js +++ b/frontend/utils/EventEmitter.js @@ -1,18 +1,25 @@ export const EventEmitter = { events: {}, + lastUid: -1, dispatch: function (event, data) { if (!this.events[event]) return; - this.events[event].forEach((callback) => callback(data)); + for (let token of Object.keys(this.events[event])) { + const callback = this.events[event][token]; + if (callback && typeof callback == "function") callback(data); + } }, subscribe: function (event, callback) { - if (!this.events[event]) this.events[event] = []; - this.events[event].push(callback); + if (!callback) return; + if (!this.events[event]) this.events[event] = {}; + const token = "uID_" + String(++this.lastUid); + this.events[event][token] = callback; }, - isSubscribed: function (event) { - if (!Array.isArray(this.events[event])) return false; - else { - if (this.events[event].length == 0) return false; - else return true; + unsubcribe: function (...tokens) { + for (let k of Object.keys(events)) { + let toks = Object.keys(events[k]); + for (let tok of toks) { + if (tokens.includes(tok)) delete events[k][tok]; + } } }, };