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/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/.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 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..8bc6d749 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": "tsc --build && pnpm run compile_validator", "compile_validator": "node scripts/compile_validator.js" }, + "main": "dist/index.js", "browser": "browser.js", "keywords": [ "factorio" @@ -16,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", @@ -31,5 +33,12 @@ }, "publishConfig": { "access": "public" + }, + "devDependencies": { + "@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/scripts/compile_validator.js b/packages/lib/scripts/compile_validator.js index bc90dcf4..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"), - `// generated by scripts/compile_validator.js\n/* eslint-disable */\n${moduleCode}` + 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/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 66% rename from packages/lib/src/PlayerStats.js rename to packages/lib/src/PlayerStats.ts index 1c7c873b..f3f96691 100644 --- a/packages/lib/src/PlayerStats.js +++ b/packages/lib/src/PlayerStats.ts @@ -1,52 +1,43 @@ -"use strict"; +import { Type, Static } from "@sinclair/typebox"; /** * 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", - 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"]; } @@ -62,12 +53,12 @@ class PlayerStats { } } - static fromJSON(json) { + static fromJSON(json: Static) { return new this(json); } toJSON() { - let json = {}; + let json: Static = {}; if (this.joinCount) { json["join_count"] = this.joinCount; } @@ -84,5 +75,3 @@ class PlayerStats { return json; } } - -module.exports = PlayerStats; diff --git a/packages/lib/src/RateLimiter.js b/packages/lib/src/RateLimiter.ts similarity index 70% rename from packages/lib/src/RateLimiter.js rename to packages/lib/src/RateLimiter.ts index 7a82d0c1..24390c6b 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,35 @@ * // 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; + + private _lastRun = 0; + private _runTimeout?: NodeJS.Timeout; + /** * 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(); + this.action = options.action || undefined; } /** @@ -59,16 +56,18 @@ 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(); 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; @@ -77,11 +76,11 @@ 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; @@ -100,5 +99,3 @@ class RateLimiter { } } } - -module.exports = RateLimiter; diff --git a/packages/lib/src/command.js b/packages/lib/src/command.ts similarity index 68% rename from packages/lib/src/command.js rename to packages/lib/src/command.ts index 972f6969..d2173ed0 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 | undefined; 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 | undefined; 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 | undefined; 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 | undefined; 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/config/classes.js b/packages/lib/src/config/classes.ts similarity index 62% rename from packages/lib/src/config/classes.js rename to packages/lib/src/config/classes.ts index 1855b584..b1e956d1 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; @@ -492,7 +521,7 @@ 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}`); } } @@ -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 Record; 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) { - let fields = {}; + serialize(location: ConfigLocation = this.config.location) { + let fields: Record = {}; 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`); } @@ -711,82 +753,83 @@ 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`); } - let computed = { + let computed: FieldDefinition = { access: this.defaultAccess, optional: false, fullName: `${this.groupName}.${item.name}`, @@ -829,10 +872,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 +888,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/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/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 69% rename from packages/lib/src/data/ModInfo.js rename to packages/lib/src/data/ModInfo.ts index 8ad802d1..f642a9f7 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; } @@ -197,13 +174,13 @@ class ModInfo { return json; } - static async fromModFile(modPath) { - let modInfo; + 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) { @@ -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 65% rename from packages/lib/src/data/ModPack.js rename to packages/lib/src/data/ModPack.ts index de4335f8..b3194ec4 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,21 +192,21 @@ 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 buf = zlib.inflateSync(buf); - } catch (err) { + } catch (err: any) { if (err.code.startsWith("Z_")) { throw new Error("Malformed mod pack string: zlib inflate failed"); } } - let json; + let json: unknown; try { - json = JSON.parse(buf); - } catch (err) { + json = JSON.parse(buf.toString()); + } catch (err: any) { 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 | undefined, 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); @@ -315,7 +295,7 @@ 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 { @@ -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)) @@ -352,21 +332,20 @@ 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 }); } } } - -module.exports = ModPack; diff --git a/packages/lib/src/data/composites.js b/packages/lib/src/data/composites.js deleted file mode 100644 index de62d1c6..00000000 --- a/packages/lib/src/data/composites.js +++ /dev/null @@ -1,32 +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; - }, - }; -} - -module.exports = { - JsonBoolean: jsonPrimitive("boolean"), - JsonNumber: jsonPrimitive("number"), - JsonString: jsonPrimitive("string"), - - 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.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..fdd40377 --- /dev/null +++ b/packages/lib/src/data/message_validate.d.ts @@ -0,0 +1,4 @@ +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..ba885145 --- /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" 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.controller.update_config" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.controller.update_config" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.host.generate_token" as const; + + 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" 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, + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.log.follow" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.log.query" as const; + + 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" as const; + static src = ["host", "controller"] as const; + static dst = ["controller", "control"] as const; + + 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" 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" as const; + static src = "controller" as const; + static dst = "control" as const; + + 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 beda4502..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 {string} */ - id; - - /** @type {string|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..207fc01a --- /dev/null +++ b/packages/lib/src/data/messages_core.ts @@ -0,0 +1,630 @@ +// 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 | undefined; + 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 { + 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.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..c271a3b9 --- /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" 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.host.subscribe" as const; + + 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" as const; + static src = "controller" as const; + static dst = "control" as const; + + 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" 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[], + ) { } + + 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" as const; + static src = "host" as const; + static dst = "instance" as const; + + 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" as const; + static src = "host" as const; + static dst = "instance" as const; +} + +export class SyncUserListsEvent { + declare ["constructor"]: typeof SyncUserListsEvent; + static type = "event" as const; + static src = "controller" as const; + static dst = "host" as const; + + 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 311bf0cf..00000000 --- a/packages/lib/src/data/messages_instance.js +++ /dev/null @@ -1,1335 +0,0 @@ -"use strict"; -const PlayerStats = require("../PlayerStats"); -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..9fc2741a --- /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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.get" as const; + + 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" 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.subscribe" as const; + + 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" as const; + static src = "controller" as const; + static dst = "control" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.create" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.get_config" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.update_config" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.update_config" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.assign" as const; + + 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" as const; + static src = "host" as const; + static dst = "instance" as const; + + 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" as const; + static src = ["control", "controller"] as const; + static dst = "instance" as const; + static permission = "core.instance.start" as const; + + 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" 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" 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, + 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" as const; + static src = ["instance", "host", "controller"] as const; + static dst = ["controller", "control"] as const; + + 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" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.save.create" as const; + + 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" 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, + 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" 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, + 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" 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, + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.save.download" as const; + + 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" 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) { + 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" as const; + static src = "controller" as const; + static dst = "host" as const; + + 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" as const; + static src = "controller" as const; + static dst = "host" as const; + + 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" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.load_scenario" as const; + + 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" 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" 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" 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" 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.delete" as const; + + 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" as const; + static src = "controller" as const; + static dst = "host" as const; + + 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" as const; + static src = "control" as const; + static dst = "instance" as const; + static permission = "core.instance.send_rcon" as const; + + 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" as const; + static src = "host" as const; + static dst = "controller" as const; + + 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" as const; + static src = "controller" as const; + static dst = "host" as const; + + 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" as const; + static src = "controller" as const; + static dst = "host" as const; + + 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" as const; + static src = "instance" as const; + static dst = "host" as const; + + 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" as const; + static src = ["instance", "host"] as const; + static dst = "controller" as const; + + 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" as const; + static src = "instance" as const; + static dst = "controller" as const; + + 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" as const; + static src = "controller" as const; + static dst = "instance" as const; + + 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" as const; + static src = "controller" as const; + static dst = "instance" as const; + + 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" as const; + static src = "controller" as const; + static dst = "instance" as const; + + 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" as const; + static src = "instance" as const; + static dst = "controller" as const; + + 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..0fadcfda --- /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" 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, + ) { } + + 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" 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" 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod_pack.create" as const; + + constructor( + public modPack: ModPack, + ) { } + + static jsonSchema = Type.Object({ + "modPack": ModPack.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(ModPack.fromJSON(json.modPack)); + } +} + +export class ModPackUpdateRequest { + declare ["constructor"]: typeof ModPackUpdateRequest; + 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, + ) { } + + static jsonSchema = Type.Object({ + "modPack": ModPack.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(ModPack.fromJSON(json.modPack)); + } +} + +export class ModPackDeleteRequest { + declare ["constructor"]: typeof ModPackDeleteRequest; + 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, + ) { } + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod_pack.subscribe" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.get" as const; + + 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" 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.search" as const; + + 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(), + "page": Type.Integer(), + "pageSize": 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(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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.subscribe" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.download" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.mod.delete" as const; + + 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" as const; + static src = "controller" as const; + static dst = "control" as const; + + constructor( + public modPack: ModPack, + ) { } + + static jsonSchema = Type.Object({ + "modPack": ModPack.jsonSchema, + }); + + static fromJSON(json: Static) { + return new this(ModPack.fromJSON(json.modPack)); + } +} + +export class ModUpdateEvent { + declare ["constructor"]: typeof ModUpdateEvent; + static type = "event" as const; + static src = "controller" as const; + static dst = "control" as const; + + constructor( + public mod: ModInfo, + ) { } + + static jsonSchema = Type.Object({ + "mod": 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 47bc1ff3..00000000 --- a/packages/lib/src/data/messages_user.js +++ /dev/null @@ -1,708 +0,0 @@ -"use strict"; -const PlayerStats = require("../PlayerStats"); -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..a975476b --- /dev/null +++ b/packages/lib/src/data/messages_user.ts @@ -0,0 +1,474 @@ +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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.permission.list" as const; + 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" 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.create" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.update" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.update" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.delete" as const; + + 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(PlayerStats.jsonSchema), + instanceStats: Type.Optional( + Type.Array( + Type.Tuple([Type.Integer(), PlayerStats.jsonSchema]), + ) + ), + }); + + static fromJSON(json: Static) { + let playerStats: PlayerStats | undefined; + if (json.playerStats) { + playerStats = PlayerStats.fromJSON(json.playerStats); + } + let instanceStats: Map | undefined; + 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, + }; + if (this.playerStats) { + json.playerStats = this.playerStats.toJSON() + } + if (this.instanceStats) { + json.instanceStats = [...this.instanceStats].map(([k, v]) => [k, v.toJSON()]); + } + return json; + } +} + +export class UserGetRequest { + declare ["constructor"]: typeof UserGetRequest; + 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, + ) { } + + 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" 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.subscribe" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.create" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.revoke_token" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.update_roles" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.set_admin" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.set_whitelisted" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.set_banned" as const; + + 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" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.user.delete" as const; + + 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" as const; + static src = "controller" as const; + static dst = "control" as const; + + 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" as const; + static src = "controller" as const; + static dst = "control" as const; + + 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/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+$/; diff --git a/packages/lib/src/database.js b/packages/lib/src/database.ts similarity index 73% rename from packages/lib/src/database.js rename to packages/lib/src/database.ts index 4c2b8520..3b388ecf 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)}`); @@ -55,7 +54,7 @@ async function loadJsonAsMap(filePath) { return new Map(Object.entries(parsed)); - } catch (err) { + } catch (err: any) { if (err.code === "ENOENT") { // File does not exist, return empty map return new Map(); @@ -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)}`); @@ -116,7 +115,7 @@ async function loadJsonArrayAsMap(filePath) { } return map; - } catch (err) { + } catch (err: any) { if (err.code === "ENOENT") { // File does not exist, return empty map return new Map(); @@ -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,15 +239,15 @@ 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); if (this._items.has(name)) { - count += this._items.get(name); + count += this._items.get(name)!; } this._items.set(name, count); @@ -262,27 +260,18 @@ 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); count = -count; if (this._items.has(name)) { - count += this._items.get(name); + count += this._items.get(name)!; } 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..cb157c50 --- /dev/null +++ b/packages/lib/src/errors.ts @@ -0,0 +1,96 @@ +/** + * Errors thrown in Clusterio + * @module lib/errors + */ + +/** + * Thrown by commands when they fail + */ +export class CommandError extends Error { + code = "CommandError"; +} + +/** + * Thrown from requests sent when an error occured handling it + */ +export class RequestError extends Error { + constructor( + message: string, + public code = "RequestError", + 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 { + 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, + ) { + super(message); + } +} + +/** + * Thrown from requests when the session it was sent on was lost + */ +export class SessionLost extends Error { + code = "SessionLost"; +} + +/** + * Thrown when WebSocket authentication failed + */ +export class AuthenticationFailed extends Error { + code = "AuthenticationFailed"; +} + +/** + * Errror class for known errors occuring during startup + */ +export class StartupError extends Error { + code = "StartupError"; +} + +/** + * Errors outside of our control + */ +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 + ) { + super(`PluginError: ${original.message}`); + } +} 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 a4373356..13a4ef6a 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} module:lib.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 {module:lib.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}<<} + * @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,20 +39,20 @@ 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) { + } catch (err: any) { if (err.code === "ENOENT") { return 0; } 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); } @@ -93,7 +92,7 @@ async function findUnusedName(directory, name, extension = "") { return `${name}${extension}`; } - let match = /^(.*?)(-(\d+))?$/.exec(name); + let match = /^(.*?)(-(\d+))?$/.exec(name)!; if (!match[2]) { name = `${match[1]}-2`; } else { @@ -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/helpers.js b/packages/lib/src/helpers.ts similarity index 67% rename from packages/lib/src/helpers.js rename to packages/lib/src/helpers.ts index 5104f74a..31afdabc 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: NodeJS.Timer | undefined; 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 | undefined] { // attribute = identifier, ':', word // term = word | attribute - let term; + let term: ParsedTerm | undefined; 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} module:lib~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 - - * 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} module:lib~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,24 +260,26 @@ 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 {module:lib~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(); let parsed = { - terms: [], - issues: [], + terms: [] as ParsedTerm[], + issues: [] as string[], }; let pos = 0; while (pos < input.length) { - let term; + let term: ParsedTerm | undefined; ([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, -}; 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/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.js b/packages/lib/src/link/connectors.ts similarity index 73% rename from packages/lib/src/link/connectors.js rename to packages/lib/src/link/connectors.ts index 1afad7e2..b52d2103 100644 --- a/packages/lib/src/link/connectors.js +++ b/packages/lib/src/link/connectors.ts @@ -1,28 +1,29 @@ // Connection adapters for Links "use strict"; -const assert = require("assert").strict; -const events = require("events"); -// eslint-disable-next-line node/no-process-env -const WebSocket = process.env.APP_ENV === "browser" ? window.WebSocket : require("ws"); +import { strict as assert } from "assert"; +import events from "events"; +import WebSocket from "./WebSocket"; -const libData = require("../data"); -const libErrors = require("../errors"); -const { logger } = require("../logging"); -const ExponentialBackoff = require("../ExponentialBackoff"); +import * as libData from "../data"; +import * as libErrors from "../errors"; +import { logger } from "../logging"; +import ExponentialBackoff from "../ExponentialBackoff"; +import type { Request, Event } from "./link"; /** * Base connector for links * * @extends events.EventEmitter - * @memberof module:lib */ -class BaseConnector extends events.EventEmitter { - constructor(src, dst) { +export abstract class BaseConnector extends events.EventEmitter { + protected _seq = 1; + + constructor( + public src: libData.Address, + public dst: libData.Address, + ) { super(); - this._seq = 1; - this.src = src; - this.dst = dst; } _reset() { @@ -33,7 +34,13 @@ class BaseConnector extends events.EventEmitter { this._seq = 1; } - sendRequest(request, requestId, dst) { + abstract send(message: libData.Message): void; + + 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); @@ -45,7 +52,7 @@ class BaseConnector extends events.EventEmitter { return seq; } - sendResponse(response, dst, src = this.src) { + sendResponse(response: unknown, dst: libData.Address, src = this.src) { let seq = this._seq; this._seq += 1; const message = new libData.MessageResponse(seq, src, dst, response); @@ -53,7 +60,7 @@ class BaseConnector extends events.EventEmitter { return seq; } - sendResponseError(error, dst, src = this.src) { + sendResponseError(error: libData.ResponseError, dst: libData.Address, src = this.src) { let seq = this._seq; this._seq += 1; const message = new libData.MessageResponseError(seq, src, dst, error); @@ -61,7 +68,7 @@ class BaseConnector extends events.EventEmitter { return seq; } - sendEvent(event, dst) { + sendEvent(event: Event, dst: libData.Address) { let seq = this._seq; this._seq += 1; let Event = event.constructor; @@ -73,25 +80,32 @@ 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 * * @extends module:lib.BaseConnector - * @memberof module:lib */ -class WebSocketBaseConnector extends BaseConnector { - constructor(src, dst) { - super(src, dst); +export abstract class WebSocketBaseConnector extends BaseConnector { + // One of closed, connecting (client only), connected and resuming. + _state: ConnectorState = "closed"; + _closing = false; + _socket: WebSocket | null = null; + _lastHeartbeat: number | null = null; + _heartbeatId: NodeJS.Timer | null = null; + _heartbeatInterval: number | null = null; + _lastReceivedSeq = undefined; + _sendBuffer: (ReliableMessages)[] = []; - // One of closed, connecting (client only), connected and resuming. - this._state = "closed"; - this._closing = false; - this._socket = null; - this._lastHeartbeat = null; - this._heartbeatId = null; - this._heartbeatInterval = null; - this._lastReceivedSeq = undefined; - this._sendBuffer = []; + constructor(src: libData.Address, dst: libData.Address) { + super(src, dst); } _reset() { @@ -118,7 +132,7 @@ class WebSocketBaseConnector extends BaseConnector { super._invalidate(); } - _check(...expectedStates) { + _check(...expectedStates: ConnectorState[]) { if (!expectedStates.includes(this._state)) { throw new Error( `Expected state ${expectedStates} but state is ${this._state}` @@ -126,7 +140,7 @@ class WebSocketBaseConnector extends BaseConnector { } } - _dropSendBufferSeq(seq) { + _dropSendBufferSeq(seq?: number) { if (seq === undefined) { return; } @@ -142,8 +156,8 @@ class WebSocketBaseConnector extends BaseConnector { this._sendBuffer.splice(0, dropCount); } - _parseMessage(text) { - let json; + _parseMessage(text: string) { + let json: object; try { json = JSON.parse(text); } catch (err) { @@ -159,22 +173,24 @@ class WebSocketBaseConnector extends BaseConnector { return libData.Message.fromJSON(json); } - _processMessage(message) { + _processMessage(message: libData.Message) { + // TODO check if this can be inferred from Message type if (message.type === "heartbeat") { - this._processHeartbeat(message); + this._processHeartbeat(message as libData.MessageHeartbeat); } else if (message.type === "disconnect") { - if (message.data === "ready") { + const data = (message as libData.MessageDisconnect).data; + if (data === "ready") { this.emit("disconnectReady"); - } else if (message.data === "prepare") { + } else if (data === "prepare") { this.setClosing(); this.emit("disconnectPrepare"); } } else { - if (message.seq !== undefined) { - this._lastReceivedSeq = message.seq; + if ((message as any).seq !== undefined) { + this._lastReceivedSeq = (message as any).seq; } this.emit("message", message); @@ -183,13 +199,13 @@ 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 { @@ -197,7 +213,7 @@ class WebSocketBaseConnector extends BaseConnector { } } - _processHeartbeat(message) { + _processHeartbeat(message: libData.MessageHeartbeat) { this._lastHeartbeat = Date.now(); this._dropSendBufferSeq(message.seq); } @@ -212,7 +228,7 @@ class WebSocketBaseConnector extends BaseConnector { this._lastHeartbeat = Date.now(); this._heartbeatId = setInterval(() => { this._doHeartbeat(); - }, this._heartbeatInterval * 1000); + }, this._heartbeatInterval! * 1000); } /** @@ -231,9 +247,9 @@ class WebSocketBaseConnector extends BaseConnector { * This is a low level method that should only be used for implementing * links. See sendTo for sending requests and events. * - * @param {module:lib.Message} message - Message to send. + * @param message - Message to send. */ - send(message) { + send(message: ReliableMessages) { if (!["connected", "resuming"].includes(this._state)) { throw new libErrors.SessionLost("No session"); } @@ -244,8 +260,8 @@ class WebSocketBaseConnector extends BaseConnector { } } - _sendInternal(message) { - this._socket.send(JSON.stringify(message)); + _sendInternal(message: libData.Message) { + this._socket!.send(JSON.stringify(message)); } /** @@ -254,9 +270,8 @@ class WebSocketBaseConnector extends BaseConnector { * Signal the connection is about to close and that once the close * frame is received it should be considered finished. */ - setClosing() { - throw new Error("Abstract function"); - } + abstract setClosing(): void; + abstract close(code: number, reason: string): Promise; /** * Gracefully disconnect @@ -270,9 +285,9 @@ class WebSocketBaseConnector extends BaseConnector { } this.setClosing(); - this.send(new libData.MessageDisconnect("prepare")); + this._sendInternal(new libData.MessageDisconnect("prepare")); - let timeout; + let timeout: NodeJS.Timeout | undefined; let waitTimeout = new Promise(resolve => { timeout = setTimeout(resolve, 10000); }); // Make Configurable? try { await Promise.race([ @@ -314,44 +329,46 @@ 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; + _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 as any, 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() { 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; } /** @@ -360,9 +377,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,17 +389,17 @@ 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; } 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 { @@ -409,7 +426,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { } if (this._socket) { - this._socket.close(1000, "Connector closing"); + this._socket!.close(1000, "Connector closing"); } else { this._reset(); @@ -443,7 +460,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); } @@ -459,18 +476,18 @@ 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(); - if (this._disconnecting) { + if (this._closing) { this._reset(); this.emit("close"); } else { @@ -482,14 +499,13 @@ 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); - this._reconnectTime = Date.now() + delay; } _attachSocketHandlers() { - this._socket.onclose = event => { + this._socket!.onclose = (event) => { const previousState = this._state; // Authentication failed @@ -532,7 +548,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { } }; - this._socket.onerror = event => { + 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}`; @@ -556,13 +572,13 @@ class WebSocketClientConnector extends WebSocketBaseConnector { } }; - this._socket.onopen = () => { + this._socket!.onopen = () => { logger.verbose("Connector | Open"); }; // Handle messages - this._socket.onmessage = event => { - let message = this._parseMessage(event.data); + this._socket!.onmessage = (event) => { + let message = this._parseMessage(event.data as string); if (!message) { return; } @@ -578,9 +594,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 +637,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 +654,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 +677,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 +695,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.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..e13d1d8b --- /dev/null +++ b/packages/lib/src/link/index.ts @@ -0,0 +1,7 @@ +/** + * Library for message based communication between nodes + * @module lib/link + * @author Hornwitser + */ +export * from "./link" +export * from "./connectors" diff --git a/packages/lib/src/link/link.js b/packages/lib/src/link/link.ts similarity index 57% rename from packages/lib/src/link/link.js rename to packages/lib/src/link/link.ts index 758c524c..e4352b3a 100644 --- a/packages/lib/src/link/link.js +++ b/packages/lib/src/link/link.ts @@ -1,11 +1,78 @@ // 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"; +import type { User } from "../users"; +import type { AddressType, JSONDeserialisable, Message, MessageRequest, MessageEvent } from "../data"; -const libData = require("../data"); -const libErrors = require("../errors"); -const { logger } = require("../logging"); -const libSchema = require("../schema"); -const { dataClasses } = require("./messages"); +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: RequestClass; + name: string, + allowedSrcTypes: Set; + allowedDstTypes: Set; + requestFromJSON: (json: any) => 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: EventClass; + name: string, + allowedSrcTypes: Set; + allowedDstTypes: Set; + 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, +} // Some definitions for the terminology used here: // link: Either side of a controller - client connection @@ -16,23 +83,22 @@ 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?: 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 + ) { this.handle(libData.PingRequest, () => {}); // Process messages received by the connector @@ -74,11 +140,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 +162,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 +184,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,16 +201,23 @@ 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); + 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(message, entry); + this._processEvent( + message as libData.MessageEvent, + entry as EventEntry + ); } } @@ -149,18 +226,20 @@ 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"); } this.validateIngress(message); - } catch (err) { + } catch (err: any) { if (message.type === "request") { this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src); } @@ -168,7 +247,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 +273,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 +303,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 +335,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,10 +356,10 @@ class Link { return; } - let response; + 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)); } @@ -280,7 +374,7 @@ class Link { } response.then( - result => { + (result: any) => { if (this.validateSent) { if (entry.Response) { entry.responseFromJSON(JSON.parse(JSON.stringify(result))); @@ -291,7 +385,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) { @@ -300,15 +394,19 @@ 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: any) { + logger.error(`Unexpected error sending error response for ${message.name}:\n${err.stack}`); + } } ); } - _processResponse(message) { - let pending = this._pendingRequests.get(message.dst.requestId); + _processResponse(message: libData.MessageResponse) { + let pending = this._pendingRequests.get(message.dst.requestId!); if (!pending) { throw new libErrors.InvalidMessage( `Received response ${message.dst.requestId} without a pending request` @@ -321,14 +419,14 @@ 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) { - let pending = this._pendingRequests.get(message.dst.requestId); + _processResponseError(message: libData.MessageResponseError) { + let pending = this._pendingRequests.get(message.dst.requestId!); if (!pending) { throw new libErrors.InvalidMessage( `Received error response ${message.dst.requestId} without a pending request` @@ -338,13 +436,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 +468,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) { - 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); } /** * 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: Request): Promise; + sendTo(destination: libData.AddressShorthand, requestOrEvent: Event): void; + sendTo( + destination: libData.AddressShorthand, + requestOrEvent: Request | Event, + ): Promise | void { let dst = libData.Address.fromShorthand(destination); + // TODO investigate if these types can be inferred if (requestOrEvent.constructor.type === "request") { - return this.sendRequest(requestOrEvent, dst); + return this.sendRequest(requestOrEvent as Request, dst); } if (requestOrEvent.constructor.type === "event") { - return this.sendEvent(requestOrEvent, dst); + return this.sendEvent(requestOrEvent as Event, 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: 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}`); } @@ -407,30 +515,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: resolve!, reject: reject!, dst, } + ); this.connector.sendRequest(request, requestId, dst); - return pending.promise; + return promise; } - 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); - } - + forwardRequest(message: libData.MessageRequest, origin: Link) { let pending = { origin, src: message.src, @@ -440,8 +540,8 @@ class Link { this.connector.send(message); } - sendEvent(event, dst) { - let entry = this.constructor._eventsByClass.get(event.constructor); + 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}`); } @@ -452,18 +552,23 @@ class Link { this.connector.sendEvent(event, dst); } - handle(Class, handler) { + handle(Class: RequestClass, handler: RequestHandler): void; + handle(Class: EventClass, handler: EventHandler): void; + handle( + 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.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: RequestClass, handler: RequestHandler) { + let entry = Link._requestsByClass.get(Request as RequestClass); if (!entry) { throw new Error(`Unregistered Request class ${Request.name}`); } @@ -473,8 +578,8 @@ class Link { this._requestHandlers.set(Request, handler); } - fallbackRequest(Request, handler) { - let entry = this.constructor._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}`); } @@ -484,8 +589,8 @@ class Link { this._requestFallbacks.set(Request, handler); } - handleEvent(Event, handler) { - let entry = this.constructor._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}`); } @@ -495,8 +600,8 @@ class Link { this._eventHandlers.set(Event, handler); } - snoopEvent(Event, handler) { - let entry = this.constructor._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}`); } @@ -506,27 +611,29 @@ class Link { this._eventSnoopers.set(Event, handler); } - static register(Class) { + 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") { 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: 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`); } - 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); } - return Request.fromJSON(json); + return Request.fromJSON!(json); }; } if (Request.jsonSchema) { @@ -535,7 +642,7 @@ class Link { return () => new Request(); } - static responseFromJSON(Response, name) { + static responseFromJSON(Response: JSONDeserialisable, name: string) { if (!Response.jsonSchema) { throw new Error(`Response for Request ${name} is missing static jsonSchema`); } @@ -543,8 +650,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 @@ -554,7 +661,11 @@ class Link { }; } - static allowedTypes(types, name, side) { + static allowedTypes( + types: libData.AddressType | readonly libData.AddressType[], + name: string, + side: "src" | "dst" + ) { if (types === undefined) { throw new Error(`Missing ${side} specification in ${name}`); } @@ -563,7 +674,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") { @@ -577,21 +688,22 @@ class Link { return allowed; } - static _requestsByName = new Map(); - static _requestsByClass = new Map(); + static _requestsByName = new Map(); + static _requestsByClass = new Map, RequestEntry>(); - static registerRequest(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`); } - 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 ( @@ -605,44 +717,42 @@ 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: EventClass, 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); } - return Event.fromJSON(json); + return Event.fromJSON!(json); }; } 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, EventEntry>(); - static registerEvent(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"), @@ -657,15 +767,15 @@ class Link { } this._eventsByName.set(name, entry); - this._eventsByClass.set(Event, entry); + this._eventsByClass.set(Event as EventClass, entry); } } for (let Class of dataClasses) { - Link.register(Class); + Link.register(Class as RequestClass); } -function registerPluginMessages(pluginInfos) { +export function registerPluginMessages(pluginInfos: PluginInfo[]) { for (let pluginInfo of pluginInfos) { for (let Class of pluginInfo.messages || []) { if (Class.plugin !== pluginInfo.name) { @@ -678,9 +788,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 83% rename from packages/lib/src/link/messages.js rename to packages/lib/src/link/messages.ts index 6a6acf5c..b6161e10 100644 --- a/packages/lib/src/link/messages.js +++ b/packages/lib/src/link/messages.ts @@ -1,15 +1,19 @@ "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"; +import type { RequestClass, EventClass } from "./link"; -// 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: (RequestClass | EventClass)[] = [ core.PingRequest, controller.ControllerConfigGetRequest, @@ -107,5 +111,3 @@ const dataClasses = [ user.AccountUpdateEvent, user.UserUpdateEvent, ]; - -module.exports = { dataClasses }; diff --git a/packages/lib/src/logging.js b/packages/lib/src/logging.ts similarity index 51% rename from packages/lib/src/logging.js rename to packages/lib/src/logging.ts index 98aeb928..8eac6098 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,33 +94,43 @@ class WebConsoleFormat { /** * Filter object for logs - * @typedef {Object} module:lib~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 {module:lib~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])) { + if (maxLevel && !((levels as any)[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 56% rename from packages/lib/src/logging_utils.js rename to packages/lib/src/logging_utils.ts index c45a961d..f55b0ddd 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, @@ -168,7 +256,12 @@ class LogIndex { serialize() { let serialized = { version: logIndexVersion, - files: {}, + files: {} as Record, }; for (let [file, entry] of this.index) { @@ -190,14 +283,14 @@ 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)); - } catch (err) { + return new LogIndex(logDirectory, JSON.parse(content.toString())); + } catch (err: any) { if (err.code !== "ENOENT") { logger.warn(`Failed to load ${path.join(logDirectory, "index.json")}: ${err}`); } @@ -218,19 +311,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,15 +366,15 @@ 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 {module:lib~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 }) { - let entry = this.index.get(file); + filterIncludesFile(file: string, { maxLevel, all, controller, instanceIds, hostIds }: LogFilter) { + const entry = this.index.get(file); if (!entry) { return true; } @@ -314,35 +407,51 @@ class LogIndex { /** * Filter object for querying logs - * @typedef {Object} module:lib~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 {module:lib~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 = { @@ -357,14 +466,14 @@ async function queryLog(logDirectory, filter, index) { } const includeEntry = logFilter(filter); - let log = []; + let log: object[] = []; for (let file of files) { if (index && !index.filterIncludesFile(file, filter)) { 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 +482,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) { @@ -381,7 +490,7 @@ async function queryLog(logDirectory, filter, index) { } if (includeEntry(info)) { log.push(info); - if (log.length >= filter.limit) { + if (log.length >= Number(filter.limit)) { lineStream.destroy(); fileStream.destroy(); } @@ -390,40 +499,31 @@ async function queryLog(logDirectory, filter, index) { 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; } } 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 */ } - -module.exports = { - TerminalFormat, - LinkTransport, - LogIndex, - handleUnhandledErrors, - queryLog, - - // for testing only - _formatServerOutput: 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 59% rename from packages/lib/src/plugin.js rename to packages/lib/src/plugin.ts index 48e22cbb..de231701 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"; - -const helpers = require("./helpers"); +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"; /** * Conceptual base for controller and instance plugins. - * @typedef { - * module:lib.BaseControllerPlugin - * |module:lib.BaseInstancePlugin - * |module:lib.BaseControlPlugin - * } module:lib.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) { @@ -271,11 +297,11 @@ 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); } } @@ -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} module:lib~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} module:lib~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,28 +799,28 @@ 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( - plugin[hook](...args), + let result = await libHelpers.timeout( + (plugin as any)[hook](...args), 15000, timeout ); @@ -800,19 +829,9 @@ async function invokeHook(plugins, hook, ...args) { } 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}`); } } 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..e557bea9 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,22 +16,21 @@ 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")); pluginPackage = require(path.posix.join(pluginPath, "package.json")); - } catch (err) { + } catch (err: any) { throw new libErrors.PluginError(pluginName, err); } @@ -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 74% rename from packages/lib/src/prometheus.js rename to packages/lib/src/prometheus.ts index 7a69f391..5ce2d685 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: any) => (void | Promise)) | undefined; + metric: Metric; + + private _children: Map; + private _childClass: { new(collector: any, 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: any) => (void | Promise), + }, + childClass: { new(collector: any, 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: any) => (void | Promise), + }, + childClass: { new(collector: ValueCollector, key: string): Child } + ) { super(type, name, help, options, childClass); this._values = new Map(); @@ -509,18 +504,19 @@ 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(); + return this._defaultChild!.get(); } - remove(...labels) { + remove(...labels: LabelValues) { let key = super.remove(...labels); this._values.delete(key); + return key; } - removeAll(labels) { + removeAll(labels: Record) { super.removeAll(labels); removeMatchingLabels(this._values, labels); } @@ -535,7 +531,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,22 +544,21 @@ 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); + return this._values.get(this._key)!; } } /** * 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 @@ -568,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); } } @@ -583,7 +581,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 +594,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 +604,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 +636,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 +649,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 +668,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: Collector) => (void | Promise), + } = {}, + ) { super("counter", name, help, options, CounterChild); } @@ -680,42 +684,42 @@ 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); + this._defaultChild!.inc(value); } } /** * 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); + this._values.set(this._key, this._values.get(this._key)! + value); } /** * 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); + this._values.set(this._key, this._values.get(this._key)! - value); } /** * 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 +733,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 +757,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 +774,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 +808,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 +834,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 +845,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,30 +880,30 @@ 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); + this._defaultChild!.inc(value); } /** * 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); + this._defaultChild!.dec(value); } /** * 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) { - this._defaultChild.set(value); + set(value: number) { + this._defaultChild!.set(value); } /** @@ -902,19 +912,19 @@ class Gauge extends ValueCollector { * Note: Only works if this is an unlabeled collector. */ setToCurrentTime() { - this._defaultChild.setToCurrentTime(); + this._defaultChild!.setToCurrentTime(); } /** * 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 */ startTimer() { - return this._defaultChild.startTimer(); + return this._defaultChild!.startTimer(); } } @@ -922,7 +932,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 +947,6 @@ class SummaryChild { /** * Sum of all observations for label set - * @type {number} */ get sum() { return this._sumValues.get(this._key); @@ -941,7 +954,6 @@ class SummaryChild { /** * Count of observations for label set - * @type {number} */ get count() { return this._countValues.get(this._key); @@ -950,17 +962,17 @@ 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) { - this._sumValues.set(this._key, this._sumValues.get(this._key) + value); - this._countValues.set(this._key, this._countValues.get(this._key) + 1); + 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); } /** * 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 +993,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 +1022,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 +1046,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 +1056,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(); @@ -1090,7 +1112,7 @@ class Summary extends LabeledCollector { * @type {number} */ get sum() { - return this._defaultChild.sum; + return this._defaultChild!.sum; } /** @@ -1100,38 +1122,39 @@ class Summary extends LabeledCollector { * @type {number} */ get count() { - return this._defaultChild.count; + return this._defaultChild!.count; } /** * 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) { - this._defaultChild.observe(value); + observe(value: number) { + this._defaultChild!.observe(value); } /** * 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. */ startTimer() { - return this._defaultChild.startTimer(); + return this._defaultChild!.startTimer(); } - remove(...labels) { + remove(...labels: LabelValues) { let key = super.remove(...labels); this._sumValues.delete(key); this._countValues.delete(key); + return key; } - removeAll(labels) { + removeAll(labels: Record) { super.removeAll(labels); removeMatchingLabels(this._sumValues, labels); removeMatchingLabels(this._countValues, labels); @@ -1144,7 +1167,7 @@ class Summary extends LabeledCollector { } } -function formatValue(value) { +function formatValue(value: number) { if (value === Infinity) { return "+Inf"; } @@ -1154,7 +1177,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 +1185,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 +1211,6 @@ class HistogramChild { /** * Mapping of bucket upper bounds to count of observations for label set - * @type {Map} */ get buckets() { return new Map( @@ -1194,7 +1222,6 @@ class HistogramChild { /** * Sum of all observations for label set - * @type {number} */ get sum() { return this._sumValues.get(this._key); @@ -1202,7 +1229,6 @@ class HistogramChild { /** * Count of observations for label set - * @type {number} */ get count() { return this._countValues.get(this._key); @@ -1211,23 +1237,23 @@ 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); + 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); } /** * 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 +1274,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 +1302,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 +1326,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 +1338,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,55 +1411,51 @@ 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; + return this._defaultChild!.buckets; } /** * Sum of all observations for label set * * Note: Only available if this is an unlabeled collector. - * @type {number} */ get sum() { - return this._defaultChild.sum; + return this._defaultChild!.sum; } /** * Count of observations for label set * * Note: Only available if this is an unlabeled collector. - * @type {number} */ get count() { - return this._defaultChild.count; + return this._defaultChild!.count; } /** * 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) { - this._defaultChild.observe(value); + observe(value: number) { + this._defaultChild!.observe(value); } /** * 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. */ startTimer() { - return this._defaultChild.startTimer(); + 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); @@ -1434,9 +1463,10 @@ class Histogram extends LabeledCollector { } this._sumValues.delete(key); this._countValues.delete(key); + return key; } - removeAll(labels) { + removeAll(labels: Record) { super.removeAll(labels); for (let bucket of this._buckets) { removeMatchingLabels( @@ -1462,13 +1492,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 +1514,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 +1541,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 +1553,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 +1571,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 +1587,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 +1603,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 +1658,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 +1716,7 @@ function serializeResult(result, options = {}) { } } - let samples; + let samples: Map>; if (addLabels === null) { samples = result.samples; addLabels = {}; @@ -1717,20 +1744,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 +1778,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 50% rename from packages/lib/src/schema.js rename to packages/lib/src/schema.ts index 63c2f5c9..d39416e9 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) { - if (typeof global !== "object" || !global.lazySchemaCompilation) { - return ajv.compile(schema); +export function compile(schema: JSONSchemaType) { + if (typeof global !== "object" || !(global as any).lazySchemaCompilation) { + 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 70% rename from packages/lib/src/shared_commands.js rename to packages/lib/src/shared_commands.ts index c641d1df..06fc5f1c 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,24 +40,28 @@ 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) { - let command = args._[1]; +export async function handlePluginCommand( + args: Record, + pluginList: Map, + pluginListPath: string +) { + let command = (args._ as string[])[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")); - } catch (err) { + } catch (err: any) { logger.error(`Unable to import plugin info from ${args.path}:\n${err.stack}`); process.exitCode = 1; return; @@ -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) { - let command = args._[1]; +export async function handleConfigCommand( + args: Record, + instance: libConfig.Config, + configPath: string +) { + let command = (args._ as string[])[1]; if (command === "list") { - for (let GroupClass of instance.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)}`); @@ -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.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..5e2ab36c 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, -}; diff --git a/packages/lib/src/users.js b/packages/lib/src/users.ts similarity index 82% rename from packages/lib/src/users.js rename to packages/lib/src/users.ts index 5e505fc4..fd2469fc 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); @@ -309,10 +310,16 @@ 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; } @@ -321,12 +328,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 +683,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 68% rename from packages/lib/src/zip_ops.js rename to packages/lib/src/zip_ops.ts index 39cb1bb0..492d2072 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,14 +6,11 @@ * 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) { - let root = null; +export function findRoot(zip: JSZip) { + let root: undefined | string; zip.forEach((relativePath, file) => { let index = relativePath.indexOf("/"); if (index === -1) { @@ -21,20 +18,16 @@ function findRoot(zip) { } 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"); } return root; } - -module.exports = { - findRoot, -}; diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json new file mode 100644 index 00000000..f4ae38e6 --- /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": true, + "sourceMap": true, + "strict": true, + "alwaysStrict": true, + "allowJs": true, + "checkJs": false, + "esModuleInterop": true, + "skipLibCheck": true + } +} 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/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/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/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]); 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"; 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", + ) ), ]); }); 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" +}