Refactored the Socket connection and callbacks to work properly

This commit is contained in:
inaseem 2021-11-02 15:34:10 +05:30
parent 4068822489
commit dbf6763a99
5 changed files with 233 additions and 185 deletions

View File

@ -9,6 +9,7 @@ import React from "react";
import { createMessageConnection } from "vscode-jsonrpc"; import { createMessageConnection } from "vscode-jsonrpc";
import RijuMessageReader from "../services/RijuMessageReader"; import RijuMessageReader from "../services/RijuMessageReader";
import RijuMessageWriter from "../services/RijuMessageWriter"; import RijuMessageWriter from "../services/RijuMessageWriter";
import { SocketManager } from "../services/WS";
import { EventEmitter } from "../utils/EventEmitter"; import { EventEmitter } from "../utils/EventEmitter";
let clientDisposable = null; let clientDisposable = null;
@ -20,11 +21,11 @@ const RijuEditor = (props) => {
const monacoRef = React.useRef(); const monacoRef = React.useRef();
React.useEffect(() => { React.useEffect(() => {
EventEmitter.subscribe("lspStarted", (data) => { const token1 = EventEmitter.subscribe("lspStarted", (data) => {
const { message, socket } = data; const { message } = data;
initLSP(socket, message, monacoRef.current, editorRef.current); initLSP(message, monacoRef.current, editorRef.current);
}); });
EventEmitter.subscribe("lspStopped", () => { const token2 = EventEmitter.subscribe("lspStopped", () => {
if (clientDisposable) { if (clientDisposable) {
clientDisposable.dispose(); clientDisposable.dispose();
clientDisposable = null; clientDisposable = null;
@ -34,9 +35,11 @@ const RijuEditor = (props) => {
servicesDisposable = null; 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, { const services = MonacoServices.create(editor, {
rootUri: `file://${message.root}`, rootUri: `file://${message.root}`,
}); });

View File

@ -19,11 +19,11 @@ function RijuTerminal() {
term.write("Connecting to server..."); term.write("Connecting to server...");
window.addEventListener("resize", () => fitAddon.fit()); window.addEventListener("resize", () => fitAddon.fit());
EventEmitter.subscribe("resize", () => { const token1 = EventEmitter.subscribe("resize", () => {
const event = new Event("resize"); const event = new Event("resize");
window.dispatchEvent(event); window.dispatchEvent(event);
}); });
EventEmitter.subscribe("terminal", (payload) => { const token2 = EventEmitter.subscribe("terminal", (payload) => {
if (!payload) return; if (!payload) return;
const { type, data } = payload; const { type, data } = payload;
switch (type) { switch (type) {
@ -41,6 +41,8 @@ function RijuTerminal() {
term.onData((data) => { term.onData((data) => {
EventEmitter.dispatch("send", { event: "terminalInput", input: data }); EventEmitter.dispatch("send", { event: "terminalInput", input: data });
}); });
() => EventEmitter.unsubcribe(token1, token2);
}, []); }, []);
return ( return (

View File

@ -24,6 +24,7 @@ import React, { useEffect, useRef, useState } from "react";
import Layouts from "../../components/Layouts"; import Layouts from "../../components/Layouts";
import langs from "../../assets/langs.json"; import langs from "../../assets/langs.json";
import { EventEmitter } from "../../utils/EventEmitter"; import { EventEmitter } from "../../utils/EventEmitter";
import { SocketManager } from "../../services/WS";
ansi.rgb = { ansi.rgb = {
green: "#00FD61", green: "#00FD61",
}; };
@ -42,7 +43,7 @@ const CodeRunner = (props) => {
const router = useRouter(); const router = useRouter();
const { langConfig } = props; const { langConfig } = props;
const editorRef = useRef(null); const editorRef = useRef(null);
const [config, setConfig] = useState(langConfig); const [config] = useState(langConfig);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [isRunning, setRunning] = useState(false); const [isRunning, setRunning] = useState(false);
const [isFormatting, setFormatting] = useState(false); const [isFormatting, setFormatting] = useState(false);
@ -56,179 +57,162 @@ const CodeRunner = (props) => {
EventEmitter.dispatch("terminal", { type, data }); EventEmitter.dispatch("terminal", { type, data });
} }
function connect() { const handleWsOpen = (event) => {
serviceLogBuffers = {}; setStatus("connected");
serviceLogLines = {}; };
setStatus("connecting");
const socket = new WebSocket( const handleWsMessage = (event) => {
// (document.location.protocol === "http:" ? "ws://" : "wss://") + let message;
"wss://" + try {
"riju.codes" + message = JSON.parse(event.data);
`/api/v1/ws?lang=${encodeURIComponent(config.id)}` } catch (err) {
); console.error("Malformed message from server:", event.data);
socket.addEventListener("open", () => { return;
console.log("Successfully connected to server playground"); }
setStatus("connected"); if (
}); DEBUG &&
EventEmitter.subscribe("send", (payload) => { message &&
if (DEBUG) { message.event !== "lspOutput" &&
console.log("SEND:", payload); message.event !== "serviceLog"
} ) {
if (socket) { console.log("RECEIVE:", message);
socket.send(JSON.stringify(payload)); }
} if (message && message.event && message.event !== "error") {
}); // retryDelayMs = initialRetryDelayMs;
socket.addEventListener("message", async (event) => { }
let message; switch (message && message.event) {
try { case "terminalClear":
message = JSON.parse(event.data); // term.reset();
} catch (err) { sendToTerminal("terminalClear");
console.error("Malformed message from server:", event.data);
return; return;
} case "terminalOutput":
if ( if (typeof message.output !== "string") {
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:
console.error("Unexpected message from server:", message); console.error("Unexpected message from server:", message);
} return;
}); }
socket.addEventListener("close", (event) => { sendToTerminal("terminalOutput", ansi.white(message.output));
if (event.wasClean) { setRunning(false);
console.log("Connection closed cleanly"); return;
} else { case "formattedCode":
console.error("Connection died"); setFormatting(false);
} if (
EventEmitter.dispatch("lspStopped"); typeof message.code !== "string" ||
setRunning(false); typeof message.originalCode !== "string"
setLspStarted(false); ) {
setIsLspRequested(false); console.error("Unexpected message from server:", message);
setStatus("idle"); 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(() => { useEffect(() => {
if (!config || !mounted) return; if (!config || !mounted) return;
const socket = connect(); serviceLogBuffers = {};
return () => socket && socket.close(); serviceLogLines = {};
setStatus("connecting");
SocketManager.connect(config, handleWsOpen, handleWsMessage, handleWsClose);
return () => SocketManager.disconnect();
}, [config, mounted]); }, [config, mounted]);
function showValue() { function showValue() {
setRunning(true); setRunning(true);
EventEmitter.dispatch("send", { SocketManager.send({
event: "runCode", event: "runCode",
code: editorRef.current.getValue(), code: editorRef.current.getValue(),
}); });
@ -238,7 +222,7 @@ const CodeRunner = (props) => {
setFormatting(true); setFormatting(true);
serviceLogBuffers["formatter"] = ""; serviceLogBuffers["formatter"] = "";
serviceLogLines["formatter"] = []; serviceLogLines["formatter"] = [];
EventEmitter.dispatch("send", { SocketManager.send({
event: "formatCode", event: "formatCode",
code: editorRef.current.getValue(), code: editorRef.current.getValue(),
}); });
@ -261,21 +245,28 @@ const CodeRunner = (props) => {
const handleLspClick = () => { const handleLspClick = () => {
setIsLspRequested(true); setIsLspRequested(true);
if (isLspStarted) { if (isLspStarted) {
EventEmitter.dispatch("send", { SocketManager.send({
event: "lspStop", event: "lspStop",
}); });
} else { } else {
EventEmitter.dispatch("send", { SocketManager.send({
event: "lspStart", event: "lspStart",
}); });
} }
}; };
const handleChange = () => { const handleChange = () => {
if (status != "idle") { if (SocketManager.isConnected) {
return; return;
} else { } else {
connect(); if (!SocketManager.isConnected) {
SocketManager.connect(
config,
handleWsOpen,
handleWsMessage,
handleWsClose
);
}
} }
}; };
@ -285,7 +276,6 @@ const CodeRunner = (props) => {
const es = document.querySelectorAll(".split .panel"); const es = document.querySelectorAll(".split .panel");
for (const e of es) { for (const e of es) {
e.removeAttribute("style"); e.removeAttribute("style");
e.removeAttribute("style");
} }
setSplitType(value); setSplitType(value);
}; };

46
frontend/services/WS.js Normal file
View File

@ -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));
}
}
},
};

View File

@ -1,18 +1,25 @@
export const EventEmitter = { export const EventEmitter = {
events: {}, events: {},
lastUid: -1,
dispatch: function (event, data) { dispatch: function (event, data) {
if (!this.events[event]) return; 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) { subscribe: function (event, callback) {
if (!this.events[event]) this.events[event] = []; if (!callback) return;
this.events[event].push(callback); if (!this.events[event]) this.events[event] = {};
const token = "uID_" + String(++this.lastUid);
this.events[event][token] = callback;
}, },
isSubscribed: function (event) { unsubcribe: function (...tokens) {
if (!Array.isArray(this.events[event])) return false; for (let k of Object.keys(events)) {
else { let toks = Object.keys(events[k]);
if (this.events[event].length == 0) return false; for (let tok of toks) {
else return true; if (tokens.includes(tok)) delete events[k][tok];
}
} }
}, },
}; };