From 90798205e029c7855b82b1e3f445fa32f945ebeb Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 30 Jul 2023 01:42:30 +0200 Subject: [PATCH 01/13] Set up TypeScript for lib Add a TypeScript build suitable for incremental migration to TypeScript to the lib package. --- packages/lib/browser.js | 28 ++++----- packages/lib/index.js | 63 ------------------- packages/lib/index.ts | 31 +++++++++ packages/lib/package.json | 7 ++- packages/lib/scripts/compile_validator.js | 2 +- packages/lib/src/config/index.js | 13 ---- packages/lib/src/config/index.ts | 7 +++ packages/lib/src/data/composites.js | 9 ++- packages/lib/src/data/index.js | 31 --------- packages/lib/src/data/index.ts | 16 +++++ packages/lib/src/data/message_validate.d.ts | 2 + .../lib/src/factorio/{index.js => index.ts} | 7 +-- packages/lib/src/link/index.js | 17 ----- packages/lib/src/link/index.ts | 11 ++++ packages/lib/src/logging_utils.js | 4 +- packages/lib/tsconfig.json | 18 ++++++ 16 files changed, 115 insertions(+), 151 deletions(-) delete mode 100644 packages/lib/index.js create mode 100644 packages/lib/index.ts delete mode 100644 packages/lib/src/config/index.js create mode 100644 packages/lib/src/config/index.ts delete mode 100644 packages/lib/src/data/index.js create mode 100644 packages/lib/src/data/index.ts create mode 100644 packages/lib/src/data/message_validate.d.ts rename packages/lib/src/factorio/{index.js => index.ts} (50%) delete mode 100644 packages/lib/src/link/index.js create mode 100644 packages/lib/src/link/index.ts create mode 100644 packages/lib/tsconfig.json diff --git a/packages/lib/browser.js b/packages/lib/browser.js index 29cce7f5..1f2d0428 100644 --- a/packages/lib/browser.js +++ b/packages/lib/browser.js @@ -1,15 +1,15 @@ -export * from "./src/config"; -export * from "./src/data"; -export * from "./src/errors"; -export * from "./src/factorio/exchange_string"; -export * from "./src/helpers"; -export * from "./src/ini"; -export * from "./src/link"; -export * from "./src/logging"; -export * from "./src/plugin"; -export * from "./src/schema"; -export * from "./src/users"; +export * from "./dist/src/config"; +export * from "./dist/src/data"; +export * from "./dist/src/errors"; +export * from "./dist/src/factorio/exchange_string"; +export * from "./dist/src/helpers"; +export * from "./dist/src/ini"; +export * from "./dist/src/link"; +export * from "./dist/src/logging"; +export * from "./dist/src/plugin"; +export * from "./dist/src/schema"; +export * from "./dist/src/users"; -export { default as ExponentialBackoff } from "./src/ExponentialBackoff"; -export { default as PlayerStats } from "./src/PlayerStats"; -export { default as RateLimiter } from "./src/RateLimiter"; +export { default as ExponentialBackoff } from "./dist/src/ExponentialBackoff"; +export { default as PlayerStats } from "./dist/src/PlayerStats"; +export { default as RateLimiter } from "./dist/src/RateLimiter"; diff --git a/packages/lib/index.js b/packages/lib/index.js deleted file mode 100644 index 2fce1f00..00000000 --- a/packages/lib/index.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Shared library for Clusterio - * @module lib - */ -"use strict"; -const libBuildMod = require("./build_mod"); -const libCommand = require("./src/command"); -const libConfig = require("./src/config"); -const libData = require("./src/data"); -const libDatabase = require("./src/database"); -const libErrors = require("./src/errors"); -const libFactorio = require("./src/factorio"); -const libFileOps = require("./src/file_ops"); -const libHash = require("./src/hash"); -const libHelpers = require("./src/helpers"); -const libIni = require("./src/ini"); -const libLink = require("./src/link"); -const libLogging = require("./src/logging"); -const libLoggingUtils = require("./src/logging_utils"); -const libLuaTools = require("./src/lua_tools"); -const libPlugin = require("./src/plugin"); -const libPluginLoader = require("./src/plugin_loader"); -const libPrometheus = require("./src/prometheus"); -const libSchema = require("./src/schema"); -const libSharedCommands = require("./src/shared_commands"); -const libStream = require("./src/stream"); -const libUsers = require("./src/users"); -const libZipOps = require("./src/zip_ops"); - -const ExponentialBackoff = require("./src/ExponentialBackoff"); -const PlayerStats = require("./src/PlayerStats"); -const RateLimiter = require("./src/RateLimiter"); - - -module.exports = { - ...libBuildMod, - ...libCommand, - ...libConfig, - ...libData, - ...libDatabase, - ...libErrors, - ...libFactorio, - ...libFileOps, - ...libHash, - ...libHelpers, - ...libIni, - ...libLink, - ...libLogging, - ...libLoggingUtils, - ...libLuaTools, - ...libPlugin, - ...libPluginLoader, - ...libPrometheus, - ...libSchema, - ...libSharedCommands, - ...libStream, - ...libUsers, - ...libZipOps, - - ExponentialBackoff, - PlayerStats, - RateLimiter, -}; diff --git a/packages/lib/index.ts b/packages/lib/index.ts new file mode 100644 index 00000000..14b567e0 --- /dev/null +++ b/packages/lib/index.ts @@ -0,0 +1,31 @@ +/** + * Shared library for Clusterio + * @module lib + */ +export * from "./build_mod"; +export * from "./src/command"; +export * from "./src/config"; +export * from "./src/data"; +export * from "./src/database"; +export * from "./src/errors"; +export * from "./src/factorio"; +export * from "./src/file_ops"; +export * from "./src/hash"; +export * from "./src/helpers"; +export * from "./src/ini"; +export * from "./src/link"; +export * from "./src/logging"; +export * from "./src/logging_utils"; +export * from "./src/lua_tools"; +export * from "./src/plugin"; +export * from "./src/plugin_loader"; +export * from "./src/prometheus"; +export * from "./src/schema"; +export * from "./src/shared_commands"; +export * from "./src/stream"; +export * from "./src/users"; +export * from "./src/zip_ops"; + +export { default as ExponentialBackoff } from "./src/ExponentialBackoff"; +export { default as PlayerStats } from "./src/PlayerStats"; +export { default as RateLimiter } from "./src/RateLimiter"; diff --git a/packages/lib/package.json b/packages/lib/package.json index ce052530..a8cdc5b7 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -5,9 +5,10 @@ "repository": "https://github.com/clusterio/clusterio", "license": "MIT", "scripts": { - "prepare": "pnpm run compile_validator", + "prepare": "pnpm run compile_validator && tsc || true", "compile_validator": "node scripts/compile_validator.js" }, + "main": "dist/index.js", "browser": "browser.js", "keywords": [ "factorio" @@ -31,5 +32,9 @@ }, "publishConfig": { "access": "public" + }, + "devDependencies": { + "@types/node": "^20.4.5", + "typescript": "^5.1.6" } } diff --git a/packages/lib/scripts/compile_validator.js b/packages/lib/scripts/compile_validator.js index bc90dcf4..41745df1 100644 --- a/packages/lib/scripts/compile_validator.js +++ b/packages/lib/scripts/compile_validator.js @@ -24,5 +24,5 @@ const validate = ajv.compile(Message.jsonSchema); let moduleCode = standaloneCode(ajv, validate); fs.outputFileSync( path.join(__dirname, "../src/data/message_validate.js"), - `// generated by scripts/compile_validator.js\n/* eslint-disable */\n${moduleCode}` + `// generated by scripts/compile_validator.js\n// @ts-nocheck\n/* eslint-disable */\n${moduleCode}` ); diff --git a/packages/lib/src/config/index.js b/packages/lib/src/config/index.js deleted file mode 100644 index 858c7712..00000000 --- a/packages/lib/src/config/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Configuration System - * @author Hornwitser - * @module lib/config - */ -"use strict"; -const classes = require("./classes"); -const definitions = require("./definitions"); - -module.exports = { - ...classes, - ...definitions, -}; diff --git a/packages/lib/src/config/index.ts b/packages/lib/src/config/index.ts new file mode 100644 index 00000000..a111da14 --- /dev/null +++ b/packages/lib/src/config/index.ts @@ -0,0 +1,7 @@ +/** + * Configuration System + * @author Hornwitser + * @module lib/config + */ +export * from "./classes"; +export * from "./definitions"; diff --git a/packages/lib/src/data/composites.js b/packages/lib/src/data/composites.js index de62d1c6..cdd80943 100644 --- a/packages/lib/src/data/composites.js +++ b/packages/lib/src/data/composites.js @@ -21,11 +21,14 @@ function jsonPrimitive(type) { }, }; } +const JsonBoolean = jsonPrimitive("boolean"); +const JsonNumber = jsonPrimitive("number"); +const JsonString = jsonPrimitive("string"); module.exports = { - JsonBoolean: jsonPrimitive("boolean"), - JsonNumber: jsonPrimitive("number"), - JsonString: jsonPrimitive("string"), + JsonBoolean, + JsonNumber, + JsonString, jsonArray, jsonPrimitive, diff --git a/packages/lib/src/data/index.js b/packages/lib/src/data/index.js deleted file mode 100644 index 5522a49a..00000000 --- a/packages/lib/src/data/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Shared data types used in Clusterio - * @module lib/data - * @author Hornwitser - */ -"use strict"; -const ExportManifest = require("./ExportManifest"); -const ModInfo = require("./ModInfo"); -const ModPack = require("./ModPack"); -const composites = require("./composites"); -const messagesCore = require("./messages_core"); -const messagesController = require("./messages_controller"); -const messagesHost = require("./messages_host"); -const messagesInstance = require("./messages_instance"); -const messagesMod = require("./messages_mod"); -const messagesUser = require("./messages_user"); -const version = require("./version"); - -module.exports = { - ExportManifest, - ModInfo, - ModPack, - ...composites, - ...messagesCore, - ...messagesController, - ...messagesHost, - ...messagesInstance, - ...messagesMod, - ...messagesUser, - ...version, -}; diff --git a/packages/lib/src/data/index.ts b/packages/lib/src/data/index.ts new file mode 100644 index 00000000..46c1ed2a --- /dev/null +++ b/packages/lib/src/data/index.ts @@ -0,0 +1,16 @@ +/** + * Shared data types used in Clusterio + * @module lib/data + * @author Hornwitser + */ +export { default as ExportManifest } from "./ExportManifest"; +export { default as ModInfo } from "./ModInfo"; +export { default as ModPack } from "./ModPack"; +export * from "./composites"; +export * from "./messages_core"; +export * from "./messages_controller"; +export * from "./messages_host"; +export * from "./messages_instance"; +export * from "./messages_mod"; +export * from "./messages_user"; +export * from "./version"; diff --git a/packages/lib/src/data/message_validate.d.ts b/packages/lib/src/data/message_validate.d.ts new file mode 100644 index 00000000..177658ef --- /dev/null +++ b/packages/lib/src/data/message_validate.d.ts @@ -0,0 +1,2 @@ +import type { ValidateFunction } from "ajv" +export default ValidateFunction diff --git a/packages/lib/src/factorio/index.js b/packages/lib/src/factorio/index.ts similarity index 50% rename from packages/lib/src/factorio/index.js rename to packages/lib/src/factorio/index.ts index c7d1aab7..72335610 100644 --- a/packages/lib/src/factorio/index.js +++ b/packages/lib/src/factorio/index.ts @@ -3,9 +3,4 @@ * @module lib/factorio * @author Hornwitser */ -"use strict"; -const exchange_string = require("./exchange_string"); - -module.exports = { - ...exchange_string, -}; +export * from "./exchange_string"; diff --git a/packages/lib/src/link/index.js b/packages/lib/src/link/index.js deleted file mode 100644 index 132e6150..00000000 --- a/packages/lib/src/link/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Library for message based communication between nodes - * @module lib/link - * @author Hornwitser - */ -"use strict"; -const link = require("./link"); -const connectors = require("./connectors"); - -module.exports = { - ...link, - ...connectors, - - // migrate: Allow info for plugins from before link refactor to load. - Event: class Event {}, - Request: class Request {}, -}; diff --git a/packages/lib/src/link/index.ts b/packages/lib/src/link/index.ts new file mode 100644 index 00000000..5f873f85 --- /dev/null +++ b/packages/lib/src/link/index.ts @@ -0,0 +1,11 @@ +/** + * Library for message based communication between nodes + * @module lib/link + * @author Hornwitser + */ +export * from "./link" +export * from "./connectors" + +// migrate: Allow info for plugins from before link refactor to load. +export class Event {} +export class Request {} diff --git a/packages/lib/src/logging_utils.js b/packages/lib/src/logging_utils.js index c45a961d..23f86de3 100644 --- a/packages/lib/src/logging_utils.js +++ b/packages/lib/src/logging_utils.js @@ -416,7 +416,7 @@ function handleUnhandledErrors() { /* eslint-enable node/no-process-exit */ } - +const _formatServerOutput = formatServerOutput; module.exports = { TerminalFormat, LinkTransport, @@ -425,5 +425,5 @@ module.exports = { queryLog, // for testing only - _formatServerOutput: formatServerOutput, + _formatServerOutput, }; diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json new file mode 100644 index 00000000..0f0b112f --- /dev/null +++ b/packages/lib/tsconfig.json @@ -0,0 +1,18 @@ +{ + "include": ["index.ts", "build_mod.js", "src/**/*.ts", "src/**/*.js"], + "compilerOptions": { + "lib": ["es2023"], + "module": "commonjs", + "target": "es2022", + "rootDir": ".", + "outDir": "dist", + "composite": false, + "strict": false, + "alwaysStrict": true, + "allowJs": true, + "checkJs": false, + "noEmitOnError": false, + "esModuleInterop": true, + "skipLibCheck": true + } +} From 3a321b39e12179285c6f8299d37de992cc6453c0 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 30 Jul 2023 21:59:54 +0200 Subject: [PATCH 02/13] Remove incorrect validation of forwared messages Messages are forwareded when received from another link, this means it's too late to throw an error that bubbles up to the code trying to send the incorrect message thus defeating the purpose of the validation. --- packages/lib/src/link/link.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/lib/src/link/link.js b/packages/lib/src/link/link.js index 758c524c..a9aacc42 100644 --- a/packages/lib/src/link/link.js +++ b/packages/lib/src/link/link.js @@ -423,14 +423,6 @@ class Link { } forwardRequest(message, origin) { - if (this.validateSent) { - let entry = this.constructor._requestsByName.get(message.name); - if (!entry) { - throw new Error(`Attempt to forward unregistered Request ${message.name}`); - } - entry.requestFromJSON(message.data); - } - let pending = { origin, src: message.src, From 74e9ed82578b7ad5f838dd76fff406b730cd8f5e Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 3 Aug 2023 14:56:38 +0200 Subject: [PATCH 03/13] Replace jsdoc with typedoc Use TypeDoc instead of JSDoc because it has better support for documenting TypeScript APIs. --- docs/jsdoc.json | 25 -------------------- package.json | 6 ++--- packages/lib/src/factorio/exchange_string.js | 4 ++-- packages/lib/src/helpers.js | 10 ++++---- packages/lib/src/logging.js | 4 ++-- packages/lib/src/logging_utils.js | 6 ++--- packages/lib/src/plugin.js | 14 +++++------ packages/lib/typedoc.json | 4 ++++ typedoc.json | 7 ++++++ 9 files changed, 33 insertions(+), 47 deletions(-) delete mode 100644 docs/jsdoc.json create mode 100644 packages/lib/typedoc.json create mode 100644 typedoc.json diff --git a/docs/jsdoc.json b/docs/jsdoc.json deleted file mode 100644 index 777c03a5..00000000 --- a/docs/jsdoc.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "plugins": [ - "plugins/markdown" - ], - "tags": { - "allowUnknownTags": false - }, - "opts": { - "destination": "docs/api", - "recurse": true - }, - "source": { - "include": [ - "packages/controller/src/", - "packages/controller/controller.js", - "packages/host/src/", - "packages/host/host.js", - "packages/ctl/src/", - "packages/ctl/ctl.js", - "packages/lib/", - "plugins/" - ], - "excludePattern": "node_modules" - } -} diff --git a/package.json b/package.json index 67323b9e..7ff3a61b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "ci-cover": "nyc --reporter=lcovonly pnpm run-script test", "lint": "eslint \"*.js\" packages plugins test", "lint-fix": "eslint --fix \"*.js\" packages plugins test", - "docs": "jsdoc --configure docs/jsdoc.json", + "docs": "typedoc", "bundle-dependencies": "bundle-dependencies" }, "nyc": { @@ -31,11 +31,11 @@ "express": "^4.18.2", "form-data": "^4.0.0", "fs-extra": "^11.1.1", - "jsdoc": "^4.0.2", "jsonwebtoken": "^9.0.1", "jszip": "^3.10.0", "mocha": "^10.2.0", "nyc": "^15.1.0", - "phin": "^3.7.0" + "phin": "^3.7.0", + "typedoc": "^0.24.8" } } diff --git a/packages/lib/src/factorio/exchange_string.js b/packages/lib/src/factorio/exchange_string.js index a4373356..249cd3d1 100644 --- a/packages/lib/src/factorio/exchange_string.js +++ b/packages/lib/src/factorio/exchange_string.js @@ -324,7 +324,7 @@ function readMapSettings(state) { } /** - * @typedef {object} module:lib.MapExchangeResult + * @typedef {object} MapExchangeResult * @property {array} version - * Version of Factorio the string was created with. * @property {object} map_gen_settings - @@ -344,7 +344,7 @@ function readMapSettings(state) { * save to set the map gen settings and the map settings for the save. * * @param {string} exchangeString - Max Exchange String to parse. - * @returns {module:lib.MapExchangeResult} Parsed result. + * @returns {MapExchangeResult} Parsed result. * @memberof module:lib */ function readMapExchangeString(exchangeString) { diff --git a/packages/lib/src/helpers.js b/packages/lib/src/helpers.js index 5104f74a..88b096cb 100644 --- a/packages/lib/src/helpers.js +++ b/packages/lib/src/helpers.js @@ -210,18 +210,18 @@ function wordMatches(word, ...texts) { } /** - * @typedef {object} module:lib~ParsedTerm + * @typedef {object} ParsedTerm * @property {string} type - Type of term, either attribute or word. * @property {string=} name - attribute only: Name of attribute. * @property {boolean=} exclude - word only: exclude results with this word. - * @property {string | module:lib.ParsedTerm} value - + * @property {string | ParsedTerm} value - * Parsed value of this term. Is a string if type is word, and a * ParsedTerm if type is attribute. */ /** - * @typedef {object} module:lib~ParsedSearch - * @property {Array} terms - + * @typedef {object} ParsedSearch + * @property {Array} terms - * Parsed result of search terms. * @property {Array} issues - * Issues detected while parsing the seach string. @@ -248,7 +248,7 @@ function wordMatches(word, ...texts) { * @param {Object} attributes - * Recognized attributes and their format. Currently only word is * supported. - * @returns {module:lib~ParsedSearch} parsed terms of the search. + * @returns {ParsedSearch} parsed terms of the search. * @static */ function parseSearchString(input, attributes = {}) { diff --git a/packages/lib/src/logging.js b/packages/lib/src/logging.js index 98aeb928..f4fa9c4c 100644 --- a/packages/lib/src/logging.js +++ b/packages/lib/src/logging.js @@ -84,7 +84,7 @@ class WebConsoleFormat { /** * Filter object for logs - * @typedef {Object} module:lib~LogFilter + * @typedef {Object} LogFilter * @property {string} [maxLevel] - * Maximum log level to include. Higher levels are more verbose. * @property {boolean} [all] - @@ -101,7 +101,7 @@ class WebConsoleFormat { /** * Create log filter by level and source. * - * @param {module:lib~LogFilter} filter - + * @param {LogFilter} filter - * Filter to filter log entries by. * @returns {function(object): boolean} * filter returning true for log entries that match it. diff --git a/packages/lib/src/logging_utils.js b/packages/lib/src/logging_utils.js index 23f86de3..fe627044 100644 --- a/packages/lib/src/logging_utils.js +++ b/packages/lib/src/logging_utils.js @@ -275,7 +275,7 @@ class LogIndex { * * @param {string} file - * Name of file in the log directory to check for. - * @param {module:lib~LogFilter} filter - + * @param {LogFilter} filter - * Filter to check index if file contains entries for. * @returns {boolean} * true if the file may contain entries included by the filter. @@ -314,7 +314,7 @@ class LogIndex { /** * Filter object for querying logs - * @typedef {Object} module:lib~QueryLogFilter + * @typedef {Object} QueryLogFilter * @property {number} [limit=100] - Maximum number of entries to return. * @property {string} [order="asc"] - * Return entries in ascending ("asc") date order or desceding ("desc") @@ -336,7 +336,7 @@ class LogIndex { * Query log directory * * @param {string} logDirectory - path to directory with logs. - * @param {module:lib~QueryLogFilter} filter - + * @param {QueryLogFilter} filter - * Filter to limit logs by. * @param {module:lib.LogIndex=} index - * Index to speed up query with. diff --git a/packages/lib/src/plugin.js b/packages/lib/src/plugin.js index 48e22cbb..74accdbb 100644 --- a/packages/lib/src/plugin.js +++ b/packages/lib/src/plugin.js @@ -6,13 +6,13 @@ const helpers = require("./helpers"); -/** +/* TODO fix when converting to ts * Conceptual base for controller and instance plugins. * @typedef { - * module:lib.BaseControllerPlugin - * |module:lib.BaseInstancePlugin - * |module:lib.BaseControlPlugin - * } module:lib.BasePlugin + * BaseControllerPlugin + * |BaseInstancePlugin + * |BaseControlPlugin + * } BasePlugin */ /** @@ -612,7 +612,7 @@ class BaseControlPlugin { /** * Plugin supplied login form - * @typedef {Object} module:lib~LoginForm + * @typedef {Object} LoginForm * @property {string} name - * Internal name of the login form, this should start with the * plugin name followed by a dot. @@ -627,7 +627,7 @@ class BaseControlPlugin { /** * Plugin supplied pages - * @typedef {Object} module:lib~Page + * @typedef {Object} Page * @property {string} path - URL path to this page. * @property {string=} sidebarPath - * If present and this path matches one of the pages in the sidebar it diff --git a/packages/lib/typedoc.json b/packages/lib/typedoc.json new file mode 100644 index 00000000..164665cc --- /dev/null +++ b/packages/lib/typedoc.json @@ -0,0 +1,4 @@ +{ + "entryPoints": ["index.ts"], + "includeVersion": true +} diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 00000000..ce9e6a37 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["packages/lib/"], + "entryPointStrategy": "packages", + "cleanOutputDir": false, + "out": "docs/api" +} From dedb42447ca1346251daf6cc8bcf48800d4be9b3 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 3 Aug 2023 14:59:56 +0200 Subject: [PATCH 04/13] Port helpers.js to TypeScript --- packages/lib/src/{helpers.js => helpers.ts} | 154 ++++++++++---------- 1 file changed, 80 insertions(+), 74 deletions(-) rename packages/lib/src/{helpers.js => helpers.ts} (68%) diff --git a/packages/lib/src/helpers.js b/packages/lib/src/helpers.ts similarity index 68% rename from packages/lib/src/helpers.js rename to packages/lib/src/helpers.ts index 88b096cb..faabc117 100644 --- a/packages/lib/src/helpers.js +++ b/packages/lib/src/helpers.ts @@ -2,7 +2,6 @@ * Collection of small utilites that are useful in multiple places. * @module lib/helpers */ -"use strict"; /** * Return a string describing the type of the value passed @@ -10,11 +9,10 @@ * Works the same as typeof, excpet that null and array types get their * own string. * - * @param {*} value - value to return the type of. - * @returns {string} basic type of the value passed. - * @static + * @param value - value to return the type of. + * @returns basic type of the value passed. */ -function basicType(value) { +export function basicType(value: unknown) { if (value === null) { return "null"; } if (value instanceof Array) { return "array"; } return typeof value; @@ -24,10 +22,9 @@ function basicType(value) { /** * Asynchronously wait for the given duration * - * @param {number} duration - Time to wait for in milliseconds. - * @static + * @param duration - Time to wait for in milliseconds. */ -async function wait(duration) { +export async function wait(duration: number) { await new Promise(resolve => { setTimeout(resolve, duration); }); } @@ -38,10 +35,9 @@ async function wait(duration) { * @param {Promise} promise - Promise to wait for. * @param {number} time - Maximum time im milliseconds to wait for. * @param {*=} timeoutResult - Value to return if the operation timed out. - * @static */ -async function timeout(promise, time, timeoutResult) { - let timer; +export async function timeout(promise: Promise, time: number, timeoutResult: T) { + let timer: ReturnType; try { return await Promise.race([ promise, @@ -62,18 +58,17 @@ async function timeout(promise, time, timeoutResult) { * Reads the stream given asynchronously until the end is reached and * returns all the data which was read from the stream. * - * @param {Readable} stream - byte stream to read to the end. - * @returns {Promise} content of the stream. - * @static + * @param stream - byte stream to read to the end. + * @returns content of the stream. */ -async function readStream(stream) { - let chunks = []; +export async function readStream(stream: NodeJS.ReadableStream & { isTTY?: boolean }) { + let chunks: Buffer[] = []; for await (let chunk of stream) { // Support using ^Z to end input on Windows if (process.platform === "win32" && stream.isTTY && chunk.toString() === "\x1a\r\n") { break; } - chunks.push(chunk); + chunks.push(chunk as Buffer); } return Buffer.concat(chunks); } @@ -87,11 +82,10 @@ async function readStream(stream) { * * See https://stackoverflow.com/a/9310752 * - * @param {string} text - Text to escape RegExp meta chars in. - * @returns {string} escaped text. - * @static + * @param text - Text to escape RegExp meta chars in. + * @returns escaped text. */ -function escapeRegExp(text) { +export function escapeRegExp(text: string) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); } @@ -99,11 +93,10 @@ function escapeRegExp(text) { * Format byte count for human readable display * * Shortens a large number of bytes using the kB/MB/GB/TB prefixes. - * @param {number} bytes - Count of bytes to format. - * @returns {string} formatted text. - * @static + * @param bytes - Count of bytes to format. + * @returns formatted text. */ -function formatBytes(bytes) { +export function formatBytes(bytes: number) { if (bytes === 0) { return "0\u{A0}Bytes"; // No-break space } @@ -114,7 +107,7 @@ function formatBytes(bytes) { return (power > 0 ? (bytes / factor ** power).toFixed(2) : bytes) + units[power]; } -function parseSearchIdentifier(pos, input) { +function parseSearchIdentifier(pos: number, input: string): [number, string] { // character = ? all characters excluding space : " ? // identifier = 1*character let startPos = pos; @@ -124,15 +117,14 @@ function parseSearchIdentifier(pos, input) { return [pos, input.slice(startPos, pos)]; } -function parseSearchWord(pos, input) { +function parseSearchWord(pos: number, input: string): [number, ParsedWord] { // non quote character = ? all characters excluding " ? // word op = "-" // word = [ word op ], ( identifier | '"', *non quote character, '"' ) - let word = { - type: "word", - }; + let exclude = false; + let value: string; if (input.charAt(pos) === "-") { - word.exclude = true; + exclude = true; pos += 1; } if (input.charAt(pos) === '"') { @@ -146,21 +138,33 @@ function parseSearchWord(pos, input) { if (input.charAt(pos) === '"') { pos += 1; } - word.value = input.slice(startPos, endPos); + value = input.slice(startPos, endPos); } else { - ([pos, word.value] = parseSearchIdentifier(pos, input)); + ([pos, value] = parseSearchIdentifier(pos, input)); + } + let word: ParsedWord = { + type: "word", + value, + }; + if (exclude) { + word.exclude = true; } return [pos, word]; } -function parseSearchTerm(pos, input, attributes, issues) { +function parseSearchTerm( + pos: number, + input: string, + attributes: Record, + issues: Array +): [number, ParsedTerm] { // attribute = identifier, ':', word // term = word | attribute - let term; + let term: ParsedTerm; if (["-", '"'].includes(input.charAt(pos))) { ([pos, term] = parseSearchWord(pos, input)); } else { - let identifier; + let identifier: string; ([pos, identifier] = parseSearchIdentifier(pos, input)); if (input.charAt(pos) === ":") { pos += 1; @@ -168,7 +172,7 @@ function parseSearchTerm(pos, input, attributes, issues) { if (!Object.prototype.hasOwnProperty.call(attributes, identifier)) { issues.push(`Unregonized attribute "${identifier}", use quotes to escape colons`); } else if (attributes[identifier] === "word") { - let value; + let value: ParsedWord; ([pos, value] = parseSearchWord(pos, input)); term = { type: "attribute", name: identifier, value }; } else { @@ -188,17 +192,17 @@ function parseSearchTerm(pos, input, attributes, issues) { * of the passed text snippets. If the word mode is exclude then returns * true if none of the passed text snippets match. * - * @param {lib/helpers.ParsedTerm} word - Word to match. - * @param {...string} texts - Text to match word in. - * @returns {boolean} true if the word matches. + * @param word - Word to match. + * @param texts - Text to match word in. + * @returns true if the word matches. */ -function wordMatches(word, ...texts) { +export function wordMatches(word: ParsedTerm, ...texts: string[]) { if (word.type !== "word") { throw Error("wordMatches: parameter is not a word"); } let matches = false; for (let text of texts) { - if (text.includes(word.value)) { + if (text.includes(word.value as string)) { matches = true; break; } @@ -209,23 +213,35 @@ function wordMatches(word, ...texts) { return matches; } -/** - * @typedef {object} ParsedTerm - * @property {string} type - Type of term, either attribute or word. - * @property {string=} name - attribute only: Name of attribute. - * @property {boolean=} exclude - word only: exclude results with this word. - * @property {string | ParsedTerm} value - - * Parsed value of this term. Is a string if type is word, and a - * ParsedTerm if type is attribute. - */ +export interface ParsedAttribute { + /** Type of term, either attribute or word. */ + type: "attribute" + /** Name of attribute. */ + name: string; + /** Parsed value of this attribute. */ + value: ParsedWord; +} + +export interface ParsedWord { + /** Type of term, either attribute or word. */ + type: "word"; + /** Exclude results with this word. */ + exclude?: boolean; + /** Parsed text of this word. */ + value: string; +} + +export type ParsedTerm = ParsedAttribute | ParsedWord; /** - * @typedef {object} ParsedSearch - * @property {Array} terms - - * Parsed result of search terms. - * @property {Array} issues - - * Issues detected while parsing the seach string. + * Result from {@link parseSearchString}. */ +export interface ParsedSearch { + /** Parsed result of search terms. */ + terms: Array; + /** Issues detected while parsing the seach string. */ + issues: Array; +}; /** * Parse a search string with optional attributes @@ -244,14 +260,16 @@ function wordMatches(word, ...texts) { * The issues should be shown to the end user so that they can correct their * search. * - * @param {string} input - Search expression to parse. - * @param {Object} attributes - + * @param input - Search expression to parse. + * @param attributes - * Recognized attributes and their format. Currently only word is * supported. - * @returns {ParsedSearch} parsed terms of the search. - * @static + * @returns parsed terms of the search. */ -function parseSearchString(input, attributes = {}) { +export function parseSearchString( + input: string, + attributes: Record = {} +): ParsedSearch { // whitespace = 1*" " // search = [ term, *( [ whitespace ], term ) ] input = input.trim(); @@ -261,7 +279,7 @@ function parseSearchString(input, attributes = {}) { }; let pos = 0; while (pos < input.length) { - let term; + let term: ParsedTerm; ([pos, term] = parseSearchTerm(pos, input, attributes, parsed.issues)); if (term) { parsed.terms.push(term); @@ -276,15 +294,3 @@ function parseSearchString(input, attributes = {}) { } return parsed; } - - -module.exports = { - basicType, - wait, - timeout, - readStream, - escapeRegExp, - formatBytes, - wordMatches, - parseSearchString, -}; From 2988cd962c9cffed61c29dbade8aa86d2884ed11 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 3 Aug 2023 15:51:58 +0200 Subject: [PATCH 05/13] Port stream.ts to TypeScript --- packages/lib/package.json | 1 + packages/lib/src/{stream.js => stream.ts} | 88 ++++++++++++----------- 2 files changed, 46 insertions(+), 43 deletions(-) rename packages/lib/src/{stream.js => stream.ts} (64%) diff --git a/packages/lib/package.json b/packages/lib/package.json index a8cdc5b7..c0a121c0 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -34,6 +34,7 @@ "access": "public" }, "devDependencies": { + "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", "typescript": "^5.1.6" } diff --git a/packages/lib/src/stream.js b/packages/lib/src/stream.ts similarity index 64% rename from packages/lib/src/stream.js rename to packages/lib/src/stream.ts index a05180a9..a35185b2 100644 --- a/packages/lib/src/stream.js +++ b/packages/lib/src/stream.ts @@ -1,7 +1,7 @@ "use strict"; -const assert = require("assert"); -const fs = require("fs-extra"); -const stream = require("stream"); +import assert from "assert"; +import fs from "fs-extra"; +import stream from "stream"; /** @@ -11,20 +11,17 @@ const stream = require("stream"); * one removing the line feed and optional carriage return from the lines. * * @example - * let lineStream = new libStream.LineSplitter({ readableObjectMode: true }); + * ```ts + * let lineStream = new lib.LineSplitter({ readableObjectMode: true }); * lineStream.on("data", line => { console.log(line.toString("utf8")); }); * let fileStream = fs.createReadStream(path); * fileStream.pipe(lineStream); - * - * @memberof module:lib + * ``` */ -class LineSplitter extends stream.Transform { - constructor(options) { - super(options); - this._partial = null; - } +export class LineSplitter extends stream.Transform { + _partial: Buffer | null = null; - _transform(chunk, encoding, callback) { + _transform(chunk: Buffer, _encoding: string, callback: stream.TransformCallback) { if (this._partial) { chunk = Buffer.concat([this._partial, chunk]); this._partial = null; @@ -43,14 +40,14 @@ class LineSplitter extends stream.Transform { end -= 1; } - let line = chunk.slice(0, end); - chunk = chunk.slice(next); + let line = chunk.subarray(0, end); + chunk = chunk.subarray(next); this.push(line); } callback(); } - _flush(callback) { + _flush(callback: stream.TransformCallback) { if (this._partial) { this.push(this._partial); this._partial = null; @@ -63,25 +60,22 @@ class LineSplitter extends stream.Transform { * Transform splitting a reverse stream into lines * * Splits up a reverse stream of chunks created by {@link - * module:lib.createReverseReadStream} by newlines and passes on the + * createReverseReadStream} by newlines and passes on the * lines one by one in reverse order removing the line feed and optional * carriage return from the lines. * * @example - * let revLineStream = new libStream.ReverseLineSplitter({ readableObjectMode: true }); + * ```ts + * let revLineStream = new lib.ReverseLineSplitter({ readableObjectMode: true }); * revLineStream.on("data", line => { console.log(line.toString("utf8")); }); - * let revFileStream = await libStream.createReverseReadStream(path); + * let revFileStream = await lib.createReverseReadStream(path); * revFileStream.pipe(revLineStream); - * - * @memberof module:lib + * ``` */ -class ReverseLineSplitter extends stream.Transform { - constructor(options) { - super(options); - this._partial = null; - } +export class ReverseLineSplitter extends stream.Transform { + _partial: Buffer | null = null; - _transform(chunk, encoding, callback) { + _transform(chunk: Buffer, _encoding: string, callback: stream.TransformCallback) { if (this._partial) { chunk = Buffer.concat([chunk, this._partial]); this._partial = null; @@ -103,14 +97,14 @@ class ReverseLineSplitter extends stream.Transform { } } - let line = chunk.slice(next + 1, end + 1); - chunk = chunk.slice(0, next + 1); + let line = chunk.subarray(next + 1, end + 1); + chunk = chunk.subarray(0, next + 1); this.push(line); } callback(); } - _flush(callback) { + _flush(callback: stream.TransformCallback) { if (this._partial) { let end = this._partial.length - 1; if (this._partial[end] === "\n".charCodeAt(0)) { @@ -120,7 +114,7 @@ class ReverseLineSplitter extends stream.Transform { end -= 1; } } - this.push(this._partial.slice(0, end + 1)); + this.push(this._partial.subarray(0, end + 1)); this._partial = null; } callback(); @@ -128,6 +122,8 @@ class ReverseLineSplitter extends stream.Transform { } +type ReadStreamOptions = Parameters[1]; + /** * Create a file read stream running in reverse * @@ -136,19 +132,28 @@ class ReverseLineSplitter extends stream.Transform { * are not reversed, thus the file content can be recreated by concatenating * the chunks together in the reverse order they are read. * - * @param {string} path - + * @param path - * Path to file to open. May be anything accepted by fs.open. - * @param {object} options - + * @param options - * Options to pass to fs.createReadStream. Values passed for `fd` and * `fs` will be ignored. - * @memberof module:lib */ -async function createReverseReadStream(path, options) { +export async function createReverseReadStream(path: string, options: ReadStreamOptions) { + if (typeof options === "string") { + options = { encoding: options }; + } const fileFd = await fs.open(path, "r"); const fileSize = (await fs.fstat(fileFd)).size; let filePosition = fileSize; const reverseFs = { - read(fd, buffer, offset, length, position, callback) { + read( + fd: number, + buffer: Buffer, + offset: number, + length: number, + position: number, + callback: () => void, + ) { assert(fd === fileFd); assert(position === undefined); length = Math.min(length, filePosition); @@ -156,13 +161,10 @@ async function createReverseReadStream(path, options) { return fs.read(fd, buffer, offset, length, filePosition, callback); }, open() { assert(false); }, - close(fd, callback) { return fs.close(fd, callback); }, + close(fd: number, callback: () => void) { return fs.close(fd, callback); }, }; - return fs.createReadStream("", { ...options, fd: fileFd, fs: reverseFs }); + return fs.createReadStream( + "", + { ...options, fd: fileFd, fs: reverseFs } as ReadStreamOptions, // type specification is missing fs prop. + ); } - -module.exports = { - LineSplitter, - ReverseLineSplitter, - createReverseReadStream, -}; From e0c197b9bc0e17101d0cf4f520a4f7579b9ea2a7 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 6 Aug 2023 01:03:13 +0200 Subject: [PATCH 06/13] Port basic lib modules to TypeScript --- packages/lib/package.json | 1 + ...entialBackoff.js => ExponentialBackoff.ts} | 56 +- .../src/{PlayerStats.js => PlayerStats.ts} | 20 +- .../src/{RateLimiter.js => RateLimiter.ts} | 46 +- packages/lib/src/{command.js => command.ts} | 123 ++- packages/lib/src/data/messages_instance.js | 2 +- packages/lib/src/data/messages_user.js | 2 +- packages/lib/src/{database.js => database.ts} | 83 +- packages/lib/src/errors.js | 92 --- packages/lib/src/errors.ts | 75 ++ packages/lib/src/{file_ops.js => file_ops.ts} | 84 +- packages/lib/src/{hash.js => hash.ts} | 22 +- packages/lib/src/{ini.js => ini.ts} | 24 +- packages/lib/src/is_deep_strict_equal.d.ts | 1 + packages/lib/src/{logging.js => logging.ts} | 106 ++- .../{logging_utils.js => logging_utils.ts} | 259 +++++-- .../lib/src/{lua_tools.js => lua_tools.ts} | 11 +- packages/lib/src/{plugin.js => plugin.ts} | 575 +++++++------- .../{plugin_loader.js => plugin_loader.ts} | 72 +- .../lib/src/{prometheus.js => prometheus.ts} | 729 +++++++++--------- packages/lib/src/{schema.js => schema.ts} | 22 +- ...{shared_commands.js => shared_commands.ts} | 67 +- packages/lib/src/stream.ts | 2 +- packages/lib/src/{users.js => users.ts} | 205 +++-- packages/lib/src/{zip_ops.js => zip_ops.ts} | 15 +- 25 files changed, 1370 insertions(+), 1324 deletions(-) rename packages/lib/src/{ExponentialBackoff.js => ExponentialBackoff.ts} (70%) rename packages/lib/src/{PlayerStats.js => PlayerStats.ts} (85%) rename packages/lib/src/{RateLimiter.js => RateLimiter.ts} (77%) rename packages/lib/src/{command.js => command.ts} (69%) rename packages/lib/src/{database.js => database.ts} (74%) delete mode 100644 packages/lib/src/errors.js create mode 100644 packages/lib/src/errors.ts rename packages/lib/src/{file_ops.js => file_ops.ts} (73%) rename packages/lib/src/{hash.js => hash.ts} (60%) rename packages/lib/src/{ini.js => ini.ts} (81%) create mode 100644 packages/lib/src/is_deep_strict_equal.d.ts rename packages/lib/src/{logging.js => logging.ts} (53%) rename packages/lib/src/{logging_utils.js => logging_utils.ts} (59%) rename packages/lib/src/{lua_tools.js => lua_tools.ts} (64%) rename packages/lib/src/{plugin.js => plugin.ts} (60%) rename packages/lib/src/{plugin_loader.js => plugin_loader.ts} (66%) rename packages/lib/src/{prometheus.js => prometheus.ts} (76%) rename packages/lib/src/{schema.js => schema.ts} (59%) rename packages/lib/src/{shared_commands.js => shared_commands.ts} (72%) rename packages/lib/src/{users.js => users.ts} (83%) rename packages/lib/src/{zip_ops.js => zip_ops.ts} (75%) diff --git a/packages/lib/package.json b/packages/lib/package.json index c0a121c0..40f530a5 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", + "@types/triple-beam": "^1.3.2", "typescript": "^5.1.6" } } diff --git a/packages/lib/src/ExponentialBackoff.js b/packages/lib/src/ExponentialBackoff.ts similarity index 70% rename from packages/lib/src/ExponentialBackoff.js rename to packages/lib/src/ExponentialBackoff.ts index 5af3033f..1e5550b5 100644 --- a/packages/lib/src/ExponentialBackoff.js +++ b/packages/lib/src/ExponentialBackoff.ts @@ -1,6 +1,3 @@ -"use strict"; - - /** * Helper class for implementing the exponential backoff algorithm * @@ -14,49 +11,48 @@ * the passed value of `max`. The value for the delay will reset back to * `base` if more time than the value for `reset` has passed between two * invocations of delay. - * - * @alias module:lib.ExponentialBackoff */ -class ExponentialBackoff { +export default class ExponentialBackoff { + /** + * Base delay in seconds. The first delay will on average be this + * long, but can be anywhere between 0 and 2 times this value. + */ + base = 1; + /** + * Maximum delay in seconds. A delay will not take more than this + * value. + */ + max = 60; + /** + * Time in seconds between two invocations of delay which will cause + * it to start over with `base` instead of doubling. + */ + reset: number; + + private _exp = 0; + private _lastInvocationTime = Date.now(); + /** * Construct ExponentialBackoff helper * * The options passed may also be modified on the instance at runtime. * - * @param {object=} options - + * @param options - * Options for this exponential backoff helper - * @param {number=} [options.base=1] - + * @param [options.base=1] - * Base delay in seconds. The first delay will on average be this * long, but can be anywhere between 0 and 2 times this value. - * @param {number=} [options.max=60] - + * @param [options.max=60] - * Maximum delay in seconds. A delay will not take more than this * value. - * @param {number=} [options.reset=2*max] - + * @param [options.reset=2*max] - * Time in seconds between two invocations of delay which will cause * it to start over with `base` instead of doubling. */ - constructor(options = {}) { - /** - * Base delay in seconds. The first delay will on average be this - * long, but can be anywhere between 0 and 2 times this value. - * @type {number} - */ + constructor(options: { base?: number, max?: number, reset?: number } = {}) { this.base = options.base || 1; - /** - * Maximum delay in seconds. A delay will not take more than this - * value. - * @type {number} - */ this.max = options.max || 60; - /** - * Time in seconds between two invocations of delay which will cause - * it to start over with `base` instead of doubling. - * @type {number} - */ this.reset = options.reset || 2 * this.max; - - this._exp = 0; - this._lastInvocationTime = Date.now(); } /** @@ -66,7 +62,7 @@ class ExponentialBackoff { * algorithm. This is a value between 0 and min(2 * options.base * * 2retries, optinos.max). * - * @returns {number} time in milliseconds to wait. + * @returns time in milliseconds to wait. */ delay() { let invocationTime = Date.now(); diff --git a/packages/lib/src/PlayerStats.js b/packages/lib/src/PlayerStats.ts similarity index 85% rename from packages/lib/src/PlayerStats.js rename to packages/lib/src/PlayerStats.ts index 1c7c873b..ed1bb244 100644 --- a/packages/lib/src/PlayerStats.js +++ b/packages/lib/src/PlayerStats.ts @@ -1,39 +1,31 @@ -"use strict"; - /** * Stastics collected about a player. - * @alias module:lib.PlayerStats */ -class PlayerStats { +export default class PlayerStats { /** * Count of the number of times this player has been seen joining this server. - * @type {number} */ joinCount = 0; /** * Time in ms this player has been seen online on this instance. - * @type {number} */ onlineTimeMs = 0; /** * Timestamp the player was last seen joining. - * @type {Date=} */ - lastJoinAt; + lastJoinAt?: Date; /** * Timestamp the player was last seen leaving. - * @type {Date=} */ - lastLeaveAt; + lastLeaveAt?: Date; /** * Reason the player was last seen leaving with. - * @type {string=} */ - lastLeaveReason; + lastLeaveReason?: string; static jsonSchema = { type: "object", @@ -62,7 +54,7 @@ class PlayerStats { } } - static fromJSON(json) { + static fromJSON(json: any) { return new this(json); } @@ -84,5 +76,3 @@ class PlayerStats { return json; } } - -module.exports = PlayerStats; diff --git a/packages/lib/src/RateLimiter.js b/packages/lib/src/RateLimiter.ts similarity index 77% rename from packages/lib/src/RateLimiter.js rename to packages/lib/src/RateLimiter.ts index 7a82d0c1..c7362881 100644 --- a/packages/lib/src/RateLimiter.js +++ b/packages/lib/src/RateLimiter.ts @@ -1,6 +1,3 @@ -"use strict"; - - /** * Helper class for rate limiting actions * @@ -8,6 +5,7 @@ * limited on invocations. * * @example + * ```ts * const rateLimiter = RateLimiter({ * action: () => { console.log("action"); }, * maxRate: 1, @@ -19,36 +17,36 @@ * // and one more time 1 second later for 99 other activations that were * // exceeding the rate limit. * } - * @alias module:lib.RateLimiter + * ``` */ -class RateLimiter { +export default class RateLimiter { + /** + * Maximum rate of activations per second. + */ + maxRate: number; + /** + * Callback invoked at most `maxRate` per second on activation. + */ + action: Function | null; + + private _lastRun = 0; + private _runTimeout = null; + private lastProgressBroadcast = Date.now(); + /** * Construct RateLimiter helper * * The options passed may also be modified on the instance at runtime. * - * @param {object=} options - Options for this rate limiter - * @param {number=} options.maxRate - + * @param options - Options for this rate limiter + * @param options.maxRate - * Maximum rate of activations per second. - * @param {function()=} options.action - + * @param options.action - * Callback invoked at maste `maxRate` per second on activation. */ - constructor(options = {}) { - /** - * Maximum rate of activations per second. - * @type {number} - */ + constructor(options: { maxRate?: number, action?: Function } = {}) { this.maxRate = options.maxRate || 1; - - /** - * Callback invoked at most `maxRate` per second on activation. - * @type {function()} - */ this.action = options.action || null; - - this._lastRun = 0; - this._runTimeout = null; - this.lastProgressBroadcast = Date.now(); } /** @@ -59,7 +57,7 @@ class RateLimiter { * invoked immediatly, otherwise a timeout is registered to invoke the * action at next point in time where it will be within the rate limit. * - * @returns {boolean} true if activation was within the rate limit. + * @returns true if activation was within the rate limit. */ activate() { let now = Date.now(); @@ -100,5 +98,3 @@ class RateLimiter { } } } - -module.exports = RateLimiter; diff --git a/packages/lib/src/command.js b/packages/lib/src/command.ts similarity index 69% rename from packages/lib/src/command.js rename to packages/lib/src/command.ts index 972f6969..f0caf2f5 100644 --- a/packages/lib/src/command.js +++ b/packages/lib/src/command.ts @@ -6,29 +6,37 @@ * @author Hornwitser * @module lib/command */ -"use strict"; -const libData = require("./data"); -const libLink = require("./link"); -const libErrors = require("./errors"); +import * as libData from "./data"; +import * as libErrors from "./errors"; +import type { Link } from "./link"; + + +export type CommandHandler = (args: Record, control: Link) => Promise; +export type CommandDefinition = [string | string[], string?, Function?]; /** * Represents a command that can be runned by clusterioctl - * @static */ -class Command { +export class Command { + private _handler: CommandHandler; + private _definition: CommandDefinition; + + name: string; + alias: string[]; + /** * Define an executable command. * - * @param {Object} cmd - Command definiton. - * @param {Array} cmd.definiton - + * @param cmd - Command definiton. + * @param cmd.definiton - * Arguments to pass to yargs .command method to define this * command. - * @param {function(args, control)} handler - + * @param cmd.handler - * Async function invoked when the command is executed. Is given * the parsed args and a reference to the {@link * module:ctl/ctl.Control} instance. */ - constructor({ definition, handler }) { + constructor({ definition, handler }: { definition: CommandDefinition, handler: CommandHandler }) { if (definition[0] instanceof Array) { this.name = definition[0][0].split(" ")[0]; this.alias = definition[0].slice(1); @@ -40,11 +48,11 @@ class Command { this._handler = handler; } - register(yargs) { + register(yargs: any) { yargs.command(...this._definition); } - async run(args, control) { + async run(args: Record, control: Link) { await this._handler(args, control); } } @@ -56,20 +64,23 @@ class Command { * CommandTrees. This is used by clusterioctl to hold the full tree of * available commands, you may extend this tree by using a control plugin, * see {@link module:lib.BaseControlPlugin#addCommands} - * - * @static */ -class CommandTree { +export class CommandTree { + public name: string; + public alias: string[]; + public description: string; + public subCommands: Map; + /** * Define a command containing sub commands. * - * @param {Object} cmd - Command definition - * @param {string} cmd.name - Name of the command. - * @param {Array} cmd.alias - Aliases for this tree. - * @param {description} cmd.description - + * @param cmd - Command definition + * @param cmd.name - Name of the command. + * @param cmd.alias - Aliases for this tree. + * @param cmd.description - * Descripton to provide for this command tree node. */ - constructor({ name, alias, description }) { + constructor({ name, alias, description }: { name: string, alias: string[], description: string }) { if (typeof name !== "string") { throw new Error("name must be a string"); } @@ -83,10 +94,10 @@ class CommandTree { /** * Add a command or command tree to this tree * - * @param {module:lib.Command|module:lib.CommandTree} command - + * @param command - * The command to add to this command tree. */ - add(command) { + add(command: Command | CommandTree) { if (this.subCommands.has(command.name)) { throw new Error(`Command ${command.name} already exists`); } @@ -102,13 +113,13 @@ class CommandTree { /** * Get a command or command tree from this tree * - * @param {string} name - + * @param name - * The name of the command or command tree to retrieve. - * @return {?module:lib.Command|module:lib.CommandTree} + * @return * The command to add to this command tree. */ - get(name) { - return this.subCommands.get(name) || null; + get(name: string) { + return this.subCommands.get(name); } /** @@ -117,10 +128,10 @@ class CommandTree { * Traverses the the tree of sub-commands recursively and adds all of * them to the given yargs parser. * - * @param {Object} yargs - yargs parser to register with. + * @param yargs - yargs parser to register with. */ - register(yargs) { - yargs.command([this.name].concat(this.alias), this.description, (yargs) => { + register(yargs: any) { + yargs.command([this.name].concat(this.alias), this.description, (yargs: any) => { for (let [name, command] of this.subCommands) { // Check if the entry is not an alias. if (name === command.name) { @@ -141,13 +152,12 @@ class CommandTree { * Resolves a string with either an host name or an id into an integer with * the host ID. * - * @param {module:lib.Link} client - link to controller to query host on. - * @param {string} hostName - string with name or id of host. - * @returns {Promise} host ID. - * @static + * @param client - link to controller to query host on. + * @param hostName - string with name or id of host. + * @returns host ID. */ -async function resolveHost(client, hostName) { - let hostId; +export async function resolveHost(client: Link, hostName: string) { + let hostId: number; if (/^-?\d+$/.test(hostName)) { hostId = parseInt(hostName, 10); } else { @@ -173,13 +183,12 @@ async function resolveHost(client, hostName) { * Resolves a string with either an instance name or an id into an integer * with the instance ID. * - * @param {module:lib.Link} client - link to controller to query instance on. - * @param {string} instanceName - string with name or id of instance. - * @returns {Promise} instance ID. - * @static + * @param client - link to controller to query instance on. + * @param instanceName - string with name or id of instance. + * @returns instance ID. */ -async function resolveInstance(client, instanceName) { - let instanceId; +export async function resolveInstance(client: Link, instanceName: string) { + let instanceId: number; if (/^-?\d+$/.test(instanceName)) { instanceId = parseInt(instanceName, 10); } else { @@ -205,14 +214,13 @@ async function resolveInstance(client, instanceName) { * Resolevs a string with either a mod pack name or an id into an integer * with the mod pack ID. * - * @param {module:lib.Link} client - + * @param client - * link to controller to query mod pack on. - * @param {string} modPackName - string with name or id of mod pack. - * @returns {Promise} mod pack ID. - * @static + * @param modPackName - string with name or id of mod pack. + * @returns mod pack ID. */ -async function resolveModPack(client, modPackName) { - let modPackId; +export async function resolveModPack(client: Link, modPackName: string) { + let modPackId: number; if (/^-?\d+/.test(modPackName)) { modPackId = parseInt(modPackName, 10); } else { @@ -238,15 +246,14 @@ async function resolveModPack(client, modPackName) { * Resolves a string with either a role name or an id into an object * representing the role. * - * @param {module:lib.Link} client - link to controller to query role on. - * @param {string} roleName - string with name or id of role. - * @returns {Promise} Role info. - * @static + * @param client - link to controller to query role on. + * @param roleName - string with name or id of role. + * @returns Role info. */ -async function retrieveRole(client, roleName) { +export async function retrieveRole(client: Link, roleName: string) { let roles = await client.sendTo("controller", new libData.RoleListRequest()); - let resolvedRole; + let resolvedRole: libData.RawRole; if (/^-?\d+$/.test(roleName)) { let roleId = parseInt(roleName, 10); for (let role of roles) { @@ -271,13 +278,3 @@ async function retrieveRole(client, roleName) { return resolvedRole; } - -module.exports = { - Command, - CommandTree, - - resolveHost, - resolveInstance, - resolveModPack, - retrieveRole, -}; diff --git a/packages/lib/src/data/messages_instance.js b/packages/lib/src/data/messages_instance.js index 311bf0cf..da661418 100644 --- a/packages/lib/src/data/messages_instance.js +++ b/packages/lib/src/data/messages_instance.js @@ -1,5 +1,5 @@ "use strict"; -const PlayerStats = require("../PlayerStats"); +const PlayerStats = require("../PlayerStats").default; const { JsonString, jsonArray } = require("./composites"); /** @memberof module:lib */ diff --git a/packages/lib/src/data/messages_user.js b/packages/lib/src/data/messages_user.js index 47bc1ff3..edd391cd 100644 --- a/packages/lib/src/data/messages_user.js +++ b/packages/lib/src/data/messages_user.js @@ -1,5 +1,5 @@ "use strict"; -const PlayerStats = require("../PlayerStats"); +const PlayerStats = require("../PlayerStats").default; const { jsonArray, JsonNumber } = require("./composites"); /* @memberof module:lib */ diff --git a/packages/lib/src/database.js b/packages/lib/src/database.ts similarity index 74% rename from packages/lib/src/database.js rename to packages/lib/src/database.ts index 4c2b8520..3bc65235 100644 --- a/packages/lib/src/database.js +++ b/packages/lib/src/database.ts @@ -4,23 +4,21 @@ * @module lib/database * @author Hornwitser */ +import fs from "fs-extra"; -"use strict"; -const fs = require("fs-extra"); - -const libFileOps = require("./file_ops"); -const { basicType } = require("./helpers"); +import * as libFileOps from "./file_ops"; +import { basicType } from "./helpers"; /** * Converts a Map with only string keys to a JavaScript object. * - * @param {Map} map - the Map to convert to an object. - * @return {object} - Object with the mapping's key-values as properties. + * @param map - the Map to convert to an object. + * @return Object with the mapping's key-values as properties. * @throws {Error} if there are non-string keys in map. */ -function mapToObject(map) { - let obj = {}; +export function mapToObject(map: Map) { + let obj: Record = {}; for (let [key, value] of map) { if (typeof key !== "string") { throw new Error( @@ -40,14 +38,15 @@ function mapToObject(map) { * properties as key value pairs and converts it into a JavaScript Map. * Returns an empty Map if the file does not exist. * - * @param {string} filePath - The path to the JSON file to load. + * @param filePath - The path to the JSON file to load. + * @returns promise resolving to the loaded map * @throws {Error} If JSON file did not contain an object. * @throws {Error} If an error occured reading the file. */ -async function loadJsonAsMap(filePath) { +export async function loadJsonAsMap(filePath: string): Promise> { try { let content = await fs.readFile(filePath); - let parsed = JSON.parse(content); + let parsed = JSON.parse(content.toString()); if (basicType(parsed) !== "object") { throw new Error(`Expected object but got ${basicType(parsed)}`); @@ -72,12 +71,12 @@ async function loadJsonAsMap(filePath) { * If the directory the file is to be saved into doesn't exist it will be * created. * - * @param {string} filePath - The path to the JSON file that will be saved. - * @param {Map} map - Map with only string keys to save. + * @param filePath - The path to the JSON file that will be saved. + * @param map - Map with only string keys to save. * @throws {Error} if there are non-string keys in map. * @throws {Error} if an error occured writing to the file. */ -async function saveMapAsJson(filePath, map) { +export async function saveMapAsJson(filePath: string, map: Map) { let obj = mapToObject(map); await libFileOps.safeOutputFile(filePath, JSON.stringify(obj, null, 4)); } @@ -88,15 +87,15 @@ async function saveMapAsJson(filePath, map) { * Loads the JSON file specified by path containing an array of objects that * each have an id property into a Map from the id to the object. * - * @param {string} filePath - The path to the JSON file to load. + * @param filePath - The path to the JSON file to load. * @throws {Error} If JSON file did not contain an array. * @throws {Error} If there are objects in the array without an id property. * @throws {Error} If an error occured reading the file. */ -async function loadJsonArrayAsMap(filePath) { +export async function loadJsonArrayAsMap(filePath: string): Promise> { try { let content = await fs.readFile(filePath); - let parsed = JSON.parse(content); + let parsed = JSON.parse(content.toString()); if (basicType(parsed) !== "array") { throw new Error(`Expected array but got ${basicType(parsed)}`); @@ -132,24 +131,24 @@ async function loadJsonArrayAsMap(filePath) { * Save the values of a Map with objects containing an id property that is * equal to the key the objects are stored at into a JSON file. * - * @param {string} filePath - The path to the JSON file that will be saved. - * @param {Map} map - + * @param filePath - The path to the JSON file that will be saved. + * @param map - * Map with objects containing an id property equal to the key they are * stored under. * @throws {Error} if an error occured writing to the file. */ -async function saveMapAsJsonArray(filePath, map) { +export async function saveMapAsJsonArray(filePath: string, map: Map) { await libFileOps.safeOutputFile(filePath, JSON.stringify([...map.values()], null, 4)); } -function checkName(name) { +function checkName(name: unknown): asserts name is string { if (typeof name !== "string") { throw new Error("name must be a string"); } } -function checkCount(count) { +function checkCount(count: unknown): asserts count is number { if (typeof count !== "number" || isNaN(count)) { throw new Error("count must be a number"); } @@ -164,7 +163,8 @@ function checkCount(count) { * serialized the database discards entries with a zero counts, and when * deserialized the content is verified. */ -class ItemDatabase { +export class ItemDatabase { + private _items: Map = new Map(); /** * Create a new item database @@ -174,9 +174,7 @@ class ItemDatabase { * database from. An empty database will be created if this parameter * is left undefined. */ - constructor(serialized) { - this._items = new Map(); - + constructor(serialized?: object) { // Verify the content of the serialized database if (serialized !== undefined) { for (let [name, count] of Object.entries(serialized)) { @@ -194,10 +192,10 @@ class ItemDatabase { * Serialize the item database into a plain JavaScript object that can be * turned inta a string with JSON.stringify(). * - * @returns {object} Serialized representation of the database + * @returns Serialized representation of the database */ serialize() { - let obj = {}; + let obj: Record = {}; for (let [name, count] of this._items) { if (count !== 0) { obj[name] = count; @@ -223,10 +221,10 @@ class ItemDatabase { * database. If the item has not been previously stored into the database * then 0 is returned. * - * @param {string} name - The name of the item to get the count of. - * @returns {number} The count of the item stored. + * @param name - The name of the item to get the count of. + * @returns The count of the item stored. */ - getItemCount(name) { + getItemCount(name: string) { checkName(name); if (!this._items.has(name)) { @@ -241,10 +239,10 @@ class ItemDatabase { * * Add count copies of the item specified by name to the item database. * - * @param {string} name - The name of the item to add. - * @param {number} count - The count of item to remove. + * @param name - The name of the item to add. + * @param count - The count of item to remove. */ - addItem(name, count) { + addItem(name: string, count: number) { checkName(name); checkCount(count); @@ -262,10 +260,10 @@ class ItemDatabase { * If count is greater than the currently stored count the stored item * count will become negative. * - * @param {string} name - The name of the item to remove. - * @param {number} count - The count of items to remove. + * @param name - The name of the item to remove. + * @param count - The count of items to remove. */ - removeItem(name, count) { + removeItem(name: string, count: number) { checkName(name); checkCount(count); @@ -277,12 +275,3 @@ class ItemDatabase { this._items.set(name, count); }; } - -module.exports = { - mapToObject, - loadJsonAsMap, - saveMapAsJson, - loadJsonArrayAsMap, - saveMapAsJsonArray, - ItemDatabase, -}; diff --git a/packages/lib/src/errors.js b/packages/lib/src/errors.js deleted file mode 100644 index e7de55df..00000000 --- a/packages/lib/src/errors.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Errors thrown in Clusterio - * @module lib/errors - */ -"use strict"; - -/** - * Thrown by commands when they fail - * @static - */ -class CommandError extends Error { } - -/** - * Thrown from requests sent when an error occured handling it - * @static - */ -class RequestError extends Error { - constructor(message, code, stack) { - super(message); - if (code) { this.code = code; } - if (stack) { this.stack = stack; } - } -} - -/** - * Thrown when a permission check fails. - * - * Is a subclass of RequestError to prevent logging stack traces when - * requests fail due to permission denied. - * - * @static - */ -class PermissionError extends RequestError { } - -/** - * Signal for messages that fail validation - * @static - */ -class InvalidMessage extends Error { - constructor(msg, errors) { - super(msg); - this.errors = errors || null; - } -} - -/** - * Thrown from requests when the session it was sent on was lost - * @static - */ -class SessionLost extends Error { } - -/** - * Thrown when WebSocket authentication failed - * @static - */ -class AuthenticationFailed extends Error { } - -/** - * Errror class for known errors occuring during startup - * @static - */ -class StartupError extends Error { } - -/** - * Errors outside of our control - * @static - */ -class EnvironmentError extends Error { } - -/** - * Errors caused by plugins - * @static - */ -class PluginError extends Error { - constructor(pluginName, original) { - super(`PluginError: ${original.message}`); - this.pluginName = pluginName; - this.original = original; - } -} - -module.exports = { - CommandError, - RequestError, - PermissionError, - InvalidMessage, - SessionLost, - AuthenticationFailed, - StartupError, - EnvironmentError, - PluginError, -}; diff --git a/packages/lib/src/errors.ts b/packages/lib/src/errors.ts new file mode 100644 index 00000000..30b26cf3 --- /dev/null +++ b/packages/lib/src/errors.ts @@ -0,0 +1,75 @@ +/** + * Errors thrown in Clusterio + * @module lib/errors + */ + +/** + * Thrown by commands when they fail + */ +export class CommandError extends Error { } + +/** + * Thrown from requests sent when an error occured handling it + */ +export class RequestError extends Error { + constructor( + message: string, + public code?: string, + public stack?: string, + ) { + super(message); + } +} + +/** + * Thrown when a permission check fails. + * + * Is a subclass of RequestError to prevent logging stack traces when + * requests fail due to permission denied. + * + */ +export class PermissionError extends RequestError { } + +/** + * Signal for messages that fail validation + */ +export class InvalidMessage extends Error { + constructor( + message: string, + public errors: object = null, + ) { + super(message); + } +} + +/** + * Thrown from requests when the session it was sent on was lost + */ +export class SessionLost extends Error { } + +/** + * Thrown when WebSocket authentication failed + */ +export class AuthenticationFailed extends Error { } + +/** + * Errror class for known errors occuring during startup + */ +export class StartupError extends Error { } + +/** + * Errors outside of our control + */ +export class EnvironmentError extends Error { } + +/** + * Errors caused by plugins + */ +export class PluginError extends Error { + constructor( + public pluginName: string, + public original: Error + ) { + super(`PluginError: ${original.message}`); + } +} diff --git a/packages/lib/src/file_ops.js b/packages/lib/src/file_ops.ts similarity index 73% rename from packages/lib/src/file_ops.js rename to packages/lib/src/file_ops.ts index 3a19ba2f..0a67ebb3 100644 --- a/packages/lib/src/file_ops.js +++ b/packages/lib/src/file_ops.ts @@ -1,25 +1,24 @@ /** * @module lib/file_ops */ -"use strict"; -const fs = require("fs-extra"); -const path = require("path"); -const crypto = require("crypto"); // needed for getTempFile +import fs from "fs-extra"; +import path from "path"; +import crypto from "crypto"; // needed for getTempFile /** * Returns the newest file in a directory * - * @param {string} directory - The directory to look for the newest file in. - * @param {function(string): boolean} filter - + * @param directory - The directory to look for the newest file in. + * @param filter - * Optional function to filter out files to ignore. Should return true * if the file is to be considered. - * @returns {Promise} + * @returns * Name of the file with the newest timestamp or null if the * directory contains no files. */ -async function getNewestFile(directory, filter = (name) => true) { +export async function getNewestFile(directory: string, filter = (_name: string) => true) { let newestTime = new Date(0); - let newestFile; + let newestFile: string | undefined; for (let entry of await fs.readdir(directory, { withFileTypes: true })) { if (entry.isFile()) { let stat = await fs.stat(path.join(directory, entry.name)); @@ -40,11 +39,11 @@ async function getNewestFile(directory, filter = (name) => true) { * Error reading the size of files are ignored, and error reading the * directory will result in 0 being returned. * - * @param {string} directory - The directory to sum files in. - * @returns {Promise} The size in bytes of all files in the directory. + * @param directory - The directory to sum files in. + * @returns The size in bytes of all files in the directory. */ -async function directorySize(directory) { - let dirEntries; +export async function directorySize(directory: string) { + let dirEntries: fs.Dirent[]; try { dirEntries = await fs.readdir(directory, { withFileTypes: true }); } catch (err) { @@ -53,7 +52,7 @@ async function directorySize(directory) { } throw err; } - let statTasks = []; + let statTasks: Promise[] = []; for (let entry of dirEntries) { if (entry.isFile()) { statTasks.push(fs @@ -77,13 +76,13 @@ async function directorySize(directory) { * racy and a file or folder may have been created in the folder by the time * this function returns. * - * @param {string} directory - directory to check in. - * @param {string} name - file name to check for, may have extension. - * @param {string} extension - dot extension used for the file name. - * @returns {Promise} modified name with extension that likely does + * @param directory - directory to check in. + * @param name - file name to check for, may have extension. + * @param extension - dot extension used for the file name. + * @returns modified name with extension that likely does * not exist in the folder */ -async function findUnusedName(directory, name, extension = "") { +export async function findUnusedName(directory: string, name: string, extension = "") { if (extension && name.endsWith(extension)) { name = name.slice(0, -extension.length); } @@ -101,25 +100,27 @@ async function findUnusedName(directory, name, extension = "") { } } } + /** + * Generate collision resistante temporary file name + * * Warning: this function should not be relied upon to be accurate for * security sensitive applications. The selection process is inherently * racy and a file or folder may have been created in the folder by the time * this function returns. * - * @param {string} prefix - Prefix for file - * @param {string} suffix - Suffix for file - * @param {string} tmpdir - Directory for temp file + * @param prefix - Prefix for file + * @param suffix - Suffix for file + * @param tmpdir - Directory for temp file + * @returns Temporary file name */ -async function getTempFile(prefix, suffix, tmpdir) { - prefix = (typeof prefix !== "undefined") ? prefix : "tmp."; - suffix = (typeof suffix !== "undefined") ? suffix : ""; - tmpdir = (typeof tmpdir !== "undefined") ? tmpdir : "./"; +export async function getTempFile(prefix = "tmp.", suffix = "", tmpdir = "./") { let fileName = path.join(prefix + crypto.randomBytes(16).toString("hex") + suffix); let freeFile = await findUnusedName(tmpdir, fileName); let fullPath = path.join(tmpdir, freeFile); return fullPath; } + /** * Safely write data to a file * @@ -131,11 +132,11 @@ async function getTempFile(prefix, suffix, tmpdir) { * should not be too much of an issue as the next time the same file is * written the temporary will be overwritten and renamed to the target file. * - * @param {string} file - Path to file to write. - * @param {string|Buffer} data - Content to write. - * @param {object|string} options - see fs.writeFile, `flag` must not be set. + * @param file - Path to file to write. + * @param data - Content to write. + * @param options - see fs.writeFile, `flag` must not be set. */ -async function safeOutputFile(file, data, options={}) { +export async function safeOutputFile(file: string, data: string | Buffer, options: fs.WriteFileOptions ={}) { let directory = path.dirname(file); if (!await fs.pathExists(directory)) { await fs.mkdirs(directory); @@ -153,7 +154,7 @@ async function safeOutputFile(file, data, options={}) { } -// Reserved names by allmost all filesystems +// Reserved names by almost all filesystems const badNames = [".", ".."]; // Reserved namespaces in Windows @@ -167,10 +168,10 @@ const badWinNamespaces = [ /** * Check if a string is a valid file name * - * @param {string} name - Name to check + * @param name - Name to check * @throws Error if the name is unsuitable. */ -function checkFilename(name) { +export function checkFilename(name: string) { // All of these are bad in Windows only, except for /, . and .. // See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file const badChars = /[<>:"\/\\|?*\x00-\x1f]/g; @@ -208,10 +209,10 @@ function checkFilename(name) { /** * Clean up string to be suitable for use as filename * - * @param {string} name - Arbitrary name string - * @returns {string} Filename suitable to use in the filesystem + * @param name - Arbitrary name string + * @returns Filename suitable to use in the filesystem */ -function cleanFilename(name) { +export function cleanFilename(name: string) { // copied from checkFilename due to RegExp with global flag containing state. const badChars = /[<>:"\/\\|?*\x00-\x1f]/g; const badEnd = /[. ]$/; @@ -233,14 +234,3 @@ function cleanFilename(name) { return name; } - - -module.exports = { - getNewestFile, - directorySize, - findUnusedName, - getTempFile, - safeOutputFile, - checkFilename, - cleanFilename, -}; diff --git a/packages/lib/src/hash.js b/packages/lib/src/hash.ts similarity index 60% rename from packages/lib/src/hash.js rename to packages/lib/src/hash.ts index 12359602..a2542b9b 100644 --- a/packages/lib/src/hash.js +++ b/packages/lib/src/hash.ts @@ -2,18 +2,17 @@ * Hashing functions * @module lib/hash */ -"use strict"; -const crypto = require("crypto"); -const fs = require("fs-extra"); +import crypto from "crypto"; +import fs from "fs-extra"; /** * Returns a promise that resolves to the SHA1 hash of the stream given * - * @param {Stream} stream - Node stream of the content to hash. - * @returns {Promise} hash of the stream. + * @param stream - Node stream of the content to hash. + * @returns hash of the stream. */ -function hashStream(stream) { +export function hashStream(stream: NodeJS.ReadableStream): Promise { return new Promise((resolve, reject) => { let hasher = crypto.createHash("sha1"); hasher.setEncoding("hex"); @@ -33,14 +32,9 @@ function hashStream(stream) { /** * Returns a promise that resolves to the SHA1 hash of the file given by path * - * @param {string} path - Path to the file to hash. - * @returns {Promise} hash of the file given. + * @param path - Path to the file to hash. + * @returns hash of the file given. */ -function hashFile(path) { +export function hashFile(path: string) { return hashStream(fs.createReadStream(path)); } - -module.exports = { - hashStream, - hashFile, -}; diff --git a/packages/lib/src/ini.js b/packages/lib/src/ini.ts similarity index 81% rename from packages/lib/src/ini.js rename to packages/lib/src/ini.ts index 413aeb92..592d7fb1 100644 --- a/packages/lib/src/ini.js +++ b/packages/lib/src/ini.ts @@ -5,17 +5,14 @@ * @author Hornwitser */ -"use strict"; - /** * Parse INI file into object of sections - * @param {string} input - content of INI file to parse. - * @returns {object>} - * sections parsed from the ini file. + * @param input - content of INI file to parse. + * @returns sections parsed from the ini file. */ -function parse(input) { - const sections = {}; - let currentSection = sections; +export function parse(input: string) { + const sections: Record> = {}; + let currentSection: Record = sections as Record; const lines = input.split(/\r\n|\n/); for (let number = 1; number <= lines.length; number++) { let line = lines[number - 1].replace(/^[\t ]+/, ""); @@ -56,11 +53,11 @@ function parse(input) { /** * Serialize an object of section definitions into an INI file - * @param {object>} sections - + * @param sections - * Sections to encode into ini format - * @returns {string} sections serialized into INI format + * @returns sections serialized into INI format */ -function stringify(sections) { +export function stringify(sections: Record>) { let serialized = []; let first = true; @@ -87,8 +84,3 @@ function stringify(sections) { if (serialized.length) { serialized.push(""); } return serialized.join("\n"); } - -module.exports = { - parse, - stringify, -}; diff --git a/packages/lib/src/is_deep_strict_equal.d.ts b/packages/lib/src/is_deep_strict_equal.d.ts new file mode 100644 index 00000000..ec5e4c72 --- /dev/null +++ b/packages/lib/src/is_deep_strict_equal.d.ts @@ -0,0 +1 @@ +export default function isDeepStrictEqual(a: any, b: any): boolean diff --git a/packages/lib/src/logging.js b/packages/lib/src/logging.ts similarity index 53% rename from packages/lib/src/logging.js rename to packages/lib/src/logging.ts index f4fa9c4c..0a24cfb7 100644 --- a/packages/lib/src/logging.js +++ b/packages/lib/src/logging.ts @@ -1,9 +1,9 @@ "use strict"; -const winston = require("winston"); -const Transport = require("winston-transport"); -const { LEVEL, MESSAGE } = require("triple-beam"); +import winston from "winston"; +import Transport from "winston-transport"; +import { LEVEL, MESSAGE } from "triple-beam"; -const levels = Object.freeze({ +export const levels = Object.freeze({ fatal: 0, error: 1, warn: 2, @@ -13,7 +13,7 @@ const levels = Object.freeze({ verbose: 6, }); -const colors = { +export const colors = { fatal: "inverse red", error: "bold brightRed", warn: "bold brightYellow", @@ -21,21 +21,31 @@ const colors = { info: "bold brightBlue", server: "bold", verbose: "bold grey", -}; +} as const; winston.addColors(colors); // The Console Transport is replicated here because it's overly complicated // in winston, writes to stdout/err instead of console and doesn't work in // the browser. -class ConsoleTransport extends Transport { - constructor(options = {}) { +export class ConsoleTransport extends Transport { + errorLevels: Set; + warnLevels: Set; + filter: (info: object) => boolean; + + constructor( + options: Transport.TransportStreamOptions & { + errorLevels?: string[], + warnLevels?: string[], + filter?: ConsoleTransport["filter"] + } = {} + ) { super(options); this.errorLevels = new Set(options.errorLevels || ["fatal", "error"]); this.warnLevels = new Set(options.warnLevels || ["warn"]); this.filter = options.filter || (() => true); } - log(info, callback) { + log(info: any, callback: Function) { if (!this.filter(info)) { return callback(); } @@ -56,12 +66,12 @@ class ConsoleTransport extends Transport { /** * Formats winston log messages for the web console */ -class WebConsoleFormat { - constructor(options = {}) { - this.options = options; - } +export class WebConsoleFormat { + constructor( + public options = {} + ) { } - transform(info, options) { + transform(info: any, _options: {}) { let src = " "; if (info.host_id !== undefined) { src += `s:${info.host_name} - `; @@ -84,31 +94,41 @@ class WebConsoleFormat { /** * Filter object for logs - * @typedef {Object} LogFilter - * @property {string} [maxLevel] - - * Maximum log level to include. Higher levels are more verbose. - * @property {boolean} [all] - - * Include log entries from controller, all hosts and all instances. - * @property {boolean} [controller] - - * Include log entries from the controller. - * @property {Array} [hostIds] - - * Include log entries for the given hosts and instances of those - * hosts by id. - * @property {Array} [instanceIds] - - * Include log entries for the given instances by id. */ +export interface LogFilter { + /** + * Maximum log level to include. Higher levels are more verbose. + */ + maxLevel?: keyof typeof levels; + /** + * Include log entries from controller, all hosts and all instances. + */ + all?: boolean; + /** + * Include log entries from the controller. + */ + controller?: boolean; + /** + * Include log entries for the given hosts and instances of those + * hosts by id. + */ + hostIds?: number[]; + /** + * Include log entries for the given instances by id. + */ + instanceIds?: number[]; +} /** * Create log filter by level and source. * - * @param {LogFilter} filter - + * @param filter - * Filter to filter log entries by. - * @returns {function(object): boolean} + * @returns * filter returning true for log entries that match it. - * @static */ -function logFilter({ all, controller, hostIds, instanceIds, maxLevel }) { - return info => { +export function logFilter({ all, controller, hostIds, instanceIds, maxLevel }: LogFilter) { + return (info: any) => { // Note: reversed to filter out undefined levels if (maxLevel && !(levels[info.level] <= levels[maxLevel])) { return false; @@ -135,17 +155,23 @@ function logFilter({ all, controller, hostIds, instanceIds, maxLevel }) { }; } -const logger = winston.createLogger({ +export const logger = winston.createLogger({ level: "verbose", levels, format: winston.format.timestamp(), -}); +}) as unknown as Logger; -module.exports = { - ConsoleTransport, - WebConsoleFormat, - colors, - levels, - logFilter, - logger, +// Who in their right mind thought that log levels should be hard coded +// into the type when you can define custom levels you want to support? +export type Logger = Omit & { + fatal: winston.LeveledLogMethod, + error: winston.LeveledLogMethod, + warn: winston.LeveledLogMethod, + audit: winston.LeveledLogMethod, + info: winston.LeveledLogMethod, + server: winston.LeveledLogMethod, + verbose: winston.LeveledLogMethod, }; diff --git a/packages/lib/src/logging_utils.js b/packages/lib/src/logging_utils.ts similarity index 59% rename from packages/lib/src/logging_utils.js rename to packages/lib/src/logging_utils.ts index fe627044..0d075e5c 100644 --- a/packages/lib/src/logging_utils.js +++ b/packages/lib/src/logging_utils.ts @@ -5,24 +5,95 @@ * @module lib/logging_utils */ "use strict"; -const events = require("events"); -const fs = require("fs-extra"); -const path = require("path"); -const stream = require("stream"); -const util = require("util"); -const winston = require("winston"); -const Transport = require("winston-transport"); -const { LEVEL, MESSAGE } = require("triple-beam"); -const chalk = require("chalk"); +import fs from "fs-extra"; +import path from "path"; +import stream from "stream"; +import util from "util"; +import winston from "winston"; +import Transport from "winston-transport"; +import { LEVEL, MESSAGE } from "triple-beam"; +import chalk from "chalk"; -const libData = require("./data"); -const libErrors = require("./errors"); -const libFileOps = require("./file_ops"); -const { levels, logFilter, logger } = require("./logging"); -const libStream = require("./stream"); +import * as libData from "./data"; +import * as libErrors from "./errors"; +import * as libFileOps from "./file_ops"; +import { type LogFilter, levels, logFilter, logger } from "./logging"; +import * as libStream from "./stream"; +import type { Link } from "./link"; const finished = util.promisify(stream.finished); +interface SecondsLogOutput { + /** Where the message came from, one of "stdout" and "stderr". */ + source: "stdout" | "stderr"; + /** Timestamp format, one of "date", "seconds" and "none". */ + format: "seconds"; + /** Timestamp of the message. Not present if format is "none". */ + time: string; + /** Type of message, one of "log", "action" and "generic". */ + type: "log"; + /** Log level for "log" type. i.e "Info" normally. */ + level: string; + /** File reported for "log" type. */ + file: string; + /** Main content of the line. */ + message: string; +} +interface SecondsGenericOutput { + /** Where the message came from, one of "stdout" and "stderr". */ + source: "stdout" | "stderr"; + /** Timestamp format, one of "date", "seconds" and "none". */ + format: "seconds"; + /** Timestamp of the message. Not present if format is "none". */ + time: string; + /** Type of message, one of "log", "action" and "generic". */ + type: "generic"; + /** Main content of the line. */ + message: string; +} +interface DateActionOutput { + /** Where the message came from, one of "stdout" and "stderr". */ + source: "stdout" | "stderr"; + /** Timestamp format, one of "date", "seconds" and "none". */ + format: "date"; + /** Timestamp of the message. Not present if format is "none". */ + time: string; + /** Type of message, one of "log", "action" and "generic". */ + type: "action"; + /** Kind of action for "action" type. i.e "CHAT" for chat. */ + action: string; + /** Main content of the line. */ + message: string; +} +interface DateGenericOutput { + /** Where the message came from, one of "stdout" and "stderr". */ + source: "stdout" | "stderr"; + /** Timestamp format, one of "date", "seconds" and "none". */ + format: "date"; + /** Timestamp of the message. Not present if format is "none". */ + time: string; + /** Type of message, one of "log", "action" and "generic". */ + type: "generic"; + /** Main content of the line. */ + message: string; +} +interface UnformattedOutput { + /** Where the message came from, one of "stdout" and "stderr". */ + source: "stdout" | "stderr"; + /** Timestamp format, one of "date", "seconds" and "none". */ + format: "none"; + /** Type of message, one of "log", "action" and "generic". */ + type: "generic"; + /** Main content of the line. */ + message: string; +} +export type ParsedFactorioOutput = + | SecondsLogOutput + | SecondsGenericOutput + | DateActionOutput + | DateGenericOutput + | UnformattedOutput +; /** * Format a parsed Factorio message with colors @@ -30,11 +101,11 @@ const finished = util.promisify(stream.finished); * Formats a parsed Factorio output from lib/factorio into a readable * colorized output using terminal escape codes that can be printed. * - * @param {Object} parsed - Parsed Factorio server output. - * @returns {string} terminal colorized message. + * @param parsed - Parsed Factorio server output. + * @returns terminal colorized message. * @private */ -function formatServerOutput(parsed) { +function formatServerOutput(parsed: ParsedFactorioOutput) { let time = ""; if (parsed.format === "seconds") { time = `${chalk.yellow(parsed.time.padStart(8))} `; @@ -71,15 +142,17 @@ function formatServerOutput(parsed) { /** * Formats winston log messages for a character terminal. - * @static */ -class TerminalFormat { - constructor(options = {}) { - this.options = options; +export class TerminalFormat { + colorize: winston.Logform.Colorizer; + + constructor( + public options: object = {} + ) { this.colorize = winston.format.colorize(options); } - transform(info, options) { + transform(info: any, options: { showTimestamp: boolean }) { info = this.colorize.transform(info, this.colorize.options); let ts = ""; if (options.showTimestamp && info.timestamp) { @@ -112,17 +185,24 @@ class TerminalFormat { /** * Sends logs over a lib/link connection. - * @static */ -class LinkTransport extends Transport { - constructor(options) { +export class LinkTransport extends Transport { + link: Link; + filter?: (info: any) => boolean; + + constructor( + options: Transport.TransportStreamOptions & { + link: Link, + filter?: (info: any) => boolean, + } + ) { super(options); this.link = options.link; - this.filter = options.filter || null; + this.filter = options.filter; } - log(info, callback) { + log(info: any, callback: Function) { if (this.filter && !this.filter(info)) { return callback(); } @@ -144,18 +224,26 @@ const logIndexVersion = 2; /** * Keeps an index over a log directory to speed up queries to it - * @static */ -class LogIndex { - constructor(logDirectory, serialized) { - this.logDirectory = logDirectory; +export class LogIndex { + index: Map, + controller: boolean, + hostIds: Set, + instanceIds: Set, + }>; + + constructor( + public logDirectory: string, + serialized: any, + ) { this.index = new Map(); if (serialized.version !== logIndexVersion) { return; } - for (let [file, serializedEntry] of Object.entries(serialized.files)) { + for (let [file, serializedEntry] of Object.entries(serialized.files) as [string, any][]) { this.index.set(file, { levels: new Set(serializedEntry.levels), controller: serializedEntry.controller, @@ -190,13 +278,13 @@ class LogIndex { * data about previously indexed files saved with {@link * module:lib.LogIndex#save}. * - * @param {string} logDirectory - Path to directory to load from. - * @returns {Promise} loaded or created log index. + * @param logDirectory - Path to directory to load from. + * @returns loaded or created log index. */ - static async load(logDirectory) { + static async load(logDirectory: string) { try { let content = await fs.readFile(path.join(logDirectory, "index.json")); - return new LogIndex(logDirectory, JSON.parse(content)); + return new LogIndex(logDirectory, JSON.parse(content.toString())); } catch (err) { if (err.code !== "ENOENT") { logger.warn(`Failed to load ${path.join(logDirectory, "index.json")}: ${err}`); @@ -218,19 +306,19 @@ class LogIndex { ); } - async indexFile(file) { + async indexFile(file: string) { let filePath = path.join(this.logDirectory, file); let lineStream = new libStream.LineSplitter({ readableObjectMode: true }); let fileStream = fs.createReadStream(filePath); fileStream.pipe(lineStream); let entry = { - levels: new Set(), + levels: new Set() as Set, controller: false, - hostIds: new Set(), - instanceIds: new Set(), + hostIds: new Set() as Set, + instanceIds: new Set() as Set, }; for await (let line of lineStream) { - let info; + let info: any; try { info = JSON.parse(line); } catch (err) { @@ -273,14 +361,14 @@ class LogIndex { /** * Check if a file includes entries for the given filter * - * @param {string} file - + * @param file - * Name of file in the log directory to check for. - * @param {LogFilter} filter - + * @param filter - * Filter to check index if file contains entries for. - * @returns {boolean} + * @returns * true if the file may contain entries included by the filter. */ - filterIncludesFile(file, { maxLevel, all, controller, instanceIds, hostIds }) { + filterIncludesFile(file: string, { maxLevel, all, controller, instanceIds, hostIds }: LogFilter) { let entry = this.index.get(file); if (!entry) { return true; @@ -314,35 +402,51 @@ class LogIndex { /** * Filter object for querying logs - * @typedef {Object} QueryLogFilter - * @property {number} [limit=100] - Maximum number of entries to return. - * @property {string} [order="asc"] - - * Return entries in ascending ("asc") date order or desceding ("desc") - * date order. - * @property {string} [maxLevel] - - * Maximum log level to include. Higher levels are more verbose. - * @property {boolean} [all] - - * Include log entries from controller, all hosts and all instances. - * @property {boolean} [controller] - - * Include log entries from the controller. - * @property {Array} [hostIds] - - * Include log entries for the given hosts and instances of those - * hosts by id. - * @property {Array} [instanceIds] - - * Include log entries for the given instances by id. */ +export interface QueryLogFilter { + /** + * Maximum number of entries to return. Defaults to 100. + */ + limit?: number; + /** + * Return entries in ascending ("asc") date order or desceding ("desc") + * date order. Defaults to "asc". + */ + order: "asc" | "desc"; + /** + * Maximum log level to include. Higher levels are more verbose. + */ + maxLevel: keyof typeof levels; + /** + * Include log entries from controller, all hosts and all instances. + */ + all: boolean; + /** + * Include log entries from the controller. + */ + controller: boolean; + /** + * Include log entries for the given hosts and instances of those + * hosts by id. + */ + hostIds: number[]; + /** + * Include log entries for the given instances by id. + */ + instanceIds: number[]; +} /** * Query log directory * - * @param {string} logDirectory - path to directory with logs. - * @param {QueryLogFilter} filter - + * @param logDirectory - path to directory with logs. + * @param filter - * Filter to limit logs by. - * @param {module:lib.LogIndex=} index - + * @param index - * Index to speed up query with. - * @returns {Promise>} log entries matching the filter + * @returns log entries matching the filter */ -async function queryLog(logDirectory, filter, index) { +export async function queryLog(logDirectory: string, filter: QueryLogFilter, index: LogIndex) { let files = (await fs.readdir(logDirectory)).filter(entry => logFileGlob.test(entry)); filter = { @@ -363,8 +467,8 @@ async function queryLog(logDirectory, filter, index) { continue; } let filePath = path.join(logDirectory, file); - let fileStream; - let lineStream; + let fileStream: fs.ReadStream; + let lineStream: libStream.LineSplitter; if (filter.order === "asc") { lineStream = new libStream.LineSplitter({ readableObjectMode: true }); fileStream = fs.createReadStream(filePath); @@ -373,7 +477,7 @@ async function queryLog(logDirectory, filter, index) { fileStream = await libStream.createReverseReadStream(filePath); } lineStream.on("data", line => { - let info; + let info: any; try { info = JSON.parse(line); } catch (err) { @@ -403,27 +507,18 @@ async function queryLog(logDirectory, filter, index) { return log; } -function handleUnhandledErrors() { +export function handleUnhandledErrors() { /* eslint-disable node/no-process-exit */ process.on("uncaughtException", err => { logger.fatal(`Uncaught exception:\n${err.stack}`); process.exit(1); }); - process.on("unhandledRejection", (reason, promise) => { + process.on("unhandledRejection", (reason: Error, _promise) => { logger.fatal(`Unhandled rejection:\n${reason.stack ? reason.stack : reason}`); process.exit(1); }); /* eslint-enable node/no-process-exit */ } -const _formatServerOutput = formatServerOutput; -module.exports = { - TerminalFormat, - LinkTransport, - LogIndex, - handleUnhandledErrors, - queryLog, - - // for testing only - _formatServerOutput, -}; +// for testing only +export const _formatServerOutput = formatServerOutput; diff --git a/packages/lib/src/lua_tools.js b/packages/lib/src/lua_tools.ts similarity index 64% rename from packages/lib/src/lua_tools.js rename to packages/lib/src/lua_tools.ts index 086af446..b9dbba13 100644 --- a/packages/lib/src/lua_tools.js +++ b/packages/lib/src/lua_tools.ts @@ -2,15 +2,14 @@ * Utilities for dealing with Lua * @module lib/lua_tools */ -"use strict"; /** * Escapes a string for inclusion into a lua string * - * @param {string} content - String to escape. - * @returns {string} Escaped string. + * @param content - String to escape. + * @returns Escaped string. */ -function escapeString(content) { +export function escapeString(content: string) { return content .replace(/\\/g, "\\\\") .replace(/"/g, '\\"') @@ -20,7 +19,3 @@ function escapeString(content) { .replace(/\r/g, "\\r") ; } - -module.exports = { - escapeString, -}; diff --git a/packages/lib/src/plugin.js b/packages/lib/src/plugin.ts similarity index 60% rename from packages/lib/src/plugin.js rename to packages/lib/src/plugin.ts index 74accdbb..7ccf4b20 100644 --- a/packages/lib/src/plugin.js +++ b/packages/lib/src/plugin.ts @@ -2,18 +2,61 @@ * Plugin interfaces and utilities. * @module lib/plugin */ -"use strict"; +import * as libHelpers from "./helpers"; +import type { Logger } from "./logging"; +import type { ConfigGroup } 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"; -const helpers = require("./helpers"); - -/* TODO fix when converting to ts +/** * Conceptual base for controller and instance plugins. - * @typedef { - * BaseControllerPlugin - * |BaseInstancePlugin - * |BaseControlPlugin - * } BasePlugin */ +export type BasePlugin = + | BaseControllerPlugin + | BaseInstancePlugin + | BaseControlPlugin + | BaseWebPlugin +; + +// TODO Add proper typing for plugins +export type PluginInfo = any; +type Controller = any; +type Instance = any; +type Host = any; +type InstanceInfo = any; +type HostConnection = any; +type ControlConnection = any; +type Event = any; +type React = any; + +/** + * Information about the event. + */ +interface PlayerEvent { + type: "join" | "leave" | "import"; + /** Name of the player that joined/left */ + name: string, + /** + * Only present for type "leave". Reason for player leaving the + * game, one of the possible reasons in defines.disconnect_reason + * or "server_quit" if the server exits while the player is online. + */ + reason?: string; +} +type InstanceStatus = + | "unassigned" + | "unknown" + | "stopped" + | "starting" + | "running" + | "stopping" + | "creating_save" + | "exporting_data" + | "deleted" +; /** * Base class for instance plugins @@ -27,40 +70,43 @@ const helpers = require("./helpers"); * Instances may be started and stopped many times, and many instances may * be running at the same time, each of which will have their own instance * of the InstancePlugin class. - * - * @static */ -class BaseInstancePlugin { - constructor(info, instance, host) { +export class BaseInstancePlugin { + /** + * Logger for this plugin + * + * Instance of winston Logger for sending log messages from this + * plugin. Supported methods and their corresponding log levels are + * `error`, `warn`, `audit`, `info` and `verbose`. + */ + logger: Logger; + + private _pendingRconMessages: { + resolve: (result: string) => void, + reject: (err: Error) => void, + message: string, + expectEmpty: boolean, + }[] = []; + private _sendingRconMessages = false; + + constructor( /** * The plugin's own info module */ - this.info = info; - + public info: PluginInfo, /** * Instance the plugin started for - * @type {module:host/src/Instance} */ - this.instance = instance; - + public instance: Instance, /** * Host running the instance * * With the exepction of accessing the host's config you should * avoid ineracting with the host object directly. - * - * @type {module:host/src/Host} */ - this.host = host; - - /** - * Logger for this plugin - * - * Instance of winston Logger for sending log messages from this - * plugin. Supported methods and their corresponding log levels are - * `error`, `warn`, `audit`, `info` and `verbose`. - */ - this.logger = instance.logger.child({ plugin: this.info.name }); + public host: Host, + ) { + this.logger = instance.logger.child({ plugin: this.info.name }) as unknown as Logger; this._pendingRconMessages = []; this._sendingRconMessages = false; @@ -77,12 +123,12 @@ class BaseInstancePlugin { * Invoked after the value of the config field given by `field` has * changed. * - * @param {module:lib.ConfigGroup} group - + * @param group - * The group who's field got changed. - * @param {string} field - Name of the field that changed. - * @param {*} prev - The previous value of the field. + * @param field - Name of the field that changed. + * @param prev - The previous value of the field. */ - async onInstanceConfigFieldChanged(group, field, prev) { } + async onInstanceConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { } /** * Called before collecting Prometheus metrics @@ -97,9 +143,9 @@ class BaseInstancePlugin { * the Gauges/Counters/etc in onMetric and let Clusterio deal will * collecting the values. * - * @returns {*} an async iterator of prometheus metric results or undefined. + * @returns an async iterator of prometheus metric results or undefined. */ - async onMetrics() { } + async onMetrics(): Promise> { } /** * Called after the Factorio server is started @@ -130,24 +176,10 @@ class BaseInstancePlugin { * stderr. The output is a parsed form of the line and contains the * following properties: * - * @param {Object} parsed - parsed server output. - * @param {string} parsed.source - - * Where the message came from, one of "stdout" and "stderr". - * @param {string} parsed.format - - * Timestamp format, one of "date", "seconds" and "none". - * @param {string=} parsed.time - - * Timestamp of the message. Not present if format is "none". - * @param {string} parsed.type - - * Type of message, one of "log", "action" and "generic". - * @param {string=} parsed.level - - * Log level for "log" type. i.e "Info" normally. - * @param {string=} parsed.file - File reported for "log" type. - * @param {string=} parsed.action - - * Kind of action for "action" type. i.e "CHAT" for chat. - * @param {string} parsed.message - Main content of the line. - * @param {string} line - raw line of server output. + * @param parsed - parsed server output. + * @param line - raw line of server output. */ - async onOutput(parsed, line) { } + async onOutput(parsed: ParsedFactorioOutput, line: string) { } /** * Called when an event on the controller connection happens @@ -181,9 +213,9 @@ class BaseInstancePlugin { * send any messages that goes to or via the controller after the * connection has been closed and before a new one is established. * - * @param {string} event - one of connect, drop, resume and close + * @param event - one of connect, drop, resume and close */ - onControllerConnectionEvent(event) { } + onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { } /** * Called when the controller is preparing to disconnect from the host @@ -195,25 +227,19 @@ class BaseInstancePlugin { * Plugins must stop sending messages to the controller, or are forwarded * via the controller after the prepare disconnect has been handled. * - * @param {module:controller/src/HostConnection} connection - + * @param connection - * The connection to the host preparing to disconnect. */ - async onPrepareControllerDisconnect(connection) { } + async onPrepareControllerDisconnect(connection: HostConnection) { } /** * Called when a player joins or leaves the game * * Invoked when a player either joins or leaves the instance. * - * @param {Object} event - Information about the event. - * @param {string} event.type - Either "join", "leave" or "import". - * @param {string} event.name - Name of the player that joined/left. - * @param {string=} event.reason - - * Only present for type "leave". Reason for player leaving the - * game, one of the possible reasons in defines.disconnect_reason - * or "server_quit" if the server exits while the player is online. + * @param event - Information about the event. */ - async onPlayerEvent(event) { } + async onPlayerEvent(event: PlayerEvent) { } /** * Send RCON message to instance @@ -227,13 +253,13 @@ class BaseInstancePlugin { * running some time after `onStart` and some time before `onStop` is * invoked. * - * @param {string} message - message to send to server over RCON. - * @param {boolean=} [expectEmpty=false] - + * @param message - message to send to server over RCON. + * @param expectEmpty - * if true throw if the response is not empty. Useful for detecting * errors that might have been sent in response. - * @returns {Promise} response from server. + * @returns response from server. */ - async sendRcon(message, expectEmpty=false) { + async sendRcon(message: string, expectEmpty=false): Promise { return await this.instance.sendRcon(message, expectEmpty, this.info.name); } @@ -252,14 +278,14 @@ class BaseInstancePlugin { * running some time after `onStart` and some time before `onStop` is * invoked. * - * @param {string} message - message to send to server over RCON. - * @param {boolean=} [expectEmpty=false] - + * @param message - message to send to server over RCON. + * @param [expectEmpty=false] - * if true throw if the response is not empty. Useful for detecting * errors that might have been sent in response. - * @returns {Promise} response from server. + * @returns response from server. */ - async sendOrderedRcon(message, expectEmpty=false) { - let promise = new Promise((resolve, reject) => { + async sendOrderedRcon(message: string, expectEmpty=false) { + let promise = new Promise((resolve, reject) => { this._pendingRconMessages.push({resolve, reject, message, expectEmpty}); }); if (!this._sendingRconMessages) { @@ -291,28 +317,24 @@ class BaseInstancePlugin { * To be discovered the class must be exported under the name `ControllerPlugin` * in the module specified by the `controllerEntrypoint` in the plugin's info.js * file. - * - * @static */ -class BaseControllerPlugin { - constructor(info, controller, metrics, logger) { - this.info = info; +export class BaseControllerPlugin { + /** + * Logger for this plugin + * + * Instance of winston Logger for sending log messages from this + * plugin. Supported methods and their corresponding log levels are + * `error`, `warn`, `audit`, `info` and `verbose`. + */ + logger: Logger; - /** - * Controller - * @type {module:controller/src/Controller} - */ - this.controller = controller; - this.metrics = metrics; - - /** - * Logger for this plugin - * - * Instance of winston Logger for sending log messages from this - * plugin. Supported methods and their corresponding log levels are - * `error`, `warn`, `audit`, `info` and `verbose`. - */ - this.logger = logger.child({ plugin: this.info.name }); + constructor( + public info: PluginInfo, + public controller: Controller, + public metrics: any[], + logger: Logger + ) { + this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger; } /** @@ -354,11 +376,11 @@ class BaseControllerPlugin { * the case of network outages if reconnect fails to re-establish the * session between the controller and the host. * - * @param {module:controller/src/InstanceInfo} instance - + * @param instance - * The instance that changed. - * @param {?string} prev - the previous status of the instance. + * @param prev - the previous status of the instance. */ - async onInstanceStatusChanged(instance, prev) { } + async onInstanceStatusChanged(instance: InstanceInfo, prev?: InstanceStatus) { } /** * Called when the value of a controller config field changed. @@ -366,12 +388,12 @@ class BaseControllerPlugin { * Invoked after the value of the config field given by `field` has * changed on the controller. * - * @param {module:lib.ConfigGroup} group - + * @param group - * The group who's field got changed. - * @param {string} field - Name of the field that changed. - * @param {*} prev - The previous value of the field. + * @param field - Name of the field that changed. + * @param prev - The previous value of the field. */ - async onControllerConfigFieldChanged(group, field, prev) { } + async onControllerConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { } /** * Called when the value of an instance config field changed. @@ -379,14 +401,14 @@ class BaseControllerPlugin { * Invoked after the value of the config field given by `field` has * changed on an instance. * - * @param {module:controller/src/InstanceInfo} instance - + * @param instance - * The instance the config changed on. - * @param {module:lib.ConfigGroup} group - + * @param group - * The group who's field got changed. - * @param {string} field - Name of the field that changed. - * @param {*} prev - The previous value of the field. + * @param field - Name of the field that changed. + * @param prev - The previous value of the field. */ - async onInstanceConfigFieldChanged(instance, group, field, prev) { } + async onInstanceConfigFieldChanged(instance: InstanceInfo, group: ConfigGroup, field: string, prev: unknown) { } /** * Called before collecting Prometheus metrics @@ -401,9 +423,9 @@ class BaseControllerPlugin { * the Gauges/Counters/etc in onMetric and let Clusterio deal will * collecting the values. * - * @returns {*} an async iterator of prometheus metric results or undefined. + * @returns an async iterator of prometheus metric results or undefined. */ - async onMetrics() { } + async onMetrics(): Promise> { } /** * Called when the controller is shutting down @@ -439,11 +461,11 @@ class BaseControllerPlugin { * * Invoked when the connection to the host has been closed. * - * @param {module:controller/src/HostConnection} connection - + * @param connection - * The connection the event occured on. - * @param {string} event - one of connect, drop, resume and close + * @param event - one of connect, drop, resume and close */ - onHostConnectionEvent(connection, event) { } + onHostConnectionEvent(connection: HostConnection, event: "connect" | "drop" | "resume" | "close") { } /** * Called when an avent on a control connection happens @@ -475,11 +497,11 @@ class BaseControllerPlugin { * * Invoked when the connection to the control has been closed. * - * @param {module:controller/src/ControlConnection} connection - + * @param connection - * The connection the event occured on. - * @param {string} event - one of connect, drop, resume, and close. + * @param event - one of connect, drop, resume, and close. */ - onControlConnectionEvent(connection, event) { } + onControlConnectionEvent(connection: ControlConnection, event: "connect" | "drop" | "resume" | "close") { } /** * Called when a host is preparing to disconnect from the controller @@ -490,10 +512,10 @@ class BaseControllerPlugin { * Plugins must stop sending messages to the host in question after the * prepare disconnect has been handled. * - * @param {module:controller/src/HostConnection} connection - + * @param connection - * The connection to the host preparing to disconnect. */ - async onPrepareHostDisconnect(connection) { } + async onPrepareHostDisconnect(connection: HostConnection) { } /** * Called when a mod pack is updated @@ -504,9 +526,9 @@ class BaseControllerPlugin { * If the mod pack has been deleted its `.isDeleted` property will be * true. * - * @param {module:lib.ModPack} modPack - Mod pack that updated. + * @param modPack - Mod pack that updated. */ - async onModPackUpdated(modPack) { } + async onModPackUpdated(modPack: ModPack) { } /** * Called when a mod stored on the controller is updated @@ -516,9 +538,9 @@ class BaseControllerPlugin { * * If a mod has been deleted its `.isDeleted` property will be true. * - * @param {module:lib.ModInfo} mod - Mod that updated. + * @param mod - Mod that updated. */ - async onModUpdated(mod) { } + async onModUpdated(mod: ModInfo) { } /** * Called when a player joins or leaves an instance @@ -526,17 +548,11 @@ class BaseControllerPlugin { * Invoked when a player either joins or leaves an instance in the * cluster. * - * @param {module:controller/src/InstanceInfo} instance - + * @param instance - * The instance it occured on. - * @param {Object} event - Information about the event. - * @param {string} event.type - Either "join", "leave" or "import". - * @param {string} event.name - Name of the player that joined/left. - * @param {string=} event.reason - - * Only present for type "leave". Reason for player leaving the - * game, one of the possible reasons in defines.disconnect_reason - * or "server_quit" if the server exits while the player is online. + * @param event - Information about the event. */ - async onPlayerEvent(instance, event) { } + async onPlayerEvent(instance: InstanceInfo, event: PlayerEvent) { } /** * Broadcast event to all connected hosts @@ -545,20 +561,15 @@ class BaseControllerPlugin { * This does not include hosts that are in the process of closing the * connection, which typically happens when they are shutting down. * - * @param {*} event - Event to send - * @param {Object} data - Data ta pass with the event. + * @param event - Event to send */ - broadcastEventToHosts(event, data={}) { - if (event.type !== "event") { - this.logger.warn(`Ignoring broadcast of ${event.type}`); - return; - } + broadcastEventToHosts(event: Event) { for (let hostConnection of this.controller.wsServer.hostConnections.values()) { if ( !hostConnection.connector.closing - && (!event.plugin || hostConnection.plugins.has(event.plugin)) + && (!event.constructor.plugin || hostConnection.plugins.has(event.constructor.plugin)) ) { - event.send(hostConnection, data); + hostConnection.send(event); } } } @@ -571,24 +582,25 @@ class BaseControllerPlugin { * clusterioctl in order to extend its functionallity. To be discovered the * class must be exported under the name `ControlPlugin` in the module * specified by the `controlEntrypoint` in the plugin's info.js file. - * - * @static */ -class BaseControlPlugin { - constructor(info, logger) { +export class BaseControlPlugin { + /** + * Logger for this plugin + * + * Instance of winston Logger for sending log messages from this + * plugin. Supported methods and their corresponding log levels are + * `error`, `warn`, `audit`, `info` and `verbose`. + */ + logger: Logger; + + constructor( /** * The plugin's own info module */ - this.info = info; - - /** - * Logger for this plugin - * - * Instance of winston Logger for sending log messages from this - * plugin. Supported methods and their corresponding log levels are - * `error`, `warn`, `audit`, `info` and `verbose`. - */ - this.logger = logger.child({ plugin: this.info.name }); + public info: PluginInfo, + logger: Logger, + ) { + this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger; } /** @@ -601,136 +613,153 @@ class BaseControlPlugin { * * Invoked by clusterioctl to let plugins add commands. `rootCommand` is * the top level command node which the plugin should add its own {@link - * module:lib.CommandTree} to. + * CommandTree} to. * - * @param {module:lib.CommandTree} rootCommand - + * @param rootCommand - * Root of the clusterioctl command tree. */ - async addCommands(rootCommand) { } + async addCommands(rootCommand: CommandTree) { } } + /** * Plugin supplied login form - * @typedef {Object} LoginForm - * @property {string} name - - * Internal name of the login form, this should start with the - * plugin name followed by a dot. - * @property {string} title - - * Name displayed above this form in the login window. - * @property {React.Component} Component - - * React component that's rendered for this login form. This is - * supplied the setToken function via its props which should be - * called when an authentication token is aquired via this - * form. */ +export interface PluginLoginForm { + /** + * Internal name of the login form, this should start with the + * plugin name followed by a dot. + */ + name: string; + + /** Name displayed above this form in the login window. */ + title: string; + + /** + * React component that's rendered for this login form. This is + * supplied the setToken function via its props which should be called + * when an authentication token is aquired via this form. + */ + Component: React["Component"]; +}; + /** * Plugin supplied pages - * @typedef {Object} Page - * @property {string} path - URL path to this page. - * @property {string=} sidebarPath - - * If present and this path matches one of the pages in the sidebar it - * will cause that sidebar entry to be highlighted as active. - * @property {string=} sidebarGroup - - * If present group this entry under a group of the given name in the - * sidebar. - * @property {string=} sidebarName - - * If present creates an entry in the sidebar for this page with the - * given text. - * @property {ReactNode} content - - * A react node which is rendered when this page is navigated to. - * Should render a PageLayout. */ +export interface PluginPage { + /** URL path to this page. */ + path: string; + /** + * If present and this path matches one of the pages in the sidebar it + * will cause that sidebar entry to be highlighted as active. + */ + sidebarPath?: string; + /** + * If present group this entry under a group of the given name in the + * sidebar. + */ + sidebarGroup?: string; + /** + * If present creates an entry in the sidebar for this page with the + * given text. + */ + sidebarName?: string; + /** + * A react node which is rendered when this page is navigated to. + * Should render a PageLayout. + */ + content?: React["ReactNode"]; +}; /** * Base class for web interface plugins - * - * @static */ -class BaseWebPlugin { - constructor(container, packageData, info, logger) { +export class BaseWebPlugin { + /** + * Contents of the plugin's package.json file + */ + package: any; + /** + * Control link to the controller, not available until the + * connect event in onControllerConnectionEvent is signaled. + */ + control?: Link; + /** + * Logger for this plugin + * + * Instance of winston Logger for sending log messages from this + * plugin. Supported methods and their corresponding log levels are + * `error`, `warn`, `audit`, `info` and `verbose`. + */ + logger: Logger; + /** + * List of login forms provided by this plugin + */ + loginForms: PluginLoginForm[] = []; + /** + * List of pages provided by this plugin + */ + pages: PluginPage[] = []; + /** + * Extra react component to add to core components + * + * Interface to augment core components of the web UI. Setting a + * component as one of the supported properties of this object will + * cause the web UI to render it when displaying that component, + * usually at the end. Each component will receive a `plugin` param + * which is the instance of the web plugin that contained the + * component extra. + */ + componentExtra: { + /** Placed at the end of the controller page. */ + ControllerPage?: React["ComponentType"], + /** Placed at the end of the hosts list page. */ + HostsPage?: React["ComponentType"], + /** + * Placed at the end of each host page. Takes a `host` param which + * is the host the page is displayed for. + */ + HostViewPage?: React["ComponentType"], + /** Placed at the end of the instance list page. */ + InstancesPage?: React["ComponentType"], + /** + * Placed at the end of each instance page. Takes an `instance` + * param which is the instance the page is displayed for. + */ + InstanceViewPage?: React["ComponentType"], + /** Placed at the end of the users list page. */ + UsersPage?: React["ComponentType"], + /** + * Placed at the end of each user page. Takes a `user` param which + * is the user object the page is displayed for. + */ + UserViewPage?: React["ComponentType"], + /** Placed at the end of the roles list page. */ + RolesPage?: React["ComponentType"], + /** + * Placed at the end of each role page. Takes a `role` param which + * is the role object the page is displayed for. + */ + RoleViewPage?: React["ComponentType"], + } = {}; + + + constructor( /** * Webpack container for this plugin */ - this.container = container; - - /** - * Contents of the plugin's package.json file - * @type {Object} - */ - this.package = packageData; - + public container: any, + packageData: any, /** * The plugin's own info module */ - this.info = info; - - /** - * Control link to the controller, not available until the - * connect event in onControllerConnectionEvent is signaled. - * @type {?module:lib.Link} - */ - this.control = null; - - /** - * Logger for this plugin - * - * Instance of winston Logger for sending log messages from this - * plugin. Supported methods and their corresponding log levels are - * `error`, `warn`, `audit`, `info` and `verbose`. - */ - this.logger = logger.child({ plugin: this.info.name }); - - /** - * List of login forms provided by this plugin - * - * @type {Array} - */ - this.loginForms = []; - - /** - * List of pages provided by this plugin - * - * @type {Array} - */ - this.pages = []; - - /** - * Extra react component to add to core components - * - * Interface to augment core components of the web UI. Setting a - * component as one of the supported properties of this object will - * cause the web UI to render it when displaying that component, - * usually at the end. Each component will receive a `plugin` param - * which is the instance of the web plugin that contained the - * component extra. - * - * @type {object} - * @property {React.ComponentType} ControllerPage - - * Placed at the end of the controller page. - * @property {React.ComponentType} HostsPage - - * Placed at the end of the hosts list page. - * @property {React.ComponentType} HostViewPage - - * Placed at the end of each host page. Takes a `host` param - * which is the host the page is displayed for. - * @property {React.ComponentType} InstancesPage - - * Placed at the end of the instance list page. - * @property {React.ComponentType} InstanceViewPage - - * Placed at the end of each instance page. Takes an `instance` - * param which is the instance the page is displayed for. - * @property {React.ComponentType} UsersPage - - * Placed at the end of the users list page. - * @property {React.ComponentType} UserViewPage - - * Placed at the end of each user page. Takes a `user` param - * which is the user object the page is displayed for. - * @property {React.ComponentType} RolesPage - - * Placed at the end of the roles list page. - * @property {React.ComponentType} RoleViewPage - - * Placed at the end of each role page. Takes a `role` param - * which is the role object the page is displayed for. - */ - this.componentExtra = {}; + public info: PluginInfo, + logger: Logger, + ) { + this.package = packageData; + this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger; } /** @@ -770,27 +799,27 @@ class BaseWebPlugin { * send any messages that goes to or via the controller after the * connection has been closed and before a new one is established. * - * @param {string} event - one of connect, drop, resume and close + * @param event - one of connect, drop, resume and close */ - onControllerConnectionEvent(event) { } + onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { } } /** * Invokes the given hook on all plugins * - * @param {Map} plugins - + * @param plugins - * Mapping of plugin names to plugins to invoke the hook on. - * @param {string} hook - Name of hook to invoke. - * @param {...*} args - Arguments to pass on to the hook. - * @returns {Promise} Non-empty return values from the hooks. + * @param hook - Name of hook to invoke. + * @param args - Arguments to pass on to the hook. + * @returns Non-undefined return values from the hooks. */ -async function invokeHook(plugins, hook, ...args) { - let results = []; +export async function invokeHook(plugins: Map, hook: string, ...args: any[]) { + let results: unknown[] = []; for (let [name, plugin] of plugins) { try { // Use an object to detect if the hook failed on timeout or tried to return a value looking like an error const timeout = {}; - let result = await helpers.timeout( + let result = await libHelpers.timeout( plugin[hook](...args), 15000, timeout @@ -806,13 +835,3 @@ async function invokeHook(plugins, hook, ...args) { } return results; } - - -module.exports = { - BaseInstancePlugin, - BaseControllerPlugin, - BaseControlPlugin, - BaseWebPlugin, - - invokeHook, -}; diff --git a/packages/lib/src/plugin_loader.js b/packages/lib/src/plugin_loader.ts similarity index 66% rename from packages/lib/src/plugin_loader.js rename to packages/lib/src/plugin_loader.ts index e97d7541..6771dd7b 100644 --- a/packages/lib/src/plugin_loader.js +++ b/packages/lib/src/plugin_loader.ts @@ -3,12 +3,10 @@ * Helpers for loading plugins in Node.js * @module lib/plugin_loader */ -"use strict"; - -const libErrors = require("./errors"); -const libPlugin = require("./plugin"); -const { logger } = require("./logging"); -const path = require("path"); +import path from "path"; +import * as libErrors from "./errors"; +import * as libPlugin from "./plugin"; +import { logger } from "./logging"; /** @@ -18,16 +16,15 @@ const path = require("path"); * loaded the info modules will not be reloaded should this function be * called again. * - * @param {Map} pluginList - + * @param pluginList - * Mapping of plugin name to require path for the plugins to load. - * @returns {Promise>} Array of plugin info modules. - * @static + * @returns Array of plugin info modules. */ -async function loadPluginInfos(pluginList) { - let plugins = []; +export async function loadPluginInfos(pluginList: Map) { + let plugins: libPlugin.PluginInfo[] = []; for (let [pluginName, pluginPath] of pluginList) { - let pluginInfo; - let pluginPackage; + let pluginInfo: libPlugin.PluginInfo; + let pluginPackage: { version: string }; try { pluginInfo = require(path.posix.join(pluginPath, "info")); @@ -56,7 +53,12 @@ async function loadPluginInfos(pluginList) { return plugins; } -function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) { +function loadPluginClass( + entrypointName: string, + className: string, + pluginClass: any, + pluginInfo: libPlugin.PluginInfo +) { let resolvedPath = path.posix.join(pluginInfo.requirePath, pluginInfo[entrypointName]); let entrypoint = require(resolvedPath); if (!entrypoint[className]) { @@ -75,45 +77,41 @@ function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) { /** * Load controller plugin class of a plugin * - * @param {Object} pluginInfo - + * @param pluginInfo - * Plugin info object returned from {@link - * module:lib.loadPluginInfos} to load class from. - * @returns {Promise} plugin class - * @static + * loadPluginInfos} to load class from. + * @returns plugin class */ -async function loadControllerPluginClass(pluginInfo) { +export async function loadControllerPluginClass( + pluginInfo: libPlugin.PluginInfo +): Promise { return loadPluginClass("controllerEntrypoint", "ControllerPlugin", libPlugin.BaseControllerPlugin, pluginInfo); } /** * Load instance plugin class of a plugin - * - * @param {Object} pluginInfo - + + * @param pluginInfo - * Plugin info object returned from {@link - * module:lib.loadPluginInfos} to load class from. - * @returns {Promise} plugin class - * @static + * loadPluginInfos} to load class from. + * @returns plugin class */ -async function loadInstancePluginClass(pluginInfo) { +export async function loadInstancePluginClass( + pluginInfo: libPlugin.PluginInfo +): Promise { return loadPluginClass("instanceEntrypoint", "InstancePlugin", libPlugin.BaseInstancePlugin, pluginInfo); } /** * Load control plugin class of a plugin * - * @param {Object} pluginInfo - + * @param pluginInfo - * Plugin info object returned from {@link - * module:lib.loadPluginInfos} to load class from. - * @returns {Promise} plugin class - * @static + * loadPluginInfos} to load class from. + * @returns plugin class */ -async function loadControlPluginClass(pluginInfo) { +export async function loadControlPluginClass( + pluginInfo: libPlugin.PluginInfo +): Promise { return loadPluginClass("controlEntrypoint", "ControlPlugin", libPlugin.BaseControlPlugin, pluginInfo); } - -module.exports = { - loadPluginInfos, - loadControllerPluginClass, - loadInstancePluginClass, - loadControlPluginClass, -}; diff --git a/packages/lib/src/prometheus.js b/packages/lib/src/prometheus.ts similarity index 76% rename from packages/lib/src/prometheus.js rename to packages/lib/src/prometheus.ts index 7a69f391..93540e8c 100644 --- a/packages/lib/src/prometheus.js +++ b/packages/lib/src/prometheus.ts @@ -7,9 +7,8 @@ * metric collections in prom-client. * * The ordinary use case of instrumenting a code base should be covered by - * {@link module:lib.Counter}, {@link - * module:lib.Gauge}, {@link module:lib.Histogram} and - * {@link module:lib.exposition}. See documentation for each of + * {@link Counter}, {@link Gauge}, {@link Histogram} and + * {@link exposition}. See documentation for each of * the listed interfaces for more information. * * @example @@ -39,21 +38,13 @@ * app.listen(9100); * @module lib/prometheus */ -"use strict"; /** - * Result from collecting a {@link module:lib.Collector} - * @typedef {Object} CollectorResult - * @property {module:lib.Metric} metric - - * Metric collected. - * @property {Map>} samples - - * Mapping of metric suffix to mapping of label keys to values - * collected. For normal metrics the first level contains a single - * entry under the empty string as key. If the metric does not have - * labels the second level also contains a single entry under the empty - * string as a key. + * Result from collecting a {@link Collector} * - * @example Simple Metric + * @example + * Simple Metric + * ```ts * let result = { * metric: new Metric("count", "simple_total_count", "A simple counter"), * samples: new Map([ @@ -62,8 +53,11 @@ * ])], * ]), * } + * ``` * - * @example Labeled Metric + * @example + * Labeled Metric + * ```ts * let result = { * metric: new Metric( * "count", "labeled_total_count", "A labeled counter", ["a", "b"] @@ -76,8 +70,11 @@ * ])], * ]), * } + * ``` * - * @example Histogram Metric + * @example + * Histogram Metric + * ```ts * let result = { * metric: new Metric( * "histogram", "histogram_size", "A histogram of sizes" @@ -97,25 +94,42 @@ * ]), * } */ +export interface CollectorResult { + /** Metric collected. */ + metric: Metric; + /** + * Mapping of metric suffix to mapping of label keys to values + * collected. For normal metrics the first level contains a single + * entry under the empty string as key. If the metric does not have + * labels the second level also contains a single entry under the empty + * string as a key. + */ + samples: Map>; +} + +export type MetricType = "counter" | "gauge" | "histogram" | "summary" | "untyped"; /** * Represents a collectable metric * * Used in implementing collectors in order to validate the name, help text * and labels attached to the collector. - * @static */ -class Metric { - /** - * @param {string} type - - * Metric type, should be one of `counter`, `gauge`, `histogram`, - * `summary` or `untyped`, - * @param {string} name - Name of the metric. - * @param {string} help - Help text for the metric. - * @param {Array} labels - - * @param {Array} _reserved_labels - - */ - constructor(type, name, help, labels = [], _reserved_labels = []) { +export class Metric { + constructor( + /** + * Metric type, should be one of `counter`, `gauge`, `histogram`, + * `summary` or `untyped`. + */ + public type: MetricType, + /** Name of the metric */ + public name: string, + /** Help text for the metric */ + public help: string, + /** Labels for this metric. */ + public labels: string[] = [], + _reserved_labels: string[] = [] + ) { if (typeof name !== "string") { throw new Error("Expected name to be a string"); } @@ -135,40 +149,13 @@ class Metric { throw new Error(`Reserved label '${label}'`); } } - - /** - * Name of the metric - * @type {string} - */ - this.name = name; - - /** - * Help text for the metric - * @type {string} - */ - this.help = help; - - /** - * Metric type, should be one of `counter`, `gauge`, `histogram`, - * `summary` or `untyped`. - * @type {string} - */ - this.type = type; - - /** - * Labels for this metric. - * @type {Array} - */ - this.labels = labels; } } /** * The default registry which collectors are regististered to. - * @type {module:lib.CollectorRegistry} - * @static */ -let defaultRegistry; +export let defaultRegistry: CollectorRegistry; /** * Base class for all collectors @@ -176,16 +163,14 @@ let defaultRegistry; * This servers mostly as a conceptual base for all collectors, the only * feature implemented here is registring to the default registry on * construction. If you want to implement a custom collector you most - * likely want to base it on {@link module:lib.LabeledCollector} + * likely want to base it on {@link LabeledCollector} * instead. - * - * @static */ -class Collector { +export class Collector { /** * Create collector * - * @param {boolean=} register - + * @param register - * Whether to register this collector to the default registry. */ constructor(register = true) { @@ -197,23 +182,21 @@ class Collector { /** * Retrieve metric data from this collctor. * - * Called by {@link module:lib.CollectorRegistry} to gather + * Called by {@link CollectorRegistry} to gather * the metric data this Collector exports. - * - * @returns {*} Async iterator of {@link CollectorResult}. */ - async* collect() { } + async* collect(): AsyncIterable { } } const labelEscapesChars = /[\\\n\"]/; /** * Escapes a label value in accordance with the text exposition format * - * @param {string} value - Value to escape. - * @returns {string} escaped value with \, ", and newline escaped. + * @param value - Value to escape. + * @returns escaped value with \, ", and newline escaped. * @private */ -function escapeLabelValue(value) { +function escapeLabelValue(value: string) { if (!labelEscapesChars.test(value)) { return value; } @@ -224,32 +207,34 @@ function escapeLabelValue(value) { ; } +export type LabelValues = string[] | [Record]; + /** * Convert label expression to unique string * - * @param {Array<(string|Object)>} labels - + * @param labels - * label values to compute key for. - * @param {Array} metricLabels - labels defined for the metric. - * @returns {string} computed key + * @param metricLabels - labels defined for the metric. + * @returns computed key * @private */ -function labelsToKey(labels, metricLabels) { +function labelsToKey(labels: LabelValues, metricLabels: string[]) { let items = []; if (labels.length === 1 && typeof labels[0] === "object") { - labels = labels[0]; + let labelObj = labels[0]; for (let name of metricLabels) { - if (!Object.hasOwnProperty.call(labels, name)) { + if (!Object.hasOwnProperty.call(labelObj, name)) { throw new Error(`Missing label '${name}'`); } - if (typeof labels[name] !== "string") { + if (typeof labelObj[name] !== "string") { throw new Error( `Expected value for label '${name}' to be a string` ); } - items.push(`${name}="${escapeLabelValue(labels[name])}"`); + items.push(`${name}="${escapeLabelValue(labelObj[name])}"`); } - for (let name of Object.keys(labels)) { + for (let name of Object.keys(labelObj)) { if (!metricLabels.includes(name)) { throw new Error(`Extra label '${name}'`); } @@ -265,12 +250,13 @@ function labelsToKey(labels, metricLabels) { } for (let i=0; i < metricLabels.length; ++i) { - if (typeof labels[i] !== "string") { + let label = labels[i]; + if (typeof label !== "string") { throw new Error( `Expected value for label '${metricLabels[i]}' to be a string` ); } - items.push(`${metricLabels[i]}="${escapeLabelValue(labels[i])}"`); + items.push(`${metricLabels[i]}="${escapeLabelValue(label)}"`); } } @@ -281,8 +267,8 @@ function labelsToKey(labels, metricLabels) { return items.join(","); } -function keyToLabels(key) { - let labels = new Map(); +function keyToLabels(key: string) { + let labels = new Map(); if (key !== "") { for (let pair of key.split(",")) { @@ -299,7 +285,7 @@ function keyToLabels(key) { return labels; } -function removeMatchingLabels(mapping, labels) { +function removeMatchingLabels(mapping: Map, labels: Record) { for (let key of mapping.keys()) { let candidate = keyToLabels(key); let hasLabels = Object.entries(labels).every(([name, value]) => ( @@ -318,55 +304,57 @@ function removeMatchingLabels(mapping, labels) { * set is a child to the collector that can be retrieved with the .labels() * method. * - * @extends module:lib.Collector - * @static + * @extends Collector */ -class LabeledCollector extends Collector { +export class LabeledCollector extends Collector { + callback: (collector: LabeledCollector) => void | Promise; + metric: Metric; + + private _children: Map; + private _childClass: { new(collector: LabeledCollector, key: string): Child }; + /** * Create optionally labeled collector * - * @param {string} type - Type for metric. - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object} options - options for collector. - * @param {Array=} options.labels - + * @param type - Type for metric. + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. + * @param options.labels - * Labels for this metric, defaults to no labels. - * @param {Array=} options._reservedLabels - + * @param options._reservedLabels - * Labels which may not be used. Is passed to Metric constructor. - * @param {boolean=} options.register - + * @param options.register - * If true registers this collector with the default registry. * Defaults to true. - * @param {function()=} options.callback - + * @param options.callback - * Possibly async function that is called before the metric is * collected. The collector being collected is passed as the * argument. - * @param { - * function(module:lib.LabeledCollector,string) - * } childClass - + * @param childClass - * Constructor taking instance of collector and label key as * arguments and returns a child instance. */ - constructor(type, name, help, options, childClass) { - let labels = []; - let reservedLabels = []; - let register = true; - let callback = null; - for (let [key, value] of Object.entries(options)) { - if (key === "labels") { - labels = value; - } else if (key === "_reservedLabels") { - reservedLabels = value; - } else if (key === "register") { - register = value; - } else if (key === "callback") { - callback = value; - } else { - throw new Error(`Unrecognized option '${key}'`); - } + constructor( + type: MetricType, + name: string, + help: string, + options: { + labels?: string[], + _reservedLabels?: string[], + register?: boolean, + callback?: (collector: LabeledCollector) => void | Promise, + }, + childClass: { new(collector: LabeledCollector, key: string): Child } + ) { + const { labels = [], _reservedLabels = [], register = true, callback, ...rest } = options; + + for (let key of Object.keys(rest)) { + throw new Error(`Unrecognized option '${key}'`); } // Make sure we don't register to the default registry if metric throws. - let metric = new Metric(type, name, help, labels, reservedLabels); + let metric = new Metric(type, name, help, labels, _reservedLabels); super(register); @@ -387,13 +375,13 @@ class LabeledCollector extends Collector { * exported from the collector until it's removed explicitly with * the .remove() or .removeAll() methods. * - * @param {...string|Object} labels - + * @param labels - * A string value passed for each label defined on the metric in the * same order as the labels option given to the collector, or an * object mapping label name to label value. - * @returns {*} Child collector for the given labels. + * @returns Child collector for the given labels. */ - labels(...labels) { + labels(...labels: LabelValues): Child { let key = labelsToKey(labels, this.metric.labels); let child = this._children.get(key); if (child === undefined) { @@ -409,13 +397,13 @@ class LabeledCollector extends Collector { * Remove the child collector and the value it stores from the collector * itself. This will remove the entry exported for the given labels. * - * @param {...string|Object} labels - + * @param labels - * A string value passed for each label defined on the metric in the * same order as the labels option given to the collector, or an * object mapping label name to label value. - * @returns {string} key for labels to remove (for use in subclasses). + * @returns key for labels to remove (for use in subclasses). */ - remove(...labels) { + remove(...labels: LabelValues) { let key = labelsToKey(labels, this.metric.labels); if (key === "") { throw new Error("labels cannot be empty"); @@ -436,7 +424,7 @@ class LabeledCollector extends Collector { * Object mapping with label name to label values that should be * matches. */ - removeAll(labels) { + removeAll(labels: Record) { if (!Object.keys(labels).length) { throw new Error("labels cannot be empty"); } @@ -461,39 +449,46 @@ class LabeledCollector extends Collector { /** * Base class for implementing single value per label collectors * - * @extends module:lib.LabeledCollector - * @static + * @extends LabeledCollector */ -class ValueCollector extends LabeledCollector { +export class ValueCollector extends LabeledCollector { + _values: Map; + protected _defaultChild: Child | null; + /** * Create optionally labeled value collector * - * @param {string} type - Type for metric. - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object} options - options for collector. - * @param {Array=} options.labels - + * @param type - Type for metric. + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. + * @param options.labels - * Labels for this metric, defaults to no labels. - * @param {Array=} options._reservedLabels - + * @param options._reservedLabels - * Labels which may not be used. Is passed to Metric constructor. - * @param {boolean=} options.register - + * @param options.register - * If true registers this collector with the default registry. * Defaults to true. - * @param {function()=} options.callback - + * @param options.callback - * Possibly async function that is called before the collector value * is collected. The collector being collected is passed as the * argument. - * @param { - * function( - * new:module:lib.ValueCollectorChild, - * module:lib.LabeledCollector, - * string - * ) - * } childClass - + * @param childClass - * Constructor taking instance of collector and label key as * arguments and returns a child instance. */ - constructor(type, name, help, options, childClass) { + constructor( + type: MetricType, + name: string, + help: string, + options: { + labels?: string[], + _reservedLabels?: string[], + register?: boolean, + callback?: (collector: ValueCollector) => void | Promise, + }, + childClass: { new(collector: ValueCollector, key: string): Child } + ) { super(type, name, help, options, childClass); this._values = new Map(); @@ -509,18 +504,18 @@ class ValueCollector extends LabeledCollector { * Get the current value for this collector. * * Note: Only works if this is an unlabeled collector. - * @returns {number} value stored. + * @returns value stored. */ get() { return this._defaultChild.get(); } - remove(...labels) { + remove(...labels: LabelValues) { let key = super.remove(...labels); this._values.delete(key); } - removeAll(labels) { + removeAll(labels: Record) { super.removeAll(labels); removeMatchingLabels(this._values, labels); } @@ -535,7 +530,10 @@ class ValueCollector extends LabeledCollector { * Child collector representing the value of a single label set */ class ValueCollectorChild { - constructor(collector, key) { + protected _values: Map; + protected _key: string; + + constructor(collector: ValueCollector, key: string) { this._values = collector._values; this._key = key; @@ -545,8 +543,7 @@ class ValueCollectorChild { /** * Returns the current value of label set * - * Note: Only works if this is an unlabeled collector. - * @returns {number} value stored. + * @returns value stored. */ get() { return this._values.get(this._key); @@ -555,12 +552,12 @@ class ValueCollectorChild { /** * Child counter holding the value for a single label set. - * @extends module:lib~ValueCollectorChild + * @extends ValueCollectorChild */ class CounterChild extends ValueCollectorChild { /** * Increment counter for label set - * @param {number} value - Positive number to increment by. + * @param value - Positive number to increment by. */ inc(value = 1) { // Note: Inverted to also catch NaN @@ -583,7 +580,7 @@ class CounterChild extends ValueCollectorChild { * Counters should be created at module load time and referenced in the * functions that increment them, for example: * - * ```js + * ```ts * const totalRequests = new Counter( * "app_request_count_total", * "Total requests handled.", @@ -596,10 +593,9 @@ class CounterChild extends ValueCollectorChild { * ``` * * The `totalRequests` counter will register with the default registry and - * provided exposition is set up (see {@link - * module:lib.exposition}) the counter will be exported to - * Prometheus starting out with a value of 0. And that is all there is to - * it. + * provided exposition is set up (see {@link exposition}) the counter will + * be exported to Prometheus starting out with a value of 0. And that is + * all there is to it. * * It is sometimes useful however to divide a metric up into diffrent * sections, for example to have a different count for each endpoint handled @@ -607,7 +603,7 @@ class CounterChild extends ValueCollectorChild { * an error. For this Prometheus provides labels, and to use them pass the * labels option as the third argument to Counter: * - * ```js + * ```ts * const totalRequests = new Counter( * "app_request_count_total", * "Total requests handled.", @@ -639,7 +635,7 @@ class CounterChild extends ValueCollectorChild { * that will be used. This is done by calling `.labels()` for each * combination, for example: * - * ```js + * ```ts * for (let endpoint of allEndpoints) { * for (let status of ["ok", "err"]) { * totalRequests.labels(endpoint, status); @@ -652,16 +648,15 @@ class CounterChild extends ValueCollectorChild { * labels you actually need as resource usage for a metric increases * exponentially with the number of labels used. * - * @extends module:lib.ValueCollector - * @static + * @extends ValueCollector */ -class Counter extends ValueCollector { +export class Counter extends ValueCollector { /** * Create optionally labeled counter * - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object=} options - options for collector. + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. * @param {Array=} options.labels - * Labels for this metric, defaults to no labels. * @param {boolean=} options.register - @@ -672,7 +667,15 @@ class Counter extends ValueCollector { * collected. The collector being collected is passed as the * argument. */ - constructor(name, help, options = {}) { + constructor( + name: string, + help: string, + options: { + labels?: string[], + register?: boolean, + callback?: (collector: Counter) => void | Promise, + } = {}, + ) { super("counter", name, help, options, CounterChild); } @@ -680,7 +683,7 @@ class Counter extends ValueCollector { * Increment counter value * * Note: Only works if this is an unlabeled collector. - * @param {number} value - Positive number to increment by. + * @param value - Positive number to increment by. */ inc(value = 1) { this._defaultChild.inc(value); @@ -689,13 +692,13 @@ class Counter extends ValueCollector { /** * Child gauge holding the value for a single label set. - * @extends module:lib~ValueCollectorChild + * @extends ValueCollectorChild */ class GaugeChild extends ValueCollectorChild { /** * Increment gague for label set * - * @param {number} value - number to increment by. + * @param value - number to increment by. */ inc(value = 1) { this._values.set(this._key, this._values.get(this._key) + value); @@ -704,7 +707,7 @@ class GaugeChild extends ValueCollectorChild { /** * Decrement gague for label set * - * @param {number} value - number to decrease by. + * @param value - number to decrease by. */ dec(value = 1) { this._values.set(this._key, this._values.get(this._key) - value); @@ -713,9 +716,9 @@ class GaugeChild extends ValueCollectorChild { /** * Set gague for label set * - * @param {number} value - number to set gauge to. + * @param value - number to set gauge to. */ - set(value) { + set(value: number) { this._values.set(this._key, value); } @@ -729,7 +732,7 @@ class GaugeChild extends ValueCollectorChild { /** * Start a timer for setting a duration for label set * - * @returns {function()} + * @returns * function that when called will set the guage to the duration in * seconds from when the timer was started */ @@ -753,7 +756,7 @@ class GaugeChild extends ValueCollectorChild { * Gauges should be created at module load time and referenced in the * functions that modify them, for example: * - * ```js + * ```ts * const activeRequests = new Gauge( * "app_active_request_count", * "Number of requests in-flight.", @@ -770,16 +773,15 @@ class GaugeChild extends ValueCollectorChild { * ``` * * The `activeRequests` gauge will register with the default registry and - * provided exposition is set up (see {@link - * module:lib.exposition}) the gauge will be exported to - * Prometheus starting out with a value of 0. + * provided exposition is set up (see {@link exposition}) the gauge will be + * exported to Prometheus starting out with a value of 0. * * Sometimes keeping track of the value measured is impractical or * prohibitly expensive. In those cases you can update the value * of the collector as it's being collected for export with a callback * function passed as one of the options. * - * ```js + * ```ts * const userCount = new Gauge( * "app_user_count", * "Number of users in the app.", @@ -805,7 +807,7 @@ class GaugeChild extends ValueCollectorChild { * directly on the counter itself, instead a child counter with label values * set has to be retrieved with the `.labels()` method. For example: * - * ```js + * ```ts * const userCount = new Gauge( * "app_user_count", * "Number of users in the app", @@ -831,7 +833,7 @@ class GaugeChild extends ValueCollectorChild { * label values that will be used. This is done by calling `.labels()` for * each combination, for example: * - * ```js + * ```ts * for (let role of ["system", "admin", "normal") { * userCount.labels(role); * } @@ -842,27 +844,34 @@ class GaugeChild extends ValueCollectorChild { * labels you actually need as resource usage for a metric increases * exponentially with the number of labels used. * - * @extends module:lib.ValueCollector - * @static + * @extends ValueCollector */ -class Gauge extends ValueCollector { +export class Gauge extends ValueCollector { /** * Create optionally labeled gauge * - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object=} options - options for collector. - * @param {Array=} options.labels - + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. + * @param options.labels - * Labels for this metric, defaults to no labels. - * @param {boolean=} options.register - + * @param options.register - * If true registers this collector with the default registry. * Defaults to true. - * @param {function()=} options.callback - + * @param options.callback - * Possibly async function that is called when the metric is * collected. The collector being collected is passed as the * argument. */ - constructor(name, help, options = {}) { + constructor( + name: string, + help: string, + options: { + labels?: string[], + register?: boolean, + callback?: (collector: Gauge) => void | Promise, + } = {}, + ) { super("gauge", name, help, options, GaugeChild); } @@ -870,7 +879,7 @@ class Gauge extends ValueCollector { * Increment gague value * * Note: Only works if this is an unlabeled collector. - * @param {number} value - number to increment by. + * @param value - number to increment by. */ inc(value = 1) { this._defaultChild.inc(value); @@ -880,7 +889,7 @@ class Gauge extends ValueCollector { * Decrement gague value * * Note: Only works if this is an unlabeled collector. - * @param {number} value - number to decrease by. + * @param value - number to decrease by. */ dec(value = 1) { this._defaultChild.dec(value); @@ -890,9 +899,9 @@ class Gauge extends ValueCollector { * Set gague value * * Note: Only works if this is an unlabeled collector. - * @param {number} value - number to set gauge to. + * @param value - number to set gauge to. */ - set(value) { + set(value: number) { this._defaultChild.set(value); } @@ -909,7 +918,7 @@ class Gauge extends ValueCollector { * Start a timer for setting a duration * * Note: Only works if this is an unlabeled collector. - * @returns {function()} + * @returns * function that when called will set the guage to the duration in * seconds from when the timer was started */ @@ -922,7 +931,11 @@ class Gauge extends ValueCollector { * Child Summary holding the sum and count for a single label set. */ class SummaryChild { - constructor(collector, key) { + protected _sumValues: Map; + protected _countValues: Map; + protected _key: string; + + constructor(collector: Summary, key: string) { this._sumValues = collector._sumValues; this._countValues = collector._countValues; this._key = key; @@ -933,7 +946,6 @@ class SummaryChild { /** * Sum of all observations for label set - * @type {number} */ get sum() { return this._sumValues.get(this._key); @@ -941,7 +953,6 @@ class SummaryChild { /** * Count of observations for label set - * @type {number} */ get count() { return this._countValues.get(this._key); @@ -950,9 +961,9 @@ class SummaryChild { /** * Observe a given value and increment matching buckets for label set * - * @param {number} value - number to count into summary. + * @param value - number to count into summary. */ - observe(value) { + observe(value: number) { this._sumValues.set(this._key, this._sumValues.get(this._key) + value); this._countValues.set(this._key, this._countValues.get(this._key) + 1); } @@ -960,7 +971,7 @@ class SummaryChild { /** * Start a timer for observing a duration for label set * - * @returns {function()} + * @returns * function that when called will store the duration in seconds from * when the timer was started into the metric. */ @@ -981,7 +992,7 @@ class SummaryChild { * and average size of values for. A common case for this is request * duration, for example: * - * ```js + * ```ts * const requestDuration = new Summary( * "app_request_duration_seconds", * "Time to process app requests", @@ -1010,7 +1021,7 @@ class SummaryChild { * child summary with label values set has to be retrieved with the * `.labels()` method. For example: * - * ```js + * ```ts * const requestDuration = new Summary( * "app_request_duration_seconds", * "Time to process app requests", @@ -1034,7 +1045,7 @@ class SummaryChild { * that will be used. This is done by calling `.labels()` for each * combination, for example: * - * ```js + * ```ts * for (let endpoint of ["/status", "/api", ...) { * requestDuration.labels(endpoint); * } @@ -1044,27 +1055,37 @@ class SummaryChild { * series that need to be stored and processed. You should carefully * evaluate which labels you actually need as resource usage for a metric * increases exponentially with the number of labels used. - * - * @static */ -class Summary extends LabeledCollector { +export class Summary extends LabeledCollector { + _sumValues: Map; + _countValues: Map; + protected _defaultChild: SummaryChild | null; + /** * Create optionally labeled summary * - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object=} options - options for collector. - * @param {Array=} options.labels - + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. + * @param options.labels - * Labels for this metric, defaults to no labels. - * @param {boolean=} options.register - + * @param options.register - * If true registers this collector with the default registry. * Defaults to true. - * @param {function()=} options.callback - + * @param options.callback - * Possibly async function that is called when the metric is * collected. The collector being collected is passed as the * argument. */ - constructor(name, help, options = {}) { + constructor( + name: string, + help: string, + options: { + labels?: string[], + register?: boolean, + callback?: (collector: Summary) => void | Promise, + } = {}, + ) { super("summary", name, help, options, SummaryChild); this._sumValues = new Map(); @@ -1107,9 +1128,9 @@ class Summary extends LabeledCollector { * Observe a given value and increment matching buckets * * Note: Only works if this is an unlabeled collector. - * @param {number} value - number to count into histogram buckets. + * @param value - number to count into histogram buckets. */ - observe(value) { + observe(value: number) { this._defaultChild.observe(value); } @@ -1117,7 +1138,7 @@ class Summary extends LabeledCollector { * Start a timer for observing a duration * * Note: Only works if this is an unlabeled collector. - * @returns {function()} + * @returns * function that when called will store the duration in seconds from * when the timer was started into the metric. */ @@ -1125,13 +1146,13 @@ class Summary extends LabeledCollector { return this._defaultChild.startTimer(); } - remove(...labels) { + remove(...labels: LabelValues) { let key = super.remove(...labels); this._sumValues.delete(key); this._countValues.delete(key); } - removeAll(labels) { + removeAll(labels: Record) { super.removeAll(labels); removeMatchingLabels(this._sumValues, labels); removeMatchingLabels(this._countValues, labels); @@ -1144,7 +1165,7 @@ class Summary extends LabeledCollector { } } -function formatValue(value) { +function formatValue(value: number) { if (value === Infinity) { return "+Inf"; } @@ -1154,7 +1175,7 @@ function formatValue(value) { return value.toString(); } -function formatBucketKey(bucket, key) { +function formatBucketKey(bucket: number, key: string) { return `${key === "" ? "" : `${key},`}le="${formatValue(bucket)}"`; } @@ -1162,7 +1183,13 @@ function formatBucketKey(bucket, key) { * Child histogram holding the buckets for a single label set. */ class HistogramChild { - constructor(collector, key) { + protected _bucketValues: Map; + protected _sumValues: Map; + protected _countValues: Map; + protected _key: string + protected _bucketKeys: Map; + + constructor(collector: Histogram, key: string) { this._bucketValues = collector._bucketValues; this._sumValues = collector._sumValues; this._countValues = collector._countValues; @@ -1182,7 +1209,6 @@ class HistogramChild { /** * Mapping of bucket upper bounds to count of observations for label set - * @type {Map} */ get buckets() { return new Map( @@ -1194,7 +1220,6 @@ class HistogramChild { /** * Sum of all observations for label set - * @type {number} */ get sum() { return this._sumValues.get(this._key); @@ -1202,7 +1227,6 @@ class HistogramChild { /** * Count of observations for label set - * @type {number} */ get count() { return this._countValues.get(this._key); @@ -1211,9 +1235,9 @@ class HistogramChild { /** * Observe a given value and increment matching buckets for label set * - * @param {number} value - number to count into histogram buckets. + * @param value - number to count into histogram buckets. */ - observe(value) { + observe(value: number) { for (let [bound, key] of this._bucketKeys) { if (value <= bound) { this._bucketValues.set(key, this._bucketValues.get(key) + 1); @@ -1227,7 +1251,7 @@ class HistogramChild { /** * Start a timer for observing a duration for label set * - * @returns {function()} + * @returns * function that when called will store the duration in seconds from * when the timer was started into the metric. */ @@ -1248,7 +1272,7 @@ class HistogramChild { * reports a metric that you want insight into the distribution of values * for. A common case for this is request duration, for example: * - * ```js + * ```ts * const requestDuration = new Histogram( * "app_request_duration_seconds", * "Time to process app requests", @@ -1276,7 +1300,7 @@ class HistogramChild { * child histogram with label values set has to be retrieved with the * `.labels()` method. For example: * - * ```js + * ```ts * const requestDuration = new Histogram( * "app_request_duration_seconds", * "Time to process app requests", @@ -1300,7 +1324,7 @@ class HistogramChild { * will be used. This is done by calling `.labels()` for each combination, * for example: * - * ```js + * ```ts * for (let endpoint of ["/status", "/api", ...) { * requestDuration.labels(endpoint); * } @@ -1312,44 +1336,51 @@ class HistogramChild { * time series being made.) You should carefully evaluate which labels you * actually need as resource usage for a metric increases exponentially with * the number of labels used. - * - * @static */ -class Histogram extends LabeledCollector { +export class Histogram extends LabeledCollector { + _bucketValues: Map; + _sumValues: Map; + _countValues: Map; + _buckets: number[]; + protected _defaultChild: HistogramChild | null; + /** * Create optionally labeled histogram * - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object=} options - options for collector. - * @param {Array=} options.buckets - + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. + * @param options.buckets - * Buckets to use for the histogram. This is an array of inclusive * upper bounds. Values observed will increment a bucket if it is * less than or equal to the upper bound. - * @param {Array=} options.labels - + * @param options.labels - * Labels for this metric, defaults to no labels. - * @param {boolean=} options.register - + * @param options.register - * If true registers this collector with the default registry. * Defaults to true. - * @param {function()=} options.callback - + * @param options.callback - * Possibly async function that is called when the metric is * collected. The collector being collected is passed as the * argument. */ - constructor(name, help, options = {}) { + constructor( + name: string, + help: string, + options: { + buckets?: number[], + labels?: string[], + register?: boolean, + callback?: (collector: Histogram) => void | Promise, + } = {}, + ) { // These defaults are taken from the Python Prometheus client - let buckets = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, Infinity]; + let { + buckets = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, Infinity], + ...rest + } = options; - let parentOptions = { _reservedLabels: ["le"] }; - for (let [key, value] of Object.entries(options)) { - if (key === "buckets") { - buckets = value; - } else { - parentOptions[key] = value; - } - } - - super("histogram", name, help, parentOptions, HistogramChild); + super("histogram", name, help, { ...rest, _reservedLabels: ["le"] }, HistogramChild); if (buckets.slice(-1)[0] !== Infinity) { buckets = [...buckets, Infinity]; @@ -1378,7 +1409,6 @@ class Histogram extends LabeledCollector { * Mapping of bucket upper bounds to count of observations for label set * * Note: Only available if this is an unlabeled collector. - * @type {Map} */ get buckets() { return this._defaultChild.buckets; @@ -1388,7 +1418,6 @@ class Histogram extends LabeledCollector { * Sum of all observations for label set * * Note: Only available if this is an unlabeled collector. - * @type {number} */ get sum() { return this._defaultChild.sum; @@ -1398,7 +1427,6 @@ class Histogram extends LabeledCollector { * Count of observations for label set * * Note: Only available if this is an unlabeled collector. - * @type {number} */ get count() { return this._defaultChild.count; @@ -1408,9 +1436,8 @@ class Histogram extends LabeledCollector { * Observe a given value and increment matching buckets * * Note: Only works if this is an unlabeled collector. - * @param {number} value - number to count into histogram buckets. */ - observe(value) { + observe(value: number) { this._defaultChild.observe(value); } @@ -1418,7 +1445,7 @@ class Histogram extends LabeledCollector { * Start a timer for observing a duration * * Note: Only works if this is an unlabeled collector. - * @returns {function()} + * @returns * function that when called will store the duration in seconds from * when the timer was started into the metric. */ @@ -1426,7 +1453,7 @@ class Histogram extends LabeledCollector { return this._defaultChild.startTimer(); } - remove(...labels) { + remove(...labels: LabelValues) { let key = super.remove(...labels); for (let bucket of this._buckets) { let bucketKey = formatBucketKey(bucket, key); @@ -1436,7 +1463,7 @@ class Histogram extends LabeledCollector { this._countValues.delete(key); } - removeAll(labels) { + removeAll(labels: Record) { super.removeAll(labels); for (let bucket of this._buckets) { removeMatchingLabels( @@ -1462,13 +1489,13 @@ class Histogram extends LabeledCollector { * `start` and with each subsequent number being equal to the previous * one plus `width`. * - * @param {number} start - Number to start buckets at. - * @param {number} width - Distance between ecah bucket. - * @param {number} count - Number of buckets. - * @returns {Array} array of buckets. + * @param start - Number to start buckets at. + * @param width - Distance between ecah bucket. + * @param count - Number of buckets. + * @returns array of buckets. */ - static linear(start, width, count) { - let buckets = []; + static linear(start: number, width: number, count: number) { + let buckets: number[] = []; for (let i = 0; i < count; i++) { buckets.push(start); start += width; @@ -1484,13 +1511,13 @@ class Histogram extends LabeledCollector { * `start` and with each subsequent number being equal to the previous * multiplied with `factor`. * - * @param {number} start - Number to start buckets at. - * @param {number} factor - Ratio between each bucket. - * @param {number} count - Number of buckets. - * @returns {Array} array of buckets. + * @param start - Number to start buckets at. + * @param factor - Ratio between each bucket. + * @param count - Number of buckets. + * @returns array of buckets. */ - static exponential(start, factor, count) { - let buckets = []; + static exponential(start: number, factor: number, count: number) { + let buckets: number[] = []; for (let i = 0; i < count; i++) { buckets.push(start); start *= factor; @@ -1511,7 +1538,7 @@ class Histogram extends LabeledCollector { * registring them to the default registry and then adding them to your own * registry, for example: * - * ```js + * ```ts * const myRegistry = new CollectorRegistry(); * const myCounter = new Counter( "a_counter", "A counter.", { register: false }); * myRegistry.register(myCounter); @@ -1523,20 +1550,14 @@ class Histogram extends LabeledCollector { * The same collector can be registered to multiple registries. This may be * used to implement responding with different sets of metrics depending on * what is requested. - * - * @static */ -class CollectorRegistry { - constructor() { - this.collectors = []; - } +export class CollectorRegistry { + collectors: Collector[] = []; /** * Collect metrics from all registered collectors. - * - * @returns {*} Async iterator of {@link CollectorResult}. */ - async* collect() { + async* collect(): AsyncIterable { for (let collector of this.collectors) { for await (let result of collector.collect()) { yield result; @@ -1547,9 +1568,9 @@ class CollectorRegistry { /** * Add collector to the registry. * - * @param {lib/prometheus.Collector} collector - Collector to add. + * @param collector - Collector to add. */ - register(collector) { + register(collector: Collector) { let index = this.collectors.lastIndexOf(collector); if (index !== -1) { throw new Error( @@ -1563,9 +1584,9 @@ class CollectorRegistry { /** * Remove collector from the registry. * - * @param {lib/prometheus.Collector} collector - Collector to remove. + * @param collector - Collector to remove. */ - unregister(collector) { + unregister(collector: Collector) { let index = this.collectors.lastIndexOf(collector); if (index === -1) { throw new Error( @@ -1579,14 +1600,14 @@ class CollectorRegistry { defaultRegistry = new CollectorRegistry(); -function escapeHelp(help) { +function escapeHelp(help: string) { return help .replace(/\\/g, "\\\\") .replace(/\n/g, "\\n") ; } -async function* expositionLines(resultsIterator) { +async function* expositionLines(resultsIterator: AsyncIterable) { let first = true; for await (let result of resultsIterator) { if (first) { @@ -1634,46 +1655,49 @@ async function* expositionLines(resultsIterator) { * app.get("/metrics", (req, res, next) => getMetrics(req, res).catch(next)); * app.listen(9100); * - * @param {*} resultsIterator - + * @param resultsIterator - * Asynchronously itreable of {@link CollectorResult} results to create * exposition for. Defaults to collecting results from {@link - * module:lib.defaultRegistry}. - * @returns {string} Prometheus exposition. + * defaultRegistry}. + * @returns Prometheus exposition. * - * @property {string} exposition.contentType + * @property exposition.contentType * HTTP Content-Type for the exposition format that's implemented - * - * @static */ -async function exposition(resultsIterator = defaultRegistry.collect()) { +export async function exposition(resultsIterator = defaultRegistry.collect()) { let lines = ""; for await (let line of expositionLines(resultsIterator)) { lines += line; } return lines; } - exposition.contentType = "text/plain; version=0.0.4"; /** * Serialize CollectorResult into a plain object * - * Converts a {@link module:lib~CollectorResult} into a plain + * Converts a {@link CollectorResult} into a plain * object form that can be stringified to JSON. * - * @param {module:lib~CollectorResult} result - + * @param result - * Result to serialize into plain object form. - * @param {Object} options - Options for controlling the serialization. - * @param {string} options.addLabels - + * @param options - Options for controlling the serialization. + * @param options.addLabels - * Additional labels to append to each value. This may be used if * multiple sources are combined have the same metric and a qualifier is * needed to make sure the label sets are unique. - * @param {string} options.metricName - Override metric name of the result. - * @param {string} options.metricHelp - Override metric help of the result. - * @returns {Object} plain object form of the result. - * @static + * @param options.metricName - Override metric name of the result. + * @param options.metricHelp - Override metric help of the result. + * @returns plain object form of the result. */ -function serializeResult(result, options = {}) { +export function serializeResult( + result: CollectorResult, + options: { + addLabels?: string, + metricName?: string, + metricHelp?: string, + } = {} +) { let addLabels = null; let metricName = result.metric.name; let metricHelp = result.metric.help; @@ -1689,7 +1713,7 @@ function serializeResult(result, options = {}) { } } - let samples; + let samples: Map>; if (addLabels === null) { samples = result.samples; addLabels = {}; @@ -1717,20 +1741,23 @@ function serializeResult(result, options = {}) { help: metricHelp, labels: [...result.metric.labels, ...Object.keys(addLabels)], }, - samples: [...samples].map(([name, metricSamples]) => [name, [...metricSamples]]), + samples: [...samples].map( + ([name, metricSamples]) => [name, [...metricSamples]] as [string, [string, number][]] + ), }; } /** * Deserialize CollectorResult from plain object * - * Reverse counterpart to {@link module:lib.serializeResult}. + * Reverse counterpart to {@link serializeResult}. * - * @param {Object} serializedResult - Previously serialized result object. - * @returns {module:lib~CollectorResult} deserialized result. - * @static + * @param serializedResult - Previously serialized result object. + * @returns deserialized result. */ -function deserializeResult(serializedResult) { +export function deserializeResult( + serializedResult: ReturnType +): CollectorResult { return { metric: new Metric( serializedResult.metric.type, @@ -1748,61 +1775,43 @@ function deserializeResult(serializedResult) { /** * Default collectors provided by this library - * @type {Object} */ -let defaultCollectors = {}; -defaultCollectors.processCpuSecondsTotal = new Gauge( - "process_cpu_seconds_total", - "Total user and system CPU time spent in seconds.", - { - callback: async function(collector) { - let usage = process.cpuUsage(); - collector.set((usage.user + usage.system) / 1e6); +export const defaultCollectors = { + processCpuSecondsTotal: new Gauge( + "process_cpu_seconds_total", + "Total user and system CPU time spent in seconds.", + { + callback: function(collector) { + let usage = process.cpuUsage(); + collector.set((usage.user + usage.system) / 1e6); + }, }, - }, -); + ), -defaultCollectors.processResidentMemoryBytes = new Gauge( - "process_resident_memory_bytes", - "Resident memory size in bytes.", - { - callback: function(collector) { - collector.set(process.memoryUsage().rss); + processResidentMemoryBytes: new Gauge( + "process_resident_memory_bytes", + "Resident memory size in bytes.", + { + callback: function(collector) { + collector.set(process.memoryUsage().rss); + }, }, - }, -); + ), -defaultCollectors.processHeapBytes = new Gauge( - "process_heap_bytes", - "Process heap size in bytes.", - { - callback: function(collector) { - collector.set(process.memoryUsage().heapUsed); + processHeapBytes: new Gauge( + "process_heap_bytes", + "Process heap size in bytes.", + { + callback: function(collector) { + collector.set(process.memoryUsage().heapUsed); + }, }, - }, -); + ), + + processStartTimeSeconds: new Gauge( + "process_start_time_seconds", + "Start time of the process since unix epoch in seconds." + ), +} as const; -defaultCollectors.processStartTimeSeconds = new Gauge( - "process_start_time_seconds", - "Start time of the process since unix epoch in seconds." -); defaultCollectors.processStartTimeSeconds.setToCurrentTime(); - - -module.exports = { - Counter, - Gauge, - Summary, - Histogram, - CollectorRegistry, - Collector, - LabeledCollector, - ValueCollector, - Metric, - - exposition, - defaultRegistry, - defaultCollectors, - serializeResult, - deserializeResult, -}; diff --git a/packages/lib/src/schema.js b/packages/lib/src/schema.ts similarity index 59% rename from packages/lib/src/schema.js rename to packages/lib/src/schema.ts index 63c2f5c9..d06fdfc2 100644 --- a/packages/lib/src/schema.js +++ b/packages/lib/src/schema.ts @@ -2,8 +2,7 @@ * JSON schemas used for validating link messages * @module lib/schema */ -"use strict"; -const Ajv = require("ajv"); +import Ajv, { JSONSchemaType, ValidateFunction } from "ajv"; const ajv = new Ajv({ // These are duplicated in scripts/compile_validator allowUnionTypes: true, @@ -14,17 +13,16 @@ const ajv = new Ajv({ /** * Compile JSON schema into validator - * @function - * @param {Object} schema - JSON schema to create validator for. - * @returns {Function} Validator for the schema. + * @param schema - JSON schema to create validator for. + * @returns Validator for the schema. */ -function compile(schema) { +export function compile(schema: JSONSchemaType) { if (typeof global !== "object" || !global.lazySchemaCompilation) { - return ajv.compile(schema); + return ajv.compile(schema); } - let doValidate; - function validate(data) { + let doValidate: ValidateFunction; + const validate = >function (data) { if (!doValidate) { doValidate = ajv.compile(schema); } @@ -32,9 +30,5 @@ function compile(schema) { validate.errors = doValidate.errors; return result; } - return validate; + return validate } - -module.exports = { - compile, -}; diff --git a/packages/lib/src/shared_commands.js b/packages/lib/src/shared_commands.ts similarity index 72% rename from packages/lib/src/shared_commands.js rename to packages/lib/src/shared_commands.ts index c641d1df..ada6ac4a 100644 --- a/packages/lib/src/shared_commands.js +++ b/packages/lib/src/shared_commands.ts @@ -3,15 +3,15 @@ * @module lib/shared_commands */ "use strict"; -const path = require("path"); +import path from "path"; -const libConfig = require("./config"); -const libFileOps = require("./file_ops"); -const { logger } = require("./logging"); -const libHelpers = require("./helpers"); +import * as libConfig from "./config"; +import * as libFileOps from "./file_ops"; +import { logger } from "./logging"; +import * as libHelpers from "./helpers"; -function print(...content) { +function print(...content: any[]) { // eslint-disable-next-line no-console console.log(...content); } @@ -22,9 +22,9 @@ function print(...content) { * Can be passed to yargs.command to implement a plugin list management * command. Use handlePluginCommand to do the requested action. * - * @param {Object} yargs - yargs command builder. + * @param yargs - yargs command builder. */ -function pluginCommand(yargs) { +export function pluginCommand(yargs: any) { yargs .command("add ", "Add plugin by require path") .command("remove ", "Remove plugin by name") @@ -40,20 +40,24 @@ function pluginCommand(yargs) { * * Handle the actions that are made available by pluginCommand. * - * @param {Object} args - yargs args object. - * @param {Map} pluginList - Current list of plugins. - * @param {string} pluginListPath - Path to plugin list config file. + * @param args - yargs args object. + * @param pluginList - Current list of plugins. + * @param pluginListPath - Path to plugin list config file. */ -async function handlePluginCommand(args, pluginList, pluginListPath) { +export async function handlePluginCommand( + args: Record, + pluginList: Map, + pluginListPath: string +) { let command = args._[1]; if (command === "add") { - let pluginPath = args.path; + let pluginPath = args.path as string; if (/^\.\.?[\/\\]/.test(pluginPath)) { pluginPath = path.resolve(pluginPath); } - let pluginInfo; + let pluginInfo: { name: string }; try { // eslint-disable-next-line node/global-require pluginInfo = require(path.posix.join(pluginPath, "info")); @@ -74,7 +78,7 @@ async function handlePluginCommand(args, pluginList, pluginListPath) { print(`Added ${pluginInfo.name}`); } else if (command === "remove") { - if (!pluginList.delete(args.name)) { + if (!pluginList.delete(args.name as string)) { logger.error(`Plugin with name ${args.name} does not exist`); process.exitCode = 1; return; @@ -97,11 +101,11 @@ async function handlePluginCommand(args, pluginList, pluginListPath) { * Can be passed to yargs.command to implement a config command. Use * handleConfigCommand to do the requested action. * - * @param {Object} yargs - yargs command builder. + * @param yargs - yargs command builder. */ -function configCommand(yargs) { +export function configCommand(yargs: any) { yargs - .command("set [value]", "Set config field", yargs => { + .command("set [value]", "Set config field", (yargs: any) => { yargs.options({ "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, }); @@ -119,15 +123,19 @@ function configCommand(yargs) { * * Handle the actions that are made available by configCommand. * - * @param {Object} args - yargs args object. - * @param {module:lib.Config} instance - Config instance. - * @param {string} configPath - Path to configuration file. + * @param args - yargs args object. + * @param instance - Config instance. + * @param configPath - Path to configuration file. */ -async function handleConfigCommand(args, instance, configPath) { +export async function handleConfigCommand( + args: Record, + instance: libConfig.Config, + configPath: string +) { let command = args._[1]; if (command === "list") { - for (let GroupClass of instance.constructor.groups.values()) { + for (let GroupClass of (instance as any).constructor.groups.values()) { for (let def of GroupClass.definitions.values()) { let value = instance.get(def.fullName); print(`${def.fullName} ${JSON.stringify(value)}`); @@ -136,7 +144,7 @@ async function handleConfigCommand(args, instance, configPath) { } else if (command === "show") { try { - print(instance.get(args.field)); + print(instance.get(args.field as string)); } catch (err) { if (err instanceof libConfig.InvalidField) { logger.error(err.message); @@ -154,7 +162,7 @@ async function handleConfigCommand(args, instance, configPath) { } try { - instance.set(args.field, args.value); + instance.set(args.field as string, args.value); await libFileOps.safeOutputFile(configPath, JSON.stringify(instance.serialize(), null, 4)); } catch (err) { if (err instanceof libConfig.InvalidField || err instanceof libConfig.InvalidValue) { @@ -165,12 +173,3 @@ async function handleConfigCommand(args, instance, configPath) { } } } - - -module.exports = { - pluginCommand, - handlePluginCommand, - - configCommand, - handleConfigCommand, -}; diff --git a/packages/lib/src/stream.ts b/packages/lib/src/stream.ts index a35185b2..5e2ab36c 100644 --- a/packages/lib/src/stream.ts +++ b/packages/lib/src/stream.ts @@ -138,7 +138,7 @@ type ReadStreamOptions = Parameters[1]; * Options to pass to fs.createReadStream. Values passed for `fd` and * `fs` will be ignored. */ -export async function createReverseReadStream(path: string, options: ReadStreamOptions) { +export async function createReverseReadStream(path: string, options?: ReadStreamOptions) { if (typeof options === "string") { options = { encoding: options }; } diff --git a/packages/lib/src/users.js b/packages/lib/src/users.ts similarity index 83% rename from packages/lib/src/users.js rename to packages/lib/src/users.ts index 5e505fc4..40d93b10 100644 --- a/packages/lib/src/users.js +++ b/packages/lib/src/users.ts @@ -7,24 +7,23 @@ * @module lib/users */ "use strict"; -const libErrors = require("./errors"); +import * as libErrors from "./errors"; -const PlayerStats = require("./PlayerStats"); +import PlayerStats from "./PlayerStats"; /** * Represents a permission that can be granted - * @static */ -class Permission { - constructor(name, title, description, grantByDefault) { - this.name = name; - this.title = title; - this.description = description; - this.grantByDefault = grantByDefault; - } +export class Permission { + constructor( + public name: string, + public title: string, + public description: string, + public grantByDefault: boolean, + ) { } } -const permissions = new Map(); +export const permissions = new Map(); /** * Define a new user permission for Clusterio @@ -33,15 +32,25 @@ const permissions = new Map(); * granted to roles, and checked on users. Name should be `lower_case` * style and start with the plugin name followed by a dot. * - * @param {Object} def - The definition for the permission. - * @param {string} def.name - The internal name for the permission. - * @param {string} def.title - User facing name for permission. - * @param {string} def.description - User facing description. - * @param {boolean} def.grantByDefault - + * @param def - The definition for the permission. + * @param def.name - The internal name for the permission. + * @param def.title - User facing name for permission. + * @param def.description - User facing description. + * @param def.grantByDefault - * If true this role is granted by the generated Player role by default. * */ -function definePermission({ name, title, description, grantByDefault=false }) { +export function definePermission({ + name, + title, + description, + grantByDefault = false, +}: { + name: string, + title: string, + description: string, + grantByDefault?: boolean, +}) { if (typeof name !== "string" || !name.length) { throw new Error("Expected name to be a non-empty string"); } @@ -64,10 +73,14 @@ function definePermission({ name, title, description, grantByDefault=false }) { /** * Represents a collection of granted permissions - * @static */ -class Role { - constructor(serializedRole) { +export class Role { + id: number; + name: string; + description: string; + permissions: Set; + + constructor(serializedRole: ReturnType) { this.id = serializedRole.id; this.name = serializedRole.name; this.description = serializedRole.description; @@ -98,14 +111,18 @@ class Role { * * Ensures the role with id 0 exisits and has the core.admin permission. * - * @param {Map} roles - role storage to modify. - * @returns {module:lib.Role} the default admin role - * @static + * @param roles - role storage to modify. + * @returns the default admin role */ -function ensureDefaultAdminRole(roles) { +export function ensureDefaultAdminRole(roles: Map) { let admin = roles.get(0); if (!admin) { - admin = new Role({ id: 0, name: "Cluster Admin", description: "Cluster wide administrator." }); + admin = new Role({ + id: 0, + name: "Cluster Admin", + description: "Cluster wide administrator.", + permissions: [], + }); roles.set(0, admin); } admin.permissions.add("core.admin"); @@ -119,13 +136,17 @@ function ensureDefaultAdminRole(roles) { * assigned by default. Note that this may not be the same role as the one * that is assigned to new users by default. * - * @param {Map} roles - role storage to modify. - * @static + * @param roles - role storage to modify. */ -function ensureDefaultPlayerRole(roles) { +export function ensureDefaultPlayerRole(roles: Map) { let player = roles.get(1); if (!player) { - player = new Role({ id: 1, name: "Player", description: "Default player role." }); + player = new Role({ + id: 1, + name: "Player", + description: "Default player role.", + permissions: [], + }); roles.set(1, player); } player.grantDefaultPermissions(); @@ -136,56 +157,42 @@ function ensureDefaultPlayerRole(roles) { * Represeents a user in the cluster * * Holds data about a Factorio user in the cluster. - * @static */ -class User { - constructor(serializedUser, loadedRoles) { - /** - * Factorio user name. - * @type {string} - */ +export class User { + /** Factorio user name. */ + name: string; + /** Instances this user is online on. */ + instances: Set; + /** Unix time in seconds the user token must be issued after to be valid. */ + tokenValidAfter: number; + /** True if the user is promoted to admin on the Factorio instances. */ + isAdmin: boolean; + /** True if the user is whitelisted on the Factorio instances. */ + isWhitelisted: boolean; + /** True if the user is banned from Factorio instances. */ + isBanned: boolean; + /** Reason for being banned. Ignored if isBanned is false. */ + banReason: string; + /** Roles this user has */ + roles: Set; + /** Per instance statistics for the player this user account is tied to. */ + instanceStats: Map; + /** Combined statistics for the player this user account is tied to. */ + playerStats: PlayerStats; + /** True if this user object has been removed from the cluster. */ + isDeleted: boolean; + + constructor( + serializedUser: ReturnType, + loadedRoles: Map + ) { this.name = serializedUser.name; - - /** - * Instances this user is online on. - * @type {Set} - */ this.instances = new Set(); - - /** - * Unix time in seconds the user token must be issued after to be valid. - * @type {number} - */ this.tokenValidAfter = serializedUser.token_valid_after || 0; - - /** - * True if the user is promoted to admin on the Factorio instances. - * @type {boolean} - */ this.isAdmin = Boolean(serializedUser.is_admin); - - /** - * True if the user is whitelisted on the Factorio instances. - * @type {boolean} - */ this.isWhitelisted = Boolean(serializedUser.is_whitelisted); - - /** - * True if the user is banned from Factorio instances. - * @type {boolean} - */ this.isBanned = Boolean(serializedUser.is_banned); - - /** - * Reason for being banned. Ignored if isBanned is false. - * @type {string} - */ this.banReason = serializedUser.ban_reason || ""; - - /** - * Roles this user has - * @type {Set} - */ this.roles = new Set(); if (serializedUser.roles) { for (let roleId of serializedUser.roles) { @@ -195,32 +202,26 @@ class User { } } } - - /** - * Per instance statistics for the player this user account is tied to. - * @type {Map} - */ this.instanceStats = new Map( (serializedUser.instance_stats ? serializedUser.instance_stats : []).map( ([id, stats]) => [id, new PlayerStats(stats)] ) ); - - /** - * Combined statistics for the player this user account is tied to. - * @type {module:lib.PlayerStats} - */ this.playerStats = this._calculatePlayerStats(); - - /** - * True if this user object has been removed from the cluster. - * @type {boolean} - */ this.isDeleted = false; } serialize() { - let serialized = { + let serialized: { + name: string, + roles?: number[], + token_valid_after?: number, + is_admin?: boolean, + is_whitelisted?: boolean, + is_banned?: boolean, + ban_reason?: string, + instance_stats?: [number, object][], + } = { name: this.name, }; @@ -272,11 +273,11 @@ class User { * permission. If the permission is not granted for the user a * "Permission denied" error is thrown. * - * @param {string} permission - The permission to check for. + * @param permission - The permission to check for. * @throws {Error} If the given permission does not exist. - * @throws {module:lib.PermissionError} if the user does noh have the given permission. + * @throws {libErrors.PermissionError} if the user does noh have the given permission. */ - checkPermission(permission) { + checkPermission(permission: string) { if (!permissions.has(permission)) { throw new Error(`permission ${permission} does not exist`); } @@ -290,12 +291,12 @@ class User { throw new libErrors.PermissionError("Permission denied"); } - notifyJoin(instance_id) { + notifyJoin(instance_id: number) { this.instances.add(instance_id); User.onlineUsers.add(this); } - notifyLeave(instance_id) { + notifyLeave(instance_id: number) { this.instances.delete(instance_id); if (!this.instances.size) { User.onlineUsers.delete(this); @@ -321,12 +322,11 @@ class User { } return playerStats; } + /** + * Set of users currently online in the cluster. + */ + static onlineUsers = new Set(); } -/** - * Set of users currently online in the cluster. - * @type {module:lib.User} - */ -User.onlineUsers = new Set(); // Definitions for the built in permissions used in Clusterio. // description should answer "this permission allows you to ___" @@ -677,14 +677,3 @@ definePermission({ title: "Dump WebSocket", description: "Dump all WebSocket communicatation from the controller.", }); - -module.exports = { - Permission, - Role, - User, - - permissions, - definePermission, - ensureDefaultAdminRole, - ensureDefaultPlayerRole, -}; diff --git a/packages/lib/src/zip_ops.js b/packages/lib/src/zip_ops.ts similarity index 75% rename from packages/lib/src/zip_ops.js rename to packages/lib/src/zip_ops.ts index 39cb1bb0..f69f63e8 100644 --- a/packages/lib/src/zip_ops.js +++ b/packages/lib/src/zip_ops.ts @@ -1,4 +1,4 @@ -"use strict"; +import type JSZip from "jszip"; /** * Returns the root folder in the zip file @@ -6,13 +6,10 @@ * Returns the name of the folder that all files in the zip file is * contained in. Throws an error if there are multiple such folders. * - * @param {JSZip} zip - Zip to search through. - * @returns {string} name of the root folder. - * @memberof module:lib - * @private - * @inner + * @param zip - Zip to search through. + * @returns name of the root folder. */ -function findRoot(zip) { +export function findRoot(zip: JSZip) { let root = null; zip.forEach((relativePath, file) => { let index = relativePath.indexOf("/"); @@ -34,7 +31,3 @@ function findRoot(zip) { return root; } - -module.exports = { - findRoot, -}; From 15ea9a91dc599fc3ce32c9d9465009eb0ff2418b Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 6 Aug 2023 18:38:20 +0200 Subject: [PATCH 07/13] Port basic lib sub-modules to TypeScript --- packages/lib/package.json | 2 +- packages/lib/src/command.ts | 5 + .../lib/src/config/{classes.js => classes.ts} | 328 ++++++++++-------- .../config/{definitions.js => definitions.ts} | 49 +-- packages/lib/src/data/index.ts | 37 ++ packages/lib/src/data/messages_core.js | 4 +- packages/lib/src/errors.ts | 35 +- ...{exchange_string.js => exchange_string.ts} | 126 +++---- .../src/link/{connectors.js => connectors.ts} | 193 ++++++----- packages/lib/src/link/index.ts | 4 - packages/lib/src/link/{link.js => link.ts} | 305 +++++++++------- .../lib/src/link/{messages.js => messages.ts} | 25 +- packages/lib/src/shared_commands.ts | 2 +- test/lib/link/connector.js | 9 - 14 files changed, 636 insertions(+), 488 deletions(-) rename packages/lib/src/config/{classes.js => classes.ts} (70%) rename packages/lib/src/config/{definitions.js => definitions.ts} (92%) rename packages/lib/src/factorio/{exchange_string.js => exchange_string.ts} (79%) rename packages/lib/src/link/{connectors.js => connectors.ts} (79%) rename packages/lib/src/link/{link.js => link.ts} (65%) rename packages/lib/src/link/{messages.js => messages.ts} (85%) diff --git a/packages/lib/package.json b/packages/lib/package.json index 40f530a5..519495da 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -5,7 +5,7 @@ "repository": "https://github.com/clusterio/clusterio", "license": "MIT", "scripts": { - "prepare": "pnpm run compile_validator && tsc || true", + "prepare": "pnpm run compile_validator && tsc", "compile_validator": "node scripts/compile_validator.js" }, "main": "dist/index.js", diff --git a/packages/lib/src/command.ts b/packages/lib/src/command.ts index f0caf2f5..1577b486 100644 --- a/packages/lib/src/command.ts +++ b/packages/lib/src/command.ts @@ -162,6 +162,7 @@ export async function resolveHost(client: Link, hostName: string) { hostId = parseInt(hostName, 10); } else { let hosts = await client.sendTo("controller", new libData.HostListRequest()); + // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let host of hosts) { if (host.name === hostName) { hostId = host.id; @@ -193,6 +194,7 @@ export async function resolveInstance(client: Link, instanceName: string) { instanceId = parseInt(instanceName, 10); } else { let instances = await client.sendTo("controller", new libData.InstanceDetailsListRequest()); + // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let instance of instances) { if (instance.name === instanceName) { instanceId = instance.id; @@ -225,6 +227,7 @@ export async function resolveModPack(client: Link, modPackName: string) { modPackId = parseInt(modPackName, 10); } else { let modPacks = await client.sendTo("controller", new libData.ModPackListRequest()); + // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let modPack of modPacks) { if (modPack.name === modPackName) { modPackId = modPack.id; @@ -256,6 +259,7 @@ export async function retrieveRole(client: Link, roleName: string) { let resolvedRole: libData.RawRole; if (/^-?\d+$/.test(roleName)) { let roleId = parseInt(roleName, 10); + // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let role of roles) { if (role.id === roleId) { resolvedRole = role; @@ -264,6 +268,7 @@ export async function retrieveRole(client: Link, roleName: string) { } } else { + // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let role of roles) { if (role.name === roleName) { resolvedRole = role; diff --git a/packages/lib/src/config/classes.js b/packages/lib/src/config/classes.ts similarity index 70% rename from packages/lib/src/config/classes.js rename to packages/lib/src/config/classes.ts index 1855b584..a05bff29 100644 --- a/packages/lib/src/config/classes.js +++ b/packages/lib/src/config/classes.ts @@ -1,11 +1,11 @@ // Configuration classes -"use strict"; +import events from "events"; -const events = require("events"); +import isDeepStrictEqual from "../is_deep_strict_equal"; +import { basicType } from"../helpers"; -const isDeepStrictEqual = require("../is_deep_strict_equal"); -const { basicType } = require("../helpers"); +export type ConfigLocation = "controller" | "host" | "instance" | "control"; /** * Invalid Access exception @@ -14,9 +14,8 @@ const { basicType } = require("../helpers"); * does not permitt this access is done. * * @extends Error - * @memberof module:lib */ -class InvalidAccess extends Error { }; +export class InvalidAccess extends Error { }; /** * Invalid Value exception @@ -25,9 +24,8 @@ class InvalidAccess extends Error { }; * field. * * @extends Error - * @memberof module:lib */ -class InvalidValue extends Error { }; +export class InvalidValue extends Error { }; /** * Invalid Field exception @@ -36,9 +34,8 @@ class InvalidValue extends Error { }; * attempted to be accessed. * * @extends Error - * @memberof module:lib */ -class InvalidField extends Error { }; +export class InvalidField extends Error { }; /** * Split the given string on the first instance of separator @@ -48,14 +45,12 @@ class InvalidField extends Error { }; * after the separator. Returns an array with the string and an empty * string if the separator is not present. * - * @param {string} separator - Separator to split string by. - * @param {string} string - String to split - * @returns {Array} string split on separator. - * @memberof module:lib - * @private - * @inner + * @param separator - Separator to split string by. + * @param string - String to split + * @returns string split on separator. + * @internal */ -function splitOn(separator, string) { +function splitOn(separator: string, string: string) { let index = string.indexOf(separator); if (index === -1) { return [string, ""]; @@ -63,6 +58,9 @@ function splitOn(separator, string) { return [string.slice(0, index), string.slice(index + separator.length)]; } +type SerializedConfig = ReturnType; +type SerializedGroup = ReturnType; + /** * Collection of config groups * @@ -76,34 +74,38 @@ function splitOn(separator, string) { * prev. * * @extends events.EventEmitter - * - * @memberof module:lib */ -class Config extends events.EventEmitter { +export class Config extends events.EventEmitter { + declare static _finalized: boolean; + declare static _initialized: boolean; + declare static _groups: Map; + + _initialized = false; + _groups = new Map(); + _unknownGroups = new Map(); + /** * Create a new instance of the given config * - * @param {string} location - + * @param location - * Location to evaluate access for this instance from */ - constructor(location) { + constructor( + public location: ConfigLocation, + ) { super(); - if (!this.constructor._finalized) { + if (!(this.constructor as typeof Config)._finalized) { throw new Error(`Cannot instantiate incomplete Config class ${this.constructor.name}`); } if (typeof location !== "string") { throw new Error("location must be a string"); } - - this.location = location; - this._groups = new Map(); - this._unknownGroups = new Map(); } async init() { - for (let [name, GroupClass] of this.constructor._groups) { + for (let [name, GroupClass] of (this.constructor as typeof Config)._groups) { if (!this._groups.has(name)) { let group = new GroupClass(this); await group.init(); @@ -113,7 +115,7 @@ class Config extends events.EventEmitter { this._initialized = true; } - _validate(serializedConfig) { + _validate(serializedConfig: SerializedConfig) { if (basicType(serializedConfig) !== "object") { throw new Error(`Expected object, not ${basicType(serializedConfig)} for config`); } @@ -126,13 +128,16 @@ class Config extends events.EventEmitter { /** * Load config from a serialized object * - * @param {object} serializedConfig - Serialized config to load. - * @param {string=} location - Location used for access control. + * @param serializedConfig - Serialized config to load. + * @param location - Location used for access control. */ - async load(serializedConfig, location = this.location) { + async load( + serializedConfig: SerializedConfig, + location = this.location + ) { this._validate(serializedConfig); for (let serializedGroup of serializedConfig.groups) { - let GroupClass = this.constructor._groups.get(serializedGroup.name); + let GroupClass = (this.constructor as typeof Config)._groups.get(serializedGroup.name); if (!GroupClass) { this._unknownGroups.set(serializedGroup.name, serializedGroup); continue; @@ -149,11 +154,11 @@ class Config extends events.EventEmitter { /** * Serialize the config to a plain JavaScript object * - * @param {string=} location - Location used for access control. - * @returns {Object} JSON serializable representation of the config. + * @param location - Location used for access control. + * @returns JSON serializable representation of the config. */ serialize(location = this.location) { - let groups = [...this._unknownGroups.values()]; + let groups: SerializedGroup[] = [...this._unknownGroups.values()]; for (let group of this._groups.values()) { groups.push(group.serialize(location)); } @@ -167,11 +172,15 @@ class Config extends events.EventEmitter { * Updates all groups in the config with groups in the serialized * config passed. * - * @param {Object} serializedConfig - Output from .serialize(). - * @param {boolean} notify - Invoke fieldChanged events if true. - * @param {string=} location - Location used for access control. + * @param serializedConfig - Output from .serialize(). + * @param notify - Invoke fieldChanged events if true. + * @param location - Location used for access control. */ - update(serializedConfig, notify, location = this.location) { + update( + serializedConfig: SerializedConfig, + notify: boolean, + location = this.location + ) { this._validate(serializedConfig); for (let serializedGroup of serializedConfig.groups) { let group = this._groups.get(serializedGroup.name); @@ -186,11 +195,11 @@ class Config extends events.EventEmitter { /** * Check if field can be accessed from the given location * - * @param {string} name - Name of the field to check. - * @param {string=} location - Location used for access control. - * @returns {boolean} true if field is accessible + * @param name - Name of the field to check. + * @param location - Location used for access control. + * @returns true if field is accessible */ - canAccess(name, location = this.location) { + canAccess(name: string, location = this.location) { let [group, field] = splitOn(".", name); return this.group(group).canAccess(field, location); } @@ -198,11 +207,11 @@ class Config extends events.EventEmitter { /** * Get the value for a config field * - * @param {string} name - Name of field to get. - * @param {string=} location - Location used for access control. - * @returns {*} the value for the field. + * @param name - Name of field to get. + * @param location - Location used for access control. + * @returns the value for the field. */ - get(name, location = this.location) { + get(name: string, location = this.location) { let [group, field] = splitOn(".", name); return this.group(group).get(field, location); } @@ -210,11 +219,11 @@ class Config extends events.EventEmitter { /** * Set the value for a config field * - * @param {string} name - Name of field to set. - * @param {*} value - the value to set on field - * @param {string=} location - Location used for access control. + * @param name - Name of field to set. + * @param value - the value to set on field + * @param location - Location used for access control. */ - set(name, value, location = this.location) { + set(name: string, value: unknown, location = this.location) { let [group, field] = splitOn(".", name); this.group(group).set(field, value, location); } @@ -222,12 +231,12 @@ class Config extends events.EventEmitter { /** * Set property of an object value for a config field * - * @param {string} name - Name of field to set property on. - * @param {string} prop - Name of property to set on field. - * @param {*} value - the value to set the property to. - * @param {string=} location - Location used for access control. + * @param name - Name of field to set property on. + * @param prop - Name of property to set on field. + * @param value - the value to set the property to. + * @param location - Location used for access control. */ - setProp(name, prop, value, location = this.location) { + setProp(name: string, prop: string, value: unknown, location = this.location) { let [group, field] = splitOn(".", name); this.group(group).setProp(field, prop, value, location); } @@ -235,10 +244,10 @@ class Config extends events.EventEmitter { /** * Get the config group instance with the given name * - * @param {string} name - Name of group to get. - * @returns {module:lib.ConfigGroup} config group. + * @param name - Name of group to get. + * @returns config group. */ - group(name) { + group(name: string) { if (!this._initialized) { throw new Error(`${this.constructor.name} instance is uninitialized`); } @@ -266,7 +275,7 @@ class Config extends events.EventEmitter { * mave been registered with this config. Note that mutating this * object will lead to unexpected behaviour. * - * @returns {Map} config groups + * @returns config groups */ static get groups() { this._initSubclass(); @@ -287,9 +296,9 @@ class Config extends events.EventEmitter { * Adds the ConfigGroup to this config as a group that will get * initialized, loaded and serialized with this config class. * - * @param {module:lib.ConfigGroup} group - class to register. + * @param group - class to register. */ - static registerGroup(group) { + static registerGroup(group: typeof ConfigGroup) { this._initSubclass(); if (this._finalized) { @@ -308,6 +317,20 @@ class Config extends events.EventEmitter { } } +type FieldType = "boolean" | "string" | "number" | "object"; +interface FieldDefinition { + type: FieldType; + name: string; + fullName: string; + title?: string; + description?: string; + optional: boolean; + initial_value?: unknown | (() => unknown); + enum?: unknown[]; + restartRequired: boolean; + restartRequiredProps?: string[]; + access: ConfigLocation[]; +} /** * Collection of related config entries @@ -316,41 +339,47 @@ class Config extends events.EventEmitter { * to set invalid values for fields will result in an error thrown while * loading a config with invalid values will result in those being replaced * by the innitial value for those fields. - * - * @memberof module:lib */ -class ConfigGroup { +export class ConfigGroup { + declare static _finalized: boolean; + declare static _initialized: boolean; + declare static _definitions: Map; + declare static groupName: string; + declare static defaultAccess: ConfigLocation[]; + + _fields = new Map(); + _unknownFields = new Map(); + + /** * Creates a new config group. * * After the creation of the new config group you have to call * either .init() or .load() on it in order to fully initialize it. * - * @param {module:lib.Config} config - + * @param config - * Parent config for this group instance. */ - constructor(config) { - if (!this.constructor._finalized) { + constructor( + public config: Config + ) { + if (!(this.constructor as typeof ConfigGroup)._finalized) { throw new Error(`Cannot instantiate incomplete ConfigGroup class ${this.constructor.name}`); } - - this._fields = new Map(); - this._unknownFields = new Map(); - this.config = config; } /** * Check access for a field * - * @param {object} def - definition for for the field to check. - * @param {string} remote - location of remote access to check for. - * @param {boolean} error - throw on failure to pass check. - * @throws {module:lib.InvalidAccess} + * @param def - definition for for the field to check. + * @param remote - location of remote access to check for. + * @param error - throw on failure to pass check. + * @throws {InvalidAccess} * if access is not granted and error is true. - * @returns {boolean} true if access is granted + * @returns true if access is granted * @private */ - _checkAccess(def, remote, error) { + _checkAccess(def: FieldDefinition, remote: ConfigLocation, error: boolean) { for (let loc of [remote, this.config.location]) { if (!def.access.includes(loc)) { if (error) { @@ -370,7 +399,7 @@ class ConfigGroup { * config group. */ async init() { - for (let [name, def] of this.constructor._definitions) { + for (let [name, def] of (this.constructor as typeof ConfigGroup)._definitions) { if (this._fields.has(name)) { continue; } @@ -397,11 +426,11 @@ class ConfigGroup { * group and then initializes any possible missing fields from it to * their default values. * - * @param {Object} serializedGroup - + * @param serializedGroup - * Result from a previous call to .serialize(). - * @param {string=} location - Location used for access control. + * @param location - Location used for access control. */ - async load(serializedGroup, location = this.config.location) { + async load(serializedGroup: SerializedGroup, location = this.config.location) { this.update(serializedGroup, false, location); await this.init(); } @@ -412,18 +441,18 @@ class ConfigGroup { * Shortcut for `group.constructor.groupName` */ get name() { - return this.constructor.groupName; + return (this.constructor as typeof ConfigGroup).groupName; } /** * Check if field can be accessed from the given location * - * @param {string} name - Name of field to check. - * @param {string=} location - Location used for access control. - * @returns {boolean} true if field is accessible + * @param name - Name of field to check. + * @param location - Location used for access control. + * @returns true if field is accessible */ - canAccess(name, location = this.config.location) { - let def = this.constructor._definitions.get(name); + canAccess(name: string, location = this.config.location) { + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!def) { throw new InvalidField(`No field named '${name}'`); } @@ -433,12 +462,12 @@ class ConfigGroup { /** * Get value for field * - * @param {string} name - Name of field to get. - * @param {string=} location - Location used for access control. - * @returns {*} Value stored for the field. + * @param name - Name of field to get. + * @param location - Location used for access control. + * @returns Value stored for the field. */ - get(name, location = this.config.location) { - let def = this.constructor._definitions.get(name); + get(name: string, location = this.config.location) { + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!def) { throw new InvalidField(`No field named '${name}'`); } @@ -453,14 +482,14 @@ class ConfigGroup { * The field must be defined for the config group and the value is * type checked against the field definition. * - * @param {string} name - Name of field to set. - * @param {*} value - Value to set for field. - * @param {string=} location - Location used for access control. - * @throws {module:lib.InvalidField} if field is not defined. - * @throws {module:lib.InvalidValue} if value is not allowed for the field. + * @param name - Name of field to set. + * @param value - Value to set for field. + * @param location - Location used for access control. + * @throws {InvalidField} if field is not defined. + * @throws {InvalidValue} if value is not allowed for the field. */ - set(name, value, location = this.config.location) { - let def = this.constructor._definitions.get(name); + set(name: string, value: unknown, location = this.config.location) { + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!def) { throw new InvalidField(`No field named '${name}'`); } @@ -475,7 +504,7 @@ class ConfigGroup { throw new InvalidValue(`Expected one of [${def.enum.join(", ")}], not ${value}`); } - if (basicType(value) === "string") { + if (typeof value === "string") { if (def.type === "boolean") { if (value === "true") { value = true; @@ -520,15 +549,15 @@ class ConfigGroup { * Update value of stored object field by setting the specified property * on it. The field must be defined as an object for the config group. * - * @param {string} name - Name of field to set property on. - * @param {string} prop - Name of property to set on field. - * @param {*} value - the value to set the property to. - * @param {string=} location - Location used for access control. - * @throws {module:lib.InvalidField} if field is not defined. - * @throws {module:lib.InvalidValue} if field is not an object. + * @param name - Name of field to set property on. + * @param prop - Name of property to set on field. + * @param value - the value to set the property to. + * @param location - Location used for access control. + * @throws {InvalidField} if field is not defined. + * @throws {InvalidValue} if field is not an object. */ - setProp(name, prop, value, location = this.config.location) { - let def = this.constructor._definitions.get(name); + setProp(name: string, prop: string, value: unknown, location = this.config.location) { + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!def) { throw new InvalidField(`No field named '${name}'`); } @@ -538,7 +567,7 @@ class ConfigGroup { throw new InvalidValue(`Cannot set property on non-object field '${name}'`); } - let prev = this._fields.get(name); + let prev = this._fields.get(name) as object; let updated = {...prev || {}}; if (value !== undefined) { @@ -558,12 +587,16 @@ class ConfigGroup { * Overwrites the values of all the fields in this config group that * has a valid value in the passed serialized group with that value. * - * @param {Object} serializedGroup - + * @param serializedGroup - * Result from a previous call to .serialize(). - * @param {boolean} notify - Invoke fieldChanged events if true. - * @param {string=} location - Location used for access control. + * @param notify - Invoke fieldChanged events if true. + * @param location - Location used for access control. */ - update(serializedGroup, notify, location = this.config.location) { + update( + serializedGroup: SerializedGroup, + notify: boolean, + location = this.config.location + ) { if (basicType(serializedGroup) !== "object") { throw new Error(`Expected object, not ${basicType(serializedGroup)} for ConfigGroup`); } @@ -577,7 +610,7 @@ class ConfigGroup { } for (let [name, value] of Object.entries(serializedGroup.fields)) { - let def = this.constructor._definitions.get(name); + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!def) { this._unknownFields.set(name, value); continue; @@ -611,13 +644,13 @@ class ConfigGroup { /** * Serialize the config to a plain JavaScript object * - * @param {string=} location - Location used for access control. - * @returns {Object} JSON serializable representation of the group. + * @param location - Location used for access control. + * @returns JSON serializable representation of the group. */ - serialize(location = this.config.location) { + serialize(location: ConfigLocation = this.config.location) { let fields = {}; for (let [name, value] of this._fields) { - let def = this.constructor._definitions.get(name); + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!this._checkAccess(def, location, false)) { continue; } @@ -649,8 +682,6 @@ class ConfigGroup { * * Returns the internal meta data for the fields of this class. * Note that mutating this object will lead to unexpected behaviour. - * - * @returns {Map} field meta data */ static get definitions() { this._initSubclass(); @@ -660,37 +691,48 @@ class ConfigGroup { /** * Define a new configuration field * - * @param {Object} item - Field definition. - * @param {Array=} item.access - + * @param item - Field definition. + * @param item.access - * Array of strings declaring where this config entry can be read * and modified from. - * @param {string} item.type - + * @param item.type - * string declaring the type of the config value, supports * boolean, string, number and object. - * @param {string} item.name - + * @param item.name - * Name of the configuration field. Should follow the * lower_case_underscore style. - * @param {string} item.title - + * @param item.title - * Text used to identify the config field in user interfaces. - * @param {string} item.description - + * @param item.description - * Text used to describe the config field in user interfaces. - * @param {Array=} item.enum - + * @param item.enum - * Array of all values that are acceptible. Useful for making * enumerated values. - * @param {boolean=} item.restartRequired - + * @param item.restartRequired - * True if a restart of the entity this config is attached to is * required for changes to take effect. Informative only, defaults * to false. - * @param {Array=} item.restartRequiredProps - + * @param item.restartRequiredProps - * Properties to invert the value of restartRequired for if present. - * @param {boolean=} item.optional - + * @param item.optional - * True if this field can be set to null. Defaults to false. - * @param {*=} item.initial_value - + * @param item.initial_value - * Value this config field should take in a newly initialized * config. This can also be an async function returning the * value to use. */ - static define(item) { + static define(item: { + access?: ConfigLocation[], + type: FieldType, + name: string, + title?: string, + description?: string, + enum?: unknown[], + restartRequired?: boolean, + restartRequiredProps?: string[], + optional?: boolean, + initial_value?: unknown | (() => unknown), + }) { if (this._finalized) { throw new Error(`Cannot define field for ConfigGroup class ${this.name} after it has been finalized`); } @@ -786,7 +828,7 @@ class ConfigGroup { throw new Error(`Config field ${item.name} has already been defined`); } - let computed = { + let computed: FieldDefinition = { access: this.defaultAccess, optional: false, fullName: `${this.groupName}.${item.name}`, @@ -829,10 +871,9 @@ class ConfigGroup { * plugins. This is currently only the enabled field. It is otherwise * identical to the ConfigGroup class. * - * @extends module:lib.ConfigGroup - * @memberof module:lib + * @extends ConfigGroup */ -class PluginConfigGroup extends ConfigGroup { +export class PluginConfigGroup extends ConfigGroup { static _initSubclass() { if (!this._initialized) { super._initSubclass(); @@ -846,14 +887,3 @@ class PluginConfigGroup extends ConfigGroup { } } } - - -module.exports = { - InvalidAccess, - InvalidField, - InvalidValue, - - Config, - ConfigGroup, - PluginConfigGroup, -}; diff --git a/packages/lib/src/config/definitions.js b/packages/lib/src/config/definitions.ts similarity index 92% rename from packages/lib/src/config/definitions.js rename to packages/lib/src/config/definitions.ts index a21bf40b..2d25e542 100644 --- a/packages/lib/src/config/definitions.js +++ b/packages/lib/src/config/definitions.ts @@ -1,13 +1,11 @@ // Core definitions for the configuration system -"use strict"; - -const classes = require("./classes"); +import * as classes from "./classes"; +import type { PluginInfo } from "../plugin"; /** - * Controller config group for {@link module:lib.ControllerConfig} - * @extends module:lib.ConfigGroup - * @memberof module:lib + * Controller config group for {@link ControllerConfig} + * @extends classes.ConfigGroup */ class ControllerGroup extends classes.ConfigGroup { } ControllerGroup.defaultAccess = ["controller", "host", "control"]; @@ -140,17 +138,15 @@ ControllerGroup.finalize(); /** * Controller Config class - * @extends module:lib.Config - * @memberof module:lib + * @extends classes.Config */ class ControllerConfig extends classes.Config { } ControllerConfig.registerGroup(ControllerGroup); /** - * Host config group for {@link module:lib.HostConfig} - * @extends module:lib.ConfigGroup - * @memberof module:lib + * Host config group for {@link HostConfig} + * @extends classes.ConfigGroup */ class HostGroup extends classes.ConfigGroup {} HostGroup.defaultAccess = ["controller", "host", "control"]; @@ -232,17 +228,15 @@ HostGroup.finalize(); /** * Host Config class - * @extends module:lib.Config - * @memberof module:lib + * @extends classes.Config */ class HostConfig extends classes.Config { } HostConfig.registerGroup(HostGroup); /** - * Instance config group for {@link module:lib.InstanceConfig} - * @extends module:lib.ConfigGroup - * @memberof module:lib + * Instance config group for {@link classes.InstanceConfig} + * @extends classes.ConfigGroup */ class InstanceGroup extends classes.ConfigGroup { } InstanceGroup.defaultAccess = ["controller", "host", "control"]; @@ -272,9 +266,8 @@ InstanceGroup.define({ InstanceGroup.finalize(); /** - * Factorio config group for {@link module:lib.InstanceConfig} - * @extends module:lib.ConfigGroup - * @memberof module:lib + * Factorio config group for {@link classes.InstanceConfig} + * @extends classes.ConfigGroup */ class FactorioGroup extends classes.ConfigGroup { } FactorioGroup.defaultAccess = ["controller", "host", "control"]; @@ -398,8 +391,7 @@ FactorioGroup.finalize(); /** * Instance config class - * @extends module:lib.Config - * @memberof module:lib + * @extends classes.Config */ class InstanceConfig extends classes.Config { } InstanceConfig.registerGroup(InstanceGroup); @@ -407,9 +399,8 @@ InstanceConfig.registerGroup(FactorioGroup); /** - * Control config group for {@link module:lib.ControlConfig} - * @extends module:lib.ConfigGroup - * @memberof module:lib + * Control config group for {@link classes.ControlConfig} + * @extends classes.ConfigGroup */ class ControlGroup extends classes.ConfigGroup {} ControlGroup.defaultAccess = ["control"]; @@ -443,14 +434,13 @@ ControlGroup.finalize(); /** * Control config class - * @extends module:lib.Config - * @memberof module:lib + * @extends classes.Config */ class ControlConfig extends classes.Config { } ControlConfig.registerGroup(ControlGroup); -function validateGroup(pluginInfo, groupName) { +function validateGroup(pluginInfo: PluginInfo, groupName: string) { if (!(pluginInfo[groupName].prototype instanceof classes.PluginConfigGroup)) { throw new Error( `Expected ${groupName} for ${pluginInfo.name} to be a subclass of PluginConfigGroup` @@ -468,9 +458,8 @@ function validateGroup(pluginInfo, groupName) { * Registers the config groups for the provided plugin infos * * @param {Array} pluginInfos - Array of plugin info objects. - * @memberof module:lib */ -function registerPluginConfigGroups(pluginInfos) { +function registerPluginConfigGroups(pluginInfos: PluginInfo[]) { for (let pluginInfo of pluginInfos) { if (pluginInfo.ControllerConfigGroup) { validateGroup(pluginInfo, "ControllerConfigGroup"); @@ -502,8 +491,6 @@ function registerPluginConfigGroups(pluginInfos) { /** * Lock configs from adding more groups and make them usable - * - * @memberof module:lib */ function finalizeConfigs() { ControllerConfig.finalize(); diff --git a/packages/lib/src/data/index.ts b/packages/lib/src/data/index.ts index 46c1ed2a..5fb50974 100644 --- a/packages/lib/src/data/index.ts +++ b/packages/lib/src/data/index.ts @@ -14,3 +14,40 @@ export * from "./messages_instance"; export * from "./messages_mod"; export * from "./messages_user"; export * from "./version"; + +// TODO: Remove/migrate after lib/data is migrated to ts +import type { User } from "../users"; +export type AddressType = "controller" | "host" | "instance" | "control" | "broadcast"; +export interface Serialisable { + jsonSchema: object, + fromJSON(json: any): object, +} + +import type { Address } from "./messages_core"; +export type AddressShorthand = + | "controller" + | { instanceId: number } + | { hostId: number } + | { controlId: number } + | Address + +export class Request { + static type: "request"; + static src: AddressType | AddressType[]; + static dst: AddressType | AddressType[]; + static permission: null | "string" | ((user: User, message: Request) => void); + static plugin?: string; + static jsonSchema?: object; + static fromJSON?: (json: any) => Request; + static Response?: Serialisable; +} + +export class Event { + static type: "event"; + static src: AddressType | AddressType[]; + static dst: AddressType | AddressType[]; + static permission: null | "string" | ((user: User, message: Event) => void); + static plugin?: string; + static jsonSchema?: object; + static fromJSON?: (json: any) => Event; +} diff --git a/packages/lib/src/data/messages_core.js b/packages/lib/src/data/messages_core.js index beda4502..d418a60f 100644 --- a/packages/lib/src/data/messages_core.js +++ b/packages/lib/src/data/messages_core.js @@ -19,10 +19,10 @@ class Address { /** @type {number} */ type; - /** @type {string} */ + /** @type {number} */ id; - /** @type {string|undefined} */ + /** @type {number|undefined} */ requestId; constructor(type, id, requestId) { diff --git a/packages/lib/src/errors.ts b/packages/lib/src/errors.ts index 30b26cf3..54c307c8 100644 --- a/packages/lib/src/errors.ts +++ b/packages/lib/src/errors.ts @@ -6,7 +6,9 @@ /** * Thrown by commands when they fail */ -export class CommandError extends Error { } +export class CommandError extends Error { + code: "CommandError"; +} /** * Thrown from requests sent when an error occured handling it @@ -14,7 +16,7 @@ export class CommandError extends Error { } export class RequestError extends Error { constructor( message: string, - public code?: string, + public code = "RequestError", public stack?: string, ) { super(message); @@ -28,12 +30,22 @@ export class RequestError extends Error { * requests fail due to permission denied. * */ -export class PermissionError extends RequestError { } +export class PermissionError extends RequestError { + constructor( + message: string, + code = "PermissionError", + stack?: string, + ) { + super(message, code, stack); + } + +} /** * Signal for messages that fail validation */ export class InvalidMessage extends Error { + code: "InvalidMessage"; constructor( message: string, public errors: object = null, @@ -45,27 +57,36 @@ export class InvalidMessage extends Error { /** * Thrown from requests when the session it was sent on was lost */ -export class SessionLost extends Error { } +export class SessionLost extends Error { + code: "SessionLost"; +} /** * Thrown when WebSocket authentication failed */ -export class AuthenticationFailed extends Error { } +export class AuthenticationFailed extends Error { + code: "AuthenticationFailed"; +} /** * Errror class for known errors occuring during startup */ -export class StartupError extends Error { } +export class StartupError extends Error { + code: "StartupError"; +} /** * Errors outside of our control */ -export class EnvironmentError extends Error { } +export class EnvironmentError extends Error { + code: "EnvironmentError"; +} /** * Errors caused by plugins */ export class PluginError extends Error { + code: "PluginError"; constructor( public pluginName: string, public original: Error diff --git a/packages/lib/src/factorio/exchange_string.js b/packages/lib/src/factorio/exchange_string.ts similarity index 79% rename from packages/lib/src/factorio/exchange_string.js rename to packages/lib/src/factorio/exchange_string.ts index 249cd3d1..a0d9606f 100644 --- a/packages/lib/src/factorio/exchange_string.js +++ b/packages/lib/src/factorio/exchange_string.ts @@ -1,51 +1,50 @@ "use strict"; -const zlib = require("zlib"); - +import zlib from "zlib"; class MapReaderState { - constructor(buf) { - this.pos = 0; - this.buf = buf; - this.last_position = { x: 0, y: 0 }; - } + pos = 0; + last_position = { x: 0, y: 0 }; + constructor( + public buf: Buffer + ) { } } -function readUInt8(state) { +function readUInt8(state: MapReaderState) { let value = state.buf.readUInt8(state.pos); state.pos += 1; return value; } -function readBool(state) { +function readBool(state: MapReaderState) { let value = readUInt8(state) !== 0; return value; } -function readInt16(state) { +function readInt16(state: MapReaderState) { let value = state.buf.readInt16LE(state.pos); state.pos += 2; return value; } -function readUInt16(state) { +function readUInt16(state: MapReaderState) { let value = state.buf.readUInt16LE(state.pos); state.pos += 2; return value; } -function readInt32(state) { +function readInt32(state: MapReaderState) { let value = state.buf.readInt32LE(state.pos); state.pos += 4; return value; } -function readUInt32(state) { +function readUInt32(state: MapReaderState) { let value = state.buf.readUInt32LE(state.pos); state.pos += 4; return value; } -function readUInt32so(state) { +function readUInt32so(state: MapReaderState) { let value = readUInt8(state); if (value === 0xff) { return readUInt32(state); @@ -54,26 +53,26 @@ function readUInt32so(state) { return value; } -function readFloat(state) { +function readFloat(state: MapReaderState) { let value = state.buf.readFloatLE(state.pos); state.pos += 4; return value; } -function readDouble(state) { +function readDouble(state: MapReaderState) { let value = state.buf.readDoubleLE(state.pos); state.pos += 8; return value; } -function readString(state) { +function readString(state: MapReaderState) { let size = readUInt32so(state); - let data = state.buf.slice(state.pos, state.pos + size).toString("utf-8"); + let data = state.buf.subarray(state.pos, state.pos + size).toString("utf-8"); state.pos += size; return data; } -function readOptional(state, readValue) { +function readOptional(state: MapReaderState, readValue: (state: MapReaderState) => T) { let load = readUInt8(state) !== 0; if (!load) { return null; @@ -81,10 +80,10 @@ function readOptional(state, readValue) { return readValue(state); } -function readArray(state, readItem) { +function readArray(state: MapReaderState, readItem: (state: MapReaderState) => T) { let size = readUInt32so(state); - let array = []; + let array: T[] = []; for (let i = 0; i < size; i++) { let item = readItem(state); array.push(item); @@ -93,10 +92,14 @@ function readArray(state, readItem) { return array; } -function readDict(state, readKey, readValue) { +function readDict( + state: MapReaderState, + readKey: (state: MapReaderState) => K, + readValue: (state: MapReaderState) => V +) { let size = readUInt32so(state); - let mapping = new Map(); + let mapping = new Map(); for (let i = 0; i < size; i++) { let key = readKey(state); let value = readValue(state); @@ -106,7 +109,7 @@ function readDict(state, readKey, readValue) { return mapping; } -function readVersion(state) { +function readVersion(state: MapReaderState) { let major = readUInt16(state); let minor = readUInt16(state); let patch = readUInt16(state); @@ -114,7 +117,7 @@ function readVersion(state) { return [major, minor, patch, developer]; } -function readFrequencySizeRichness(state) { +function readFrequencySizeRichness(state: MapReaderState) { return { frequency: readFloat(state), size: readFloat(state), @@ -122,15 +125,15 @@ function readFrequencySizeRichness(state) { }; } -function readAutoplaceSetting(state) { +function readAutoplaceSetting(state: MapReaderState) { return { treat_missing_as_default: readBool(state), settings: Object.fromEntries(readDict(state, readString, readFrequencySizeRichness)), }; } -function readMapPosition(state) { - let x, y; +function readMapPosition(state: MapReaderState) { + let x: number, y: number; let x_diff = readInt16(state) / 256; if (x_diff === 0x7fff / 256) { x = readInt32(state) / 256; @@ -145,7 +148,7 @@ function readMapPosition(state) { return { x, y }; } -function readBoundingBox(state) { +function readBoundingBox(state: MapReaderState) { return { left_top: readMapPosition(state), right_bottom: readMapPosition(state), @@ -156,7 +159,7 @@ function readBoundingBox(state) { }; } -function readCliffSettings(state) { +function readCliffSettings(state: MapReaderState) { return { name: readString(state), elevation_0: readFloat(state), @@ -165,7 +168,7 @@ function readCliffSettings(state) { }; } -function readMapGenSettings(state) { +function readMapGenSettings(state: MapReaderState) { return { terrain_segmentation: readFloat(state), water: readFloat(state), @@ -184,7 +187,7 @@ function readMapGenSettings(state) { }; } -function readPollution(state) { +function readPollution(state: MapReaderState) { return { enabled: readOptional(state, readBool), diffusion_ratio: readOptional(state, readDouble), @@ -201,24 +204,23 @@ function readPollution(state) { }; } -function readSteeringValue(state) { +function readSteeringValue(state: MapReaderState) { return { radius: readOptional(state, readDouble), separation_factor: readOptional(state, readDouble), separation_force: readOptional(state, readDouble), force_unit_fuzzy_goto_behavior: readOptional(state, readBool), }; - } -function readSteering(state) { +function readSteering(state: MapReaderState) { return { default: readSteeringValue(state), moving: readSteeringValue(state), }; } -function readEnemyEvolution(state) { +function readEnemyEvolution(state: MapReaderState) { return { enabled: readOptional(state, readBool), time_factor: readOptional(state, readDouble), @@ -227,7 +229,7 @@ function readEnemyEvolution(state) { }; } -function readEnemyExpansion(state) { +function readEnemyExpansion(state: MapReaderState) { return { enabled: readOptional(state, readBool), max_expansion_distance: readOptional(state, readUInt32), @@ -245,7 +247,7 @@ function readEnemyExpansion(state) { }; } -function readUnitGroup(state) { +function readUnitGroup(state: MapReaderState) { return { min_group_gathering_time: readOptional(state, readUInt32), max_group_gathering_time: readOptional(state, readUInt32), @@ -263,7 +265,7 @@ function readUnitGroup(state) { }; } -function readPathFinder(state) { +function readPathFinder(state: MapReaderState) { return { fwd2bwd_ratio: readOptional(state, readInt32), goal_pressure_ratio: readOptional(state, readDouble), @@ -301,7 +303,7 @@ function readPathFinder(state) { }; } -function readDifficultySettings(state) { +function readDifficultySettings(state: MapReaderState) { return { recipe_difficulty: readUInt8(state), technology_difficulty: readUInt8(state), @@ -310,7 +312,7 @@ function readDifficultySettings(state) { }; } -function readMapSettings(state) { +function readMapSettings(state: MapReaderState) { return { pollution: readPollution(state), steering: readSteering(state), @@ -323,18 +325,23 @@ function readMapSettings(state) { }; } -/** - * @typedef {object} MapExchangeResult - * @property {array} version - - * Version of Factorio the string was created with. - * @property {object} map_gen_settings - - * Decoded map generator settings in the format the --map-gen-settings - * command line option to Factorio expect. - * @property {object} map_settings - - * Decoded map settings in the format the --map-settings command line option - * to Factorio expects. - * @property {number} checksum - checksum for the exchange string. - */ +export interface MapExchangeData { + /** Version of Factorio the string was created with. */ + version: ReturnType; + unknown: number; + /** + * Decoded map generator settings in the format the --map-gen-settings + * command line option to Factorio expect. + */ + map_gen_settings: ReturnType; + /** + * Decoded map settings in the format the --map-settings command line + * option to Factorio expects. + */ + map_settings: ReturnType; + /** CRC32 checksum for the exchange string. */ + checksum: number; +} /** * Parse a Map Exchange String @@ -343,11 +350,10 @@ function readMapSettings(state) { * data structures that can be fed into the Factorio server when creating a * save to set the map gen settings and the map settings for the save. * - * @param {string} exchangeString - Max Exchange String to parse. - * @returns {MapExchangeResult} Parsed result. - * @memberof module:lib + * @param exchangeString - Max Exchange String to parse. + * @returns Parsed result. */ -function readMapExchangeString(exchangeString) { +export function readMapExchangeString(exchangeString: string) { exchangeString = exchangeString.replace(/[ \t\n\r]+/g, ""); if (!/>>>[0-9a-zA-Z\/+]+={0,3}<<; /** * Gracefully disconnect @@ -272,7 +282,7 @@ class WebSocketBaseConnector extends BaseConnector { this.setClosing(); this.send(new libData.MessageDisconnect("prepare")); - let timeout; + let timeout: NodeJS.Timeout; let waitTimeout = new Promise(resolve => { timeout = setTimeout(resolve, 10000); }); // Make Configurable? try { await Promise.race([ @@ -314,25 +324,27 @@ class WebSocketBaseConnector extends BaseConnector { * Connector for controller clients * * @extends module:lib.WebSocketBaseConnector - * @memberof module:lib */ -class WebSocketClientConnector extends WebSocketBaseConnector { - constructor(url, maxReconnectDelay, tlsCa = null) { - super(undefined, new libData.Address(libData.Address.controller, 0)); +export abstract class WebSocketClientConnector extends WebSocketBaseConnector { + _reconnectId: NodeJS.Timeout | null = null; + _sessionToken: string | null = null; + _sessionTimeout: number | null = null; + _startedResuming: number | null = null; + _backoff: ExponentialBackoff; - this._url = url; + constructor( + protected _url: string, + maxReconnectDelay: number, + protected _tlsCa = null + ) { + super(undefined, new libData.Address(libData.Address.controller, 0)); this._backoff = new ExponentialBackoff({ max: maxReconnectDelay }); - this._tlsCa = tlsCa; // The following states are used in the client connector // closed: Not connected // connecting: Attempting to connect to server. // resuming: Attempting to resume an existing connection to the server. // connected: Connection is online - this._reconnectId = null; - this._sessionToken = null; - this._sessionTimeout = null; - this._startedResuming = null; } _reset() { @@ -360,9 +372,9 @@ class WebSocketClientConnector extends WebSocketBaseConnector { * Used in the register function in order to send the handshake messages * over the WebSocket. * - * @param {module:lib.Message} message - Message to send. + * @param message - Message to send. */ - sendHandshake(message) { + sendHandshake(message: libData.Message) { this._check("connecting", "resuming"); this._sendInternal(message); } @@ -372,10 +384,10 @@ class WebSocketClientConnector extends WebSocketBaseConnector { * * Sends a close frame and disconnects the connector. * - * @param {number} code - WebSocket close code. - * @param {string} reason - WebSocket close reason. + * @param code - WebSocket close code. + * @param reason - WebSocket close reason. */ - async close(code, reason) { + async close(code: number, reason: string) { if (this._state === "closed") { return; } @@ -443,7 +455,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { this._socket = new WebSocket(url); } else { - let options = {}; + let options: { ca?: string } = {}; if (this._tlsCa) { options.ca = this._tlsCa; } this._socket = new WebSocket(url, options); } @@ -470,7 +482,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { ) { logger.error("Connector | Session timed out trying to resume"); this._reset(); - if (this._disconnecting) { + if (this._closing) { this._reset(); this.emit("close"); } else { @@ -485,11 +497,11 @@ class WebSocketClientConnector extends WebSocketBaseConnector { this._reconnectId = null; this._doConnect(); }, delay); - this._reconnectTime = Date.now() + delay; } _attachSocketHandlers() { - this._socket.onclose = event => { + // TODO type event + this._socket.onclose = (event: any) => { const previousState = this._state; // Authentication failed @@ -532,7 +544,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { } }; - this._socket.onerror = event => { + this._socket.onerror = (event: any) => { // It's assumed that close is always called by ws let code = !event.error ? "" : `, code: ${event.error.code}`; let message = `Connector | Socket error: ${event.message || "unknown error"}${code}`; @@ -561,7 +573,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { }; // Handle messages - this._socket.onmessage = event => { + this._socket.onmessage = (event: any) => { let message = this._parseMessage(event.data); if (!message) { return; @@ -578,9 +590,9 @@ class WebSocketClientConnector extends WebSocketBaseConnector { }; } - _processHandshake(message) { - - let { type, data } = message; + _processHandshake(message: libData.Message) { + // TODO infer data from Message type. + let { type, data } = message as libData.Message & { data: any }; if (type === "hello") { logger.verbose(`Connector | received hello from controller version ${data.version}`); this.emit("hello", data); @@ -621,7 +633,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { } else if (type === "invalidate") { logger.warn("Connector | session invalidated by controller"); - if (this._disconnecting) { + if (this._closing) { this._reset(); this.emit("close"); } else { @@ -638,19 +650,18 @@ class WebSocketClientConnector extends WebSocketBaseConnector { * This function is expected to be overriden by sub-classes and send the * register message over the socket when invoked. */ - register() { - throw Error("Abstract function"); - } + abstract register(): void; } /** * Connector for in-memory local links * * @extends events.EventEmitter - * @memberof module:lib */ -class VirtualConnector extends BaseConnector { - constructor(src, dst) { +export class VirtualConnector extends BaseConnector { + other: VirtualConnector; + + constructor(src: libData.Address, dst: libData.Address) { super(src, dst); this.other = this; @@ -662,11 +673,14 @@ class VirtualConnector extends BaseConnector { * Creates two virtual connectors that are liked to each othes such that * a message sent on one is received by the other. * - * @param {module:lib.Address} src - Source for obverse connector - * @param {module:lib.Address} dst - destination for obverse connector - * @returns {Array} two virtuarl connectors. + * @param src - Source for obverse connector + * @param dst - destination for obverse connector + * @returns two virtuarl connectors. */ - static makePair(src, dst) { + static makePair( + src: libData.Address, + dst: libData.Address, + ): [VirtualConnector, VirtualConnector] { let obverse = new this(src, dst); let reverse = new this(dst, src); obverse.other = reverse; @@ -677,16 +691,9 @@ class VirtualConnector extends BaseConnector { /** * Send a message to the other end of the connector * - * @param {module:lib.Message} message - Message type to send. + * @param message - Message type to send. */ - send(message) { + send(message: libData.Message) { this.other.emit("message", message); } } - -module.exports = { - BaseConnector, - WebSocketBaseConnector, - WebSocketClientConnector, - VirtualConnector, -}; diff --git a/packages/lib/src/link/index.ts b/packages/lib/src/link/index.ts index 5f873f85..e13d1d8b 100644 --- a/packages/lib/src/link/index.ts +++ b/packages/lib/src/link/index.ts @@ -5,7 +5,3 @@ */ export * from "./link" export * from "./connectors" - -// migrate: Allow info for plugins from before link refactor to load. -export class Event {} -export class Request {} diff --git a/packages/lib/src/link/link.js b/packages/lib/src/link/link.ts similarity index 65% rename from packages/lib/src/link/link.js rename to packages/lib/src/link/link.ts index a9aacc42..f62411a6 100644 --- a/packages/lib/src/link/link.js +++ b/packages/lib/src/link/link.ts @@ -1,11 +1,45 @@ // Implementation of Link class -"use strict"; +import * as libData from "../data"; +import * as libErrors from "../errors"; +import { logger } from "../logging"; +import * as libSchema from "../schema"; +import { dataClasses } from "./messages"; +import { BaseConnector, WebSocketBaseConnector } from "./connectors"; +import { strict as assert } from "assert"; +import type { PluginInfo } from "../plugin"; -const libData = require("../data"); -const libErrors = require("../errors"); -const { logger } = require("../logging"); -const libSchema = require("../schema"); -const { dataClasses } = require("./messages"); +interface RequestEntry { + Request: typeof libData.Request; + name: string, + allowedSrcTypes: Set; + allowedDstTypes: Set; + requestFromJSON: (json: any) => libData.Request; + Response?: any; + responseFromJSON: (json: any) => any; +} + +interface PendingRequest { + promise: Promise; + request: RequestEntry; + resolve: (result: any) => void; + reject: (err: Error) => void; + dst: libData.Address; +} +interface ForwardedRequest { + origin: Link; + src: libData.Address; + dst: libData.Address; +} + +interface EventEntry { + Event: typeof libData.Event; + name: string, + allowedSrcTypes: Set; + allowedDstTypes: Set; + eventFromJSON: (json: any) => libData.Event; +} +type RequestHandler = (request: libData.Request, src: libData.Address, dst: libData.Address) => any; +type EventHandler = (request: libData.Event, src: libData.Address, dst: libData.Address) => any; // Some definitions for the terminology used here: // link: Either side of a controller - client connection @@ -16,23 +50,23 @@ const { dataClasses } = require("./messages"); /** * Common interface for server and client connections - * - * @memberof module:lib */ -class Link { - constructor(connector) { - this.connector = connector; - this.router = undefined; - this.validateSent = true; +export class Link { + router = undefined; + validateSent = true; - this._requestHandlers = new Map(); - this._requestFallbacks = new Map(); - this._eventHandlers = new Map(); - this._eventSnoopers = new Map(); - this._pendingRequests = new Map(); - this._forwardedRequests = new Map(); - this._nextRequestId = 1; + _requestHandlers = new Map(); + _requestFallbacks = new Map(); + _eventHandlers = new Map(); + _eventSnoopers = new Map(); + _pendingRequests = new Map(); + _forwardedRequests = new Map(); + _nextRequestId = 1; + constructor( + public connector: BaseConnector + ) { + // @ts-ignore: TODO Remove when lib/data is migrated to ts this.handle(libData.PingRequest, () => {}); // Process messages received by the connector @@ -74,11 +108,12 @@ class Link { }); } - _clearPendingRequests(err) { + _clearPendingRequests(err: Error & { code?: string }) { for (let pending of this._pendingRequests.values()) { pending.reject(err); } for (let pending of this._forwardedRequests.values()) { + assert(pending.origin.connector instanceof WebSocketBaseConnector); if (pending.origin.connector.hasSession) { pending.origin.connector.sendResponseError( new libData.ResponseError(err.message, err.code), pending.src, pending.dst @@ -95,10 +130,12 @@ class Link { * Validates and invokes the handler and/or waiters for a message that has * been received. An unhandled message is considered to be an error. * - * @param {module:lib.Message} message - Message to process. - * @throws {module:lib.InvalidMessage} if the message is invalid or not handled. + * @param message - Message to process. + * @throws {libErrors.InvalidMessage} if the message is invalid or not handled. */ - _processMessage(message) { + _processMessage( + message: libData.MessageRequest | libData.MessageResponse | libData.MessageResponseError | libData.MessageEvent + ) { if (!["request", "response", "responseError", "event"].includes(message.type)) { throw new libErrors.InvalidMessage(`Unhandled message type ${message.type}`); } @@ -115,10 +152,11 @@ class Link { } } - if (message.type === "event" && this._eventSnoopers.has(entry.Event)) { - let handler = this._eventSnoopers.get(entry.Event); - handler(entry.eventFromJSON(message.data), message.src, message.dst).catch(err => { - logger.error(`Unexpected error snooping ${message.name}:\n${err.stack}`); + if (message.type === "event" && this._eventSnoopers.has((entry as EventEntry).Event)) { + let handler = this._eventSnoopers.get((entry as EventEntry).Event); + let event = message as libData.MessageEvent; + handler((entry as EventEntry).eventFromJSON(event.data), event.src, event.dst).catch((err: Error) => { + logger.error(`Unexpected error snooping ${event.name}:\n${err.stack}`); }); } @@ -131,7 +169,11 @@ class Link { } if (message.type === "request") { - this._processRequest(message, entry, this._requestHandlers.get(entry.Request)); + this._processRequest( + message as libData.MessageRequest, + entry as RequestEntry, + this._requestHandlers.get((entry as RequestEntry).Request) + ); } else if (message.type === "response") { this._processResponse(message); @@ -140,7 +182,10 @@ class Link { this._processResponseError(message); } else if (message.type === "event") { - this._processEvent(message, entry); + this._processEvent( + message as libData.MessageEvent, + entry as EventEntry + ); } } @@ -149,12 +194,14 @@ class Link { * * Should be overridden by sub-classes to validate messages received. * - * @param {module:lib.Message} message - Message to check. - * @throws {module:lib.InvalidMessage} if the message is invalid. + * @param message - Message to check. + * @throws {libErrors.InvalidMessage} if the message is invalid. */ - validateIngress(message) { } + validateIngress(message: libData.Message) { } - _validateMessage(message) { + _validateMessage( + message: libData.MessageRequest | libData.MessageResponse | libData.MessageResponseError | libData.MessageEvent + ) { try { if (message.src.type === libData.Address.broadcast) { throw new libErrors.InvalidMessage("Message src may not be broadcast"); @@ -168,7 +215,8 @@ class Link { } if (message.type === "request") { - let entry = this.constructor._requestsByName.get(message.name); + assert(message instanceof libData.MessageRequest); + let entry = Link._requestsByName.get(message.name); if (!entry) { let err = new libErrors.InvalidMessage(`Unrecognized request ${message.name}`); this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src); @@ -193,7 +241,8 @@ class Link { return undefined; } else if (message.type === "event") { - let entry = this.constructor._eventsByName.get(message.name); + assert(message instanceof libData.MessageEvent); + let entry = Link._eventsByName.get(message.name); if (!entry) { throw new libErrors.InvalidMessage(`Unrecognized event ${message.name}`); } @@ -222,26 +271,29 @@ class Link { * Called when an request or event is received by a control connector. * Should be overridden by sub-classes to validate messages received. * - * @param {module:lib.Message} message - Message to check. - * @param {object} entry - Request or Event entry for this Message. - * @throws {module:lib.PermissionError} if unauthorized. + * @param message - Message to check. + * @param entry - Request or Event entry for this Message. + * @throws {libErrors.PermissionError} if unauthorized. */ - validatePermission(message, entry) { } + validatePermission(message: libData.Message, entry: RequestEntry | EventEntry) { } - _routeMessage(message, entry) { + _routeMessage(message: libData.Message, entry?: RequestEntry | EventEntry) { if (!this.router) { let err = new libErrors.InvalidMessage( - `Received message addressed to ${message.dst} but this link does not route messages` + `Received message addressed to ${(message as libData.MessageRequest).dst} but this link `+ + `does not route messages` ); if (message.type === "request") { this.connector.sendResponseError( - new libData.ResponseError(err.message, err.code), message.src + new libData.ResponseError(err.message, err.code), (message as libData.MessageRequest).src ); } throw err; } - let fallback = message.type === "request" ? this._requestFallbacks.get(entry.Request) : undefined; + let fallback = + message.type === "request" ? this._requestFallbacks.get((entry as RequestEntry).Request) : undefined + ; if (this.router.forwardMessage(this, message, Boolean(fallback))) { return; } @@ -251,10 +303,20 @@ class Link { if (message.type !== "request") { throw new Error(`Router requested fallback handling of unsupported message type ${message.type}`); } - this._processRequest(message, entry, fallback, message.dst); + this._processRequest( + message as libData.MessageRequest, + entry as RequestEntry, + fallback, + (message as libData.MessageRequest).dst + ); } - _processRequest(message, entry, handler, spoofedSrc) { + _processRequest( + message: libData.MessageRequest, + entry: RequestEntry, + handler: RequestHandler, + spoofedSrc?: libData.Address + ) { if (!handler) { this.connector.sendResponseError( new libData.ResponseError(`No handler for ${entry.Request.name}`), message.src @@ -262,7 +324,7 @@ class Link { return; } - let response; + let response: any; try { response = handler(entry.requestFromJSON(message.data), message.src, message.dst); } catch (err) { @@ -280,7 +342,7 @@ class Link { } response.then( - result => { + (result: any) => { if (this.validateSent) { if (entry.Response) { entry.responseFromJSON(JSON.parse(JSON.stringify(result))); @@ -291,7 +353,7 @@ class Link { this.connector.sendResponse(result, message.src, spoofedSrc); } ).catch( - err => { + (err: Error & { code?: string }) => { if (err instanceof libErrors.InvalidMessage) { logger.error(err.message); if (err.errors) { @@ -307,7 +369,7 @@ class Link { ); } - _processResponse(message) { + _processResponse(message: libData.MessageResponse) { let pending = this._pendingRequests.get(message.dst.requestId); if (!pending) { throw new libErrors.InvalidMessage( @@ -327,7 +389,7 @@ class Link { } } - _processResponseError(message) { + _processResponseError(message: libData.MessageResponseError) { let pending = this._pendingRequests.get(message.dst.requestId); if (!pending) { throw new libErrors.InvalidMessage( @@ -338,13 +400,15 @@ class Link { pending.reject(new libErrors.RequestError(message.data.message, message.data.code, message.data.stack)); } - _processEvent(message, entry) { + _processEvent(message: libData.MessageEvent, entry: EventEntry) { let handler = this._eventHandlers.get(entry.Event); if (!handler) { throw new libErrors.InvalidMessage(`Unhandled event ${message.name}`); } - handler(entry.eventFromJSON(message.data), message.src, message.dst).catch(err => { + handler( + entry.eventFromJSON(message.data), message.src, message.dst + ).catch((err: Error) => { logger.error(`Unexpected error handling ${message.name}:\n${err.stack}`); }); } @@ -368,38 +432,46 @@ class Link { /** * Send a request or event to the other side of this link - * @param {*} requestOrEvent - Request or event to send - * @returns {Promise<*>|undefined} + * @param requestOrEvent - Request or event to send + * @returns * Promise that resolves to the response if a request was sent or * undefined if it was an event. */ - send(requestOrEvent) { + send(requestOrEvent: libData.Request): Promise; + send(requestOrEvent: libData.Event): void; + send(requestOrEvent: libData.Request | libData.Event): Promise | void { return this.sendTo(this.connector.dst, requestOrEvent); } /** * Send a request or event to the given address * - * @param {*|module:lib.Address} destination - Where to send it - * @param {*} requestOrEvent - Request or event to send - * @returns {Promise<*>|undefined} + * @param destination - Where to send it + * @param requestOrEvent - Request or event to send + * @returns * Promise that resolves to the response if a request was sent or * undefined if it was an event. */ - sendTo(destination, requestOrEvent) { + sendTo(destination: libData.AddressShorthand, requestOrEvent: libData.Request): Promise; + sendTo(destination: libData.AddressShorthand, requestOrEvent: libData.Event): void; + sendTo( + destination: libData.AddressShorthand, + requestOrEvent: libData.Request | libData.Event, + ): Promise | void { let dst = libData.Address.fromShorthand(destination); - if (requestOrEvent.constructor.type === "request") { + // TODO investigate if these types can be inferred + if ((requestOrEvent.constructor as any).type === "request") { return this.sendRequest(requestOrEvent, dst); } - if (requestOrEvent.constructor.type === "event") { + if ((requestOrEvent.constructor as any).type === "event") { return this.sendEvent(requestOrEvent, dst); } - throw Error(`Expected request or event but got type ${requestOrEvent.constructor.type}`); + throw Error(`Expected request or event but got type ${(requestOrEvent.constructor as any).type}`); } - sendRequest(request, dst) { - let entry = this.constructor._requestsByClass.get(request.constructor); + sendRequest(request: libData.Request, dst: libData.Address) { + let entry = Link._requestsByClass.get(request.constructor as typeof libData.Request); if (!entry) { throw new Error(`Attempt to send unregistered Request ${request.constructor.name}`); } @@ -407,22 +479,22 @@ class Link { entry.requestFromJSON(JSON.parse(JSON.stringify(request))); } - let pending = { - request: entry, - dst, - }; - pending.promise = new Promise((resolve, reject) => { - pending.resolve = resolve; - pending.reject = reject; + let resolve: (value: unknown) => void; + let reject: (reason: any) => void; + let promise = new Promise((res, rej) => { + resolve = res; + reject = rej; }); let requestId = this._nextRequestId; this._nextRequestId += 1; - this._pendingRequests.set(requestId, pending); + this._pendingRequests.set( + requestId, { request: entry, promise, resolve, reject, dst, } + ); this.connector.sendRequest(request, requestId, dst); - return pending.promise; + return promise; } - forwardRequest(message, origin) { + forwardRequest(message: libData.MessageRequest, origin: Link) { let pending = { origin, src: message.src, @@ -432,8 +504,8 @@ class Link { this.connector.send(message); } - sendEvent(event, dst) { - let entry = this.constructor._eventsByClass.get(event.constructor); + sendEvent(event: libData.Event, dst: libData.Address) { + let entry = Link._eventsByClass.get(event.constructor as typeof libData.Event); if (!entry) { throw new Error(`Attempt to send unregistered Event ${event.constructor.name}`); } @@ -444,18 +516,23 @@ class Link { this.connector.sendEvent(event, dst); } - handle(Class, handler) { + handle(Class: typeof libData.Request, handler: RequestHandler): void; + handle(Class: typeof libData.Event, handler: EventHandler): void; + handle( + Class: typeof libData.Request | typeof libData.Event, + handler: RequestHandler | EventHandler, + ) { if (Class.type === "request") { this.handleRequest(Class, handler); } else if (Class.type === "event") { this.handleEvent(Class, handler); } else { - throw new Error(`Class ${Class.name} has unrecognized type ${Class.type}`); + throw new Error(`Class ${(Class as any).name} has unrecognized type ${(Class as any).type}`); } } - handleRequest(Request, handler) { - let entry = this.constructor._requestsByClass.get(Request); + handleRequest(Request: typeof libData.Request, handler: RequestHandler) { + let entry = Link._requestsByClass.get(Request); if (!entry) { throw new Error(`Unregistered Request class ${Request.name}`); } @@ -465,8 +542,8 @@ class Link { this._requestHandlers.set(Request, handler); } - fallbackRequest(Request, handler) { - let entry = this.constructor._requestsByClass.get(Request); + fallbackRequest(Request: typeof libData.Request, handler: RequestHandler) { + let entry = Link._requestsByClass.get(Request); if (!entry) { throw new Error(`Unregistered Request class ${Request.name}`); } @@ -476,8 +553,8 @@ class Link { this._requestFallbacks.set(Request, handler); } - handleEvent(Event, handler) { - let entry = this.constructor._eventsByClass.get(Event); + handleEvent(Event: typeof libData.Event, handler: EventHandler) { + let entry = Link._eventsByClass.get(Event); if (!entry) { throw new Error(`Unregistered Event class ${Event.name}`); } @@ -487,8 +564,8 @@ class Link { this._eventHandlers.set(Event, handler); } - snoopEvent(Event, handler) { - let entry = this.constructor._eventsByClass.get(Event); + snoopEvent(Event: typeof libData.Event, handler: EventHandler) { + let entry = Link._eventsByClass.get(Event); if (!entry) { throw new Error(`Unregistered Event class ${Event.name}`); } @@ -498,23 +575,23 @@ class Link { this._eventSnoopers.set(Event, handler); } - static register(Class) { + static register(Class: typeof libData.Request | typeof libData.Event) { if (Class.type === "request") { this.registerRequest(Class); } else if (Class.type === "event") { this.registerEvent(Class); } else { - throw new Error(`Data class ${Class.name} has unknown type ${Class.type}`); + throw new Error(`Data class ${(Class as any).name} has unknown type ${(Class as any).type}`); } } - static requestFromJSON(Request, name) { + static requestFromJSON(Request: typeof libData.Request, name: string) { if (Request.fromJSON) { if (!Request.jsonSchema) { throw new Error(`Request ${name} has static fromJSON but is missing static jsonSchema`); } - let validate = libSchema.compile(Request.jsonSchema); - return (json) => { + let validate = libSchema.compile(Request.jsonSchema as any); + return (json: any) => { if (!validate(json)) { throw new libErrors.InvalidMessage(`Request ${name} failed validation`, validate.errors); } @@ -527,7 +604,7 @@ class Link { return () => new Request(); } - static responseFromJSON(Response, name) { + static responseFromJSON(Response: libData.Serialisable, name: string) { if (!Response.jsonSchema) { throw new Error(`Response for Request ${name} is missing static jsonSchema`); } @@ -535,8 +612,8 @@ class Link { throw new Error(`Response for Request ${name} is missing static fromJSON`); } - let validate = libSchema.compile(Response.jsonSchema); - return (json) => { + let validate = libSchema.compile(Response.jsonSchema as any); + return (json: any) => { if (!validate(json)) { throw new libErrors.InvalidMessage( `Response for request ${name} failed validation`, validate.errors @@ -546,7 +623,7 @@ class Link { }; } - static allowedTypes(types, name, side) { + static allowedTypes(types: libData.AddressType | libData.AddressType[], name: string, side: "src" | "dst") { if (types === undefined) { throw new Error(`Missing ${side} specification in ${name}`); } @@ -555,7 +632,7 @@ class Link { types = [types]; } - let allowed = new Set(); + let allowed = new Set(); for (let type of types) { let id = libData.Address[type]; if (typeof id !== "number") { @@ -569,21 +646,22 @@ class Link { return allowed; } - static _requestsByName = new Map(); - static _requestsByClass = new Map(); + static _requestsByName = new Map(); + static _requestsByClass = new Map(); - static registerRequest(Request) { + static registerRequest(Request: typeof libData.Request) { const name = Request.plugin ? `${Request.plugin}:${Request.name}` : Request.name; if (this._requestsByName.has(name)) { throw new Error(`Request ${name} is already registered`); } - let entry = { + let entry: RequestEntry = { Request, name, requestFromJSON: this.requestFromJSON(Request, name), allowedSrcTypes: this.allowedTypes(Request.src, name, "src"), allowedDstTypes: this.allowedTypes(Request.dst, name, "dst"), + responseFromJSON: (_json: any) => undefined, }; if ( @@ -597,21 +675,19 @@ class Link { if (Response) { entry.Response = Response; entry.responseFromJSON = this.responseFromJSON(Response, name); - } else { - entry.responseFromJSON = () => undefined; } this._requestsByName.set(name, entry); this._requestsByClass.set(Request, entry); } - static eventFromJSON(Event, name) { + static eventFromJSON(Event: typeof libData.Event, name: string) { if (Event.fromJSON) { if (!Event.jsonSchema) { throw new Error(`Event ${name} has static fromJSON but is missing static jsonSchema`); } - let validate = libSchema.compile(Event.jsonSchema); - return (json) => { + let validate = libSchema.compile(Event.jsonSchema as any); + return (json: any) => { if (!validate(json)) { throw new libErrors.InvalidMessage(`Event ${name} failed validation`, validate.errors); } @@ -621,13 +697,13 @@ class Link { if (Event.jsonSchema) { throw new Error(`Event ${name} has static jsonSchema but is missing static fromJSON`); } - return () => new Event(); + return (json: any) => new Event(); } - static _eventsByName = new Map(); - static _eventsByClass = new Map(); + static _eventsByName = new Map(); + static _eventsByClass = new Map(); - static registerEvent(Event) { + static registerEvent(Event: typeof libData.Event) { const name = Event.plugin ? `${Event.plugin}:${Event.name}` : Event.name; if (this._eventsByName.has(name)) { throw new Error(`Event ${name} is already registered`); @@ -654,10 +730,11 @@ class Link { } for (let Class of dataClasses) { + // @ts-ignore: TODO Remove when lib/data is migrated to ts Link.register(Class); } -function registerPluginMessages(pluginInfos) { +export function registerPluginMessages(pluginInfos: PluginInfo[]) { for (let pluginInfo of pluginInfos) { for (let Class of pluginInfo.messages || []) { if (Class.plugin !== pluginInfo.name) { @@ -670,9 +747,3 @@ function registerPluginMessages(pluginInfos) { } } } - - -module.exports = { - Link, - registerPluginMessages, -}; diff --git a/packages/lib/src/link/messages.js b/packages/lib/src/link/messages.ts similarity index 85% rename from packages/lib/src/link/messages.js rename to packages/lib/src/link/messages.ts index 6a6acf5c..1566c1de 100644 --- a/packages/lib/src/link/messages.js +++ b/packages/lib/src/link/messages.ts @@ -1,15 +1,18 @@ "use strict"; -const core = require("../data/messages_core"); -const controller = require("../data/messages_controller"); -const host = require("../data/messages_host"); -const instance = require("../data/messages_instance"); -const mod = require("../data/messages_mod"); -const user = require("../data/messages_user"); +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"; -// Event and Request payloads registered with the Link class. Used to decode -// the data payload of incomming requsts and event messages into instances of -// their proper classes. -const dataClasses = [ +/** + * Event and Request payloads registered with the Link class. Used to decode + * the data payload of incomming requsts and event messages into instances of + * their proper classes. + * @internal + */ +export const dataClasses = [ core.PingRequest, controller.ControllerConfigGetRequest, @@ -107,5 +110,3 @@ const dataClasses = [ user.AccountUpdateEvent, user.UserUpdateEvent, ]; - -module.exports = { dataClasses }; diff --git a/packages/lib/src/shared_commands.ts b/packages/lib/src/shared_commands.ts index ada6ac4a..5635f1ad 100644 --- a/packages/lib/src/shared_commands.ts +++ b/packages/lib/src/shared_commands.ts @@ -135,7 +135,7 @@ export async function handleConfigCommand( let command = args._[1]; if (command === "list") { - for (let GroupClass of (instance as any).constructor.groups.values()) { + for (let GroupClass of (instance.constructor as typeof libConfig.Config).groups.values()) { for (let def of GroupClass.definitions.values()) { let value = instance.get(def.fullName); print(`${def.fullName} ${JSON.stringify(value)}`); diff --git a/test/lib/link/connector.js b/test/lib/link/connector.js index e589af21..bbfbf39c 100644 --- a/test/lib/link/connector.js +++ b/test/lib/link/connector.js @@ -37,15 +37,6 @@ describe("lib/link/connectors", function() { let testConnector = new lib.WebSocketClientConnector("url", 1); testConnector._socket = new mock.MockSocket(); - describe(".register()", function() { - it("is abstract", function() { - assert.throws( - () => testConnector.register(), - new Error("Abstract function") - ); - }); - }); - describe(".sendHandshake()", function() { it("calls send on the socket", function() { testConnector._state = "connecting"; From 2a52a82fb7d7f4eaeb3d0a2daf3601f4f1792b8c Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 10 Aug 2023 23:11:00 +0200 Subject: [PATCH 08/13] Port message types to TypeScript --- packages/controller/src/ControlConnection.js | 12 +- packages/ctl/ctl.js | 11 +- packages/ctl/src/commands.js | 12 +- packages/host/src/Host.js | 2 +- packages/host/src/Instance.js | 2 +- packages/host/src/server.js | 16 +- packages/lib/package.json | 3 +- packages/lib/scripts/compile_validator.js | 6 +- packages/lib/src/data/composites.js | 35 - packages/lib/src/data/composites.ts | 34 + packages/lib/src/data/index.ts | 10 +- packages/lib/src/data/message_validate.d.ts | 6 +- packages/lib/src/data/messages_controller.js | 384 ----- packages/lib/src/data/messages_controller.ts | 247 +++ packages/lib/src/data/messages_core.js | 890 ----------- packages/lib/src/data/messages_core.ts | 629 ++++++++ packages/lib/src/data/messages_host.js | 243 --- packages/lib/src/data/messages_host.ts | 157 ++ packages/lib/src/data/messages_instance.js | 1335 ----------------- packages/lib/src/data/messages_instance.ts | 916 +++++++++++ packages/lib/src/data/messages_mod.js | 474 ------ packages/lib/src/data/messages_mod.ts | 324 ++++ packages/lib/src/data/messages_user.js | 708 --------- packages/lib/src/data/messages_user.ts | 472 ++++++ packages/lib/src/link/link.ts | 14 +- .../web_ui/src/components/CreateSaveModal.jsx | 6 +- .../src/components/LoadScenarioModal.jsx | 6 +- packages/web_ui/src/components/LogConsole.jsx | 2 +- packages/web_ui/src/util/websocket.jsx | 2 +- test/integration/clusterio.js | 10 +- test/lib/command.js | 2 +- 31 files changed, 2846 insertions(+), 4124 deletions(-) delete mode 100644 packages/lib/src/data/composites.js create mode 100644 packages/lib/src/data/composites.ts delete mode 100644 packages/lib/src/data/messages_controller.js create mode 100644 packages/lib/src/data/messages_controller.ts delete mode 100644 packages/lib/src/data/messages_core.js create mode 100644 packages/lib/src/data/messages_core.ts delete mode 100644 packages/lib/src/data/messages_host.js create mode 100644 packages/lib/src/data/messages_host.ts delete mode 100644 packages/lib/src/data/messages_instance.js create mode 100644 packages/lib/src/data/messages_instance.ts delete mode 100644 packages/lib/src/data/messages_mod.js create mode 100644 packages/lib/src/data/messages_mod.ts delete mode 100644 packages/lib/src/data/messages_user.js create mode 100644 packages/lib/src/data/messages_user.ts diff --git a/packages/controller/src/ControlConnection.js b/packages/controller/src/ControlConnection.js index d14bb5e7..ee446c8f 100644 --- a/packages/controller/src/ControlConnection.js +++ b/packages/controller/src/ControlConnection.js @@ -248,7 +248,7 @@ class ControlConnection extends BaseConnection { async handleHostGenerateTokenRequest(message) { let hostId = message.host_id; - if (hostId === null) { + if (hostId === undefined) { hostId = Math.random() * 2**31 | 0; } return this._controller.generateHostToken(hostId); @@ -279,7 +279,7 @@ class ControlConnection extends BaseConnection { instance.config.get("instance.name"), instance.id, instance.config.get("instance.assigned_host"), - instance.game_port || null, + instance.game_port || undefined, instance.status, ); } @@ -290,8 +290,8 @@ class ControlConnection extends BaseConnection { list.push(new lib.InstanceDetails( instance.config.get("instance.name"), instance.id, - instance.config.get("instance.assigned_host"), - instance.game_port || null, + instance.config.get("instance.assigned_host") || undefined, + instance.game_port || undefined, instance.status, )); } @@ -311,8 +311,8 @@ class ControlConnection extends BaseConnection { new lib.InstanceDetails( instance.config.get("instance.name"), instance.id, - instance.config.get("instance.assigned_host"), - instance.game_port || null, + instance.config.get("instance.assigned_host") || undefined, + instance.game_port || undefined, instance.status, ) )); diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index 07c16fbc..4bdb33c5 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -83,7 +83,7 @@ class Control extends lib.Link { controller = false, hostIds = [], instanceIds = [], - maxLevel = null, + maxLevel = undefined, }) { await this.send( new lib.LogSetSubscriptionsRequest( @@ -286,7 +286,11 @@ async function startControl() { process.exitCode = 1; } else if (err instanceof lib.RequestError) { - logger.error(`Error sending request:\n${err.stack}`); + if (err.stack) { + logger.error(`Error sending request:\n${err.stack}`); + } else { + logger.error(`Error sending request: ${err.message}`); + } process.exitCode = 1; } else { @@ -315,6 +319,9 @@ I version of clusterio. Expect things to break. I ` ); startControl().catch(err => { + if (err.errors) { + logger.fatal(JSON.stringify(err.errors, null, 4)); + } if (!(err instanceof lib.StartupError)) { logger.fatal(` +------------------------------------------------------------+ diff --git a/packages/ctl/src/commands.js b/packages/ctl/src/commands.js index 35e675be..f3c5a9d1 100644 --- a/packages/ctl/src/commands.js +++ b/packages/ctl/src/commands.js @@ -237,9 +237,9 @@ hostCommands.add(new lib.Command({ yargs.option("id", { type: "number", nargs: 1, describe: "Host id" }); }], handler: async function(args, control) { - let hostId = typeof args.id === "number" ? args.id : null; + let hostId = typeof args.id === "number" ? args.id : undefined; let response = await control.send(new lib.HostGenerateTokenRequest(hostId)); - print(response.token); + print(response); }, })); @@ -480,9 +480,9 @@ instanceCommands.add(new lib.Command({ })); async function loadMapSettings(args) { - let seed = args.seed !== undefined ? args.seed : null; - let mapGenSettings = null; - let mapSettings = null; + let seed = args.seed; + let mapGenSettings; + let mapSettings; if (args.mapExchangeString) { let parsed = lib.readMapExchangeString(args.mapExchangeString); mapGenSettings = parsed.map_gen_settings; @@ -1551,7 +1551,7 @@ logCommands.add(new lib.Command({ "controller": { describe: "Query log of the controller", nargs: 0, type: "boolean", default: false }, "host": { describe: "Query log of given host", nargs: 1, type: "string", default: null }, "instance": { describe: "Query log of given instance", nargs: 1, type: "string", default: null }, - "max-level": { describe: "Maximum log level to return", nargs: 1, type: "string", default: null }, + "max-level": { describe: "Maximum log level to return", nargs: 1, type: "string", default: undefined }, "limit": { describe: "Max number of entries to return", nargs: 1, type: "number", default: 1000 }, "start": { describe: "Limit from the start instead of the end", nargs: 0, type: "boolean", default: false }, }); diff --git a/packages/host/src/Host.js b/packages/host/src/Host.js index c92ddc0b..01852403 100644 --- a/packages/host/src/Host.js +++ b/packages/host/src/Host.js @@ -457,7 +457,7 @@ class Host extends lib.Link { let instanceConnection = this.instanceConnections.get(instanceId); this.send( new lib.InstanceStatusChangedEvent( - instanceId, instanceConnection ? instanceConnection.status : "stopped", null + instanceId, instanceConnection ? instanceConnection.status : "stopped", undefined ) ); diff --git a/packages/host/src/Instance.js b/packages/host/src/Instance.js index 2d19f98f..132873fc 100644 --- a/packages/host/src/Instance.js +++ b/packages/host/src/Instance.js @@ -438,7 +438,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); new lib.InstanceStatusChangedEvent( this.id, status, - this.server && this.server.gamePort || this.config.get("factorio.game_port") || null, + this.server && this.server.gamePort || this.config.get("factorio.game_port") || undefined, ), ); } diff --git a/packages/host/src/server.js b/packages/host/src/server.js index 2237e7ae..73090b45 100644 --- a/packages/host/src/server.js +++ b/packages/host/src/server.js @@ -796,9 +796,11 @@ class FactorioServer extends events.EventEmitter { [ "--config", this.writePath("config.ini"), "--create", this.writePath("saves", name), - ...(seed !== null ? ["--map-gen-seed", seed] : []), - ...(mapGenSettings !== null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []), - ...(mapSettings !== null ? ["--map-settings", this.writePath("map-settings.json")] : []), + /* eslint-disable eqeqeq, no-eq-null */ + ...(seed != null ? ["--map-gen-seed", seed] : []), + ...(mapGenSettings != null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []), + ...(mapSettings != null ? ["--map-settings", this.writePath("map-settings.json")] : []), + /* eslint-enable eqeqeq, no-eq-null */ ...(this.verboseLogging ? ["--verbose"] : []), ], { @@ -886,9 +888,11 @@ class FactorioServer extends events.EventEmitter { [ "--config", this.writePath("config.ini"), "--start-server-load-scenario", scenario, - ...(seed !== null ? ["--map-gen-seed", seed] : []), - ...(mapGenSettings !== null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []), - ...(mapSettings !== null ? ["--map-settings", this.writePath("map-settings.json")] : []), + /* eslint-disable eqeqeq, no-eq-null */ + ...(seed != null ? ["--map-gen-seed", seed] : []), + ...(mapGenSettings != null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []), + ...(mapSettings != null ? ["--map-settings", this.writePath("map-settings.json")] : []), + /* eslint-enable eqeqeq, no-eq-null */ "--port", this.gamePort, "--rcon-port", this.rconPort, "--rcon-password", this.rconPassword, diff --git a/packages/lib/package.json b/packages/lib/package.json index 519495da..93696564 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -5,7 +5,7 @@ "repository": "https://github.com/clusterio/clusterio", "license": "MIT", "scripts": { - "prepare": "pnpm run compile_validator && tsc", + "prepare": "tsc && pnpm run compile_validator", "compile_validator": "node scripts/compile_validator.js" }, "main": "dist/index.js", @@ -17,6 +17,7 @@ "node": ">=12" }, "dependencies": { + "@sinclair/typebox": "^0.30.4", "ajv": "^8.12.0", "chalk": "^4.1.2", "fast-deep-equal": "^3.1.3", diff --git a/packages/lib/scripts/compile_validator.js b/packages/lib/scripts/compile_validator.js index 41745df1..61421e01 100644 --- a/packages/lib/scripts/compile_validator.js +++ b/packages/lib/scripts/compile_validator.js @@ -7,9 +7,9 @@ const standaloneCode = require("ajv/dist/standalone").default; /* eslint-disable node/no-sync */ // Provide an empty file for message_core to load -fs.outputFileSync(path.join(__dirname, "../src/data/message_validate.js"), ""); +fs.outputFileSync(path.join(__dirname, "../dist/src/data/message_validate.js"), ""); -const { Message } = require("../src/data/messages_core"); +const { Message } = require("../dist/src/data/messages_core"); const ajv = new Ajv({ allowUnionTypes: true, @@ -23,6 +23,6 @@ const ajv = new Ajv({ const validate = ajv.compile(Message.jsonSchema); let moduleCode = standaloneCode(ajv, validate); fs.outputFileSync( - path.join(__dirname, "../src/data/message_validate.js"), + path.join(__dirname, "../dist/src/data/message_validate.js"), `// generated by scripts/compile_validator.js\n// @ts-nocheck\n/* eslint-disable */\n${moduleCode}` ); diff --git a/packages/lib/src/data/composites.js b/packages/lib/src/data/composites.js deleted file mode 100644 index cdd80943..00000000 --- a/packages/lib/src/data/composites.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; - -function jsonArray(ItemClass) { - return { - jsonSchema: { - type: "array", - items: ItemClass.jsonSchema, - }, - - fromJSON(json) { - return json.map(i => ItemClass.fromJSON(i)); - }, - }; -} - -function jsonPrimitive(type) { - return { - jsonSchema: { type }, - fromJSON(json) { - return json; - }, - }; -} -const JsonBoolean = jsonPrimitive("boolean"); -const JsonNumber = jsonPrimitive("number"); -const JsonString = jsonPrimitive("string"); - -module.exports = { - JsonBoolean, - JsonNumber, - JsonString, - - jsonArray, - jsonPrimitive, -}; diff --git a/packages/lib/src/data/composites.ts b/packages/lib/src/data/composites.ts new file mode 100644 index 00000000..8566185f --- /dev/null +++ b/packages/lib/src/data/composites.ts @@ -0,0 +1,34 @@ +import { Type } from "@sinclair/typebox"; + +export function StringEnum(values: [...T]) { + return Type.Unsafe({ type: 'string', enum: values }) +} + +export interface JSONDeserialisable { + fromJSON(json: unknown): T; + jsonSchema: object; +} + +export function jsonArray(ItemClass: JSONDeserialisable) { + return { + jsonSchema: { + type: "array", + items: ItemClass.jsonSchema, + }, + fromJSON(json: Array) { + return json.map(i => ItemClass.fromJSON(i)); + }, + }; +} + +export function jsonPrimitive(type: U) { + return { + jsonSchema: { type }, + fromJSON(json: T) { + return json; + }, + }; +} +export const JsonBoolean = jsonPrimitive("boolean"); +export const JsonNumber = jsonPrimitive("number"); +export const JsonString = jsonPrimitive("string"); diff --git a/packages/lib/src/data/index.ts b/packages/lib/src/data/index.ts index 5fb50974..7f5e2349 100644 --- a/packages/lib/src/data/index.ts +++ b/packages/lib/src/data/index.ts @@ -17,19 +17,11 @@ export * from "./version"; // TODO: Remove/migrate after lib/data is migrated to ts import type { User } from "../users"; -export type AddressType = "controller" | "host" | "instance" | "control" | "broadcast"; export interface Serialisable { jsonSchema: object, fromJSON(json: any): object, } - -import type { Address } from "./messages_core"; -export type AddressShorthand = - | "controller" - | { instanceId: number } - | { hostId: number } - | { controlId: number } - | Address +import { AddressType } from "./messages_core"; export class Request { static type: "request"; diff --git a/packages/lib/src/data/message_validate.d.ts b/packages/lib/src/data/message_validate.d.ts index 177658ef..fdd40377 100644 --- a/packages/lib/src/data/message_validate.d.ts +++ b/packages/lib/src/data/message_validate.d.ts @@ -1,2 +1,4 @@ -import type { ValidateFunction } from "ajv" -export default ValidateFunction +import type { ValidateFunction } from "ajv"; +import type { Message } from "./messages_core"; +var message_validate: ValidateFunction; +export default message_validate; diff --git a/packages/lib/src/data/messages_controller.js b/packages/lib/src/data/messages_controller.js deleted file mode 100644 index 5fb97303..00000000 --- a/packages/lib/src/data/messages_controller.js +++ /dev/null @@ -1,384 +0,0 @@ -"use strict"; -const { JsonString } = require("./composites"); - -// TODO: remove after config refactor -/** @memberof module:lib */ -class RawConfig { - /** @type {object} */ - serializedConfig; - - constructor(serializedConfig) { - this.serializedConfig = serializedConfig; - } - - static jsonSchema = { - type: "object", - required: ["serializedConfig"], - properties: { - "serializedConfig": { type: "object" }, - }, - }; - - static fromJSON(json) { - return new this(json.serializedConfig); - } -} - -/* @memberof module:lib */ -class ControllerConfigGetRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.controller.get_config"; - static Response = RawConfig; -} - -/** @memberof module:lib */ -class ControllerConfigSetFieldRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.controller.update_config"; - - /** @type {string} */ - field; - /** @type {string} */ - value; - - constructor(field, value) { - this.field = field; - this.value = value; - } - - static jsonSchema = { - type: "object", - required: ["field", "value"], - properties: { - "field": { type: "string" }, - "value": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.field, json.value); - } -} - -/* @memberof module:lib */ -class ControllerConfigSetPropRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.controller.update_config"; - - /** @type {string} */ - field; - /** @type {string} */ - prop; - /** @type {undefined} */ - value; - - constructor(field, prop, value) { - this.field = field; - this.prop = prop; - if (value) { this.value = value; } - } - - static jsonSchema = { - type: "object", - required: ["field", "prop"], - properties: { - field: { type: "string" }, - prop: { type: "string" }, - value: {}, - }, - }; - - static fromJSON(json) { - return new this(json.field, json.prop, json.value); - } -} - -/* @memberof module:lib */ -class HostGenerateTokenRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.host.generate_token"; - - /** @type {?number} */ - hostId; - - constructor(hostId) { - this.hostId = hostId; - } - - static jsonSchema = { - type: "object", - required: ["hostId"], - properties: { - hostId: { type: ["integer", "null"] }, - }, - }; - - static fromJSON(json) { - return new this(json.hostId); - } - - static Response = JsonString; -} - -/** @memberof module:lib */ -class HostConfigCreateRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.host.create_config"; - - /** @type {?number} */ - id; - /** @type {?string} */ - name; - /** @type {boolean} */ - generateToken; - - constructor(id, name, generateToken) { - this.id = id; - this.name = name; - this.generateToken = generateToken; - } - - static jsonSchema = { - type: "object", - required: ["id", "name", "generateToken"], - properties: { - "id": { type: ["integer", "null"] }, - "name": { type: ["string", "null"] }, - "generateToken": { type: "boolean" }, - }, - }; - - static fromJSON(json) { - return new this(json.id, json.name, json.generateToken); - } - - static Response = RawConfig; -} - -/* @memberof module:lib */ -class LogSetSubscriptionsRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.log.follow"; - - /** @type {boolean} */ - all; - /** @type {boolean} */ - controller; - /** @type {array} */ - hostIds; - /** @type {array} */ - instanceIds; - /** @type {?string} */ - maxLevel; - - constructor(all, controller, hostIds, instanceIds, maxLevel) { - this.all = all; - this.controller = controller; - this.hostIds = hostIds; - this.instanceIds = instanceIds; - this.maxLevel = maxLevel; - } - - static jsonSchema = { - type: "object", - required: ["all", "controller", "hostIds", "instanceIds", "maxLevel"], - properties: { - all: { type: "boolean" }, - controller: { type: "boolean" }, - hostIds: { - type: "array", - items: { type: "integer" }, - }, - instanceIds: { - type: "array", - items: { type: "integer" }, - }, - maxLevel: { type: ["string", "null"] }, - }, - }; - - static fromJSON(json) { - return new this(json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel); - } -} - -/* @memberof module:lib */ -class LogQueryRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.log.query"; - - /** @type {boolean} */ - all; - /** @type {boolean} */ - controller; - /** @type {Array} */ - hostIds; - /** @type {Array} */ - instanceIds; - /** @type {?string} */ - maxLevel; - /** @type {integer} */ - limit; - /** @type {undefined} */ - order; - - constructor(all, controller, hostIds, instanceIds, maxLevel, limit, order) { - this.all = all; - this.controller = controller; - this.hostIds = hostIds; - this.instanceIds = instanceIds; - this.maxLevel = maxLevel; - this.limit = limit; - this.order = order; - } - - static jsonSchema = { - type: "object", - required: ["all", "controller", "hostIds", "instanceIds", "maxLevel", "limit", "order"], - properties: { - all: { type: "boolean" }, - controller: { type: "boolean" }, - hostIds: { - type: "array", - items: { type: "integer" }, - }, - instanceIds: { - type: "array", - items: { type: "integer" }, - }, - maxLevel: { type: ["string", "null"] }, - limit: { type: "integer" }, - order: { - enum: ["asc", "desc"], - }, - }, - }; - - static fromJSON(json) { - return new this( - json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel, json.limit, json.order - ); - } -} -LogQueryRequest.Response = class Response { - /** @type {Array} */ - log; - - constructor(log) { - this.log = log; - } - - static jsonSchema = { - type: "object", - required: ["log"], - properties: { - log: { - type: "array", - items: { type: "object" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.log); - } -}; - -/** @memberof module:lib */ -class LogMessageEvent { - static type = "event"; - static src = ["host", "controller"]; - static dst = ["controller", "control"]; - - /** @type {object} */ - info; - - constructor(info) { - this.info = info; - } - - static jsonSchema = { - type: "object", - required: ["info"], - properties: { - "info": { - type: "object", - required: ["level", "message"], - properties: { - "level": { type: "string" }, - "message": { type: "string" }, - }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.info); - } -} - -/* @memberof module:lib */ -class DebugDumpWsRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.debug.dump_ws"; -} - -/* @memberof module:lib */ -class DebugWsMessageEvent { - static type = "event"; - static src = "controller"; - static dst = "control"; - - /** @type {string} */ - direction; - /** @type {string} */ - content; - - constructor(direction, content) { - this.direction = direction; - this.content = content; - } - - static jsonSchema = { - type: "object", - required: ["direction", "content"], - properties: { - direction: { type: "string" }, - content: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.direction, json.content); - } -} - -module.exports = { - RawConfig, - ControllerConfigGetRequest, - ControllerConfigSetFieldRequest, - ControllerConfigSetPropRequest, - HostGenerateTokenRequest, - HostConfigCreateRequest, - LogSetSubscriptionsRequest, - LogQueryRequest, - LogMessageEvent, - DebugDumpWsRequest, - DebugWsMessageEvent, -}; diff --git a/packages/lib/src/data/messages_controller.ts b/packages/lib/src/data/messages_controller.ts new file mode 100644 index 00000000..3ee14f7e --- /dev/null +++ b/packages/lib/src/data/messages_controller.ts @@ -0,0 +1,247 @@ +import { Type, Static } from "@sinclair/typebox"; +import { JsonString, StringEnum } from "./composites"; +import { levels } from "../logging"; + +// TODO: remove after config refactor +export class RawConfig { + constructor( + public serializedConfig: object + ) { } + + static jsonSchema = Type.Object({ + "serializedConfig": Type.Object({}), + }); + + static fromJSON(json: Static) { + return new this(json.serializedConfig); + } +} + +export class ControllerConfigGetRequest { + declare ["constructor"]: typeof ControllerConfigGetRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.controller.get_config"; + static Response = RawConfig; +} + +export class ControllerConfigSetFieldRequest { + declare ["constructor"]: typeof ControllerConfigSetFieldRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.controller.update_config"; + + constructor( + public field: string, + public value: string, + ) { } + + static jsonSchema = Type.Object({ + "field": Type.String(), + "value": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.field, json.value); + } +} + +export class ControllerConfigSetPropRequest { + declare ["constructor"]: typeof ControllerConfigSetPropRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.controller.update_config"; + + constructor( + public field: string, + public prop: string, + public value?: unknown, + ) { } + + static jsonSchema = Type.Object({ + "field": Type.String(), + "prop": Type.String(), + "value": Type.Optional(Type.Unknown()), + }); + + static fromJSON(json: Static) { + return new this(json.field, json.prop, json.value); + } +} + +export class HostGenerateTokenRequest { + declare ["constructor"]: typeof HostGenerateTokenRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.host.generate_token"; + + constructor( + public hostId?: number, + ) { } + + static jsonSchema = Type.Object({ + "hostId": Type.Optional(Type.Integer()), + }); + + static fromJSON(json: Static) { + return new this(json.hostId); + } + + static Response = JsonString; +} + +export class HostConfigCreateRequest { + declare ["constructor"]: typeof HostConfigCreateRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.host.create_config"; + + constructor( + public id: number | undefined, + public name: string | undefined, + public generateToken: boolean, + ) { } + + static jsonSchema = Type.Object({ + "id": Type.Optional(Type.Integer()), + "name": Type.Optional(Type.String()), + "generateToken": Type.Boolean(), + }); + + static fromJSON(json: Static) { + return new this(json.id, json.name, json.generateToken); + } + + static Response = RawConfig; +} + +export class LogSetSubscriptionsRequest { + declare ["constructor"]: typeof LogSetSubscriptionsRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.log.follow"; + + constructor( + public all?: boolean, + public controller?: boolean, + public hostIds?: number[], + public instanceIds?: number[], + public maxLevel?: keyof typeof levels, + ) { } + + static jsonSchema = Type.Object({ + all: Type.Optional(Type.Boolean()), + controller: Type.Optional(Type.Boolean()), + hostIds: Type.Array(Type.Integer()), + instanceIds: Type.Array(Type.Integer()), + maxLevel: Type.Optional(StringEnum(Object.keys(levels) as (keyof typeof levels)[])), + }); + + static fromJSON(json: Static) { + return new this(json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel); + } +} + +export class LogQueryRequest { + declare ["constructor"]: typeof LogQueryRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.log.query"; + + constructor( + public all: boolean, + public controller: boolean, + public hostIds: number[], + public instanceIds: number[], + public maxLevel: undefined | keyof typeof levels, + public limit: number, + public order: "asc" | "desc", + ) { } + + static jsonSchema = Type.Object({ + all: Type.Boolean(), + controller: Type.Boolean(), + hostIds: Type.Array(Type.Integer()), + instanceIds: Type.Array(Type.Integer()), + maxLevel: Type.Optional(StringEnum(Object.keys(levels) as (keyof typeof levels)[])), + limit: Type.Integer(), + order: StringEnum(["asc", "desc"]), + }); + + static fromJSON(json: Static) { + return new this( + json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel, json.limit, json.order + ); + } + static Response = class Response { + constructor( + public log: object[], + ) { } + + static jsonSchema = Type.Object({ + "log": Type.Array(Type.Object({})), + }); + + static fromJSON(json: Static) { + return new this(json.log); + } + }; +} + +export class LogMessageEvent { + declare ["constructor"]: typeof LogMessageEvent; + static type = "event"; + static src = ["host", "controller"]; + static dst = ["controller", "control"]; + + constructor( + public info: object, + ) { } + + static jsonSchema = Type.Object({ + "info": Type.Object({ + "level": Type.String(), + "message": Type.String(), + }), + }); + + static fromJSON(json: Static) { + return new this(json.info); + } +} + +export class DebugDumpWsRequest { + declare ["constructor"]: typeof DebugDumpWsRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.debug.dump_ws"; +} + +export class DebugWsMessageEvent { + declare ["constructor"]: typeof DebugWsMessageEvent; + static type = "event"; + static src = "controller"; + static dst = "control"; + + constructor( + public direction: string, + public content: string, + ) { } + + static jsonSchema = Type.Object({ + "direction": Type.String(), + "content": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.direction, json.content); + } +} diff --git a/packages/lib/src/data/messages_core.js b/packages/lib/src/data/messages_core.js deleted file mode 100644 index d418a60f..00000000 --- a/packages/lib/src/data/messages_core.js +++ /dev/null @@ -1,890 +0,0 @@ -// Note: Run compile_validator script after making changes to this file. -"use strict"; -const message_validate = require("./message_validate.js"); - - -/** @memberof module:lib */ -class Address { - /** Controller address */ - static controller = 0; - /** Instance address */ - static instance = 1; - /** Host address */ - static host = 2; - /** Control address */ - static control = 3; - /** Broadcast */ - static broadcast = 4; - - /** @type {number} */ - type; - - /** @type {number} */ - id; - - /** @type {number|undefined} */ - requestId; - - constructor(type, id, requestId) { - if (typeof type !== "number") { - throw Error(`Invalid type ${type}`); - } - this.type = type; - this.id = id; - this.requestId = requestId; - } - - /** - * Convert from convenient shorthand notation to address. - * - * Supported notations and their corresponding address: - * - `"controller"`: new Address(Address.controller, 0) - * - `{ hostId: id }`: new Address(Address.host, id) - * - `{ instanceId: id }`: new Address(Address.instance, id) - * - `{ controlId: id }`: new Address(Address.control, id) - * `"allHosts"`: new Address(Address.broadcast, Address.host) - * `"allInstances"`: new Address(Address.broadcast, Address.instance) - * `"allControls"`: new Address(Address.broadcast, Address.control) - * @param {module:lib.Address|string|object} shorthand - - * Shorthand to translate with. - * @returns {module:lib.Address} Translated address. - */ - static fromShorthand(shorthand) { - if (shorthand instanceof Address) { return shorthand; } - if (shorthand === "controller") { return new Address(Address.controller, 0); } - if (shorthand === "allHosts") { return new Address(Address.broadcast, Address.host); } - if (shorthand === "allInstances") { return new Address(Address.broadcast, Address.instance); } - if (shorthand === "allControls") { return new Address(Address.broadcast, Address.control); } - let entries = Object.entries(shorthand); - if (entries.length === 1) { - let [name, value] = entries[0]; - if (name === "hostId") { return new Address(Address.host, value); } - if (name === "instanceId") { return new Address(Address.instance, value); } - if (name === "controlId") { return new Address(Address.control, value); } - } - throw Error("Unrecognized shorthand"); - } - - static jsonSchema = { - type: "array", - minItems: 2, - items: [ - { type: "integer", minimum: 0, maximum: 4 }, - { type: "integer" }, - { type: "integer" }, - ], - }; - - static fromJSON(json) { - return new this(json[0], json[1], json[2]); - } - - toJSON() { - if (this.requestId !== undefined) { - return [this.type, this.id, this.requestId]; - } - return [this.type, this.id]; - } - - toString() { - const typeMap = ["controller", "instance", "host", "control"]; - const type = typeMap[this.type] || this.type; - if (this.type === Address.broadcast) { - return `[Address ${type}:${typeMap[this.id] || this.id}]`; - } - if (this.requestId === undefined) { - return `[Address ${type}:${this.id}]`; - } - return `[Address ${type}:${this.id}:${this.requestId}]`; - } - - /** - * Returns a string suitable for indexing the address in a Map - * @returns {string} unique string for this address - */ - index() { - return `${this.type}:${this.id}:${this.requestId}`; - } - - /** - * Returns true if this address targets the given destination - * @param {module:lib.Address} dst - Destination to check - * @returns {boolean} true if this address is addressed to the given destination - */ - addressedTo(dst) { - if (this.type !== Address.broadcast) { - return ( - this.type === dst.type - && this.id === dst.id - ); - } - return dst.type !== Address.broadcast && this.id === dst.type; - } - - equals(other) { - return ( - this.type === other.type - && this.id === other.id - && this.requestId === other.requestId - ); - } -} - -/** @memberof module:lib */ -class Message { - /** @type {string} */ - type; - - constructor(type) { - this.type = type; - } - - static jsonSchema; - static validate; - - static fromJSON(json) { - /* eslint-disable no-use-before-define */ - if (json.type === "hello") { return MessageHello.fromJSON(json); } - if (json.type === "registerHost") { return MessageRegisterHost.fromJSON(json); } - if (json.type === "registerControl") { return MessageRegisterControl.fromJSON(json); } - if (json.type === "ready") { return MessageReady.fromJSON(json); } - if (json.type === "resume") { return MessageResume.fromJSON(json); } - if (json.type === "continue") { return MessageContinue.fromJSON(json); } - if (json.type === "invalidate") { return MessageInvalidate.fromJSON(json); } - if (json.type === "heartbeat") { return MessageHeartbeat.fromJSON(json); } - if (json.type === "request") { return MessageRequest.fromJSON(json); } - if (json.type === "response") { return MessageResponse.fromJSON(json); } - if (json.type === "responseError") { return MessageResponseError.fromJSON(json); } - if (json.type === "event") { return MessageEvent.fromJSON(json); } - if (json.type === "disconnect") { return MessageDisconnect.fromJSON(json); } - /* eslint-enable no-use-before-define */ - throw new Error(`Unrecognized message type ${json.type}`); - } -} - -/** @memberof module:lib */ -class HelloData { - /** @type {string} */ - version; - /** @type {object} */ - plugins; - - constructor(version, plugins) { - this.version = version; - this.plugins = plugins; - } - - static jsonSchema = { - type: "object", - required: ["version", "plugins"], - properties: { - "version": { type: "string" }, - "plugins": { - type: "object", - additionalProperties: { type: "string" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.version, json.plugins); - } -} - -/** @memberof module:lib */ -class MessageHello extends Message { - /** @type {module:lib.HelloData} */ - data; - - constructor(data) { - super("hello"); - this.data = data; - } - - static jsonSchema = { - type: "object", - required: ["type", "data"], - properties: { - "type": { const: "hello" }, - "data": HelloData.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(HelloData.fromJSON(json.data)); - } -}; - -/** @memberof module:lib */ -class RegisterHostData { - /** @type {string} */ - token; - /** @type {string} */ - agent; - /** @type {string} */ - version; - /** @type {string} */ - name; - /** @type {string} */ - id; - /** @type {string|undefined} */ - publicAddress; - /** @type {Object} */ - plugins; - - constructor(token, agent, version, name, id, publicAddress, plugins) { - this.token = token; - this.agent = agent; - this.version = version; - this.name = name; - this.id = id; - this.publicAddress = publicAddress; - this.plugins = plugins; - } - - static jsonSchema = { - type: "object", - required: ["token", "agent", "version", "name", "id", "plugins"], - properties: { - "token": { type: "string" }, - "agent": { type: "string" }, - "version": { type: "string" }, - "name": { type: "string" }, - "id": { type: "integer" }, - "publicAddress": { type: "string" }, - "plugins": { - type: "object", - additionalProperties: { type: "string" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.token, json.agent, json.version, json.name, json.id, json.publicAddress, json.plugins); - } -} - -/** @memberof module:lib */ -class MessageRegisterHost extends Message { - /** @type {module:lib.RegisterHostData} */ - data; - - constructor(data) { - super("registerHost"); - this.data = data; - } - - static jsonSchema = { - type: "object", - required: ["type", "data"], - properties: { - "type": { const: "registerHost" }, - "data": RegisterHostData.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(RegisterHostData.fromJSON(json.data)); - } -}; - -/** @memberof module:lib */ -class RegisterControlData { - /** @type {string} */ - token; - /** @type {string} */ - agent; - /** @type {string} */ - version; - - constructor(token, agent, version) { - this.token = token; - this.agent = agent; - this.version = version; - } - - static jsonSchema = { - type: "object", - required: ["token", "agent", "version"], - properties: { - "token": { type: "string" }, - "agent": { type: "string" }, - "version": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.token, json.agent, json.version); - } -} - -/** @memberof module:lib */ -class MessageRegisterControl extends Message { - /** @type {module:lib.RegisterControlData} */ - data; - - constructor(data) { - super("registerControl"); - this.data = data; - } - - static jsonSchema = { - type: "object", - required: ["data"], - properties: { - "type": { const: "registerControl" }, - "data": RegisterControlData.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(RegisterControlData.fromJSON(json.data)); - } -}; - -/** @memberof module:lib */ -class AccountDetails { - /** @type {string} */ - name; - /** @type {Array} */ - roles; - - constructor(name, roles) { - this.name = name; - this.roles = roles; - } - - static jsonSchema = { - type: "object", - required: ["name", "roles"], - properties: { - "name": { type: "string" }, - "roles": { - type: "array", - items: { - type: "object", - required: ["name", "id", "permissions"], - properties: { - "name": { type: "string" }, - "id": { type: "integer" }, - "permissions": { - type: "array", - items: { type: "string" }, - }, - }, - }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.roles); - } -} - -/** @memberof module:lib */ -class ReadyData { - /** @type {module:lib.Address} */ - src; - /** @type {string} */ - sessionToken; - /** @type {number} */ - sessionTimeout; - /** @type {number} */ - heartbeatInterval; - /** @type {AccountDetails|undefined} */ - account; - - constructor(src, sessionToken, sessionTimeout, heartbeatInterval, account) { - this.src = src; - this.sessionToken = sessionToken; - this.sessionTimeout = sessionTimeout; - this.heartbeatInterval = heartbeatInterval; - if (account) { this.account = account; } - } - - static jsonSchema = { - type: "object", - required: ["src", "sessionToken", "sessionTimeout", "heartbeatInterval"], - properties: { - "src": Address.jsonSchema, - "sessionToken": { type: "string" }, - "sessionTimeout": { type: "number" }, - "heartbeatInterval": { type: "number" }, - "account": AccountDetails.jsonSchema, - }, - }; - - static fromJSON(json) { - let account; - if (json.account) { account = AccountDetails.fromJSON(json.account); } - return new this( - Address.fromJSON(json.src), - json.sessionToken, - json.sessionTimeout, - json.heartbeatInterval, - account - ); - } -} - -/** @memberof module:lib */ -class MessageReady extends Message { - /** @type {module:lib.ReadyData} */ - data; - - constructor(data) { - super("ready"); - this.data = data; - } - - static jsonSchema = { - type: "object", - required: ["data"], - properties: { - "type": { const: "ready" }, - "data": ReadyData.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(ReadyData.fromJSON(json.data)); - } -} - -/** @memberof module:lib */ -class ResumeData { - /** @type {string} */ - sessionToken; - /** @type {integer|undefined} */ - lastSeq; - - constructor(sessionToken, lastSeq) { - this.sessionToken = sessionToken; - if (lastSeq !== undefined) { this.lastSeq = lastSeq; } - } - - static jsonSchema = { - type: "object", - required: ["sessionToken"], - properties: { - "sessionToken": { type: "string" }, - "lastSeq": { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.sessionToken, json.lastSeq); - } -} - - -/** @memberof module:lib */ -class MessageResume extends Message { - /** @type {module:lib.ResumeData} */ - data; - - constructor(data) { - super("resume"); - this.data = data; - } - - static jsonSchema = { - type: "object", - required: ["data"], - properties: { - "type": { const: "resume" }, - "data": ResumeData.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(ResumeData.fromJSON(json.data)); - } -} - -/** @memberof module:lib */ -class ContinueData { - /** @type {number} */ - sessionTimeout; - /** @type {number} */ - heartbeatInterval; - /** @type {integer|undefined} */ - lastSeq; - - constructor(sessionTimeout, heartbeatInterval, lastSeq) { - this.sessionTimeout = sessionTimeout; - this.heartbeatInterval = heartbeatInterval; - if (lastSeq !== undefined) { this.lastSeq = lastSeq; } - } - - static jsonSchema = { - type: "object", - required: ["sessionTimeout", "heartbeatInterval"], - properties: { - "sessionTimeout": { type: "number" }, - "heartbeatInterval": { type: "number" }, - "lastSeq": { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.sessionTimeout, json.heartbeatInterval, json.lastSeq); - } -} - -/** @memberof module:lib */ -class MessageContinue extends Message { - /** @type {module:lib.ContinueData} */ - data; - - constructor(data) { - super("continue"); - this.data = data; - } - - static jsonSchema = { - type: "object", - required: ["data"], - properties: { - "type": { const: "continue" }, - "data": ContinueData.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(ContinueData.fromJSON(json.data)); - } -} - -/** @memberof module:lib */ -class MessageInvalidate extends Message { - constructor() { - super("invalidate"); - } - - static jsonSchema = { - type: "object", - properties: { - "type": { const: "invalidate" }, - }, - }; - - static fromJSON(json) { - return new this(); - } -} - -/** @memberof module:lib */ -class MessageHeartbeat extends Message { - /** @type {integer|undefined} */ - seq; - - constructor(seq) { - super("heartbeat"); - this.seq = seq; - } - - static jsonSchema = { - type: "object", - required: ["type"], - properties: { - "type": { const: "heartbeat" }, - "seq": { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.seq); - } -} - -/** @memberof module:lib */ -class MessageRequest extends Message { - /** @type {number} */ - seq; - /** @type {Address} */ - src; - /** @type {Address} */ - dst; - /** @type {string} */ - name; - /** @type {*} */ - data; - - constructor(seq, src, dst, name, data) { - super("request"); - this.seq = seq; - this.src = src; - this.dst = dst; - this.name = name; - this.data = data; - } - - static jsonSchema = { - type: "object", - required: ["type", "seq", "src", "dst", "name"], - properties: { - "type": { const: "request" }, - "seq": { type: "integer" }, - "src": Address.jsonSchema, - "dst": Address.jsonSchema, - "name": { type: "string" }, - "data": {}, - }, - }; - - static fromJSON(json) { - return new this( - json.seq, - Address.fromJSON(json.src), - Address.fromJSON(json.dst), - json.name, - json.data, - ); - } -}; - -/** @memberof module:lib */ -class MessageResponse extends Message { - /** @type {number} */ - seq; - /** @type {Address} */ - src; - /** @type {Address} */ - dst; - /** @type {*} */ - data; - - constructor(seq, src, dst, data) { - super("response"); - this.seq = seq; - this.src = src; - this.dst = dst; - this.data = data; - } - - static jsonSchema = { - type: "object", - required: ["type", "seq", "src", "dst"], - properties: { - "type": { const: "response" }, - "seq": { type: "integer" }, - "src": Address.jsonSchema, - "dst": Address.jsonSchema, - "data": {}, - }, - }; - - static fromJSON(json) { - return new this( - json.seq, - Address.fromJSON(json.src), - Address.fromJSON(json.dst), - json.data, - ); - } -}; - -/** @memberof module:lib */ -class ResponseError { - /** @type {string} */ - message; - /** @type {string|undefined} */ - code; - /** @type {string|undefined} */ - stack; - - constructor(message, code, stack) { - this.message = message; - if (code) { this.code = code; } - if (stack) { this.stack = stack; } - } - - static jsonSchema = { - type: "object", - required: ["message"], - properties: { - "message": { type: "string" }, - "code": { type: "string" }, - "stack": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.message, json.code, json.stack); - } -} - -/** @memberof module:lib */ -class MessageResponseError extends Message { - /** @type {number} */ - seq; - /** @type {Address} */ - src; - /** @type {Address} */ - dst; - /** @type {module:lib.ResponseError} */ - data; - - constructor(seq, src, dst, data) { - super("responseError"); - this.seq = seq; - this.src = src; - this.dst = dst; - this.data = data; - } - - static jsonSchema = { - type: "object", - required: ["type", "seq", "src", "dst", "data"], - properties: { - "type": { const: "responseError" }, - "seq": { type: "integer" }, - "src": Address.jsonSchema, - "dst": Address.jsonSchema, - "data": ResponseError.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this( - json.seq, - Address.fromJSON(json.src), - Address.fromJSON(json.dst), - ResponseError.fromJSON(json.data), - ); - } -}; - -/** @memberof module:lib */ -class MessageEvent extends Message { - /** @type {number} */ - seq; - /** @type {Address} */ - src; - /** @type {Address} */ - dst; - /** @type {string} */ - name; - /** @type {*} */ - data; - - constructor(seq, src, dst, name, data) { - super("event"); - this.seq = seq; - this.src = src; - this.dst = dst; - this.name = name; - this.data = data; - } - - static jsonSchema = { - type: "object", - required: ["type", "seq", "src", "dst", "name"], - properties: { - "type": { const: "event" }, - "seq": { type: "integer" }, - "src": Address.jsonSchema, - "dst": Address.jsonSchema, - "name": { type: "string" }, - "data": {}, - }, - }; - - static fromJSON(json) { - return new this( - json.seq, - Address.fromJSON(json.src), - Address.fromJSON(json.dst), - json.name, - json.data, - ); - } -}; - -/** @memberof module:lib */ -class MessageDisconnect extends Message { - /** @type {string} */ - data; - - constructor(stage) { - super("disconnect"); - this.data = stage; - } - - static jsonSchema = { - type: "object", - required: ["type"], - properties: { - "type": { const: "disconnect" }, - "data": { enum: ["prepare", "ready"] }, - }, - }; - - static fromJSON(json) { - return new this(json.data); - } -}; - -Message.jsonSchema = { - type: "object", - required: ["type"], - properties: { - "type": { enum: [ - "hello", "registerHost", "registerControl", "ready", "resume", "continue", "invalidate", - "heartbeat", "request", "response", "responseError", "event", "disconnect", - ] }, - }, - oneOf: [ - MessageHello.jsonSchema, - MessageRegisterHost.jsonSchema, - MessageRegisterControl.jsonSchema, - MessageReady.jsonSchema, - MessageResume.jsonSchema, - MessageContinue.jsonSchema, - MessageInvalidate.jsonSchema, - MessageHeartbeat.jsonSchema, - MessageRequest.jsonSchema, - MessageResponse.jsonSchema, - MessageResponseError.jsonSchema, - MessageEvent.jsonSchema, - MessageDisconnect.jsonSchema, - ], -}; -Message.validate = message_validate; - -/** @memberof module:lib */ -class PingRequest { - static type = "request"; - static src = ["controller", "host", "control"]; - static dst = ["controller", "host", "control"]; - static permission = null; -} - -module.exports = { - Message, - HelloData, - MessageHello, - RegisterHostData, - MessageRegisterHost, - RegisterControlData, - MessageRegisterControl, - AccountDetails, - ReadyData, - MessageReady, - ResumeData, - MessageResume, - ContinueData, - MessageContinue, - MessageInvalidate, - MessageHeartbeat, - Address, - MessageRequest, - MessageResponse, - ResponseError, - MessageResponseError, - MessageEvent, - MessageDisconnect, - PingRequest, -}; diff --git a/packages/lib/src/data/messages_core.ts b/packages/lib/src/data/messages_core.ts new file mode 100644 index 00000000..5f6b8ac4 --- /dev/null +++ b/packages/lib/src/data/messages_core.ts @@ -0,0 +1,629 @@ +// Note: Run compile_validator script after making changes to this file. +import { TSchema, Type, Static } from "@sinclair/typebox"; +import messageValidate from "./message_validate"; +import { StringEnum } from "./composites"; + +export type AddressType = "controller" | "host" | "instance" | "control" | "broadcast"; +export type AddressShorthand = + | "controller" + | "allHosts" + | "allInstances" + | "allControls" + | { instanceId: number } + | { hostId: number } + | { controlId: number } + | Address +; + +export class Address { + /** Controller address */ + static controller = 0; + /** Instance address */ + static instance = 1; + /** Host address */ + static host = 2; + /** Control address */ + static control = 3; + /** Broadcast */ + static broadcast = 4; + + constructor( + public type: number, + public id: number, + public requestId?: number, + ) { } + + /** + * Convert from convenient shorthand notation to address. + * + * Supported notations and their corresponding address: + * - `"controller"`: new Address(Address.controller, 0) + * - `{ hostId: id }`: new Address(Address.host, id) + * - `{ instanceId: id }`: new Address(Address.instance, id) + * - `{ controlId: id }`: new Address(Address.control, id) + * `"allHosts"`: new Address(Address.broadcast, Address.host) + * `"allInstances"`: new Address(Address.broadcast, Address.instance) + * `"allControls"`: new Address(Address.broadcast, Address.control) + * @param shorthand - + * Shorthand to translate with. + * @returns Translated address. + */ + static fromShorthand(shorthand: AddressShorthand) { + if (shorthand instanceof Address) { return shorthand; } + if (shorthand === "controller") { return new Address(Address.controller, 0); } + if (shorthand === "allHosts") { return new Address(Address.broadcast, Address.host); } + if (shorthand === "allInstances") { return new Address(Address.broadcast, Address.instance); } + if (shorthand === "allControls") { return new Address(Address.broadcast, Address.control); } + let entries = Object.entries(shorthand); + if (entries.length === 1) { + let [name, value] = entries[0]; + if (name === "hostId") { return new Address(Address.host, value); } + if (name === "instanceId") { return new Address(Address.instance, value); } + if (name === "controlId") { return new Address(Address.control, value); } + } + throw Error("Unrecognized shorthand"); + } + + static jsonSchema = Type.Unsafe<[number, number, number?]>({ + type: "array", + minItems: 2, + items: [ + { type: "integer", minimum: 0, maximum: 4 }, + Type.Integer(), + Type.Integer(), + ], + }); + + static fromJSON(json: Static) { + return new this(json[0], json[1], json[2]); + } + + toJSON() { + if (this.requestId !== undefined) { + return [this.type, this.id, this.requestId]; + } + return [this.type, this.id]; + } + + toString() { + const typeMap = ["controller", "instance", "host", "control"]; + const type = typeMap[this.type] || this.type; + if (this.type === Address.broadcast) { + return `[Address ${type}:${typeMap[this.id] || this.id}]`; + } + if (this.requestId === undefined) { + return `[Address ${type}:${this.id}]`; + } + return `[Address ${type}:${this.id}:${this.requestId}]`; + } + + /** + * Returns a string suitable for indexing the address in a Map + * @returns unique string for this address + */ + index() { + return `${this.type}:${this.id}:${this.requestId}`; + } + + /** + * Returns true if this address targets the given destination + * @param dst - Destination to check + * @returns true if this address is addressed to the given destination + */ + addressedTo(dst: Address) { + if (this.type !== Address.broadcast) { + return ( + this.type === dst.type + && this.id === dst.id + ); + } + return dst.type !== Address.broadcast && this.id === dst.type; + } + + equals(other: Address) { + return ( + this.type === other.type + && this.id === other.id + && this.requestId === other.requestId + ); + } +} + +type MessageType = + | "hello" + | "registerHost" + | "registerControl" + | "ready" + | "resume" + | "continue" + | "invalidate" + | "heartbeat" + | "request" + | "response" + | "responseError" + | "event" + | "disconnect" +; +export class Message { + constructor( + public type: MessageType, + ) { } + + static jsonSchema: TSchema; + static validate = messageValidate; + + static fromJSON(json: any) { + /* eslint-disable no-use-before-define */ + if (json.type === "hello") { return MessageHello.fromJSON(json); } + if (json.type === "registerHost") { return MessageRegisterHost.fromJSON(json); } + if (json.type === "registerControl") { return MessageRegisterControl.fromJSON(json); } + if (json.type === "ready") { return MessageReady.fromJSON(json); } + if (json.type === "resume") { return MessageResume.fromJSON(json); } + if (json.type === "continue") { return MessageContinue.fromJSON(json); } + if (json.type === "invalidate") { return MessageInvalidate.fromJSON(json); } + if (json.type === "heartbeat") { return MessageHeartbeat.fromJSON(json); } + if (json.type === "request") { return MessageRequest.fromJSON(json); } + if (json.type === "response") { return MessageResponse.fromJSON(json); } + if (json.type === "responseError") { return MessageResponseError.fromJSON(json); } + if (json.type === "event") { return MessageEvent.fromJSON(json); } + if (json.type === "disconnect") { return MessageDisconnect.fromJSON(json); } + /* eslint-enable no-use-before-define */ + throw new Error(`Unrecognized message type ${json.type}`); + } +} + +export class HelloData { + constructor( + public version: string, + public plugins: Record, + ) { } + + static jsonSchema = Type.Object({ + "version": Type.String(), + "plugins": Type.Record(Type.String(), Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.version, json.plugins); + } +} + +export class MessageHello extends Message { + constructor( + public data: HelloData, + ) { + super("hello"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("hello"), + "data": HelloData.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(HelloData.fromJSON(json.data)); + } +}; + +export class RegisterHostData { + constructor( + public token: string, + public agent: string, + public version: string, + public name: string, + public id: number, + public publicAddress: string | undefined, + public plugins: Record, + ) { } + + static jsonSchema = Type.Object({ + "token": Type.String(), + "agent": Type.String(), + "version": Type.String(), + "name": Type.String(), + "id": Type.Integer(), + "publicAddress": Type.Optional(Type.String()), + "plugins": Type.Record(Type.String(), Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.token, json.agent, json.version, json.name, json.id, json.publicAddress, json.plugins); + } +} + +export class MessageRegisterHost extends Message { + constructor( + public data: RegisterHostData, + ) { + super("registerHost"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("registerHost"), + "data": RegisterHostData.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(RegisterHostData.fromJSON(json.data)); + } +}; + +export class RegisterControlData { + constructor( + public token: string, + public agent: string, + public version: string, + ) { } + + static jsonSchema = Type.Object({ + "token": Type.String(), + "agent": Type.String(), + "version": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.token, json.agent, json.version); + } +} + +export class MessageRegisterControl extends Message { + constructor( + public data: RegisterControlData, + ) { + super("registerControl"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("registerControl"), + "data": RegisterControlData.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(RegisterControlData.fromJSON(json.data)); + } +}; + +export class AccountDetails { + constructor( + public name: string, + public roles: { name: string, id: number, permissions: string[] }[], + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "roles": Type.Array( + Type.Object({ + "name": Type.String(), + "id": Type.Integer(), + "permissions": Type.Array(Type.String()), + }), + ), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.roles); + } +} + +export class ReadyData { + constructor( + public src: Address, + public sessionToken: string, + public sessionTimeout: number, + public heartbeatInterval: number, + public account?: AccountDetails, + ) { } + + static jsonSchema = Type.Object({ + "src": Address.jsonSchema, + "sessionToken": Type.String(), + "sessionTimeout": Type.Number(), + "heartbeatInterval": Type.Number(), + "account": Type.Optional(AccountDetails.jsonSchema), + }); + + static fromJSON(json: Static) { + let account: AccountDetails; + if (json.account) { account = AccountDetails.fromJSON(json.account); } + return new this( + Address.fromJSON(json.src), + json.sessionToken, + json.sessionTimeout, + json.heartbeatInterval, + account + ); + } +} + +export class MessageReady extends Message { + constructor( + public data: ReadyData, + ) { + super("ready"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("ready"), + "data": ReadyData.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(ReadyData.fromJSON(json.data)); + } +} + +export class ResumeData { + constructor( + public sessionToken: string, + public lastSeq?: number, + ) { } + + static jsonSchema = Type.Object({ + "sessionToken": Type.String(), + "lastSeq": Type.Optional(Type.Integer()), + }); + + static fromJSON(json: Static) { + return new this(json.sessionToken, json.lastSeq); + } +} + + +export class MessageResume extends Message { + constructor( + public data: ResumeData, + ) { + super("resume"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("resume"), + "data": ResumeData.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(ResumeData.fromJSON(json.data)); + } +} + +export class ContinueData { + constructor( + public sessionTimeout: number, + public heartbeatInterval: number, + public lastSeq?: number, + ) { } + + static jsonSchema = Type.Object({ + "sessionTimeout": Type.Number(), + "heartbeatInterval": Type.Number(), + "lastSeq": Type.Optional(Type.Integer()), + }); + + static fromJSON(json: Static) { + return new this(json.sessionTimeout, json.heartbeatInterval, json.lastSeq); + } +} + +export class MessageContinue extends Message { + constructor( + public data: ContinueData, + ) { + super("continue"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("continue"), + "data": ContinueData.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(ContinueData.fromJSON(json.data)); + } +} + +export class MessageInvalidate extends Message { + constructor() { + super("invalidate"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("invalidate"), + }); + + static fromJSON(json: Static) { + return new this(); + } +} + +export class MessageHeartbeat extends Message { + constructor( + public seq?: number, + ) { + super("heartbeat"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("heartbeat"), + "seq": Type.Optional(Type.Integer()), + }); + + static fromJSON(json: Static) { + return new this(json.seq); + } +} + +export class MessageRequest extends Message { + constructor( + public seq: number, + public src: Address, + public dst: Address, + public name: string, + public data?: T, + ) { + super("request"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("request"), + "seq": Type.Integer(), + "src": Address.jsonSchema, + "dst": Address.jsonSchema, + "name": Type.String(), + "data": Type.Optional(Type.Unknown()), + }); + + static fromJSON(json: Static) { + return new this( + json.seq, + Address.fromJSON(json.src), + Address.fromJSON(json.dst), + json.name, + json.data, + ); + } +}; + +export class MessageResponse extends Message { + constructor( + public seq: number, + public src: Address, + public dst: Address, + public data?: T, + ) { + super("response"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("response"), + "seq": Type.Integer(), + "src": Address.jsonSchema, + "dst": Address.jsonSchema, + "data": Type.Optional(Type.Unknown()), + }); + + static fromJSON(json: Static) { + return new this( + json.seq, + Address.fromJSON(json.src), + Address.fromJSON(json.dst), + json.data, + ); + } +}; + +export class ResponseError { + constructor( + public message: string, + public code?: string, + public stack?: string, + ) { } + + static jsonSchema = Type.Object({ + "message": Type.String(), + "code": Type.Optional(Type.String()), + "stack": Type.Optional(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.message, json.code, json.stack); + } +} + +export class MessageResponseError extends Message { + constructor( + public seq: number, + public src: Address, + public dst: Address, + public data: ResponseError, + ) { + super("responseError"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("responseError"), + "seq": Type.Integer(), + "src": Address.jsonSchema, + "dst": Address.jsonSchema, + "data": ResponseError.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this( + json.seq, + Address.fromJSON(json.src), + Address.fromJSON(json.dst), + ResponseError.fromJSON(json.data), + ); + } +}; + +export class MessageEvent extends Message { + constructor( + public seq: number, + public src: Address, + public dst: Address, + public name: string, + public data?: T, + ) { + super("event"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("event"), + "seq": Type.Integer(), + "src": Address.jsonSchema, + "dst": Address.jsonSchema, + "name": Type.String(), + "data": Type.Optional(Type.Unknown()), + }); + + static fromJSON(json: Static) { + return new this( + json.seq, + Address.fromJSON(json.src), + Address.fromJSON(json.dst), + json.name, + json.data, + ); + } +}; + +export class MessageDisconnect extends Message { + constructor( + public data: string, + ) { + super("disconnect"); + } + + static jsonSchema = Type.Object({ + "type": Type.Literal("disconnect"), + "data": StringEnum(["prepare", "ready"]), + }); + + static fromJSON(json: Static) { + return new this(json.data); + } +}; + +Message.jsonSchema = Type.Union([ + MessageHello.jsonSchema, + MessageRegisterHost.jsonSchema, + MessageRegisterControl.jsonSchema, + MessageReady.jsonSchema, + MessageResume.jsonSchema, + MessageContinue.jsonSchema, + MessageInvalidate.jsonSchema, + MessageHeartbeat.jsonSchema, + MessageRequest.jsonSchema, + MessageResponse.jsonSchema, + MessageResponseError.jsonSchema, + MessageEvent.jsonSchema, + MessageDisconnect.jsonSchema, +]); + +export class PingRequest { + static type = "request"; + static src = ["controller", "host", "control"]; + static dst = ["controller", "host", "control"]; + static permission = null; +} diff --git a/packages/lib/src/data/messages_host.js b/packages/lib/src/data/messages_host.js deleted file mode 100644 index d9d50174..00000000 --- a/packages/lib/src/data/messages_host.js +++ /dev/null @@ -1,243 +0,0 @@ -"use strict"; -const { jsonArray } = require("./composites"); - - -/** @memberof module:lib */ -class HostDetails { - /** @type {string} */ - agent; - /** @type {string} */ - version; - /** @type {string} */ - name; - /** @type {number} */ - id; - /** @type {boolean} */ - connected; - /** @type {string|undefined} */ - publicAddress; - - constructor(agent, version, name, id, connected, publicAddress) { - this.agent = agent; - this.version = version; - this.name = name; - this.id = id; - this.connected = connected; - if (publicAddress !== undefined) { this.publicAddress = publicAddress; } - } - - static jsonSchema = { - type: "object", - required: ["agent", "version", "name", "id", "connected"], - properties: { - "agent": { type: "string" }, - "version": { type: "string" }, - "name": { type: "string" }, - "id": { type: "integer" }, - "connected": { type: "boolean" }, - "publicAddress": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.agent, json.version, json.name, json.id, json.connected, json.publicAddress); - } -} - -/** @memberof module:lib */ -class HostListRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.host.list"; - static Response = jsonArray(HostDetails); -} - -/* @memberof module:lib */ -class HostSetSubscriptionsRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.host.subscribe"; - - /** @type {boolean} */ - all; - /** @type {Array} */ - hostIds; - - constructor(all, hostIds) { - this.all = all; - this.hostIds = hostIds; - } - - static jsonSchema = { - type: "object", - required: ["all", "hostIds"], - properties: { - all: { type: "boolean" }, - hostIds: { - type: "array", - items: { type: "integer" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.all, json.hostIds); - } -} - - -/* @memberof module:lib */ -class HostUpdateEvent { - static type = "event"; - static src = "controller"; - static dst = "control"; - - /** @type {module:lib.HostDetails} */ - update; - - constructor(update) { - this.update = update; - } - - static jsonSchema = { - type: "object", - required: ["update"], - properties: { - update: HostDetails.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(HostDetails.fromJSON(json.update)); - } -} - -/** @memberof module:lib */ -class HostMetricsRequest { - static type = "request"; - static src = "controller"; - static dst = "host"; -} -HostMetricsRequest.Response = class Response { // TODO: Use JSON class pattern in Prometheus - /** @type {array} */ - results; - - constructor(results) { - this.results = results; - } - - static jsonSchema = { - type: "object", - required: ["results"], - properties: { - "results": { - type: "array", - }, - }, - }; - - static fromJSON(json) { - return new this(json.results); - } -}; - -/** @memberof module:lib */ -class ControllerConnectionEvent { - static type = "event"; - static src = "host"; - static dst = "instance"; - - /** @type {string} */ - event; - - constructor(event) { - this.event = event; - } - - static jsonSchema = { - type: "object", - required: ["event"], - properties: { - "event": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.event); - } -} - -/* @memberof module:lib */ -class PrepareControllerDisconnectRequest { - static type = "request"; - static src = "host"; - static dst = "instance"; -} - -/** @memberof module:lib */ -class SyncUserListsEvent { - static type = "event"; - static src = "controller"; - static dst = "host"; - - /** @type {Set} */ - adminlist; - /** @type {Map} */ - banlist; - /** @type {Set} */ - whitelist; - - constructor(adminlist, banlist, whitelist) { - this.adminlist = adminlist; - this.banlist = banlist; - this.whitelist = whitelist; - } - - static jsonSchema = { - type: "object", - required: ["adminlist", "banlist", "whitelist"], - properties: { - "adminlist": { - type: "array", - items: { type: "string" }, - }, - "banlist": { - type: "array", - items: { - type: "array", - minItems: 2, - items: [{ type: "string" }, { type: "string" }], - }, - }, - "whitelist": { - type: "array", - items: { type: "string" }, - }, - }, - }; - - static fromJSON(json) { - return new this(new Set(json.adminlist), new Set(json.banlist), new Set(json.whitelist)); - } - - static toJSON() { - return { - adminlist: [...this.adminlist], - banlist: [...this.banlist], - whitelist: [...this.whitelist], - }; - } -} - -module.exports = { - HostDetails, - HostListRequest, - HostSetSubscriptionsRequest, - HostUpdateEvent, - HostMetricsRequest, - ControllerConnectionEvent, - PrepareControllerDisconnectRequest, - SyncUserListsEvent, -}; diff --git a/packages/lib/src/data/messages_host.ts b/packages/lib/src/data/messages_host.ts new file mode 100644 index 00000000..bbde6a6a --- /dev/null +++ b/packages/lib/src/data/messages_host.ts @@ -0,0 +1,157 @@ +import { Type, Static } from "@sinclair/typebox"; +import { jsonArray } from "./composites"; + + +export class HostDetails { + constructor( + public agent: string, + public version: string, + public name: string, + public id: number, + public connected: boolean, + public publicAddress?: string, + ) { } + + static jsonSchema = Type.Object({ + "agent": Type.String(), + "version": Type.String(), + "name": Type.String(), + "id": Type.Integer(), + "connected": Type.Boolean(), + "publicAddress": Type.Optional(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.agent, json.version, json.name, json.id, json.connected, json.publicAddress); + } +} + +export class HostListRequest { + declare ["constructor"]: typeof HostListRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.host.list"; + static Response = jsonArray(HostDetails); +} + +export class HostSetSubscriptionsRequest { + declare ["constructor"]: typeof HostSetSubscriptionsRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.host.subscribe"; + + constructor( + public all: boolean, + public hostIds: number[], + ) { } + + static jsonSchema = Type.Object({ + "all": Type.Boolean(), + "hostIds": Type.Array(Type.Integer()), + }); + + static fromJSON(json: Static) { + return new this(json.all, json.hostIds); + } +} + + +export class HostUpdateEvent { + declare ["constructor"]: typeof HostUpdateEvent; + static type = "event"; + static src = "controller"; + static dst = "control"; + + constructor( + public update: HostDetails, + ) { } + + static jsonSchema = Type.Object({ + "update": HostDetails.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(HostDetails.fromJSON(json.update)); + } +} + +export class HostMetricsRequest { + declare ["constructor"]: typeof HostMetricsRequest; + static type = "request"; + static src = "controller"; + static dst = "host"; + static Response = class Response { // TODO: Use JSON class pattern in Prometheus + constructor( + public results: object[], + ) { } + + static jsonSchema = Type.Object({ + "results": Type.Array(Type.Object({})), + }); + + static fromJSON(json: Static) { + return new this(json.results); + } + }; +} + +export class ControllerConnectionEvent { + declare ["constructor"]: typeof ControllerConnectionEvent; + static type = "event"; + static src = "host"; + static dst = "instance"; + + constructor( + public event: string, + ) { } + + static jsonSchema = Type.Object({ + "event": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.event); + } +} + +export class PrepareControllerDisconnectRequest { + declare ["constructor"]: typeof PrepareControllerDisconnectRequest; + static type = "request"; + static src = "host"; + static dst = "instance"; +} + +export class SyncUserListsEvent { + declare ["constructor"]: typeof SyncUserListsEvent; + static type = "event"; + static src = "controller"; + static dst = "host"; + + constructor( + public adminlist: Set, + public banlist: Map, + public whitelist: Set, + ) { } + + static jsonSchema = Type.Object({ + "adminlist": Type.Array(Type.String()), + "banlist": Type.Array( + Type.Tuple([Type.String(), Type.String()]) + ), + "whitelist": Type.Array(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(new Set(json.adminlist), new Map(json.banlist), new Set(json.whitelist)); + } + + toJSON() { + return { + adminlist: [...this.adminlist], + banlist: [...this.banlist], + whitelist: [...this.whitelist], + }; + } +} diff --git a/packages/lib/src/data/messages_instance.js b/packages/lib/src/data/messages_instance.js deleted file mode 100644 index da661418..00000000 --- a/packages/lib/src/data/messages_instance.js +++ /dev/null @@ -1,1335 +0,0 @@ -"use strict"; -const PlayerStats = require("../PlayerStats").default; -const { JsonString, jsonArray } = require("./composites"); - -/** @memberof module:lib */ -class InstanceDetails { - /** @type {string} */ - name; - /** @type {number} */ - id; - /** @type {?integer} */ - assignedHost; - /** @type {?integer} */ - gamePort; - /** @type {string} */ - status; - - constructor(name, id, assignedHost, gamePort, status) { - this.name = name; - this.id = id; - this.assignedHost = assignedHost; - this.gamePort = gamePort; - this.status = status; - } - - static jsonSchema = { - type: "object", - required: ["name", "id", "assignedHost", "gamePort", "status"], - properties: { - "name": { type: "string" }, - "id": { type: "integer" }, - "assignedHost": { type: ["null", "integer"] }, - "gamePort": { type: ["null", "integer"] }, - "status": { enum: [ - "unknown", "unassigned", "stopped", "starting", "running", "stopping", - "creating_save", "exporting_data", "deleted", - ]}, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.id, json.assignedHost, json.gamePort, json.status); - } -} - -/** @memberof module:lib */ -class InstanceDetailsGetRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.get"; - - /** @type {number} */ - instanceId; - - constructor(instanceId) { - this.instanceId = instanceId; - } - - static jsonSchema = { - type: "object", - required: ["instanceId"], - properties: { - "instanceId": { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId); - } - - static Response = InstanceDetails; -} - -/** @memberof module:lib */ -class InstanceDetailsListRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.list"; - static Response = jsonArray(InstanceDetails); -}; - -/** @memberof module:lib */ -class InstanceDetailsSetSubscriptionsRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.subscribe"; - - /** @type {boolean} */ - all; - - /** @type {Array} */ - instanceIds; - - constructor(all, instanceIds) { - this.all = all || false; - this.instanceIds = instanceIds || []; - } - - static jsonSchema = { - type: "object", - properties: { - "all": { type: "boolean" }, - "instanceIds": { - type: "array", - items: { type: "integer" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.all, json.instanceIds); - } -}; - -/** @memberof module:lib */ -class InstanceDetailsUpdateEvent { - static type = "event"; - static src = "controller"; - static dst = "control"; - - /** @type {module:lib.InstanceDetails} */ - details; - - constructor(details) { - this.details = details; - } - - static jsonSchema = InstanceDetails.jsonSchema; - - toJSON() { - return this.details; - } - - static fromJSON(json) { - return new this(InstanceDetails.fromJSON(json)); - } -}; - -/** @memberof module:lib */ -class InstanceCreateRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.create"; - - /** @type {object} */ - config; - - constructor(config) { - this.config = config; - } - - static jsonSchema = { - type: "object", - required: ["config"], - properties: { - "config": { type: "object" }, - }, - }; - - static fromJSON(json) { - // TODO deserialise config here after config refactor - return new this(json.config); - } -} - -/** @memberof module:lib */ -class InstanceConfigGetRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.get_config"; - - /** @type {number} */ - instanceId; - - constructor(instanceId) { - this.instanceId = instanceId; - } - - static jsonSchema = { - type: "object", - required: ["instanceId"], - properties: { - "instanceId": { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId); - } -} -// TODO replace with InstanceConfig after config refactor -InstanceConfigGetRequest.Response = class Response { - /** @type {object} */ - config; - - constructor(config) { - this.config = config; - } - - static jsonSchema = { - type: "object", - required: ["config"], - properties: { - "config": { type: "object" }, - }, - }; - - static fromJSON(json) { - return new this(json.config); - } -}; - -/** @memberof module:lib */ -class InstanceConfigSetFieldRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.update_config"; - - /** @type {number} */ - instanceId; - /** @type {string} */ - field; - /** @type {string} */ - value; - - constructor(instanceId, field, value) { - this.instanceId = instanceId; - this.field = field; - this.value = value; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "field", "value"], - properties: { - "instanceId": { type: "integer" }, - "field": { type: "string" }, - "value": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.field, json.value); - } -} - -/** @memberof module:lib */ -class InstanceConfigSetPropRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.update_config"; - - /** @type {number} */ - instanceId; - /** @type {string} */ - field; - /** @type {string} */ - prop; - /** @type {*} */ - value; - - constructor(instanceId, field, prop, value) { - this.instanceId = instanceId; - this.field = field; - this.prop = prop; - if (value !== undefined) { - this.value = value; - } - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "field", "prop"], - properties: { - "instanceId": { type: "integer" }, - "field": { type: "string" }, - "prop": { type: "string" }, - "value": {}, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.field, json.prop, json.value); - } -} - -/** @memberof module:lib */ -class InstanceAssignRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.assign"; - - /** @type {number} */ - instanceId; - /** @type {?number} */ - hostId; - - constructor(instanceId, hostId) { - this.instanceId = instanceId; - this.hostId = hostId; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "hostId"], - properties: { - "instanceId": { type: "number" }, - "hostId": { type: ["number", "null"] }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.hostId); - } -} - -/** @memberof module:lib */ -class InstanceMetricsRequest { - static type = "request"; - static src = "host"; - static dst = "instance"; -} -InstanceMetricsRequest.Response = class Response { // TODO: Use JSON class pattern in Prometheus - /** @type {array} */ - results; - - constructor(results) { - this.results = results; - } - - static jsonSchema = { - type: "object", - required: ["results"], - properties: { - "results": { - type: "array", - }, - }, - }; - - static fromJSON(json) { - return new this(json.results); - } -}; - -/** @memberof module:lib */ -class InstanceStartRequest { - static type = "request"; - static src = ["control", "controller"]; - static dst = "instance"; - static permission = "core.instance.start"; - - /** @type {string|undefined} */ - save; - - constructor(save) { - if (save !== undefined) { this.save = save; }; - } - - static jsonSchema = { - type: "object", - properties: { - "save": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.save); - } -} - -/** @memberof module:lib */ -class SaveDetails { - /** @type {string} */ - type; - /** @type {string} */ - name; - /** @type {number} */ - size; - /** @type {number} */ - mtimeMs; - /** @type {boolean} */ - loaded; - /** @type {boolean} */ - loadByDefault; - - constructor(type, name, size, mtimeMs, loaded, loadByDefault) { - this.type = type; - this.name = name; - this.size = size; - this.mtimeMs = mtimeMs; - this.loaded = loaded; - this.loadByDefault = loadByDefault; - } - - static jsonSchema = { - type: "object", - required: ["type", "name", "size", "mtimeMs", "loaded"], - properties: { - "type": { enum: ["file", "directory", "special"] }, - "name": { type: "string" }, - "size": { type: "integer" }, - "mtimeMs": { type: "number" }, - "loaded": { type: "boolean" }, - "loadByDefault": { type: "boolean" }, - }, - }; - - static fromJSON(json) { - return new this(json.type, json.name, json.size, json.mtimeMs, json.loaded, json.loadByDefault); - } -} - -/** @memberof module:lib */ -class InstanceListSavesRequest { - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.save.list"; - static Response = jsonArray(SaveDetails); -} - -/** @memberof module:lib */ -class InstanceSetSaveListSubscriptionsRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.save.list_subscribe"; - - /** @type {boolean} */ - all; - - /** @type {Array} */ - instanceIds; - - constructor(all, instanceIds) { - this.all = all || false; - this.instanceIds = instanceIds || []; - } - - static jsonSchema = { - type: "object", - properties: { - "all": { type: "boolean" }, - "instanceIds": { - type: "array", - items: { type: "integer" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.all, json.instanceIds); - } -}; - -/** @memberof module:lib */ -class InstanceSaveListUpdateEvent { - static type = "event"; - static src = ["instance", "host", "controller"]; - static dst = ["controller", "control"]; - - /** @type {number} */ - instanceId; - /** @type {module:lib.SaveDetails} */ - saves; - - constructor(instanceId, saves) { - this.instanceId = instanceId; - this.saves = saves; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "saves"], - properties: { - "instanceId": { type: "integer" }, - "saves": { - type: "array", - items: SaveDetails.jsonSchema, - }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.saves.map(i => SaveDetails.fromJSON(i))); - } -} - -/** @memberof module:lib */ -class InstanceCreateSaveRequest { - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.save.create"; - - /** @type {string} */ - name; - /** @type {?number} */ - seed; - /** @type {?object} */ - mapGenSettings; - /** @type {?object} */ - mapSettings; - - constructor(name, seed, mapGenSettings, mapSettings) { - this.name = name; - if (seed) { this.seed = seed; } - if (mapGenSettings) { this.mapGenSettings = mapGenSettings; } - if (mapSettings) { this.mapSettings = mapSettings; } - } - - static jsonSchema = { - type: "object", - required: ["name"], - properties: { - "name": { type: "string" }, - "seed": { type: "integer" }, - "mapGenSettings": { type: "object" }, - "mapSettings": { type: "object" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.seed, json.mapGenSettings, json.mapSettings); - } -} - -/** @memberof module:lib */ -class InstanceRenameSaveRequest { - static type = "request"; - static src = ["control", "controller"]; - static dst = ["controller", "host"]; - static permission = "core.instance.save.rename"; - - /** @type {number} */ - instanceId; - /** @type {string} */ - oldName; - /** @type {string} */ - newName; - - constructor(instanceId, oldName, newName) { - this.instanceId = instanceId; - this.oldName = oldName; - this.newName = newName; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "oldName", "newName"], - properties: { - "instanceId": { type: "integer" }, - "oldName": { type: "string" }, - "newName": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.oldName, json.newName); - } -} - -/** @memberof module:lib */ -class InstanceCopySaveRequest { - static type = "request"; - static src = ["control", "controller"]; - static dst = ["controller", "host"]; - static permission = "core.instance.save.copy"; - - /** @type {number} */ - instanceId; - /** @type {string} */ - source; - /** @type {string} */ - destination; - - constructor(instanceId, source, destination) { - this.instanceId = instanceId; - this.source = source; - this.destination = destination; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "source", "destination"], - properties: { - "instanceId": { type: "integer" }, - "source": { type: "string" }, - "destination": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.source, json.destination); - } -} - -/** @memberof module:lib */ -class InstanceDeleteSaveRequest { - static type = "request"; - static src = ["control", "controller"]; - static dst = ["controller", "host"]; - static permission = "core.instance.save.delete"; - - /** @type {number} */ - instanceId; - /** @type {string} */ - name; - - constructor(instanceId, name) { - this.instanceId = instanceId; - this.name = name; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "name"], - properties: { - "instanceId": { type: "integer" }, - "name": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.name); - } -} - -/** @memberof module:lib */ -class InstanceDownloadSaveRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.save.download"; - - /** @type {number} */ - instanceId; - /** @type {string} */ - name; - - constructor(instanceId, name) { - this.instanceId = instanceId; - this.name = name; - } - - static jsonSchema = { - type: "object", - required: ["name"], - properties: { - "instanceId": { type: "integer" }, - "name": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.name); - } - - static Response = JsonString; -} - -/** @memberof module:lib */ -class InstanceTransferSaveRequest { - static type = "request"; - static src = ["control", "controller"]; - static dst = ["controller", "host"]; - static permission(user, message) { - user.checkPermission("core.instance.save.transfer"); - if (message.data.copy) { - user.checkPermission("core.instance.save.copy"); - } else if (message.data.sourceName !== message.data.targetName) { - user.checkPermission("core.instance.save.rename"); - } - } - - - /** @type {number} */ - sourceInstanceId; - /** @type {string} */ - sourceName; - /** @type {number} */ - targetInstanceId; - /** @type {string} */ - targetName; - /** @type {boolean} */ - copy; - - constructor(sourceInstanceId, sourceName, targetInstanceId, targetName, copy) { - this.sourceInstanceId = sourceInstanceId; - this.sourceName = sourceName; - this.targetInstanceId = targetInstanceId; - this.targetName = targetName; - this.copy = copy; - } - - static jsonSchema = { - type: "object", - required: ["sourceInstanceId", "sourceName", "targetInstanceId", "targetName", "copy"], - properties: { - "sourceInstanceId": { type: "number" }, - "sourceName": { type: "string" }, - "targetInstanceId": { type: "number" }, - "targetName": { type: "string" }, - "copy": { type: "boolean" }, - }, - }; - - static fromJSON(json) { - return new this( - json.sourceInstanceId, json.sourceName, json.targetInstanceId, json.targetName, json.copy - ); - } - - static Response = JsonString; -} - - -/** @memberof module:lib */ -class InstancePullSaveRequest { - static type = "request"; - static src = "controller"; - static dst = "host"; - - /** @type {number} */ - instanceId; - /** @type {string} */ - streamId; - /** @type {string} */ - name; - - constructor(instanceId, streamId, name) { - this.instanceId = instanceId; - this.streamId = streamId; - this.name = name; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "streamId", "name"], - properties: { - "instanceId": { type: "integer" }, - "streamId": { type: "string" }, - "name": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.streamId, json.name); - } - - static Response = JsonString; -} - -/** @memberof module:lib */ -class InstancePushSaveRequest { - static type = "request"; - static src = "controller"; - static dst = "host"; - - /** @type {number} */ - instanceId; - /** @type {string} */ - streamId; - /** @type {string} */ - name; - - constructor(instanceId, streamId, name) { - this.instanceId = instanceId; - this.streamId = streamId; - this.name = name; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "streamId", "name"], - properties: { - "instanceId": { type: "integer" }, - "streamId": { type: "string" }, - "name": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.streamId, json.name); - } -} - -/** @memberof module:lib */ -class InstanceLoadScenarioRequest { - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.load_scenario"; - - /** @type {string} */ - scenario; - /** @type {?number} */ - seed; - /** @type {?object} */ - mapGenSettings; - /** @type {?object} */ - mapSettings; - - constructor(scenario, seed, mapGenSettings, mapSettings) { - this.scenario = scenario; - if (seed) { this.seed = seed; } - if (mapGenSettings) { this.mapGenSettings = mapGenSettings; } - if (mapSettings) { this.mapSettings = mapSettings; } - } - - static jsonSchema = { - type: "object", - required: ["scenario"], - properties: { - "scenario": { type: "string" }, - "seed": { type: "integer" }, - "mapGenSettings": { type: "object" }, - "mapSettings": { type: "object" }, - }, - }; - - static fromJSON(json) { - return new this(json.scenario, json.seed, json.mapGenSettings, json.mapSettings); - } -} - -/** @memberof module:lib */ -class InstanceExportDataRequest { - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.export_data"; -} - -/** @memberof module:lib */ -class InstanceExtractPlayersRequest { - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.extract_players"; -} - -/** @memberof module:lib */ -class InstanceStopRequest { - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.stop"; -} - -/** @memberof module:lib */ -class InstanceKillRequest { - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.kill"; -} - -/** @memberof module:lib */ -class InstanceDeleteRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.delete"; - - /** @type {number} */ - instanceId; - - constructor(instanceId) { - this.instanceId = instanceId; - } - - static jsonSchema = { - type: "object", - required: ["instanceId"], - properties: { - instanceId: { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId); - } -} - -/** @memberof module:lib */ -class InstanceDeleteInternalRequest { - static type = "request"; - static src = "controller"; - static dst = "host"; - - /** @type {number} */ - instanceId; - - constructor(instanceId) { - this.instanceId = instanceId; - } - - static jsonSchema = { - type: "object", - required: ["instanceId"], - properties: { - instanceId: { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId); - } -} - -/** @memberof module:lib */ -class InstanceSendRconRequest { - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.send_rcon"; - - /** @type {string} */ - command; - - constructor(command) { - this.command = command; - } - - static jsonSchema = { - type: "object", - required: ["command"], - properties: { - "command": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.command); - } - - static Response = JsonString; -} - -// TODO remove this after config refactor -/** @memberof module:lib */ -class RawInstanceInfo { - /** @type {object} */ - config; - - /** @type {string} */ - status; - - constructor(config, status) { - this.config = config; - this.status = status; - } - - static jsonSchema = { - type: "object", - required: ["config", "status"], - properties: { - "config": { type: "object" }, - "status": { enum: [ - "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", - ]}, - }, - }; - - static fromJSON(json) { - return new this(json.config, json.status); - } -} - -/** @memberof module:lib */ -class InstancesUpdateRequest { - static type = "request"; - static src = "host"; - static dst = "controller"; - - /** @type {Array} */ - instances; - - constructor(instances) { - this.instances = instances; - } - - static jsonSchema = { - type: "array", - items: RawInstanceInfo.jsonSchema, - }; - - toJSON() { - return this.instances; - } - - static fromJSON(json) { - return new this(json.map(i => RawInstanceInfo.fromJSON(i))); - } -} - -/** @memberof module:lib */ -class InstanceAssignInternalRequest { - static type = "request"; - static src = "controller"; - static dst = "host"; - - /** @type {number} */ - instanceId; - /** @type {object} */ - config; - - constructor(instanceId, config) { - this.instanceId = instanceId; - this.config = config; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "config"], - properties: { - "instanceId": { type: "integer" }, - "config": { type: "object" }, - }, - }; - - static fromJSON(json) { - // TODO deserialise config here after config refactor - return new this(json.instanceId, json.config); - } -} - -/** @memberof module:lib */ -class InstanceUnassignInternalRequest { - static type = "request"; - static src = "controller"; - static dst = "host"; - - /** @type {number} */ - instanceId; - - constructor(instanceId) { - this.instanceId = instanceId; - } - - static jsonSchema = { - type: "object", - required: ["instanceId"], - properties: { - "instanceId": { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId); - } -} - -/** @memberof module:lib */ -class InstanceInitialisedEvent { - static type = "event"; - static src = "instance"; - static dst = "host"; - - /** @type {Object} */ - plugins; - - constructor(plugins) { - this.plugins = plugins; - } - - static jsonSchema = { - type: "object", - required: ["plugins"], - properties: { - "plugins": { - type: "object", - additionalProperties: { type: "string" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.plugins); - } -} - -/** @memberof module:lib */ -class InstanceStatusChangedEvent { - static type = "event"; - static src = ["instance", "host"]; - static dst = "controller"; - - /** @type {integer} */ - instanceId; - /** @type {string} */ - status; - /** @type {?number} */ - gamePort; - - constructor(instanceId, status, gamePort) { - this.instanceId = instanceId; - this.status = status; - this.gamePort = gamePort; - } - - static jsonSchema = { - type: "object", - required: ["instanceId", "status", "gamePort"], - properties: { - "instanceId": { type: "integer" }, - "status": { - type: "string", - enum: [ - "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", - ], - }, - "gamePort": { type: ["null", "integer"] }, - }, - }; - - static fromJSON(json) { - return new this(json.instanceId, json.status, json.gamePort); - } -} - -/** @memberof module:lib */ -class InstanceDetailsChangedEvent { - static type = "event"; - static src = "instance"; - static dst = "controller"; - - /** @type {module:lib.InstanceDetails} */ - details; - - constructor(details) { - this.details = details; - } - - static jsonSchema = InstanceDetails.jsonSchema; - - static fromJSON(json) { - return new this(InstanceDetails.fromJSON(json)); - } -} - -/** @memberof module:lib */ -class InstanceBanlistUpdateEvent { - static type = "event"; - static src = "controller"; - static dst = "instance"; - - /** @type {string} */ - name; - - /** @type {boolean} */ - banned; - - /** @type {string} */ - reason; - - constructor(name, banned, reason) { - this.name = name; - this.banned = banned; - this.reason = reason; - } - - static jsonSchema = { - type: "object", - required: ["name", "banned", "reason"], - properties: { - "name": { type: "string" }, - "banned": { type: "boolean" }, - "reason": { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.banned, json.reason); - } -} - -/** @memberof module:lib */ -class InstanceAdminlistUpdateEvent { - static type = "event"; - static src = "controller"; - static dst = "instance"; - - /** @type {string} */ - name; - - /** @type {boolean} */ - admin; - - constructor(name, admin) { - this.name = name; - this.admin = admin; - } - - static jsonSchema = { - type: "object", - required: ["name", "admin"], - properties: { - "name": { type: "string" }, - "admin": { type: "boolean" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.admin); - } -} - -/** @memberof module:lib */ -class InstanceWhitelistUpdateEvent { - static type = "event"; - static src = "controller"; - static dst = "instance"; - - /** @type {string} */ - name; - - /** @type {boolean} */ - whitelisted; - - constructor(name, whitelisted) { - this.name = name; - this.whitelisted = whitelisted; - } - - static jsonSchema = { - type: "object", - required: ["name", "whitelisted"], - properties: { - "name": { type: "string" }, - "whitelisted": { type: "boolean" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.whitelisted); - } -} - -/** @memberof module:lib */ -class InstancePlayerUpdateEvent { - static type = "event"; - static src = "instance"; - static dst = "controller"; - - /** @type {string} */ - type; - - /** @type {string} */ - name; - - /** @type {?boolean} */ - reason; - - /** @type {?string} */ - stats; - - constructor(type, name, reason, stats) { - this.type = type; - this.name = name; - if (reason !== undefined) { this.reason = reason; } - if (stats !== undefined) { this.stats = stats; } - } - - static jsonSchema = { - type: "object", - required: ["type", "name"], - properties: { - "type": { type: "string", enum: ["join", "leave", "import"] }, - "name": { type: "string" }, - "reason": { type: "string" }, - "stats": PlayerStats.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.whitelisted); - } -} - -module.exports = { - InstanceDetails, - InstanceDetailsGetRequest, - InstanceDetailsListRequest, - InstanceDetailsSetSubscriptionsRequest, - InstanceDetailsUpdateEvent, - InstanceCreateRequest, - InstanceConfigGetRequest, - InstanceConfigSetFieldRequest, - InstanceConfigSetPropRequest, - InstanceAssignRequest, - InstanceMetricsRequest, - InstanceStartRequest, - SaveDetails, - InstanceListSavesRequest, - InstanceSetSaveListSubscriptionsRequest, - InstanceSaveListUpdateEvent, - InstanceCreateSaveRequest, - InstanceRenameSaveRequest, - InstanceCopySaveRequest, - InstanceDeleteSaveRequest, - InstanceDownloadSaveRequest, - InstanceTransferSaveRequest, - InstancePullSaveRequest, - InstancePushSaveRequest, - InstanceLoadScenarioRequest, - InstanceExportDataRequest, - InstanceExtractPlayersRequest, - InstanceStopRequest, - InstanceKillRequest, - InstanceDeleteRequest, - InstanceDeleteInternalRequest, - InstanceSendRconRequest, - RawInstanceInfo, - InstancesUpdateRequest, - InstanceAssignInternalRequest, - InstanceUnassignInternalRequest, - InstanceInitialisedEvent, - InstanceStatusChangedEvent, - InstanceDetailsChangedEvent, - InstanceBanlistUpdateEvent, - InstanceAdminlistUpdateEvent, - InstanceWhitelistUpdateEvent, - InstancePlayerUpdateEvent, -}; diff --git a/packages/lib/src/data/messages_instance.ts b/packages/lib/src/data/messages_instance.ts new file mode 100644 index 00000000..214a665a --- /dev/null +++ b/packages/lib/src/data/messages_instance.ts @@ -0,0 +1,916 @@ +import { Type, Static } from "@sinclair/typebox"; +import PlayerStats from "../PlayerStats"; +import { JsonString, StringEnum, jsonArray } from "./composites"; +import type { User } from "../users"; +import type { MessageRequest } from "./messages_core"; + +export type InstanceStatus = + "unknown" | "unassigned" | "stopped" | "starting" | "running" + | "stopping" | "creating_save" | "exporting_data" | "deleted" +; +export class InstanceDetails { + constructor( + public name: string, + public id: number, + public assignedHost: number | undefined, + public gamePort: number | undefined, + public status: InstanceStatus, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "id": Type.Integer(), + "assignedHost": Type.Optional(Type.Integer()), + "gamePort": Type.Optional(Type.Integer()), + "status": StringEnum([ + "unknown", "unassigned", "stopped", "starting", "running", "stopping", + "creating_save", "exporting_data", "deleted", + ]), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.id, json.assignedHost, json.gamePort, json.status); + } +} + +export class InstanceDetailsGetRequest { + declare ["constructor"]: typeof InstanceDetailsGetRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.get"; + + constructor( + public instanceId: number, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId); + } + + static Response = InstanceDetails; +} + +export class InstanceDetailsListRequest { + declare ["constructor"]: typeof InstanceDetailsListRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.list"; + static Response = jsonArray(InstanceDetails); +}; + +export class InstanceDetailsSetSubscriptionsRequest { + declare ["constructor"]: typeof InstanceDetailsSetSubscriptionsRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.subscribe"; + + constructor( + public all: boolean = false, + public instanceIds: number[] = [], + ) { } + + static jsonSchema = Type.Object({ + "all": Type.Boolean(), + "instanceIds": Type.Array(Type.Integer()), + }); + + static fromJSON(json: Static) { + return new this(json.all, json.instanceIds); + } +}; + +export class InstanceDetailsUpdateEvent { + declare ["constructor"]: typeof InstanceDetailsUpdateEvent; + static type = "event"; + static src = "controller"; + static dst = "control"; + + constructor( + public details: InstanceDetails, + ) { } + + static jsonSchema = InstanceDetails.jsonSchema; + + toJSON() { + return this.details; + } + + static fromJSON(json: Static) { + return new this(InstanceDetails.fromJSON(json)); + } +}; + +export class InstanceCreateRequest { + declare ["constructor"]: typeof InstanceCreateRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.create"; + + constructor( + public config: object, + ) { } + + static jsonSchema = Type.Object({ + "config": Type.Object({}), + }); + + static fromJSON(json: Static) { + // TODO deserialise config here after config refactor + return new this(json.config); + } +} + +export class InstanceConfigGetRequest { + declare ["constructor"]: typeof InstanceConfigGetRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.get_config"; + + constructor( + public instanceId: number, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId); + } + + // TODO replace with InstanceConfig after config refactor + static Response = class Response { + constructor( + public config: object, + ) { } + + static jsonSchema = Type.Object({ + "config": Type.Object({}), + }); + + static fromJSON(json: Static) { + return new this(json.config); + } + }; +} + +export class InstanceConfigSetFieldRequest { + declare ["constructor"]: typeof InstanceConfigSetFieldRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.update_config"; + + constructor( + public instanceId: number, + public field: string, + public value: string, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "field": Type.String(), + "value": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.field, json.value); + } +} + +export class InstanceConfigSetPropRequest { + declare ["constructor"]: typeof InstanceConfigSetPropRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.update_config"; + + constructor( + public instanceId: number, + public field: string, + public prop: string, + public value?: unknown, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "field": Type.String(), + "prop": Type.String(), + "value": Type.Unknown(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.field, json.prop, json.value); + } +} + +export class InstanceAssignRequest { + declare ["constructor"]: typeof InstanceAssignRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.assign"; + + constructor( + public instanceId: number, + public hostId?: number, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Number(), + "hostId": Type.Optional(Type.Number()), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.hostId); + } +} + +export class InstanceMetricsRequest { + declare ["constructor"]: typeof InstanceMetricsRequest; + static type = "request"; + static src = "host"; + static dst = "instance"; + + static Response = class Response { // TODO: Use JSON class pattern in Prometheus + constructor( + public results: object[], + ) { } + + static jsonSchema = Type.Object({ + "results": Type.Array(Type.Object({})), + }); + + static fromJSON(json: Static) { + return new this(json.results); + } + }; +} + +export class InstanceStartRequest { + declare ["constructor"]: typeof InstanceStartRequest; + static type = "request"; + static src = ["control", "controller"]; + static dst = "instance"; + static permission = "core.instance.start"; + + constructor( + public save?: string, + ) { } + + static jsonSchema = Type.Object({ + "save": Type.Optional(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.save); + } +} + +export class SaveDetails { + constructor( + public type: string, + public name: string, + public size: number, + public mtimeMs: number, + public loaded: boolean, + public loadByDefault: boolean, + ) { } + + static jsonSchema = Type.Object({ + "type": StringEnum(["file", "directory", "special"]), + "name": Type.String(), + "size": Type.Integer(), + "mtimeMs": Type.Number(), + "loaded": Type.Boolean(), + "loadByDefault": Type.Boolean(), + }); + + static fromJSON(json: Static) { + return new this(json.type, json.name, json.size, json.mtimeMs, json.loaded, json.loadByDefault); + } +} + +export class InstanceListSavesRequest { + declare ["constructor"]: typeof InstanceListSavesRequest; + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.save.list"; + static Response = jsonArray(SaveDetails); +} + +export class InstanceSetSaveListSubscriptionsRequest { + declare ["constructor"]: typeof InstanceSetSaveListSubscriptionsRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.save.list_subscribe"; + + constructor( + public all: boolean = false, + public instanceIds: number[] = [], + ) { } + + static jsonSchema = Type.Object({ + "all": Type.Boolean(), + "instanceIds": Type.Array(Type.Integer()), + }); + + static fromJSON(json: Static) { + return new this(json.all, json.instanceIds); + } +}; + +export class InstanceSaveListUpdateEvent { + declare ["constructor"]: typeof InstanceSaveListUpdateEvent; + static type = "event"; + static src = ["instance", "host", "controller"]; + static dst = ["controller", "control"]; + + constructor( + public instanceId: number, + public saves: SaveDetails[], + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "saves": Type.Array(SaveDetails.jsonSchema), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.saves.map(i => SaveDetails.fromJSON(i))); + } +} + +export class InstanceCreateSaveRequest { + declare ["constructor"]: typeof InstanceCreateSaveRequest; + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.save.create"; + + constructor( + public name: string, + public seed?: number, + public mapGenSettings?: object, + public mapSettings?: object, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "seed": Type.Optional(Type.Integer()), + "mapGenSettings": Type.Optional(Type.Object({})), + "mapSettings": Type.Optional(Type.Object({})), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.seed, json.mapGenSettings, json.mapSettings); + } +} + +export class InstanceRenameSaveRequest { + declare ["constructor"]: typeof InstanceRenameSaveRequest; + static type = "request"; + static src = ["control", "controller"]; + static dst = ["controller", "host"]; + static permission = "core.instance.save.rename"; + + constructor( + public instanceId: number, + public oldName: string, + public newName: string, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "oldName": Type.String(), + "newName": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.oldName, json.newName); + } +} + +export class InstanceCopySaveRequest { + declare ["constructor"]: typeof InstanceCopySaveRequest; + static type = "request"; + static src = ["control", "controller"]; + static dst = ["controller", "host"]; + static permission = "core.instance.save.copy"; + + constructor( + public instanceId: number, + public source: string, + public destination: string, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "source": Type.String(), + "destination": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.source, json.destination); + } +} + +export class InstanceDeleteSaveRequest { + declare ["constructor"]: typeof InstanceDeleteSaveRequest; + static type = "request"; + static src = ["control", "controller"]; + static dst = ["controller", "host"]; + static permission = "core.instance.save.delete"; + + constructor( + public instanceId: number, + public name: string, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "name": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.name); + } +} + +export class InstanceDownloadSaveRequest { + declare ["constructor"]: typeof InstanceDownloadSaveRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.save.download"; + + constructor( + public instanceId: number, + public name: string, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "name": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.name); + } + + static Response = JsonString; +} + +export class InstanceTransferSaveRequest { + declare ["constructor"]: typeof InstanceTransferSaveRequest; + static type = "request"; + static src = ["control", "controller"]; + static dst = ["controller", "host"]; + static permission(user: User, message: MessageRequest) { + user.checkPermission("core.instance.save.transfer"); + if (typeof message.data === "object" && message.data !== null) { + const data = message.data as { copy: boolean, sourceName: string, targetName: string }; + if (data.copy) { + user.checkPermission("core.instance.save.copy"); + } else if (data.sourceName !== data.targetName) { + user.checkPermission("core.instance.save.rename"); + } + } + } + + constructor( + public sourceInstanceId: number, + public sourceName: string, + public targetInstanceId: number, + public targetName: string, + public copy: boolean, + ) { } + + static jsonSchema = Type.Object({ + "sourceInstanceId": Type.Number(), + "sourceName": Type.String(), + "targetInstanceId": Type.Number(), + "targetName": Type.String(), + "copy": Type.Boolean(), + }); + + static fromJSON(json: Static) { + return new this( + json.sourceInstanceId, json.sourceName, json.targetInstanceId, json.targetName, json.copy + ); + } + + static Response = JsonString; +} + + +export class InstancePullSaveRequest { + declare ["constructor"]: typeof InstancePullSaveRequest; + static type = "request"; + static src = "controller"; + static dst = "host"; + + constructor( + public instanceId: number, + public streamId: string, + public name: string, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "streamId": Type.String(), + "name": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.streamId, json.name); + } + + static Response = JsonString; +} + +export class InstancePushSaveRequest { + declare ["constructor"]: typeof InstancePushSaveRequest; + static type = "request"; + static src = "controller"; + static dst = "host"; + + constructor( + public instanceId: number, + public streamId: string, + public name: string, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "streamId": Type.String(), + "name": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.streamId, json.name); + } +} + +export class InstanceLoadScenarioRequest { + declare ["constructor"]: typeof InstanceLoadScenarioRequest; + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.load_scenario"; + + constructor( + public scenario: string, + public seed?: number, + public mapGenSettings?: object, + public mapSettings?: object, + ) { } + + static jsonSchema = Type.Object({ + "scenario": Type.String(), + "seed": Type.Optional(Type.Integer()), + "mapGenSettings": Type.Optional(Type.Object({})), + "mapSettings": Type.Optional(Type.Object({})), + }); + + static fromJSON(json: Static) { + return new this(json.scenario, json.seed, json.mapGenSettings, json.mapSettings); + } +} + +export class InstanceExportDataRequest { + declare ["constructor"]: typeof InstanceExportDataRequest; + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.export_data"; +} + +export class InstanceExtractPlayersRequest { + declare ["constructor"]: typeof InstanceExtractPlayersRequest; + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.extract_players"; +} + +export class InstanceStopRequest { + declare ["constructor"]: typeof InstanceStopRequest; + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.stop"; +} + +export class InstanceKillRequest { + declare ["constructor"]: typeof InstanceKillRequest; + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.kill"; +} + +export class InstanceDeleteRequest { + declare ["constructor"]: typeof InstanceDeleteRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.delete"; + + constructor( + public instanceId: number, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId); + } +} + +export class InstanceDeleteInternalRequest { + declare ["constructor"]: typeof InstanceDeleteInternalRequest; + static type = "request"; + static src = "controller"; + static dst = "host"; + + constructor( + public instanceId: number, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId); + } +} + +export class InstanceSendRconRequest { + declare ["constructor"]: typeof InstanceSendRconRequest; + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.send_rcon"; + + constructor( + public command: string, + ) { } + + static jsonSchema = Type.Object({ + "command": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.command); + } + + static Response = JsonString; +} + +// TODO remove this after config refactor +export class RawInstanceInfo { + + constructor( + public config: object, + public status: string, + ) { } + + static jsonSchema = Type.Object({ + "config": Type.Object({}), + "status": StringEnum([ + "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", + ]), + }); + + static fromJSON(json: Static) { + return new this(json.config, json.status); + } +} + +export class InstancesUpdateRequest { + declare ["constructor"]: typeof InstancesUpdateRequest; + static type = "request"; + static src = "host"; + static dst = "controller"; + + constructor( + public instances: RawInstanceInfo[], + ) { } + + static jsonSchema = Type.Array(RawInstanceInfo.jsonSchema); + + toJSON() { + return this.instances; + } + + static fromJSON(json: Static) { + return new this(json.map(i => RawInstanceInfo.fromJSON(i))); + } +} + +export class InstanceAssignInternalRequest { + declare ["constructor"]: typeof InstanceAssignInternalRequest; + static type = "request"; + static src = "controller"; + static dst = "host"; + + constructor( + public instanceId: number, + public config: object, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "config": Type.Object({}), + }); + + static fromJSON(json: Static) { + // TODO deserialise config here after config refactor + return new this(json.instanceId, json.config); + } +} + +export class InstanceUnassignInternalRequest { + declare ["constructor"]: typeof InstanceUnassignInternalRequest; + static type = "request"; + static src = "controller"; + static dst = "host"; + + constructor( + public instanceId: number, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId); + } +} + +export class InstanceInitialisedEvent { + declare ["constructor"]: typeof InstanceInitialisedEvent; + static type = "event"; + static src = "instance"; + static dst = "host"; + + constructor( + public plugins: Record, + ) { } + + static jsonSchema = Type.Object({ + "plugins": Type.Record(Type.String(), Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.plugins); + } +} + +export class InstanceStatusChangedEvent { + declare ["constructor"]: typeof InstanceStatusChangedEvent; + static type = "event"; + static src = ["instance", "host"]; + static dst = "controller"; + + constructor( + public instanceId: number, + public status: string, + public gamePort?: number, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "status": StringEnum([ + "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", + ]), + "gamePort": Type.Optional(Type.Integer()), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.status, json.gamePort); + } +} + +export class InstanceDetailsChangedEvent { + declare ["constructor"]: typeof InstanceDetailsChangedEvent; + static type = "event"; + static src = "instance"; + static dst = "controller"; + + constructor( + public details: InstanceDetails, + ) { } + + static jsonSchema = InstanceDetails.jsonSchema; + + static fromJSON(json: Static) { + return new this(InstanceDetails.fromJSON(json)); + } +} + +export class InstanceBanlistUpdateEvent { + declare ["constructor"]: typeof InstanceBanlistUpdateEvent; + static type = "event"; + static src = "controller"; + static dst = "instance"; + + constructor( + public name: string, + public banned: boolean, + public reason: string, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "banned": Type.Boolean(), + "reason": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.banned, json.reason); + } +} + +export class InstanceAdminlistUpdateEvent { + declare ["constructor"]: typeof InstanceAdminlistUpdateEvent; + static type = "event"; + static src = "controller"; + static dst = "instance"; + + constructor( + public name: string, + public admin: boolean, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "admin": Type.Boolean(), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.admin); + } +} + +export class InstanceWhitelistUpdateEvent { + declare ["constructor"]: typeof InstanceWhitelistUpdateEvent; + static type = "event"; + static src = "controller"; + static dst = "instance"; + + constructor( + public name: string, + public whitelisted: boolean, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "whitelisted": Type.Boolean(), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.whitelisted); + } +} + +export class InstancePlayerUpdateEvent { + declare ["constructor"]: typeof InstancePlayerUpdateEvent; + static type = "event"; + static src = "instance"; + static dst = "controller"; + + constructor( + public type: string, + public name: string, + public reason?: string, + public stats?: PlayerStats, + ) { } + + static jsonSchema = Type.Object({ + "type": StringEnum(["join", "leave", "import"]), + "name": Type.String(), + "reason": Type.Optional(Type.String()), + "stats": Type.Unsafe(PlayerStats.jsonSchema), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.name, json.reason, PlayerStats.fromJSON(json.stats)); + } +} diff --git a/packages/lib/src/data/messages_mod.js b/packages/lib/src/data/messages_mod.js deleted file mode 100644 index 7f4d34de..00000000 --- a/packages/lib/src/data/messages_mod.js +++ /dev/null @@ -1,474 +0,0 @@ -"use strict"; -const ModPack = require("./ModPack"); -const ModInfo = require("./ModInfo"); -const { jsonArray, JsonString } = require("./composites"); - - -/* @memberof module:lib */ -class ModPackGetRequest { - static type = "request"; - static src = ["instance", "control"]; - static dst = "controller"; - static permission = "core.mod_pack.get"; - - /** @type {number} */ - id; - - constructor(id) { - this.id = id; - } - - static jsonSchema = { - type: "object", - required: ["id"], - properties: { - id: { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.id); - } - - static Response = ModPack; -} - -/* @memberof module:lib */ -class ModPackGetDefaultRequest { - static type = "request"; - static src = ["instance", "control"]; - static dst = "controller"; - static permission = "core.mod_pack.get"; - static Response = ModPack; -} - -/* @memberof module:lib */ -class ModPackListRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod_pack.list"; - static Response = jsonArray(ModPack); -} - -/** @memberof module:lib */ -class ModPackCreateRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod_pack.create"; - - /** @type {module:lib.ModPack} */ - modPack; - - constructor(modPack) { - this.modPack = modPack; - } - - static jsonSchema = { - type: "object", - required: ["modPack"], - properties: { - "modPack": ModPack.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(ModPack.fromJSON(json.modPack)); - } -} - -/* @memberof module:lib */ -class ModPackUpdateRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod_pack.update"; - - /** @type {module:lib.ModPack} */ - modPack; - - constructor(modPack) { - this.modPack = modPack; - } - - static jsonSchema = { - type: "object", - required: ["modPack"], - properties: { - "modPack": ModPack.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(ModPack.fromJSON(json.modPack)); - } -} - -/* @memberof module:lib */ -class ModPackDeleteRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod_pack.delete"; - - /** @type {number} */ - id; - - constructor(id) { - this.id = id; - } - - static jsonSchema = { - type: "object", - required: ["id"], - properties: { - id: { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.id); - } -} - -/* @memberof module:lib */ -class ModPackSetSubscriptionsRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod_pack.subscribe"; - - /** @type {boolean} */ - all; - /** @type {Array} */ - modPackIds; - - constructor(all, modPackIds) { - this.all = all; - this.modPackIds = modPackIds; - } - - static jsonSchema = { - type: "object", - required: ["all", "modPackIds"], - properties: { - all: { type: "boolean" }, - modPackIds: { - type: "array", - items: { type: "integer" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.all, json.modPackIds); - } -} - -/* @memberof module:lib */ -class ModGetRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.get"; - - /** @type {string} */ - name; - /** @type {string} */ - version; - - constructor(name, version) { - this.name = name; - this.version = version; - } - - static jsonSchema = { - type: "object", - required: ["name", "version"], - properties: { - name: { type: "string" }, - version: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.version); - } - - static Response = ModInfo; -} - -/* @memberof module:lib */ -class ModListRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.list"; - static Response = jsonArray(ModInfo); -} - -/* @memberof module:lib */ -class ModSearchRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.search"; - - /** @type {string} */ - query; - /** @type {string} */ - factorioVersion; - /** @type {number} */ - page; - /** @type {number|undefined} */ - pageSize; - /** @type {string|undefined} */ - sort; - /** @type {string|undefined} */ - sortOrder; - - constructor(query, factorioVersion, page, pageSize, sort, sortOrder) { - this.query = query; - this.factorioVersion = factorioVersion; - this.page = page; - if (pageSize) { this.pageSize = pageSize; } - if (sort) { this.sort = sort; } - if (sortOrder) { this.sortOrder = sortOrder; } - } - - static jsonSchema = { - type: "object", - required: ["query", "factorioVersion", "page"], - properties: { - query: { type: "string" }, - factorioVersion: { type: "string" }, - pageSize: { type: "integer" }, - page: { type: "integer" }, - sort: { type: "string" }, - sortOrder: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.query, json.factorioVersion, json.page, json.pageSize, json.sort, json.sortOrder); - } -} -ModSearchRequest.Response = class Response { - /** @type {Array} */ - queryIssues; - /** @type {number} */ - pageCount; - /** @type {number} */ - resultCount; - /** @type {Array} */ - results; - - constructor(queryIssues, pageCount, resultCount, results) { - this.queryIssues = queryIssues; - this.pageCount = pageCount; - this.resultCount = resultCount; - this.results = results; - } - - static jsonSchema = { - type: "object", - required: ["queryIssues", "pageCount", "resultCount", "results"], - properties: { - queryIssues: { - type: "array", - items: { type: "string" }, - }, - pageCount: { type: "integer" }, - resultCount: { type: "integer" }, - results: { - type: "array", - items: { - type: "object", - properties: { - name: { type: "string" }, - versions: { - type: "array", - items: ModInfo.jsonSchema, - }, - }, - }, - }, - }, - }; - - static fromJSON(json) { - let results = json.results.map( - ({ name, versions }) => ({ name, versions: versions.map(mod => ModInfo.fromJSON(mod)) }) - ); - return new this(json.queryIssues, json.pageCount, json.resultCount, results); - } -}; - -/* @memberof module:lib */ -class ModSetSubscriptionsRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.subscribe"; - - /** @type {boolean} */ - all; - /** @type {Array} */ - modNames; - - constructor(all, modNames) { - this.all = all; - this.modNames = modNames; - } - - static jsonSchema = { - type: "object", - required: ["all", "modNames"], - properties: { - all: { type: "boolean" }, - modNames: { - type: "array", - items: { type: "string" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.all, json.modNames); - } -} - -/* @memberof module:lib */ -class ModDownloadRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.download"; - - /** @type {string} */ - name; - /** @type {string} */ - version; - - constructor(name, version) { - this.name = name; - this.version = version; - } - - static jsonSchema = { - type: "object", - required: ["name", "version"], - properties: { - name: { type: "string" }, - version: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.version); - } - - static Response = JsonString; -} - -/* @memberof module:lib */ -class ModDeleteRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.delete"; - - /** @type {string} */ - name; - /** @type {string} */ - version; - - constructor(name, version) { - this.name = name; - this.version = version; - } - - static jsonSchema = { - type: "object", - required: ["name", "version"], - properties: { - name: { type: "string" }, - version: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.version); - } -} - -/* @memberof module:lib */ -class ModPackUpdateEvent { - static type = "event"; - static src = "controller"; - static dst = "control"; - - /** @type {module:lib.ModPack} */ - modPack; - - constructor(modPack) { - this.modPack = modPack; - } - - static jsonSchema = { - type: "object", - required: ["modPack"], - properties: { - modPack: ModPack.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(ModPack.fromJSON(json.modPack)); - } -} - -/* @memberof module:lib */ -class ModUpdateEvent { - static type = "event"; - static src = "controller"; - static dst = "control"; - - /** @type {module:lib.ModInfo} */ - mod; - - constructor(mod) { - this.mod = mod; - } - - static jsonSchema = { - type: "object", - required: ["mod"], - properties: { - mod: ModInfo.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(ModInfo.fromJSON(json.mod)); - } -} - -module.exports = { - ModPackGetRequest, - ModPackGetDefaultRequest, - ModPackListRequest, - ModPackCreateRequest, - ModPackUpdateRequest, - ModPackDeleteRequest, - ModPackSetSubscriptionsRequest, - ModGetRequest, - ModListRequest, - ModSearchRequest, - ModSetSubscriptionsRequest, - ModDownloadRequest, - ModDeleteRequest, - ModPackUpdateEvent, - ModUpdateEvent, -}; diff --git a/packages/lib/src/data/messages_mod.ts b/packages/lib/src/data/messages_mod.ts new file mode 100644 index 00000000..a32aac3f --- /dev/null +++ b/packages/lib/src/data/messages_mod.ts @@ -0,0 +1,324 @@ +import { Type, Static } from "@sinclair/typebox"; +import ModInfo from "./ModInfo"; +import ModPack from "./ModPack"; +import { JsonString, jsonArray } from "./composites"; + + +export class ModPackGetRequest { + declare ["constructor"]: typeof ModPackGetRequest; + static type = "request"; + static src = ["instance", "control"]; + static dst = "controller"; + static permission = "core.mod_pack.get"; + + constructor( + public id: number, + ) { } + + static jsonSchema = Type.Object({ + "id": Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.id); + } + + static Response = ModPack; +} + +export class ModPackGetDefaultRequest { + declare ["constructor"]: typeof ModPackGetDefaultRequest; + static type = "request"; + static src = ["instance", "control"]; + static dst = "controller"; + static permission = "core.mod_pack.get"; + static Response = ModPack; +} + +export class ModPackListRequest { + declare ["constructor"]: typeof ModPackListRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod_pack.list"; + static Response = jsonArray(ModPack); +} + +export class ModPackCreateRequest { + declare ["constructor"]: typeof ModPackCreateRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod_pack.create"; + + constructor( + public modPack: ModPack, + ) { } + + static jsonSchema = Type.Object({ + "modPack": Type.Unsafe(ModPack.jsonSchema), + }); + + static fromJSON(json: Static) { + return new this(ModPack.fromJSON(json.modPack)); + } +} + +export class ModPackUpdateRequest { + declare ["constructor"]: typeof ModPackUpdateRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod_pack.update"; + + constructor( + public modPack: ModPack, + ) { } + + static jsonSchema = Type.Object({ + "modPack": Type.Unsafe(ModPack.jsonSchema), + }); + + static fromJSON(json: Static) { + return new this(ModPack.fromJSON(json.modPack)); + } +} + +export class ModPackDeleteRequest { + declare ["constructor"]: typeof ModPackDeleteRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod_pack.delete"; + + constructor( + public id: number, + ) { } + + static jsonSchema = Type.Object({ + "id": Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.id); + } +} + +export class ModPackSetSubscriptionsRequest { + declare ["constructor"]: typeof ModPackSetSubscriptionsRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod_pack.subscribe"; + + constructor( + public all: boolean, + public modPackIds: number[], + ) { } + + static jsonSchema = Type.Object({ + "all": Type.Boolean(), + "modPackIds": Type.Array(Type.Integer()), + }); + + static fromJSON(json: Static) { + return new this(json.all, json.modPackIds); + } +} + +export class ModGetRequest { + declare ["constructor"]: typeof ModGetRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod.get"; + + constructor( + public name: string, + public version: string, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "version": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.version); + } + + static Response = ModInfo; +} + +export class ModListRequest { + declare ["constructor"]: typeof ModListRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod.list"; + static Response = jsonArray(ModInfo); +} + +export class ModSearchRequest { + declare ["constructor"]: typeof ModSearchRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod.search"; + + constructor( + public query: string, + public factorioVersion: string, + public page: number, + public pageSize?: number, + public sort?: string, + public sortOrder?: string, + ) { } + + static jsonSchema = Type.Object({ + "query": Type.String(), + "factorioVersion": Type.String(), + "pageSize": Type.Integer(), + "page": Type.Optional(Type.Integer()), + "sort": Type.Optional(Type.String()), + "sortOrder": Type.Optional(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.query, json.factorioVersion, json.page, json.pageSize, json.sort, json.sortOrder); + } + + static Response = class Response { + constructor( + public queryIssues: string[], + public pageCount: number, + public resultCount: number, + public results: object[], + ) { } + + static jsonSchema = Type.Object({ + "queryIssues": Type.Array(Type.String()), + "pageCount": Type.Integer(), + "resultCount": Type.Integer(), + "results": Type.Array( + Type.Object({ + "name": Type.String(), + "versions": Type.Array(Type.Unsafe(ModInfo.jsonSchema)), + }), + ), + }); + + static fromJSON(json: Static) { + let results = json.results.map( + ({ name, versions }) => ({ name, versions: versions.map(mod => ModInfo.fromJSON(mod)) }) + ); + return new this(json.queryIssues, json.pageCount, json.resultCount, results); + } + }; +} + +export class ModSetSubscriptionsRequest { + declare ["constructor"]: typeof ModSetSubscriptionsRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod.subscribe"; + + constructor( + public all: boolean, + public modNames: string[], + ) { } + + static jsonSchema = Type.Object({ + "all": Type.Boolean(), + "modNames": Type.Array(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.all, json.modNames); + } +} + +export class ModDownloadRequest { + declare ["constructor"]: typeof ModDownloadRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod.download"; + + constructor( + public name: string, + public version: string, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "version": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.version); + } + + static Response = JsonString; +} + +export class ModDeleteRequest { + declare ["constructor"]: typeof ModDeleteRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.mod.delete"; + + constructor( + public name: string, + public version: string, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "version": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.version); + } +} + +export class ModPackUpdateEvent { + declare ["constructor"]: typeof ModPackUpdateEvent; + static type = "event"; + static src = "controller"; + static dst = "control"; + + constructor( + public modPack: ModPack, + ) { } + + static jsonSchema = Type.Object({ + "modPack": Type.Unsafe(ModPack.jsonSchema), + }); + + static fromJSON(json: Static) { + return new this(ModPack.fromJSON(json.modPack)); + } +} + +export class ModUpdateEvent { + declare ["constructor"]: typeof ModUpdateEvent; + static type = "event"; + static src = "controller"; + static dst = "control"; + + constructor( + public mod: ModInfo, + ) { } + + static jsonSchema = Type.Object({ + "mod": Type.Unsafe(ModInfo.jsonSchema), + }); + + static fromJSON(json: Static) { + return new this(ModInfo.fromJSON(json.mod)); + } +} diff --git a/packages/lib/src/data/messages_user.js b/packages/lib/src/data/messages_user.js deleted file mode 100644 index edd391cd..00000000 --- a/packages/lib/src/data/messages_user.js +++ /dev/null @@ -1,708 +0,0 @@ -"use strict"; -const PlayerStats = require("../PlayerStats").default; -const { jsonArray, JsonNumber } = require("./composites"); - -/* @memberof module:lib */ -class RawPermission { // TODO refactor into lib/user.Permission - /** @type {string} */ - name; - /** @type {string} */ - title; - /** @type {string} */ - description; - - constructor(name, title, description) { - this.name = name; - this.title = title; - this.description = description; - } - - static jsonSchema = { - type: "object", - required: ["name", "title", "description"], - properties: { - name: { type: "string" }, - title: { type: "string" }, - description: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.title, json.description); - } -} - -/* @memberof module:lib */ -class PermissionListRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.permission.list"; - static Response = jsonArray(RawPermission); -} - -class RawRole { // TODO refactor into lib/user.Role - /** @type {number} */ - id; - /** @type {string} */ - name; - /** @type {string} */ - description; - /** @type {Array} */ - permissions; - - constructor(id, name, description, permissions) { - this.id = id; - this.name = name; - this.description = description; - this.permissions = permissions; - } - - static jsonSchema = { - type: "object", - required: ["id", "name", "description", "permissions"], - properties: { - id: { type: "integer" }, - name: { type: "string" }, - description: { type: "string" }, - permissions: { - type: "array", - items: { type: "string" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.id, json.name, json.description, json.permissions); - } -} - -/* @memberof module:lib */ -class RoleListRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.role.list"; - static Response = jsonArray(RawRole); -} - -/* @memberof module:lib */ -class RoleCreateRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.role.create"; - - /** @type {string} */ - name; - /** @type {string} */ - description; - /** @type {Array} */ - permissions; - - constructor(name, description, permissions) { - this.name = name; - this.description = description; - this.permissions = permissions; - } - - static jsonSchema = { - type: "object", - required: ["name", "description", "permissions"], - properties: { - name: { type: "string" }, - description: { type: "string" }, - permissions: { - type: "array", - items: { type: "string" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.description, json.permissions); - } - - static Response = JsonNumber; -} - -/* @memberof module:lib */ -class RoleUpdateRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.role.update"; - - /** @type {number} */ - id; - /** @type {string} */ - name; - /** @type {string} */ - description; - /** @type {Array} */ - permissions; - - constructor(id, name, description, permissions) { - this.id = id; - this.name = name; - this.description = description; - this.permissions = permissions; - } - - static jsonSchema = { - type: "object", - required: ["id", "name", "description", "permissions"], - properties: { - id: { type: "integer" }, - name: { type: "string" }, - description: { type: "string" }, - permissions: { - type: "array", - items: { type: "string" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.id, json.name, json.description, json.permissions); - } -} - -/* @memberof module:lib */ -class RoleGrantDefaultPermissionsRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.role.update"; - - /** @type {number} */ - id; - - constructor(id) { - this.id = id; - } - - static jsonSchema = { - type: "object", - required: ["id"], - properties: { - id: { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.id); - } -} - -/* @memberof module:lib */ -class RoleDeleteRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.role.delete"; - - /** @type {number} */ - id; - - constructor(id) { - this.id = id; - } - - static jsonSchema = { - type: "object", - required: ["id"], - properties: { - id: { type: "integer" }, - }, - }; - - static fromJSON(json) { - return new this(json.id); - } -} - -/* @memberof module:lib */ -class RawUser { // TODO refactor into lib/user.User - /** @type {string} */ - name; - /** @type {Array} */ - roles; - /** @type {Array} */ - instances; - /** @type {boolean|undefined} */ - isAdmin; - /** @type {boolean|undefined} */ - isBanned; - /** @type {boolean|undefined} */ - isWhitelisted; - /** @type {string|undefined} */ - banReason; - /** @type {boolean|undefined} */ - isDeleted; - /** @type {module:lib.PlayerStats} */ - playerStats; - /** @type {Map|undefined} */ - instanceStats; - - constructor( - name, roles, instances, isAdmin, isBanned, isWhitelisted, banReason, - isDeleted, playerStats, instanceStats - ) { - this.name = name; - this.roles = roles; - this.instances = instances; - if (isAdmin !== undefined) { this.isAdmin = isAdmin; } - if (isBanned !== undefined) { this.isBanned = isBanned; } - if (isWhitelisted !== undefined) { this.isWhitelisted = isWhitelisted; } - if (banReason !== undefined) { this.banReason = banReason; } - if (isDeleted !== undefined) { this.isDeleted = isDeleted; } - if (playerStats !== undefined) { this.playerStats = playerStats; } - if (instanceStats !== undefined) { this.instanceStats = instanceStats; } - } - - static jsonSchema = { - type: "object", - required: ["name", "roles", "instances"], - properties: { - name: { type: "string" }, - roles: { type: "array", items: { type: "integer" } }, - instances: { type: "array", items: { type: "integer" } }, - isAdmin: { type: "boolean" }, - isBanned: { type: "boolean" }, - isWhitelisted: { type: "boolean" }, - banReason: { type: "string" }, - isDeleted: { type: "boolean" }, - playerStats: PlayerStats.jsonSchema, - instanceStats: { - type: "array", - items: { - type: "array", - minItems: 2, - items: [{ type: "integer" }, PlayerStats.jsonSchema], - }, - }, - }, - }; - - static fromJSON(json) { - let playerStats; - if (json.playerStats) { - playerStats = PlayerStats.fromJSON(json.playerStats); - } - let instanceStats; - if (json.instanceStats) { - instanceStats = new Map( - json.instanceStats.map(([id, stats]) => [id, PlayerStats.fromJSON(stats)]) - ); - } - return new this( - json.name, json.roles, json.instances, json.isAdmin, json.isBanned, json.isWhitelisted, json.banReason, - json.isDeleted, playerStats, instanceStats - ); - } - - toJSON() { - const json = { - name: this.name, - roles: this.roles, - instances: this.instances, - isAdmin: this.isAdmin, - isBanned: this.isBanned, - isWhitelisted: this.isWhitelisted, - banReason: this.banReason, - isDeleted: this.isDeleted, - playerStats: this.playerStats, - }; - if (this.instanceStats) { - json.instanceStats = [...this.instanceStats]; - } - return json; - } -} - -/* @memberof module:lib */ -class UserGetRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.get"; - - /** @type {string} */ - name; - - constructor(name) { - this.name = name; - } - - static jsonSchema = { - type: "object", - required: ["name"], - properties: { - name: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.name); - } - - static Response = RawUser; -} - -/* @memberof module:lib */ -class UserListRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.list"; - static Response = jsonArray(RawUser); -} - -/* @memberof module:lib */ -class UserSetSubscriptionsRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.subscribe"; - - /** @type {boolean} */ - all; - /** @type {Array} */ - names; - - constructor(all, names) { - this.all = all; - this.names = names; - } - - static jsonSchema = { - type: "object", - required: ["all", "names"], - properties: { - all: { type: "boolean" }, - names: { - type: "array", - items: { type: "string" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.all, json.names); - } -} - -/* @memberof module:lib */ -class UserCreateRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.create"; - - /** @type {string} */ - name; - - constructor(name) { - this.name = name; - } - - static jsonSchema = { - type: "object", - required: ["name"], - properties: { - name: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.name); - } -} - -/* @memberof module:lib */ -class UserRevokeTokenRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.revoke_token"; - - /** @type {string} */ - name; - - constructor(name) { - this.name = name; - } - - static jsonSchema = { - type: "object", - required: ["name"], - properties: { - name: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.name); - } -} - -/* @memberof module:lib */ -class UserUpdateRolesRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.update_roles"; - - /** @type {string} */ - name; - /** @type {Array} */ - roles; - - constructor(name, roles) { - this.name = name; - this.roles = roles; - } - - static jsonSchema = { - type: "object", - required: ["name", "roles"], - properties: { - name: { type: "string" }, - roles: { - type: "array", - items: { type: "integer" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.roles); - } -} - -/* @memberof module:lib */ -class UserSetAdminRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.set_admin"; - - /** @type {string} */ - name; - /** @type {boolean} */ - create; - /** @type {boolean} */ - admin; - - constructor(name, create, admin) { - this.name = name; - this.create = create; - this.admin = admin; - } - - static jsonSchema = { - type: "object", - required: ["name", "create", "admin"], - properties: { - name: { type: "string" }, - create: { type: "boolean" }, - admin: { type: "boolean" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.create, json.admin); - } -} - -/* @memberof module:lib */ -class UserSetWhitelistedRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.set_whitelisted"; - - /** @type {string} */ - name; - /** @type {boolean} */ - create; - /** @type {boolean} */ - whitelisted; - - constructor(name, create, whitelisted) { - this.name = name; - this.create = create; - this.whitelisted = whitelisted; - } - - static jsonSchema = { - type: "object", - required: ["name", "create", "whitelisted"], - properties: { - name: { type: "string" }, - create: { type: "boolean" }, - whitelisted: { type: "boolean" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.create, json.whitelisted); - } -} - -/* @memberof module:lib */ -class UserSetBannedRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.set_banned"; - - /** @type {string} */ - name; - /** @type {boolean} */ - create; - /** @type {boolean} */ - banned; - /** @type {string} */ - reason; - - constructor(name, create, banned, reason) { - this.name = name; - this.create = create; - this.banned = banned; - this.reason = reason; - } - - static jsonSchema = { - type: "object", - required: ["name", "create", "banned", "reason"], - properties: { - name: { type: "string" }, - create: { type: "boolean" }, - banned: { type: "boolean" }, - reason: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.name, json.create, json.banned, json.reason); - } -} - -/* @memberof module:lib */ -class UserDeleteRequest { - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.delete"; - - /** @type {string} */ - name; - - constructor(name) { - this.name = name; - } - - static jsonSchema = { - type: "object", - required: ["name"], - properties: { - name: { type: "string" }, - }, - }; - - static fromJSON(json) { - return new this(json.name); - } -} - - -/* @memberof module:lib */ -class AccountUpdateEvent { - static type = "event"; - static src = "controller"; - static dst = "control"; - - /** @type {Array|undefined} */ - roles; - - constructor(roles) { - if (roles) { this.roles = roles; } - } - - static jsonSchema = { - type: "object", - properties: { - roles: { - type: "array", - items: { - type: "object", - required: ["name", "id", "permissions"], - properties: { - name: { type: "string" }, - id: { type: "integer" }, - permissions: { - type: "array", - items: { type: "string" }, - }, - }, - }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.roles); - } -} - -/* @memberof module:lib */ -class UserUpdateEvent { - static type = "event"; - static src = "controller"; - static dst = "control"; - - /** @type {module:lib.RawUser} */ - user; - - constructor(user) { - this.user = user; - } - - static jsonSchema = { - type: "object", - required: ["user"], - properties: { - user: RawUser.jsonSchema, - }, - }; - - static fromJSON(json) { - return new this(RawUser.fromJSON(json.user)); - } -} - -module.exports = { - RawPermission, - PermissionListRequest, - RawRole, - RoleListRequest, - RoleCreateRequest, - RoleUpdateRequest, - RoleGrantDefaultPermissionsRequest, - RoleDeleteRequest, - RawUser, - UserGetRequest, - UserListRequest, - UserSetSubscriptionsRequest, - UserCreateRequest, - UserRevokeTokenRequest, - UserUpdateRolesRequest, - UserSetAdminRequest, - UserSetWhitelistedRequest, - UserSetBannedRequest, - UserDeleteRequest, - AccountUpdateEvent, - UserUpdateEvent, -}; diff --git a/packages/lib/src/data/messages_user.ts b/packages/lib/src/data/messages_user.ts new file mode 100644 index 00000000..a9b72769 --- /dev/null +++ b/packages/lib/src/data/messages_user.ts @@ -0,0 +1,472 @@ +import { Type, Static } from "@sinclair/typebox"; +import PlayerStats from "../PlayerStats"; +import { JsonNumber, jsonArray } from "./composites"; + +export class RawPermission { // TODO refactor into lib/user.Permission + constructor( + public name: string, + public title: string, + public description: string, + ) { } + + static jsonSchema = Type.Object({ + name: Type.String(), + title: Type.String(), + description: Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.title, json.description); + } +} + +export class PermissionListRequest { + declare ["constructor"]: typeof PermissionListRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.permission.list"; + static Response = jsonArray(RawPermission); +} + +export class RawRole { // TODO refactor into lib/user.Role + constructor( + public id: number, + public name: string, + public description: string, + public permissions: string[], + ) { } + + static jsonSchema = Type.Object({ + id: Type.Integer(), + name: Type.String(), + description: Type.String(), + permissions: Type.Array(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.id, json.name, json.description, json.permissions); + } +} + +export class RoleListRequest { + declare ["constructor"]: typeof RoleListRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.role.list"; + static Response = jsonArray(RawRole); +} + +export class RoleCreateRequest { + declare ["constructor"]: typeof RoleCreateRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.role.create"; + + constructor( + public name: string, + public description: string, + public permissions: string[], + ) { } + + static jsonSchema = Type.Object({ + name: Type.String(), + description: Type.String(), + permissions: Type.Array(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.description, json.permissions); + } + + static Response = JsonNumber; +} + +export class RoleUpdateRequest { + declare ["constructor"]: typeof RoleUpdateRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.role.update"; + + constructor( + public id: number, + public name: string, + public description: string, + public permissions: string[], + ) { } + + static jsonSchema = Type.Object({ + id: Type.Integer(), + name: Type.String(), + description: Type.String(), + permissions: Type.Array(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.id, json.name, json.description, json.permissions); + } +} + +export class RoleGrantDefaultPermissionsRequest { + declare ["constructor"]: typeof RoleGrantDefaultPermissionsRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.role.update"; + + constructor( + public id: number, + ) { } + + static jsonSchema = Type.Object({ + id: Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.id); + } +} + +export class RoleDeleteRequest { + declare ["constructor"]: typeof RoleDeleteRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.role.delete"; + + constructor( + public id: number, + ) { } + + static jsonSchema = Type.Object({ + id: Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.id); + } +} + +export class RawUser { // TODO refactor into lib/user.User + constructor( + public name: string, + public roles: number[], + public instances: number[], + public isAdmin?: boolean, + public isBanned?: boolean, + public isWhitelisted?: boolean, + public banReason?: string, + public isDeleted?: boolean, + public playerStats?: PlayerStats, + public instanceStats?: Map, + ) { } + + static jsonSchema = Type.Object({ + name: Type.String(), + roles: Type.Array(Type.Integer()), + instances: Type.Array(Type.Integer()), + isAdmin: Type.Optional(Type.Boolean()), + isBanned: Type.Optional(Type.Boolean()), + isWhitelisted: Type.Optional(Type.Boolean()), + banReason: Type.Optional(Type.String()), + isDeleted: Type.Optional(Type.Boolean()), + playerStats: Type.Optional(Type.Unsafe(PlayerStats.jsonSchema)), + instanceStats: Type.Optional( + Type.Array( + Type.Tuple([Type.Integer(), Type.Unsafe(PlayerStats.jsonSchema)]), + ) + ), + }); + + static fromJSON(json: Static) { + let playerStats: PlayerStats; + if (json.playerStats) { + playerStats = PlayerStats.fromJSON(json.playerStats); + } + let instanceStats: Map; + if (json.instanceStats) { + instanceStats = new Map( + json.instanceStats.map(([id, stats]) => [id, PlayerStats.fromJSON(stats)]) + ); + } + return new this( + json.name, json.roles, json.instances, json.isAdmin, json.isBanned, json.isWhitelisted, json.banReason, + json.isDeleted, playerStats, instanceStats + ); + } + + toJSON() { + const json: Static = { + name: this.name, + roles: this.roles, + instances: this.instances, + isAdmin: this.isAdmin, + isBanned: this.isBanned, + isWhitelisted: this.isWhitelisted, + banReason: this.banReason, + isDeleted: this.isDeleted, + playerStats: this.playerStats, + }; + if (this.instanceStats) { + json.instanceStats = [...this.instanceStats]; + } + return json; + } +} + +export class UserGetRequest { + declare ["constructor"]: typeof UserGetRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.user.get"; + + constructor( + public name: string, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.name); + } + + static Response = RawUser; +} + +export class UserListRequest { + declare ["constructor"]: typeof UserListRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.user.list"; + static Response = jsonArray(RawUser); +} + +export class UserSetSubscriptionsRequest { + declare ["constructor"]: typeof UserSetSubscriptionsRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.user.subscribe"; + + constructor( + public all: boolean, + public names: string[], + ) { } + + static jsonSchema = Type.Object({ + "all": Type.Boolean(), + "names": Type.Array(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.all, json.names); + } +} + +export class UserCreateRequest { + declare ["constructor"]: typeof UserCreateRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.user.create"; + + constructor( + public name: string, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.name); + } +} + +export class UserRevokeTokenRequest { + declare ["constructor"]: typeof UserRevokeTokenRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.user.revoke_token"; + + constructor( + public name: string, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.name); + } +} + +export class UserUpdateRolesRequest { + declare ["constructor"]: typeof UserUpdateRolesRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.user.update_roles"; + + constructor( + public name: string, + public roles: number[], + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "roles": Type.Array(Type.Integer()), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.roles); + } +} + +export class UserSetAdminRequest { + declare ["constructor"]: typeof UserSetAdminRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.user.set_admin"; + + constructor( + public name: string, + public create: boolean, + public admin: boolean, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "create": Type.Boolean(), + "admin": Type.Boolean(), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.create, json.admin); + } +} + +export class UserSetWhitelistedRequest { + declare ["constructor"]: typeof UserSetWhitelistedRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.user.set_whitelisted"; + + constructor( + public name: string, + public create: boolean, + public whitelisted: boolean, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "create": Type.Boolean(), + "whitelisted": Type.Boolean(), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.create, json.whitelisted); + } +} + +export class UserSetBannedRequest { + declare ["constructor"]: typeof UserSetBannedRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.user.set_banned"; + + constructor( + public name: string, + public create: boolean, + public banned: boolean, + public reason: string, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + "create": Type.Boolean(), + "banned": Type.Boolean(), + "reason": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.create, json.banned, json.reason); + } +} + +export class UserDeleteRequest { + declare ["constructor"]: typeof UserDeleteRequest; + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.user.delete"; + + constructor( + public name: string, + ) { } + + static jsonSchema = Type.Object({ + "name": Type.String(), + }); + + static fromJSON(json: Static) { + return new this(json.name); + } +} + + +export class AccountUpdateEvent { + declare ["constructor"]: typeof AccountUpdateEvent; + static type = "event"; + static src = "controller"; + static dst = "control"; + + constructor( + public roles?: { name: string, id: number, permissions: string[] }[], + ) { } + + static jsonSchema = Type.Object({ + roles: Type.Array( + Type.Object({ + name: Type.String(), + id: Type.Integer(), + permissions: Type.Array(Type.String()), + }), + ), + }); + + static fromJSON(json: Static) { + return new this(json.roles); + } +} + +export class UserUpdateEvent { + declare ["constructor"]: typeof UserUpdateEvent; + static type = "event"; + static src = "controller"; + static dst = "control"; + + constructor( + public user: RawUser, + ) { } + + static jsonSchema = Type.Object({ + "user": RawUser.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(RawUser.fromJSON(json.user)); + } +} diff --git a/packages/lib/src/link/link.ts b/packages/lib/src/link/link.ts index f62411a6..20d50713 100644 --- a/packages/lib/src/link/link.ts +++ b/packages/lib/src/link/link.ts @@ -176,10 +176,10 @@ export class Link { ); } else if (message.type === "response") { - this._processResponse(message); + this._processResponse(message as libData.MessageResponse); } else if (message.type === "responseError") { - this._processResponseError(message); + this._processResponseError(message as libData.MessageResponseError); } else if (message.type === "event") { this._processEvent( @@ -362,9 +362,13 @@ export class Link { } else if (!(err instanceof libErrors.RequestError)) { logger.error(`Unexpected error responding to ${message.name}:\n${err.stack}`); } - this.connector.sendResponseError( - new libData.ResponseError(err.message, err.code, err.stack), message.src, spoofedSrc - ); + try { + this.connector.sendResponseError( + new libData.ResponseError(err.message, err.code, err.stack), message.src, spoofedSrc + ); + } catch (err) { + logger.error(`Unexpected error sending error response for ${message.name}:\n${err.stack}`); + } } ); } diff --git a/packages/web_ui/src/components/CreateSaveModal.jsx b/packages/web_ui/src/components/CreateSaveModal.jsx index 5d7395f9..100fd4fe 100644 --- a/packages/web_ui/src/components/CreateSaveModal.jsx +++ b/packages/web_ui/src/components/CreateSaveModal.jsx @@ -16,9 +16,9 @@ export default function CreateSaveModal(props) { function createSave() { let values = form.getFieldsValue(); let name = values.saveName || "world.zip"; - let seed = values.seed === undefined ? null : Number.parseInt(values.seed, 10); - let mapGenSettings = null; - let mapSettings = null; + let seed = values.seed === undefined ? undefined : Number.parseInt(values.seed, 10); + let mapGenSettings; + let mapSettings; if (values.exchangeString && values.exchangeString.trim()) { let result; try { diff --git a/packages/web_ui/src/components/LoadScenarioModal.jsx b/packages/web_ui/src/components/LoadScenarioModal.jsx index 0da4a12b..0fda52ef 100644 --- a/packages/web_ui/src/components/LoadScenarioModal.jsx +++ b/packages/web_ui/src/components/LoadScenarioModal.jsx @@ -16,9 +16,9 @@ export default function LoadScenarioModal(props) { function loadScenario() { let values = form.getFieldsValue(); let scenario = values.scenario || "base/freeplay"; - let seed = values.seed === undefined ? null : Number.parseInt(values.seed, 10); - let mapGenSettings = null; - let mapSettings = null; + let seed = values.seed === undefined ? undefined : Number.parseInt(values.seed, 10); + let mapGenSettings; + let mapSettings; if (values.exchangeString && values.exchangeString.trim()) { let result; try { diff --git a/packages/web_ui/src/components/LogConsole.jsx b/packages/web_ui/src/components/LogConsole.jsx index 53c31ef0..820d8586 100644 --- a/packages/web_ui/src/components/LogConsole.jsx +++ b/packages/web_ui/src/components/LogConsole.jsx @@ -75,7 +75,7 @@ export default function LogConsole(props) { logFilter.controller, logFilter.hostIds, logFilter.instanceIds, - null, + undefined, 400, "desc", )).then(result => { diff --git a/packages/web_ui/src/util/websocket.jsx b/packages/web_ui/src/util/websocket.jsx index 3f06a68c..4bac6df1 100644 --- a/packages/web_ui/src/util/websocket.jsx +++ b/packages/web_ui/src/util/websocket.jsx @@ -565,7 +565,7 @@ export class Control extends lib.Link { } } - await this.send(new lib.LogSetSubscriptionsRequest(all, controller, hostIds, instanceIds, null)); + await this.send(new lib.LogSetSubscriptionsRequest(all, controller, hostIds, instanceIds, undefined)); } async handleDebugWsMessageEvent(message) { diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 8e01758d..56620cd2 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -92,12 +92,14 @@ describe("Integration of Clusterio", function() { describe("queryLogRequestHandler", function() { it("should honnor the limit", async function() { - let result = await getControl().send(new lib.LogQueryRequest(true, false, [], [], null, 10, "asc")); + let result = await getControl().send( + new lib.LogQueryRequest(true, false, [], [], undefined, 10, "asc") + ); assert.equal(result.log.length, 10); }); it("should return entries by order", async function() { - let first = await getControl().send(new lib.LogQueryRequest(true, false, [], [], null, 1, "asc")); - let last = await getControl().send(new lib.LogQueryRequest(true, false, [], [], null, 1, "desc")); + let first = await getControl().send(new lib.LogQueryRequest(true, false, [], [], undefined, 1, "asc")); + let last = await getControl().send(new lib.LogQueryRequest(true, false, [], [], undefined, 1, "desc")); assert(first.log[0].timestamp < last.log[0].timestamp, "first log entry happened after last"); }); }); @@ -346,7 +348,7 @@ describe("Integration of Clusterio", function() { slowTest(this); await execCtl("instance send-rcon test technobabble"); let { log } = await getControl().send( - new lib.LogQueryRequest(false, false, [], [44], null, 10, "desc") + new lib.LogQueryRequest(false, false, [], [44], undefined, 10, "desc") ); assert(log.some(info => /technobabble/.test(info.message)), "Command was not sent"); }); diff --git a/test/lib/command.js b/test/lib/command.js index a78adf02..7fd94d91 100644 --- a/test/lib/command.js +++ b/test/lib/command.js @@ -19,7 +19,7 @@ describe("lib/command", function() { lib.HostListRequest, () => [new lib.HostDetails("test", "0.1", "Test Host", 11, false)] ); testController.handle( - lib.InstanceDetailsListRequest, () => [new lib.InstanceDetails("Test Instance", 57, 4, null, "stopped")] + lib.InstanceDetailsListRequest, () => [new lib.InstanceDetails("Test Instance", 57, 4, undefined, "stopped")] ); testController.handle(lib.RoleListRequest, () => [testRole]); From 679c00527f1655dabd94113c28602ac81c7393bd Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 11 Aug 2023 01:13:38 +0200 Subject: [PATCH 09/13] Port remaining lib/data modules to TypeScript --- packages/lib/src/data/ExportManifest.js | 46 ----- packages/lib/src/data/ExportManifest.ts | 33 ++++ .../lib/src/data/{ModInfo.js => ModInfo.ts} | 89 ++++----- .../lib/src/data/{ModPack.js => ModPack.ts} | 180 ++++++++---------- packages/lib/src/data/messages_mod.ts | 10 +- .../lib/src/data/{version.js => version.ts} | 25 +-- 6 files changed, 156 insertions(+), 227 deletions(-) delete mode 100644 packages/lib/src/data/ExportManifest.js create mode 100644 packages/lib/src/data/ExportManifest.ts rename packages/lib/src/data/{ModInfo.js => ModInfo.ts} (70%) rename packages/lib/src/data/{ModPack.js => ModPack.ts} (68%) rename packages/lib/src/data/{version.js => version.ts} (58%) diff --git a/packages/lib/src/data/ExportManifest.js b/packages/lib/src/data/ExportManifest.js deleted file mode 100644 index 9e52a725..00000000 --- a/packages/lib/src/data/ExportManifest.js +++ /dev/null @@ -1,46 +0,0 @@ -"use strict"; - -/** - * Export manifest for a mod pack - * - * Tracks files stored on disk for data that extracted from Factorio using a - * given mod pack. - * @alias module:lib.ExportManifest - */ -class ExportManifest { - - /** - * Mapping between known assets and their file names. - * - * Currently exported assets are - * - settings: JSON of mod settings prototypes. - * - prototypes: JSON of all game prototypes except settings. - * - item-spritesheet: PNG containing icons from the game. - * - item-metadata: JSON of Map entries describing icons in item-spritesheet - * - locale: JSON of flattened Map entries of en locale strings from the game. - * @type {Object} - */ - assets = {}; - - constructor(assets) { - if (assets) { this.assets = assets; } - } - - static jsonSchema = { - type: "object", - additionalProperties: false, - required: ["assets"], - properties: { - "assets": { - type: "object", - additionalProperties: { type: "string" }, - }, - }, - }; - - static fromJSON(json) { - return new this(json.assets); - } -} - -module.exports = ExportManifest; diff --git a/packages/lib/src/data/ExportManifest.ts b/packages/lib/src/data/ExportManifest.ts new file mode 100644 index 00000000..f1abc319 --- /dev/null +++ b/packages/lib/src/data/ExportManifest.ts @@ -0,0 +1,33 @@ +import { Type, Static } from "@sinclair/typebox"; + +/** + * Export manifest for a mod pack + * + * Tracks files stored on disk for data that extracted from Factorio using a + * given mod pack. + */ +export default class ExportManifest { + constructor( + /** + * Mapping between known assets and their file names. + * + * Currently exported assets are + * - settings: JSON of mod settings prototypes. + * - prototypes: JSON of all game prototypes except settings. + * - item-spritesheet: PNG containing icons from the game. + * - item-metadata: JSON of Map entries describing icons in item-spritesheet + * - locale: JSON of flattened Map entries of en locale strings from the game. + */ + public assets: Record + ) { + if (assets) { this.assets = assets; } + } + + static jsonSchema = Type.Object({ + "assets": Type.Record(Type.String(), Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.assets); + } +} diff --git a/packages/lib/src/data/ModInfo.js b/packages/lib/src/data/ModInfo.ts similarity index 70% rename from packages/lib/src/data/ModInfo.js rename to packages/lib/src/data/ModInfo.ts index 8ad802d1..ce9e46b4 100644 --- a/packages/lib/src/data/ModInfo.js +++ b/packages/lib/src/data/ModInfo.ts @@ -1,13 +1,12 @@ -"use strict"; -const fs = require("fs-extra"); -const JSZip = require("jszip"); -const path = require("path"); +import fs from "fs-extra"; +import JSZip from "jszip"; +import { Type, Static } from "@sinclair/typebox"; -const libHash = require("../hash"); -const libSchema = require("../schema"); -const { findRoot } = require("../zip_ops"); +import * as libHash from "../hash"; +import * as libSchema from "../schema"; +import { findRoot } from "../zip_ops"; -const { integerModVersion, integerFactorioVersion, modVersionRegExp } = require("./version"); +import { integerModVersion, integerFactorioVersion, modVersionRegExp } from "./version"; /** @@ -15,27 +14,23 @@ const { integerModVersion, integerFactorioVersion, modVersionRegExp } = require( * * See https://wiki.factorio.com/Tutorial:Mod_structure#info.json for * details of the invidiual fields sourced from info.json. - * @alias module:lib.ModInfo */ -class ModInfo { +export default class ModInfo { /** * Internal name of this mod. * This is the name of the zip file as well as the name that appears in * info.json. - * @type {string} */ name = ""; /** * Version of the mod. * Sourced from info.json. - * @type {string} */ version = ""; /** * Integer representation of the version - * @type {number} */ get integerVersion() { return integerModVersion(this.version); @@ -44,42 +39,36 @@ class ModInfo { /** * Display name of the mod. * Sourced from info.json. - * @type {string} */ title = ""; /** * Author of the mod. * Sourced from info.json. - * @type {string} */ author = ""; /** * Contact field for mod. * Sourced from info.json. - * @type {string} */ contact = ""; /** * Homepage of the mod. * Sourced from info.json. - * @type {string} */ homepage = ""; /** * Description of the mod. * Sourced from info.json. - * @type {string} */ description = ""; /** * Major version of Factorio this mod supports. * Sourced from info.json. - * @type {string} */ factorioVersion = "0.12"; @@ -94,13 +83,11 @@ class ModInfo { /** * Dependiences for this mod. * Sourced from info.json. - * @type {Array} */ dependencies = ["base"]; /** * Expected name of zip file containing this mod. - * @type {number} */ get filename() { return `${this.name}_${this.version}.zip`; @@ -108,54 +95,44 @@ class ModInfo { /** * Size of the mod in bytes - * @type {number} */ size = 0; /** * SHA1 hash of this mod - * @type {string=} */ - sha1; + sha1: string; /** * True if this mod has been deleted - * @type {boolean} */ isDeleted = false; // Content of info.json found in mod files - static infoJsonSchema = { - type: "object", - required: ["name", "version", "title", "author"], - properties: { - "name": { type: "string" }, - "version": { type: "string" }, - "title": { type: "string" }, - "author": { type: "string" }, - "contact": { type: "string" }, - "homepage": { type: "string" }, - "description": { type: "string" }, - "factorio_version": { type: "string" }, - "dependencies": { type: "array", items: { type: "string" } }, - }, - }; + static infoJsonSchema = Type.Object({ + "name": Type.String(), + "version": Type.String(), + "title": Type.String(), + "author": Type.String(), + "contact": Type.Optional(Type.String()), + "homepage": Type.Optional(Type.String()), + "description": Type.Optional(Type.String()), + "factorio_version": Type.Optional(Type.String()), + "dependencies": Type.Optional(Type.Array(Type.String())), + }); - static validateInfo = libSchema.compile(this.infoJsonSchema); + static validateInfo = libSchema.compile>(this.infoJsonSchema as any); - static jsonSchema = { - type: "object", - properties: { - ...this.infoJsonSchema.properties, - "size": { type: "integer" }, - "sha1": { type: "string" }, - "is_deleted": { type: "boolean" }, - }, - }; + static jsonSchema = Type.Object({ + ...this.infoJsonSchema.properties, + "size": Type.Optional(Type.Integer()), + "sha1": Type.Optional(Type.String()), + "is_deleted": Type.Optional(Type.Boolean()), + }); - static validate = libSchema.compile(this.jsonSchema); + static validate = libSchema.compile(this.jsonSchema as any); - static fromJSON(json) { + static fromJSON(json: Static) { const modInfo = new this(); // info.json fields @@ -178,7 +155,7 @@ class ModInfo { } toJSON() { - let json = { + let json: Static = { name: this.name, version: this.version, title: this.title, @@ -187,7 +164,7 @@ class ModInfo { if (this.contact) { json.contact = this.contact; } if (this.homepage) { json.homepage = this.homepage; } if (this.description) { json.description = this.description; } - if (this.factorio_version !== "0.12") { json.factorio_version = this.factorioVersion; } + if (this.factorioVersion !== "0.12") { json.factorio_version = this.factorioVersion; } if (this.dependencies.length !== 1 || this.dependencies[0] !== "base") { json.dependencies = this.dependencies; } @@ -198,7 +175,7 @@ class ModInfo { } static async fromModFile(modPath) { - let modInfo; + let modInfo: Static; { // XXX: JSZip needs the whole archive loaded in memory to work. // This is clearly untenable and will be replaced later. @@ -231,5 +208,3 @@ class ModInfo { }); } } - -module.exports = ModInfo; diff --git a/packages/lib/src/data/ModPack.js b/packages/lib/src/data/ModPack.ts similarity index 68% rename from packages/lib/src/data/ModPack.js rename to packages/lib/src/data/ModPack.ts index de4335f8..47dfce83 100644 --- a/packages/lib/src/data/ModPack.js +++ b/packages/lib/src/data/ModPack.ts @@ -1,84 +1,81 @@ "use strict"; -const zlib = require("zlib"); +import zlib from "zlib"; +import { Type, Static } from "@sinclair/typebox"; -const libSchema = require("../schema"); +import * as libSchema from "../schema"; -const ExportManifest = require("./ExportManifest"); -const { integerFactorioVersion } = require("./version"); +import ExportManifest from "./ExportManifest"; +import { integerFactorioVersion } from "./version"; +import type { Logger } from "../logging"; /** * A setting for a mod. - * @typedef {object} module:lib.ModPack~ModSetting - * @property {boolean|number|string} value - Value of the given mod setting. */ -const ModSettingJsonSchema = { - type: "object", - additionalProperties: false, - required: ["value"], - properties: { - "value": { type: ["boolean", "number", "string"] }, - }, -}; +interface ModSetting { + /** Value of the given mod setting. */ + value: boolean | number | string; +} -const ModSettingsJsonSchema = { - type: "object", - additionalProperties: ModSettingJsonSchema, -}; +const ModSettingJsonSchema = Type.Object({ + "value": Type.Union([Type.Boolean(), Type.Number(), Type.String()]), +}); + +const ModSettingsJsonSchema = Type.Record(Type.String(), ModSettingJsonSchema); /** * A mod in a mod pack - * @typedef {object} module:lib.ModPack~ModRecord - * @property {string} name - name of the mod. - * @property {boolean} enabled - if mod is to be loaded. - * @property {string} version - version of the mod. - * @property {string=} sha1 - SHA1 hash of the zip file. */ -const ModRecordJsonSchema = { - type: "object", - required: ["name", "enabled", "version"], - properties: { - "name": { type: "string" }, - "enabled": { type: "boolean" }, - "version": { type: "string" }, - "sha1": { type: "string" }, - }, -}; +interface ModRecord { + /** name of the mod. */ + name: string, + /** if mod is to be loaded. */ + enabled: boolean, + /** version of the mod. */ + version: string, + /** SHA1 hash of the zip file. */ + sha1?: string, +} + +const ModRecordJsonSchema = Type.Object({ + "name": Type.String(), + "enabled": Type.Boolean(), + "version": Type.String(), + "sha1": Type.Optional(Type.String()), +}); + +type PropertyType = null | boolean | number | string | PropertyType[] | { [key: string]: PropertyType }; /** * Factorio Mod Pack * * Tracks mods and settings for a collection of Factorio mods. - * @alias module:lib.ModPack */ -class ModPack { +export default class ModPack { + declare ["constructor"]: typeof ModPack; + /** * Id of this mod pack - * @type {number} */ - id; + id: number; /** * Name of this mod pack. - * @type {string} */ name = ""; /** * Description for this modpcak. - * @type {string} */ description = ""; /** * Version of Factorio this mod pack is for - * @type {string} */ factorioVersion = "1.1.0"; /** * Integer representation of the factorioVersion - * @type {number} */ get integerFactorioVersion() { return integerFactorioVersion(this.factorioVersion); @@ -86,29 +83,25 @@ class ModPack { /** * Mods included in this mod pack - * @type {Map} */ - mods = new Map(); + mods = new Map(); /** * Mod settings for this mod pack - * @type {Object>=} */ settings = { - "startup": new Map(), - "runtime-global": new Map(), - "runtime-per-user": new Map(), + "startup": new Map(), + "runtime-global": new Map(), + "runtime-per-user": new Map(), }; /** * Mapping to files containing exported data for this modpack - * @type {module:lib.ExportManifest|undefined} */ - exportManifest; + exportManifest?: ExportManifest; /** * True if this mod pack has been deleted from the list of mod packs. - * @type {boolean} */ isDeleted = false; @@ -125,37 +118,24 @@ class ModPack { return clone; } - static jsonSchema = { - type: "object", - additionalProperties: false, - required: ["name", "description", "factorio_version", "mods", "settings"], - properties: { - "id": { type: "integer" }, - "name": { type: "string" }, - "description": { type: "string" }, - "factorio_version": { type: "string" }, - "mods": { - type: "array", - items: ModRecordJsonSchema, - }, - "settings": { - type: "object", - additionalProperties: false, - required: ["startup", "runtime-global", "runtime-per-user"], - properties: { - "startup": ModSettingsJsonSchema, - "runtime-global": ModSettingsJsonSchema, - "runtime-per-user": ModSettingsJsonSchema, - }, - }, - "export_manifest": ExportManifest.jsonSchema, - "is_deleted": { type: "boolean" }, - }, - }; + static jsonSchema = Type.Object({ + "id": Type.Optional(Type.Integer()), + "name": Type.String(), + "description": Type.String(), + "factorio_version": Type.String(), + "mods": Type.Array(ModRecordJsonSchema), + "settings": Type.Object({ + "startup": ModSettingsJsonSchema, + "runtime-global": ModSettingsJsonSchema, + "runtime-per-user": ModSettingsJsonSchema, + }), + "export_manifest": Type.Optional(ExportManifest.jsonSchema), + "is_deleted": Type.Optional(Type.Boolean()), + }); - static validate = libSchema.compile(this.jsonSchema); + static validate = libSchema.compile>(this.jsonSchema as any); - static fromJSON(json) { + static fromJSON(json: Static) { const modPack = new this(); if (json.id) { @@ -185,7 +165,7 @@ class ModPack { } toJSON() { - let json = { + let json: Static = { id: this.id, name: this.name, description: this.description, @@ -212,7 +192,7 @@ class ModPack { return buf.toString("base64"); } - static fromModPackString(modPackString) { + static fromModPackString(modPackString: string) { let buf = Buffer.from(modPackString, "base64"); try { // eslint-disable-next-line node/no-sync @@ -223,9 +203,9 @@ class ModPack { } } - let json; + let json: unknown; try { - json = JSON.parse(buf); + json = JSON.parse(buf.toString()); } catch (err) { throw new Error(`Malformed mod pack string: ${err.message}`); } @@ -238,16 +218,16 @@ class ModPack { } toModSettingsDat() { - function uint8Byte(int) { + function uint8Byte(int: number) { return Buffer.from(Uint8Array.from([int]).buffer); } - function int16Bytes(int) { + function int16Bytes(int: number) { return Buffer.from(Int16Array.from([int]).buffer); } - function uint32Bytes(int) { + function uint32Bytes(int: number) { return Buffer.from(Uint32Array.from([int]).buffer); } - function uint32SpaceOptimizedBytes(int) { + function uint32SpaceOptimizedBytes(int: number) { if (0 <= int && int < 0xff) { return uint8Byte(int); } @@ -256,17 +236,17 @@ class ModPack { uint32Bytes(int), ]); } - function doubleBytes(double) { + function doubleBytes(double: number) { return Buffer.from(Float64Array.from([double]).buffer); } - function stringBytes(str) { + function stringBytes(str: string) { const buf = Buffer.from(str, "utf8"); return Buffer.concat([ uint32SpaceOptimizedBytes(buf.length), buf, ]); } - function immutableStringBytes(str) { + function immutableStringBytes(str: string) { if (str === undefined) { return uint8Byte(1); // empty } @@ -275,7 +255,7 @@ class ModPack { stringBytes(str), ]); } - function versionBytes(version) { + function versionBytes(version: string) { const [main, major, minor] = version.split(".").map(n => Number.parseInt(n, 10)); return Buffer.concat([ int16Bytes(main), @@ -285,7 +265,7 @@ class ModPack { Buffer.alloc(1), // reserved byte ]); } - function propertyTreeListBytes(entries) { + function propertyTreeListBytes(entries: [string, PropertyType][]) { const sizeBytes = uint32Bytes(entries.length); const itemBytes = entries.flatMap(([key, item]) => [ immutableStringBytes(key), @@ -297,15 +277,15 @@ class ModPack { ...itemBytes, ]); } - function propertyTreeBytes(element) { - let type; - let dataBytes; + function propertyTreeBytes(element: PropertyType) { + let type: number; + let dataBytes: Buffer; if (typeof element === null) { type = 1; dataBytes = Buffer.alloc(0); } else if (typeof element === "boolean") { type = 1; - dataBytes = uint8Byte(element); + dataBytes = uint8Byte(Number(element)); } else if (typeof element === "number") { type = 2; dataBytes = doubleBytes(element); @@ -340,11 +320,11 @@ class ModPack { * Uses the provided setting prototypes to add any missing mod settings * in the mod pack with the default value from the prototype. * - * @param {Object} settingPrototypes - + * @param settingPrototypes - * Setting prototypes exported from the game. - * @param {Logger} logger - Logger used to report warnings on. + * @param logger - Logger used to report warnings on. */ - fillDefaultSettings(settingPrototypes, logger) { + fillDefaultSettings(settingPrototypes: Record, logger: Logger) { const knownTypes = ["bool-setting", "int-setting", "double-setting", "string-setting"]; let prototypes = Object.entries(settingPrototypes) .filter(([type, _]) => knownTypes.includes(type)) @@ -368,5 +348,3 @@ class ModPack { } } } - -module.exports = ModPack; diff --git a/packages/lib/src/data/messages_mod.ts b/packages/lib/src/data/messages_mod.ts index a32aac3f..a7c8c72c 100644 --- a/packages/lib/src/data/messages_mod.ts +++ b/packages/lib/src/data/messages_mod.ts @@ -56,7 +56,7 @@ export class ModPackCreateRequest { ) { } static jsonSchema = Type.Object({ - "modPack": Type.Unsafe(ModPack.jsonSchema), + "modPack": ModPack.jsonSchema, }); static fromJSON(json: Static) { @@ -76,7 +76,7 @@ export class ModPackUpdateRequest { ) { } static jsonSchema = Type.Object({ - "modPack": Type.Unsafe(ModPack.jsonSchema), + "modPack": ModPack.jsonSchema, }); static fromJSON(json: Static) { @@ -203,7 +203,7 @@ export class ModSearchRequest { "results": Type.Array( Type.Object({ "name": Type.String(), - "versions": Type.Array(Type.Unsafe(ModInfo.jsonSchema)), + "versions": Type.Array(ModInfo.jsonSchema), }), ), }); @@ -296,7 +296,7 @@ export class ModPackUpdateEvent { ) { } static jsonSchema = Type.Object({ - "modPack": Type.Unsafe(ModPack.jsonSchema), + "modPack": ModPack.jsonSchema, }); static fromJSON(json: Static) { @@ -315,7 +315,7 @@ export class ModUpdateEvent { ) { } static jsonSchema = Type.Object({ - "mod": Type.Unsafe(ModInfo.jsonSchema), + "mod": ModInfo.jsonSchema, }); static fromJSON(json: Static) { diff --git a/packages/lib/src/data/version.js b/packages/lib/src/data/version.ts similarity index 58% rename from packages/lib/src/data/version.js rename to packages/lib/src/data/version.ts index c20f44db..bd79bece 100644 --- a/packages/lib/src/data/version.js +++ b/packages/lib/src/data/version.ts @@ -1,5 +1,4 @@ // Helper functions for dealing with factorio version numbers -"use strict"; /** @@ -7,11 +6,10 @@ * * Suitable for sorting and comparing version numbers which may have different * number of leading zeros in them. - * @param {string} version - string representation of version. - * @returns {number} numeric representation. - * @alias module:lib.integerModVersion + * @param version - string representation of version. + * @returns numeric representation. */ -function integerModVersion(version) { +export function integerModVersion(version: string) { const [major, minor, sub] = version.split(".").map(n => Number.parseInt(n, 10)); return major * 0x100000000 + minor * 0x10000 + sub; // Can't use bitwise here because this is 48-bits. } @@ -21,24 +19,15 @@ function integerModVersion(version) { * * Suitable for sorting and comparing version numbers which may have different * number of leading zeros in them. - * @param {string} version - string representation of version. - * @returns {number} numeric representation. - * @alias module:lib.integerFactorioVersion + * @param version - string representation of version. + * @returns numeric representation. */ -function integerFactorioVersion(version) { +export function integerFactorioVersion(version: string) { const [main, major, minor] = version.split(".").map(n => Number.parseInt(n, 10)); return main * 0x100000000 + major * 0x10000 + (minor || 0); // Can't use bitwise here because this is 48-bits. } /** * Matches valid mod versions. - * @type {RegExp} - * @alias module:lib.modVersionRegExp */ -const modVersionRegExp = /^\d+\.\d+\.\d+$/; - -module.exports = { - integerModVersion, - integerFactorioVersion, - modVersionRegExp, -}; +export const modVersionRegExp = /^\d+\.\d+\.\d+$/; From 2ca8e690257a0131f02f7e8197ba30c18489f322 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 11 Aug 2023 01:16:12 +0200 Subject: [PATCH 10/13] Remove message_validate.js ignore This file was relocated to the dist folder. --- packages/lib/.gitignore | 1 - packages/lib/.npmignore | 0 2 files changed, 1 deletion(-) delete mode 100644 packages/lib/.gitignore delete mode 100644 packages/lib/.npmignore diff --git a/packages/lib/.gitignore b/packages/lib/.gitignore deleted file mode 100644 index 922aef1f..00000000 --- a/packages/lib/.gitignore +++ /dev/null @@ -1 +0,0 @@ -src/data/message_validate.js diff --git a/packages/lib/.npmignore b/packages/lib/.npmignore deleted file mode 100644 index e69de29b..00000000 From a66e9309978f62f822455bd304bcfd61daa82b1c Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 12 Aug 2023 02:11:15 +0200 Subject: [PATCH 11/13] Use strict type checking --- packages/lib/package.json | 1 + packages/lib/src/PlayerStats.ts | 25 +- packages/lib/src/RateLimiter.ts | 19 +- packages/lib/src/command.ts | 13 +- packages/lib/src/config/classes.ts | 145 +++++----- packages/lib/src/data/ModInfo.ts | 6 +- packages/lib/src/data/ModPack.ts | 21 +- packages/lib/src/data/index.ts | 29 -- packages/lib/src/data/messages_controller.ts | 76 ++--- packages/lib/src/data/messages_core.ts | 9 +- packages/lib/src/data/messages_host.ts | 46 +-- packages/lib/src/data/messages_instance.ts | 286 +++++++++---------- packages/lib/src/data/messages_mod.ts | 120 ++++---- packages/lib/src/data/messages_user.ts | 154 +++++----- packages/lib/src/database.ts | 8 +- packages/lib/src/errors.ts | 16 +- packages/lib/src/factorio/exchange_string.ts | 4 +- packages/lib/src/file_ops.ts | 4 +- packages/lib/src/helpers.ts | 12 +- packages/lib/src/link/WebSocket.d.ts | 2 + packages/lib/src/link/WebSocket.js | 3 + packages/lib/src/link/connectors.ts | 78 ++--- packages/lib/src/link/link.ts | 159 +++++++---- packages/lib/src/link/messages.ts | 3 +- packages/lib/src/logging.ts | 2 +- packages/lib/src/logging_utils.ts | 23 +- packages/lib/src/plugin.ts | 8 +- packages/lib/src/plugin_loader.ts | 2 +- packages/lib/src/prometheus.ts | 71 ++--- packages/lib/src/schema.ts | 2 +- packages/lib/src/shared_commands.ts | 6 +- packages/lib/src/users.ts | 10 +- packages/lib/src/zip_ops.ts | 6 +- packages/lib/tsconfig.json | 2 +- plugins/research_sync/controller.js | 2 - test/lib/link/link.js | 17 +- 36 files changed, 714 insertions(+), 676 deletions(-) create mode 100644 packages/lib/src/link/WebSocket.d.ts create mode 100644 packages/lib/src/link/WebSocket.js diff --git a/packages/lib/package.json b/packages/lib/package.json index 93696564..7a56055a 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -38,6 +38,7 @@ "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", "@types/triple-beam": "^1.3.2", + "@types/ws": "^8.5.5", "typescript": "^5.1.6" } } diff --git a/packages/lib/src/PlayerStats.ts b/packages/lib/src/PlayerStats.ts index ed1bb244..f3f96691 100644 --- a/packages/lib/src/PlayerStats.ts +++ b/packages/lib/src/PlayerStats.ts @@ -1,3 +1,5 @@ +import { Type, Static } from "@sinclair/typebox"; + /** * Stastics collected about a player. */ @@ -27,18 +29,15 @@ export default class PlayerStats { */ lastLeaveReason?: string; - static jsonSchema = { - type: "object", - properties: { - "join_count": { type: "integer" }, - "online_time_ms": { type: "number" }, - "last_join_at": { type: "number" }, - "last_leave_at": { type: "number" }, - "last_leave_reason": { type: "string" }, - }, - }; + static jsonSchema = Type.Object({ + "join_count": Type.Optional(Type.Integer()), + "online_time_ms": Type.Optional(Type.Number()), + "last_join_at": Type.Optional(Type.Number()), + "last_leave_at": Type.Optional(Type.Number()), + "last_leave_reason": Type.Optional(Type.String()), + }); - constructor(json = {}) { + constructor(json: Static = {}) { if (json["join_count"]) { this.joinCount = json["join_count"]; } @@ -54,12 +53,12 @@ export default class PlayerStats { } } - static fromJSON(json: any) { + static fromJSON(json: Static) { return new this(json); } toJSON() { - let json = {}; + let json: Static = {}; if (this.joinCount) { json["join_count"] = this.joinCount; } diff --git a/packages/lib/src/RateLimiter.ts b/packages/lib/src/RateLimiter.ts index c7362881..24390c6b 100644 --- a/packages/lib/src/RateLimiter.ts +++ b/packages/lib/src/RateLimiter.ts @@ -27,11 +27,10 @@ export default class RateLimiter { /** * Callback invoked at most `maxRate` per second on activation. */ - action: Function | null; + action?: Function; private _lastRun = 0; - private _runTimeout = null; - private lastProgressBroadcast = Date.now(); + private _runTimeout?: NodeJS.Timeout; /** * Construct RateLimiter helper @@ -46,7 +45,7 @@ export default class RateLimiter { */ constructor(options: { maxRate?: number, action?: Function } = {}) { this.maxRate = options.maxRate || 1; - this.action = options.action || null; + this.action = options.action || undefined; } /** @@ -62,11 +61,13 @@ export default class RateLimiter { activate() { let now = Date.now(); if (now < this._lastRun + 1000 / this.maxRate) { - if (!this._runTimeout && this.action !== null) { + if (!this._runTimeout && this.action) { this._runTimeout = setTimeout(() => { - this._runTimeout = null; + this._runTimeout = undefined; this._lastRun = Date.now(); - this.action(); + if (this.action) { + this.action(); + } }, this._lastRun + 1000 / this.maxRate - now); } return false; @@ -75,11 +76,11 @@ export default class RateLimiter { // We may end up activating before the timeout does. if (this._runTimeout) { clearTimeout(this._runTimeout); - this._runTimeout = null; + this._runTimeout = undefined; } this._lastRun = now; - if (this.action !== null) { + if (this.action) { this.action(); } return true; diff --git a/packages/lib/src/command.ts b/packages/lib/src/command.ts index 1577b486..d2173ed0 100644 --- a/packages/lib/src/command.ts +++ b/packages/lib/src/command.ts @@ -157,12 +157,11 @@ export class CommandTree { * @returns host ID. */ export async function resolveHost(client: Link, hostName: string) { - let hostId: number; + let hostId: number | undefined; if (/^-?\d+$/.test(hostName)) { hostId = parseInt(hostName, 10); } else { let hosts = await client.sendTo("controller", new libData.HostListRequest()); - // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let host of hosts) { if (host.name === hostName) { hostId = host.id; @@ -189,12 +188,11 @@ export async function resolveHost(client: Link, hostName: string) { * @returns instance ID. */ export async function resolveInstance(client: Link, instanceName: string) { - let instanceId: number; + let instanceId: number | undefined; if (/^-?\d+$/.test(instanceName)) { instanceId = parseInt(instanceName, 10); } else { let instances = await client.sendTo("controller", new libData.InstanceDetailsListRequest()); - // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let instance of instances) { if (instance.name === instanceName) { instanceId = instance.id; @@ -222,12 +220,11 @@ export async function resolveInstance(client: Link, instanceName: string) { * @returns mod pack ID. */ export async function resolveModPack(client: Link, modPackName: string) { - let modPackId: number; + let modPackId: number | undefined; if (/^-?\d+/.test(modPackName)) { modPackId = parseInt(modPackName, 10); } else { let modPacks = await client.sendTo("controller", new libData.ModPackListRequest()); - // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let modPack of modPacks) { if (modPack.name === modPackName) { modPackId = modPack.id; @@ -256,10 +253,9 @@ export async function resolveModPack(client: Link, modPackName: string) { export async function retrieveRole(client: Link, roleName: string) { let roles = await client.sendTo("controller", new libData.RoleListRequest()); - let resolvedRole: libData.RawRole; + let resolvedRole: libData.RawRole | undefined; if (/^-?\d+$/.test(roleName)) { let roleId = parseInt(roleName, 10); - // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let role of roles) { if (role.id === roleId) { resolvedRole = role; @@ -268,7 +264,6 @@ export async function retrieveRole(client: Link, roleName: string) { } } else { - // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let role of roles) { if (role.name === roleName) { resolvedRole = role; diff --git a/packages/lib/src/config/classes.ts b/packages/lib/src/config/classes.ts index a05bff29..b1e956d1 100644 --- a/packages/lib/src/config/classes.ts +++ b/packages/lib/src/config/classes.ts @@ -521,7 +521,7 @@ export class ConfigGroup { } else if (def.type === "object") { try { value = JSON.parse(value); - } catch (err) { + } catch (err: any) { throw new InvalidValue(`Error parsing value for ${name}: ${err.message}`); } } @@ -567,7 +567,7 @@ export class ConfigGroup { throw new InvalidValue(`Cannot set property on non-object field '${name}'`); } - let prev = this._fields.get(name) as object; + let prev = this._fields.get(name) as Record; let updated = {...prev || {}}; if (value !== undefined) { @@ -648,9 +648,9 @@ export class ConfigGroup { * @returns JSON serializable representation of the group. */ serialize(location: ConfigLocation = this.config.location) { - let fields = {}; + let fields: Record = {}; for (let [name, value] of this._fields) { - let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name)!; if (!this._checkAccess(def, location, false)) { continue; } @@ -753,76 +753,77 @@ export class ConfigGroup { }); // Validate properties - let validTypes = ["boolean", "string", "number", "object"]; - // eslint-disable-next-line complexity - Object.keys(item).forEach(key => { - let value = item[key]; - let valueType = basicType(value); - switch (key) { - case "type": - if (!validTypes.includes(value)) { - throw new Error(`${value} is not a valid type`); - } - break; + { + const { + type, access, enum: enum_, name, title, description, restartRequired, + restartRequiredProps, optional, initial_value, ...rest + } = item; - case "access": - if (valueType !== "array") { - throw new Error("access must be an array"); - } - break; - - case "enum": - if (valueType !== "array") { - throw new Error("enum must be an array"); - } - break; - - case "name": - if (valueType !== "string") { - throw new Error("name must be a string"); - } - break; - - case "title": - if (valueType !== "string") { - throw new Error("title must be a string"); - } - break; - - case "description": - if (valueType !== "string") { - throw new Error("description must be a string"); - } - break; - - case "restartRequired": - if (valueType !== "boolean") { - throw new Error("restartRequired must be a boolean"); - } - break; - - case "restartRequiredProps": - if (valueType !== "array") { - throw new Error("restartRequiredProps must be a array"); - } - break; - - case "optional": - if (valueType !== "boolean") { - throw new Error("optional must be a boolean"); - } - break; - - case "initial_value": - if (valueType !== item.type && valueType !== "function") { - throw new Error("initial_value must match the type or be a function"); - } - break; - - default: - throw new Error(`Unknown property ${key}`); + let validTypes = ["boolean", "string", "number", "object"]; + if (type !== undefined) { + if (!validTypes.includes(type)) { + throw new Error(`${type} is not a valid type`); + } } - }); + + if (access !== undefined) { + if (basicType(access) !== "array") { + throw new Error("access must be an array"); + } + } + + if (enum_ !== undefined) { + if (basicType(enum_) !== "array") { + throw new Error("enum must be an array"); + } + } + + if (name !== undefined) { + if (typeof name !== "string") { + throw new Error("name must be a string"); + } + } + + if (title !== undefined) { + if (typeof title !== "string") { + throw new Error("title must be a string"); + } + } + + if (description !== undefined) { + if (typeof description !== "string") { + throw new Error("description must be a string"); + } + } + + if (restartRequired !== undefined) { + if (typeof restartRequired !== "boolean") { + throw new Error("restartRequired must be a boolean"); + } + } + + if (restartRequiredProps !== undefined) { + if (basicType(restartRequiredProps) !== "array") { + throw new Error("restartRequiredProps must be a array"); + } + } + + if (optional !== undefined) { + if (typeof optional !== "boolean") { + throw new Error("optional must be a boolean"); + } + } + + if (initial_value !== undefined) { + if (basicType(initial_value) !== item.type && basicType(initial_value) !== "function") { + throw new Error("initial_value must match the type or be a function"); + } + } + + for (let key of Object.keys(rest)) { + throw new Error(`Unknown property ${key}`); + } + } if (this._definitions.has(item.name)) { throw new Error(`Config field ${item.name} has already been defined`); diff --git a/packages/lib/src/data/ModInfo.ts b/packages/lib/src/data/ModInfo.ts index ce9e46b4..f642a9f7 100644 --- a/packages/lib/src/data/ModInfo.ts +++ b/packages/lib/src/data/ModInfo.ts @@ -101,7 +101,7 @@ export default class ModInfo { /** * SHA1 hash of this mod */ - sha1: string; + sha1?: string; /** * True if this mod has been deleted @@ -174,13 +174,13 @@ export default class ModInfo { return json; } - static async fromModFile(modPath) { + static async fromModFile(modPath: string) { let modInfo: Static; { // XXX: JSZip needs the whole archive loaded in memory to work. // This is clearly untenable and will be replaced later. let zip = await JSZip.loadAsync(await fs.readFile(modPath)); - let root = zip.folder(findRoot(zip)); + let root = zip.folder(findRoot(zip))!; let infoFile = root.file("info.json"); if (!infoFile) { diff --git a/packages/lib/src/data/ModPack.ts b/packages/lib/src/data/ModPack.ts index 47dfce83..b3194ec4 100644 --- a/packages/lib/src/data/ModPack.ts +++ b/packages/lib/src/data/ModPack.ts @@ -57,7 +57,7 @@ export default class ModPack { /** * Id of this mod pack */ - id: number; + id?: number; /** * Name of this mod pack. @@ -197,7 +197,7 @@ export default class ModPack { try { // eslint-disable-next-line node/no-sync buf = zlib.inflateSync(buf); - } catch (err) { + } catch (err: any) { if (err.code.startsWith("Z_")) { throw new Error("Malformed mod pack string: zlib inflate failed"); } @@ -206,7 +206,7 @@ export default class ModPack { let json: unknown; try { json = JSON.parse(buf.toString()); - } catch (err) { + } catch (err: any) { throw new Error(`Malformed mod pack string: ${err.message}`); } @@ -246,7 +246,7 @@ export default class ModPack { buf, ]); } - function immutableStringBytes(str: string) { + function immutableStringBytes(str?: string) { if (str === undefined) { return uint8Byte(1); // empty } @@ -265,7 +265,7 @@ export default class ModPack { Buffer.alloc(1), // reserved byte ]); } - function propertyTreeListBytes(entries: [string, PropertyType][]) { + function propertyTreeListBytes(entries: [string | undefined, PropertyType][]) { const sizeBytes = uint32Bytes(entries.length); const itemBytes = entries.flatMap(([key, item]) => [ immutableStringBytes(key), @@ -295,7 +295,7 @@ export default class ModPack { } else if (element instanceof Array) { type = 4; dataBytes = propertyTreeListBytes(element.map(item => [undefined, item])); - } else if (typeof element === "object") { + } else if (typeof element === "object" && element !== null) { type = 5; dataBytes = propertyTreeListBytes(Object.entries(element)); } else { @@ -332,19 +332,20 @@ export default class ModPack { ; for (let prototype of prototypes) { - if (!["startup", "runtime-global", "runtime-per-user"].includes(prototype.setting_type)) { - logger.warn(`Ignoring ${prototype.name} with unknown setting_type ${prototype.setting_type}`); + const settingType = prototype.setting_type as keyof typeof this.settings; + if (!["startup", "runtime-global", "runtime-per-user"].includes(settingType)) { + logger.warn(`Ignoring ${prototype.name} with unknown setting_type ${settingType}`); continue; } if (prototype.default_value === undefined) { logger.warn(`Ignoring ${prototype.name} with missing default_value`); continue; } - if (this.settings[prototype.setting_type].get(prototype.name)) { + if (this.settings[settingType].get(prototype.name)) { continue; } - this.settings[prototype.setting_type].set(prototype.name, { value: prototype.default_value }); + this.settings[settingType].set(prototype.name, { value: prototype.default_value }); } } } diff --git a/packages/lib/src/data/index.ts b/packages/lib/src/data/index.ts index 7f5e2349..46c1ed2a 100644 --- a/packages/lib/src/data/index.ts +++ b/packages/lib/src/data/index.ts @@ -14,32 +14,3 @@ export * from "./messages_instance"; export * from "./messages_mod"; export * from "./messages_user"; export * from "./version"; - -// TODO: Remove/migrate after lib/data is migrated to ts -import type { User } from "../users"; -export interface Serialisable { - jsonSchema: object, - fromJSON(json: any): object, -} -import { AddressType } from "./messages_core"; - -export class Request { - static type: "request"; - static src: AddressType | AddressType[]; - static dst: AddressType | AddressType[]; - static permission: null | "string" | ((user: User, message: Request) => void); - static plugin?: string; - static jsonSchema?: object; - static fromJSON?: (json: any) => Request; - static Response?: Serialisable; -} - -export class Event { - static type: "event"; - static src: AddressType | AddressType[]; - static dst: AddressType | AddressType[]; - static permission: null | "string" | ((user: User, message: Event) => void); - static plugin?: string; - static jsonSchema?: object; - static fromJSON?: (json: any) => Event; -} diff --git a/packages/lib/src/data/messages_controller.ts b/packages/lib/src/data/messages_controller.ts index 3ee14f7e..ba885145 100644 --- a/packages/lib/src/data/messages_controller.ts +++ b/packages/lib/src/data/messages_controller.ts @@ -19,19 +19,19 @@ export class RawConfig { export class ControllerConfigGetRequest { declare ["constructor"]: typeof ControllerConfigGetRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.controller.get_config"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.controller.get_config" as const; static Response = RawConfig; } export class ControllerConfigSetFieldRequest { declare ["constructor"]: typeof ControllerConfigSetFieldRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.controller.update_config"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.controller.update_config" as const; constructor( public field: string, @@ -50,10 +50,10 @@ export class ControllerConfigSetFieldRequest { export class ControllerConfigSetPropRequest { declare ["constructor"]: typeof ControllerConfigSetPropRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.controller.update_config"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.controller.update_config" as const; constructor( public field: string, @@ -74,10 +74,10 @@ export class ControllerConfigSetPropRequest { export class HostGenerateTokenRequest { declare ["constructor"]: typeof HostGenerateTokenRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.host.generate_token"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.host.generate_token" as const; constructor( public hostId?: number, @@ -96,10 +96,10 @@ export class HostGenerateTokenRequest { export class HostConfigCreateRequest { declare ["constructor"]: typeof HostConfigCreateRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.host.create_config"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.host.create_config" as const; constructor( public id: number | undefined, @@ -122,10 +122,10 @@ export class HostConfigCreateRequest { export class LogSetSubscriptionsRequest { declare ["constructor"]: typeof LogSetSubscriptionsRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.log.follow"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.log.follow" as const; constructor( public all?: boolean, @@ -150,10 +150,10 @@ export class LogSetSubscriptionsRequest { export class LogQueryRequest { declare ["constructor"]: typeof LogQueryRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.log.query"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.log.query" as const; constructor( public all: boolean, @@ -197,9 +197,9 @@ export class LogQueryRequest { export class LogMessageEvent { declare ["constructor"]: typeof LogMessageEvent; - static type = "event"; - static src = ["host", "controller"]; - static dst = ["controller", "control"]; + static type = "event" as const; + static src = ["host", "controller"] as const; + static dst = ["controller", "control"] as const; constructor( public info: object, @@ -219,17 +219,17 @@ export class LogMessageEvent { export class DebugDumpWsRequest { declare ["constructor"]: typeof DebugDumpWsRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.debug.dump_ws"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.debug.dump_ws" as const; } export class DebugWsMessageEvent { declare ["constructor"]: typeof DebugWsMessageEvent; - static type = "event"; - static src = "controller"; - static dst = "control"; + static type = "event" as const; + static src = "controller" as const; + static dst = "control" as const; constructor( public direction: string, diff --git a/packages/lib/src/data/messages_core.ts b/packages/lib/src/data/messages_core.ts index 5f6b8ac4..207fc01a 100644 --- a/packages/lib/src/data/messages_core.ts +++ b/packages/lib/src/data/messages_core.ts @@ -323,7 +323,7 @@ export class ReadyData { }); static fromJSON(json: Static) { - let account: AccountDetails; + let account: AccountDetails | undefined; if (json.account) { account = AccountDetails.fromJSON(json.account); } return new this( Address.fromJSON(json.src), @@ -622,8 +622,9 @@ Message.jsonSchema = Type.Union([ ]); export class PingRequest { - static type = "request"; - static src = ["controller", "host", "control"]; - static dst = ["controller", "host", "control"]; + declare ["constructor"]: typeof PingRequest; + static type = "request" as const; + static src = ["controller", "host", "control"] as const; + static dst = ["controller", "host", "control"] as const; static permission = null; } diff --git a/packages/lib/src/data/messages_host.ts b/packages/lib/src/data/messages_host.ts index bbde6a6a..c271a3b9 100644 --- a/packages/lib/src/data/messages_host.ts +++ b/packages/lib/src/data/messages_host.ts @@ -28,19 +28,19 @@ export class HostDetails { export class HostListRequest { declare ["constructor"]: typeof HostListRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.host.list"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.host.list" as const; static Response = jsonArray(HostDetails); } export class HostSetSubscriptionsRequest { declare ["constructor"]: typeof HostSetSubscriptionsRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.host.subscribe"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.host.subscribe" as const; constructor( public all: boolean, @@ -60,9 +60,9 @@ export class HostSetSubscriptionsRequest { export class HostUpdateEvent { declare ["constructor"]: typeof HostUpdateEvent; - static type = "event"; - static src = "controller"; - static dst = "control"; + static type = "event" as const; + static src = "controller" as const; + static dst = "control" as const; constructor( public update: HostDetails, @@ -79,9 +79,9 @@ export class HostUpdateEvent { export class HostMetricsRequest { declare ["constructor"]: typeof HostMetricsRequest; - static type = "request"; - static src = "controller"; - static dst = "host"; + static type = "request" as const; + static src = "controller" as const; + static dst = "host" as const; static Response = class Response { // TODO: Use JSON class pattern in Prometheus constructor( public results: object[], @@ -99,9 +99,9 @@ export class HostMetricsRequest { export class ControllerConnectionEvent { declare ["constructor"]: typeof ControllerConnectionEvent; - static type = "event"; - static src = "host"; - static dst = "instance"; + static type = "event" as const; + static src = "host" as const; + static dst = "instance" as const; constructor( public event: string, @@ -118,16 +118,16 @@ export class ControllerConnectionEvent { export class PrepareControllerDisconnectRequest { declare ["constructor"]: typeof PrepareControllerDisconnectRequest; - static type = "request"; - static src = "host"; - static dst = "instance"; + static type = "request" as const; + static src = "host" as const; + static dst = "instance" as const; } export class SyncUserListsEvent { declare ["constructor"]: typeof SyncUserListsEvent; - static type = "event"; - static src = "controller"; - static dst = "host"; + static type = "event" as const; + static src = "controller" as const; + static dst = "host" as const; constructor( public adminlist: Set, diff --git a/packages/lib/src/data/messages_instance.ts b/packages/lib/src/data/messages_instance.ts index 214a665a..9fc2741a 100644 --- a/packages/lib/src/data/messages_instance.ts +++ b/packages/lib/src/data/messages_instance.ts @@ -35,10 +35,10 @@ export class InstanceDetails { export class InstanceDetailsGetRequest { declare ["constructor"]: typeof InstanceDetailsGetRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.get"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.get" as const; constructor( public instanceId: number, @@ -57,19 +57,19 @@ export class InstanceDetailsGetRequest { export class InstanceDetailsListRequest { declare ["constructor"]: typeof InstanceDetailsListRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.list"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.list" as const; static Response = jsonArray(InstanceDetails); }; export class InstanceDetailsSetSubscriptionsRequest { declare ["constructor"]: typeof InstanceDetailsSetSubscriptionsRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.subscribe"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.subscribe" as const; constructor( public all: boolean = false, @@ -88,9 +88,9 @@ export class InstanceDetailsSetSubscriptionsRequest { export class InstanceDetailsUpdateEvent { declare ["constructor"]: typeof InstanceDetailsUpdateEvent; - static type = "event"; - static src = "controller"; - static dst = "control"; + static type = "event" as const; + static src = "controller" as const; + static dst = "control" as const; constructor( public details: InstanceDetails, @@ -109,10 +109,10 @@ export class InstanceDetailsUpdateEvent { export class InstanceCreateRequest { declare ["constructor"]: typeof InstanceCreateRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.create"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.create" as const; constructor( public config: object, @@ -130,10 +130,10 @@ export class InstanceCreateRequest { export class InstanceConfigGetRequest { declare ["constructor"]: typeof InstanceConfigGetRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.get_config"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.get_config" as const; constructor( public instanceId: number, @@ -165,10 +165,10 @@ export class InstanceConfigGetRequest { export class InstanceConfigSetFieldRequest { declare ["constructor"]: typeof InstanceConfigSetFieldRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.update_config"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.update_config" as const; constructor( public instanceId: number, @@ -189,10 +189,10 @@ export class InstanceConfigSetFieldRequest { export class InstanceConfigSetPropRequest { declare ["constructor"]: typeof InstanceConfigSetPropRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.update_config"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.update_config" as const; constructor( public instanceId: number, @@ -215,10 +215,10 @@ export class InstanceConfigSetPropRequest { export class InstanceAssignRequest { declare ["constructor"]: typeof InstanceAssignRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.assign"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.assign" as const; constructor( public instanceId: number, @@ -237,9 +237,9 @@ export class InstanceAssignRequest { export class InstanceMetricsRequest { declare ["constructor"]: typeof InstanceMetricsRequest; - static type = "request"; - static src = "host"; - static dst = "instance"; + static type = "request" as const; + static src = "host" as const; + static dst = "instance" as const; static Response = class Response { // TODO: Use JSON class pattern in Prometheus constructor( @@ -258,10 +258,10 @@ export class InstanceMetricsRequest { export class InstanceStartRequest { declare ["constructor"]: typeof InstanceStartRequest; - static type = "request"; - static src = ["control", "controller"]; - static dst = "instance"; - static permission = "core.instance.start"; + static type = "request" as const; + static src = ["control", "controller"] as const; + static dst = "instance" as const; + static permission = "core.instance.start" as const; constructor( public save?: string, @@ -302,19 +302,19 @@ export class SaveDetails { export class InstanceListSavesRequest { declare ["constructor"]: typeof InstanceListSavesRequest; - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.save.list"; + static type = "request" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.save.list" as const; static Response = jsonArray(SaveDetails); } export class InstanceSetSaveListSubscriptionsRequest { declare ["constructor"]: typeof InstanceSetSaveListSubscriptionsRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.save.list_subscribe"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.save.list_subscribe" as const; constructor( public all: boolean = false, @@ -333,9 +333,9 @@ export class InstanceSetSaveListSubscriptionsRequest { export class InstanceSaveListUpdateEvent { declare ["constructor"]: typeof InstanceSaveListUpdateEvent; - static type = "event"; - static src = ["instance", "host", "controller"]; - static dst = ["controller", "control"]; + static type = "event" as const; + static src = ["instance", "host", "controller"] as const; + static dst = ["controller", "control"] as const; constructor( public instanceId: number, @@ -354,10 +354,10 @@ export class InstanceSaveListUpdateEvent { export class InstanceCreateSaveRequest { declare ["constructor"]: typeof InstanceCreateSaveRequest; - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.save.create"; + static type = "request" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.save.create" as const; constructor( public name: string, @@ -380,10 +380,10 @@ export class InstanceCreateSaveRequest { export class InstanceRenameSaveRequest { declare ["constructor"]: typeof InstanceRenameSaveRequest; - static type = "request"; - static src = ["control", "controller"]; - static dst = ["controller", "host"]; - static permission = "core.instance.save.rename"; + static type = "request" as const; + static src = ["control", "controller"] as const; + static dst = ["controller", "host"] as const; + static permission = "core.instance.save.rename" as const; constructor( public instanceId: number, @@ -404,10 +404,10 @@ export class InstanceRenameSaveRequest { export class InstanceCopySaveRequest { declare ["constructor"]: typeof InstanceCopySaveRequest; - static type = "request"; - static src = ["control", "controller"]; - static dst = ["controller", "host"]; - static permission = "core.instance.save.copy"; + static type = "request" as const; + static src = ["control", "controller"] as const; + static dst = ["controller", "host"] as const; + static permission = "core.instance.save.copy" as const; constructor( public instanceId: number, @@ -428,10 +428,10 @@ export class InstanceCopySaveRequest { export class InstanceDeleteSaveRequest { declare ["constructor"]: typeof InstanceDeleteSaveRequest; - static type = "request"; - static src = ["control", "controller"]; - static dst = ["controller", "host"]; - static permission = "core.instance.save.delete"; + static type = "request" as const; + static src = ["control", "controller"] as const; + static dst = ["controller", "host"] as const; + static permission = "core.instance.save.delete" as const; constructor( public instanceId: number, @@ -450,10 +450,10 @@ export class InstanceDeleteSaveRequest { export class InstanceDownloadSaveRequest { declare ["constructor"]: typeof InstanceDownloadSaveRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.save.download"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.save.download" as const; constructor( public instanceId: number, @@ -474,9 +474,9 @@ export class InstanceDownloadSaveRequest { export class InstanceTransferSaveRequest { declare ["constructor"]: typeof InstanceTransferSaveRequest; - static type = "request"; - static src = ["control", "controller"]; - static dst = ["controller", "host"]; + static type = "request" as const; + static src = ["control", "controller"] as const; + static dst = ["controller", "host"] as const; static permission(user: User, message: MessageRequest) { user.checkPermission("core.instance.save.transfer"); if (typeof message.data === "object" && message.data !== null) { @@ -517,9 +517,9 @@ export class InstanceTransferSaveRequest { export class InstancePullSaveRequest { declare ["constructor"]: typeof InstancePullSaveRequest; - static type = "request"; - static src = "controller"; - static dst = "host"; + static type = "request" as const; + static src = "controller" as const; + static dst = "host" as const; constructor( public instanceId: number, @@ -542,9 +542,9 @@ export class InstancePullSaveRequest { export class InstancePushSaveRequest { declare ["constructor"]: typeof InstancePushSaveRequest; - static type = "request"; - static src = "controller"; - static dst = "host"; + static type = "request" as const; + static src = "controller" as const; + static dst = "host" as const; constructor( public instanceId: number, @@ -565,10 +565,10 @@ export class InstancePushSaveRequest { export class InstanceLoadScenarioRequest { declare ["constructor"]: typeof InstanceLoadScenarioRequest; - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.load_scenario"; + static type = "request" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.load_scenario" as const; constructor( public scenario: string, @@ -591,42 +591,42 @@ export class InstanceLoadScenarioRequest { export class InstanceExportDataRequest { declare ["constructor"]: typeof InstanceExportDataRequest; - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.export_data"; + static type = "request" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.export_data" as const; } export class InstanceExtractPlayersRequest { declare ["constructor"]: typeof InstanceExtractPlayersRequest; - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.extract_players"; + static type = "request" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.extract_players" as const; } export class InstanceStopRequest { declare ["constructor"]: typeof InstanceStopRequest; - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.stop"; + static type = "request" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.stop" as const; } export class InstanceKillRequest { declare ["constructor"]: typeof InstanceKillRequest; - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.kill"; + static type = "request" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.kill" as const; } export class InstanceDeleteRequest { declare ["constructor"]: typeof InstanceDeleteRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.instance.delete"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.delete" as const; constructor( public instanceId: number, @@ -643,9 +643,9 @@ export class InstanceDeleteRequest { export class InstanceDeleteInternalRequest { declare ["constructor"]: typeof InstanceDeleteInternalRequest; - static type = "request"; - static src = "controller"; - static dst = "host"; + static type = "request" as const; + static src = "controller" as const; + static dst = "host" as const; constructor( public instanceId: number, @@ -662,10 +662,10 @@ export class InstanceDeleteInternalRequest { export class InstanceSendRconRequest { declare ["constructor"]: typeof InstanceSendRconRequest; - static type = "request"; - static src = "control"; - static dst = "instance"; - static permission = "core.instance.send_rcon"; + static type = "request" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.send_rcon" as const; constructor( public command: string, @@ -704,9 +704,9 @@ export class RawInstanceInfo { export class InstancesUpdateRequest { declare ["constructor"]: typeof InstancesUpdateRequest; - static type = "request"; - static src = "host"; - static dst = "controller"; + static type = "request" as const; + static src = "host" as const; + static dst = "controller" as const; constructor( public instances: RawInstanceInfo[], @@ -725,9 +725,9 @@ export class InstancesUpdateRequest { export class InstanceAssignInternalRequest { declare ["constructor"]: typeof InstanceAssignInternalRequest; - static type = "request"; - static src = "controller"; - static dst = "host"; + static type = "request" as const; + static src = "controller" as const; + static dst = "host" as const; constructor( public instanceId: number, @@ -747,9 +747,9 @@ export class InstanceAssignInternalRequest { export class InstanceUnassignInternalRequest { declare ["constructor"]: typeof InstanceUnassignInternalRequest; - static type = "request"; - static src = "controller"; - static dst = "host"; + static type = "request" as const; + static src = "controller" as const; + static dst = "host" as const; constructor( public instanceId: number, @@ -766,9 +766,9 @@ export class InstanceUnassignInternalRequest { export class InstanceInitialisedEvent { declare ["constructor"]: typeof InstanceInitialisedEvent; - static type = "event"; - static src = "instance"; - static dst = "host"; + static type = "event" as const; + static src = "instance" as const; + static dst = "host" as const; constructor( public plugins: Record, @@ -785,9 +785,9 @@ export class InstanceInitialisedEvent { export class InstanceStatusChangedEvent { declare ["constructor"]: typeof InstanceStatusChangedEvent; - static type = "event"; - static src = ["instance", "host"]; - static dst = "controller"; + static type = "event" as const; + static src = ["instance", "host"] as const; + static dst = "controller" as const; constructor( public instanceId: number, @@ -810,9 +810,9 @@ export class InstanceStatusChangedEvent { export class InstanceDetailsChangedEvent { declare ["constructor"]: typeof InstanceDetailsChangedEvent; - static type = "event"; - static src = "instance"; - static dst = "controller"; + static type = "event" as const; + static src = "instance" as const; + static dst = "controller" as const; constructor( public details: InstanceDetails, @@ -827,9 +827,9 @@ export class InstanceDetailsChangedEvent { export class InstanceBanlistUpdateEvent { declare ["constructor"]: typeof InstanceBanlistUpdateEvent; - static type = "event"; - static src = "controller"; - static dst = "instance"; + static type = "event" as const; + static src = "controller" as const; + static dst = "instance" as const; constructor( public name: string, @@ -850,9 +850,9 @@ export class InstanceBanlistUpdateEvent { export class InstanceAdminlistUpdateEvent { declare ["constructor"]: typeof InstanceAdminlistUpdateEvent; - static type = "event"; - static src = "controller"; - static dst = "instance"; + static type = "event" as const; + static src = "controller" as const; + static dst = "instance" as const; constructor( public name: string, @@ -871,9 +871,9 @@ export class InstanceAdminlistUpdateEvent { export class InstanceWhitelistUpdateEvent { declare ["constructor"]: typeof InstanceWhitelistUpdateEvent; - static type = "event"; - static src = "controller"; - static dst = "instance"; + static type = "event" as const; + static src = "controller" as const; + static dst = "instance" as const; constructor( public name: string, @@ -892,9 +892,9 @@ export class InstanceWhitelistUpdateEvent { export class InstancePlayerUpdateEvent { declare ["constructor"]: typeof InstancePlayerUpdateEvent; - static type = "event"; - static src = "instance"; - static dst = "controller"; + static type = "event" as const; + static src = "instance" as const; + static dst = "controller" as const; constructor( public type: string, diff --git a/packages/lib/src/data/messages_mod.ts b/packages/lib/src/data/messages_mod.ts index a7c8c72c..0fadcfda 100644 --- a/packages/lib/src/data/messages_mod.ts +++ b/packages/lib/src/data/messages_mod.ts @@ -6,10 +6,10 @@ import { JsonString, jsonArray } from "./composites"; export class ModPackGetRequest { declare ["constructor"]: typeof ModPackGetRequest; - static type = "request"; - static src = ["instance", "control"]; - static dst = "controller"; - static permission = "core.mod_pack.get"; + static type = "request" as const; + static src = ["instance", "control"] as const; + static dst = "controller" as const; + static permission = "core.mod_pack.get" as const; constructor( public id: number, @@ -28,28 +28,28 @@ export class ModPackGetRequest { export class ModPackGetDefaultRequest { declare ["constructor"]: typeof ModPackGetDefaultRequest; - static type = "request"; - static src = ["instance", "control"]; - static dst = "controller"; - static permission = "core.mod_pack.get"; + static type = "request" as const; + static src = ["instance", "control"] as const; + static dst = "controller" as const; + static permission = "core.mod_pack.get" as const; static Response = ModPack; } export class ModPackListRequest { declare ["constructor"]: typeof ModPackListRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod_pack.list"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod_pack.list" as const; static Response = jsonArray(ModPack); } export class ModPackCreateRequest { declare ["constructor"]: typeof ModPackCreateRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod_pack.create"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod_pack.create" as const; constructor( public modPack: ModPack, @@ -66,10 +66,10 @@ export class ModPackCreateRequest { export class ModPackUpdateRequest { declare ["constructor"]: typeof ModPackUpdateRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod_pack.update"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod_pack.update" as const; constructor( public modPack: ModPack, @@ -86,10 +86,10 @@ export class ModPackUpdateRequest { export class ModPackDeleteRequest { declare ["constructor"]: typeof ModPackDeleteRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod_pack.delete"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod_pack.delete" as const; constructor( public id: number, @@ -106,10 +106,10 @@ export class ModPackDeleteRequest { export class ModPackSetSubscriptionsRequest { declare ["constructor"]: typeof ModPackSetSubscriptionsRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod_pack.subscribe"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod_pack.subscribe" as const; constructor( public all: boolean, @@ -128,10 +128,10 @@ export class ModPackSetSubscriptionsRequest { export class ModGetRequest { declare ["constructor"]: typeof ModGetRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.get"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.get" as const; constructor( public name: string, @@ -152,19 +152,19 @@ export class ModGetRequest { export class ModListRequest { declare ["constructor"]: typeof ModListRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.list"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.list" as const; static Response = jsonArray(ModInfo); } export class ModSearchRequest { declare ["constructor"]: typeof ModSearchRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.search"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.search" as const; constructor( public query: string, @@ -178,8 +178,8 @@ export class ModSearchRequest { static jsonSchema = Type.Object({ "query": Type.String(), "factorioVersion": Type.String(), - "pageSize": Type.Integer(), - "page": Type.Optional(Type.Integer()), + "page": Type.Integer(), + "pageSize": Type.Optional(Type.Integer()), "sort": Type.Optional(Type.String()), "sortOrder": Type.Optional(Type.String()), }); @@ -219,10 +219,10 @@ export class ModSearchRequest { export class ModSetSubscriptionsRequest { declare ["constructor"]: typeof ModSetSubscriptionsRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.subscribe"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.subscribe" as const; constructor( public all: boolean, @@ -241,10 +241,10 @@ export class ModSetSubscriptionsRequest { export class ModDownloadRequest { declare ["constructor"]: typeof ModDownloadRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.download"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.download" as const; constructor( public name: string, @@ -265,10 +265,10 @@ export class ModDownloadRequest { export class ModDeleteRequest { declare ["constructor"]: typeof ModDeleteRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.mod.delete"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.delete" as const; constructor( public name: string, @@ -287,9 +287,9 @@ export class ModDeleteRequest { export class ModPackUpdateEvent { declare ["constructor"]: typeof ModPackUpdateEvent; - static type = "event"; - static src = "controller"; - static dst = "control"; + static type = "event" as const; + static src = "controller" as const; + static dst = "control" as const; constructor( public modPack: ModPack, @@ -306,9 +306,9 @@ export class ModPackUpdateEvent { export class ModUpdateEvent { declare ["constructor"]: typeof ModUpdateEvent; - static type = "event"; - static src = "controller"; - static dst = "control"; + static type = "event" as const; + static src = "controller" as const; + static dst = "control" as const; constructor( public mod: ModInfo, diff --git a/packages/lib/src/data/messages_user.ts b/packages/lib/src/data/messages_user.ts index a9b72769..a975476b 100644 --- a/packages/lib/src/data/messages_user.ts +++ b/packages/lib/src/data/messages_user.ts @@ -22,10 +22,10 @@ export class RawPermission { // TODO refactor into lib/user.Permission export class PermissionListRequest { declare ["constructor"]: typeof PermissionListRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.permission.list"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.permission.list" as const; static Response = jsonArray(RawPermission); } @@ -51,19 +51,19 @@ export class RawRole { // TODO refactor into lib/user.Role export class RoleListRequest { declare ["constructor"]: typeof RoleListRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.role.list"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.list" as const; static Response = jsonArray(RawRole); } export class RoleCreateRequest { declare ["constructor"]: typeof RoleCreateRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.role.create"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.create" as const; constructor( public name: string, @@ -86,10 +86,10 @@ export class RoleCreateRequest { export class RoleUpdateRequest { declare ["constructor"]: typeof RoleUpdateRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.role.update"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.update" as const; constructor( public id: number, @@ -112,10 +112,10 @@ export class RoleUpdateRequest { export class RoleGrantDefaultPermissionsRequest { declare ["constructor"]: typeof RoleGrantDefaultPermissionsRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.role.update"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.update" as const; constructor( public id: number, @@ -132,10 +132,10 @@ export class RoleGrantDefaultPermissionsRequest { export class RoleDeleteRequest { declare ["constructor"]: typeof RoleDeleteRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.role.delete"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.delete" as const; constructor( public id: number, @@ -173,20 +173,20 @@ export class RawUser { // TODO refactor into lib/user.User isWhitelisted: Type.Optional(Type.Boolean()), banReason: Type.Optional(Type.String()), isDeleted: Type.Optional(Type.Boolean()), - playerStats: Type.Optional(Type.Unsafe(PlayerStats.jsonSchema)), + playerStats: Type.Optional(PlayerStats.jsonSchema), instanceStats: Type.Optional( Type.Array( - Type.Tuple([Type.Integer(), Type.Unsafe(PlayerStats.jsonSchema)]), + Type.Tuple([Type.Integer(), PlayerStats.jsonSchema]), ) ), }); static fromJSON(json: Static) { - let playerStats: PlayerStats; + let playerStats: PlayerStats | undefined; if (json.playerStats) { playerStats = PlayerStats.fromJSON(json.playerStats); } - let instanceStats: Map; + let instanceStats: Map | undefined; if (json.instanceStats) { instanceStats = new Map( json.instanceStats.map(([id, stats]) => [id, PlayerStats.fromJSON(stats)]) @@ -208,10 +208,12 @@ export class RawUser { // TODO refactor into lib/user.User isWhitelisted: this.isWhitelisted, banReason: this.banReason, isDeleted: this.isDeleted, - playerStats: this.playerStats, }; + if (this.playerStats) { + json.playerStats = this.playerStats.toJSON() + } if (this.instanceStats) { - json.instanceStats = [...this.instanceStats]; + json.instanceStats = [...this.instanceStats].map(([k, v]) => [k, v.toJSON()]); } return json; } @@ -219,10 +221,10 @@ export class RawUser { // TODO refactor into lib/user.User export class UserGetRequest { declare ["constructor"]: typeof UserGetRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.get"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.get" as const; constructor( public name: string, @@ -241,19 +243,19 @@ export class UserGetRequest { export class UserListRequest { declare ["constructor"]: typeof UserListRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.list"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.list" as const; static Response = jsonArray(RawUser); } export class UserSetSubscriptionsRequest { declare ["constructor"]: typeof UserSetSubscriptionsRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.subscribe"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.subscribe" as const; constructor( public all: boolean, @@ -272,10 +274,10 @@ export class UserSetSubscriptionsRequest { export class UserCreateRequest { declare ["constructor"]: typeof UserCreateRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.create"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.create" as const; constructor( public name: string, @@ -292,10 +294,10 @@ export class UserCreateRequest { export class UserRevokeTokenRequest { declare ["constructor"]: typeof UserRevokeTokenRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.revoke_token"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.revoke_token" as const; constructor( public name: string, @@ -312,10 +314,10 @@ export class UserRevokeTokenRequest { export class UserUpdateRolesRequest { declare ["constructor"]: typeof UserUpdateRolesRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.update_roles"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.update_roles" as const; constructor( public name: string, @@ -334,10 +336,10 @@ export class UserUpdateRolesRequest { export class UserSetAdminRequest { declare ["constructor"]: typeof UserSetAdminRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.set_admin"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.set_admin" as const; constructor( public name: string, @@ -358,10 +360,10 @@ export class UserSetAdminRequest { export class UserSetWhitelistedRequest { declare ["constructor"]: typeof UserSetWhitelistedRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.set_whitelisted"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.set_whitelisted" as const; constructor( public name: string, @@ -382,10 +384,10 @@ export class UserSetWhitelistedRequest { export class UserSetBannedRequest { declare ["constructor"]: typeof UserSetBannedRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.set_banned"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.set_banned" as const; constructor( public name: string, @@ -408,10 +410,10 @@ export class UserSetBannedRequest { export class UserDeleteRequest { declare ["constructor"]: typeof UserDeleteRequest; - static type = "request"; - static src = "control"; - static dst = "controller"; - static permission = "core.user.delete"; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.delete" as const; constructor( public name: string, @@ -429,9 +431,9 @@ export class UserDeleteRequest { export class AccountUpdateEvent { declare ["constructor"]: typeof AccountUpdateEvent; - static type = "event"; - static src = "controller"; - static dst = "control"; + static type = "event" as const; + static src = "controller" as const; + static dst = "control" as const; constructor( public roles?: { name: string, id: number, permissions: string[] }[], @@ -454,9 +456,9 @@ export class AccountUpdateEvent { export class UserUpdateEvent { declare ["constructor"]: typeof UserUpdateEvent; - static type = "event"; - static src = "controller"; - static dst = "control"; + static type = "event" as const; + static src = "controller" as const; + static dst = "control" as const; constructor( public user: RawUser, diff --git a/packages/lib/src/database.ts b/packages/lib/src/database.ts index 3bc65235..3b388ecf 100644 --- a/packages/lib/src/database.ts +++ b/packages/lib/src/database.ts @@ -54,7 +54,7 @@ export async function loadJsonAsMap(filePath: string): Promise(promise: Promise, time: number, timeoutResult: T) { - let timer: ReturnType; + let timer: NodeJS.Timer | undefined; try { return await Promise.race([ promise, @@ -157,10 +157,10 @@ function parseSearchTerm( input: string, attributes: Record, issues: Array -): [number, ParsedTerm] { +): [number, ParsedTerm | undefined] { // attribute = identifier, ':', word // term = word | attribute - let term: ParsedTerm; + let term: ParsedTerm | undefined; if (["-", '"'].includes(input.charAt(pos))) { ([pos, term] = parseSearchWord(pos, input)); } else { @@ -274,12 +274,12 @@ export function parseSearchString( // search = [ term, *( [ whitespace ], term ) ] input = input.trim(); let parsed = { - terms: [], - issues: [], + terms: [] as ParsedTerm[], + issues: [] as string[], }; let pos = 0; while (pos < input.length) { - let term: ParsedTerm; + let term: ParsedTerm | undefined; ([pos, term] = parseSearchTerm(pos, input, attributes, parsed.issues)); if (term) { parsed.terms.push(term); diff --git a/packages/lib/src/link/WebSocket.d.ts b/packages/lib/src/link/WebSocket.d.ts new file mode 100644 index 00000000..c44f2672 --- /dev/null +++ b/packages/lib/src/link/WebSocket.d.ts @@ -0,0 +1,2 @@ +import ws = require("ws"); +export = ws; diff --git a/packages/lib/src/link/WebSocket.js b/packages/lib/src/link/WebSocket.js new file mode 100644 index 00000000..b85227a1 --- /dev/null +++ b/packages/lib/src/link/WebSocket.js @@ -0,0 +1,3 @@ +"use strict"; +// eslint-disable-next-line node/no-process-env +module.exports = process.env.APP_ENV === "browser" ? window.WebSocket : require("ws"); diff --git a/packages/lib/src/link/connectors.ts b/packages/lib/src/link/connectors.ts index 86ec3980..b52d2103 100644 --- a/packages/lib/src/link/connectors.ts +++ b/packages/lib/src/link/connectors.ts @@ -2,14 +2,13 @@ "use strict"; import { strict as assert } from "assert"; import events from "events"; -// eslint-disable-next-line node/no-process-env -// @ts-ignore -const WebSocket = process.env.APP_ENV === "browser" ? window.WebSocket : require("ws"); +import WebSocket from "./WebSocket"; import * as libData from "../data"; import * as libErrors from "../errors"; import { logger } from "../logging"; import ExponentialBackoff from "../ExponentialBackoff"; +import type { Request, Event } from "./link"; /** @@ -37,15 +36,15 @@ export abstract class BaseConnector extends events.EventEmitter { abstract send(message: libData.Message): void; - sendRequest( - request: libData.Request, + sendRequest( + request: Request, requestId: number, dst: libData.Address ) { let seq = this._seq; this._seq += 1; let src = new libData.Address(this.src.type, this.src.id, requestId); - let Request = request.constructor as typeof libData.Request; + let Request = request.constructor; let name = Request.plugin ? `${Request.plugin}:${Request.name}` : Request.name; let hasData = Boolean(Request.jsonSchema); const message = new libData.MessageRequest(seq, src, dst, name, hasData ? request : undefined); @@ -69,10 +68,10 @@ export abstract class BaseConnector extends events.EventEmitter { return seq; } - sendEvent(event: libData.Event, dst: libData.Address) { + sendEvent(event: Event, dst: libData.Address) { let seq = this._seq; this._seq += 1; - let Event = event.constructor as typeof libData.Event; + let Event = event.constructor; let name = Event.plugin ? `${Event.plugin}:${Event.name}` : Event.name; let hasData = Boolean(Event.jsonSchema); const message = new libData.MessageEvent(seq, this.src, dst, name, hasData ? event : undefined); @@ -82,6 +81,12 @@ export abstract class BaseConnector extends events.EventEmitter { } type ConnectorState = "closed" | "connecting" | "connected" | "resuming"; +type ReliableMessages = + | libData.MessageRequest + | libData.MessageResponse + | libData.MessageResponseError + | libData.MessageEvent +; /** * Base connector for links @@ -92,12 +97,12 @@ export abstract class WebSocketBaseConnector extends BaseConnector { // One of closed, connecting (client only), connected and resuming. _state: ConnectorState = "closed"; _closing = false; - _socket = null; - _lastHeartbeat = null; - _heartbeatId: NodeJS.Timer = null; + _socket: WebSocket | null = null; + _lastHeartbeat: number | null = null; + _heartbeatId: NodeJS.Timer | null = null; _heartbeatInterval: number | null = null; _lastReceivedSeq = undefined; - _sendBuffer = []; + _sendBuffer: (ReliableMessages)[] = []; constructor(src: libData.Address, dst: libData.Address) { super(src, dst); @@ -194,13 +199,13 @@ export abstract class WebSocketBaseConnector extends BaseConnector { _doHeartbeat() { this._check("connected"); - if (Date.now() - this._lastHeartbeat > 2000 * this._heartbeatInterval) { + if (Date.now() - this._lastHeartbeat! > 2000 * this._heartbeatInterval!) { logger.verbose("Connector | closing after heartbeat timed out"); // eslint-disable-next-line node/no-process-env if (process.env.APP_ENV === "browser") { - this._socket.close(4008, "Heartbeat timeout"); + this._socket!.close(4008, "Heartbeat timeout"); } else { - this._socket.terminate(); + this._socket!.terminate(); } } else { @@ -223,7 +228,7 @@ export abstract class WebSocketBaseConnector extends BaseConnector { this._lastHeartbeat = Date.now(); this._heartbeatId = setInterval(() => { this._doHeartbeat(); - }, this._heartbeatInterval * 1000); + }, this._heartbeatInterval! * 1000); } /** @@ -244,7 +249,7 @@ export abstract class WebSocketBaseConnector extends BaseConnector { * * @param message - Message to send. */ - send(message: libData.Message) { + send(message: ReliableMessages) { if (!["connected", "resuming"].includes(this._state)) { throw new libErrors.SessionLost("No session"); } @@ -256,7 +261,7 @@ export abstract class WebSocketBaseConnector extends BaseConnector { } _sendInternal(message: libData.Message) { - this._socket.send(JSON.stringify(message)); + this._socket!.send(JSON.stringify(message)); } /** @@ -280,9 +285,9 @@ export abstract class WebSocketBaseConnector extends BaseConnector { } this.setClosing(); - this.send(new libData.MessageDisconnect("prepare")); + this._sendInternal(new libData.MessageDisconnect("prepare")); - let timeout: NodeJS.Timeout; + let timeout: NodeJS.Timeout | undefined; let waitTimeout = new Promise(resolve => { timeout = setTimeout(resolve, 10000); }); // Make Configurable? try { await Promise.race([ @@ -326,7 +331,7 @@ export abstract class WebSocketBaseConnector extends BaseConnector { * @extends module:lib.WebSocketBaseConnector */ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { - _reconnectId: NodeJS.Timeout | null = null; + _reconnectId?: NodeJS.Timeout; _sessionToken: string | null = null; _sessionTimeout: number | null = null; _startedResuming: number | null = null; @@ -337,7 +342,7 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { maxReconnectDelay: number, protected _tlsCa = null ) { - super(undefined, new libData.Address(libData.Address.controller, 0)); + super(undefined as any, new libData.Address(libData.Address.controller, 0)); this._backoff = new ExponentialBackoff({ max: maxReconnectDelay }); // The following states are used in the client connector @@ -349,21 +354,21 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { _reset() { clearTimeout(this._reconnectId); - this._reconnectId = null; + this._reconnectId = undefined; this._sessionToken = null; this._sessionTimeout = null; this._startedResuming = null; super._reset(); - this.src = undefined; + this.src = undefined as any; } _invalidate() { - assert(this._reconnectId === null); + assert(this._reconnectId === undefined); this._sessionToken = null; this._sessionTimeout = null; this._startedResuming = null; super._invalidate(); - this.src = undefined; + this.src = undefined as any; } /** @@ -394,7 +399,7 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { this._closing = true; if (this._state === "connected" || this._socket) { - this._socket.close(code, reason); + this._socket!.close(code, reason); await events.once(this, "close"); } else { @@ -421,7 +426,7 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { } if (this._socket) { - this._socket.close(1000, "Connector closing"); + this._socket!.close(1000, "Connector closing"); } else { this._reset(); @@ -471,14 +476,14 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { throw new Error("Cannot reconnect while socket is open"); } - if (this._reconnectId !== null) { + if (this._reconnectId !== undefined) { logger.error("Unexpected double call to reconnect"); } let delay = this._backoff.delay(); if ( this._state === "resuming" - && this._startedResuming + this._sessionTimeout * 1000 < Date.now() + delay + && this._startedResuming! + this._sessionTimeout! * 1000 < Date.now() + delay ) { logger.error("Connector | Session timed out trying to resume"); this._reset(); @@ -494,14 +499,13 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { `Connector | waiting ${(Math.round(delay / 10) / 100)} seconds for reconnect` ); this._reconnectId = setTimeout(() => { - this._reconnectId = null; + this._reconnectId = undefined; this._doConnect(); }, delay); } _attachSocketHandlers() { - // TODO type event - this._socket.onclose = (event: any) => { + this._socket!.onclose = (event) => { const previousState = this._state; // Authentication failed @@ -544,7 +548,7 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { } }; - this._socket.onerror = (event: any) => { + this._socket!.onerror = (event) => { // It's assumed that close is always called by ws let code = !event.error ? "" : `, code: ${event.error.code}`; let message = `Connector | Socket error: ${event.message || "unknown error"}${code}`; @@ -568,13 +572,13 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { } }; - this._socket.onopen = () => { + this._socket!.onopen = () => { logger.verbose("Connector | Open"); }; // Handle messages - this._socket.onmessage = (event: any) => { - let message = this._parseMessage(event.data); + this._socket!.onmessage = (event) => { + let message = this._parseMessage(event.data as string); if (!message) { return; } diff --git a/packages/lib/src/link/link.ts b/packages/lib/src/link/link.ts index 20d50713..e4352b3a 100644 --- a/packages/lib/src/link/link.ts +++ b/packages/lib/src/link/link.ts @@ -7,13 +7,42 @@ import { dataClasses } from "./messages"; import { BaseConnector, WebSocketBaseConnector } from "./connectors"; import { strict as assert } from "assert"; import type { PluginInfo } from "../plugin"; +import type { User } from "../users"; +import type { AddressType, JSONDeserialisable, Message, MessageRequest, MessageEvent } from "../data"; + +export interface Request { + constructor: Partial>> & { + new (...args: any): Request, + name: string, + type: "request"; + src: AddressType | readonly AddressType[]; + dst: AddressType | readonly AddressType[]; + permission?: null | string | ((user: User, message: MessageRequest) => void); + plugin?: string; + Response?: JSONDeserialisable, + } +} +export type RequestClass = Request["constructor"]; + +export interface Event { + constructor: Partial>> & { + new (...args: any): Event, + name: string, + type: "event"; + src: AddressType | readonly AddressType[]; + dst: AddressType | readonly AddressType[]; + permission?: null | string | ((user: User, message: MessageEvent) => void); + plugin?: string; + } +} +export type EventClass = Event["constructor"]; interface RequestEntry { - Request: typeof libData.Request; + Request: RequestClass; name: string, allowedSrcTypes: Set; allowedDstTypes: Set; - requestFromJSON: (json: any) => libData.Request; + requestFromJSON: (json: any) => Request; Response?: any; responseFromJSON: (json: any) => any; } @@ -32,14 +61,18 @@ interface ForwardedRequest { } interface EventEntry { - Event: typeof libData.Event; + Event: EventClass; name: string, allowedSrcTypes: Set; allowedDstTypes: Set; - eventFromJSON: (json: any) => libData.Event; + eventFromJSON: (json: any) => Event; +} +type RequestHandler = (request: Request, src: libData.Address, dst: libData.Address) => any; +type EventHandler = (event: Event, src: libData.Address, dst: libData.Address) => any; + +interface Router { + forwardMessage(origin: Link, message: Message, fallback: boolean): boolean, } -type RequestHandler = (request: libData.Request, src: libData.Address, dst: libData.Address) => any; -type EventHandler = (request: libData.Event, src: libData.Address, dst: libData.Address) => any; // Some definitions for the terminology used here: // link: Either side of a controller - client connection @@ -52,7 +85,7 @@ type EventHandler = (request: libData.Event, src: libData.Address, dst: libData. * Common interface for server and client connections */ export class Link { - router = undefined; + router?: Router = undefined; validateSent = true; _requestHandlers = new Map(); @@ -66,7 +99,6 @@ export class Link { constructor( public connector: BaseConnector ) { - // @ts-ignore: TODO Remove when lib/data is migrated to ts this.handle(libData.PingRequest, () => {}); // Process messages received by the connector @@ -207,7 +239,7 @@ export class Link { throw new libErrors.InvalidMessage("Message src may not be broadcast"); } this.validateIngress(message); - } catch (err) { + } catch (err: any) { if (message.type === "request") { this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src); } @@ -314,7 +346,7 @@ export class Link { _processRequest( message: libData.MessageRequest, entry: RequestEntry, - handler: RequestHandler, + handler: RequestHandler, spoofedSrc?: libData.Address ) { if (!handler) { @@ -327,7 +359,7 @@ export class Link { let response: any; try { response = handler(entry.requestFromJSON(message.data), message.src, message.dst); - } catch (err) { + } catch (err: any) { if (err.errors) { logger.error(JSON.stringify(err.errors, null, 4)); } @@ -366,7 +398,7 @@ export class Link { this.connector.sendResponseError( new libData.ResponseError(err.message, err.code, err.stack), message.src, spoofedSrc ); - } catch (err) { + } catch (err: any) { logger.error(`Unexpected error sending error response for ${message.name}:\n${err.stack}`); } } @@ -374,7 +406,7 @@ export class Link { } _processResponse(message: libData.MessageResponse) { - let pending = this._pendingRequests.get(message.dst.requestId); + let pending = this._pendingRequests.get(message.dst.requestId!); if (!pending) { throw new libErrors.InvalidMessage( `Received response ${message.dst.requestId} without a pending request` @@ -387,14 +419,14 @@ export class Link { try { pending.resolve(pending.request.responseFromJSON(message.data)); - } catch (err) { + } catch (err: any) { // An invalid response object was likely received pending.reject(err); } } _processResponseError(message: libData.MessageResponseError) { - let pending = this._pendingRequests.get(message.dst.requestId); + let pending = this._pendingRequests.get(message.dst.requestId!); if (!pending) { throw new libErrors.InvalidMessage( `Received error response ${message.dst.requestId} without a pending request` @@ -441,10 +473,10 @@ export class Link { * Promise that resolves to the response if a request was sent or * undefined if it was an event. */ - send(requestOrEvent: libData.Request): Promise; - send(requestOrEvent: libData.Event): void; - send(requestOrEvent: libData.Request | libData.Event): Promise | void { - return this.sendTo(this.connector.dst, requestOrEvent); + send(requestOrEvent: Request): Promise; + send(requestOrEvent: Event): void; + send(requestOrEvent: Request | Event): Promise | void { + return this.sendTo(this.connector.dst, requestOrEvent as any); } /** @@ -456,26 +488,26 @@ export class Link { * Promise that resolves to the response if a request was sent or * undefined if it was an event. */ - sendTo(destination: libData.AddressShorthand, requestOrEvent: libData.Request): Promise; - sendTo(destination: libData.AddressShorthand, requestOrEvent: libData.Event): void; + sendTo(destination: libData.AddressShorthand, requestOrEvent: Request): Promise; + sendTo(destination: libData.AddressShorthand, requestOrEvent: Event): void; sendTo( destination: libData.AddressShorthand, - requestOrEvent: libData.Request | libData.Event, + requestOrEvent: Request | Event, ): Promise | void { let dst = libData.Address.fromShorthand(destination); // TODO investigate if these types can be inferred - if ((requestOrEvent.constructor as any).type === "request") { - return this.sendRequest(requestOrEvent, dst); + if (requestOrEvent.constructor.type === "request") { + return this.sendRequest(requestOrEvent as Request, dst); } - if ((requestOrEvent.constructor as any).type === "event") { - return this.sendEvent(requestOrEvent, dst); + if (requestOrEvent.constructor.type === "event") { + return this.sendEvent(requestOrEvent as Event, dst); } throw Error(`Expected request or event but got type ${(requestOrEvent.constructor as any).type}`); } - sendRequest(request: libData.Request, dst: libData.Address) { - let entry = Link._requestsByClass.get(request.constructor as typeof libData.Request); + sendRequest(request: Request, dst: libData.Address) { + let entry = Link._requestsByClass.get(request.constructor as RequestClass); if (!entry) { throw new Error(`Attempt to send unregistered Request ${request.constructor.name}`); } @@ -492,7 +524,7 @@ export class Link { let requestId = this._nextRequestId; this._nextRequestId += 1; this._pendingRequests.set( - requestId, { request: entry, promise, resolve, reject, dst, } + requestId, { request: entry, promise, resolve: resolve!, reject: reject!, dst, } ); this.connector.sendRequest(request, requestId, dst); return promise; @@ -508,8 +540,8 @@ export class Link { this.connector.send(message); } - sendEvent(event: libData.Event, dst: libData.Address) { - let entry = Link._eventsByClass.get(event.constructor as typeof libData.Event); + sendEvent(event: Event, dst: libData.Address) { + let entry = Link._eventsByClass.get(event.constructor as EventClass); if (!entry) { throw new Error(`Attempt to send unregistered Event ${event.constructor.name}`); } @@ -520,23 +552,23 @@ export class Link { this.connector.sendEvent(event, dst); } - handle(Class: typeof libData.Request, handler: RequestHandler): void; - handle(Class: typeof libData.Event, handler: EventHandler): void; + handle(Class: RequestClass, handler: RequestHandler): void; + handle(Class: EventClass, handler: EventHandler): void; handle( - Class: typeof libData.Request | typeof libData.Event, - handler: RequestHandler | EventHandler, + Class: RequestClass | EventClass, + handler: RequestHandler | EventHandler, ) { if (Class.type === "request") { - this.handleRequest(Class, handler); + this.handleRequest(Class, handler as RequestHandler); } else if (Class.type === "event") { - this.handleEvent(Class, handler); + this.handleEvent(Class, handler as EventHandler); } else { throw new Error(`Class ${(Class as any).name} has unrecognized type ${(Class as any).type}`); } } - handleRequest(Request: typeof libData.Request, handler: RequestHandler) { - let entry = Link._requestsByClass.get(Request); + handleRequest(Request: RequestClass, handler: RequestHandler) { + let entry = Link._requestsByClass.get(Request as RequestClass); if (!entry) { throw new Error(`Unregistered Request class ${Request.name}`); } @@ -546,8 +578,8 @@ export class Link { this._requestHandlers.set(Request, handler); } - fallbackRequest(Request: typeof libData.Request, handler: RequestHandler) { - let entry = Link._requestsByClass.get(Request); + fallbackRequest(Request: RequestClass, handler: RequestHandler) { + let entry = Link._requestsByClass.get(Request as RequestClass); if (!entry) { throw new Error(`Unregistered Request class ${Request.name}`); } @@ -557,8 +589,8 @@ export class Link { this._requestFallbacks.set(Request, handler); } - handleEvent(Event: typeof libData.Event, handler: EventHandler) { - let entry = Link._eventsByClass.get(Event); + handleEvent(Event: EventClass, handler: EventHandler) { + let entry = Link._eventsByClass.get(Event as EventClass); if (!entry) { throw new Error(`Unregistered Event class ${Event.name}`); } @@ -568,8 +600,8 @@ export class Link { this._eventHandlers.set(Event, handler); } - snoopEvent(Event: typeof libData.Event, handler: EventHandler) { - let entry = Link._eventsByClass.get(Event); + snoopEvent(Event: EventClass, handler: EventHandler) { + let entry = Link._eventsByClass.get(Event as EventClass); if (!entry) { throw new Error(`Unregistered Event class ${Event.name}`); } @@ -579,7 +611,9 @@ export class Link { this._eventSnoopers.set(Event, handler); } - static register(Class: typeof libData.Request | typeof libData.Event) { + static register(Class: RequestClass): void; + static register(Class: EventClass): void; + static register(Class: RequestClass | EventClass) { if (Class.type === "request") { this.registerRequest(Class); } else if (Class.type === "event") { @@ -589,7 +623,7 @@ export class Link { } } - static requestFromJSON(Request: typeof libData.Request, name: string) { + static requestFromJSON(Request: RequestClass, name: string): (json: any) => Request { if (Request.fromJSON) { if (!Request.jsonSchema) { throw new Error(`Request ${name} has static fromJSON but is missing static jsonSchema`); @@ -599,7 +633,7 @@ export class Link { if (!validate(json)) { throw new libErrors.InvalidMessage(`Request ${name} failed validation`, validate.errors); } - return Request.fromJSON(json); + return Request.fromJSON!(json); }; } if (Request.jsonSchema) { @@ -608,7 +642,7 @@ export class Link { return () => new Request(); } - static responseFromJSON(Response: libData.Serialisable, name: string) { + static responseFromJSON(Response: JSONDeserialisable, name: string) { if (!Response.jsonSchema) { throw new Error(`Response for Request ${name} is missing static jsonSchema`); } @@ -627,7 +661,11 @@ export class Link { }; } - static allowedTypes(types: libData.AddressType | libData.AddressType[], name: string, side: "src" | "dst") { + static allowedTypes( + types: libData.AddressType | readonly libData.AddressType[], + name: string, + side: "src" | "dst" + ) { if (types === undefined) { throw new Error(`Missing ${side} specification in ${name}`); } @@ -651,9 +689,9 @@ export class Link { } static _requestsByName = new Map(); - static _requestsByClass = new Map(); + static _requestsByClass = new Map, RequestEntry>(); - static registerRequest(Request: typeof libData.Request) { + static registerRequest(Request: RequestClass) { const name = Request.plugin ? `${Request.plugin}:${Request.name}` : Request.name; if (this._requestsByName.has(name)) { throw new Error(`Request ${name} is already registered`); @@ -685,7 +723,7 @@ export class Link { this._requestsByClass.set(Request, entry); } - static eventFromJSON(Event: typeof libData.Event, name: string) { + static eventFromJSON(Event: EventClass, name: string) { if (Event.fromJSON) { if (!Event.jsonSchema) { throw new Error(`Event ${name} has static fromJSON but is missing static jsonSchema`); @@ -695,7 +733,7 @@ export class Link { if (!validate(json)) { throw new libErrors.InvalidMessage(`Event ${name} failed validation`, validate.errors); } - return Event.fromJSON(json); + return Event.fromJSON!(json); }; } if (Event.jsonSchema) { @@ -705,16 +743,16 @@ export class Link { } static _eventsByName = new Map(); - static _eventsByClass = new Map(); + static _eventsByClass = new Map, EventEntry>(); - static registerEvent(Event: typeof libData.Event) { + static registerEvent(Event: EventClass) { const name = Event.plugin ? `${Event.plugin}:${Event.name}` : Event.name; if (this._eventsByName.has(name)) { throw new Error(`Event ${name} is already registered`); } - let entry = { - Event, + let entry: EventEntry = { + Event: Event as EventClass, name, eventFromJSON: this.eventFromJSON(Event, name), allowedSrcTypes: this.allowedTypes(Event.src, name, "src"), @@ -729,13 +767,12 @@ export class Link { } this._eventsByName.set(name, entry); - this._eventsByClass.set(Event, entry); + this._eventsByClass.set(Event as EventClass, entry); } } for (let Class of dataClasses) { - // @ts-ignore: TODO Remove when lib/data is migrated to ts - Link.register(Class); + Link.register(Class as RequestClass); } export function registerPluginMessages(pluginInfos: PluginInfo[]) { diff --git a/packages/lib/src/link/messages.ts b/packages/lib/src/link/messages.ts index 1566c1de..b6161e10 100644 --- a/packages/lib/src/link/messages.ts +++ b/packages/lib/src/link/messages.ts @@ -5,6 +5,7 @@ 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 type { RequestClass, EventClass } from "./link"; /** * Event and Request payloads registered with the Link class. Used to decode @@ -12,7 +13,7 @@ import * as user from "../data/messages_user"; * their proper classes. * @internal */ -export const dataClasses = [ +export const dataClasses: (RequestClass | EventClass)[] = [ core.PingRequest, controller.ControllerConfigGetRequest, diff --git a/packages/lib/src/logging.ts b/packages/lib/src/logging.ts index 0a24cfb7..8eac6098 100644 --- a/packages/lib/src/logging.ts +++ b/packages/lib/src/logging.ts @@ -130,7 +130,7 @@ export interface LogFilter { export function logFilter({ all, controller, hostIds, instanceIds, maxLevel }: LogFilter) { return (info: any) => { // Note: reversed to filter out undefined levels - if (maxLevel && !(levels[info.level] <= levels[maxLevel])) { + if (maxLevel && !((levels as any)[info.level] <= levels[maxLevel])) { return false; } diff --git a/packages/lib/src/logging_utils.ts b/packages/lib/src/logging_utils.ts index 0d075e5c..f55b0ddd 100644 --- a/packages/lib/src/logging_utils.ts +++ b/packages/lib/src/logging_utils.ts @@ -256,7 +256,12 @@ export class LogIndex { serialize() { let serialized = { version: logIndexVersion, - files: {}, + files: {} as Record, }; for (let [file, entry] of this.index) { @@ -285,7 +290,7 @@ export class LogIndex { try { let content = await fs.readFile(path.join(logDirectory, "index.json")); return new LogIndex(logDirectory, JSON.parse(content.toString())); - } catch (err) { + } catch (err: any) { if (err.code !== "ENOENT") { logger.warn(`Failed to load ${path.join(logDirectory, "index.json")}: ${err}`); } @@ -369,7 +374,7 @@ export class LogIndex { * true if the file may contain entries included by the filter. */ filterIncludesFile(file: string, { maxLevel, all, controller, instanceIds, hostIds }: LogFilter) { - let entry = this.index.get(file); + const entry = this.index.get(file); if (!entry) { return true; } @@ -412,7 +417,7 @@ export interface QueryLogFilter { * Return entries in ascending ("asc") date order or desceding ("desc") * date order. Defaults to "asc". */ - order: "asc" | "desc"; + order?: "asc" | "desc"; /** * Maximum log level to include. Higher levels are more verbose. */ @@ -461,7 +466,7 @@ export async function queryLog(logDirectory: string, filter: QueryLogFilter, ind } const includeEntry = logFilter(filter); - let log = []; + let log: object[] = []; for (let file of files) { if (index && !index.filterIncludesFile(file, filter)) { continue; @@ -485,7 +490,7 @@ export async function queryLog(logDirectory: string, filter: QueryLogFilter, ind } if (includeEntry(info)) { log.push(info); - if (log.length >= filter.limit) { + if (log.length >= Number(filter.limit)) { lineStream.destroy(); fileStream.destroy(); } @@ -494,13 +499,13 @@ export async function queryLog(logDirectory: string, filter: QueryLogFilter, ind fileStream.pipe(lineStream); try { await finished(lineStream); - } catch (err) { - if (log.length < filter.limit || err.code !== "ERR_STREAM_PREMATURE_CLOSE") { + } catch (err: any) { + if (log.length < Number(filter.limit) || err.code !== "ERR_STREAM_PREMATURE_CLOSE") { throw err; } } - if (log.length >= filter.limit) { + if (log.length >= Number(filter.limit)) { break; } } diff --git a/packages/lib/src/plugin.ts b/packages/lib/src/plugin.ts index 7ccf4b20..de231701 100644 --- a/packages/lib/src/plugin.ts +++ b/packages/lib/src/plugin.ts @@ -297,11 +297,11 @@ export class BaseInstancePlugin { async _sendPendingRconMessages() { this._sendingRconMessages = true; while (this._pendingRconMessages.length) { - let task = this._pendingRconMessages.shift(); + let task = this._pendingRconMessages.shift()!; try { let result = await this.sendRcon(task.message, task.expectEmpty); task.resolve(result); - } catch (err) { + } catch (err: any) { task.reject(err); } } @@ -820,7 +820,7 @@ export async function invokeHook(plugins: Map, hook: string, // Use an object to detect if the hook failed on timeout or tried to return a value looking like an error const timeout = {}; let result = await libHelpers.timeout( - plugin[hook](...args), + (plugin as any)[hook](...args), 15000, timeout ); @@ -829,7 +829,7 @@ export async function invokeHook(plugins: Map, hook: string, } else if (result !== undefined) { results.push(result); } - } catch (err) { + } catch (err: any) { plugin.logger.error(`Ignoring error from plugin ${name} in ${hook}:\n${err.stack}`); } } diff --git a/packages/lib/src/plugin_loader.ts b/packages/lib/src/plugin_loader.ts index 6771dd7b..e557bea9 100644 --- a/packages/lib/src/plugin_loader.ts +++ b/packages/lib/src/plugin_loader.ts @@ -30,7 +30,7 @@ export async function loadPluginInfos(pluginList: Map) { pluginInfo = require(path.posix.join(pluginPath, "info")); pluginPackage = require(path.posix.join(pluginPath, "package.json")); - } catch (err) { + } catch (err: any) { throw new libErrors.PluginError(pluginName, err); } diff --git a/packages/lib/src/prometheus.ts b/packages/lib/src/prometheus.ts index 93540e8c..5ce2d685 100644 --- a/packages/lib/src/prometheus.ts +++ b/packages/lib/src/prometheus.ts @@ -307,11 +307,11 @@ function removeMatchingLabels(mapping: Map, labels: Record extends Collector { - callback: (collector: LabeledCollector) => void | Promise; + callback: ((collector: any) => (void | Promise)) | undefined; metric: Metric; private _children: Map; - private _childClass: { new(collector: LabeledCollector, key: string): Child }; + private _childClass: { new(collector: any, key: string): Child }; /** * Create optionally labeled collector @@ -343,9 +343,9 @@ export class LabeledCollector extends Collector { labels?: string[], _reservedLabels?: string[], register?: boolean, - callback?: (collector: LabeledCollector) => void | Promise, + callback?: (collector: any) => (void | Promise), }, - childClass: { new(collector: LabeledCollector, key: string): Child } + childClass: { new(collector: any, key: string): Child } ) { const { labels = [], _reservedLabels = [], register = true, callback, ...rest } = options; @@ -485,7 +485,7 @@ export class ValueCollector extends LabeledColl labels?: string[], _reservedLabels?: string[], register?: boolean, - callback?: (collector: ValueCollector) => void | Promise, + callback?: (collector: any) => (void | Promise), }, childClass: { new(collector: ValueCollector, key: string): Child } ) { @@ -507,12 +507,13 @@ export class ValueCollector extends LabeledColl * @returns value stored. */ get() { - return this._defaultChild.get(); + return this._defaultChild!.get(); } remove(...labels: LabelValues) { let key = super.remove(...labels); this._values.delete(key); + return key; } removeAll(labels: Record) { @@ -546,7 +547,7 @@ class ValueCollectorChild { * @returns value stored. */ get() { - return this._values.get(this._key); + return this._values.get(this._key)!; } } @@ -565,7 +566,7 @@ class CounterChild extends ValueCollectorChild { throw new Error("Expected value to be a positive number"); } - this._values.set(this._key, this._values.get(this._key) + value); + this._values.set(this._key, this._values.get(this._key)! + value); } } @@ -673,7 +674,7 @@ export class Counter extends ValueCollector { options: { labels?: string[], register?: boolean, - callback?: (collector: Counter) => void | Promise, + callback?: (collector: Collector) => (void | Promise), } = {}, ) { super("counter", name, help, options, CounterChild); @@ -686,7 +687,7 @@ export class Counter extends ValueCollector { * @param value - Positive number to increment by. */ inc(value = 1) { - this._defaultChild.inc(value); + this._defaultChild!.inc(value); } } @@ -701,7 +702,7 @@ class GaugeChild extends ValueCollectorChild { * @param value - number to increment by. */ inc(value = 1) { - this._values.set(this._key, this._values.get(this._key) + value); + this._values.set(this._key, this._values.get(this._key)! + value); } /** @@ -710,7 +711,7 @@ class GaugeChild extends ValueCollectorChild { * @param value - number to decrease by. */ dec(value = 1) { - this._values.set(this._key, this._values.get(this._key) - value); + this._values.set(this._key, this._values.get(this._key)! - value); } /** @@ -869,7 +870,7 @@ export class Gauge extends ValueCollector { options: { labels?: string[], register?: boolean, - callback?: (collector: Gauge) => void | Promise, + callback?: (collector: Gauge) => (void | Promise), } = {}, ) { super("gauge", name, help, options, GaugeChild); @@ -882,7 +883,7 @@ export class Gauge extends ValueCollector { * @param value - number to increment by. */ inc(value = 1) { - this._defaultChild.inc(value); + this._defaultChild!.inc(value); } /** @@ -892,7 +893,7 @@ export class Gauge extends ValueCollector { * @param value - number to decrease by. */ dec(value = 1) { - this._defaultChild.dec(value); + this._defaultChild!.dec(value); } /** @@ -902,7 +903,7 @@ export class Gauge extends ValueCollector { * @param value - number to set gauge to. */ set(value: number) { - this._defaultChild.set(value); + this._defaultChild!.set(value); } /** @@ -911,7 +912,7 @@ export class Gauge extends ValueCollector { * Note: Only works if this is an unlabeled collector. */ setToCurrentTime() { - this._defaultChild.setToCurrentTime(); + this._defaultChild!.setToCurrentTime(); } /** @@ -923,7 +924,7 @@ export class Gauge extends ValueCollector { * seconds from when the timer was started */ startTimer() { - return this._defaultChild.startTimer(); + return this._defaultChild!.startTimer(); } } @@ -964,8 +965,8 @@ class SummaryChild { * @param value - number to count into summary. */ observe(value: number) { - this._sumValues.set(this._key, this._sumValues.get(this._key) + value); - this._countValues.set(this._key, this._countValues.get(this._key) + 1); + this._sumValues.set(this._key, this._sumValues.get(this._key)! + value); + this._countValues.set(this._key, this._countValues.get(this._key)! + 1); } /** @@ -1083,7 +1084,7 @@ export class Summary extends LabeledCollector { options: { labels?: string[], register?: boolean, - callback?: (collector: Summary) => void | Promise, + callback?: (collector: Summary) => (void | Promise), } = {}, ) { super("summary", name, help, options, SummaryChild); @@ -1111,7 +1112,7 @@ export class Summary extends LabeledCollector { * @type {number} */ get sum() { - return this._defaultChild.sum; + return this._defaultChild!.sum; } /** @@ -1121,7 +1122,7 @@ export class Summary extends LabeledCollector { * @type {number} */ get count() { - return this._defaultChild.count; + return this._defaultChild!.count; } /** @@ -1131,7 +1132,7 @@ export class Summary extends LabeledCollector { * @param value - number to count into histogram buckets. */ observe(value: number) { - this._defaultChild.observe(value); + this._defaultChild!.observe(value); } /** @@ -1143,13 +1144,14 @@ export class Summary extends LabeledCollector { * when the timer was started into the metric. */ startTimer() { - return this._defaultChild.startTimer(); + return this._defaultChild!.startTimer(); } remove(...labels: LabelValues) { let key = super.remove(...labels); this._sumValues.delete(key); this._countValues.delete(key); + return key; } removeAll(labels: Record) { @@ -1240,12 +1242,12 @@ class HistogramChild { observe(value: number) { for (let [bound, key] of this._bucketKeys) { if (value <= bound) { - this._bucketValues.set(key, this._bucketValues.get(key) + 1); + this._bucketValues.set(key, this._bucketValues.get(key)! + 1); } } - this._sumValues.set(this._key, this._sumValues.get(this._key) + value); - this._countValues.set(this._key, this._countValues.get(this._key) + 1); + this._sumValues.set(this._key, this._sumValues.get(this._key)! + value); + this._countValues.set(this._key, this._countValues.get(this._key)! + 1); } /** @@ -1371,7 +1373,7 @@ export class Histogram extends LabeledCollector { buckets?: number[], labels?: string[], register?: boolean, - callback?: (collector: Histogram) => void | Promise, + callback?: (collector: Histogram) => (void | Promise), } = {}, ) { // These defaults are taken from the Python Prometheus client @@ -1411,7 +1413,7 @@ export class Histogram extends LabeledCollector { * Note: Only available if this is an unlabeled collector. */ get buckets() { - return this._defaultChild.buckets; + return this._defaultChild!.buckets; } /** @@ -1420,7 +1422,7 @@ export class Histogram extends LabeledCollector { * Note: Only available if this is an unlabeled collector. */ get sum() { - return this._defaultChild.sum; + return this._defaultChild!.sum; } /** @@ -1429,7 +1431,7 @@ export class Histogram extends LabeledCollector { * Note: Only available if this is an unlabeled collector. */ get count() { - return this._defaultChild.count; + return this._defaultChild!.count; } /** @@ -1438,7 +1440,7 @@ export class Histogram extends LabeledCollector { * Note: Only works if this is an unlabeled collector. */ observe(value: number) { - this._defaultChild.observe(value); + this._defaultChild!.observe(value); } /** @@ -1450,7 +1452,7 @@ export class Histogram extends LabeledCollector { * when the timer was started into the metric. */ startTimer() { - return this._defaultChild.startTimer(); + return this._defaultChild!.startTimer(); } remove(...labels: LabelValues) { @@ -1461,6 +1463,7 @@ export class Histogram extends LabeledCollector { } this._sumValues.delete(key); this._countValues.delete(key); + return key; } removeAll(labels: Record) { diff --git a/packages/lib/src/schema.ts b/packages/lib/src/schema.ts index d06fdfc2..d39416e9 100644 --- a/packages/lib/src/schema.ts +++ b/packages/lib/src/schema.ts @@ -17,7 +17,7 @@ const ajv = new Ajv({ * @returns Validator for the schema. */ export function compile(schema: JSONSchemaType) { - if (typeof global !== "object" || !global.lazySchemaCompilation) { + if (typeof global !== "object" || !(global as any).lazySchemaCompilation) { return ajv.compile(schema); } diff --git a/packages/lib/src/shared_commands.ts b/packages/lib/src/shared_commands.ts index 5635f1ad..06fc5f1c 100644 --- a/packages/lib/src/shared_commands.ts +++ b/packages/lib/src/shared_commands.ts @@ -49,7 +49,7 @@ export async function handlePluginCommand( pluginList: Map, pluginListPath: string ) { - let command = args._[1]; + let command = (args._ as string[])[1]; if (command === "add") { let pluginPath = args.path as string; @@ -61,7 +61,7 @@ export async function handlePluginCommand( try { // eslint-disable-next-line node/global-require pluginInfo = require(path.posix.join(pluginPath, "info")); - } catch (err) { + } catch (err: any) { logger.error(`Unable to import plugin info from ${args.path}:\n${err.stack}`); process.exitCode = 1; return; @@ -132,7 +132,7 @@ export async function handleConfigCommand( instance: libConfig.Config, configPath: string ) { - let command = args._[1]; + let command = (args._ as string[])[1]; if (command === "list") { for (let GroupClass of (instance.constructor as typeof libConfig.Config).groups.values()) { diff --git a/packages/lib/src/users.ts b/packages/lib/src/users.ts index 40d93b10..fd2469fc 100644 --- a/packages/lib/src/users.ts +++ b/packages/lib/src/users.ts @@ -310,10 +310,16 @@ export class User { _calculatePlayerStats() { let playerStats = new PlayerStats(); for (let instanceStats of this.instanceStats.values()) { - if (!playerStats.lastJoinAt || instanceStats.lastJoinAt > playerStats.lastJoinAt) { + if ( + instanceStats.lastJoinAt + && (!playerStats.lastJoinAt || instanceStats.lastJoinAt > playerStats.lastJoinAt) + ) { playerStats.lastJoinAt = instanceStats.lastJoinAt; } - if (!playerStats.lastLeaveAt || instanceStats.lastLeaveAt > playerStats.lastLeaveAt) { + if ( + instanceStats.lastLeaveAt + && (!playerStats.lastLeaveAt || instanceStats.lastLeaveAt > playerStats.lastLeaveAt) + ) { playerStats.lastLeaveAt = instanceStats.lastLeaveAt; playerStats.lastLeaveReason = instanceStats.lastLeaveReason; } diff --git a/packages/lib/src/zip_ops.ts b/packages/lib/src/zip_ops.ts index f69f63e8..492d2072 100644 --- a/packages/lib/src/zip_ops.ts +++ b/packages/lib/src/zip_ops.ts @@ -10,7 +10,7 @@ import type JSZip from "jszip"; * @returns name of the root folder. */ export function findRoot(zip: JSZip) { - let root = null; + let root: undefined | string; zip.forEach((relativePath, file) => { let index = relativePath.indexOf("/"); if (index === -1) { @@ -18,14 +18,14 @@ export function findRoot(zip: JSZip) { } let pathRoot = relativePath.slice(0, index); - if (root === null) { + if (root === undefined) { root = pathRoot; } else if (root !== pathRoot) { throw new Error("Zip contains multiple root folders"); } }); - if (root === null) { + if (root === undefined) { throw new Error("Empty zip file"); } diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 0f0b112f..da4d5a41 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -7,7 +7,7 @@ "rootDir": ".", "outDir": "dist", "composite": false, - "strict": false, + "strict": true, "alwaysStrict": true, "allowJs": true, "checkJs": false, diff --git a/plugins/research_sync/controller.js b/plugins/research_sync/controller.js index dc82ead3..20503480 100644 --- a/plugins/research_sync/controller.js +++ b/plugins/research_sync/controller.js @@ -44,7 +44,6 @@ class ControllerPlugin extends lib.BaseControllerPlugin { action: () => this.broadcastProgress(), }); - this.lastProgressBroadcast = Date.now(); this.progressBroadcastId = null; this.progressToBroadcast = new Set(); @@ -68,7 +67,6 @@ class ControllerPlugin extends lib.BaseControllerPlugin { } this.progressToBroadcast.clear(); - this.lastProgressBroadcast = Date.now(); if (techs.length) { this.controller.sendTo("allInstances", new ProgressEvent(techs)); } diff --git a/test/lib/link/link.js b/test/lib/link/link.js index 169c8c72..0c991031 100644 --- a/test/lib/link/link.js +++ b/test/lib/link/link.js @@ -80,7 +80,7 @@ describe("lib/link/link", function() { testConnector._seq - 1, src, dst, - new lib.ResponseError("Unrecognized request UnhandledRequest") + new lib.ResponseError("Unrecognized request UnhandledRequest", "InvalidMessage") ), ]); }); @@ -114,7 +114,10 @@ describe("lib/link/link", function() { testConnector._seq - 1, src, dst, - new lib.ResponseError("Source [Address controller:0] is not allowed for InvalidSrcRequest") + new lib.ResponseError( + "Source [Address controller:0] is not allowed for InvalidSrcRequest", + "InvalidMessage" + ) ), ]); }); @@ -132,7 +135,8 @@ describe("lib/link/link", function() { src, dst, new lib.ResponseError( - "Destination [Address control:1] is not allowed for InvalidDstRequest" + "Destination [Address control:1] is not allowed for InvalidDstRequest", + "InvalidMessage", ) ), ]); @@ -144,7 +148,7 @@ describe("lib/link/link", function() { testConnector._seq - 1, src, addr("allControls"), - new lib.ResponseError("Message src may not be broadcast") + new lib.ResponseError("Message src may not be broadcast", "InvalidMessage") ), ]); }); @@ -155,7 +159,10 @@ describe("lib/link/link", function() { testConnector._seq - 1, src, dst, - new lib.ResponseError("Destination [Address 4:control] is not allowed for SimpleRequest") + new lib.ResponseError( + "Destination [Address 4:control] is not allowed for SimpleRequest", + "InvalidMessage", + ) ), ]); }); From 954bc9b31450ee2f4a052806c38655bc63ad8401 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 12 Aug 2023 02:14:29 +0200 Subject: [PATCH 12/13] Use composite build Make TypeScript build faster for a multi package setup by allowing the compiller to quickly determine if a package is already built. --- packages/lib/package.json | 2 +- packages/lib/tsconfig.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/lib/package.json b/packages/lib/package.json index 7a56055a..8bc6d749 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -5,7 +5,7 @@ "repository": "https://github.com/clusterio/clusterio", "license": "MIT", "scripts": { - "prepare": "tsc && pnpm run compile_validator", + "prepare": "tsc --build && pnpm run compile_validator", "compile_validator": "node scripts/compile_validator.js" }, "main": "dist/index.js", diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index da4d5a41..c43cfa07 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -6,12 +6,11 @@ "target": "es2022", "rootDir": ".", "outDir": "dist", - "composite": false, + "composite": true, "strict": true, "alwaysStrict": true, "allowJs": true, "checkJs": false, - "noEmitOnError": false, "esModuleInterop": true, "skipLibCheck": true } From f875843c5c7a00bb18fde76b87133c5455387e3a Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 12 Aug 2023 14:34:36 +0200 Subject: [PATCH 13/13] Turn on source maps for .ts files Fix coverage checking not being able to deduce the original TypeScript files due to lacking a way to link the compiled .js output files with the .ts input files. --- packages/lib/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index c43cfa07..f4ae38e6 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -7,6 +7,7 @@ "rootDir": ".", "outDir": "dist", "composite": true, + "sourceMap": true, "strict": true, "alwaysStrict": true, "allowJs": true,