diff --git a/.eslintrc.js b/.eslintrc.js index 76d4a5b5..084deb91 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,8 +1,10 @@ "use strict"; module.exports = { "plugins": [ + "@typescript-eslint", "node", ], + "root": true, "env": { "node": true, "commonjs": true, @@ -10,11 +12,25 @@ module.exports = { }, "overrides": [ + { + "files": ["{packages,plugins}/**/*.ts?(x)"], + "parser": "@typescript-eslint/parser", + "extends": [ + "plugin:@typescript-eslint/recommended", + ], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/no-unused-vars": "off", + "prefer-const": "off", + }, + }, { "files": ["test/**/*.js", "plugins/*/test/**/*.js"], "env": { "mocha": "true", }, + "parserOptions": { sourceType: "commonjs" }, "rules": { "prefer-arrow-callback": "off", "no-invalid-this": "off", @@ -23,7 +39,7 @@ module.exports = { }, }, { - "files": ["packages/web_ui/src/**/*.jsx", "packages/lib/browser.js", "plugins/*/web/**/*.jsx"], + "files": ["packages/web_ui/src/**/*.ts?(x)", "packages/lib/browser.js", "plugins/*/web/**/*.ts?(x)"], "env": { "browser": true, }, @@ -108,7 +124,7 @@ module.exports = { "curly": "error", "default-case": "error", "default-case-last": "error", - "default-param-last": "error", + "default-param-last": "off", "dot-location": ["error", "property"], "dot-notation": ["error", { "allowPattern": "^[a-z]+(_[a-z]+)*$" }], "eol-last": "error", @@ -148,7 +164,10 @@ module.exports = { "max-statements-per-line": "off", "multiline-comment-style": ["error", "separate-lines"], "multiline-ternary": "off", - "new-cap": "error", + "new-cap": ["error", { + "capIsNewExceptions": ["StringEnum"], + "capIsNewExceptionPattern": "^Type\\.", + }], "new-parens": "error", "newline-after-var": "off", "newline-before-return": "off", @@ -159,7 +178,7 @@ module.exports = { "no-bitwise": "off", "no-buffer-constructor": "error", "no-caller": "error", - "no-catch-shadow": "error", + "no-catch-shadow": "off", "no-confusing-arrow": "error", "no-console": "error", "no-constant-condition": ["error", { "checkLoops": false }], @@ -223,7 +242,7 @@ module.exports = { "no-script-url": "error", "no-self-compare": "error", "no-sequences": "error", - "no-shadow": ["error", { "allow": ["yargs"] }], + "no-shadow": ["error", { "allow": ["Event", "Request", "yargs"] }], "no-spaced-func": "error", "no-tabs": ["error", { "allowIndentationTabs": true }], "no-template-curly-in-string": "error", @@ -235,12 +254,12 @@ module.exports = { "no-unmodified-loop-condition": "error", "no-unneeded-ternary": "error", "no-unused-expressions": "error", - "no-use-before-define": "error", + "no-use-before-define": "off", "no-useless-backreference": "error", "no-useless-call": "error", "no-useless-computed-key": "error", "no-useless-concat": "error", - "no-useless-constructor": "error", + "no-useless-constructor": "off", "no-useless-rename": "error", "no-useless-return": "error", "no-var": "error", @@ -303,7 +322,11 @@ module.exports = { "template-curly-spacing": ["error", "never"], "template-tag-spacing": "error", "unicode-bom": ["error", "never"], - "valid-jsdoc": ["error", { "requireReturn": false }], + "valid-jsdoc": ["error", { + "requireReturn": false, + "requireReturnType": false, + "requireParamType": false, + }], "vars-on-top": "error", "wrap-iife": "error", "wrap-regex": "off", @@ -313,14 +336,16 @@ module.exports = { "node/callback-return": "off", "node/exports-style": "off", "node/file-extension-in-import": "off", - "node/global-require": "error", + "node/global-require": "off", "node/handle-callback-err": "error", "node/no-callback-literal": "error", "node/no-deprecated-api": "error", "node/no-exports-assign": "error", - "node/no-extraneous-import": "error", - "node/no-extraneous-require": "error", - "node/no-missing-import": "error", + "node/no-extraneous-import": "off", + "node/no-extraneous-require": "off", + "node/no-missing-import": ["error", { + "tryExtensions": [".js", ".ts"], + }], "node/no-missing-require": "error", "node/no-mixed-requires": "error", "node/no-new-require": "error", @@ -335,9 +360,9 @@ module.exports = { "error", { "allowModules": ["webpack", "webpack-merge", "webpack-dev-middleware"] }, ], - "node/no-unsupported-features/es-builtins": "error", - "node/no-unsupported-features/es-syntax": "error", - "node/no-unsupported-features/node-builtins": "error", + "node/no-unsupported-features/es-builtins": "off", + "node/no-unsupported-features/es-syntax": "off", + "node/no-unsupported-features/node-builtins": "off", "node/prefer-global/buffer": "error", "node/prefer-global/console": "error", "node/prefer-global/process": "error", @@ -348,6 +373,10 @@ module.exports = { "node/prefer-promises/dns": "off", "node/prefer-promises/fs": "off", "node/process-exit-as-throw": "error", - "node/shebang": "error", + "node/shebang": ["error", { + "convertPath": { + "*.ts": ["^(.+)\\.ts$", "dist/$1.js"], + }, + }], }, }; diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 5808be74..6098895e 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -16,12 +16,12 @@ jobs: # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2.4.0 with: version: 8 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: pnpm i --no-optional --no-frozen-lockfile @@ -43,11 +43,11 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2.4.0 with: version: 8 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v3 with: node-version: 18.x - run: pnpm i --no-optional --no-frozen-lockfile diff --git a/package.json b/package.json index dc107cc8..9f2eeae8 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@clusterio/ctl": "workspace:*", "@clusterio/host": "workspace:*", "@clusterio/lib": "workspace:*", + "@typescript-eslint/eslint-plugin": "^6.4.1", + "@typescript-eslint/parser": "^6.4.1", "eslint": "^8.45.0", "eslint-plugin-node": "^11.1.0", "express": "^4.18.2", diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index b7be8148..2d9a849f 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -32,7 +32,7 @@ const { ConsoleTransport, levels, logger } = lib; import Controller from "./src/Controller"; import UserManager from "./src/UserManager"; -const version = require("./package.json").version; +import { version } from "./package.json"; // globals let controller: Controller; @@ -83,18 +83,16 @@ void new lib.Gauge( "clusterio_controller_websocket_active_connections", "How many WebSocket connections are currently open to the controller", { callback: function(gauge:lib.Gauge) { - gauge.set(controller.wsServer.activeConnectors.size); - } - } + gauge.set(controller.wsServer.activeConnectors.size); + }} ); void new lib.Gauge( "clusterio_controller_active_hosts", "How many hosts are currently connected to the controller", { callback: function(gauge:lib.Gauge) { - gauge.set(controller.wsServer.hostConnections.size); - } - } + gauge.set(controller.wsServer.hostConnections.size); + }} ); void new lib.Gauge( @@ -294,7 +292,7 @@ async function initialize(): Promise { logger.info(`Loading available plugins from ${args.pluginList}`); let pluginList = new Map(); try { - pluginList = new Map(JSON.parse(await fs.readFile(args.pluginList, { encoding: 'utf8' }))); + pluginList = new Map(JSON.parse(await fs.readFile(args.pluginList, { encoding: "utf8" }))); } catch (err: any) { if (err.code !== "ENOENT") { throw err; @@ -324,7 +322,7 @@ async function initialize(): Promise { logger.info(`Loading config from ${controllerConfigPath}`); const controllerConfig = new lib.ControllerConfig("controller"); try { - let fileData = await fs.readFile(controllerConfigPath, { encoding: 'utf8' }) + let fileData = await fs.readFile(controllerConfigPath, { encoding: "utf8" }); await controllerConfig.load(JSON.parse(fileData)); } catch (err: any) { @@ -363,7 +361,7 @@ async function initialize(): Promise { controllerConfigPath, controllerConfig, shouldRun, - } + }; } async function startup() { @@ -377,7 +375,7 @@ async function startup() { clusterLogger, pluginInfos, controllerConfigPath, - controllerConfig + controllerConfig, } = await initialize(); if (!shouldRun || !pluginInfos || !controllerConfigPath || !controllerConfig) { return; diff --git a/packages/controller/package.json b/packages/controller/package.json index 38fdc352..aae9e0ee 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -4,13 +4,13 @@ "author": "Hornwitser", "license": "MIT", "version": "2.0.0-alpha.13", - "main": "dist/controller/controller.js", + "main": "dist/controller.js", "scripts": { "prepare": "tsc --build && webpack-cli --env production", "test": "echo \"Error: no test specified\" && exit 1" }, "bin": { - "clusteriocontroller": "./dist/controller/controller.js" + "clusteriocontroller": "./dist/controller.js" }, "keywords": [ "factorio" diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index 3ace64f8..b416b0f0 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -29,17 +29,13 @@ export default class ControlConnection extends BaseConnection { private _agent: string; private _version: string; logTransport: lib.LinkTransport | null = null; - logSubscriptions: { - all: boolean, - controller: boolean, - hostIds: number[], - instanceIds: number[], - } = { + logSubscriptions = { all: false, controller: false, - hostIds: [], - instanceIds: [], + hostIds: [] as number[], + instanceIds: [] as number[], }; + ws_dumper: ((...args: any[]) => void) | null = null; declare connector: WsServerConnector; @@ -254,7 +250,7 @@ export default class ControlConnection extends BaseConnection { assigned_host = undefined; } - let game_port: number|null|undefined = instance.game_port + let game_port: number|null|undefined = instance.game_port; if (game_port === null) { game_port = undefined; } @@ -275,8 +271,8 @@ export default class ControlConnection extends BaseConnection { if (assigned_host === null) { assigned_host = undefined; } - - let game_port: number|null|undefined = instance.game_port + + let game_port: number|null|undefined = instance.game_port; if (game_port === null) { game_port = undefined; } @@ -434,7 +430,7 @@ export default class ControlConnection extends BaseConnection { async handleModPackCreateRequest(request: lib.ModPackCreateRequest) { let modPack = request.modPack; if (modPack.id === undefined) { - throw new lib.RequestError(`Mod pack need an ID to be created`); + throw new lib.RequestError("Mod pack need an ID to be created"); } if (this._controller.modPacks!.has(modPack.id)) { throw new lib.RequestError(`Mod pack with ID ${modPack.id} already exist`); @@ -589,7 +585,7 @@ export default class ControlConnection extends BaseConnection { } async handleLogSetSubscriptionsRequest(request: lib.LogSetSubscriptionsRequest) { - this.logSubscriptions = { + this.logSubscriptions = { all: request.all || false, controller: request.controller || false, hostIds: request.hostIds || [], @@ -622,7 +618,7 @@ export default class ControlConnection extends BaseConnection { controller: request.controller, hostIds: request.hostIds, instanceIds: request.instanceIds, - } + }; let observeDuration = queryLogTime.startTimer(); let { all, controller, hostIds, instanceIds } = request; diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index e355b836..29249626 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -1,13 +1,10 @@ import type winston from "winston"; import type { AddressInfo } from "net"; import type { ControllerArgs } from "../controller"; -import type { HostInfo } from "./HostConnection"; -import type { Request, Response, Application } from "express"; - +import express, { type Request, type Response, type NextFunction, type Application } from "express"; import compression from "compression"; import events, { EventEmitter } from "events"; -import express from "express"; import fs from "fs-extra"; import http from "http"; import https from "https"; @@ -20,12 +17,11 @@ const { logger, Summary, Gauge } = lib; import HttpCloser from "./HttpCloser"; import InstanceInfo, { InstanceStatus } from "./InstanceInfo"; -import metrics from "./metrics"; +import * as metrics from "./metrics"; import * as routes from "./routes"; import UserManager from "./UserManager"; import WsServer from "./WsServer"; -import yargs from "yargs"; -import HostConnection from "./HostConnection"; +import HostConnection, { type HostInfo } from "./HostConnection"; const endpointDurationSummary = new Summary( "clusterio_controller_http_endpoint_duration_seconds", @@ -77,7 +73,7 @@ export default class Controller { /** Event subscription controller */ subscriptions: lib.SubscriptionController; - + // Possible states are new, starting, running, stopping, stopped private _state: string = "new"; private _shouldStop: boolean = false; @@ -87,7 +83,7 @@ export default class Controller { _snoopedEvents = new Map(); devMiddleware: any | null = null; - + logDirectory: string = ""; clusterLogIndex: lib.LogIndex | null = null; clusterLogBuildInterval: ReturnType | null = null; @@ -110,6 +106,14 @@ export default class Controller { this.wsServer = new WsServer(this); this.userManager = new UserManager(this.config); this.subscriptions = new lib.SubscriptionController(this); + + // Handle subscriptions for all internal properties + this.subscriptions.handle(lib.HostUpdateEvent); + this.subscriptions.handle(lib.InstanceDetailsUpdateEvent); + this.subscriptions.handle(lib.InstanceSaveListUpdateEvent); + this.subscriptions.handle(lib.ModPackUpdateEvent); + this.subscriptions.handle(lib.ModUpdateEvent); + this.subscriptions.handle(lib.UserUpdateEvent); } async start(args: ControllerArgs) { @@ -149,7 +153,7 @@ export default class Controller { async _startInternal(args: ControllerArgs) { this.logDirectory = args.logDirectory; this.clusterLogIndex = await lib.LogIndex.load(path.join(this.logDirectory, "cluster")); - + this.clusterLogBuildInterval = setInterval(() => { if (this.clusterLogIndex) { this.clusterLogIndex.buildIndex().catch( @@ -161,33 +165,7 @@ export default class Controller { // Start webpack development server if enabled if (args.dev || args.devPlugin) { - logger.warn("Webpack development mode enabled"); - /* eslint-disable node/global-require, node/no-unpublished-require */ - const webpack = require("webpack"); - const webpackDevMiddleware = require("webpack-dev-middleware"); - const webpackConfigs = []; - - if (args.dev) { - webpackConfigs.push(require("../../../webpack.config")({})); - } - if (args.devPlugin) { - let devPlugins = new Map(); - for (let name of args.devPlugin) { - let info = this.pluginInfos.find(i => i.name === name); - if (!info) { - throw new lib.StartupError(`No plugin named ${name}`); - } - let config = require(path.posix.join(info.requirePath, "webpack.config"))({}); - devPlugins.set(name, webpackConfigs.length); - webpackConfigs.push(config); - } - this.app.locals.devPlugins = devPlugins; - } - /* eslint-enable node/global-require, node/no-unpublished-require */ - - const compiler = webpack(webpackConfigs); - this.devMiddleware = webpackDevMiddleware(compiler, { serverSideRender: true }); - this.app.use(this.devMiddleware); + this._startDevServer(args); } let databaseDirectory = this.config.get("controller.database_directory"); @@ -231,7 +209,7 @@ export default class Controller { Controller.addAppRoutes(this.app, this.pluginInfos); if (!args.dev) { - let manifestPath = path.join(__dirname, "..", "..", "web", "manifest.json") + let manifestPath = path.join(__dirname, "..", "web", "manifest.json"); let manifest = await Controller.loadJsonObject(manifestPath); if (!manifest["main.js"]) { @@ -240,14 +218,6 @@ export default class Controller { this.app.locals.mainBundle = manifest["main.js"] || "no_web_build"; } - // Handle subscriptions for all internal properties - this.subscriptions.handle(lib.HostUpdateEvent); - this.subscriptions.handle(lib.InstanceDetailsUpdateEvent); - this.subscriptions.handle(lib.InstanceSaveListUpdateEvent); - this.subscriptions.handle(lib.ModPackUpdateEvent); - this.subscriptions.handle(lib.ModUpdateEvent); - this.subscriptions.handle(lib.UserUpdateEvent); - // Load plugins await this.loadPlugins(); @@ -286,6 +256,37 @@ export default class Controller { this._state = "running"; } + async _startDevServer(args: ControllerArgs) { + logger.warn("Webpack development mode enabled"); + /* eslint-disable @typescript-eslint/no-var-requires, node/no-unpublished-require */ + const webpack = require("webpack"); + const webpackDevMiddleware = require("webpack-dev-middleware"); + const webpackConfigs = []; + + if (args.dev) { + // eslint-disable-next-line node/no-missing-require + webpackConfigs.push(require("../../../webpack.config")({})); // Path outside of build + } + if (args.devPlugin) { + let devPlugins = new Map(); + for (let name of args.devPlugin) { + let info = this.pluginInfos.find(i => i.name === name); + if (!info) { + throw new lib.StartupError(`No plugin named ${name}`); + } + let config = require(path.posix.join(info.requirePath, "webpack.config"))({}); + devPlugins.set(name, webpackConfigs.length); + webpackConfigs.push(config); + } + this.app.locals.devPlugins = devPlugins; + } + /* eslint-enable @typescript-eslint/no-var-requires, node/no-unpublished-require */ + + const compiler = webpack(webpackConfigs); + this.devMiddleware = webpackDevMiddleware(compiler, { serverSideRender: true }); + this.app.use(this.devMiddleware); + } + /** * Stops the controller. * @@ -365,7 +366,7 @@ export default class Controller { static async loadHosts(filePath: string): Promise> { let serialized: [number, HostInfo][]; try { - serialized = JSON.parse(await fs.readFile(filePath, { encoding: 'utf8' })); + serialized = JSON.parse(await fs.readFile(filePath, { encoding: "utf8" })); } catch (err: any) { if (err.code !== "ENOENT") { @@ -425,7 +426,7 @@ export default class Controller { static async loadModPacks(filePath: string): Promise> { let json; try { - json = JSON.parse(await fs.readFile(filePath, { encoding : "utf8" })); + json = JSON.parse(await fs.readFile(filePath, { encoding: "utf8" })); } catch (err: any) { if (err.code !== "ENOENT") { throw err; @@ -439,7 +440,7 @@ export default class Controller { await lib.safeOutputFile(filePath, JSON.stringify([...modPacks.values()], null, 4)); } - static async loadModInfos(modsDirectory: string): Promise> { + static async loadModInfos(modsDirectory: string): Promise> { let mods = new Map(); for (let entry of await fs.readdir(modsDirectory, { withFileTypes: true })) { if (entry.isDirectory()) { @@ -476,7 +477,7 @@ export default class Controller { static async loadJsonObject(filePath: string, throwOnMissing: boolean = false): Promise { let manifest = {}; try { - manifest = JSON.parse(await fs.readFile(filePath, { encoding: 'utf8' })); + manifest = JSON.parse(await fs.readFile(filePath, { encoding: "utf8" })); } catch (err: any) { if (!throwOnMissing && err.code !== "ENOENT") { throw err; @@ -543,7 +544,9 @@ export default class Controller { // Set folder to serve static content from (the website) const staticOptions = { immutable: true, maxAge: 1000 * 86400 * 365 }; - app.use("/static", express.static(path.join(__dirname, "..", "..", "..", "dist", "web", "static"), staticOptions)); + app.use("/static", + express.static(path.join(__dirname, "..", "..", "dist", "web", "static"), staticOptions) + ); app.use("/static", express.static("static", staticOptions)); // Used for data export files // Add API routes @@ -595,9 +598,10 @@ export default class Controller { * Get instance by ID for a request * * @param instanceId - ID of instance to get. + * @returns Info for the given instance if it exists * @throws {module:lib.RequestError} if the instance does not exist. */ - getRequestInstance(instanceId:number):InstanceInfo { + getRequestInstance(instanceId:number): InstanceInfo { let instance = this.instances!.get(instanceId); if (!instance) { throw new lib.RequestError(`Instance with ID ${instanceId} does not exist`); @@ -773,7 +777,7 @@ export default class Controller { assigned_host = undefined; } - let game_port: number|null|undefined = instance.game_port + let game_port: number|null|undefined = instance.game_port; if (game_port === null) { game_port = undefined; } @@ -879,8 +883,7 @@ export default class Controller { ControllerPluginClass = await lib.loadControllerPluginClass(pluginInfo); } - //@ts-ignore //TODO::: metrics is lib.Counter but ControllerPluginClass expect any[], can't figure out how the metrics is used. - let controllerPlugin = new ControllerPluginClass(pluginInfo, this, metrics, logger); + let controllerPlugin = new ControllerPluginClass(pluginInfo, this, metrics as any, logger); await controllerPlugin.init(); this.plugins.set(pluginInfo.name, controllerPlugin); @@ -947,8 +950,8 @@ export default class Controller { /** * Servers the web interface with the root path set apropriately * - * @param {string} route - route the interface is served under. - * @returns {function} Experess.js route handler. + * @param route - route the interface is served under. + * @returns Experess.js route handler. */ static serveWeb(route: string) { // The depth is is the number of slashes in the route minus one, but due @@ -956,7 +959,7 @@ export default class Controller { // compensate if the request path contains a slash but not the route, // and vice versa. let routeDepth = (route.match(/\//g) || []).length - 1 - Number(route.slice(-1) === "/"); - return function(req: Request, res: Response, next: Function) { + return function(req: Request, res: Response, next: NextFunction) { let depth = routeDepth + Number(req.path.slice(-1) === "/"); let webRoot = "../".repeat(depth) || "./"; let staticRoot = webRoot; @@ -968,7 +971,7 @@ export default class Controller { mainBundle = stats.toJson().assetsByChunkName["main"]; } - fs.readFile(path.join(__dirname, "..", "..", "..", "web", "index.html"), "utf8").then((content) => { + fs.readFile(path.join(__dirname, "..", "..", "web", "index.html"), "utf8").then((content) => { res.type("text/html"); res.send(content .replace(/__CLUSTER_NAME__/g, res.app.locals.controller.config.get("controller.name")) diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index be4c4413..76a7019b 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -13,7 +13,7 @@ export interface HostInfo { id: number; name: string; version: string; - plugins: [number, any][]; + plugins: Record; public_address: string | undefined; token_valid_after?: number; } @@ -30,10 +30,10 @@ export default class HostConnection extends BaseConnection { private _id: any; private _name: any; private _version: any; - plugins: Map; + plugins: Map; constructor( - registerData: any, + registerData: lib.RegisterHostData, connector: WsServerConnector, controller: Controller, ) { @@ -46,7 +46,7 @@ export default class HostConnection extends BaseConnection { this.plugins = new Map(Object.entries(registerData.plugins)); this._checkPluginVersions(); - let currentHostInfo = this._controller.hosts!.get(this._id) + let currentHostInfo = this._controller.hosts!.get(this._id); this._controller.hosts!.set(this._id, { agent: this._agent, @@ -223,7 +223,9 @@ export default class HostConnection extends BaseConnection { } instanceConfig.set("instance.assigned_host", this._id); - let newInstance = new InstanceInfo({ config: instanceConfig, status: instanceData.status as InstanceStatus }); + let newInstance = new InstanceInfo( + { config: instanceConfig, status: instanceData.status as InstanceStatus } + ); this._controller.instances!.set(instanceConfig.get("instance.id"), newInstance); this._controller.addInstanceHooks(newInstance); await this.send( diff --git a/packages/controller/src/HttpCloser.ts b/packages/controller/src/HttpCloser.ts index 4c52364e..2631930c 100644 --- a/packages/controller/src/HttpCloser.ts +++ b/packages/controller/src/HttpCloser.ts @@ -26,7 +26,8 @@ export default class HttpCloser { private _responses: Set; /** - * Server to attach this closer to. + * Create HttpCloser attached to a server. + * @param server - Node.js HTTP(S) server to attach to. */ constructor(server: http.Server | https.Server) { this._server = server; diff --git a/packages/controller/src/InstanceInfo.ts b/packages/controller/src/InstanceInfo.ts index 7af224cf..e33f43f3 100644 --- a/packages/controller/src/InstanceInfo.ts +++ b/packages/controller/src/InstanceInfo.ts @@ -14,7 +14,7 @@ import type * as lib from "@clusterio/lib"; * - `exporting_data`: Instance is in the process of exporting game data. * - `deleted`: Instance has been deleted. */ -export type InstanceStatus = +export type InstanceStatus = "unknown" | "unassigned" | "stopped" | "starting" | "running" | "stopping" | "creating_save" | "exporting_data" | "deleted"; @@ -36,7 +36,7 @@ export default class InstanceInfo { this.status = status; } - toJSON(): Object { + toJSON() { return { config: this.config.serialize(), status: this.status, diff --git a/packages/controller/src/UserManager.ts b/packages/controller/src/UserManager.ts index bd2789c3..51254cb2 100644 --- a/packages/controller/src/UserManager.ts +++ b/packages/controller/src/UserManager.ts @@ -18,7 +18,7 @@ export default class UserManager { async load(filePath: string): Promise { try { - let content = JSON.parse(await fs.readFile(filePath, { encoding: 'utf8' })); + let content = JSON.parse(await fs.readFile(filePath, { encoding: "utf8" })); for (let serializedRole of content.roles) { let role = new lib.Role(serializedRole); this.roles.set(role.id, role); @@ -65,6 +65,7 @@ export default class UserManager { /** * Creates a new user and add it to the user database. * @param name - Name of the user to create. + * @returns The created user. */ createUser(name:string): lib.User { if (this.users.has(name)) { @@ -74,7 +75,7 @@ export default class UserManager { let roles = []; let defaultRoleId = this._config.get("controller.default_role_id"); if (defaultRoleId !== null) { - roles.push(defaultRoleId) + roles.push(defaultRoleId); } let user = new lib.User({ name, roles }, this.roles); diff --git a/packages/controller/src/WsServer.ts b/packages/controller/src/WsServer.ts index e52e267e..333c06fb 100644 --- a/packages/controller/src/WsServer.ts +++ b/packages/controller/src/WsServer.ts @@ -12,7 +12,7 @@ import ControlConnection from "./ControlConnection"; import HostConnection, { HostInfo } from "./HostConnection"; import WsServerConnector from "./WsServerConnector"; -const packageVersion = require("../package.json").version; +import { version as packageVersion } from "../package.json"; const wsMessageCounter = new lib.Counter( "clusterio_controller_websocket_message_total", @@ -54,7 +54,9 @@ export default class WsServer { noServer: true, path: "/api/socket", }); - this.wss.on("connection", (socket: lib.WebSocketClusterio, req: IncomingMessage) => this.handleConnection(socket, req)); + this.wss.on("connection", + (socket: lib.WebSocketClusterio, req: IncomingMessage) => this.handleConnection(socket, req), + ); } async stop(): Promise { @@ -109,12 +111,11 @@ export default class WsServer { } }); let originalSend = socket.send; - socket.send = (...args: any[]) => { + socket.send = (...args: [string, any]) => { wsMessageCounter.labels("out").inc(); if (typeof args[0] === "string" && !socket.clusterio_ignore_dump) { this.controller.debugEvents.emit("message", { direction: "out", content: args[0] }); } - // @ts-ignore TODO::: Issue with originalSend having multiple signatures ? return originalSend.call(socket, ...args); }; diff --git a/packages/controller/src/metrics.ts b/packages/controller/src/metrics.ts index d25d5690..b588be00 100644 --- a/packages/controller/src/metrics.ts +++ b/packages/controller/src/metrics.ts @@ -1,7 +1,7 @@ import * as lib from "@clusterio/lib"; import util from "util"; -const endpointHitCounter = new lib.Counter( +export const endpointHitCounter = new lib.Counter( "clusterio_controller_http_endpoint_hits_total", "How many requests a particular HTTP endpoint has gotten", { labels: ["route"], register: false } @@ -11,5 +11,3 @@ endpointHitCounter.labels = util.deprecate( endpointHitCounter.labels, "incrementing endpointHitCounter is no longer needed and has no effect" ); - -export default endpointHitCounter diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index 4e506313..1c9c441c 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -18,7 +18,7 @@ const finished = util.promisify(nodeStream.finished); // Merges samples from sourceResult to destinationResult -function mergeSamples(destinationResult: any, sourceResult: any) { +function mergeSamples(destinationResult: lib.CollectorResultSerialized, sourceResult: lib.CollectorResultSerialized) { let receivedSamples = new Map(sourceResult.samples); for (let [suffix, suffixSamples] of destinationResult.samples) { if (receivedSamples.has(suffix)) { @@ -34,17 +34,18 @@ function mergeSamples(destinationResult: any, sourceResult: any) { // Prometheus polling endpoint async function getMetrics(req: Request, res: Response, next: any) { - const controller: Controller = req.app.locals.controller + const controller: Controller = req.app.locals.controller; - let results: any[] = []; - let pluginResults: any[] = await lib.invokeHook(controller.plugins, "onMetrics"); + let results: lib.CollectorResult[] = []; + type ResultIterator = AsyncIterable | Iterable + let pluginResults = await lib.invokeHook(controller.plugins, "onMetrics") as ResultIterator[]; for (let metricIterator of pluginResults) { for await (let metric of metricIterator) { results.push(metric); } } - let requests: Promise[] = []; + let requests: Promise | null>[] = []; let timeout = controller.config.get("controller.metrics_timeout") * 1000; for (let [hostId, hostConnection] of controller.wsServer.hostConnections) { if (!hostConnection.connected) { @@ -61,11 +62,11 @@ async function getMetrics(req: Request, res: Response, next: any) { )); } - for await (let result of await lib.defaultRegistry.collect()) { + for await (let result of lib.defaultRegistry.collect()) { results.push(result); } - let resultMap: Map = new Map(); + let resultMap: Map = new Map(); for (let response of await Promise.all(requests)) { if (!response) { // TODO: Log timeout occured? @@ -83,19 +84,12 @@ async function getMetrics(req: Request, res: Response, next: any) { } } - // TODO: results seem to mix deserialized and serialized lib.CollectorResult ? - // mergeSamples imply samples be [string, [string, number][]] - // but lib.defaultRegistry.collect() imply Map> for (let result of resultMap.values()) { results.push(lib.deserializeResult(result)); } - - // lib.exposition expect AsyncIterable as parameter. - // Should wrap results into a Generator functions ? - // @ts-ignore let text = await lib.exposition(results); - res.set("Content-Type", lib.exposition.contentType); + res.set("Content-Type", lib.expositionContentType); res.send(text); } @@ -426,28 +420,29 @@ async function uploadSave(req: Request, res: Response) { if (contentMime === "multipart/form-data") { await new Promise(resolve => { - let instanceId:number | undefined; - let parser = busboy({ headers: req.headers }); - parser.on("file", (name: string, stream: nodeStream.Readable, { filename, mimeType }: { filename: string, mimeType: string } ) => { + let instanceId: number | undefined; + function fileHandler( + name: string, + stream: nodeStream.Readable, + { filename, mimeType }: { filename: string, mimeType: string } + ) { if (instanceId === undefined) { requestErrors.push("instance_id must come before files uploaded"); } - if (!zipMimes.includes(mimeType)) { requestErrors.push("invalid file Content-Type"); } - if (!filename.endsWith(".zip")) { requestErrors.push("filename must end with .zip"); } - if (errors.length || requestErrors.length) { stream.resume(); return; } - tasks.push(handleFile(instanceId as number, stream, filename, mimeType)); - }); + } + let parser = busboy({ headers: req.headers }); + parser.on("file", fileHandler); parser.on("field", (name, value, info) => { if (name === "instance_id") { instanceId = Number.parseInt(value, 10); @@ -593,23 +588,26 @@ async function uploadMod(req: Request, res: Response) { if (contentMime === "multipart/form-data") { await new Promise(resolve => { - let parser = busboy({ headers: req.headers }); - parser.on("file", (name:string, stream: nodeStream.Readable, { filename, mimeType }: { filename: string, mimeType: string }) => { + function fileHandler( + name: string, + stream: nodeStream.Readable, + { filename, mimeType }: { filename: string, mimeType: string } + ) { if (!zipMimes.includes(mimeType)) { requestErrors.push("invalid file Content-Type"); } - if (!filename.endsWith(".zip")) { requestErrors.push("filename must end with .zip"); } - if (errors.length || requestErrors.length) { stream.resume(); return; } - tasks.push(handleFile(stream, filename)); - }); + } + + let parser = busboy({ headers: req.headers }); + parser.on("file", fileHandler); parser.on("close", resolve); parser.on("error", (err: any) => { logger.error(`Error parsing multipart request in upload-mod:\n${err.stack}`); diff --git a/packages/controller/tsconfig.json b/packages/controller/tsconfig.json index b123e16d..51e67a4a 100644 --- a/packages/controller/tsconfig.json +++ b/packages/controller/tsconfig.json @@ -5,6 +5,6 @@ ], "include": ["controller.ts", "package.json", "src/**/*.ts"], "compilerOptions": { - "outDir": "dist/controller", + "outDir": "dist", }, } diff --git a/packages/ctl/ctl.ts b/packages/ctl/ctl.ts index 61ecf9c2..afa9e8d1 100755 --- a/packages/ctl/ctl.ts +++ b/packages/ctl/ctl.ts @@ -169,6 +169,7 @@ async function startControl() { ; // Parse the args first to get the configured plugin list. + // eslint-disable-next-line node/no-sync let args = yargs.parseSync() as CtlArguments; // Log stream for the ctl session. diff --git a/packages/ctl/src/commands.ts b/packages/ctl/src/commands.ts index 1d8d5d9a..91a70599 100644 --- a/packages/ctl/src/commands.ts +++ b/packages/ctl/src/commands.ts @@ -12,7 +12,7 @@ import type { Argv } from "yargs"; import * as lib from "@clusterio/lib"; import { ConsoleTransport, levels, logger } from "@clusterio/lib"; -import type { Control } from "../ctl.js"; +import type { Control } from "../ctl"; const asTable = asTableModule.configure({ delimiter: " | " }); const finished = util.promisify(stream.finished); @@ -89,7 +89,7 @@ const controllerConfigCommands = new lib.CommandTree({ controllerConfigCommands.add(new lib.Command({ definition: ["list", "List controller configuration"], - handler: async function(args: {}, control: Control) { + handler: async function(args: object, control: Control) { let response = await control.send(new lib.ControllerConfigGetRequest()); for (let group of (response.serializedConfig as any).groups) { @@ -212,13 +212,13 @@ const controllerPluginCommands = new lib.CommandTree({ }); controllerPluginCommands.add(new lib.Command({ definition: ["list", "List plugins on controller"], - handler: async function(args: {}, control: Control) { + handler: async function(args: object, control: Control) { let url = new URL(control.config.get("control.controller_url") as string); url.pathname += "api/plugins"; let response = await phin<[]>({ url, parse: "json", - core: { ca: control.tlsCa } as {}, + core: { ca: control.tlsCa } as object, }); print(asTable(response.body)); }, @@ -229,7 +229,7 @@ controllerCommands.add(controllerPluginCommands); const hostCommands = new lib.CommandTree({ name: "host", description: "Host management" }); hostCommands.add(new lib.Command({ definition: [["list", "l"], "List hosts connected to the controller"], - handler: async function(args: {}, control: Control) { + handler: async function(args: object, control: Control) { let hosts = await control.send(new lib.HostListRequest()); print(asTable(hosts)); }, @@ -288,7 +288,7 @@ const instanceCommands = new lib.CommandTree({ }); instanceCommands.add(new lib.Command({ definition: [["list", "l"], "List instances known to the controller"], - handler: async function(args: {}, control: Control) { + handler: async function(args: object, control: Control) { let list = await control.send(new lib.InstanceDetailsListRequest()); print(asTable(list)); }, @@ -466,7 +466,7 @@ instanceConfigCommands.add(new lib.Command({ }); await events.once(doneEmitter, "dot_on_done"); try { - await fs.unlink(tmpFile) + await fs.unlink(tmpFile); } catch (err) { print("err: temporary file", tmpFile, "could not be deleted."); print("This is not fatal, but they may build up over time if the issue persists."); @@ -612,7 +612,7 @@ instanceSaveCommands.add(new lib.Command({ "X-Access-Token": control.config.get("control.controller_token"), "Content-Type": "application/zip", }, - core: { ca: control.tlsCa } as {}, + core: { ca: control.tlsCa } as object, data: content, parse: "json", }); @@ -687,7 +687,7 @@ instanceSaveCommands.add(new lib.Command({ url.pathname += `api/stream/${streamId}`; let response = await phin({ url, method: "GET", - core: { ca: control.tlsCa } as {}, + core: { ca: control.tlsCa } as object, stream: true, }); @@ -898,13 +898,13 @@ modPackCommands.add(new lib.Command({ modPackCommands.add(new lib.Command({ definition: [["list", "l"], "List mod packs in the cluster"], - handler: async function(args: {}, control: Control) { + handler: async function(args: object, control: Control) { let modPacks = await control.send(new lib.ModPackListRequest()); let fields = ["id", "name", "factorioVersion"]; for (let entry of modPacks) { for (let field of Object.keys(entry)) { if (!fields.includes(field)) { - // @ts-ignore + // @ts-expect-error terrible hack delete entry[field]; } } @@ -1176,7 +1176,7 @@ modCommands.add(new lib.Command({ for (let entry of mods) { for (let field of Object.keys(entry)) { if (!args.fields.includes(field)) { - // @ts-ignore + // @ts-expect-error terrible hack delete entry[field]; } } @@ -1243,7 +1243,7 @@ modCommands.add(new lib.Command({ for (let entry of results) { for (let field of Object.keys(entry)) { if (!args.fields.includes(field)) { - // @ts-ignore + // @ts-expect-error terrible hack delete entry[field]; } } @@ -1281,7 +1281,7 @@ modCommands.add(new lib.Command({ "X-Access-Token": control.config.get("control.controller_token"), "Content-Type": "application/zip", }, - core: { ca: control.tlsCa } as {}, + core: { ca: control.tlsCa } as object, data: content, parse: "json", }); @@ -1317,7 +1317,7 @@ modCommands.add(new lib.Command({ url.pathname += `api/stream/${streamId}`; let response = await phin({ url, method: "GET", - core: { ca: control.tlsCa } as {}, + core: { ca: control.tlsCa } as object, stream: true, }); @@ -1358,7 +1358,7 @@ modCommands.add(new lib.Command({ const permissionCommands = new lib.CommandTree({ name: "permission", description: "Permission inspection" }); permissionCommands.add(new lib.Command({ definition: [["list", "l"], "List permissions in the cluster"], - handler: async function(args: {}, control: Control) { + handler: async function(args: object, control: Control) { let permissions = await control.send(new lib.PermissionListRequest()); print(asTable(permissions)); }, @@ -1368,7 +1368,7 @@ permissionCommands.add(new lib.Command({ const roleCommands = new lib.CommandTree({ name: "role", description: "Role management" }); roleCommands.add(new lib.Command({ definition: [["list", "l"], "List roles in the cluster"], - handler: async function(args: {}, control: Control) { + handler: async function(args: object, control: Control) { let roles = await control.send(new lib.RoleListRequest()); print(asTable(roles)); }, @@ -1716,7 +1716,7 @@ logCommands.add(new lib.Command({ const debugCommands = new lib.CommandTree({ name: "debug", description: "Debugging utilities" }); debugCommands.add(new lib.Command({ definition: ["dump-ws", "Dump WebSocket messages sent and received by controller"], - handler: async function(args: {}, control: Control) { + handler: async function(args: object, control: Control) { await control.send(new lib.DebugDumpWsRequest()); control.keepOpen = true; }, diff --git a/packages/host/host.ts b/packages/host/host.ts index 88c2d900..8c96106c 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -66,6 +66,7 @@ export class HostConnector extends lib.WebSocketClientConnector { async function startHost() { // argument parsing + // eslint-disable-next-line node/no-sync const args = yargs .scriptName("host") .usage("$0 [options]") diff --git a/packages/host/package.json b/packages/host/package.json index 0468f46b..c333e939 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@clusterio/lib": "^2.0.0-alpha.13", + "@sinclair/typebox": "^0.30.4", "fs-extra": "^11.1.1", "jimp": "^0.22.8", "jszip": "^3.10.1", @@ -36,7 +37,6 @@ "access": "public" }, "devDependencies": { - "@sinclair/typebox": "^0.30.4", "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", "@types/pidusage": "^2.0.2", diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index 835c39b7..602d1217 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -1,4 +1,3 @@ -"use strict"; import fs from "fs-extra"; import path from "path"; import events from "events"; @@ -543,7 +542,7 @@ export default class Host extends lib.Link { } async handleHostMetricsRequest() { - let requests = []; + let requests: Promise>[] = []; for (let instanceConnection of this.instanceConnections.values()) { requests.push(instanceConnection.send(new lib.InstanceMetricsRequest())); } @@ -689,7 +688,7 @@ export default class Host extends lib.Link { url.pathname += `api/stream/${streamId}`; let response = await phin({ url, method: "GET", - core: { ca: this.tlsCa } as {}, + core: { ca: this.tlsCa } as object, stream: true, }); @@ -744,7 +743,7 @@ export default class Host extends lib.Link { url.pathname += `api/stream/${streamId}`; phin({ url, method: "PUT", - core: { ca: this.tlsCa } as {}, + core: { ca: this.tlsCa } as object, data: content, }).catch(err => { logger.error(`Error pushing save to controller:\n${err.stack}`, this.instanceLogMeta(instanceId)); diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 9c3e059f..dd9e7e21 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -11,7 +11,7 @@ import * as lib from "@clusterio/lib"; import { FactorioServer } from "./server"; import { SaveModule, patch } from "./patch"; import { exportData } from "./export"; -import type Host from "./Host" +import type Host from "./Host"; const instanceRconCommandDuration = new lib.Histogram( @@ -99,7 +99,7 @@ export default class Instance extends lib.Link { /** * ID of this instance, equivalenet to `instance.config.get("instance.id")`. */ - readonly id: number + readonly id: number; plugins: Map; config: lib.InstanceConfig; logger: lib.Logger; @@ -962,11 +962,12 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); } async handleInstanceMetricsRequest() { - let results = []; + let results: ReturnType[] = []; if (!["stopped", "stopping"].includes(this._status)) { - let pluginResults = await lib.invokeHook(this.plugins, "onMetrics"); + type ResultIterator = AsyncIterable | Iterable + let pluginResults = await lib.invokeHook(this.plugins, "onMetrics") as ResultIterator[]; for (let metricIterator of pluginResults) { - for await (let metric of metricIterator as any) { + for await (let metric of metricIterator) { results.push(lib.serializeResult(metric)); } } @@ -1067,7 +1068,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); let response = await phin({ url, method: "PUT", data: content, - core: { ca: this._host.tlsCa } as {}, + core: { ca: this._host.tlsCa } as object, headers: { "Content-Type": "application/zip", "x-access-token": this._host.config.get("host.controller_token"), diff --git a/packages/host/src/export.ts b/packages/host/src/export.ts index 76f13ebb..dcd2b01a 100644 --- a/packages/host/src/export.ts +++ b/packages/host/src/export.ts @@ -1,5 +1,4 @@ // Export of item icons and locale -"use strict"; import fs from "fs-extra"; import path from "path"; import Jimp from "jimp"; diff --git a/packages/host/src/patch.ts b/packages/host/src/patch.ts index 7e16bf92..035b824d 100644 --- a/packages/host/src/patch.ts +++ b/packages/host/src/patch.ts @@ -18,7 +18,7 @@ export class SaveModule { /** Module */ public info: lib.ModuleInfo, /** Files and their content that will be patched into the save */ - public files = new Map, + public files = new Map(), ) { } static moduleFilePath(filePath: string, moduleName: string) { @@ -47,7 +47,7 @@ export class SaveModule { if (entry.isFile()) { let savePath = SaveModule.moduleFilePath(relativePath, this.info.name); this.files.set(savePath, await fs.readFile(fsPath)); - if (relativePath == "module_exports.lua") { + if (relativePath === "module_exports.lua") { this.files.set( `modules/${this.info.name}.lua`, Buffer.from(`return require("modules/${this.info.name}/module_exports")`, "utf-8") @@ -96,7 +96,7 @@ export class SaveModule { version: plugin.info.version, dependencies: { "clusterio": "*" }, ...JSON.parse(await fs.readFile(moduleJsonPath, "utf8")), - } + }; } catch (err: any) { throw new Error(`Loading module/module.json in plugin ${plugin.info.name} failed: ${err.message}`); } @@ -173,7 +173,7 @@ export class PatchInfo { interface PatchInfoV0 { patch_number: number; scenario: ScenarioInfoV0; - modules: { name: string, files: { path: string, load: boolean, require: boolean }[], }[]; + modules: { name: string, files: { path: string, load: boolean, require: boolean }[], }[]; } const info = json as unknown as PatchInfoV0; @@ -394,7 +394,7 @@ export async function patch(savePath: string, modules: SaveModule[]) { } else { for (let module of patchInfo.modules) { for (let file of module.files.keys()) { - zip.remove(root.file(file)!.name) + zip.remove(root.file(file)!.name); } } patchInfo.modules = []; diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 93a93364..628db7f0 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -220,65 +220,61 @@ function parseOutput(line: string, source: "stdout" | "stderr"): lib.ParsedFacto level: secLogMatch[1], file: secLogMatch[2], message: secLogMatch[5], - } + }; + } // The other possibility is that the content is a generic message - } else { + return { + source, + format, + time, + type: "generic", + message: secContent, + }; + } + + // The second category of output starts with a date stamp of the format + // "yyyy-mm-dd hh:mm:ss message" + const dateRegex = /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d) (.*)$/; + let dateMatch = dateRegex.exec(line); + if (dateMatch) { + const format = "date"; + const time = dateMatch[1]; + let dateContent = dateMatch[2]; + + // A date output has two general formats. The first is an action + // followed by a message and has a format of "[ACTION] message" + const dateActionRegex = /^\[(\w+)\] (.*)$/; + let dateActionMatch = dateActionRegex.exec(dateContent); + if (dateActionMatch) { return { source, format, time, - type: "generic", - message: secContent, - } + type: "action", + action: dateActionMatch[1], + message: dateActionMatch[2], + }; } - // The second category of output starts with a date stamp of the format - // "yyyy-mm-dd hh:mm:ss message" - } else { - const dateRegex = /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d) (.*)$/; - let dateMatch = dateRegex.exec(line); - if (dateMatch) { - const format = "date"; - const time = dateMatch[1]; - let dateContent = dateMatch[2]; - - // A date output has two general formats. The first is an action - // followed by a message and has a format of "[ACTION] message" - const dateActionRegex = /^\[(\w+)\] (.*)$/; - let dateActionMatch = dateActionRegex.exec(dateContent); - if (dateActionMatch) { - return { - source, - format, - time, - type: "action", - action: dateActionMatch[1], - message: dateActionMatch[2], - } - - // The other format is a generic message - } else { - return { - source, - format, - time, - type: "generic", - message: dateContent, - } - } - - // The last category of output is simply a generic message with no - // formating. - } else { - return { - source, - format: "none", - type: "generic", - message: line, - } - } + // The other format is a generic message + return { + source, + format, + time, + type: "generic", + message: dateContent, + }; } + + // The last category of output is simply a generic message with no + // formating. + return { + source, + format: "none", + type: "generic", + message: line, + }; } // https://stackoverflow.com/a/49402091 @@ -465,7 +461,7 @@ export class FactorioServer extends events.EventEmitter { /** Enable Factorio.com based multiplayer bans **/ enableAuthserverBans: boolean; /** Enable verbose logging */ - verboseLogging: boolean + verboseLogging: boolean; /** Timeout in ms to wait for a response to a shutdown command */ hangTimeout = 5000; @@ -696,9 +692,9 @@ export class FactorioServer extends events.EventEmitter { this._maxConcurrentCommands = value; this.setMaxListeners(value + 5); if (this._rconClient) { - // @ts-ignore + // @ts-expect-error sendQueue is private if (this._rconClient.sendQueue) { - // @ts-ignore + // @ts-expect-error sendQueue is private this._rconClient.sendQueue.maxConcurrent = value; } // XXX: Workaround to suppress bogus event listener warning @@ -852,8 +848,8 @@ export class FactorioServer extends events.EventEmitter { "--config", this.writePath("config.ini"), "--create", this.writePath("saves", name), ...(seed !== undefined ? ["--map-gen-seed", String(seed)] : []), - ...(mapGenSettings !== undefined ? - ["--map-gen-settings", this.writePath("map-gen-settings.json")] : [] + ...(mapGenSettings !== undefined + ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : [] ), ...(mapSettings !== undefined ? ["--map-settings", this.writePath("map-settings.json")] : []), ...(this.verboseLogging ? ["--verbose"] : []), @@ -944,8 +940,8 @@ export class FactorioServer extends events.EventEmitter { "--config", this.writePath("config.ini"), "--start-server-load-scenario", scenario, ...(seed !== undefined ? ["--map-gen-seed", String(seed)] : []), - ...(mapGenSettings !== undefined ? - ["--map-gen-settings", this.writePath("map-gen-settings.json")] : [] + ...(mapGenSettings !== undefined + ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : [] ), ...(mapSettings !== undefined ? ["--map-settings", this.writePath("map-settings.json")] : []), "--port", String(this.gamePort), diff --git a/packages/lib/index.ts b/packages/lib/index.ts index 19583966..428086e3 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -25,7 +25,7 @@ export * from "./src/shared_commands"; export * from "./src/stream"; export * from "./src/users"; export * from "./src/zip_ops"; -export * from "./src/subscriptions" +export * from "./src/subscriptions"; export { default as ExponentialBackoff } from "./src/ExponentialBackoff"; export { default as PlayerStats } from "./src/PlayerStats"; diff --git a/packages/lib/src/RateLimiter.ts b/packages/lib/src/RateLimiter.ts index 16271d0f..b96fbc51 100644 --- a/packages/lib/src/RateLimiter.ts +++ b/packages/lib/src/RateLimiter.ts @@ -27,7 +27,7 @@ export default class RateLimiter { /** * Callback invoked at most `maxRate` per second on activation. */ - action?: Function; + action?: () => void; private _lastRun = 0; private _runTimeout?: ReturnType; @@ -43,7 +43,7 @@ export default class RateLimiter { * @param options.action - * Callback invoked at maste `maxRate` per second on activation. */ - constructor(options: { maxRate?: number, action?: Function } = {}) { + constructor(options: { maxRate?: number, action?: () => void } = {}) { this.maxRate = options.maxRate || 1; this.action = options.action || undefined; } diff --git a/packages/lib/src/config/classes.ts b/packages/lib/src/config/classes.ts index 88614815..a3ce2fa4 100644 --- a/packages/lib/src/config/classes.ts +++ b/packages/lib/src/config/classes.ts @@ -3,7 +3,7 @@ import { Type, Static } from "@sinclair/typebox"; import events from "events"; import isDeepStrictEqual from "../is_deep_strict_equal"; -import { basicType } from"../helpers"; +import { basicType } from "../helpers"; export type ConfigLocation = "controller" | "host" | "instance" | "control"; @@ -748,6 +748,7 @@ export class ConfigGroup { * config. This can also be an async function returning the * value to use. */ + // eslint-disable-next-line complexity static define(item: { access?: ConfigLocation[], type: FieldType, diff --git a/packages/lib/src/config/definitions.ts b/packages/lib/src/config/definitions.ts index 72a5be05..9c878065 100644 --- a/packages/lib/src/config/definitions.ts +++ b/packages/lib/src/config/definitions.ts @@ -264,7 +264,7 @@ export class HostConfig extends classes.Config { get(name: "host.public_address"): string; get(name: "host.max_reconnect_delay"): number; get(name: string): unknown; - get(name: string) { + get(name: string) { return super.get(name); } } @@ -503,7 +503,10 @@ export class ControlConfig extends classes.Config { } ControlConfig.registerGroup(ControlGroup); -function validateGroup(pluginInfo: PluginNodeEnvInfo|PluginWebpackEnvInfo, groupName: "ControllerConfigGroup"|"InstanceConfigGroup") { +function validateGroup( + pluginInfo: PluginNodeEnvInfo | PluginWebpackEnvInfo, + groupName: "ControllerConfigGroup"|"InstanceConfigGroup", +) { if (!(pluginInfo[groupName]?.prototype instanceof classes.PluginConfigGroup)) { throw new Error( `Expected ${groupName} for ${pluginInfo.name} to be a subclass of PluginConfigGroup` diff --git a/packages/lib/src/data/ModPack.ts b/packages/lib/src/data/ModPack.ts index 95d1c31f..14fc7f29 100644 --- a/packages/lib/src/data/ModPack.ts +++ b/packages/lib/src/data/ModPack.ts @@ -1,4 +1,3 @@ -"use strict"; import zlib from "zlib"; import { Type, Static } from "@sinclair/typebox"; diff --git a/packages/lib/src/data/composites.ts b/packages/lib/src/data/composites.ts index 8566185f..c4f912ef 100644 --- a/packages/lib/src/data/composites.ts +++ b/packages/lib/src/data/composites.ts @@ -1,7 +1,7 @@ import { Type } from "@sinclair/typebox"; export function StringEnum(values: [...T]) { - return Type.Unsafe({ type: 'string', enum: values }) + return Type.Unsafe({ type: "string", enum: values }); } export interface JSONDeserialisable { diff --git a/packages/lib/src/data/message_validate.d.ts b/packages/lib/src/data/message_validate.d.ts index fdd40377..d9ec5838 100644 --- a/packages/lib/src/data/message_validate.d.ts +++ b/packages/lib/src/data/message_validate.d.ts @@ -1,4 +1,4 @@ import type { ValidateFunction } from "ajv"; import type { Message } from "./messages_core"; -var message_validate: ValidateFunction; +const message_validate: ValidateFunction; export default message_validate; diff --git a/packages/lib/src/data/messages_controller.ts b/packages/lib/src/data/messages_controller.ts index e39de057..5a0aefc8 100644 --- a/packages/lib/src/data/messages_controller.ts +++ b/packages/lib/src/data/messages_controller.ts @@ -166,6 +166,7 @@ export class LogQueryRequest { json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel, json.limit, json.order ); } + static Response = class Response { constructor( public log: object[], diff --git a/packages/lib/src/data/messages_core.ts b/packages/lib/src/data/messages_core.ts index 7704bc17..a2cab960 100644 --- a/packages/lib/src/data/messages_core.ts +++ b/packages/lib/src/data/messages_core.ts @@ -1,6 +1,7 @@ // Note: Run compile_validator script after making changes to this file. import { TSchema, Type, Static } from "@sinclair/typebox"; -import messageValidate from "./message_validate"; +// eslint-disable-next-line node/no-missing-import +import messageValidate from "./message_validate"; // generated file import { StringEnum } from "./composites"; import { AccountRole } from "../plugin"; diff --git a/packages/lib/src/data/messages_host.ts b/packages/lib/src/data/messages_host.ts index 5389a7c2..92b5452f 100644 --- a/packages/lib/src/data/messages_host.ts +++ b/packages/lib/src/data/messages_host.ts @@ -1,5 +1,6 @@ import { Type, Static } from "@sinclair/typebox"; import { jsonArray, StringEnum } from "./composites"; +import { CollectorResultSerialized } from "../prometheus"; export class HostDetails { constructor( @@ -23,7 +24,15 @@ export class HostDetails { }); static fromJSON(json: Static) { - return new this(json.agent, json.version, json.name, json.id, json.connected, json.publicAddress, json.tokenValidAfter); + return new this( + json.agent, + json.version, + json.name, + json.id, + json.connected, + json.publicAddress, + json.tokenValidAfter, + ); } } @@ -67,11 +76,11 @@ export class HostMetricsRequest { static dst = "host" as const; static Response = class Response { // TODO: Use JSON class pattern in Prometheus constructor( - public results: object[], + public results: CollectorResultSerialized[], ) { } static jsonSchema = Type.Object({ - "results": Type.Array(Type.Object({})), + "results": Type.Array(CollectorResultSerialized), }); static fromJSON(json: Static) { diff --git a/packages/lib/src/data/messages_instance.ts b/packages/lib/src/data/messages_instance.ts index 32cdab2b..d97ca0be 100644 --- a/packages/lib/src/data/messages_instance.ts +++ b/packages/lib/src/data/messages_instance.ts @@ -4,7 +4,7 @@ import { JsonString, StringEnum, jsonArray } from "./composites"; import { RawConfig } from "../config"; import type { User } from "../users"; import type { MessageRequest } from "./messages_core"; -import type { SerializedConfig } from "../config"; +import { CollectorResultSerialized } from "../prometheus"; export type InstanceStatus = @@ -216,11 +216,11 @@ export class InstanceMetricsRequest { static Response = class Response { // TODO: Use JSON class pattern in Prometheus constructor( - public results: object[], + public results: CollectorResultSerialized[], ) { } static jsonSchema = Type.Object({ - "results": Type.Array(Type.Object({})), + "results": Type.Array(CollectorResultSerialized), }); static fromJSON(json: Static) { diff --git a/packages/lib/src/data/messages_user.ts b/packages/lib/src/data/messages_user.ts index be7c1c9d..c50a9339 100644 --- a/packages/lib/src/data/messages_user.ts +++ b/packages/lib/src/data/messages_user.ts @@ -211,7 +211,7 @@ export class RawUser { // TODO refactor into lib/user.User isDeleted: this.isDeleted, }; if (this.playerStats) { - json.playerStats = this.playerStats.toJSON() + json.playerStats = this.playerStats.toJSON(); } if (this.instanceStats) { json.instanceStats = [...this.instanceStats].map(([k, v]) => [k, v.toJSON()]); diff --git a/packages/lib/src/database.ts b/packages/lib/src/database.ts index 3f54c301..aaedd478 100644 --- a/packages/lib/src/database.ts +++ b/packages/lib/src/database.ts @@ -277,8 +277,9 @@ export class ItemDatabase { /** * Allow to iterate through the items. + * @returns array of items. */ getEntries(): IterableIterator<[string, number]> { - return this._items.entries() + return this._items.entries(); } } diff --git a/packages/lib/src/factorio/exchange_string.ts b/packages/lib/src/factorio/exchange_string.ts index 13a4ef6a..3ca83522 100644 --- a/packages/lib/src/factorio/exchange_string.ts +++ b/packages/lib/src/factorio/exchange_string.ts @@ -1,4 +1,3 @@ -"use strict"; import zlib from "zlib"; class MapReaderState { @@ -72,7 +71,7 @@ function readString(state: MapReaderState) { return data; } -function readOptional(state: MapReaderState, readValue: (state: MapReaderState) => T) { +function readOptional(state: MapReaderState, readValue: (s: MapReaderState) => T) { let load = readUInt8(state) !== 0; if (!load) { return null; @@ -80,7 +79,7 @@ function readOptional(state: MapReaderState, readValue: (state: MapReaderStat return readValue(state); } -function readArray(state: MapReaderState, readItem: (state: MapReaderState) => T) { +function readArray(state: MapReaderState, readItem: (s: MapReaderState) => T) { let size = readUInt32so(state); let array: T[] = []; @@ -94,8 +93,8 @@ function readArray(state: MapReaderState, readItem: (state: MapReaderState) = function readDict( state: MapReaderState, - readKey: (state: MapReaderState) => K, - readValue: (state: MapReaderState) => V + readKey: (s: MapReaderState) => K, + readValue: (s: MapReaderState) => V ) { let size = readUInt32so(state); diff --git a/packages/lib/src/helpers.ts b/packages/lib/src/helpers.ts index 37c41e51..a3e58571 100644 --- a/packages/lib/src/helpers.ts +++ b/packages/lib/src/helpers.ts @@ -41,7 +41,7 @@ export async function timeout(promise: Promise, time: number, timeoutResul try { return await Promise.race([ promise, - new Promise(resolve => { + new Promise(resolve => { timer = setTimeout(() => resolve(timeoutResult), time); }), ]); diff --git a/packages/lib/src/is_deep_strict_equal.d.ts b/packages/lib/src/is_deep_strict_equal.d.ts index ec5e4c72..3ec0532c 100644 --- a/packages/lib/src/is_deep_strict_equal.d.ts +++ b/packages/lib/src/is_deep_strict_equal.d.ts @@ -1 +1 @@ -export default function isDeepStrictEqual(a: any, b: any): boolean +export default function isDeepStrictEqual(a: any, b: any): boolean; diff --git a/packages/lib/src/link/connectors.ts b/packages/lib/src/link/connectors.ts index 8345d975..b8b6686c 100644 --- a/packages/lib/src/link/connectors.ts +++ b/packages/lib/src/link/connectors.ts @@ -1,13 +1,12 @@ // Connection adapters for Links -"use strict"; import { strict as assert } from "assert"; import events from "events"; import WebSocket from "./WebSocket"; import * as libData from "../data"; -import * as libErrors from "../errors"; -import { logger } from "../logging"; -import ExponentialBackoff from "../ExponentialBackoff"; +import * as libErrors from "../errors"; +import { logger } from "../logging"; +import ExponentialBackoff from "../ExponentialBackoff"; import type { Request, Event } from "./link"; @@ -102,10 +101,6 @@ export abstract class WebSocketBaseConnector extends BaseConnector { _lastReceivedSeq = undefined; _sendBuffer: (libData.MessageRoutable)[] = []; - constructor(src: libData.Address, dst: libData.Address) { - super(src, dst); - } - _reset() { this._state = "closed"; this._closing = false; diff --git a/packages/lib/src/link/index.ts b/packages/lib/src/link/index.ts index e13d1d8b..d48297e3 100644 --- a/packages/lib/src/link/index.ts +++ b/packages/lib/src/link/index.ts @@ -3,5 +3,5 @@ * @module lib/link * @author Hornwitser */ -export * from "./link" -export * from "./connectors" +export * from "./link"; +export * from "./connectors"; diff --git a/packages/lib/src/link/link.ts b/packages/lib/src/link/link.ts index 73bb633e..998e3a63 100644 --- a/packages/lib/src/link/link.ts +++ b/packages/lib/src/link/link.ts @@ -316,7 +316,7 @@ export class Link { if (!this.router) { let err = new libErrors.InvalidMessage( `Received message addressed to ${(message as libData.MessageRequest).dst} but this link `+ - `does not route messages` + "does not route messages" ); if (message.type === "request") { this.connector.sendResponseError( @@ -401,8 +401,8 @@ export class Link { this.connector.sendResponseError( new libData.ResponseError(err.message, err.code, err.stack), message.src, spoofedSrc ); - } catch (err: any) { - logger.error(`Unexpected error sending error response for ${message.name}:\n${err.stack}`); + } catch (subErr: any) { + logger.error(`Unexpected error sending error response for ${message.name}:\n${subErr.stack}`); } } ); @@ -527,7 +527,7 @@ export class Link { let requestId = this._nextRequestId; this._nextRequestId += 1; this._pendingRequests.set( - requestId, { request: entry, promise, resolve: resolve!, reject: reject!, dst, } + requestId, { request: entry, promise, resolve: resolve!, reject: reject!, dst } ); this.connector.sendRequest(request, requestId, dst); return promise; diff --git a/packages/lib/src/link/messages.ts b/packages/lib/src/link/messages.ts index 0f21bbd0..73868284 100644 --- a/packages/lib/src/link/messages.ts +++ b/packages/lib/src/link/messages.ts @@ -1,11 +1,10 @@ -"use strict"; import * as core from "../data/messages_core"; import * as controller from "../data/messages_controller"; import * as host from "../data/messages_host"; import * as instance from "../data/messages_instance"; import * as mod from "../data/messages_mod"; import * as user from "../data/messages_user"; -import * as subscriptions from "../subscriptions" +import * as subscriptions from "../subscriptions"; import type { RequestClass, EventClass } from "./link"; /** diff --git a/packages/lib/src/logging.ts b/packages/lib/src/logging.ts index 03b08870..c673f98e 100644 --- a/packages/lib/src/logging.ts +++ b/packages/lib/src/logging.ts @@ -1,4 +1,3 @@ -"use strict"; import winston from "winston"; import Transport from "winston-transport"; import { LEVEL, MESSAGE } from "triple-beam"; @@ -45,7 +44,7 @@ export class ConsoleTransport extends Transport { this.filter = options.filter || (() => true); } - log(info: any, callback: Function) { + log(info: any, callback: () => void) { if (!this.filter(info)) { return callback(); } @@ -71,7 +70,7 @@ export class WebConsoleFormat { public options = {} ) { } - transform(info: any, _options: {}) { + transform(info: any, _options: object) { let src = " "; if (info.host_id !== undefined) { src += `s:${info.host_name} - `; @@ -175,5 +174,5 @@ export type Logger = Omit void) { if (this.filter && !this.filter(info)) { return callback(); } diff --git a/packages/lib/src/plugin.ts b/packages/lib/src/plugin.ts index 67623f35..261a684d 100644 --- a/packages/lib/src/plugin.ts +++ b/packages/lib/src/plugin.ts @@ -4,15 +4,12 @@ */ import * as libHelpers from "./helpers"; import type { Logger } from "./logging"; -import type { ConfigGroup } from "./config"; +import type { ConfigGroup, PluginConfigGroup } from "./config"; import type { CollectorResult } from "./prometheus"; import type { ParsedFactorioOutput } from "./logging_utils"; import type { ModPack, ModInfo } from "./data"; import type { Link } from "./link"; import type { CommandTree } from "./command"; -import type { User } from "./users"; -import type { MessageRequest, MessageEvent } from "./data"; -import type { PluginConfigGroup } from "./config"; /** * Conceptual base for controller and instance plugins. @@ -120,6 +117,7 @@ export class BaseInstancePlugin { message: string, expectEmpty: boolean, }[] = []; + private _sendingRconMessages = false; constructor( @@ -364,7 +362,7 @@ export class BaseControllerPlugin { constructor( public info: PluginNodeEnvInfo, public controller: Controller, - public metrics: any[], + public metrics: any, logger: Logger ) { this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger; @@ -655,7 +653,6 @@ export class BaseControlPlugin { } - /** * Plugin supplied login form */ diff --git a/packages/lib/src/plugin_loader.ts b/packages/lib/src/plugin_loader.ts index 6a1a72bf..bb6c3573 100644 --- a/packages/lib/src/plugin_loader.ts +++ b/packages/lib/src/plugin_loader.ts @@ -1,4 +1,4 @@ -/* eslint-disable node/global-require */ +/* eslint-disable @typescript-eslint/no-var-requires */ /** * Helpers for loading plugins in Node.js * @module lib/plugin_loader @@ -89,7 +89,7 @@ export async function loadControllerPluginClass( /** * Load instance plugin class of a plugin - + * * @param pluginInfo - * Plugin info object returned from {@link * loadPluginInfos} to load class from. diff --git a/packages/lib/src/prometheus.ts b/packages/lib/src/prometheus.ts index fd1ab626..d3359fe7 100644 --- a/packages/lib/src/prometheus.ts +++ b/packages/lib/src/prometheus.ts @@ -38,6 +38,8 @@ * app.listen(9100); * @module lib/prometheus */ +import { Type, Static } from "@sinclair/typebox"; +import { StringEnum } from "./data/composites"; /** * Result from collecting a {@link Collector} @@ -1188,7 +1190,7 @@ class HistogramChild { protected _bucketValues: Map; protected _sumValues: Map; protected _countValues: Map; - protected _key: string + protected _key: string; protected _bucketKeys: Map; constructor(collector: Histogram, key: string) { @@ -1438,6 +1440,7 @@ export class Histogram extends LabeledCollector { * Observe a given value and increment matching buckets * * Note: Only works if this is an unlabeled collector. + * @param value - number to count into histogram buckets. */ observe(value: number) { this._defaultChild!.observe(value); @@ -1610,7 +1613,9 @@ function escapeHelp(help: string) { ; } -async function* expositionLines(resultsIterator: AsyncIterable) { +async function* expositionLines( + resultsIterator: AsyncIterable | Iterable +) { let first = true; for await (let result of resultsIterator) { if (first) { @@ -1663,18 +1668,39 @@ async function* expositionLines(resultsIterator: AsyncIterable) * exposition for. Defaults to collecting results from {@link * defaultRegistry}. * @returns Prometheus exposition. - * - * @property exposition.contentType - * HTTP Content-Type for the exposition format that's implemented */ -export async function exposition(resultsIterator = defaultRegistry.collect()) { +export async function exposition( + resultsIterator: AsyncIterable | Iterable = defaultRegistry.collect() +) { let lines = ""; for await (let line of expositionLines(resultsIterator)) { lines += line; } return lines; } -exposition.contentType = "text/plain; version=0.0.4"; +/** + * HTTP Content-Type for the exposition format that's implemented by {@link + * exposition}. + */ +export const expositionContentType = "text/plain; version=0.0.4"; + +export const CollectorResultSerialized = Type.Object({ + metric: Type.Object({ + type: StringEnum(["counter", "gauge", "histogram", "summary", "untyped"]), + name: Type.String(), + help: Type.String(), + labels: Type.Array(Type.String()), + }), + samples: Type.Array( + Type.Tuple([ + Type.String(), + Type.Array( + Type.Tuple([Type.String(), Type.Number()]) + ), + ]), + ), +}); +export type CollectorResultSerialized = Static; /** * Serialize CollectorResult into a plain object @@ -1700,7 +1726,7 @@ export function serializeResult( metricName?: string, metricHelp?: string, } = {} -) { +): CollectorResultSerialized { let { addLabels, metricName = result.metric.name, @@ -1754,7 +1780,7 @@ export function serializeResult( * @returns deserialized result. */ export function deserializeResult( - serializedResult: ReturnType + serializedResult: CollectorResultSerialized ): CollectorResult { return { metric: new Metric( diff --git a/packages/lib/src/schema.ts b/packages/lib/src/schema.ts index d39416e9..de8032f4 100644 --- a/packages/lib/src/schema.ts +++ b/packages/lib/src/schema.ts @@ -22,13 +22,13 @@ export function compile(schema: JSONSchemaType) { } let doValidate: ValidateFunction; - const validate = >function (data) { + const validate = > function (data) { if (!doValidate) { doValidate = ajv.compile(schema); } let result = doValidate(data); validate.errors = doValidate.errors; return result; - } - return validate + }; + return validate; } diff --git a/packages/lib/src/shared_commands.ts b/packages/lib/src/shared_commands.ts index 7c959b93..a85fd1f5 100644 --- a/packages/lib/src/shared_commands.ts +++ b/packages/lib/src/shared_commands.ts @@ -2,7 +2,6 @@ * Implementation of commands shared between controller/host/ctl. * @module lib/shared_commands */ -"use strict"; import path from "path"; import * as libConfig from "./config"; @@ -59,7 +58,7 @@ export async function handlePluginCommand( let pluginInfo: { name: string }; try { - // eslint-disable-next-line node/global-require + // eslint-disable-next-line @typescript-eslint/no-var-requires pluginInfo = require(pluginPath).default; } catch (err: any) { logger.error(`Unable to import plugin info from ${args.path}:\n${err.stack}`); diff --git a/packages/lib/src/stream.ts b/packages/lib/src/stream.ts index 5e2ab36c..315e6a13 100644 --- a/packages/lib/src/stream.ts +++ b/packages/lib/src/stream.ts @@ -1,4 +1,3 @@ -"use strict"; import assert from "assert"; import fs from "fs-extra"; import stream from "stream"; diff --git a/packages/lib/src/subscriptions.ts b/packages/lib/src/subscriptions.ts index ae346b65..eb82e02e 100644 --- a/packages/lib/src/subscriptions.ts +++ b/packages/lib/src/subscriptions.ts @@ -33,16 +33,15 @@ export class SubscriptionResponse { Type.String(), Type.Unknown(), ]), - Type.Null() - ]) + Type.Null(), + ]); toJSON() { if (this.eventReplay) { const entry = Link._eventsByClass.get(this.eventReplay.constructor)!; return [entry.name, this.eventReplay]; - } else { - return null; } + return null; } static fromJSON(json: Static): SubscriptionResponse { @@ -67,7 +66,7 @@ export class SubscriptionResponse { export class SubscriptionRequest { declare ["constructor"]: typeof SubscriptionRequest; static type = "request" as const; - static src = ["control", "instance"] as const; + static src = ["control", "instance"] as const; static dst = "controller" as const; static Response = SubscriptionResponse; static permission(user: User, message: MessageRequest) { @@ -100,7 +99,7 @@ export class SubscriptionRequest { Type.Boolean(), Type.Array(Type.Union([Type.String(), Type.Number()])), Type.Number(), - ]) + ]); toJSON() { return [this.eventName, this.allChannels, this.channels, this.lastRequestTime]; @@ -111,15 +110,17 @@ export class SubscriptionRequest { } } +type EventData = { + subscriptionUpdate?: SubscriptionRequestHandler, + subscriptions: Map, onceClose: () => void }>, +}; + /** * A class component to handle incoming subscription requests and offers a method to broadcast events to subscribers * After creation, no other handler can be registered for SubscriptionRequest on the controller */ export class SubscriptionController { - _events = new Map, - subscriptions: Map, onceClose: () => void }>, - }>(); + _events = new Map(); constructor( private controller: any, // Controller | Link @@ -130,6 +131,9 @@ export class SubscriptionController { /** * Allow clients to subscribe to an event by telling the subscription controller to accept them * Has an optional subscription update handler which is called when any client updates their subscription + * @param Event - Event class which is sent out as updates. + * @param subscriptionUpdate - + * Optional handler called when a client updates its subscriptions. */ handle(Event: EventClass, subscriptionUpdate?: SubscriptionRequestHandler) { const entry = Link._eventsByClass.get(Event); @@ -147,6 +151,7 @@ export class SubscriptionController { /** * Broadcast an event to all subscribers of that event, will be filtered by channel if a ChannelEvent is provided + * @param event - Event to broadcast. */ broadcast(event: Event | ChannelEvent) { const entry = Link._eventsByClass.get(event.constructor); @@ -169,6 +174,11 @@ export class SubscriptionController { /** * Handle incoming event subscription requests + * @param event - incomming event. + * @param src - Source address of incomming request. + * @param dst - destination address of incomming request. + * @returns Response to subscription request. + * @internal */ async _handleRequest(event: SubscriptionRequest, src: Address, dst: Address) { if (!Link._eventsByName.has(event.eventName)) { @@ -192,7 +202,9 @@ export class SubscriptionController { onceClose = () => eventData.subscriptions.delete(link); link.connector.once("close", onceClose); } - eventData.subscriptions.set(link, { all: event.allChannels, channels: event.channels, onceClose: onceClose }); + eventData.subscriptions.set( + link, { all: event.allChannels, channels: event.channels, onceClose: onceClose }, + ); } return new SubscriptionResponse(eventReplay); } @@ -204,15 +216,15 @@ export class SubscriptionController { * Multiple handlers can be subscribed at the same time, on the same or different channels */ export class EventSubscriber { - _callbacks = new Array>() - _channelCallbacks = new Map>>() + _callbacks = new Array>(); + _channelCallbacks = new Map>>(); lastResponse: Event | null = null; lastResponseTime = 0; - control?: Link + control?: Link; constructor( private event: EventClass | ChannelEventClass, - private prehandler?: (event: T) => V, + private prehandler?: (e: T) => V, control?: Link ) { const entry = Link._eventsByClass.get(this.event); @@ -226,6 +238,8 @@ export class EventSubscriber { /** * Handle incoming events and distribute it to the correct callbacks + * @param response - event from subscribed resource + * @internal */ async _handle(response: Event | ChannelEvent) { this.lastResponse = response; @@ -246,10 +260,14 @@ export class EventSubscriber { } /** - * If a control link was not connected at creation, it can be connected here (normally during onControllerConnectionEvent) + * If a control link was not connected at creation, it can be connected + * here (normally during onControllerConnectionEvent) + * @param control - Control link to associate with. */ async connectControl(control: Link) { - if (this.control === control) return; + if (this.control === control) { + return; + } this.control = control; this.control.handle(this.event, this._handle.bind(this)); await this._updateSubscription(); @@ -257,6 +275,8 @@ export class EventSubscriber { /** * Subscribe to receive all event notifications + * @param handler - + * callback invoked whenever the subscribed resource changes. */ async subscribe(handler: EventSubscriberCallback) { this._callbacks.push(handler); @@ -265,6 +285,10 @@ export class EventSubscriber { /** * Subscribe to receive event notifications for the given channel + * @param channel - Channel to subscribe to. + * @param handler - + * callback invoked whenever the subscribed resource changes on the + * given channel. */ async subscribeToChannel(channel: string | number, handler: EventSubscriberCallback) { if (!this._channelCallbacks.get(channel)) { @@ -275,8 +299,14 @@ export class EventSubscriber { } /** - * Unsubscribe from receiving all event notifications, does not effect the subscriptions of other handlers - * Will throw an error if your handler is not subscribed to all channels, does not accept anonymous functions + * Unsubscribe from receiving all event notifications, does not effect + * the subscriptions of other handlers. + * + * @param handler - A callback previously passed to {@link subscribe} + * + * @throws {Error} + * If your handler is not subscribed to all channels, does not + * accept anonymous functions */ async unsubscribe(handler: EventSubscriberCallback) { let index = this._callbacks.lastIndexOf(handler); @@ -289,8 +319,15 @@ export class EventSubscriber { } /** - * Unsubscribe from receiving event notifications for a given channel, does not effect the subscriptions of other handlers - * Will throw an error if your handler is not subscribed to the channel, does not accept anonymous functions + * Unsubscribe from receiving event notifications for a given channel, + * does not effect the subscriptions of other handlers. + * + * @param channel - A channel previously passed to {@link subscribeToChannel} + * @param handler - A callback previously passed to {@link subscribeToChannel} + * + * @throws {Error} + * If your handler is not subscribed to the channel or does not + * accept anonymous functions */ async unsubscribeFromChannel(channel: string | number, handler: EventSubscriberCallback) { const channelCallbacks = this._channelCallbacks.get(channel); @@ -315,7 +352,9 @@ export class EventSubscriber { * Update the subscription with the controller based on current handler counts */ async _updateSubscription() { - if (!this.control || !(this.control.connector as WebSocketClientConnector).connected) return; + if (!this.control || !(this.control.connector as WebSocketClientConnector).connected) { + return; + } const entry = Link._eventsByClass.get(this.event)!; const response = await this.control.send(new SubscriptionRequest( diff --git a/packages/lib/src/users.ts b/packages/lib/src/users.ts index 5990db08..53bc91f7 100644 --- a/packages/lib/src/users.ts +++ b/packages/lib/src/users.ts @@ -6,7 +6,6 @@ * @author Hornwitser * @module lib/users */ -"use strict"; import * as libErrors from "./errors"; import PlayerStats from "./PlayerStats"; @@ -328,6 +327,7 @@ export class User { } return playerStats; } + /** * Set of users currently online in the cluster. */ diff --git a/packages/web_ui/global.d.ts b/packages/web_ui/global.d.ts index e1a04c39..2bb8472e 100644 --- a/packages/web_ui/global.d.ts +++ b/packages/web_ui/global.d.ts @@ -1,5 +1,5 @@ -declare module '*.png' { - const value: import('react-native').ImageSourcePropType; +declare module "*.png" { + const value: import("react-native").ImageSourcePropType; export default value; } diff --git a/packages/web_ui/src/components/BaseConfigTree.tsx b/packages/web_ui/src/components/BaseConfigTree.tsx index 99c8c69d..93aab9ea 100644 --- a/packages/web_ui/src/components/BaseConfigTree.tsx +++ b/packages/web_ui/src/components/BaseConfigTree.tsx @@ -103,7 +103,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) { title="A restart is required for this setting to take effect" >; - + type ChildNode = { key: string; children: any[]; diff --git a/packages/web_ui/src/components/HostViewPage.tsx b/packages/web_ui/src/components/HostViewPage.tsx index 1d2524db..a620a9df 100644 --- a/packages/web_ui/src/components/HostViewPage.tsx +++ b/packages/web_ui/src/components/HostViewPage.tsx @@ -41,8 +41,8 @@ export default function HostViewPage() { } let hostButtons = { - account.hasPermission("core.host.revoke_access") && - { control.send(new lib.HostRevokeTokensRequest(host.id!)) .then(() => notify("Host tokens revoked")) - .catch(notifyErrorHandler(`Error revoking tokens for host id:${host.id}`)) + .catch(notifyErrorHandler(`Error revoking tokens for host id:${host.id}`)); }} > - } - + } + ; return {host["agent"]} {host["version"]} { - host.tokenValidAfter && - - {formatTimestamp(host.tokenValidAfter*1000)} + host.tokenValidAfter + && + {formatTimestamp(host.tokenValidAfter*1000)} } diff --git a/packages/web_ui/src/components/HostsPage.tsx b/packages/web_ui/src/components/HostsPage.tsx index c6123351..8b70a713 100644 --- a/packages/web_ui/src/components/HostsPage.tsx +++ b/packages/web_ui/src/components/HostsPage.tsx @@ -37,7 +37,7 @@ function GenerateHostTokenButton() { }, []); async function generateToken() { - let id = undefined; + let id; let values = form.getFieldsValue(); if (values.hostId) { id = Number.parseInt(values.hostId, 10); @@ -61,8 +61,7 @@ function GenerateHostTokenButton() { }, [open]); // Only install plugins that aren't filesystem paths. Npm modules have max 1 forward slash in their name. - //@ts-ignore TODO::: this doesn't filter anything, `x.split("/") <= 1` is always = true. repair or shouldn't filter ? - const pluginString = pluginList.map(p => `"${p.requirePath}"`).filter(x => x.split("/") <= 1).join(" "); + const pluginString = pluginList.map(p => `"${p.requirePath}"`).filter(x => x.split("/").length <= 1).join(" "); return <> - } - /> - ; - } + let instance = props.instance; + let instanceId = instance.id!; let instanceButtonMenuItems: ItemType[] = []; if (account.hasPermission("core.instance.export_data")) { @@ -174,7 +145,7 @@ export default function InstanceViewPage() { } }, }; - let instanceButtons = + return { account.hasAnyPermission("core.instance.start", "core.instance.stop") && @@ -189,11 +160,46 @@ export default function InstanceViewPage() { } ; +} + +export default function InstanceViewPage() { + let params = useParams(); + let instanceId = Number(params.id); + + let navigate = useNavigate(); + + let account = useAccount(); + let [instance] = useInstance(instanceId); + let [host] = useHost(Number(instance.assignedHost)); + + let nav = [{ name: "Instances", path: "/instances" }, { name: instance.name || "Unknown" }]; + if (instance.loading) { + return ; + } + + if (instance.missing || instance.status === "deleted") { + return + Instance with id {instanceId} was not found on the controller.} + type="warning" + action={ + + } + /> + ; + } return } /> diff --git a/packages/web_ui/src/components/ModDetails.tsx b/packages/web_ui/src/components/ModDetails.tsx index 1506f15e..08c051af 100644 --- a/packages/web_ui/src/components/ModDetails.tsx +++ b/packages/web_ui/src/components/ModDetails.tsx @@ -6,16 +6,19 @@ import * as lib from "@clusterio/lib"; const { useBreakpoint } = Grid; -type ModDetailsProps = { - mod: lib.ModInfo|lib.ModRecord; - actions: (mod: lib.ModInfo|lib.ModRecord) => React.JSX.Element; +type ModDetailsProps = { + mod: T; + actions: (mod: T) => React.JSX.Element; }; -export default function ModDetails(props: ModDetailsProps) { +export default function ModDetails(props: ModDetailsProps) { let screens = useBreakpoint(); - const mod = props.mod; + const mod: Partial & Partial = props.mod; return - {!(mod instanceof lib.ModInfo) && mod.error && Error} span={2}> + {mod.error && Error} + span={2} + > {mod.error === "missing" && "The given version of this mod is missing from the controller storage."} {mod.error === "bad_checksum" && "Checksum of the mod in the controller storage does not match the checksum provided " + @@ -23,42 +26,38 @@ export default function ModDetails(props: ModDetailsProps) { } } {props.actions && !screens.lg - && {props.actions(mod)} + && {props.actions(props.mod)} } - { (mod instanceof lib.ModInfo) && <> - {mod.title && {mod.title}} - {mod.version && {mod.version}} - {mod.description && {mod.description} } - {mod.author && {mod.author}} - } + {mod.title && {mod.title}} + {mod.version && {mod.version}} + {mod.description && {mod.description} } + {mod.author && {mod.author}} {`https://mods.factorio.com/mod/${mod.name}`} - - { (mod instanceof lib.ModInfo) && <> - {mod.contact && {mod.contact}} - {mod.homepage && {mod.homepage}} - {mod.name} - {mod.factorioVersion - && {mod.factorioVersion} - } - {mod.filename && {mod.filename}} - {mod.size !== undefined - && {lib.formatBytes(mod.size)} - } - {mod.sha1 && {mod.sha1}} - {mod.dependencies && mod.dependencies.length - && - {mod.dependencies - .filter(e => !e.startsWith("(?)")) - .map((e, i) => {e}
) - } -
- } - } + + {mod.contact && {mod.contact}} + {mod.homepage && {mod.homepage}} + {mod.name} + {mod.factorioVersion + && {mod.factorioVersion} + } + {mod.filename && {mod.filename}} + {mod.size !== undefined + && {lib.formatBytes(mod.size)} + } + {mod.sha1 && {mod.sha1}} + {mod.dependencies && mod.dependencies.length + && + {mod.dependencies + .filter(e => !e.startsWith("(?)")) + .map((e, i) => {e}
) + } +
+ }
; } diff --git a/packages/web_ui/src/components/ModPackViewPage.tsx b/packages/web_ui/src/components/ModPackViewPage.tsx index 25e8d57f..39ad5c06 100644 --- a/packages/web_ui/src/components/ModPackViewPage.tsx +++ b/packages/web_ui/src/components/ModPackViewPage.tsx @@ -70,7 +70,7 @@ function SearchModsTable(props: SearchModsTableProps) { const [modResultPage, setModResultPage] = useState(1); const [modResultPageSize, setModResultPageSize] = useState(10); const [modResultCount, setModResultCount] = useState(2); - const [modResultSelectedVersion, setModResultSelectedVersion] = useState>(new Map()); + const [modResultSelectedVersion, setModResultSelectedVersion] = useState>(new Map()); const factorioVersion = props.modPack.factorioVersion.split(".").slice(0, 2).join("."); @@ -216,7 +216,9 @@ function SearchModsTable(props: SearchModsTableProps) { title: "Action", key: "action", responsive: ["lg"], - render: (result:ModResult) => actions(result.versions[modResultSelectedVersion.get(result.name) || 0]), + render: (result:ModResult) => ( + actions(result.versions[modResultSelectedVersion.get(result.name) || 0]) + ), }, ]} expandable={{ @@ -294,11 +296,13 @@ function ModsTable(props: ModsTableProps) { >remove} {(deletedMods.has(mod.name) || changedMods.has(mod.name)) && { - props.onRevert && props.onRevert({ - type: deletedMods.has(mod.name) ? "mods.delete" : "mods.set", - name: mod.name, - value: mod, - }); + if (props.onRevert) { + props.onRevert({ + type: deletedMods.has(mod.name) ? "mods.delete" : "mods.set", + name: mod.name, + value: mod, + }); + } }} >revert}
; @@ -337,7 +341,9 @@ function ModsTable(props: ModsTableProps) { }); } } - const added = selectedRowKeys.filter(key => !props.modPack.mods.get(key as string)?.enabled) as string[]; + const added = selectedRowKeys.filter( + key => !props.modPack.mods.get(key as string)?.enabled + ) as string[]; const removed = [...props.modPack.mods.values()].filter( mod => mod.enabled && selectedRowKeys.indexOf(mod.name) === -1 ).map(mod => mod.name); diff --git a/packages/web_ui/src/components/RoleViewPage.tsx b/packages/web_ui/src/components/RoleViewPage.tsx index 3d4ae548..19137389 100644 --- a/packages/web_ui/src/components/RoleViewPage.tsx +++ b/packages/web_ui/src/components/RoleViewPage.tsx @@ -14,9 +14,9 @@ import { notifyErrorHandler } from "../util/notify"; export type RawRoleState = Partial & { - loading?: Boolean; - missing?: Boolean; - present?: Boolean; + loading?: boolean; + missing?: boolean; + present?: boolean; }; function useRole(id: number): [RawRoleState, () => void] { let control = useContext(ControlContext); diff --git a/packages/web_ui/src/components/SavesList.tsx b/packages/web_ui/src/components/SavesList.tsx index d69d1cb9..bc707dd3 100644 --- a/packages/web_ui/src/components/SavesList.tsx +++ b/packages/web_ui/src/components/SavesList.tsx @@ -17,7 +17,6 @@ import { useSaves } from "../model/saves"; import { notifyErrorHandler } from "../util/notify"; - type ModalProps = { disabled: boolean; save: lib.SaveDetails; @@ -150,7 +149,9 @@ function TransferModal(props: ModalProps) {