mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Merge pull request #517 from Hornwitser/setup-typescript-eslint
Setup TypeScript ESLint
This commit is contained in:
86 files changed
+697
-540
No files matched your search
+45
-16
@@ -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"],
|
||||
},
|
||||
}],
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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
|
||||
@@ -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}`);
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
],
|
||||
"include": ["controller.ts", "package.json", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/controller",
|
||||
"outDir": "dist",
|
||||
},
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
// Export of item icons and locale
|
||||
"use strict";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import Jimp from "jimp";
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
"use strict";
|
||||
import zlib from "zlib";
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
|
||||
|
||||
@@ -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
@@ -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[],
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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()]);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -1 +1 @@
|
||||
export default function isDeepStrictEqual(a: any, b: any): boolean
|
||||
export default function isDeepStrictEqual(a: any, b: any): boolean;
|
||||
@@ -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;
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
* @module lib/link
|
||||
* @author Hornwitser
|
||||
*/
|
||||
export * from "./link"
|
||||
export * from "./connectors"
|
||||
export * from "./link";
|
||||
export * from "./connectors";
|
||||
@@ -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,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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,4 +1,3 @@
|
||||
"use strict";
|
||||
import assert from "assert";
|
||||
import fs from "fs-extra";
|
||||
import stream from "stream";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Vendored
+2
-2
@@ -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>
|
||||
|
||||
@@ -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>> mkdir clusterio</p>
|
||||
<p>> 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} />
|
||||
|
||||
|
||||
@@ -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 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 Name" span={mod.factorioVersion ? 1 : 2}>{mod.name}</Descriptions.Item>
|
||||
{mod.factorioVersion
|
||||
&& <Descriptions.Item label="Factorio 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 Name" span={mod.factorioVersion ? 1 : 2}>{mod.name}</Descriptions.Item>
|
||||
{mod.factorioVersion
|
||||
&& <Descriptions.Item label="Factorio 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);
|
||||
|
||||
@@ -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 || {})}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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!,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ const info: lib.PluginDeclaration = {
|
||||
messages: [
|
||||
ChatEvent,
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
export default info;
|
||||
@@ -60,6 +60,6 @@ const info: lib.PluginDeclaration = {
|
||||
],
|
||||
webEntrypoint: "./web",
|
||||
routes: ["/inventory"],
|
||||
}
|
||||
};
|
||||
|
||||
export default info;
|
||||
export default info;
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user