Merge pull request #517 from Hornwitser/setup-typescript-eslint

Setup TypeScript ESLint
This commit is contained in:
Hornwitser authored and GitHub committed 2023-11-20 16:04:47 +01:00
commit 82ed0adfc3
86 files changed
+697 -540

No files matched your search

+45 -16
View File
@@ -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"],
},
}],
},
};
+4 -4
View File
@@ -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
+2
View File
@@ -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",
+9 -11
View File
@@ -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<InitializeParameters> {
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<InitializeParameters> {
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<InitializeParameters> {
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;
+2 -2
View File
@@ -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"
+10 -14
View File
@@ -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;
+62 -59
View File
@@ -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<typeof setInterval> | 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<Map<number, HostInfo>> {
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<Map<number, lib.ModPack>> {
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<Map<string,lib.ModInfo>> {
static async loadModInfos(modsDirectory: string): Promise<Map<string, lib.ModInfo>> {
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<any> {
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"))
+7 -5
View File
@@ -13,7 +13,7 @@ export interface HostInfo {
id: number;
name: string;
version: string;
plugins: [number, any][];
plugins: Record<string, string>;
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<string, Object>;
plugins: Map<string, string>;
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(
+2 -1
View File
@@ -26,7 +26,8 @@ export default class HttpCloser {
private _responses: Set<http.ServerResponse>;
/**
* 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;
+2 -2
View File
@@ -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,
+3 -2
View File
@@ -18,7 +18,7 @@ export default class UserManager {
async load(filePath: string): Promise<void> {
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);
+5 -4
View File
@@ -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<void> {
@@ -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);
};
+1 -3
View File
@@ -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
+27 -29
View File
@@ -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<lib.CollectorResult> | Iterable<lib.CollectorResult>
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<any>[] = [];
let requests: Promise<InstanceType<typeof lib.HostMetricsRequest["Response"]> | 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<string, any> = new Map();
let resultMap: Map<string, lib.CollectorResultSerialized> = 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<string, Map<string, number>>
for (let result of resultMap.values()) {
results.push(lib.deserializeResult(result));
}
// lib.exposition expect AsyncIterable<CollectorResult> 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}`);
+1 -1
View File
@@ -5,6 +5,6 @@
],
"include": ["controller.ts", "package.json", "src/**/*.ts"],
"compilerOptions": {
"outDir": "dist/controller",
"outDir": "dist",
},
}
+1
View File
@@ -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.
+18 -18
View File
@@ -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;
},
+1
View File
@@ -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 <command> [options]")
+1 -1
View File
@@ -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",
+3 -4
View File
@@ -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<InstanceType<typeof lib.InstanceMetricsRequest["Response"]>>[] = [];
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));
+7 -6
View File
@@ -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<string, lib.BaseInstancePlugin>;
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<typeof lib.serializeResult>[] = [];
if (!["stopped", "stopping"].includes(this._status)) {
let pluginResults = await lib.invokeHook(this.plugins, "onMetrics");
type ResultIterator = AsyncIterable<lib.CollectorResult> | Iterable<lib.CollectorResult>
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"),
-1
View File
@@ -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";
+5 -5
View File
@@ -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<string, Buffer>,
public files = new Map<string, Buffer>(),
) { }
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 = [];
+53 -57
View File
@@ -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),
+1 -1
View File
@@ -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";
+2 -2
View File
@@ -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<typeof setTimeout>;
@@ -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;
}
+2 -1
View File
@@ -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,
+5 -2
View File
@@ -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`
-1
View File
@@ -1,4 +1,3 @@
"use strict";
import zlib from "zlib";
import { Type, Static } from "@sinclair/typebox";
+1 -1
View File
@@ -1,7 +1,7 @@
import { Type } from "@sinclair/typebox";
export function StringEnum<T extends string[]>(values: [...T]) {
return Type.Unsafe<T[number]>({ type: 'string', enum: values })
return Type.Unsafe<T[number]>({ type: "string", enum: values });
}
export interface JSONDeserialisable<T> {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ValidateFunction } from "ajv";
import type { Message } from "./messages_core";
var message_validate: ValidateFunction<Message>;
const message_validate: ValidateFunction<Message>;
export default message_validate;
@@ -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[],
+2 -1
View File
@@ -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";
+12 -3
View File
@@ -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<typeof this.jsonSchema>) {
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<typeof this.jsonSchema>) {
+3 -3
View File
@@ -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<typeof this.jsonSchema>) {
+1 -1
View File
@@ -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()]);
+2 -1
View File
@@ -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();
}
}
+4 -5
View File
@@ -1,4 +1,3 @@
"use strict";
import zlib from "zlib";
class MapReaderState {
@@ -72,7 +71,7 @@ function readString(state: MapReaderState) {
return data;
}
function readOptional<T>(state: MapReaderState, readValue: (state: MapReaderState) => T) {
function readOptional<T>(state: MapReaderState, readValue: (s: MapReaderState) => T) {
let load = readUInt8(state) !== 0;
if (!load) {
return null;
@@ -80,7 +79,7 @@ function readOptional<T>(state: MapReaderState, readValue: (state: MapReaderStat
return readValue(state);
}
function readArray<T>(state: MapReaderState, readItem: (state: MapReaderState) => T) {
function readArray<T>(state: MapReaderState, readItem: (s: MapReaderState) => T) {
let size = readUInt32so(state);
let array: T[] = [];
@@ -94,8 +93,8 @@ function readArray<T>(state: MapReaderState, readItem: (state: MapReaderState) =
function readDict<K, V>(
state: MapReaderState,
readKey: (state: MapReaderState) => K,
readValue: (state: MapReaderState) => V
readKey: (s: MapReaderState) => K,
readValue: (s: MapReaderState) => V
) {
let size = readUInt32so(state);
+1 -1
View File
@@ -41,7 +41,7 @@ export async function timeout<T>(promise: Promise<T>, time: number, timeoutResul
try {
return await Promise.race([
promise,
new Promise(resolve => {
new Promise<T>(resolve => {
timer = setTimeout(() => resolve(timeoutResult), time);
}),
]);
+1 -1
View File
@@ -1 +1 @@
export default function isDeepStrictEqual(a: any, b: any): boolean
export default function isDeepStrictEqual(a: any, b: any): boolean;
+3 -8
View File
@@ -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;
+2 -2
View File
@@ -3,5 +3,5 @@
* @module lib/link
* @author Hornwitser
*/
export * from "./link"
export * from "./connectors"
export * from "./link";
export * from "./connectors";
+4 -4
View File
@@ -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;
+1 -2
View File
@@ -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";
/**
+3 -4
View File
@@ -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<winston.Logger,
info: winston.LeveledLogMethod,
server: winston.LeveledLogMethod,
verbose: winston.LeveledLogMethod,
child(options: Object): Logger,
child(options: object): Logger,
};
+1 -2
View File
@@ -4,7 +4,6 @@
* @author Hornwitser
* @module lib/logging_utils
*/
"use strict";
import fs from "fs-extra";
import path from "path";
import stream from "stream";
@@ -202,7 +201,7 @@ export class LinkTransport extends Transport {
this.filter = options.filter;
}
log(info: any, callback: Function) {
log(info: any, callback: () => void) {
if (this.filter && !this.filter(info)) {
return callback();
}
+3 -6
View File
@@ -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
*/
+2 -2
View File
@@ -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.
+35 -9
View File
@@ -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<string, number>;
protected _sumValues: Map<string, number>;
protected _countValues: Map<string, number>;
protected _key: string
protected _key: string;
protected _bucketKeys: Map<number, string>;
constructor(collector: Histogram, key: string) {
@@ -1438,6 +1440,7 @@ export class Histogram extends LabeledCollector<HistogramChild> {
* 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<CollectorResult>) {
async function* expositionLines(
resultsIterator: AsyncIterable<CollectorResult> | Iterable<CollectorResult>
) {
let first = true;
for await (let result of resultsIterator) {
if (first) {
@@ -1663,18 +1668,39 @@ async function* expositionLines(resultsIterator: AsyncIterable<CollectorResult>)
* 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<CollectorResult> | Iterable<CollectorResult> = 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<typeof CollectorResultSerialized>;
/**
* 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<typeof serializeResult>
serializedResult: CollectorResultSerialized
): CollectorResult {
return {
metric: new Metric(
+3 -3
View File
@@ -22,13 +22,13 @@ export function compile<T>(schema: JSONSchemaType<T>) {
}
let doValidate: ValidateFunction<T>;
const validate = <ValidateFunction<T>>function (data) {
const validate = <ValidateFunction<T>> function (data) {
if (!doValidate) {
doValidate = ajv.compile(schema);
}
let result = doValidate(data);
validate.errors = doValidate.errors;
return result;
}
return validate
};
return validate;
}
+1 -2
View File
@@ -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}`);
-1
View File
@@ -1,4 +1,3 @@
"use strict";
import assert from "assert";
import fs from "fs-extra";
import stream from "stream";
+61 -22
View File
@@ -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<typeof SubscriptionResponse.jsonSchema>): 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<unknown>,
subscriptions: Map<Link, { all: boolean, channels: Array<string | number>, 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<string, {
subscriptionUpdate?: SubscriptionRequestHandler<unknown>,
subscriptions: Map<Link, { all: boolean, channels: Array<string | number>, onceClose: () => void }>,
}>();
_events = new Map<string, EventData>();
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<T>(Event: EventClass<T>, subscriptionUpdate?: SubscriptionRequestHandler<T>) {
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<T>(event: Event<T> | ChannelEvent<T>) {
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<T, V=T> {
_callbacks = new Array<EventSubscriberCallback<V>>()
_channelCallbacks = new Map<string | number, Array<EventSubscriberCallback<V>>>()
_callbacks = new Array<EventSubscriberCallback<V>>();
_channelCallbacks = new Map<string | number, Array<EventSubscriberCallback<V>>>();
lastResponse: Event<T> | null = null;
lastResponseTime = 0;
control?: Link
control?: Link;
constructor(
private event: EventClass<T> | ChannelEventClass<T>,
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<T, V=T> {
/**
* Handle incoming events and distribute it to the correct callbacks
* @param response - event from subscribed resource
* @internal
*/
async _handle(response: Event<T> | ChannelEvent<T>) {
this.lastResponse = response;
@@ -246,10 +260,14 @@ export class EventSubscriber<T, V=T> {
}
/**
* 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<T, V=T> {
/**
* Subscribe to receive all event notifications
* @param handler -
* callback invoked whenever the subscribed resource changes.
*/
async subscribe(handler: EventSubscriberCallback<V>) {
this._callbacks.push(handler);
@@ -265,6 +285,10 @@ export class EventSubscriber<T, V=T> {
/**
* 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<V>) {
if (!this._channelCallbacks.get(channel)) {
@@ -275,8 +299,14 @@ export class EventSubscriber<T, V=T> {
}
/**
* 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<V>) {
let index = this._callbacks.lastIndexOf(handler);
@@ -289,8 +319,15 @@ export class EventSubscriber<T, V=T> {
}
/**
* 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<V>) {
const channelCallbacks = this._channelCallbacks.get(channel);
@@ -315,7 +352,9 @@ export class EventSubscriber<T, V=T> {
* 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(
+1 -1
View File
@@ -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.
*/
+2 -2
View File
@@ -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;
}
@@ -103,7 +103,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
title="A restart is required for this setting to take effect"
><ReloadOutlined/></Tooltip>;
type ChildNode = {
key: string;
children: any[];
@@ -41,8 +41,8 @@ export default function HostViewPage() {
}
let hostButtons = <Space> {
account.hasPermission("core.host.revoke_access") &&
<Popconfirm
account.hasPermission("core.host.revoke_access")
&& <Popconfirm
title={`Revoke tokens of ${host.name}?`}
placement="bottomRight"
okText="Revoke Tokens"
@@ -50,15 +50,15 @@ export default function HostViewPage() {
onConfirm={() => {
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}`));
}}
>
<Button danger>
Revoke tokens
</Button>
</Popconfirm>
}
</Space>
}
</Space>;
return <PageLayout nav={nav}>
<PageHeader
@@ -75,9 +75,9 @@ export default function HostViewPage() {
<Descriptions.Item label="Agent">{host["agent"]}</Descriptions.Item>
<Descriptions.Item label="Version">{host["version"]}</Descriptions.Item>
{
host.tokenValidAfter &&
<Descriptions.Item label="Tokens valid after:">
{formatTimestamp(host.tokenValidAfter*1000)}
host.tokenValidAfter
&& <Descriptions.Item label="Tokens valid after:">
{formatTimestamp(host.tokenValidAfter*1000)}
</Descriptions.Item>
}
</Descriptions>
+13 -16
View File
@@ -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 <>
<Button
onClick={() => { setOpen(true); }}
@@ -114,12 +113,12 @@ function GenerateHostTokenButton() {
<div className="codeblock">
<CopyButton
message={"Copied host setup commands to clipboard"}
text={`
mkdir clusterio
cd clusterio
`+
// eslint-disable-next-line max-len
`npm init "@clusterio" -- --controller-token ${token} --mode "host" --download-headless --controller-url ${document.location.origin}/ --host-name "Host ${hostId || "?"}" --public-address localhost ${pluginString.length ? "--plugins" : ""} ${pluginString}`
text={`\
mkdir clusterio
cd clusterio
npm init "@clusterio" -- --controller-token ${token} --mode "host" --download-headless \
--controller-url ${document.location.origin}/ --host-name "Host ${hostId || "?"}" \
--public-address localhost ${pluginString.length ? "--plugins" : ""} ${pluginString}`
}/>
<p>&gt; mkdir clusterio</p>
<p>&gt; cd clusterio</p>
@@ -141,12 +140,11 @@ function GenerateHostTokenButton() {
}
function CopyButton({ text, message }: { text:string, message:string }) {
let [clipboardPermision, setClipboardPermission] = useState("granted");
let [clipboardPermision, setClipboardPermission] = useState<PermissionState>("granted");
useEffect(() => {
(async () => {
//@ts-ignore https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write
// @ts-expect-error missing API https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write
let result = await navigator.permissions.query({ name: "clipboard-write" });
// result.state is "granted", "denied" or "prompt"
setClipboardPermission(result.state);
result.onchange = function () {
setClipboardPermission(result.state);
@@ -156,9 +154,8 @@ function CopyButton({ text, message }: { text:string, message:string }) {
async function checkClipboardPermission() {
try {
//@ts-ignore https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write
// @ts-expect-error missing API https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write
let result = await navigator.permissions.query({ name: "clipboard-write" });
// result.state is "granted", "denied" or "prompt"
setClipboardPermission(result.state);
result.onchange = function () {
setClipboardPermission(result.state);
@@ -166,7 +163,7 @@ function CopyButton({ text, message }: { text:string, message:string }) {
return result.state === "granted";
} catch (err: any) {
//If it fail because "clipboard-write" is not supported.
// If it fail because "clipboard-write" is not supported.
return err.name === "TypeError";
}
}
@@ -229,7 +226,7 @@ export default function HostsPage() {
{host.connected ? "Connected" : "Disconnected"}
</Tag>,
sorter: (a, b) => Number(a.connected) - Number(b.connected),
}
},
]}
dataSource={hostList}
rowKey={host => host.id}
@@ -99,7 +99,7 @@ export default function InstanceList(props: InstanceListProps) {
responsive: ["sm"],
align: "right",
width: 100,
})
});
}
if (props.hideAssignedHost) {
@@ -1,7 +1,7 @@
import React, { useContext, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Alert, Button, Descriptions, Dropdown, Menu, MenuProps, Modal, Space, Spin, Typography } from "antd";
import type { ItemType } from 'antd/es/menu/hooks/useItems';
import type { ItemType } from "antd/es/menu/hooks/useItems";
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
import DownOutlined from "@ant-design/icons/DownOutlined";
@@ -60,42 +60,13 @@ function InstanceDescription(props: InstanceDescriptionProps) {
</Descriptions>;
}
export default function InstanceViewPage() {
let params = useParams();
let instanceId = Number(params.id);
let navigate = useNavigate();
function InstanceButtons(props: { instance: InstanceState }) {
let account = useAccount();
let control = useContext(ControlContext);
let [instance] = useInstance(instanceId);
let [host] = useHost(Number(instance.assignedHost));
let navigate = useNavigate();
let [exportingData, setExportingData] = useState(false);
let nav = [{ name: "Instances", path: "/instances" }, { name: instance.name || "Unknown" }];
if (instance.loading) {
return <PageLayout nav={nav}><Spin size="large" /></PageLayout>;
}
if (instance.missing || instance.status === "deleted") {
return <PageLayout nav={nav}>
<Alert
message={instance.status === "deleted" ? "Instance has been deleted" : "Instance not found" }
showIcon
description={<>Instance with id {instanceId} was not found on the controller.</>}
type="warning"
action={
<Button
type="text"
onClick={() => { navigate("/instances"); }}
>
Go back to instances list
</Button>
}
/>
</PageLayout>;
}
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 = <Space>
return <Space>
{
account.hasAnyPermission("core.instance.start", "core.instance.stop")
&& <StartStopInstanceButton instance={instance} />
@@ -189,11 +160,46 @@ export default function InstanceViewPage() {
<Button>More <DownOutlined /></Button>
</Dropdown>}
</Space>;
}
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 <PageLayout nav={nav}><Spin size="large" /></PageLayout>;
}
if (instance.missing || instance.status === "deleted") {
return <PageLayout nav={nav}>
<Alert
message={instance.status === "deleted" ? "Instance has been deleted" : "Instance not found" }
showIcon
description={<>Instance with id {instanceId} was not found on the controller.</>}
type="warning"
action={
<Button
type="text"
onClick={() => { navigate("/instances"); }}
>
Go back to instances list
</Button>
}
/>
</PageLayout>;
}
return <PageLayout nav={nav}>
<PageHeader
title={instance.name??""}
extra={instanceButtons}
extra={<InstanceButtons instance={instance}/>}
/>
<InstanceDescription host={host} instance={instance} />
+34 -35
View File
@@ -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<T> = {
mod: T;
actions: (mod: T) => React.JSX.Element;
};
export default function ModDetails(props: ModDetailsProps) {
export default function ModDetails<T extends lib.ModInfo | lib.ModRecord>(props: ModDetailsProps<T>) {
let screens = useBreakpoint();
const mod = props.mod;
const mod: Partial<lib.ModInfo> & Partial<lib.ModRecord> = props.mod;
return <Descriptions className="borderless" bordered size="small" column={{ xs: 1, sm: 1, md: 1, lg: 2 }}>
{!(mod instanceof lib.ModInfo) && mod.error && <Descriptions.Item label={<><ExclamationCircleOutlined/> Error</>} span={2}>
{mod.error && <Descriptions.Item
label={<><ExclamationCircleOutlined/> 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) {
}
</Descriptions.Item>}
{props.actions && !screens.lg
&& <Descriptions.Item label="Action">{props.actions(mod)}</Descriptions.Item>
&& <Descriptions.Item label="Action">{props.actions(props.mod)}</Descriptions.Item>
}
{ (mod instanceof lib.ModInfo) && <>
{mod.title && <Descriptions.Item label="Title">{mod.title}</Descriptions.Item>}
{mod.version && <Descriptions.Item label="Version" span={mod.title ? 1 : 2}>{mod.version}</Descriptions.Item>}
{mod.description && <Descriptions.Item label="Description" span={2}>{mod.description}</Descriptions.Item> }
{mod.author && <Descriptions.Item label="Author" span={2}>{mod.author}</Descriptions.Item>}
</>}
{mod.title && <Descriptions.Item label="Title">{mod.title}</Descriptions.Item>}
{mod.version && <Descriptions.Item label="Version" span={mod.title ? 1 : 2}>{mod.version}</Descriptions.Item>}
{mod.description && <Descriptions.Item label="Description" span={2}>{mod.description}</Descriptions.Item> }
{mod.author && <Descriptions.Item label="Author" span={2}>{mod.author}</Descriptions.Item>}
<Descriptions.Item label="Mod&nbsp;Portal" span={2}>
<Typography.Link href={`https://mods.factorio.com/mod/${mod.name}`}>
{`https://mods.factorio.com/mod/${mod.name}`}
</Typography.Link>
</Descriptions.Item>
{ (mod instanceof lib.ModInfo) && <>
{mod.contact && <Descriptions.Item label="Contact" span={2}>{mod.contact}</Descriptions.Item>}
{mod.homepage && <Descriptions.Item label="Homepage" span={2}>{mod.homepage}</Descriptions.Item>}
<Descriptions.Item label="Internal&nbsp;Name" span={mod.factorioVersion ? 1 : 2}>{mod.name}</Descriptions.Item>
{mod.factorioVersion
&& <Descriptions.Item label="Factorio&nbsp;Version">{mod.factorioVersion}</Descriptions.Item>
}
{mod.filename && <Descriptions.Item label="Filename">{mod.filename}</Descriptions.Item>}
{mod.size !== undefined
&& <Descriptions.Item label="Size">{lib.formatBytes(mod.size)}</Descriptions.Item>
}
{mod.sha1 && <Descriptions.Item label="SHA1" span={2}>{mod.sha1}</Descriptions.Item>}
{mod.dependencies && mod.dependencies.length
&& <Descriptions.Item label="Dependencies" span={2}>
{mod.dependencies
.filter(e => !e.startsWith("(?)"))
.map((e, i) => <Fragment key={i}>{e}<br/></Fragment>)
}
</Descriptions.Item>
}
</>}
{mod.contact && <Descriptions.Item label="Contact" span={2}>{mod.contact}</Descriptions.Item>}
{mod.homepage && <Descriptions.Item label="Homepage" span={2}>{mod.homepage}</Descriptions.Item>}
<Descriptions.Item label="Internal&nbsp;Name" span={mod.factorioVersion ? 1 : 2}>{mod.name}</Descriptions.Item>
{mod.factorioVersion
&& <Descriptions.Item label="Factorio&nbsp;Version">{mod.factorioVersion}</Descriptions.Item>
}
{mod.filename && <Descriptions.Item label="Filename">{mod.filename}</Descriptions.Item>}
{mod.size !== undefined
&& <Descriptions.Item label="Size">{lib.formatBytes(mod.size)}</Descriptions.Item>
}
{mod.sha1 && <Descriptions.Item label="SHA1" span={2}>{mod.sha1}</Descriptions.Item>}
{mod.dependencies && mod.dependencies.length
&& <Descriptions.Item label="Dependencies" span={2}>
{mod.dependencies
.filter(e => !e.startsWith("(?)"))
.map((e, i) => <Fragment key={i}>{e}<br/></Fragment>)
}
</Descriptions.Item>
}
</Descriptions>;
}
@@ -70,7 +70,7 @@ function SearchModsTable(props: SearchModsTableProps) {
const [modResultPage, setModResultPage] = useState<number>(1);
const [modResultPageSize, setModResultPageSize] = useState<number>(10);
const [modResultCount, setModResultCount] = useState<number>(2);
const [modResultSelectedVersion, setModResultSelectedVersion] = useState<Map<string,number>>(new Map());
const [modResultSelectedVersion, setModResultSelectedVersion] = useState<Map<string, number>>(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</Typography.Link>}
{(deletedMods.has(mod.name) || changedMods.has(mod.name)) && <Typography.Link
onClick={() => {
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</Typography.Link>}
</Space>;
@@ -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);
@@ -14,9 +14,9 @@ import { notifyErrorHandler } from "../util/notify";
export type RawRoleState = Partial<lib.RawRole> & {
loading?: Boolean;
missing?: Boolean;
present?: Boolean;
loading?: boolean;
missing?: boolean;
present?: boolean;
};
function useRole(id: number): [RawRoleState, () => void] {
let control = useContext(ControlContext);
+3 -2
View File
@@ -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) {
<Select
autoFocus
showSearch
filterOption={(input, option) => (option?.title.toLowerCase().indexOf(input.toLowerCase())??-1) >= 0}
filterOption={(input, option) => (
(option?.title.toLowerCase().indexOf(input.toLowerCase())??-1) >= 0
)}
>
{instanceList.filter(
instance => instance["id"] !== props.instanceId
@@ -17,7 +17,7 @@ export default function StartStopInstanceButton(props: StartStopInstanceButtonPr
let control = useContext(ControlContext);
let [switching, setSwitching] = useState<boolean>(false);
const onClick: React.MouseEventHandler<HTMLElement> = function(event) {
function onClick(event: React.MouseEvent<HTMLElement>) {
event.stopPropagation();
setSwitching(true);
let action;
@@ -48,7 +48,7 @@ export default function StartStopInstanceButton(props: StartStopInstanceButtonPr
props.onFinish();
}
});
}
};
return <Button
{...(props.buttonProps || {})}
+3 -1
View File
@@ -96,7 +96,9 @@ export default function UsersPage() {
{
title: "Roles",
key: "roles",
render: (user: lib.RawUser) => user.roles.map(id => <Tag key={id}>{(roles.get(id) || { name: id }).name}</Tag>),
render: (user: lib.RawUser) => (
user.roles.map(id => <Tag key={id}>{(roles.get(id) || { name: id }).name}</Tag>)
),
},
{
title: "Online time",
+7 -5
View File
@@ -16,8 +16,9 @@ export function useHost(id: number): [ HostState, () => void ] {
function updateHost() {
// XXX optimize by requesting only the host in question
control.send(new lib.HostListRequest())
.then((hosts: lib.HostDetails[]) => {
control.send(
new lib.HostListRequest()
).then((hosts: lib.HostDetails[]) => {
let match = hosts.find(i => i.id === id);
if (!match) {
setHost({ missing: true });
@@ -35,7 +36,7 @@ export function useHost(id: number): [ HostState, () => void ] {
updateHost();
function updateHandler(newHost: lib.HostDetails) {
setHost({ ...newHost, loading:false, missing:false, present:true });
setHost({ ...newHost, loading: false, missing: false, present: true });
}
control.hostUpdate.subscribeToChannel(id, updateHandler);
@@ -52,8 +53,9 @@ export function useHostList() {
let [hostList, setHostList] = useState<lib.HostDetails[]>([]);
function updateHostList() {
control.send(new lib.HostListRequest())
.then((hosts: lib.HostDetails[]) => {
control.send(
new lib.HostListRequest()
).then((hosts: lib.HostDetails[]) => {
setHostList(hosts);
});
}
+8 -8
View File
@@ -29,14 +29,14 @@ export function useItemMetadata() {
let style = document.createElement("style");
document.head.appendChild(style);
for (let [name, meta] of itemMetadataCache) {
style.sheet!.insertRule(
`.item-${CSS.escape(name)} {
background-image: url("${window.staticRoot}static/${exportManifest.assets["item-spritesheet"]}");
background-repeat: no-repeat;
background-position: -${meta.x}px -${meta.y}px;
height: ${meta.size}px;
width: ${meta.size}px;
}`
style.sheet!.insertRule(`\
.item-${CSS.escape(name)} {
background-image: url("${window.staticRoot}static/${exportManifest.assets["item-spritesheet"]}");
background-repeat: no-repeat;
background-position: -${meta.x}px -${meta.y}px;
height: ${meta.size}px;
width: ${meta.size}px;
}`
);
}
setItemMetadata(itemMetadataCache);
+1 -1
View File
@@ -10,7 +10,7 @@ export function useSaves(instanceId?: number): lib.SaveDetails[] {
let [saves, setSaves] = useState<lib.SaveDetails[]>([]);
function updateSaves() {
control.sendTo({ instanceId:instanceId! }, new lib.InstanceListSavesRequest()).then(updatedSaves => {
control.sendTo({ instanceId: instanceId! }, new lib.InstanceListSavesRequest()).then(updatedSaves => {
setSaves(updatedSaves);
}).catch(err => {
logger.error(`Failed to list instance saves: ${err}`);
+3 -3
View File
@@ -53,9 +53,9 @@ export function sortLastSeen(userA:RawUserState, userB:RawUserState, instanceIdA
}
export type RawUserState = Partial<lib.RawUser> & {
loading?: Boolean;
present?: Boolean;
missing?: Boolean;
loading?: boolean;
present?: boolean;
missing?: boolean;
};
export function useUser(name: string): [RawUserState, () => void] {
+27 -10
View File
@@ -52,16 +52,33 @@ export class Control extends lib.Link {
accountRoles: lib.AccountRole[] | null = null;
/** Updates not handled by the subscription service */
accountUpdateHandlers: Function[] = [];
accountUpdateHandlers: accountHandler[] = [];
logHandlers: Map<lib.LogFilter, logHandler[]> = new Map();
/** Updates handled by the subscription service */
hostUpdate = new lib.EventSubscriber<lib.HostUpdateEvent, lib.HostDetails>(lib.HostUpdateEvent, event => event.update);
instanceUpdate = new lib.EventSubscriber<lib.InstanceDetailsUpdateEvent, lib.InstanceDetails>(lib.InstanceDetailsUpdateEvent, event => event.details);
saveListUpdate = new lib.EventSubscriber<lib.InstanceSaveListUpdateEvent>(lib.InstanceSaveListUpdateEvent);
modPackUpdate = new lib.EventSubscriber<lib.ModPackUpdateEvent, lib.ModPack>(lib.ModPackUpdateEvent, event => event.modPack);
modUpdate = new lib.EventSubscriber<lib.ModUpdateEvent, lib.ModInfo>(lib.ModUpdateEvent, event => event.mod);
userUpdate = new lib.EventSubscriber<lib.UserUpdateEvent, lib.RawUser>(lib.UserUpdateEvent, event => event.user);
hostUpdate = new lib.EventSubscriber<lib.HostUpdateEvent, lib.HostDetails>(
lib.HostUpdateEvent, event => event.update,
);
instanceUpdate = new lib.EventSubscriber<lib.InstanceDetailsUpdateEvent, lib.InstanceDetails>(
lib.InstanceDetailsUpdateEvent, event => event.details,
);
saveListUpdate = new lib.EventSubscriber<lib.InstanceSaveListUpdateEvent>(
lib.InstanceSaveListUpdateEvent,
);
modPackUpdate = new lib.EventSubscriber<lib.ModPackUpdateEvent, lib.ModPack>(
lib.ModPackUpdateEvent, event => event.modPack,
);
modUpdate = new lib.EventSubscriber<lib.ModUpdateEvent, lib.ModInfo>(
lib.ModUpdateEvent, event => event.mod,
);
userUpdate = new lib.EventSubscriber<lib.UserUpdateEvent, lib.RawUser>(
lib.UserUpdateEvent, event => event.user,
);
declare connector: ControlConnector;
@@ -81,7 +98,7 @@ export class Control extends lib.Link {
this.emitAccountUpdate();
this.hostUpdate.connectControl(this).catch(err => logger.error(
`Unexpected error updating host subscriptions:\n${err.stack}`
));;
));
this.instanceUpdate.connectControl(this).catch(err => logger.error(
`Unexpected error updating instance subscriptions:\n${err.stack}`
));
@@ -129,8 +146,8 @@ export class Control extends lib.Link {
emitAccountUpdate() {
for (let handler of this.accountUpdateHandlers) {
handler({
name: this.accountName,
roles: this.accountRoles,
name: this.accountName!,
roles: this.accountRoles!,
});
}
}
+1 -1
View File
@@ -12,6 +12,6 @@ const info: lib.PluginDeclaration = {
messages: [
ChatEvent,
],
}
};
export default info;
+2 -2
View File
@@ -60,6 +60,6 @@ const info: lib.PluginDeclaration = {
],
webEntrypoint: "./web",
routes: ["/inventory"],
}
};
export default info;
export default info;
+32 -11
View File
@@ -1,10 +1,14 @@
import * as lib from "@clusterio/lib";
import { AcquireRequest, AcquireResponse, ReleaseRequest, UploadRequest, DownloadRequest, DownloadResponse } from "./messages";
import {
AcquireRequest, AcquireResponse, ReleaseRequest, UploadRequest, DownloadRequest, DownloadResponse,
} from "./messages";
type IpcPlayerName = {
player_name: string
}
export type IpcPlayerData = { //.\module\serialize.lua:serialize.serialize_player()
// .\module\serialize.lua:serialize.serialize_player()
export type IpcPlayerData = {
generation: number,
controller: string,
name: string,
@@ -34,6 +38,7 @@ type IpcAcquireResponse = {
* Splits string into array of strings with max of a certain length
* @param chunkSize - Max length of each chunk
* @param string - String to split into chunks
* @returns array of substrings
*/
function chunkify(chunkSize: number, string: string): string[] {
return string.match(new RegExp(`.{1,${chunkSize}}`, "g")) || [];
@@ -48,21 +53,37 @@ export class InstancePlugin extends lib.BaseInstancePlugin {
this.disconnecting = false;
// Handle IPC from scenario script
this.instance.server.on("ipc-inventory_sync_acquire", (request: IpcPlayerName) => this.handleAcquire(request)
.catch(err => this.logger.error(`Error handling ipc-inventory_sync_acquire:\n${err.stack}`)));
this.instance.server.on("ipc-inventory_sync_release", (request: IpcPlayerName) => this.handleRelease(request)
.catch(err => this.logger.error(`Error handling ipc-inventory_sync_release:\n${err.stack}`)));
this.instance.server.on("ipc-inventory_sync_upload", (player_data: IpcPlayerData) => this.handleUpload(player_data)
.catch(err => this.logger.error(`Error handling ipc-inventory_sync_upload:\n${err.stack}`)));
this.instance.server.on("ipc-inventory_sync_download", (request: IpcPlayerName) => this.handleDownload(request)
.catch(err => this.logger.error(`Error handling ipc-inventory_sync_download:\n${err.stack}`)));
this.instance.server.on(
"ipc-inventory_sync_acquire",
(request: IpcPlayerName) => this.handleAcquire(request).catch(
err => this.logger.error(`Error handling ipc-inventory_sync_acquire:\n${err.stack}`)
),
);
this.instance.server.on(
"ipc-inventory_sync_release",
(request: IpcPlayerName) => this.handleRelease(request).catch(
err => this.logger.error(`Error handling ipc-inventory_sync_release:\n${err.stack}`)
),
);
this.instance.server.on(
"ipc-inventory_sync_upload",
(player_data: IpcPlayerData) => this.handleUpload(player_data).catch(
err => this.logger.error(`Error handling ipc-inventory_sync_upload:\n${err.stack}`)
),
);
this.instance.server.on(
"ipc-inventory_sync_download",
(request: IpcPlayerName) => this.handleDownload(request).catch(
err => this.logger.error(`Error handling ipc-inventory_sync_download:\n${err.stack}`)
),
);
}
async onPrepareControllerDisconnect() {
this.disconnecting = true;
}
onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") {
if (event === "connect") {
this.disconnecting = false;
+7 -7
View File
@@ -6,7 +6,7 @@ import { StringEnum } from "@clusterio/lib";
const jsonPlayerData = Type.Object({
generation: Type.Number(),
name: Type.String(),
})
});
export class AcquireResponse {
constructor(
@@ -45,7 +45,7 @@ export class AcquireRequest {
static jsonSchema = Type.Object({
"instanceId": Type.Number(),
"playerName": Type.String(),
})
});
static fromJSON(json: Static<typeof AcquireRequest.jsonSchema>): AcquireRequest {
return new this(json.instanceId, json.playerName);
@@ -68,7 +68,7 @@ export class ReleaseRequest {
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"playerName": Type.String(),
})
});
static fromJSON(json: Static<typeof ReleaseRequest.jsonSchema>) {
return new this(json.instanceId, json.playerName);
@@ -108,7 +108,7 @@ export class DownloadResponse {
static jsonSchema = Type.Object({
"playerData": Type.Optional(jsonPlayerData),
})
});
static fromJSON(json: Static<typeof DownloadResponse.jsonSchema>): DownloadResponse {
return new this(json.playerData as IpcPlayerData);
@@ -132,7 +132,7 @@ export class DownloadRequest {
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"playerName": Type.String(),
})
});
static fromJSON(json: Static<typeof DownloadRequest.jsonSchema>) {
return new this(json.instanceId, json.playerName);
@@ -153,8 +153,8 @@ class DatabaseStatsResponse {
"largestEntry": Type.Object({
"name": Type.String(),
"size": Type.Number(),
})
})
}),
});
static fromJSON(json: Static<typeof DatabaseStatsResponse.jsonSchema>): DatabaseStatsResponse {
return new this(json.databaseSize, json.databaseEntries, json.largestEntry);
+15 -9
View File
@@ -1,7 +1,5 @@
import type { Request, Response } from "express";
import crypto from "crypto";
import express from "express";
import express, { type Request, type Response } from "express";
import util from "util";
import jwt from "jsonwebtoken";
@@ -56,13 +54,21 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
res.send(servers);
});
this.controller.app.post("/api/player_auth/player_code", express.json(), (req: Request, res: Response, next: any) => {
this.handlePlayerCode(req, res).catch(next);
});
this.controller.app.post(
"/api/player_auth/player_code",
express.json(),
(req: Request, res: Response, next: any) => {
this.handlePlayerCode(req, res).catch(next);
}
);
this.controller.app.post("/api/player_auth/verify", express.json(), (req: Request, res: Response, next: any) => {
this.handleVerify(req, res).catch(next);
});
this.controller.app.post(
"/api/player_auth/verify",
express.json(),
(req: Request, res: Response, next: any) => {
this.handleVerify(req, res).catch(next);
}
);
this.controller.handle(FetchPlayerCodeRequest, this.handleFetchPlayerCodeRequest.bind(this));
this.controller.handle(SetVerifyCodeRequest, this.handleSetVerifyCodeRequest.bind(this));
+8 -5
View File
@@ -2,7 +2,7 @@ import * as lib from "@clusterio/lib";
import { FetchPlayerCodeRequest, SetVerifyCodeRequest } from "./messages";
type ipcPlayerAuth = {
type IpcPlayerAuth = {
type: "open_dialog",
player: string,
} | {
@@ -17,12 +17,15 @@ export class InstancePlugin extends lib.BaseInstancePlugin {
throw new Error("player_auth plugin requires save patching.");
}
this.instance.server.on("ipc-player_auth", (ipcPlayerAuth: ipcPlayerAuth) => this.handleEvent(ipcPlayerAuth).catch(
err => this.logger.error(`Error handling event:\n${err.stack}`)
));
this.instance.server.on(
"ipc-player_auth",
(ipcPlayerAuth: IpcPlayerAuth) => this.handleEvent(ipcPlayerAuth).catch(
err => this.logger.error(`Error handling event:\n${err.stack}`)
)
);
}
async handleEvent(event: ipcPlayerAuth) {
async handleEvent(event: IpcPlayerAuth) {
if (event.type === "open_dialog") {
if (!this.host.connector.connected) {
await this.sendRcon(`/web-login error ${event.player} login is temporarily unavailable`);
+3 -3
View File
@@ -10,7 +10,7 @@ class FetchPlayerCodeResponse {
static jsonSchema = Type.Object({
"playerCode": Type.String(),
"controllerUrl": Type.String(),
})
});
static fromJSON(json: Static<typeof FetchPlayerCodeResponse.jsonSchema>): FetchPlayerCodeResponse {
return new this(json.playerCode, json.controllerUrl);
@@ -32,7 +32,7 @@ export class FetchPlayerCodeRequest {
static jsonSchema = Type.Object({
"player": Type.String(),
})
});
static fromJSON(json: Static<typeof FetchPlayerCodeRequest.jsonSchema>): FetchPlayerCodeRequest {
return new this(json.player);
@@ -55,7 +55,7 @@ export class SetVerifyCodeRequest {
static jsonSchema = Type.Object({
"player": Type.String(),
"verifyCode": Type.String(),
})
});
static fromJSON(json: Static<typeof SetVerifyCodeRequest.jsonSchema>): SetVerifyCodeRequest {
return new this(json.player, json.verifyCode);
+8 -7
View File
@@ -19,7 +19,7 @@ export class ContributionEvent {
"name": Type.String(),
"level": Type.Integer(),
"contribution": Type.Number(),
})
});
static fromJSON(json: Static<typeof ContributionEvent.jsonSchema>): ContributionEvent {
return new this(json.name, json.level, json.contribution);
@@ -39,7 +39,7 @@ export class TechnologyProgress {
"name": Type.String(),
"level": Type.Integer(),
"progress": Type.Number(),
})
});
}
export class ProgressEvent {
@@ -57,8 +57,8 @@ export class ProgressEvent {
static jsonSchema = Type.Object({
"technologies": Type.Array(
TechnologyProgress.jsonSchema
)
})
),
});
static fromJSON(json: Static<typeof ProgressEvent.jsonSchema>): ProgressEvent {
return new this(json.technologies);
@@ -81,7 +81,7 @@ export class FinishedEvent {
static jsonSchema = Type.Object({
"name": Type.String(),
"level": Type.Integer(),
})
});
static fromJSON(json: Static<typeof FinishedEvent.jsonSchema>): FinishedEvent {
return new this(json.name, json.level);
@@ -100,9 +100,10 @@ export class TechnologySync {
static jsonSchema = Type.Tuple([
Type.String(),
Type.Integer(),
Type.Union([ Type.Number(), Type.Null() ]),
Type.Union([Type.Number(), Type.Null()]),
Type.Boolean(),
])
]);
toJSON() {
return [this.name, this.level, this.progress, this.researched];
}
+3 -1
View File
@@ -186,7 +186,9 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
// use fancy neural net to calculate a "fair" dole division rate.
if (method === "neural_dole") {
for (let item of request.items) {
let count = this.neuralDole.divider({ name: item.name, count: item.count, instanceId, instanceName });
let count = this.neuralDole.divider(
{ name: item.name, count: item.count, instanceId, instanceName }
);
if (count > 0) {
itemsRemoved.push(new Item(item.name, count));
}
+2 -1
View File
@@ -99,7 +99,8 @@ export class NeuralDole {
// If the server regularly can't fulfill requests, this number grows until
// it can. Then it slowly shrinks back down.
let _doleDivisionFactor: { [key:string]: number } = {};
export function doleDivider({
export function doleDivider(
{
object,
items,
logItemTransfers,
+3 -2
View File
@@ -73,7 +73,8 @@ export class InstancePlugin extends lib.BaseInstancePlugin {
return;
}
const fromIpcItems = items.map(item => new Item(item[0], item[1]))
const fromIpcItems = items.map(item => new Item(item[0], item[1]));
this.instance.sendTo("controller", new PlaceEvent(fromIpcItems));
if (this.instance.config.get("subspace_storage.log_item_transfers")) {
@@ -85,7 +86,7 @@ export class InstancePlugin extends lib.BaseInstancePlugin {
// request items --------------------------------------------------------------
async requestItems(requestItems: IpcItems) {
// Request the items all at once
let fromIpcItems = requestItems.map(item => new Item(item[0], item[1]))
let fromIpcItems = requestItems.map(item => new Item(item[0], item[1]));
let items = await this.instance.sendTo("controller", new RemoveRequest(fromIpcItems));
if (!items.length) {
+8 -8
View File
@@ -37,8 +37,8 @@ export class PlaceEvent {
}
static jsonSchema = Type.Object({
"items": Type.Array(Item.jsonSchema)
})
"items": Type.Array(Item.jsonSchema),
});
static fromJSON(json: Static<typeof PlaceEvent.jsonSchema>): PlaceEvent {
return new this(json.items.map(item => Item.fromJSON(item)));
@@ -58,8 +58,8 @@ export class RemoveRequest {
}
static jsonSchema = Type.Object({
"items": Type.Array(Item.jsonSchema)
})
"items": Type.Array(Item.jsonSchema),
});
static fromJSON(json: Static<typeof RemoveRequest.jsonSchema>): RemoveRequest {
return new this(json.items.map(item => Item.fromJSON(item)));
@@ -91,8 +91,8 @@ export class UpdateStorageEvent {
}
static jsonSchema = Type.Object({
"items": Type.Array(Item.jsonSchema)
})
"items": Type.Array(Item.jsonSchema),
});
static fromJSON(json: Static<typeof UpdateStorageEvent.jsonSchema>): UpdateStorageEvent {
return new this(json.items.map(item => Item.fromJSON(item)));
@@ -113,8 +113,8 @@ export class SetStorageSubscriptionRequest {
}
static jsonSchema = Type.Object({
"storage": Type.Boolean()
})
"storage": Type.Boolean(),
});
static fromJSON(json: Static<typeof SetStorageSubscriptionRequest.jsonSchema>): SetStorageSubscriptionRequest {
return new this(json.storage);
+1 -1
View File
@@ -7,7 +7,7 @@ const util = require("util");
const phin = require("phin");
const HttpCloser = require("@clusterio/controller/dist/controller/src/HttpCloser").default;
const HttpCloser = require("@clusterio/controller/dist/src/HttpCloser").default;
// Time to wait during async operations to ensure they happened in order.
const tick = 20;
+1 -1
View File
@@ -7,7 +7,7 @@ const http = require("http");
const phin = require("phin");
const { wait } = require("@clusterio/lib");
const routes = require("@clusterio/controller/dist/controller/src/routes");
const routes = require("@clusterio/controller/dist/src/routes");
const mock = require("../mock");
+1 -1
View File
@@ -5,7 +5,7 @@ const express = require("express");
const lib = require("@clusterio/lib");
const UserManager = require("@clusterio/controller/dist/controller/src/UserManager").default;
const UserManager = require("@clusterio/controller/dist/src/UserManager").default;
const addr = lib.Address.fromShorthand;