From d20f621b4797751c768069433fb89a6cb6a90295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Hoekstra?= Date: Sat, 30 Jul 2022 16:19:22 +0200 Subject: [PATCH 1/7] Add RFA backend --- src/RemoteFileAPI/MessageDefinitions.ts | 65 ++++++++++++ src/RemoteFileAPI/MessageHandlers.ts | 129 ++++++++++++++++++++++++ src/RemoteFileAPI/RFALogger.ts | 27 +++++ src/RemoteFileAPI/Remote.ts | 65 ++++++++++++ src/RemoteFileAPI/RemoteFileAPI.ts | 16 +++ src/index.tsx | 6 ++ 6 files changed, 308 insertions(+) create mode 100644 src/RemoteFileAPI/MessageDefinitions.ts create mode 100644 src/RemoteFileAPI/MessageHandlers.ts create mode 100644 src/RemoteFileAPI/RFALogger.ts create mode 100644 src/RemoteFileAPI/Remote.ts create mode 100644 src/RemoteFileAPI/RemoteFileAPI.ts diff --git a/src/RemoteFileAPI/MessageDefinitions.ts b/src/RemoteFileAPI/MessageDefinitions.ts new file mode 100644 index 000000000..15ec5740b --- /dev/null +++ b/src/RemoteFileAPI/MessageDefinitions.ts @@ -0,0 +1,65 @@ +export class RFAMessage { + jsonrpc = "2.0"; // Transmits version of JSON-RPC. Compliance maybe allows some funky interaction with external tools? + public method?:string; // Is defined when it's a request/notification, otherwise undefined + public result?:string; // Is defined when it's a response, otherwise undefined + public params?:FileMetadata; // Optional parameters to method + public error? :string; // Only defined on error + public id? :number; // ID to keep track of request -> response interaction, undefined with notifications, defined with request/response + + constructor(obj: { method?: string; result?: string; params?: FileMetadata; error?: string; id?: number } = {}){ + this.method = obj.method; + this.result = obj.result; + this.params = obj.params; + this.error = obj.error; + this.id = obj.id; + } +} + +type FileMetadata = FileData + | FileContent + | FileLocation + | FileServer; + +export interface FileData { + filename: string; + content: string; + server: string; +} + +export interface FileContent { + filename: string; + content: string; +} + +export interface FileLocation { + filename: string; + server: string; +} + +export interface FileServer { + server: string; +} + +export function isFileData(p: unknown): p is FileData { + const pf = p as FileData + return typeof pf.server === 'string' && + typeof pf.filename === 'string' && + typeof pf.content === 'string' +} + +export function isFileLocation(p: unknown): p is FileLocation { + const pf = p as FileLocation + return typeof pf.server === 'string' && + typeof pf.filename === 'string' +} + +export function isFileContent(p: unknown): p is FileContent { + const pf = p as FileContent + return typeof pf.filename === 'string' && + typeof pf.content === 'string' +} + +export function isFileServer(p: unknown): p is FileServer { + const pf = p as FileServer + return typeof pf.server === 'string' +} diff --git a/src/RemoteFileAPI/MessageHandlers.ts b/src/RemoteFileAPI/MessageHandlers.ts new file mode 100644 index 000000000..0075052ad --- /dev/null +++ b/src/RemoteFileAPI/MessageHandlers.ts @@ -0,0 +1,129 @@ +import { Player } from "../Player"; +import { isScriptFilename } from "../Script/isScriptFilename"; +import { GetServer } from "../Server/AllServers"; +import { isValidFilePath } from "../Terminal/DirectoryHelpers"; +import { TextFile } from "../TextFile"; +import { RFAMessage, FileData, FileContent, isFileServer, isFileLocation, FileLocation, isFileData } from "./MessageDefinitions"; +//@ts-ignore: Complaint of import ending with .d.ts +import libSource from "!!raw-loader!../ScriptEditor/NetscriptDefinitions.d.ts"; + + +function error(errorMsg: string, { id }: RFAMessage): RFAMessage { + console.error("[RFA-ERROR]" + (typeof (id) === "undefined" ? "" : `Request ${id}: `) + errorMsg); + return new RFAMessage({ error: errorMsg, id: id }); +} + +export const RFARequestHandler: Record void | RFAMessage> = { + + pushFile: function (msg: RFAMessage): RFAMessage { + if (!isFileData(msg.params)) return error("pushFile message misses parameters", msg); + + const fileData: FileData = msg.params; + if (!isValidFilePath(fileData.filename)) return error("pushFile with an invalid filename", msg); + + const server = GetServer(fileData.server); + if (server == null) return error("Server hostname invalid", msg); + + if (isScriptFilename(fileData.filename)) + server.writeToScriptFile(Player, fileData.filename, fileData.content); + else // Assume it's a text file + server.writeToTextFile(fileData.filename, fileData.content); + + // If and only if the content is actually changed correctly, send back an OK. + const savedCorrectly = server.getScript(fileData.filename)?.code === fileData.content + || server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0)?.text === fileData.content; + + if (!savedCorrectly) return error("File wasn't saved correctly", msg); + + return new RFAMessage({ result: "OK", id: msg.id }); + }, + + getFile: function (msg: RFAMessage): RFAMessage { + if (!isFileLocation(msg.params)) return error("getFile message misses parameters", msg); + + const fileData: FileLocation = msg.params; + if (!isValidFilePath(fileData.filename)) return error("getFile with an invalid filename", msg); + + const server = GetServer(fileData.server); + if (server == null) return error("Server hostname invalid", msg); + + if (isScriptFilename(fileData.filename)) { + const scriptContent = server.getScript(fileData.filename); + if (!scriptContent) return error("Requested script doesn't exist", msg); + return new RFAMessage({ result: scriptContent.code, id: msg.id }); + } + else { // Assume it's a text file + const file = server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0); + if (file === undefined) return error("Requested textfile doesn't exist", msg); + return new RFAMessage({ result: file.text, id: msg.id }); + } + }, + + deleteFile: function (msg: RFAMessage): RFAMessage { + if (!isFileLocation(msg.params)) return error("deleteFile message misses parameters", msg); + const fileData: FileLocation = msg.params; + if (!isValidFilePath(fileData.filename)) return error("deleteFile with an invalid filename", msg); + + const server = GetServer(fileData.server); + if (server == null) return error("Server hostname invalid", msg); + + const fileExists = (): boolean => !!server.getScript(fileData.filename) + || server.textFiles.some((t: TextFile) => t.filename === fileData.filename); + + if (!fileExists()) return error("deleteFile file doesn't exist", msg); + server.removeFile(fileData.filename); + if (fileExists()) return error("deleteFile failed to delete the file", msg); + + return new RFAMessage({ result: "OK", id: msg.id }); + }, + + getFileNames: function (msg: RFAMessage): RFAMessage { + if (!isFileServer(msg.params)) return error("getFileNames message misses parameters", msg); + + const server = GetServer(msg.params.server); + if (server == null) return error("Server hostname invalid", msg); + + const fileList: FileContent[] = [ + ...server.textFiles.map((txt): FileContent => { return { filename: txt.filename, content: txt.text } }), + ...server.scripts.map((scr): FileContent => { return { filename: scr.filename, content: scr.code } }) + ]; + + return new RFAMessage({ result: JSON.stringify(fileList), id: msg.id }); + }, + + getAllFiles: function (msg: RFAMessage): RFAMessage { + if (!isFileServer(msg.params)) return error("getAllFiles message misses parameters", msg); + + const server = GetServer(msg.params.server); + if (server == null) return error("Server hostname invalid", msg); + + const fileNameList: string[] = [ + ...server.textFiles.map((txt): string => txt.filename), + ...server.scripts.map((scr): string => scr.filename) + ]; + + return new RFAMessage({ result: JSON.stringify(fileNameList), id: msg.id }); + }, + + calculateRam: function (msg: RFAMessage): RFAMessage { + if (!isFileLocation(msg.params)) return error("calculateRam message misses parameters", msg); + const fileData: FileLocation = msg.params; + if (!isValidFilePath(fileData.filename)) return error("deleteFile with an invalid filename", msg); + + const server = GetServer(fileData.server); + if (server == null) return error("Server hostname invalid", msg); + + if (!isScriptFilename(fileData.filename)) return error("Filename isn't a script filename", msg); + const script = server.getScript(fileData.filename); + if (!script) return error("File doesn't exist", msg); + const ramUsage = script.ramUsage; + + return new RFAMessage({result: String(ramUsage), id: msg.id}); + }, + + getDefinitionFile: function (msg: RFAMessage): RFAMessage { + const source = (libSource + "").replace(/export /g, ""); + console.log(source); + return new RFAMessage({result: source, id: msg.id}); + } +} diff --git a/src/RemoteFileAPI/RFALogger.ts b/src/RemoteFileAPI/RFALogger.ts new file mode 100644 index 000000000..6a1d0f277 --- /dev/null +++ b/src/RemoteFileAPI/RFALogger.ts @@ -0,0 +1,27 @@ +class RemoteFileAPILogger { + _enabled = true; + _prefix = "[RFA]"; + _error_prefix = "[RFA-ERROR]"; + + constructor(enabled: boolean){ + this._enabled = enabled; + } + + public error(...message: any[]) : void { + if (this._enabled) console.error(this._error_prefix, ...message); + } + + public log(...message: any[]) : void { + if (this._enabled) console.log(this._prefix, ...message); + } + + public disable() : void { + this._enabled = false; + } + + public enable() : void { + this._enabled = true; + } +} + +export const RFALogger = new RemoteFileAPILogger(true); diff --git a/src/RemoteFileAPI/Remote.ts b/src/RemoteFileAPI/Remote.ts new file mode 100644 index 000000000..3cb689201 --- /dev/null +++ b/src/RemoteFileAPI/Remote.ts @@ -0,0 +1,65 @@ +import { RFALogger } from "./RFALogger"; +import { RFAMessage } from "./MessageDefinitions" +import { RFARequestHandler } from "./MessageHandlers"; + +export class Remote { + connection? : WebSocket; + protocol = "ws"; + ipaddr: string; + port: number; + + constructor(ip : string, port : number) { + this.ipaddr = ip; + this.port = port; + } + + public startConnection() : void { + this.startConnectionRecurse(1, 5); + } + + handleCloseEvent():void { + delete this.connection; + RFALogger.log("Connection closed."); + } + + startConnectionRecurse(retryN : number, retryMax : number) : void { + RFALogger.log("Trying to connect."); + this.connection = new WebSocket(this.protocol + "://" + this.ipaddr + ":" + this.port); + + this.connection.addEventListener("error", (e:Event) => { + if(!this.connection) return; + + // When having trouble connecting, try again. + if (this.connection.readyState === 3) { + RFALogger.log(`Connection lost, retrying (try #${retryN}).`); + if (retryN <= retryMax) this.startConnectionRecurse(retryN + 1, retryMax); + return; + } + + // Else handle the error normally + RFALogger.error(e); + }); + + this.connection.addEventListener("message", handleMessageEvent); + this.connection.addEventListener("open", () => RFALogger.log("Connection established: ", this.ipaddr, ":", this.port)); + this.connection.addEventListener("close", this.handleCloseEvent); + } +} + +function handleMessageEvent(e: MessageEvent):void { + const msg : RFAMessage = JSON.parse(e.data); + RFALogger.log("Message received:", msg); + + //TODO: Needs more safety, send error on invalid input, send error when msg.method isn´t in handlers, ... + + if (msg.method) + { + const response = RFARequestHandler[msg.method](msg); + RFALogger.log("Sending response: ", response); + if(response) this.send(JSON.stringify(response)); + } + + else if (msg.result) RFALogger.error("Result handling not implemented yet."); + else if (msg.error) RFALogger.error("Received an error from server", msg); + else RFALogger.error("Incorrect Message", msg); +} diff --git a/src/RemoteFileAPI/RemoteFileAPI.ts b/src/RemoteFileAPI/RemoteFileAPI.ts new file mode 100644 index 000000000..2bd0c894b --- /dev/null +++ b/src/RemoteFileAPI/RemoteFileAPI.ts @@ -0,0 +1,16 @@ +import { Remote } from "./Remote"; + +class RemoteFileAPI { + server : Remote; + + constructor(){ + this.server = new Remote("localhost", 12525); + return; + } + + enable() : void { + this.server.startConnection(); + } +} + +export const RFA = new RemoteFileAPI; diff --git a/src/index.tsx b/src/index.tsx index 3720f3eca..661d784b1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,9 @@ import ReactDOM from "react-dom"; import { TTheme as Theme, ThemeEvents, refreshTheme } from "./Themes/ui/Theme"; import { LoadingScreen } from "./ui/LoadingScreen"; import { initElectron } from "./Electron"; + +import { RFA } from "./RemoteFileAPI/RemoteFileAPI"; + initElectron(); globalThis["React"] = React; globalThis["ReactDOM"] = ReactDOM; @@ -14,6 +17,9 @@ ReactDOM.render( document.getElementById("root"), ); +console.log("[RFA] Fix this hack ASAP"); +RFA.enable(); + function rerender(): void { refreshTheme(); ReactDOM.render( From 5fc67c328b7e28ac0c05a6b1dc1b5d6aebb94506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Hoekstra?= Date: Sun, 31 Jul 2022 19:53:15 +0200 Subject: [PATCH 2/7] Add setting UI for port/reconnect, swap wrong API handlers --- src/GameOptions/ui/ConnectionBauble.tsx | 46 +++++++++++++++++++++++ src/GameOptions/ui/CurrentOptionsPage.tsx | 40 +++++++++++++++++++- src/RemoteFileAPI/MessageHandlers.ts | 30 +++++++-------- src/RemoteFileAPI/Remote.ts | 30 +++------------ src/RemoteFileAPI/RemoteFileAPI.ts | 21 ++++++----- src/Settings/Settings.ts | 7 ++++ src/index.tsx | 5 +-- 7 files changed, 127 insertions(+), 52 deletions(-) create mode 100644 src/GameOptions/ui/ConnectionBauble.tsx diff --git a/src/GameOptions/ui/ConnectionBauble.tsx b/src/GameOptions/ui/ConnectionBauble.tsx new file mode 100644 index 000000000..a7e6cd4f6 --- /dev/null +++ b/src/GameOptions/ui/ConnectionBauble.tsx @@ -0,0 +1,46 @@ +import React from "react"; + +interface baubleState { + connection: boolean; + callback: () => boolean; +} + +interface baubleProps { + callback: () => boolean; +} + +export class ConnectionBauble extends React.Component { + timerID: NodeJS.Timer; + state: baubleState; + + constructor(props: baubleProps) { + super(props); + this.state = { + connection: false, + callback: props.callback + }; + } + + componentDidMount() : void { + this.timerID = setInterval( + () => this.tick(), + 1000 + ); + } + + componentWillUnmount() : void { + clearInterval(this.timerID); + } + + tick() : void { + this.setState({ + connection: this.state.callback() + }); + } + + render() : string { + return ( + this.state.connection? "Connected" : "Disconnected" + ); + } +} diff --git a/src/GameOptions/ui/CurrentOptionsPage.tsx b/src/GameOptions/ui/CurrentOptionsPage.tsx index 639ba21a8..f5831ed02 100644 --- a/src/GameOptions/ui/CurrentOptionsPage.tsx +++ b/src/GameOptions/ui/CurrentOptionsPage.tsx @@ -1,12 +1,15 @@ -import { MenuItem, Select, SelectChangeEvent, TextField, Tooltip, Typography } from "@mui/material"; +import { MenuItem, Select, SelectChangeEvent, TextField, Tooltip, Typography, Box } from "@mui/material"; import React, { useState } from "react"; import { IPlayer } from "../../PersonObjects/IPlayer"; +import { isRemoteFileApiConnectionLive, newRemoteFileApiConnection } from "../../RemoteFileAPI/RemoteFileAPI"; import { Settings } from "../../Settings/Settings"; import { OptionSwitch } from "../../ui/React/OptionSwitch"; import { formatTime } from "../../utils/helpers/formatTime"; import { GameOptionsTab } from "../GameOptionsTab"; import { GameOptionsPage } from "./GameOptionsPage"; import { OptionsSlider } from "./OptionsSlider"; +import Button from "@mui/material/Button"; +import { ConnectionBauble } from "./ConnectionBauble"; interface IProps { currentTab: GameOptionsTab; @@ -21,6 +24,7 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => { const [terminalSize, setTerminalSize] = useState(Settings.MaxTerminalCapacity); const [autosaveInterval, setAutosaveInterval] = useState(Settings.AutosaveInterval); const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat); + const [remoteFileApiPort, setRemoteFileApiPort] = useState(Settings.RemoteFileApiPort); const [locale, setLocale] = useState(Settings.Locale); function handleExecTimeChange( @@ -81,6 +85,11 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => { Settings.TimestampsFormat = event.target.value; } + function handleRemoteFileApiPortChange(event: React.ChangeEvent): void { + setRemoteFileApiPort(Number(event.target.value) as number); + Settings.RemoteFileApiPort = Number(event.target.value); + } + const pages = { [GameOptionsTab.SYSTEM]: ( @@ -361,6 +370,35 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => { } /> + + This port number is used to connect to a Remote File API port, + please ensure that it matches with the port the Remote File API server is publishing on (12525 by default). + Click the reconnect button to try and re-establish connection. + The little colored bauble shows whether the connection is live or not. + + }> + 0 && remoteFileApiPort <= 65535? "success" : "error"} + > + Remote File API port: + + ), + endAdornment: ( + + + + + ), + }} + value={remoteFileApiPort} + onChange={handleRemoteFileApiPortChange} + placeholder="12525" + /> + ), }; diff --git a/src/RemoteFileAPI/MessageHandlers.ts b/src/RemoteFileAPI/MessageHandlers.ts index 0075052ad..14860269b 100644 --- a/src/RemoteFileAPI/MessageHandlers.ts +++ b/src/RemoteFileAPI/MessageHandlers.ts @@ -77,26 +77,12 @@ export const RFARequestHandler: Record void | R return new RFAMessage({ result: "OK", id: msg.id }); }, - getFileNames: function (msg: RFAMessage): RFAMessage { + getFileNames: function (msg: RFAMessage): RFAMessage { if (!isFileServer(msg.params)) return error("getFileNames message misses parameters", msg); const server = GetServer(msg.params.server); if (server == null) return error("Server hostname invalid", msg); - const fileList: FileContent[] = [ - ...server.textFiles.map((txt): FileContent => { return { filename: txt.filename, content: txt.text } }), - ...server.scripts.map((scr): FileContent => { return { filename: scr.filename, content: scr.code } }) - ]; - - return new RFAMessage({ result: JSON.stringify(fileList), id: msg.id }); - }, - - getAllFiles: function (msg: RFAMessage): RFAMessage { - if (!isFileServer(msg.params)) return error("getAllFiles message misses parameters", msg); - - const server = GetServer(msg.params.server); - if (server == null) return error("Server hostname invalid", msg); - const fileNameList: string[] = [ ...server.textFiles.map((txt): string => txt.filename), ...server.scripts.map((scr): string => scr.filename) @@ -105,6 +91,20 @@ export const RFARequestHandler: Record void | R return new RFAMessage({ result: JSON.stringify(fileNameList), id: msg.id }); }, + getAllFiles: function (msg: RFAMessage): RFAMessage { + if (!isFileServer(msg.params)) return error("getAllFiles message misses parameters", msg); + + const server = GetServer(msg.params.server); + if (server == null) return error("Server hostname invalid", msg); + + const fileList: FileContent[] = [ + ...server.textFiles.map((txt): FileContent => { return { filename: txt.filename, content: txt.text } }), + ...server.scripts.map((scr): FileContent => { return { filename: scr.filename, content: scr.code } }) + ]; + + return new RFAMessage({ result: JSON.stringify(fileList), id: msg.id }); + }, + calculateRam: function (msg: RFAMessage): RFAMessage { if (!isFileLocation(msg.params)) return error("calculateRam message misses parameters", msg); const fileData: FileLocation = msg.params; diff --git a/src/RemoteFileAPI/Remote.ts b/src/RemoteFileAPI/Remote.ts index 3cb689201..311eb8a0b 100644 --- a/src/RemoteFileAPI/Remote.ts +++ b/src/RemoteFileAPI/Remote.ts @@ -13,36 +13,18 @@ export class Remote { this.port = port; } + public stopConnection() : void { + this.connection?.close(); + } + public startConnection() : void { - this.startConnectionRecurse(1, 5); - } - - handleCloseEvent():void { - delete this.connection; - RFALogger.log("Connection closed."); - } - - startConnectionRecurse(retryN : number, retryMax : number) : void { RFALogger.log("Trying to connect."); this.connection = new WebSocket(this.protocol + "://" + this.ipaddr + ":" + this.port); - this.connection.addEventListener("error", (e:Event) => { - if(!this.connection) return; - - // When having trouble connecting, try again. - if (this.connection.readyState === 3) { - RFALogger.log(`Connection lost, retrying (try #${retryN}).`); - if (retryN <= retryMax) this.startConnectionRecurse(retryN + 1, retryMax); - return; - } - - // Else handle the error normally - RFALogger.error(e); - }); - + this.connection.addEventListener("error", (e:Event) => RFALogger.error(e)); this.connection.addEventListener("message", handleMessageEvent); this.connection.addEventListener("open", () => RFALogger.log("Connection established: ", this.ipaddr, ":", this.port)); - this.connection.addEventListener("close", this.handleCloseEvent); + this.connection.addEventListener("close", () => RFALogger.log("Connection closed")); } } diff --git a/src/RemoteFileAPI/RemoteFileAPI.ts b/src/RemoteFileAPI/RemoteFileAPI.ts index 2bd0c894b..ceda66007 100644 --- a/src/RemoteFileAPI/RemoteFileAPI.ts +++ b/src/RemoteFileAPI/RemoteFileAPI.ts @@ -1,16 +1,19 @@ +import { Settings } from "../Settings/Settings"; import { Remote } from "./Remote"; -class RemoteFileAPI { - server : Remote; - constructor(){ - this.server = new Remote("localhost", 12525); - return; - } +let server: Remote; - enable() : void { - this.server.startConnection(); +export function newRemoteFileApiConnection() : void { + if(server == undefined) + server = new Remote("localhost", Settings.RemoteFileApiPort); + else { + server.stopConnection(); + server = new Remote("localhost", Settings.RemoteFileApiPort); + server.startConnection(); } } -export const RFA = new RemoteFileAPI; +export function isRemoteFileApiConnectionLive() : boolean { + return server.connection != undefined && server.connection.readyState == 1; +} diff --git a/src/Settings/Settings.ts b/src/Settings/Settings.ts index c8d1cfd0c..3160dfa3c 100644 --- a/src/Settings/Settings.ts +++ b/src/Settings/Settings.ts @@ -84,6 +84,11 @@ interface IDefaultSettings { */ MaxTerminalCapacity: number; + /** + * Port the Remote File API client will try to connect to. + */ + RemoteFileApiPort: number; + /** * Save the game when you save any file. */ @@ -206,6 +211,7 @@ export const defaultSettings: IDefaultSettings = { MaxLogCapacity: 50, MaxPortCapacity: 50, MaxTerminalCapacity: 500, + RemoteFileApiPort: 12525, SaveGameOnFileSave: true, SuppressBuyAugmentationConfirmation: false, SuppressFactionInvites: false, @@ -248,6 +254,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = { MaxTerminalCapacity: defaultSettings.MaxTerminalCapacity, OwnedAugmentationsOrder: OwnedAugmentationsOrderSetting.AcquirementTime, PurchaseAugmentationsOrder: PurchaseAugmentationsOrderSetting.Default, + RemoteFileApiPort: defaultSettings.RemoteFileApiPort, SaveGameOnFileSave: defaultSettings.SaveGameOnFileSave, SuppressBuyAugmentationConfirmation: defaultSettings.SuppressBuyAugmentationConfirmation, SuppressFactionInvites: defaultSettings.SuppressFactionInvites, diff --git a/src/index.tsx b/src/index.tsx index 661d784b1..83b2692f6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,7 +5,7 @@ import { TTheme as Theme, ThemeEvents, refreshTheme } from "./Themes/ui/Theme"; import { LoadingScreen } from "./ui/LoadingScreen"; import { initElectron } from "./Electron"; -import { RFA } from "./RemoteFileAPI/RemoteFileAPI"; +import { newRemoteFileApiConnection } from "./RemoteFileAPI/RemoteFileAPI"; initElectron(); globalThis["React"] = React; @@ -17,8 +17,7 @@ ReactDOM.render( document.getElementById("root"), ); -console.log("[RFA] Fix this hack ASAP"); -RFA.enable(); +newRemoteFileApiConnection(); function rerender(): void { refreshTheme(); From 2628dc1ae80cb6a087d9e9271c9866c4600e72fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Hoekstra?= Date: Sun, 31 Jul 2022 20:18:11 +0200 Subject: [PATCH 3/7] Start RFA connection on gamestart, test connection on bauble creation immediately --- src/GameOptions/ui/ConnectionBauble.tsx | 2 +- src/RemoteFileAPI/RemoteFileAPI.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/GameOptions/ui/ConnectionBauble.tsx b/src/GameOptions/ui/ConnectionBauble.tsx index a7e6cd4f6..4e2a94c85 100644 --- a/src/GameOptions/ui/ConnectionBauble.tsx +++ b/src/GameOptions/ui/ConnectionBauble.tsx @@ -16,7 +16,7 @@ export class ConnectionBauble extends React.Component { constructor(props: baubleProps) { super(props); this.state = { - connection: false, + connection: props.callback(), callback: props.callback }; } diff --git a/src/RemoteFileAPI/RemoteFileAPI.ts b/src/RemoteFileAPI/RemoteFileAPI.ts index ceda66007..10934f18b 100644 --- a/src/RemoteFileAPI/RemoteFileAPI.ts +++ b/src/RemoteFileAPI/RemoteFileAPI.ts @@ -5,8 +5,10 @@ import { Remote } from "./Remote"; let server: Remote; export function newRemoteFileApiConnection() : void { - if(server == undefined) + if(server == undefined) { server = new Remote("localhost", Settings.RemoteFileApiPort); + server.startConnection(); + } else { server.stopConnection(); server = new Remote("localhost", Settings.RemoteFileApiPort); From 88d8f13847e7abe38bcdf790943c187a315fa634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Hoekstra?= Date: Wed, 10 Aug 2022 23:22:03 +0200 Subject: [PATCH 4/7] Cleanup + lint/format --- src/GameOptions/ui/ConnectionBauble.tsx | 21 +- src/GameOptions/ui/CurrentOptionsPage.tsx | 27 ++- src/RemoteFileAPI/MessageDefinitions.ts | 67 +++---- src/RemoteFileAPI/MessageHandlers.ts | 223 ++++++++++++---------- src/RemoteFileAPI/RFALogger.ts | 36 ++-- src/RemoteFileAPI/Remote.ts | 72 +++---- src/RemoteFileAPI/RemoteFileAPI.ts | 26 ++- 7 files changed, 235 insertions(+), 237 deletions(-) diff --git a/src/GameOptions/ui/ConnectionBauble.tsx b/src/GameOptions/ui/ConnectionBauble.tsx index 4e2a94c85..971eaffc7 100644 --- a/src/GameOptions/ui/ConnectionBauble.tsx +++ b/src/GameOptions/ui/ConnectionBauble.tsx @@ -17,30 +17,25 @@ export class ConnectionBauble extends React.Component { super(props); this.state = { connection: props.callback(), - callback: props.callback + callback: props.callback, }; } - componentDidMount() : void { - this.timerID = setInterval( - () => this.tick(), - 1000 - ); + componentDidMount(): void { + this.timerID = setInterval(() => this.tick(), 1000); } - componentWillUnmount() : void { + componentWillUnmount(): void { clearInterval(this.timerID); } - tick() : void { + tick(): void { this.setState({ - connection: this.state.callback() + connection: this.state.callback(), }); } - render() : string { - return ( - this.state.connection? "Connected" : "Disconnected" - ); + render(): string { + return this.state.connection ? "Connected" : "Disconnected"; } } diff --git a/src/GameOptions/ui/CurrentOptionsPage.tsx b/src/GameOptions/ui/CurrentOptionsPage.tsx index f5831ed02..7ab6ec88c 100644 --- a/src/GameOptions/ui/CurrentOptionsPage.tsx +++ b/src/GameOptions/ui/CurrentOptionsPage.tsx @@ -370,27 +370,26 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => { } /> - - This port number is used to connect to a Remote File API port, - please ensure that it matches with the port the Remote File API server is publishing on (12525 by default). - Click the reconnect button to try and re-establish connection. - The little colored bauble shows whether the connection is live or not. - - }> - + This port number is used to connect to a Remote File API port, please ensure that it matches with the port + the Remote File API server is publishing on (12525 by default). Click the reconnect button to try and + re-establish connection. The little colored bauble shows whether the connection is live or not. + + } + > + 0 && remoteFileApiPort <= 65535? "success" : "error"} - > + 0 && remoteFileApiPort <= 65535 ? "success" : "error"}> Remote File API port: ), endAdornment: ( - + ), }} @@ -398,7 +397,7 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => { onChange={handleRemoteFileApiPortChange} placeholder="12525" /> - + ), }; diff --git a/src/RemoteFileAPI/MessageDefinitions.ts b/src/RemoteFileAPI/MessageDefinitions.ts index 15ec5740b..f113e1a2c 100644 --- a/src/RemoteFileAPI/MessageDefinitions.ts +++ b/src/RemoteFileAPI/MessageDefinitions.ts @@ -1,65 +1,58 @@ export class RFAMessage { - jsonrpc = "2.0"; // Transmits version of JSON-RPC. Compliance maybe allows some funky interaction with external tools? - public method?:string; // Is defined when it's a request/notification, otherwise undefined - public result?:string; // Is defined when it's a response, otherwise undefined - public params?:FileMetadata; // Optional parameters to method - public error? :string; // Only defined on error - public id? :number; // ID to keep track of request -> response interaction, undefined with notifications, defined with request/response + jsonrpc = "2.0"; // Transmits version of JSON-RPC. Compliance maybe allows some funky interaction with external tools? + public method?: string; // Is defined when it's a request/notification, otherwise undefined + public result?: string; // Is defined when it's a response, otherwise undefined + public params?: FileMetadata; // Optional parameters to method + public error?: string; // Only defined on error + public id?: number; // ID to keep track of request -> response interaction, undefined with notifications, defined with request/response - constructor(obj: { method?: string; result?: string; params?: FileMetadata; error?: string; id?: number } = {}){ - this.method = obj.method; - this.result = obj.result; - this.params = obj.params; - this.error = obj.error; - this.id = obj.id; - } + constructor(obj: { method?: string; result?: string; params?: FileMetadata; error?: string; id?: number } = {}) { + this.method = obj.method; + this.result = obj.result; + this.params = obj.params; + this.error = obj.error; + this.id = obj.id; + } } -type FileMetadata = FileData - | FileContent - | FileLocation - | FileServer; +type FileMetadata = FileData | FileContent | FileLocation | FileServer; export interface FileData { - filename: string; - content: string; - server: string; + filename: string; + content: string; + server: string; } export interface FileContent { - filename: string; - content: string; + filename: string; + content: string; } export interface FileLocation { - filename: string; - server: string; + filename: string; + server: string; } export interface FileServer { - server: string; + server: string; } export function isFileData(p: unknown): p is FileData { - const pf = p as FileData - return typeof pf.server === 'string' && - typeof pf.filename === 'string' && - typeof pf.content === 'string' + const pf = p as FileData; + return typeof pf.server === "string" && typeof pf.filename === "string" && typeof pf.content === "string"; } export function isFileLocation(p: unknown): p is FileLocation { - const pf = p as FileLocation - return typeof pf.server === 'string' && - typeof pf.filename === 'string' + const pf = p as FileLocation; + return typeof pf.server === "string" && typeof pf.filename === "string"; } export function isFileContent(p: unknown): p is FileContent { - const pf = p as FileContent - return typeof pf.filename === 'string' && - typeof pf.content === 'string' + const pf = p as FileContent; + return typeof pf.filename === "string" && typeof pf.content === "string"; } export function isFileServer(p: unknown): p is FileServer { - const pf = p as FileServer - return typeof pf.server === 'string' + const pf = p as FileServer; + return typeof pf.server === "string"; } diff --git a/src/RemoteFileAPI/MessageHandlers.ts b/src/RemoteFileAPI/MessageHandlers.ts index 14860269b..82297dbc1 100644 --- a/src/RemoteFileAPI/MessageHandlers.ts +++ b/src/RemoteFileAPI/MessageHandlers.ts @@ -3,127 +3,138 @@ import { isScriptFilename } from "../Script/isScriptFilename"; import { GetServer } from "../Server/AllServers"; import { isValidFilePath } from "../Terminal/DirectoryHelpers"; import { TextFile } from "../TextFile"; -import { RFAMessage, FileData, FileContent, isFileServer, isFileLocation, FileLocation, isFileData } from "./MessageDefinitions"; +import { + RFAMessage, + FileData, + FileContent, + isFileServer, + isFileLocation, + FileLocation, + isFileData, +} from "./MessageDefinitions"; //@ts-ignore: Complaint of import ending with .d.ts import libSource from "!!raw-loader!../ScriptEditor/NetscriptDefinitions.d.ts"; - +import { RFALogger } from "./RFALogger"; function error(errorMsg: string, { id }: RFAMessage): RFAMessage { - console.error("[RFA-ERROR]" + (typeof (id) === "undefined" ? "" : `Request ${id}: `) + errorMsg); - return new RFAMessage({ error: errorMsg, id: id }); + RFALogger.error((typeof id === "undefined" ? "" : `Request ${id}: `) + errorMsg); + return new RFAMessage({ error: errorMsg, id: id }); } export const RFARequestHandler: Record void | RFAMessage> = { + pushFile: function (msg: RFAMessage): RFAMessage { + if (!isFileData(msg.params)) return error("Misses parameters", msg); - pushFile: function (msg: RFAMessage): RFAMessage { - if (!isFileData(msg.params)) return error("pushFile message misses parameters", msg); + const fileData: FileData = msg.params; + if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg); - const fileData: FileData = msg.params; - if (!isValidFilePath(fileData.filename)) return error("pushFile with an invalid filename", msg); + const server = GetServer(fileData.server); + if (!server) return error("Server hostname invalid", msg); - const server = GetServer(fileData.server); - if (server == null) return error("Server hostname invalid", msg); + if (isScriptFilename(fileData.filename)) server.writeToScriptFile(Player, fileData.filename, fileData.content); + // Assume it's a text file + else server.writeToTextFile(fileData.filename, fileData.content); - if (isScriptFilename(fileData.filename)) - server.writeToScriptFile(Player, fileData.filename, fileData.content); - else // Assume it's a text file - server.writeToTextFile(fileData.filename, fileData.content); + // If and only if the content is actually changed correctly, send back an OK. + const savedCorrectly = + server.getScript(fileData.filename)?.code === fileData.content || + server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0)?.text === fileData.content; - // If and only if the content is actually changed correctly, send back an OK. - const savedCorrectly = server.getScript(fileData.filename)?.code === fileData.content - || server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0)?.text === fileData.content; + if (!savedCorrectly) return error("File wasn't saved correctly", msg); - if (!savedCorrectly) return error("File wasn't saved correctly", msg); + return new RFAMessage({ result: "OK", id: msg.id }); + }, - return new RFAMessage({ result: "OK", id: msg.id }); - }, + getFile: function (msg: RFAMessage): RFAMessage { + if (!isFileLocation(msg.params)) return error("Message misses parameters", msg); - getFile: function (msg: RFAMessage): RFAMessage { - if (!isFileLocation(msg.params)) return error("getFile message misses parameters", msg); + const fileData: FileLocation = msg.params; + if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg); - const fileData: FileLocation = msg.params; - if (!isValidFilePath(fileData.filename)) return error("getFile with an invalid filename", msg); + const server = GetServer(fileData.server); + if (!server) return error("Server hostname invalid", msg); - const server = GetServer(fileData.server); - if (server == null) return error("Server hostname invalid", msg); - - if (isScriptFilename(fileData.filename)) { - const scriptContent = server.getScript(fileData.filename); - if (!scriptContent) return error("Requested script doesn't exist", msg); - return new RFAMessage({ result: scriptContent.code, id: msg.id }); - } - else { // Assume it's a text file - const file = server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0); - if (file === undefined) return error("Requested textfile doesn't exist", msg); - return new RFAMessage({ result: file.text, id: msg.id }); - } - }, - - deleteFile: function (msg: RFAMessage): RFAMessage { - if (!isFileLocation(msg.params)) return error("deleteFile message misses parameters", msg); - const fileData: FileLocation = msg.params; - if (!isValidFilePath(fileData.filename)) return error("deleteFile with an invalid filename", msg); - - const server = GetServer(fileData.server); - if (server == null) return error("Server hostname invalid", msg); - - const fileExists = (): boolean => !!server.getScript(fileData.filename) - || server.textFiles.some((t: TextFile) => t.filename === fileData.filename); - - if (!fileExists()) return error("deleteFile file doesn't exist", msg); - server.removeFile(fileData.filename); - if (fileExists()) return error("deleteFile failed to delete the file", msg); - - return new RFAMessage({ result: "OK", id: msg.id }); - }, - - getFileNames: function (msg: RFAMessage): RFAMessage { - if (!isFileServer(msg.params)) return error("getFileNames message misses parameters", msg); - - const server = GetServer(msg.params.server); - if (server == null) return error("Server hostname invalid", msg); - - const fileNameList: string[] = [ - ...server.textFiles.map((txt): string => txt.filename), - ...server.scripts.map((scr): string => scr.filename) - ]; - - return new RFAMessage({ result: JSON.stringify(fileNameList), id: msg.id }); - }, - - getAllFiles: function (msg: RFAMessage): RFAMessage { - if (!isFileServer(msg.params)) return error("getAllFiles message misses parameters", msg); - - const server = GetServer(msg.params.server); - if (server == null) return error("Server hostname invalid", msg); - - const fileList: FileContent[] = [ - ...server.textFiles.map((txt): FileContent => { return { filename: txt.filename, content: txt.text } }), - ...server.scripts.map((scr): FileContent => { return { filename: scr.filename, content: scr.code } }) - ]; - - return new RFAMessage({ result: JSON.stringify(fileList), id: msg.id }); - }, - - calculateRam: function (msg: RFAMessage): RFAMessage { - if (!isFileLocation(msg.params)) return error("calculateRam message misses parameters", msg); - const fileData: FileLocation = msg.params; - if (!isValidFilePath(fileData.filename)) return error("deleteFile with an invalid filename", msg); - - const server = GetServer(fileData.server); - if (server == null) return error("Server hostname invalid", msg); - - if (!isScriptFilename(fileData.filename)) return error("Filename isn't a script filename", msg); - const script = server.getScript(fileData.filename); - if (!script) return error("File doesn't exist", msg); - const ramUsage = script.ramUsage; - - return new RFAMessage({result: String(ramUsage), id: msg.id}); - }, - - getDefinitionFile: function (msg: RFAMessage): RFAMessage { - const source = (libSource + "").replace(/export /g, ""); - console.log(source); - return new RFAMessage({result: source, id: msg.id}); + if (isScriptFilename(fileData.filename)) { + const scriptContent = server.getScript(fileData.filename); + if (!scriptContent) return error("File doesn't exist", msg); + return new RFAMessage({ result: scriptContent.code, id: msg.id }); + } else { + // Assume it's a text file + const file = server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0); + if (!file) return error("File doesn't exist", msg); + return new RFAMessage({ result: file.text, id: msg.id }); } -} + }, + + deleteFile: function (msg: RFAMessage): RFAMessage { + if (!isFileLocation(msg.params)) return error("Message misses parameters", msg); + const fileData: FileLocation = msg.params; + if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg); + + const server = GetServer(fileData.server); + if (!server) return error("Server hostname invalid", msg); + + const fileExists = (): boolean => + !!server.getScript(fileData.filename) || server.textFiles.some((t: TextFile) => t.filename === fileData.filename); + + if (!fileExists()) return error("File doesn't exist", msg); + server.removeFile(fileData.filename); + if (fileExists()) return error("Failed to delete the file", msg); + + return new RFAMessage({ result: "OK", id: msg.id }); + }, + + getFileNames: function (msg: RFAMessage): RFAMessage { + if (!isFileServer(msg.params)) return error("Message misses parameters", msg); + + const server = GetServer(msg.params.server); + if (!server) return error("Server hostname invalid", msg); + + const fileNameList: string[] = [ + ...server.textFiles.map((txt): string => txt.filename), + ...server.scripts.map((scr): string => scr.filename), + ]; + + return new RFAMessage({ result: JSON.stringify(fileNameList), id: msg.id }); + }, + + getAllFiles: function (msg: RFAMessage): RFAMessage { + if (!isFileServer(msg.params)) return error("Message misses parameters", msg); + + const server = GetServer(msg.params.server); + if (!server) return error("Server hostname invalid", msg); + + const fileList: FileContent[] = [ + ...server.textFiles.map((txt): FileContent => { + return { filename: txt.filename, content: txt.text }; + }), + ...server.scripts.map((scr): FileContent => { + return { filename: scr.filename, content: scr.code }; + }), + ]; + + return new RFAMessage({ result: JSON.stringify(fileList), id: msg.id }); + }, + + calculateRam: function (msg: RFAMessage): RFAMessage { + if (!isFileLocation(msg.params)) return error("Message misses parameters", msg); + const fileData: FileLocation = msg.params; + if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg); + + const server = GetServer(fileData.server); + if (!server) return error("Server hostname invalid", msg); + + if (!isScriptFilename(fileData.filename)) return error("Filename isn't a script filename", msg); + const script = server.getScript(fileData.filename); + if (!script) return error("File doesn't exist", msg); + const ramUsage = script.ramUsage; + + return new RFAMessage({ result: String(ramUsage), id: msg.id }); + }, + + getDefinitionFile: function (msg: RFAMessage): RFAMessage { + const source = (libSource + "").replace(/export /g, ""); + console.log(source); + return new RFAMessage({ result: source, id: msg.id }); + }, +}; diff --git a/src/RemoteFileAPI/RFALogger.ts b/src/RemoteFileAPI/RFALogger.ts index 6a1d0f277..872d1dc26 100644 --- a/src/RemoteFileAPI/RFALogger.ts +++ b/src/RemoteFileAPI/RFALogger.ts @@ -1,27 +1,27 @@ class RemoteFileAPILogger { - _enabled = true; - _prefix = "[RFA]"; - _error_prefix = "[RFA-ERROR]"; + _enabled = true; + _prefix = "[RFA]"; + _error_prefix = "[RFA-ERROR]"; - constructor(enabled: boolean){ - this._enabled = enabled; - } + constructor(enabled: boolean) { + this._enabled = enabled; + } - public error(...message: any[]) : void { - if (this._enabled) console.error(this._error_prefix, ...message); - } + public error(...message: any[]): void { + if (this._enabled) console.error(this._error_prefix, ...message); + } - public log(...message: any[]) : void { - if (this._enabled) console.log(this._prefix, ...message); - } + public log(...message: any[]): void { + if (this._enabled) console.log(this._prefix, ...message); + } - public disable() : void { - this._enabled = false; - } + public disable(): void { + this._enabled = false; + } - public enable() : void { - this._enabled = true; - } + public enable(): void { + this._enabled = true; + } } export const RFALogger = new RemoteFileAPILogger(true); diff --git a/src/RemoteFileAPI/Remote.ts b/src/RemoteFileAPI/Remote.ts index 311eb8a0b..d864db8df 100644 --- a/src/RemoteFileAPI/Remote.ts +++ b/src/RemoteFileAPI/Remote.ts @@ -1,47 +1,49 @@ import { RFALogger } from "./RFALogger"; -import { RFAMessage } from "./MessageDefinitions" +import { RFAMessage } from "./MessageDefinitions"; import { RFARequestHandler } from "./MessageHandlers"; export class Remote { - connection? : WebSocket; - protocol = "ws"; - ipaddr: string; - port: number; + connection?: WebSocket; + protocol = "ws"; + ipaddr: string; + port: number; - constructor(ip : string, port : number) { - this.ipaddr = ip; - this.port = port; - } + constructor(ip: string, port: number) { + this.ipaddr = ip; + this.port = port; + } - public stopConnection() : void { - this.connection?.close(); - } + public stopConnection(): void { + this.connection?.close(); + } - public startConnection() : void { - RFALogger.log("Trying to connect."); - this.connection = new WebSocket(this.protocol + "://" + this.ipaddr + ":" + this.port); + public startConnection(): void { + RFALogger.log("Trying to connect."); + this.connection = new WebSocket(this.protocol + "://" + this.ipaddr + ":" + this.port); - this.connection.addEventListener("error", (e:Event) => RFALogger.error(e)); - this.connection.addEventListener("message", handleMessageEvent); - this.connection.addEventListener("open", () => RFALogger.log("Connection established: ", this.ipaddr, ":", this.port)); - this.connection.addEventListener("close", () => RFALogger.log("Connection closed")); - } + this.connection.addEventListener("error", (e: Event) => RFALogger.error(e)); + this.connection.addEventListener("message", handleMessageEvent); + this.connection.addEventListener("open", () => + RFALogger.log("Connection established: ", this.ipaddr, ":", this.port), + ); + this.connection.addEventListener("close", () => RFALogger.log("Connection closed")); + } } -function handleMessageEvent(e: MessageEvent):void { - const msg : RFAMessage = JSON.parse(e.data); - RFALogger.log("Message received:", msg); +function handleMessageEvent(this: WebSocket, e: MessageEvent): void { + const msg: RFAMessage = JSON.parse(e.data); + RFALogger.log("Message received:", msg); - //TODO: Needs more safety, send error on invalid input, send error when msg.method isn´t in handlers, ... - - if (msg.method) - { - const response = RFARequestHandler[msg.method](msg); - RFALogger.log("Sending response: ", response); - if(response) this.send(JSON.stringify(response)); - } - - else if (msg.result) RFALogger.error("Result handling not implemented yet."); - else if (msg.error) RFALogger.error("Received an error from server", msg); - else RFALogger.error("Incorrect Message", msg); + if (msg.method) { + if (!RFARequestHandler[msg.method]) { + const response = new RFAMessage({ error: "Unknown message received", id: msg.id }); + this.send(JSON.stringify(response)); + return; + } + const response = RFARequestHandler[msg.method](msg); + RFALogger.log("Sending response: ", response); + if (response) this.send(JSON.stringify(response)); + } else if (msg.result) RFALogger.log("Somehow retrieved a result message."); + else if (msg.error) RFALogger.error("Received an error from server", msg); + else RFALogger.error("Incorrect Message", msg); } diff --git a/src/RemoteFileAPI/RemoteFileAPI.ts b/src/RemoteFileAPI/RemoteFileAPI.ts index 10934f18b..1dccab591 100644 --- a/src/RemoteFileAPI/RemoteFileAPI.ts +++ b/src/RemoteFileAPI/RemoteFileAPI.ts @@ -1,21 +1,19 @@ import { Settings } from "../Settings/Settings"; -import { Remote } from "./Remote"; - +import { Remote } from "./Remote"; let server: Remote; -export function newRemoteFileApiConnection() : void { - if(server == undefined) { - server = new Remote("localhost", Settings.RemoteFileApiPort); - server.startConnection(); - } - else { - server.stopConnection(); - server = new Remote("localhost", Settings.RemoteFileApiPort); - server.startConnection(); - } +export function newRemoteFileApiConnection(): void { + if (server == undefined) { + server = new Remote("localhost", Settings.RemoteFileApiPort); + server.startConnection(); + } else { + server.stopConnection(); + server = new Remote("localhost", Settings.RemoteFileApiPort); + server.startConnection(); + } } -export function isRemoteFileApiConnectionLive() : boolean { - return server.connection != undefined && server.connection.readyState == 1; +export function isRemoteFileApiConnectionLive(): boolean { + return server.connection != undefined && server.connection.readyState == 1; } From 725fb05bb3323b038058b290bcd93a1ef3d08b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Hoekstra?= Date: Thu, 11 Aug 2022 19:14:35 +0200 Subject: [PATCH 5/7] Replace ConnectionBauble Class Component with Functional one --- src/GameOptions/ui/ConnectionBauble.tsx | 46 +++++++---------------- src/GameOptions/ui/CurrentOptionsPage.tsx | 2 +- 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/src/GameOptions/ui/ConnectionBauble.tsx b/src/GameOptions/ui/ConnectionBauble.tsx index 971eaffc7..cad5a17a3 100644 --- a/src/GameOptions/ui/ConnectionBauble.tsx +++ b/src/GameOptions/ui/ConnectionBauble.tsx @@ -1,41 +1,21 @@ -import React from "react"; - -interface baubleState { - connection: boolean; - callback: () => boolean; -} +import React, {useState, useEffect } from "react"; interface baubleProps { callback: () => boolean; } -export class ConnectionBauble extends React.Component { - timerID: NodeJS.Timer; - state: baubleState; +export const ConnectionBauble = (props: baubleProps): React.ReactElement => { + const [connection, setConnection] = useState(props.callback()) - constructor(props: baubleProps) { - super(props); - this.state = { - connection: props.callback(), - callback: props.callback, - }; - } + useEffect(() => { + setInterval(() => { + setConnection(props.callback()); + }, 1000); + }); - componentDidMount(): void { - this.timerID = setInterval(() => this.tick(), 1000); - } - - componentWillUnmount(): void { - clearInterval(this.timerID); - } - - tick(): void { - this.setState({ - connection: this.state.callback(), - }); - } - - render(): string { - return this.state.connection ? "Connected" : "Disconnected"; - } + return ( +
+ {connection? "Connected" : "Disconnected"} +
+ ); } diff --git a/src/GameOptions/ui/CurrentOptionsPage.tsx b/src/GameOptions/ui/CurrentOptionsPage.tsx index 7ab6ec88c..ee403ca50 100644 --- a/src/GameOptions/ui/CurrentOptionsPage.tsx +++ b/src/GameOptions/ui/CurrentOptionsPage.tsx @@ -9,7 +9,7 @@ import { GameOptionsTab } from "../GameOptionsTab"; import { GameOptionsPage } from "./GameOptionsPage"; import { OptionsSlider } from "./OptionsSlider"; import Button from "@mui/material/Button"; -import { ConnectionBauble } from "./ConnectionBauble"; +import { ConnectionBaub, ConnectionBauble } from "./ConnectionBauble"; interface IProps { currentTab: GameOptionsTab; From a6ee9a8c76b9a2f5bd279238ae3af7e8150e9595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Hoekstra?= Date: Thu, 11 Aug 2022 19:21:30 +0200 Subject: [PATCH 6/7] Format --- src/GameOptions/ui/ConnectionBauble.tsx | 14 +++++--------- src/GameOptions/ui/CurrentOptionsPage.tsx | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/GameOptions/ui/ConnectionBauble.tsx b/src/GameOptions/ui/ConnectionBauble.tsx index cad5a17a3..7b2cef418 100644 --- a/src/GameOptions/ui/ConnectionBauble.tsx +++ b/src/GameOptions/ui/ConnectionBauble.tsx @@ -1,11 +1,11 @@ -import React, {useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; interface baubleProps { callback: () => boolean; } -export const ConnectionBauble = (props: baubleProps): React.ReactElement => { - const [connection, setConnection] = useState(props.callback()) +export const ConnectionBauble = (props: baubleProps): React.ReactElement => { + const [connection, setConnection] = useState(props.callback()); useEffect(() => { setInterval(() => { @@ -13,9 +13,5 @@ export const ConnectionBauble = (props: baubleProps): React.ReactElement => { }, 1000); }); - return ( -
- {connection? "Connected" : "Disconnected"} -
- ); -} + return
{connection ? "Connected" : "Disconnected"}
; +}; diff --git a/src/GameOptions/ui/CurrentOptionsPage.tsx b/src/GameOptions/ui/CurrentOptionsPage.tsx index ee403ca50..7ab6ec88c 100644 --- a/src/GameOptions/ui/CurrentOptionsPage.tsx +++ b/src/GameOptions/ui/CurrentOptionsPage.tsx @@ -9,7 +9,7 @@ import { GameOptionsTab } from "../GameOptionsTab"; import { GameOptionsPage } from "./GameOptionsPage"; import { OptionsSlider } from "./OptionsSlider"; import Button from "@mui/material/Button"; -import { ConnectionBaub, ConnectionBauble } from "./ConnectionBauble"; +import { ConnectionBauble } from "./ConnectionBauble"; interface IProps { currentTab: GameOptionsTab; From 765cfd0c9a9075b48c7045d3691236ef8f3737f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Hoekstra?= Date: Thu, 11 Aug 2022 19:44:55 +0200 Subject: [PATCH 7/7] Clear interval when functional connectionBauble gets unmounted --- src/GameOptions/ui/ConnectionBauble.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/GameOptions/ui/ConnectionBauble.tsx b/src/GameOptions/ui/ConnectionBauble.tsx index 7b2cef418..599a3389d 100644 --- a/src/GameOptions/ui/ConnectionBauble.tsx +++ b/src/GameOptions/ui/ConnectionBauble.tsx @@ -8,9 +8,10 @@ export const ConnectionBauble = (props: baubleProps): React.ReactElement => { const [connection, setConnection] = useState(props.callback()); useEffect(() => { - setInterval(() => { + const timer = setInterval(() => { setConnection(props.callback()); }, 1000); + return () => clearInterval(timer); }); return
{connection ? "Connected" : "Disconnected"}
;