From e0c197b9bc0e17101d0cf4f520a4f7579b9ea2a7 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 6 Aug 2023 01:03:13 +0200 Subject: [PATCH] Port basic lib modules to TypeScript --- packages/lib/package.json | 1 + ...entialBackoff.js => ExponentialBackoff.ts} | 56 +- .../src/{PlayerStats.js => PlayerStats.ts} | 20 +- .../src/{RateLimiter.js => RateLimiter.ts} | 46 +- packages/lib/src/{command.js => command.ts} | 123 ++- packages/lib/src/data/messages_instance.js | 2 +- packages/lib/src/data/messages_user.js | 2 +- packages/lib/src/{database.js => database.ts} | 83 +- packages/lib/src/errors.js | 92 --- packages/lib/src/errors.ts | 75 ++ packages/lib/src/{file_ops.js => file_ops.ts} | 84 +- packages/lib/src/{hash.js => hash.ts} | 22 +- packages/lib/src/{ini.js => ini.ts} | 24 +- packages/lib/src/is_deep_strict_equal.d.ts | 1 + packages/lib/src/{logging.js => logging.ts} | 106 ++- .../{logging_utils.js => logging_utils.ts} | 259 +++++-- .../lib/src/{lua_tools.js => lua_tools.ts} | 11 +- packages/lib/src/{plugin.js => plugin.ts} | 575 +++++++------- .../{plugin_loader.js => plugin_loader.ts} | 72 +- .../lib/src/{prometheus.js => prometheus.ts} | 729 +++++++++--------- packages/lib/src/{schema.js => schema.ts} | 22 +- ...{shared_commands.js => shared_commands.ts} | 67 +- packages/lib/src/stream.ts | 2 +- packages/lib/src/{users.js => users.ts} | 205 +++-- packages/lib/src/{zip_ops.js => zip_ops.ts} | 15 +- 25 files changed, 1370 insertions(+), 1324 deletions(-) rename packages/lib/src/{ExponentialBackoff.js => ExponentialBackoff.ts} (70%) rename packages/lib/src/{PlayerStats.js => PlayerStats.ts} (85%) rename packages/lib/src/{RateLimiter.js => RateLimiter.ts} (77%) rename packages/lib/src/{command.js => command.ts} (69%) rename packages/lib/src/{database.js => database.ts} (74%) delete mode 100644 packages/lib/src/errors.js create mode 100644 packages/lib/src/errors.ts rename packages/lib/src/{file_ops.js => file_ops.ts} (73%) rename packages/lib/src/{hash.js => hash.ts} (60%) rename packages/lib/src/{ini.js => ini.ts} (81%) create mode 100644 packages/lib/src/is_deep_strict_equal.d.ts rename packages/lib/src/{logging.js => logging.ts} (53%) rename packages/lib/src/{logging_utils.js => logging_utils.ts} (59%) rename packages/lib/src/{lua_tools.js => lua_tools.ts} (64%) rename packages/lib/src/{plugin.js => plugin.ts} (60%) rename packages/lib/src/{plugin_loader.js => plugin_loader.ts} (66%) rename packages/lib/src/{prometheus.js => prometheus.ts} (76%) rename packages/lib/src/{schema.js => schema.ts} (59%) rename packages/lib/src/{shared_commands.js => shared_commands.ts} (72%) rename packages/lib/src/{users.js => users.ts} (83%) rename packages/lib/src/{zip_ops.js => zip_ops.ts} (75%) diff --git a/packages/lib/package.json b/packages/lib/package.json index c0a121c0..40f530a5 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", + "@types/triple-beam": "^1.3.2", "typescript": "^5.1.6" } } diff --git a/packages/lib/src/ExponentialBackoff.js b/packages/lib/src/ExponentialBackoff.ts similarity index 70% rename from packages/lib/src/ExponentialBackoff.js rename to packages/lib/src/ExponentialBackoff.ts index 5af3033f..1e5550b5 100644 --- a/packages/lib/src/ExponentialBackoff.js +++ b/packages/lib/src/ExponentialBackoff.ts @@ -1,6 +1,3 @@ -"use strict"; - - /** * Helper class for implementing the exponential backoff algorithm * @@ -14,49 +11,48 @@ * the passed value of `max`. The value for the delay will reset back to * `base` if more time than the value for `reset` has passed between two * invocations of delay. - * - * @alias module:lib.ExponentialBackoff */ -class ExponentialBackoff { +export default class ExponentialBackoff { + /** + * Base delay in seconds. The first delay will on average be this + * long, but can be anywhere between 0 and 2 times this value. + */ + base = 1; + /** + * Maximum delay in seconds. A delay will not take more than this + * value. + */ + max = 60; + /** + * Time in seconds between two invocations of delay which will cause + * it to start over with `base` instead of doubling. + */ + reset: number; + + private _exp = 0; + private _lastInvocationTime = Date.now(); + /** * Construct ExponentialBackoff helper * * The options passed may also be modified on the instance at runtime. * - * @param {object=} options - + * @param options - * Options for this exponential backoff helper - * @param {number=} [options.base=1] - + * @param [options.base=1] - * Base delay in seconds. The first delay will on average be this * long, but can be anywhere between 0 and 2 times this value. - * @param {number=} [options.max=60] - + * @param [options.max=60] - * Maximum delay in seconds. A delay will not take more than this * value. - * @param {number=} [options.reset=2*max] - + * @param [options.reset=2*max] - * Time in seconds between two invocations of delay which will cause * it to start over with `base` instead of doubling. */ - constructor(options = {}) { - /** - * Base delay in seconds. The first delay will on average be this - * long, but can be anywhere between 0 and 2 times this value. - * @type {number} - */ + constructor(options: { base?: number, max?: number, reset?: number } = {}) { this.base = options.base || 1; - /** - * Maximum delay in seconds. A delay will not take more than this - * value. - * @type {number} - */ this.max = options.max || 60; - /** - * Time in seconds between two invocations of delay which will cause - * it to start over with `base` instead of doubling. - * @type {number} - */ this.reset = options.reset || 2 * this.max; - - this._exp = 0; - this._lastInvocationTime = Date.now(); } /** @@ -66,7 +62,7 @@ class ExponentialBackoff { * algorithm. This is a value between 0 and min(2 * options.base * * 2retries, optinos.max). * - * @returns {number} time in milliseconds to wait. + * @returns time in milliseconds to wait. */ delay() { let invocationTime = Date.now(); diff --git a/packages/lib/src/PlayerStats.js b/packages/lib/src/PlayerStats.ts similarity index 85% rename from packages/lib/src/PlayerStats.js rename to packages/lib/src/PlayerStats.ts index 1c7c873b..ed1bb244 100644 --- a/packages/lib/src/PlayerStats.js +++ b/packages/lib/src/PlayerStats.ts @@ -1,39 +1,31 @@ -"use strict"; - /** * Stastics collected about a player. - * @alias module:lib.PlayerStats */ -class PlayerStats { +export default class PlayerStats { /** * Count of the number of times this player has been seen joining this server. - * @type {number} */ joinCount = 0; /** * Time in ms this player has been seen online on this instance. - * @type {number} */ onlineTimeMs = 0; /** * Timestamp the player was last seen joining. - * @type {Date=} */ - lastJoinAt; + lastJoinAt?: Date; /** * Timestamp the player was last seen leaving. - * @type {Date=} */ - lastLeaveAt; + lastLeaveAt?: Date; /** * Reason the player was last seen leaving with. - * @type {string=} */ - lastLeaveReason; + lastLeaveReason?: string; static jsonSchema = { type: "object", @@ -62,7 +54,7 @@ class PlayerStats { } } - static fromJSON(json) { + static fromJSON(json: any) { return new this(json); } @@ -84,5 +76,3 @@ class PlayerStats { return json; } } - -module.exports = PlayerStats; diff --git a/packages/lib/src/RateLimiter.js b/packages/lib/src/RateLimiter.ts similarity index 77% rename from packages/lib/src/RateLimiter.js rename to packages/lib/src/RateLimiter.ts index 7a82d0c1..c7362881 100644 --- a/packages/lib/src/RateLimiter.js +++ b/packages/lib/src/RateLimiter.ts @@ -1,6 +1,3 @@ -"use strict"; - - /** * Helper class for rate limiting actions * @@ -8,6 +5,7 @@ * limited on invocations. * * @example + * ```ts * const rateLimiter = RateLimiter({ * action: () => { console.log("action"); }, * maxRate: 1, @@ -19,36 +17,36 @@ * // and one more time 1 second later for 99 other activations that were * // exceeding the rate limit. * } - * @alias module:lib.RateLimiter + * ``` */ -class RateLimiter { +export default class RateLimiter { + /** + * Maximum rate of activations per second. + */ + maxRate: number; + /** + * Callback invoked at most `maxRate` per second on activation. + */ + action: Function | null; + + private _lastRun = 0; + private _runTimeout = null; + private lastProgressBroadcast = Date.now(); + /** * Construct RateLimiter helper * * The options passed may also be modified on the instance at runtime. * - * @param {object=} options - Options for this rate limiter - * @param {number=} options.maxRate - + * @param options - Options for this rate limiter + * @param options.maxRate - * Maximum rate of activations per second. - * @param {function()=} options.action - + * @param options.action - * Callback invoked at maste `maxRate` per second on activation. */ - constructor(options = {}) { - /** - * Maximum rate of activations per second. - * @type {number} - */ + constructor(options: { maxRate?: number, action?: Function } = {}) { this.maxRate = options.maxRate || 1; - - /** - * Callback invoked at most `maxRate` per second on activation. - * @type {function()} - */ this.action = options.action || null; - - this._lastRun = 0; - this._runTimeout = null; - this.lastProgressBroadcast = Date.now(); } /** @@ -59,7 +57,7 @@ class RateLimiter { * invoked immediatly, otherwise a timeout is registered to invoke the * action at next point in time where it will be within the rate limit. * - * @returns {boolean} true if activation was within the rate limit. + * @returns true if activation was within the rate limit. */ activate() { let now = Date.now(); @@ -100,5 +98,3 @@ class RateLimiter { } } } - -module.exports = RateLimiter; diff --git a/packages/lib/src/command.js b/packages/lib/src/command.ts similarity index 69% rename from packages/lib/src/command.js rename to packages/lib/src/command.ts index 972f6969..f0caf2f5 100644 --- a/packages/lib/src/command.js +++ b/packages/lib/src/command.ts @@ -6,29 +6,37 @@ * @author Hornwitser * @module lib/command */ -"use strict"; -const libData = require("./data"); -const libLink = require("./link"); -const libErrors = require("./errors"); +import * as libData from "./data"; +import * as libErrors from "./errors"; +import type { Link } from "./link"; + + +export type CommandHandler = (args: Record, control: Link) => Promise; +export type CommandDefinition = [string | string[], string?, Function?]; /** * Represents a command that can be runned by clusterioctl - * @static */ -class Command { +export class Command { + private _handler: CommandHandler; + private _definition: CommandDefinition; + + name: string; + alias: string[]; + /** * Define an executable command. * - * @param {Object} cmd - Command definiton. - * @param {Array} cmd.definiton - + * @param cmd - Command definiton. + * @param cmd.definiton - * Arguments to pass to yargs .command method to define this * command. - * @param {function(args, control)} handler - + * @param cmd.handler - * Async function invoked when the command is executed. Is given * the parsed args and a reference to the {@link * module:ctl/ctl.Control} instance. */ - constructor({ definition, handler }) { + constructor({ definition, handler }: { definition: CommandDefinition, handler: CommandHandler }) { if (definition[0] instanceof Array) { this.name = definition[0][0].split(" ")[0]; this.alias = definition[0].slice(1); @@ -40,11 +48,11 @@ class Command { this._handler = handler; } - register(yargs) { + register(yargs: any) { yargs.command(...this._definition); } - async run(args, control) { + async run(args: Record, control: Link) { await this._handler(args, control); } } @@ -56,20 +64,23 @@ class Command { * CommandTrees. This is used by clusterioctl to hold the full tree of * available commands, you may extend this tree by using a control plugin, * see {@link module:lib.BaseControlPlugin#addCommands} - * - * @static */ -class CommandTree { +export class CommandTree { + public name: string; + public alias: string[]; + public description: string; + public subCommands: Map; + /** * Define a command containing sub commands. * - * @param {Object} cmd - Command definition - * @param {string} cmd.name - Name of the command. - * @param {Array} cmd.alias - Aliases for this tree. - * @param {description} cmd.description - + * @param cmd - Command definition + * @param cmd.name - Name of the command. + * @param cmd.alias - Aliases for this tree. + * @param cmd.description - * Descripton to provide for this command tree node. */ - constructor({ name, alias, description }) { + constructor({ name, alias, description }: { name: string, alias: string[], description: string }) { if (typeof name !== "string") { throw new Error("name must be a string"); } @@ -83,10 +94,10 @@ class CommandTree { /** * Add a command or command tree to this tree * - * @param {module:lib.Command|module:lib.CommandTree} command - + * @param command - * The command to add to this command tree. */ - add(command) { + add(command: Command | CommandTree) { if (this.subCommands.has(command.name)) { throw new Error(`Command ${command.name} already exists`); } @@ -102,13 +113,13 @@ class CommandTree { /** * Get a command or command tree from this tree * - * @param {string} name - + * @param name - * The name of the command or command tree to retrieve. - * @return {?module:lib.Command|module:lib.CommandTree} + * @return * The command to add to this command tree. */ - get(name) { - return this.subCommands.get(name) || null; + get(name: string) { + return this.subCommands.get(name); } /** @@ -117,10 +128,10 @@ class CommandTree { * Traverses the the tree of sub-commands recursively and adds all of * them to the given yargs parser. * - * @param {Object} yargs - yargs parser to register with. + * @param yargs - yargs parser to register with. */ - register(yargs) { - yargs.command([this.name].concat(this.alias), this.description, (yargs) => { + register(yargs: any) { + yargs.command([this.name].concat(this.alias), this.description, (yargs: any) => { for (let [name, command] of this.subCommands) { // Check if the entry is not an alias. if (name === command.name) { @@ -141,13 +152,12 @@ class CommandTree { * Resolves a string with either an host name or an id into an integer with * the host ID. * - * @param {module:lib.Link} client - link to controller to query host on. - * @param {string} hostName - string with name or id of host. - * @returns {Promise} host ID. - * @static + * @param client - link to controller to query host on. + * @param hostName - string with name or id of host. + * @returns host ID. */ -async function resolveHost(client, hostName) { - let hostId; +export async function resolveHost(client: Link, hostName: string) { + let hostId: number; if (/^-?\d+$/.test(hostName)) { hostId = parseInt(hostName, 10); } else { @@ -173,13 +183,12 @@ async function resolveHost(client, hostName) { * Resolves a string with either an instance name or an id into an integer * with the instance ID. * - * @param {module:lib.Link} client - link to controller to query instance on. - * @param {string} instanceName - string with name or id of instance. - * @returns {Promise} instance ID. - * @static + * @param client - link to controller to query instance on. + * @param instanceName - string with name or id of instance. + * @returns instance ID. */ -async function resolveInstance(client, instanceName) { - let instanceId; +export async function resolveInstance(client: Link, instanceName: string) { + let instanceId: number; if (/^-?\d+$/.test(instanceName)) { instanceId = parseInt(instanceName, 10); } else { @@ -205,14 +214,13 @@ async function resolveInstance(client, instanceName) { * Resolevs a string with either a mod pack name or an id into an integer * with the mod pack ID. * - * @param {module:lib.Link} client - + * @param client - * link to controller to query mod pack on. - * @param {string} modPackName - string with name or id of mod pack. - * @returns {Promise} mod pack ID. - * @static + * @param modPackName - string with name or id of mod pack. + * @returns mod pack ID. */ -async function resolveModPack(client, modPackName) { - let modPackId; +export async function resolveModPack(client: Link, modPackName: string) { + let modPackId: number; if (/^-?\d+/.test(modPackName)) { modPackId = parseInt(modPackName, 10); } else { @@ -238,15 +246,14 @@ async function resolveModPack(client, modPackName) { * Resolves a string with either a role name or an id into an object * representing the role. * - * @param {module:lib.Link} client - link to controller to query role on. - * @param {string} roleName - string with name or id of role. - * @returns {Promise} Role info. - * @static + * @param client - link to controller to query role on. + * @param roleName - string with name or id of role. + * @returns Role info. */ -async function retrieveRole(client, roleName) { +export async function retrieveRole(client: Link, roleName: string) { let roles = await client.sendTo("controller", new libData.RoleListRequest()); - let resolvedRole; + let resolvedRole: libData.RawRole; if (/^-?\d+$/.test(roleName)) { let roleId = parseInt(roleName, 10); for (let role of roles) { @@ -271,13 +278,3 @@ async function retrieveRole(client, roleName) { return resolvedRole; } - -module.exports = { - Command, - CommandTree, - - resolveHost, - resolveInstance, - resolveModPack, - retrieveRole, -}; diff --git a/packages/lib/src/data/messages_instance.js b/packages/lib/src/data/messages_instance.js index 311bf0cf..da661418 100644 --- a/packages/lib/src/data/messages_instance.js +++ b/packages/lib/src/data/messages_instance.js @@ -1,5 +1,5 @@ "use strict"; -const PlayerStats = require("../PlayerStats"); +const PlayerStats = require("../PlayerStats").default; const { JsonString, jsonArray } = require("./composites"); /** @memberof module:lib */ diff --git a/packages/lib/src/data/messages_user.js b/packages/lib/src/data/messages_user.js index 47bc1ff3..edd391cd 100644 --- a/packages/lib/src/data/messages_user.js +++ b/packages/lib/src/data/messages_user.js @@ -1,5 +1,5 @@ "use strict"; -const PlayerStats = require("../PlayerStats"); +const PlayerStats = require("../PlayerStats").default; const { jsonArray, JsonNumber } = require("./composites"); /* @memberof module:lib */ diff --git a/packages/lib/src/database.js b/packages/lib/src/database.ts similarity index 74% rename from packages/lib/src/database.js rename to packages/lib/src/database.ts index 4c2b8520..3bc65235 100644 --- a/packages/lib/src/database.js +++ b/packages/lib/src/database.ts @@ -4,23 +4,21 @@ * @module lib/database * @author Hornwitser */ +import fs from "fs-extra"; -"use strict"; -const fs = require("fs-extra"); - -const libFileOps = require("./file_ops"); -const { basicType } = require("./helpers"); +import * as libFileOps from "./file_ops"; +import { basicType } from "./helpers"; /** * Converts a Map with only string keys to a JavaScript object. * - * @param {Map} map - the Map to convert to an object. - * @return {object} - Object with the mapping's key-values as properties. + * @param map - the Map to convert to an object. + * @return Object with the mapping's key-values as properties. * @throws {Error} if there are non-string keys in map. */ -function mapToObject(map) { - let obj = {}; +export function mapToObject(map: Map) { + let obj: Record = {}; for (let [key, value] of map) { if (typeof key !== "string") { throw new Error( @@ -40,14 +38,15 @@ function mapToObject(map) { * properties as key value pairs and converts it into a JavaScript Map. * Returns an empty Map if the file does not exist. * - * @param {string} filePath - The path to the JSON file to load. + * @param filePath - The path to the JSON file to load. + * @returns promise resolving to the loaded map * @throws {Error} If JSON file did not contain an object. * @throws {Error} If an error occured reading the file. */ -async function loadJsonAsMap(filePath) { +export async function loadJsonAsMap(filePath: string): Promise> { try { let content = await fs.readFile(filePath); - let parsed = JSON.parse(content); + let parsed = JSON.parse(content.toString()); if (basicType(parsed) !== "object") { throw new Error(`Expected object but got ${basicType(parsed)}`); @@ -72,12 +71,12 @@ async function loadJsonAsMap(filePath) { * If the directory the file is to be saved into doesn't exist it will be * created. * - * @param {string} filePath - The path to the JSON file that will be saved. - * @param {Map} map - Map with only string keys to save. + * @param filePath - The path to the JSON file that will be saved. + * @param map - Map with only string keys to save. * @throws {Error} if there are non-string keys in map. * @throws {Error} if an error occured writing to the file. */ -async function saveMapAsJson(filePath, map) { +export async function saveMapAsJson(filePath: string, map: Map) { let obj = mapToObject(map); await libFileOps.safeOutputFile(filePath, JSON.stringify(obj, null, 4)); } @@ -88,15 +87,15 @@ async function saveMapAsJson(filePath, map) { * Loads the JSON file specified by path containing an array of objects that * each have an id property into a Map from the id to the object. * - * @param {string} filePath - The path to the JSON file to load. + * @param filePath - The path to the JSON file to load. * @throws {Error} If JSON file did not contain an array. * @throws {Error} If there are objects in the array without an id property. * @throws {Error} If an error occured reading the file. */ -async function loadJsonArrayAsMap(filePath) { +export async function loadJsonArrayAsMap(filePath: string): Promise> { try { let content = await fs.readFile(filePath); - let parsed = JSON.parse(content); + let parsed = JSON.parse(content.toString()); if (basicType(parsed) !== "array") { throw new Error(`Expected array but got ${basicType(parsed)}`); @@ -132,24 +131,24 @@ async function loadJsonArrayAsMap(filePath) { * Save the values of a Map with objects containing an id property that is * equal to the key the objects are stored at into a JSON file. * - * @param {string} filePath - The path to the JSON file that will be saved. - * @param {Map} map - + * @param filePath - The path to the JSON file that will be saved. + * @param map - * Map with objects containing an id property equal to the key they are * stored under. * @throws {Error} if an error occured writing to the file. */ -async function saveMapAsJsonArray(filePath, map) { +export async function saveMapAsJsonArray(filePath: string, map: Map) { await libFileOps.safeOutputFile(filePath, JSON.stringify([...map.values()], null, 4)); } -function checkName(name) { +function checkName(name: unknown): asserts name is string { if (typeof name !== "string") { throw new Error("name must be a string"); } } -function checkCount(count) { +function checkCount(count: unknown): asserts count is number { if (typeof count !== "number" || isNaN(count)) { throw new Error("count must be a number"); } @@ -164,7 +163,8 @@ function checkCount(count) { * serialized the database discards entries with a zero counts, and when * deserialized the content is verified. */ -class ItemDatabase { +export class ItemDatabase { + private _items: Map = new Map(); /** * Create a new item database @@ -174,9 +174,7 @@ class ItemDatabase { * database from. An empty database will be created if this parameter * is left undefined. */ - constructor(serialized) { - this._items = new Map(); - + constructor(serialized?: object) { // Verify the content of the serialized database if (serialized !== undefined) { for (let [name, count] of Object.entries(serialized)) { @@ -194,10 +192,10 @@ class ItemDatabase { * Serialize the item database into a plain JavaScript object that can be * turned inta a string with JSON.stringify(). * - * @returns {object} Serialized representation of the database + * @returns Serialized representation of the database */ serialize() { - let obj = {}; + let obj: Record = {}; for (let [name, count] of this._items) { if (count !== 0) { obj[name] = count; @@ -223,10 +221,10 @@ class ItemDatabase { * database. If the item has not been previously stored into the database * then 0 is returned. * - * @param {string} name - The name of the item to get the count of. - * @returns {number} The count of the item stored. + * @param name - The name of the item to get the count of. + * @returns The count of the item stored. */ - getItemCount(name) { + getItemCount(name: string) { checkName(name); if (!this._items.has(name)) { @@ -241,10 +239,10 @@ class ItemDatabase { * * Add count copies of the item specified by name to the item database. * - * @param {string} name - The name of the item to add. - * @param {number} count - The count of item to remove. + * @param name - The name of the item to add. + * @param count - The count of item to remove. */ - addItem(name, count) { + addItem(name: string, count: number) { checkName(name); checkCount(count); @@ -262,10 +260,10 @@ class ItemDatabase { * If count is greater than the currently stored count the stored item * count will become negative. * - * @param {string} name - The name of the item to remove. - * @param {number} count - The count of items to remove. + * @param name - The name of the item to remove. + * @param count - The count of items to remove. */ - removeItem(name, count) { + removeItem(name: string, count: number) { checkName(name); checkCount(count); @@ -277,12 +275,3 @@ class ItemDatabase { this._items.set(name, count); }; } - -module.exports = { - mapToObject, - loadJsonAsMap, - saveMapAsJson, - loadJsonArrayAsMap, - saveMapAsJsonArray, - ItemDatabase, -}; diff --git a/packages/lib/src/errors.js b/packages/lib/src/errors.js deleted file mode 100644 index e7de55df..00000000 --- a/packages/lib/src/errors.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Errors thrown in Clusterio - * @module lib/errors - */ -"use strict"; - -/** - * Thrown by commands when they fail - * @static - */ -class CommandError extends Error { } - -/** - * Thrown from requests sent when an error occured handling it - * @static - */ -class RequestError extends Error { - constructor(message, code, stack) { - super(message); - if (code) { this.code = code; } - if (stack) { this.stack = stack; } - } -} - -/** - * Thrown when a permission check fails. - * - * Is a subclass of RequestError to prevent logging stack traces when - * requests fail due to permission denied. - * - * @static - */ -class PermissionError extends RequestError { } - -/** - * Signal for messages that fail validation - * @static - */ -class InvalidMessage extends Error { - constructor(msg, errors) { - super(msg); - this.errors = errors || null; - } -} - -/** - * Thrown from requests when the session it was sent on was lost - * @static - */ -class SessionLost extends Error { } - -/** - * Thrown when WebSocket authentication failed - * @static - */ -class AuthenticationFailed extends Error { } - -/** - * Errror class for known errors occuring during startup - * @static - */ -class StartupError extends Error { } - -/** - * Errors outside of our control - * @static - */ -class EnvironmentError extends Error { } - -/** - * Errors caused by plugins - * @static - */ -class PluginError extends Error { - constructor(pluginName, original) { - super(`PluginError: ${original.message}`); - this.pluginName = pluginName; - this.original = original; - } -} - -module.exports = { - CommandError, - RequestError, - PermissionError, - InvalidMessage, - SessionLost, - AuthenticationFailed, - StartupError, - EnvironmentError, - PluginError, -}; diff --git a/packages/lib/src/errors.ts b/packages/lib/src/errors.ts new file mode 100644 index 00000000..30b26cf3 --- /dev/null +++ b/packages/lib/src/errors.ts @@ -0,0 +1,75 @@ +/** + * Errors thrown in Clusterio + * @module lib/errors + */ + +/** + * Thrown by commands when they fail + */ +export class CommandError extends Error { } + +/** + * Thrown from requests sent when an error occured handling it + */ +export class RequestError extends Error { + constructor( + message: string, + public code?: string, + public stack?: string, + ) { + super(message); + } +} + +/** + * Thrown when a permission check fails. + * + * Is a subclass of RequestError to prevent logging stack traces when + * requests fail due to permission denied. + * + */ +export class PermissionError extends RequestError { } + +/** + * Signal for messages that fail validation + */ +export class InvalidMessage extends Error { + constructor( + message: string, + public errors: object = null, + ) { + super(message); + } +} + +/** + * Thrown from requests when the session it was sent on was lost + */ +export class SessionLost extends Error { } + +/** + * Thrown when WebSocket authentication failed + */ +export class AuthenticationFailed extends Error { } + +/** + * Errror class for known errors occuring during startup + */ +export class StartupError extends Error { } + +/** + * Errors outside of our control + */ +export class EnvironmentError extends Error { } + +/** + * Errors caused by plugins + */ +export class PluginError extends Error { + constructor( + public pluginName: string, + public original: Error + ) { + super(`PluginError: ${original.message}`); + } +} diff --git a/packages/lib/src/file_ops.js b/packages/lib/src/file_ops.ts similarity index 73% rename from packages/lib/src/file_ops.js rename to packages/lib/src/file_ops.ts index 3a19ba2f..0a67ebb3 100644 --- a/packages/lib/src/file_ops.js +++ b/packages/lib/src/file_ops.ts @@ -1,25 +1,24 @@ /** * @module lib/file_ops */ -"use strict"; -const fs = require("fs-extra"); -const path = require("path"); -const crypto = require("crypto"); // needed for getTempFile +import fs from "fs-extra"; +import path from "path"; +import crypto from "crypto"; // needed for getTempFile /** * Returns the newest file in a directory * - * @param {string} directory - The directory to look for the newest file in. - * @param {function(string): boolean} filter - + * @param directory - The directory to look for the newest file in. + * @param filter - * Optional function to filter out files to ignore. Should return true * if the file is to be considered. - * @returns {Promise} + * @returns * Name of the file with the newest timestamp or null if the * directory contains no files. */ -async function getNewestFile(directory, filter = (name) => true) { +export async function getNewestFile(directory: string, filter = (_name: string) => true) { let newestTime = new Date(0); - let newestFile; + let newestFile: string | undefined; for (let entry of await fs.readdir(directory, { withFileTypes: true })) { if (entry.isFile()) { let stat = await fs.stat(path.join(directory, entry.name)); @@ -40,11 +39,11 @@ async function getNewestFile(directory, filter = (name) => true) { * Error reading the size of files are ignored, and error reading the * directory will result in 0 being returned. * - * @param {string} directory - The directory to sum files in. - * @returns {Promise} The size in bytes of all files in the directory. + * @param directory - The directory to sum files in. + * @returns The size in bytes of all files in the directory. */ -async function directorySize(directory) { - let dirEntries; +export async function directorySize(directory: string) { + let dirEntries: fs.Dirent[]; try { dirEntries = await fs.readdir(directory, { withFileTypes: true }); } catch (err) { @@ -53,7 +52,7 @@ async function directorySize(directory) { } throw err; } - let statTasks = []; + let statTasks: Promise[] = []; for (let entry of dirEntries) { if (entry.isFile()) { statTasks.push(fs @@ -77,13 +76,13 @@ async function directorySize(directory) { * racy and a file or folder may have been created in the folder by the time * this function returns. * - * @param {string} directory - directory to check in. - * @param {string} name - file name to check for, may have extension. - * @param {string} extension - dot extension used for the file name. - * @returns {Promise} modified name with extension that likely does + * @param directory - directory to check in. + * @param name - file name to check for, may have extension. + * @param extension - dot extension used for the file name. + * @returns modified name with extension that likely does * not exist in the folder */ -async function findUnusedName(directory, name, extension = "") { +export async function findUnusedName(directory: string, name: string, extension = "") { if (extension && name.endsWith(extension)) { name = name.slice(0, -extension.length); } @@ -101,25 +100,27 @@ async function findUnusedName(directory, name, extension = "") { } } } + /** + * Generate collision resistante temporary file name + * * Warning: this function should not be relied upon to be accurate for * security sensitive applications. The selection process is inherently * racy and a file or folder may have been created in the folder by the time * this function returns. * - * @param {string} prefix - Prefix for file - * @param {string} suffix - Suffix for file - * @param {string} tmpdir - Directory for temp file + * @param prefix - Prefix for file + * @param suffix - Suffix for file + * @param tmpdir - Directory for temp file + * @returns Temporary file name */ -async function getTempFile(prefix, suffix, tmpdir) { - prefix = (typeof prefix !== "undefined") ? prefix : "tmp."; - suffix = (typeof suffix !== "undefined") ? suffix : ""; - tmpdir = (typeof tmpdir !== "undefined") ? tmpdir : "./"; +export async function getTempFile(prefix = "tmp.", suffix = "", tmpdir = "./") { let fileName = path.join(prefix + crypto.randomBytes(16).toString("hex") + suffix); let freeFile = await findUnusedName(tmpdir, fileName); let fullPath = path.join(tmpdir, freeFile); return fullPath; } + /** * Safely write data to a file * @@ -131,11 +132,11 @@ async function getTempFile(prefix, suffix, tmpdir) { * should not be too much of an issue as the next time the same file is * written the temporary will be overwritten and renamed to the target file. * - * @param {string} file - Path to file to write. - * @param {string|Buffer} data - Content to write. - * @param {object|string} options - see fs.writeFile, `flag` must not be set. + * @param file - Path to file to write. + * @param data - Content to write. + * @param options - see fs.writeFile, `flag` must not be set. */ -async function safeOutputFile(file, data, options={}) { +export async function safeOutputFile(file: string, data: string | Buffer, options: fs.WriteFileOptions ={}) { let directory = path.dirname(file); if (!await fs.pathExists(directory)) { await fs.mkdirs(directory); @@ -153,7 +154,7 @@ async function safeOutputFile(file, data, options={}) { } -// Reserved names by allmost all filesystems +// Reserved names by almost all filesystems const badNames = [".", ".."]; // Reserved namespaces in Windows @@ -167,10 +168,10 @@ const badWinNamespaces = [ /** * Check if a string is a valid file name * - * @param {string} name - Name to check + * @param name - Name to check * @throws Error if the name is unsuitable. */ -function checkFilename(name) { +export function checkFilename(name: string) { // All of these are bad in Windows only, except for /, . and .. // See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file const badChars = /[<>:"\/\\|?*\x00-\x1f]/g; @@ -208,10 +209,10 @@ function checkFilename(name) { /** * Clean up string to be suitable for use as filename * - * @param {string} name - Arbitrary name string - * @returns {string} Filename suitable to use in the filesystem + * @param name - Arbitrary name string + * @returns Filename suitable to use in the filesystem */ -function cleanFilename(name) { +export function cleanFilename(name: string) { // copied from checkFilename due to RegExp with global flag containing state. const badChars = /[<>:"\/\\|?*\x00-\x1f]/g; const badEnd = /[. ]$/; @@ -233,14 +234,3 @@ function cleanFilename(name) { return name; } - - -module.exports = { - getNewestFile, - directorySize, - findUnusedName, - getTempFile, - safeOutputFile, - checkFilename, - cleanFilename, -}; diff --git a/packages/lib/src/hash.js b/packages/lib/src/hash.ts similarity index 60% rename from packages/lib/src/hash.js rename to packages/lib/src/hash.ts index 12359602..a2542b9b 100644 --- a/packages/lib/src/hash.js +++ b/packages/lib/src/hash.ts @@ -2,18 +2,17 @@ * Hashing functions * @module lib/hash */ -"use strict"; -const crypto = require("crypto"); -const fs = require("fs-extra"); +import crypto from "crypto"; +import fs from "fs-extra"; /** * Returns a promise that resolves to the SHA1 hash of the stream given * - * @param {Stream} stream - Node stream of the content to hash. - * @returns {Promise} hash of the stream. + * @param stream - Node stream of the content to hash. + * @returns hash of the stream. */ -function hashStream(stream) { +export function hashStream(stream: NodeJS.ReadableStream): Promise { return new Promise((resolve, reject) => { let hasher = crypto.createHash("sha1"); hasher.setEncoding("hex"); @@ -33,14 +32,9 @@ function hashStream(stream) { /** * Returns a promise that resolves to the SHA1 hash of the file given by path * - * @param {string} path - Path to the file to hash. - * @returns {Promise} hash of the file given. + * @param path - Path to the file to hash. + * @returns hash of the file given. */ -function hashFile(path) { +export function hashFile(path: string) { return hashStream(fs.createReadStream(path)); } - -module.exports = { - hashStream, - hashFile, -}; diff --git a/packages/lib/src/ini.js b/packages/lib/src/ini.ts similarity index 81% rename from packages/lib/src/ini.js rename to packages/lib/src/ini.ts index 413aeb92..592d7fb1 100644 --- a/packages/lib/src/ini.js +++ b/packages/lib/src/ini.ts @@ -5,17 +5,14 @@ * @author Hornwitser */ -"use strict"; - /** * Parse INI file into object of sections - * @param {string} input - content of INI file to parse. - * @returns {object>} - * sections parsed from the ini file. + * @param input - content of INI file to parse. + * @returns sections parsed from the ini file. */ -function parse(input) { - const sections = {}; - let currentSection = sections; +export function parse(input: string) { + const sections: Record> = {}; + let currentSection: Record = sections as Record; const lines = input.split(/\r\n|\n/); for (let number = 1; number <= lines.length; number++) { let line = lines[number - 1].replace(/^[\t ]+/, ""); @@ -56,11 +53,11 @@ function parse(input) { /** * Serialize an object of section definitions into an INI file - * @param {object>} sections - + * @param sections - * Sections to encode into ini format - * @returns {string} sections serialized into INI format + * @returns sections serialized into INI format */ -function stringify(sections) { +export function stringify(sections: Record>) { let serialized = []; let first = true; @@ -87,8 +84,3 @@ function stringify(sections) { if (serialized.length) { serialized.push(""); } return serialized.join("\n"); } - -module.exports = { - parse, - stringify, -}; diff --git a/packages/lib/src/is_deep_strict_equal.d.ts b/packages/lib/src/is_deep_strict_equal.d.ts new file mode 100644 index 00000000..ec5e4c72 --- /dev/null +++ b/packages/lib/src/is_deep_strict_equal.d.ts @@ -0,0 +1 @@ +export default function isDeepStrictEqual(a: any, b: any): boolean diff --git a/packages/lib/src/logging.js b/packages/lib/src/logging.ts similarity index 53% rename from packages/lib/src/logging.js rename to packages/lib/src/logging.ts index f4fa9c4c..0a24cfb7 100644 --- a/packages/lib/src/logging.js +++ b/packages/lib/src/logging.ts @@ -1,9 +1,9 @@ "use strict"; -const winston = require("winston"); -const Transport = require("winston-transport"); -const { LEVEL, MESSAGE } = require("triple-beam"); +import winston from "winston"; +import Transport from "winston-transport"; +import { LEVEL, MESSAGE } from "triple-beam"; -const levels = Object.freeze({ +export const levels = Object.freeze({ fatal: 0, error: 1, warn: 2, @@ -13,7 +13,7 @@ const levels = Object.freeze({ verbose: 6, }); -const colors = { +export const colors = { fatal: "inverse red", error: "bold brightRed", warn: "bold brightYellow", @@ -21,21 +21,31 @@ const colors = { info: "bold brightBlue", server: "bold", verbose: "bold grey", -}; +} as const; winston.addColors(colors); // The Console Transport is replicated here because it's overly complicated // in winston, writes to stdout/err instead of console and doesn't work in // the browser. -class ConsoleTransport extends Transport { - constructor(options = {}) { +export class ConsoleTransport extends Transport { + errorLevels: Set; + warnLevels: Set; + filter: (info: object) => boolean; + + constructor( + options: Transport.TransportStreamOptions & { + errorLevels?: string[], + warnLevels?: string[], + filter?: ConsoleTransport["filter"] + } = {} + ) { super(options); this.errorLevels = new Set(options.errorLevels || ["fatal", "error"]); this.warnLevels = new Set(options.warnLevels || ["warn"]); this.filter = options.filter || (() => true); } - log(info, callback) { + log(info: any, callback: Function) { if (!this.filter(info)) { return callback(); } @@ -56,12 +66,12 @@ class ConsoleTransport extends Transport { /** * Formats winston log messages for the web console */ -class WebConsoleFormat { - constructor(options = {}) { - this.options = options; - } +export class WebConsoleFormat { + constructor( + public options = {} + ) { } - transform(info, options) { + transform(info: any, _options: {}) { let src = " "; if (info.host_id !== undefined) { src += `s:${info.host_name} - `; @@ -84,31 +94,41 @@ class WebConsoleFormat { /** * Filter object for logs - * @typedef {Object} LogFilter - * @property {string} [maxLevel] - - * Maximum log level to include. Higher levels are more verbose. - * @property {boolean} [all] - - * Include log entries from controller, all hosts and all instances. - * @property {boolean} [controller] - - * Include log entries from the controller. - * @property {Array} [hostIds] - - * Include log entries for the given hosts and instances of those - * hosts by id. - * @property {Array} [instanceIds] - - * Include log entries for the given instances by id. */ +export interface LogFilter { + /** + * Maximum log level to include. Higher levels are more verbose. + */ + maxLevel?: keyof typeof levels; + /** + * Include log entries from controller, all hosts and all instances. + */ + all?: boolean; + /** + * Include log entries from the controller. + */ + controller?: boolean; + /** + * Include log entries for the given hosts and instances of those + * hosts by id. + */ + hostIds?: number[]; + /** + * Include log entries for the given instances by id. + */ + instanceIds?: number[]; +} /** * Create log filter by level and source. * - * @param {LogFilter} filter - + * @param filter - * Filter to filter log entries by. - * @returns {function(object): boolean} + * @returns * filter returning true for log entries that match it. - * @static */ -function logFilter({ all, controller, hostIds, instanceIds, maxLevel }) { - return info => { +export function logFilter({ all, controller, hostIds, instanceIds, maxLevel }: LogFilter) { + return (info: any) => { // Note: reversed to filter out undefined levels if (maxLevel && !(levels[info.level] <= levels[maxLevel])) { return false; @@ -135,17 +155,23 @@ function logFilter({ all, controller, hostIds, instanceIds, maxLevel }) { }; } -const logger = winston.createLogger({ +export const logger = winston.createLogger({ level: "verbose", levels, format: winston.format.timestamp(), -}); +}) as unknown as Logger; -module.exports = { - ConsoleTransport, - WebConsoleFormat, - colors, - levels, - logFilter, - logger, +// Who in their right mind thought that log levels should be hard coded +// into the type when you can define custom levels you want to support? +export type Logger = Omit & { + fatal: winston.LeveledLogMethod, + error: winston.LeveledLogMethod, + warn: winston.LeveledLogMethod, + audit: winston.LeveledLogMethod, + info: winston.LeveledLogMethod, + server: winston.LeveledLogMethod, + verbose: winston.LeveledLogMethod, }; diff --git a/packages/lib/src/logging_utils.js b/packages/lib/src/logging_utils.ts similarity index 59% rename from packages/lib/src/logging_utils.js rename to packages/lib/src/logging_utils.ts index fe627044..0d075e5c 100644 --- a/packages/lib/src/logging_utils.js +++ b/packages/lib/src/logging_utils.ts @@ -5,24 +5,95 @@ * @module lib/logging_utils */ "use strict"; -const events = require("events"); -const fs = require("fs-extra"); -const path = require("path"); -const stream = require("stream"); -const util = require("util"); -const winston = require("winston"); -const Transport = require("winston-transport"); -const { LEVEL, MESSAGE } = require("triple-beam"); -const chalk = require("chalk"); +import fs from "fs-extra"; +import path from "path"; +import stream from "stream"; +import util from "util"; +import winston from "winston"; +import Transport from "winston-transport"; +import { LEVEL, MESSAGE } from "triple-beam"; +import chalk from "chalk"; -const libData = require("./data"); -const libErrors = require("./errors"); -const libFileOps = require("./file_ops"); -const { levels, logFilter, logger } = require("./logging"); -const libStream = require("./stream"); +import * as libData from "./data"; +import * as libErrors from "./errors"; +import * as libFileOps from "./file_ops"; +import { type LogFilter, levels, logFilter, logger } from "./logging"; +import * as libStream from "./stream"; +import type { Link } from "./link"; const finished = util.promisify(stream.finished); +interface SecondsLogOutput { + /** Where the message came from, one of "stdout" and "stderr". */ + source: "stdout" | "stderr"; + /** Timestamp format, one of "date", "seconds" and "none". */ + format: "seconds"; + /** Timestamp of the message. Not present if format is "none". */ + time: string; + /** Type of message, one of "log", "action" and "generic". */ + type: "log"; + /** Log level for "log" type. i.e "Info" normally. */ + level: string; + /** File reported for "log" type. */ + file: string; + /** Main content of the line. */ + message: string; +} +interface SecondsGenericOutput { + /** Where the message came from, one of "stdout" and "stderr". */ + source: "stdout" | "stderr"; + /** Timestamp format, one of "date", "seconds" and "none". */ + format: "seconds"; + /** Timestamp of the message. Not present if format is "none". */ + time: string; + /** Type of message, one of "log", "action" and "generic". */ + type: "generic"; + /** Main content of the line. */ + message: string; +} +interface DateActionOutput { + /** Where the message came from, one of "stdout" and "stderr". */ + source: "stdout" | "stderr"; + /** Timestamp format, one of "date", "seconds" and "none". */ + format: "date"; + /** Timestamp of the message. Not present if format is "none". */ + time: string; + /** Type of message, one of "log", "action" and "generic". */ + type: "action"; + /** Kind of action for "action" type. i.e "CHAT" for chat. */ + action: string; + /** Main content of the line. */ + message: string; +} +interface DateGenericOutput { + /** Where the message came from, one of "stdout" and "stderr". */ + source: "stdout" | "stderr"; + /** Timestamp format, one of "date", "seconds" and "none". */ + format: "date"; + /** Timestamp of the message. Not present if format is "none". */ + time: string; + /** Type of message, one of "log", "action" and "generic". */ + type: "generic"; + /** Main content of the line. */ + message: string; +} +interface UnformattedOutput { + /** Where the message came from, one of "stdout" and "stderr". */ + source: "stdout" | "stderr"; + /** Timestamp format, one of "date", "seconds" and "none". */ + format: "none"; + /** Type of message, one of "log", "action" and "generic". */ + type: "generic"; + /** Main content of the line. */ + message: string; +} +export type ParsedFactorioOutput = + | SecondsLogOutput + | SecondsGenericOutput + | DateActionOutput + | DateGenericOutput + | UnformattedOutput +; /** * Format a parsed Factorio message with colors @@ -30,11 +101,11 @@ const finished = util.promisify(stream.finished); * Formats a parsed Factorio output from lib/factorio into a readable * colorized output using terminal escape codes that can be printed. * - * @param {Object} parsed - Parsed Factorio server output. - * @returns {string} terminal colorized message. + * @param parsed - Parsed Factorio server output. + * @returns terminal colorized message. * @private */ -function formatServerOutput(parsed) { +function formatServerOutput(parsed: ParsedFactorioOutput) { let time = ""; if (parsed.format === "seconds") { time = `${chalk.yellow(parsed.time.padStart(8))} `; @@ -71,15 +142,17 @@ function formatServerOutput(parsed) { /** * Formats winston log messages for a character terminal. - * @static */ -class TerminalFormat { - constructor(options = {}) { - this.options = options; +export class TerminalFormat { + colorize: winston.Logform.Colorizer; + + constructor( + public options: object = {} + ) { this.colorize = winston.format.colorize(options); } - transform(info, options) { + transform(info: any, options: { showTimestamp: boolean }) { info = this.colorize.transform(info, this.colorize.options); let ts = ""; if (options.showTimestamp && info.timestamp) { @@ -112,17 +185,24 @@ class TerminalFormat { /** * Sends logs over a lib/link connection. - * @static */ -class LinkTransport extends Transport { - constructor(options) { +export class LinkTransport extends Transport { + link: Link; + filter?: (info: any) => boolean; + + constructor( + options: Transport.TransportStreamOptions & { + link: Link, + filter?: (info: any) => boolean, + } + ) { super(options); this.link = options.link; - this.filter = options.filter || null; + this.filter = options.filter; } - log(info, callback) { + log(info: any, callback: Function) { if (this.filter && !this.filter(info)) { return callback(); } @@ -144,18 +224,26 @@ const logIndexVersion = 2; /** * Keeps an index over a log directory to speed up queries to it - * @static */ -class LogIndex { - constructor(logDirectory, serialized) { - this.logDirectory = logDirectory; +export class LogIndex { + index: Map, + controller: boolean, + hostIds: Set, + instanceIds: Set, + }>; + + constructor( + public logDirectory: string, + serialized: any, + ) { this.index = new Map(); if (serialized.version !== logIndexVersion) { return; } - for (let [file, serializedEntry] of Object.entries(serialized.files)) { + for (let [file, serializedEntry] of Object.entries(serialized.files) as [string, any][]) { this.index.set(file, { levels: new Set(serializedEntry.levels), controller: serializedEntry.controller, @@ -190,13 +278,13 @@ class LogIndex { * data about previously indexed files saved with {@link * module:lib.LogIndex#save}. * - * @param {string} logDirectory - Path to directory to load from. - * @returns {Promise} loaded or created log index. + * @param logDirectory - Path to directory to load from. + * @returns loaded or created log index. */ - static async load(logDirectory) { + static async load(logDirectory: string) { try { let content = await fs.readFile(path.join(logDirectory, "index.json")); - return new LogIndex(logDirectory, JSON.parse(content)); + return new LogIndex(logDirectory, JSON.parse(content.toString())); } catch (err) { if (err.code !== "ENOENT") { logger.warn(`Failed to load ${path.join(logDirectory, "index.json")}: ${err}`); @@ -218,19 +306,19 @@ class LogIndex { ); } - async indexFile(file) { + async indexFile(file: string) { let filePath = path.join(this.logDirectory, file); let lineStream = new libStream.LineSplitter({ readableObjectMode: true }); let fileStream = fs.createReadStream(filePath); fileStream.pipe(lineStream); let entry = { - levels: new Set(), + levels: new Set() as Set, controller: false, - hostIds: new Set(), - instanceIds: new Set(), + hostIds: new Set() as Set, + instanceIds: new Set() as Set, }; for await (let line of lineStream) { - let info; + let info: any; try { info = JSON.parse(line); } catch (err) { @@ -273,14 +361,14 @@ class LogIndex { /** * Check if a file includes entries for the given filter * - * @param {string} file - + * @param file - * Name of file in the log directory to check for. - * @param {LogFilter} filter - + * @param filter - * Filter to check index if file contains entries for. - * @returns {boolean} + * @returns * true if the file may contain entries included by the filter. */ - filterIncludesFile(file, { maxLevel, all, controller, instanceIds, hostIds }) { + filterIncludesFile(file: string, { maxLevel, all, controller, instanceIds, hostIds }: LogFilter) { let entry = this.index.get(file); if (!entry) { return true; @@ -314,35 +402,51 @@ class LogIndex { /** * Filter object for querying logs - * @typedef {Object} QueryLogFilter - * @property {number} [limit=100] - Maximum number of entries to return. - * @property {string} [order="asc"] - - * Return entries in ascending ("asc") date order or desceding ("desc") - * date order. - * @property {string} [maxLevel] - - * Maximum log level to include. Higher levels are more verbose. - * @property {boolean} [all] - - * Include log entries from controller, all hosts and all instances. - * @property {boolean} [controller] - - * Include log entries from the controller. - * @property {Array} [hostIds] - - * Include log entries for the given hosts and instances of those - * hosts by id. - * @property {Array} [instanceIds] - - * Include log entries for the given instances by id. */ +export interface QueryLogFilter { + /** + * Maximum number of entries to return. Defaults to 100. + */ + limit?: number; + /** + * Return entries in ascending ("asc") date order or desceding ("desc") + * date order. Defaults to "asc". + */ + order: "asc" | "desc"; + /** + * Maximum log level to include. Higher levels are more verbose. + */ + maxLevel: keyof typeof levels; + /** + * Include log entries from controller, all hosts and all instances. + */ + all: boolean; + /** + * Include log entries from the controller. + */ + controller: boolean; + /** + * Include log entries for the given hosts and instances of those + * hosts by id. + */ + hostIds: number[]; + /** + * Include log entries for the given instances by id. + */ + instanceIds: number[]; +} /** * Query log directory * - * @param {string} logDirectory - path to directory with logs. - * @param {QueryLogFilter} filter - + * @param logDirectory - path to directory with logs. + * @param filter - * Filter to limit logs by. - * @param {module:lib.LogIndex=} index - + * @param index - * Index to speed up query with. - * @returns {Promise>} log entries matching the filter + * @returns log entries matching the filter */ -async function queryLog(logDirectory, filter, index) { +export async function queryLog(logDirectory: string, filter: QueryLogFilter, index: LogIndex) { let files = (await fs.readdir(logDirectory)).filter(entry => logFileGlob.test(entry)); filter = { @@ -363,8 +467,8 @@ async function queryLog(logDirectory, filter, index) { continue; } let filePath = path.join(logDirectory, file); - let fileStream; - let lineStream; + let fileStream: fs.ReadStream; + let lineStream: libStream.LineSplitter; if (filter.order === "asc") { lineStream = new libStream.LineSplitter({ readableObjectMode: true }); fileStream = fs.createReadStream(filePath); @@ -373,7 +477,7 @@ async function queryLog(logDirectory, filter, index) { fileStream = await libStream.createReverseReadStream(filePath); } lineStream.on("data", line => { - let info; + let info: any; try { info = JSON.parse(line); } catch (err) { @@ -403,27 +507,18 @@ async function queryLog(logDirectory, filter, index) { return log; } -function handleUnhandledErrors() { +export function handleUnhandledErrors() { /* eslint-disable node/no-process-exit */ process.on("uncaughtException", err => { logger.fatal(`Uncaught exception:\n${err.stack}`); process.exit(1); }); - process.on("unhandledRejection", (reason, promise) => { + process.on("unhandledRejection", (reason: Error, _promise) => { logger.fatal(`Unhandled rejection:\n${reason.stack ? reason.stack : reason}`); process.exit(1); }); /* eslint-enable node/no-process-exit */ } -const _formatServerOutput = formatServerOutput; -module.exports = { - TerminalFormat, - LinkTransport, - LogIndex, - handleUnhandledErrors, - queryLog, - - // for testing only - _formatServerOutput, -}; +// for testing only +export const _formatServerOutput = formatServerOutput; diff --git a/packages/lib/src/lua_tools.js b/packages/lib/src/lua_tools.ts similarity index 64% rename from packages/lib/src/lua_tools.js rename to packages/lib/src/lua_tools.ts index 086af446..b9dbba13 100644 --- a/packages/lib/src/lua_tools.js +++ b/packages/lib/src/lua_tools.ts @@ -2,15 +2,14 @@ * Utilities for dealing with Lua * @module lib/lua_tools */ -"use strict"; /** * Escapes a string for inclusion into a lua string * - * @param {string} content - String to escape. - * @returns {string} Escaped string. + * @param content - String to escape. + * @returns Escaped string. */ -function escapeString(content) { +export function escapeString(content: string) { return content .replace(/\\/g, "\\\\") .replace(/"/g, '\\"') @@ -20,7 +19,3 @@ function escapeString(content) { .replace(/\r/g, "\\r") ; } - -module.exports = { - escapeString, -}; diff --git a/packages/lib/src/plugin.js b/packages/lib/src/plugin.ts similarity index 60% rename from packages/lib/src/plugin.js rename to packages/lib/src/plugin.ts index 74accdbb..7ccf4b20 100644 --- a/packages/lib/src/plugin.js +++ b/packages/lib/src/plugin.ts @@ -2,18 +2,61 @@ * Plugin interfaces and utilities. * @module lib/plugin */ -"use strict"; +import * as libHelpers from "./helpers"; +import type { Logger } from "./logging"; +import type { ConfigGroup } from "./config"; +import type { CollectorResult } from "./prometheus"; +import type { ParsedFactorioOutput } from "./logging_utils"; +import type { ModPack, ModInfo } from "./data"; +import type { Link } from "./link"; +import type { CommandTree } from "./command"; -const helpers = require("./helpers"); - -/* TODO fix when converting to ts +/** * Conceptual base for controller and instance plugins. - * @typedef { - * BaseControllerPlugin - * |BaseInstancePlugin - * |BaseControlPlugin - * } BasePlugin */ +export type BasePlugin = + | BaseControllerPlugin + | BaseInstancePlugin + | BaseControlPlugin + | BaseWebPlugin +; + +// TODO Add proper typing for plugins +export type PluginInfo = any; +type Controller = any; +type Instance = any; +type Host = any; +type InstanceInfo = any; +type HostConnection = any; +type ControlConnection = any; +type Event = any; +type React = any; + +/** + * Information about the event. + */ +interface PlayerEvent { + type: "join" | "leave" | "import"; + /** Name of the player that joined/left */ + name: string, + /** + * Only present for type "leave". Reason for player leaving the + * game, one of the possible reasons in defines.disconnect_reason + * or "server_quit" if the server exits while the player is online. + */ + reason?: string; +} +type InstanceStatus = + | "unassigned" + | "unknown" + | "stopped" + | "starting" + | "running" + | "stopping" + | "creating_save" + | "exporting_data" + | "deleted" +; /** * Base class for instance plugins @@ -27,40 +70,43 @@ const helpers = require("./helpers"); * Instances may be started and stopped many times, and many instances may * be running at the same time, each of which will have their own instance * of the InstancePlugin class. - * - * @static */ -class BaseInstancePlugin { - constructor(info, instance, host) { +export class BaseInstancePlugin { + /** + * Logger for this plugin + * + * Instance of winston Logger for sending log messages from this + * plugin. Supported methods and their corresponding log levels are + * `error`, `warn`, `audit`, `info` and `verbose`. + */ + logger: Logger; + + private _pendingRconMessages: { + resolve: (result: string) => void, + reject: (err: Error) => void, + message: string, + expectEmpty: boolean, + }[] = []; + private _sendingRconMessages = false; + + constructor( /** * The plugin's own info module */ - this.info = info; - + public info: PluginInfo, /** * Instance the plugin started for - * @type {module:host/src/Instance} */ - this.instance = instance; - + public instance: Instance, /** * Host running the instance * * With the exepction of accessing the host's config you should * avoid ineracting with the host object directly. - * - * @type {module:host/src/Host} */ - this.host = host; - - /** - * Logger for this plugin - * - * Instance of winston Logger for sending log messages from this - * plugin. Supported methods and their corresponding log levels are - * `error`, `warn`, `audit`, `info` and `verbose`. - */ - this.logger = instance.logger.child({ plugin: this.info.name }); + public host: Host, + ) { + this.logger = instance.logger.child({ plugin: this.info.name }) as unknown as Logger; this._pendingRconMessages = []; this._sendingRconMessages = false; @@ -77,12 +123,12 @@ class BaseInstancePlugin { * Invoked after the value of the config field given by `field` has * changed. * - * @param {module:lib.ConfigGroup} group - + * @param group - * The group who's field got changed. - * @param {string} field - Name of the field that changed. - * @param {*} prev - The previous value of the field. + * @param field - Name of the field that changed. + * @param prev - The previous value of the field. */ - async onInstanceConfigFieldChanged(group, field, prev) { } + async onInstanceConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { } /** * Called before collecting Prometheus metrics @@ -97,9 +143,9 @@ class BaseInstancePlugin { * the Gauges/Counters/etc in onMetric and let Clusterio deal will * collecting the values. * - * @returns {*} an async iterator of prometheus metric results or undefined. + * @returns an async iterator of prometheus metric results or undefined. */ - async onMetrics() { } + async onMetrics(): Promise> { } /** * Called after the Factorio server is started @@ -130,24 +176,10 @@ class BaseInstancePlugin { * stderr. The output is a parsed form of the line and contains the * following properties: * - * @param {Object} parsed - parsed server output. - * @param {string} parsed.source - - * Where the message came from, one of "stdout" and "stderr". - * @param {string} parsed.format - - * Timestamp format, one of "date", "seconds" and "none". - * @param {string=} parsed.time - - * Timestamp of the message. Not present if format is "none". - * @param {string} parsed.type - - * Type of message, one of "log", "action" and "generic". - * @param {string=} parsed.level - - * Log level for "log" type. i.e "Info" normally. - * @param {string=} parsed.file - File reported for "log" type. - * @param {string=} parsed.action - - * Kind of action for "action" type. i.e "CHAT" for chat. - * @param {string} parsed.message - Main content of the line. - * @param {string} line - raw line of server output. + * @param parsed - parsed server output. + * @param line - raw line of server output. */ - async onOutput(parsed, line) { } + async onOutput(parsed: ParsedFactorioOutput, line: string) { } /** * Called when an event on the controller connection happens @@ -181,9 +213,9 @@ class BaseInstancePlugin { * send any messages that goes to or via the controller after the * connection has been closed and before a new one is established. * - * @param {string} event - one of connect, drop, resume and close + * @param event - one of connect, drop, resume and close */ - onControllerConnectionEvent(event) { } + onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { } /** * Called when the controller is preparing to disconnect from the host @@ -195,25 +227,19 @@ class BaseInstancePlugin { * Plugins must stop sending messages to the controller, or are forwarded * via the controller after the prepare disconnect has been handled. * - * @param {module:controller/src/HostConnection} connection - + * @param connection - * The connection to the host preparing to disconnect. */ - async onPrepareControllerDisconnect(connection) { } + async onPrepareControllerDisconnect(connection: HostConnection) { } /** * Called when a player joins or leaves the game * * Invoked when a player either joins or leaves the instance. * - * @param {Object} event - Information about the event. - * @param {string} event.type - Either "join", "leave" or "import". - * @param {string} event.name - Name of the player that joined/left. - * @param {string=} event.reason - - * Only present for type "leave". Reason for player leaving the - * game, one of the possible reasons in defines.disconnect_reason - * or "server_quit" if the server exits while the player is online. + * @param event - Information about the event. */ - async onPlayerEvent(event) { } + async onPlayerEvent(event: PlayerEvent) { } /** * Send RCON message to instance @@ -227,13 +253,13 @@ class BaseInstancePlugin { * running some time after `onStart` and some time before `onStop` is * invoked. * - * @param {string} message - message to send to server over RCON. - * @param {boolean=} [expectEmpty=false] - + * @param message - message to send to server over RCON. + * @param expectEmpty - * if true throw if the response is not empty. Useful for detecting * errors that might have been sent in response. - * @returns {Promise} response from server. + * @returns response from server. */ - async sendRcon(message, expectEmpty=false) { + async sendRcon(message: string, expectEmpty=false): Promise { return await this.instance.sendRcon(message, expectEmpty, this.info.name); } @@ -252,14 +278,14 @@ class BaseInstancePlugin { * running some time after `onStart` and some time before `onStop` is * invoked. * - * @param {string} message - message to send to server over RCON. - * @param {boolean=} [expectEmpty=false] - + * @param message - message to send to server over RCON. + * @param [expectEmpty=false] - * if true throw if the response is not empty. Useful for detecting * errors that might have been sent in response. - * @returns {Promise} response from server. + * @returns response from server. */ - async sendOrderedRcon(message, expectEmpty=false) { - let promise = new Promise((resolve, reject) => { + async sendOrderedRcon(message: string, expectEmpty=false) { + let promise = new Promise((resolve, reject) => { this._pendingRconMessages.push({resolve, reject, message, expectEmpty}); }); if (!this._sendingRconMessages) { @@ -291,28 +317,24 @@ class BaseInstancePlugin { * To be discovered the class must be exported under the name `ControllerPlugin` * in the module specified by the `controllerEntrypoint` in the plugin's info.js * file. - * - * @static */ -class BaseControllerPlugin { - constructor(info, controller, metrics, logger) { - this.info = info; +export class BaseControllerPlugin { + /** + * Logger for this plugin + * + * Instance of winston Logger for sending log messages from this + * plugin. Supported methods and their corresponding log levels are + * `error`, `warn`, `audit`, `info` and `verbose`. + */ + logger: Logger; - /** - * Controller - * @type {module:controller/src/Controller} - */ - this.controller = controller; - this.metrics = metrics; - - /** - * Logger for this plugin - * - * Instance of winston Logger for sending log messages from this - * plugin. Supported methods and their corresponding log levels are - * `error`, `warn`, `audit`, `info` and `verbose`. - */ - this.logger = logger.child({ plugin: this.info.name }); + constructor( + public info: PluginInfo, + public controller: Controller, + public metrics: any[], + logger: Logger + ) { + this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger; } /** @@ -354,11 +376,11 @@ class BaseControllerPlugin { * the case of network outages if reconnect fails to re-establish the * session between the controller and the host. * - * @param {module:controller/src/InstanceInfo} instance - + * @param instance - * The instance that changed. - * @param {?string} prev - the previous status of the instance. + * @param prev - the previous status of the instance. */ - async onInstanceStatusChanged(instance, prev) { } + async onInstanceStatusChanged(instance: InstanceInfo, prev?: InstanceStatus) { } /** * Called when the value of a controller config field changed. @@ -366,12 +388,12 @@ class BaseControllerPlugin { * Invoked after the value of the config field given by `field` has * changed on the controller. * - * @param {module:lib.ConfigGroup} group - + * @param group - * The group who's field got changed. - * @param {string} field - Name of the field that changed. - * @param {*} prev - The previous value of the field. + * @param field - Name of the field that changed. + * @param prev - The previous value of the field. */ - async onControllerConfigFieldChanged(group, field, prev) { } + async onControllerConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { } /** * Called when the value of an instance config field changed. @@ -379,14 +401,14 @@ class BaseControllerPlugin { * Invoked after the value of the config field given by `field` has * changed on an instance. * - * @param {module:controller/src/InstanceInfo} instance - + * @param instance - * The instance the config changed on. - * @param {module:lib.ConfigGroup} group - + * @param group - * The group who's field got changed. - * @param {string} field - Name of the field that changed. - * @param {*} prev - The previous value of the field. + * @param field - Name of the field that changed. + * @param prev - The previous value of the field. */ - async onInstanceConfigFieldChanged(instance, group, field, prev) { } + async onInstanceConfigFieldChanged(instance: InstanceInfo, group: ConfigGroup, field: string, prev: unknown) { } /** * Called before collecting Prometheus metrics @@ -401,9 +423,9 @@ class BaseControllerPlugin { * the Gauges/Counters/etc in onMetric and let Clusterio deal will * collecting the values. * - * @returns {*} an async iterator of prometheus metric results or undefined. + * @returns an async iterator of prometheus metric results or undefined. */ - async onMetrics() { } + async onMetrics(): Promise> { } /** * Called when the controller is shutting down @@ -439,11 +461,11 @@ class BaseControllerPlugin { * * Invoked when the connection to the host has been closed. * - * @param {module:controller/src/HostConnection} connection - + * @param connection - * The connection the event occured on. - * @param {string} event - one of connect, drop, resume and close + * @param event - one of connect, drop, resume and close */ - onHostConnectionEvent(connection, event) { } + onHostConnectionEvent(connection: HostConnection, event: "connect" | "drop" | "resume" | "close") { } /** * Called when an avent on a control connection happens @@ -475,11 +497,11 @@ class BaseControllerPlugin { * * Invoked when the connection to the control has been closed. * - * @param {module:controller/src/ControlConnection} connection - + * @param connection - * The connection the event occured on. - * @param {string} event - one of connect, drop, resume, and close. + * @param event - one of connect, drop, resume, and close. */ - onControlConnectionEvent(connection, event) { } + onControlConnectionEvent(connection: ControlConnection, event: "connect" | "drop" | "resume" | "close") { } /** * Called when a host is preparing to disconnect from the controller @@ -490,10 +512,10 @@ class BaseControllerPlugin { * Plugins must stop sending messages to the host in question after the * prepare disconnect has been handled. * - * @param {module:controller/src/HostConnection} connection - + * @param connection - * The connection to the host preparing to disconnect. */ - async onPrepareHostDisconnect(connection) { } + async onPrepareHostDisconnect(connection: HostConnection) { } /** * Called when a mod pack is updated @@ -504,9 +526,9 @@ class BaseControllerPlugin { * If the mod pack has been deleted its `.isDeleted` property will be * true. * - * @param {module:lib.ModPack} modPack - Mod pack that updated. + * @param modPack - Mod pack that updated. */ - async onModPackUpdated(modPack) { } + async onModPackUpdated(modPack: ModPack) { } /** * Called when a mod stored on the controller is updated @@ -516,9 +538,9 @@ class BaseControllerPlugin { * * If a mod has been deleted its `.isDeleted` property will be true. * - * @param {module:lib.ModInfo} mod - Mod that updated. + * @param mod - Mod that updated. */ - async onModUpdated(mod) { } + async onModUpdated(mod: ModInfo) { } /** * Called when a player joins or leaves an instance @@ -526,17 +548,11 @@ class BaseControllerPlugin { * Invoked when a player either joins or leaves an instance in the * cluster. * - * @param {module:controller/src/InstanceInfo} instance - + * @param instance - * The instance it occured on. - * @param {Object} event - Information about the event. - * @param {string} event.type - Either "join", "leave" or "import". - * @param {string} event.name - Name of the player that joined/left. - * @param {string=} event.reason - - * Only present for type "leave". Reason for player leaving the - * game, one of the possible reasons in defines.disconnect_reason - * or "server_quit" if the server exits while the player is online. + * @param event - Information about the event. */ - async onPlayerEvent(instance, event) { } + async onPlayerEvent(instance: InstanceInfo, event: PlayerEvent) { } /** * Broadcast event to all connected hosts @@ -545,20 +561,15 @@ class BaseControllerPlugin { * This does not include hosts that are in the process of closing the * connection, which typically happens when they are shutting down. * - * @param {*} event - Event to send - * @param {Object} data - Data ta pass with the event. + * @param event - Event to send */ - broadcastEventToHosts(event, data={}) { - if (event.type !== "event") { - this.logger.warn(`Ignoring broadcast of ${event.type}`); - return; - } + broadcastEventToHosts(event: Event) { for (let hostConnection of this.controller.wsServer.hostConnections.values()) { if ( !hostConnection.connector.closing - && (!event.plugin || hostConnection.plugins.has(event.plugin)) + && (!event.constructor.plugin || hostConnection.plugins.has(event.constructor.plugin)) ) { - event.send(hostConnection, data); + hostConnection.send(event); } } } @@ -571,24 +582,25 @@ class BaseControllerPlugin { * clusterioctl in order to extend its functionallity. To be discovered the * class must be exported under the name `ControlPlugin` in the module * specified by the `controlEntrypoint` in the plugin's info.js file. - * - * @static */ -class BaseControlPlugin { - constructor(info, logger) { +export class BaseControlPlugin { + /** + * Logger for this plugin + * + * Instance of winston Logger for sending log messages from this + * plugin. Supported methods and their corresponding log levels are + * `error`, `warn`, `audit`, `info` and `verbose`. + */ + logger: Logger; + + constructor( /** * The plugin's own info module */ - this.info = info; - - /** - * Logger for this plugin - * - * Instance of winston Logger for sending log messages from this - * plugin. Supported methods and their corresponding log levels are - * `error`, `warn`, `audit`, `info` and `verbose`. - */ - this.logger = logger.child({ plugin: this.info.name }); + public info: PluginInfo, + logger: Logger, + ) { + this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger; } /** @@ -601,136 +613,153 @@ class BaseControlPlugin { * * Invoked by clusterioctl to let plugins add commands. `rootCommand` is * the top level command node which the plugin should add its own {@link - * module:lib.CommandTree} to. + * CommandTree} to. * - * @param {module:lib.CommandTree} rootCommand - + * @param rootCommand - * Root of the clusterioctl command tree. */ - async addCommands(rootCommand) { } + async addCommands(rootCommand: CommandTree) { } } + /** * Plugin supplied login form - * @typedef {Object} LoginForm - * @property {string} name - - * Internal name of the login form, this should start with the - * plugin name followed by a dot. - * @property {string} title - - * Name displayed above this form in the login window. - * @property {React.Component} Component - - * React component that's rendered for this login form. This is - * supplied the setToken function via its props which should be - * called when an authentication token is aquired via this - * form. */ +export interface PluginLoginForm { + /** + * Internal name of the login form, this should start with the + * plugin name followed by a dot. + */ + name: string; + + /** Name displayed above this form in the login window. */ + title: string; + + /** + * React component that's rendered for this login form. This is + * supplied the setToken function via its props which should be called + * when an authentication token is aquired via this form. + */ + Component: React["Component"]; +}; + /** * Plugin supplied pages - * @typedef {Object} Page - * @property {string} path - URL path to this page. - * @property {string=} sidebarPath - - * If present and this path matches one of the pages in the sidebar it - * will cause that sidebar entry to be highlighted as active. - * @property {string=} sidebarGroup - - * If present group this entry under a group of the given name in the - * sidebar. - * @property {string=} sidebarName - - * If present creates an entry in the sidebar for this page with the - * given text. - * @property {ReactNode} content - - * A react node which is rendered when this page is navigated to. - * Should render a PageLayout. */ +export interface PluginPage { + /** URL path to this page. */ + path: string; + /** + * If present and this path matches one of the pages in the sidebar it + * will cause that sidebar entry to be highlighted as active. + */ + sidebarPath?: string; + /** + * If present group this entry under a group of the given name in the + * sidebar. + */ + sidebarGroup?: string; + /** + * If present creates an entry in the sidebar for this page with the + * given text. + */ + sidebarName?: string; + /** + * A react node which is rendered when this page is navigated to. + * Should render a PageLayout. + */ + content?: React["ReactNode"]; +}; /** * Base class for web interface plugins - * - * @static */ -class BaseWebPlugin { - constructor(container, packageData, info, logger) { +export class BaseWebPlugin { + /** + * Contents of the plugin's package.json file + */ + package: any; + /** + * Control link to the controller, not available until the + * connect event in onControllerConnectionEvent is signaled. + */ + control?: Link; + /** + * Logger for this plugin + * + * Instance of winston Logger for sending log messages from this + * plugin. Supported methods and their corresponding log levels are + * `error`, `warn`, `audit`, `info` and `verbose`. + */ + logger: Logger; + /** + * List of login forms provided by this plugin + */ + loginForms: PluginLoginForm[] = []; + /** + * List of pages provided by this plugin + */ + pages: PluginPage[] = []; + /** + * Extra react component to add to core components + * + * Interface to augment core components of the web UI. Setting a + * component as one of the supported properties of this object will + * cause the web UI to render it when displaying that component, + * usually at the end. Each component will receive a `plugin` param + * which is the instance of the web plugin that contained the + * component extra. + */ + componentExtra: { + /** Placed at the end of the controller page. */ + ControllerPage?: React["ComponentType"], + /** Placed at the end of the hosts list page. */ + HostsPage?: React["ComponentType"], + /** + * Placed at the end of each host page. Takes a `host` param which + * is the host the page is displayed for. + */ + HostViewPage?: React["ComponentType"], + /** Placed at the end of the instance list page. */ + InstancesPage?: React["ComponentType"], + /** + * Placed at the end of each instance page. Takes an `instance` + * param which is the instance the page is displayed for. + */ + InstanceViewPage?: React["ComponentType"], + /** Placed at the end of the users list page. */ + UsersPage?: React["ComponentType"], + /** + * Placed at the end of each user page. Takes a `user` param which + * is the user object the page is displayed for. + */ + UserViewPage?: React["ComponentType"], + /** Placed at the end of the roles list page. */ + RolesPage?: React["ComponentType"], + /** + * Placed at the end of each role page. Takes a `role` param which + * is the role object the page is displayed for. + */ + RoleViewPage?: React["ComponentType"], + } = {}; + + + constructor( /** * Webpack container for this plugin */ - this.container = container; - - /** - * Contents of the plugin's package.json file - * @type {Object} - */ - this.package = packageData; - + public container: any, + packageData: any, /** * The plugin's own info module */ - this.info = info; - - /** - * Control link to the controller, not available until the - * connect event in onControllerConnectionEvent is signaled. - * @type {?module:lib.Link} - */ - this.control = null; - - /** - * Logger for this plugin - * - * Instance of winston Logger for sending log messages from this - * plugin. Supported methods and their corresponding log levels are - * `error`, `warn`, `audit`, `info` and `verbose`. - */ - this.logger = logger.child({ plugin: this.info.name }); - - /** - * List of login forms provided by this plugin - * - * @type {Array} - */ - this.loginForms = []; - - /** - * List of pages provided by this plugin - * - * @type {Array} - */ - this.pages = []; - - /** - * Extra react component to add to core components - * - * Interface to augment core components of the web UI. Setting a - * component as one of the supported properties of this object will - * cause the web UI to render it when displaying that component, - * usually at the end. Each component will receive a `plugin` param - * which is the instance of the web plugin that contained the - * component extra. - * - * @type {object} - * @property {React.ComponentType} ControllerPage - - * Placed at the end of the controller page. - * @property {React.ComponentType} HostsPage - - * Placed at the end of the hosts list page. - * @property {React.ComponentType} HostViewPage - - * Placed at the end of each host page. Takes a `host` param - * which is the host the page is displayed for. - * @property {React.ComponentType} InstancesPage - - * Placed at the end of the instance list page. - * @property {React.ComponentType} InstanceViewPage - - * Placed at the end of each instance page. Takes an `instance` - * param which is the instance the page is displayed for. - * @property {React.ComponentType} UsersPage - - * Placed at the end of the users list page. - * @property {React.ComponentType} UserViewPage - - * Placed at the end of each user page. Takes a `user` param - * which is the user object the page is displayed for. - * @property {React.ComponentType} RolesPage - - * Placed at the end of the roles list page. - * @property {React.ComponentType} RoleViewPage - - * Placed at the end of each role page. Takes a `role` param - * which is the role object the page is displayed for. - */ - this.componentExtra = {}; + public info: PluginInfo, + logger: Logger, + ) { + this.package = packageData; + this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger; } /** @@ -770,27 +799,27 @@ class BaseWebPlugin { * send any messages that goes to or via the controller after the * connection has been closed and before a new one is established. * - * @param {string} event - one of connect, drop, resume and close + * @param event - one of connect, drop, resume and close */ - onControllerConnectionEvent(event) { } + onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { } } /** * Invokes the given hook on all plugins * - * @param {Map} plugins - + * @param plugins - * Mapping of plugin names to plugins to invoke the hook on. - * @param {string} hook - Name of hook to invoke. - * @param {...*} args - Arguments to pass on to the hook. - * @returns {Promise} Non-empty return values from the hooks. + * @param hook - Name of hook to invoke. + * @param args - Arguments to pass on to the hook. + * @returns Non-undefined return values from the hooks. */ -async function invokeHook(plugins, hook, ...args) { - let results = []; +export async function invokeHook(plugins: Map, hook: string, ...args: any[]) { + let results: unknown[] = []; for (let [name, plugin] of plugins) { try { // Use an object to detect if the hook failed on timeout or tried to return a value looking like an error const timeout = {}; - let result = await helpers.timeout( + let result = await libHelpers.timeout( plugin[hook](...args), 15000, timeout @@ -806,13 +835,3 @@ async function invokeHook(plugins, hook, ...args) { } return results; } - - -module.exports = { - BaseInstancePlugin, - BaseControllerPlugin, - BaseControlPlugin, - BaseWebPlugin, - - invokeHook, -}; diff --git a/packages/lib/src/plugin_loader.js b/packages/lib/src/plugin_loader.ts similarity index 66% rename from packages/lib/src/plugin_loader.js rename to packages/lib/src/plugin_loader.ts index e97d7541..6771dd7b 100644 --- a/packages/lib/src/plugin_loader.js +++ b/packages/lib/src/plugin_loader.ts @@ -3,12 +3,10 @@ * Helpers for loading plugins in Node.js * @module lib/plugin_loader */ -"use strict"; - -const libErrors = require("./errors"); -const libPlugin = require("./plugin"); -const { logger } = require("./logging"); -const path = require("path"); +import path from "path"; +import * as libErrors from "./errors"; +import * as libPlugin from "./plugin"; +import { logger } from "./logging"; /** @@ -18,16 +16,15 @@ const path = require("path"); * loaded the info modules will not be reloaded should this function be * called again. * - * @param {Map} pluginList - + * @param pluginList - * Mapping of plugin name to require path for the plugins to load. - * @returns {Promise>} Array of plugin info modules. - * @static + * @returns Array of plugin info modules. */ -async function loadPluginInfos(pluginList) { - let plugins = []; +export async function loadPluginInfos(pluginList: Map) { + let plugins: libPlugin.PluginInfo[] = []; for (let [pluginName, pluginPath] of pluginList) { - let pluginInfo; - let pluginPackage; + let pluginInfo: libPlugin.PluginInfo; + let pluginPackage: { version: string }; try { pluginInfo = require(path.posix.join(pluginPath, "info")); @@ -56,7 +53,12 @@ async function loadPluginInfos(pluginList) { return plugins; } -function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) { +function loadPluginClass( + entrypointName: string, + className: string, + pluginClass: any, + pluginInfo: libPlugin.PluginInfo +) { let resolvedPath = path.posix.join(pluginInfo.requirePath, pluginInfo[entrypointName]); let entrypoint = require(resolvedPath); if (!entrypoint[className]) { @@ -75,45 +77,41 @@ function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) { /** * Load controller plugin class of a plugin * - * @param {Object} pluginInfo - + * @param pluginInfo - * Plugin info object returned from {@link - * module:lib.loadPluginInfos} to load class from. - * @returns {Promise} plugin class - * @static + * loadPluginInfos} to load class from. + * @returns plugin class */ -async function loadControllerPluginClass(pluginInfo) { +export async function loadControllerPluginClass( + pluginInfo: libPlugin.PluginInfo +): Promise { return loadPluginClass("controllerEntrypoint", "ControllerPlugin", libPlugin.BaseControllerPlugin, pluginInfo); } /** * Load instance plugin class of a plugin - * - * @param {Object} pluginInfo - + + * @param pluginInfo - * Plugin info object returned from {@link - * module:lib.loadPluginInfos} to load class from. - * @returns {Promise} plugin class - * @static + * loadPluginInfos} to load class from. + * @returns plugin class */ -async function loadInstancePluginClass(pluginInfo) { +export async function loadInstancePluginClass( + pluginInfo: libPlugin.PluginInfo +): Promise { return loadPluginClass("instanceEntrypoint", "InstancePlugin", libPlugin.BaseInstancePlugin, pluginInfo); } /** * Load control plugin class of a plugin * - * @param {Object} pluginInfo - + * @param pluginInfo - * Plugin info object returned from {@link - * module:lib.loadPluginInfos} to load class from. - * @returns {Promise} plugin class - * @static + * loadPluginInfos} to load class from. + * @returns plugin class */ -async function loadControlPluginClass(pluginInfo) { +export async function loadControlPluginClass( + pluginInfo: libPlugin.PluginInfo +): Promise { return loadPluginClass("controlEntrypoint", "ControlPlugin", libPlugin.BaseControlPlugin, pluginInfo); } - -module.exports = { - loadPluginInfos, - loadControllerPluginClass, - loadInstancePluginClass, - loadControlPluginClass, -}; diff --git a/packages/lib/src/prometheus.js b/packages/lib/src/prometheus.ts similarity index 76% rename from packages/lib/src/prometheus.js rename to packages/lib/src/prometheus.ts index 7a69f391..93540e8c 100644 --- a/packages/lib/src/prometheus.js +++ b/packages/lib/src/prometheus.ts @@ -7,9 +7,8 @@ * metric collections in prom-client. * * The ordinary use case of instrumenting a code base should be covered by - * {@link module:lib.Counter}, {@link - * module:lib.Gauge}, {@link module:lib.Histogram} and - * {@link module:lib.exposition}. See documentation for each of + * {@link Counter}, {@link Gauge}, {@link Histogram} and + * {@link exposition}. See documentation for each of * the listed interfaces for more information. * * @example @@ -39,21 +38,13 @@ * app.listen(9100); * @module lib/prometheus */ -"use strict"; /** - * Result from collecting a {@link module:lib.Collector} - * @typedef {Object} CollectorResult - * @property {module:lib.Metric} metric - - * Metric collected. - * @property {Map>} samples - - * Mapping of metric suffix to mapping of label keys to values - * collected. For normal metrics the first level contains a single - * entry under the empty string as key. If the metric does not have - * labels the second level also contains a single entry under the empty - * string as a key. + * Result from collecting a {@link Collector} * - * @example Simple Metric + * @example + * Simple Metric + * ```ts * let result = { * metric: new Metric("count", "simple_total_count", "A simple counter"), * samples: new Map([ @@ -62,8 +53,11 @@ * ])], * ]), * } + * ``` * - * @example Labeled Metric + * @example + * Labeled Metric + * ```ts * let result = { * metric: new Metric( * "count", "labeled_total_count", "A labeled counter", ["a", "b"] @@ -76,8 +70,11 @@ * ])], * ]), * } + * ``` * - * @example Histogram Metric + * @example + * Histogram Metric + * ```ts * let result = { * metric: new Metric( * "histogram", "histogram_size", "A histogram of sizes" @@ -97,25 +94,42 @@ * ]), * } */ +export interface CollectorResult { + /** Metric collected. */ + metric: Metric; + /** + * Mapping of metric suffix to mapping of label keys to values + * collected. For normal metrics the first level contains a single + * entry under the empty string as key. If the metric does not have + * labels the second level also contains a single entry under the empty + * string as a key. + */ + samples: Map>; +} + +export type MetricType = "counter" | "gauge" | "histogram" | "summary" | "untyped"; /** * Represents a collectable metric * * Used in implementing collectors in order to validate the name, help text * and labels attached to the collector. - * @static */ -class Metric { - /** - * @param {string} type - - * Metric type, should be one of `counter`, `gauge`, `histogram`, - * `summary` or `untyped`, - * @param {string} name - Name of the metric. - * @param {string} help - Help text for the metric. - * @param {Array} labels - - * @param {Array} _reserved_labels - - */ - constructor(type, name, help, labels = [], _reserved_labels = []) { +export class Metric { + constructor( + /** + * Metric type, should be one of `counter`, `gauge`, `histogram`, + * `summary` or `untyped`. + */ + public type: MetricType, + /** Name of the metric */ + public name: string, + /** Help text for the metric */ + public help: string, + /** Labels for this metric. */ + public labels: string[] = [], + _reserved_labels: string[] = [] + ) { if (typeof name !== "string") { throw new Error("Expected name to be a string"); } @@ -135,40 +149,13 @@ class Metric { throw new Error(`Reserved label '${label}'`); } } - - /** - * Name of the metric - * @type {string} - */ - this.name = name; - - /** - * Help text for the metric - * @type {string} - */ - this.help = help; - - /** - * Metric type, should be one of `counter`, `gauge`, `histogram`, - * `summary` or `untyped`. - * @type {string} - */ - this.type = type; - - /** - * Labels for this metric. - * @type {Array} - */ - this.labels = labels; } } /** * The default registry which collectors are regististered to. - * @type {module:lib.CollectorRegistry} - * @static */ -let defaultRegistry; +export let defaultRegistry: CollectorRegistry; /** * Base class for all collectors @@ -176,16 +163,14 @@ let defaultRegistry; * This servers mostly as a conceptual base for all collectors, the only * feature implemented here is registring to the default registry on * construction. If you want to implement a custom collector you most - * likely want to base it on {@link module:lib.LabeledCollector} + * likely want to base it on {@link LabeledCollector} * instead. - * - * @static */ -class Collector { +export class Collector { /** * Create collector * - * @param {boolean=} register - + * @param register - * Whether to register this collector to the default registry. */ constructor(register = true) { @@ -197,23 +182,21 @@ class Collector { /** * Retrieve metric data from this collctor. * - * Called by {@link module:lib.CollectorRegistry} to gather + * Called by {@link CollectorRegistry} to gather * the metric data this Collector exports. - * - * @returns {*} Async iterator of {@link CollectorResult}. */ - async* collect() { } + async* collect(): AsyncIterable { } } const labelEscapesChars = /[\\\n\"]/; /** * Escapes a label value in accordance with the text exposition format * - * @param {string} value - Value to escape. - * @returns {string} escaped value with \, ", and newline escaped. + * @param value - Value to escape. + * @returns escaped value with \, ", and newline escaped. * @private */ -function escapeLabelValue(value) { +function escapeLabelValue(value: string) { if (!labelEscapesChars.test(value)) { return value; } @@ -224,32 +207,34 @@ function escapeLabelValue(value) { ; } +export type LabelValues = string[] | [Record]; + /** * Convert label expression to unique string * - * @param {Array<(string|Object)>} labels - + * @param labels - * label values to compute key for. - * @param {Array} metricLabels - labels defined for the metric. - * @returns {string} computed key + * @param metricLabels - labels defined for the metric. + * @returns computed key * @private */ -function labelsToKey(labels, metricLabels) { +function labelsToKey(labels: LabelValues, metricLabels: string[]) { let items = []; if (labels.length === 1 && typeof labels[0] === "object") { - labels = labels[0]; + let labelObj = labels[0]; for (let name of metricLabels) { - if (!Object.hasOwnProperty.call(labels, name)) { + if (!Object.hasOwnProperty.call(labelObj, name)) { throw new Error(`Missing label '${name}'`); } - if (typeof labels[name] !== "string") { + if (typeof labelObj[name] !== "string") { throw new Error( `Expected value for label '${name}' to be a string` ); } - items.push(`${name}="${escapeLabelValue(labels[name])}"`); + items.push(`${name}="${escapeLabelValue(labelObj[name])}"`); } - for (let name of Object.keys(labels)) { + for (let name of Object.keys(labelObj)) { if (!metricLabels.includes(name)) { throw new Error(`Extra label '${name}'`); } @@ -265,12 +250,13 @@ function labelsToKey(labels, metricLabels) { } for (let i=0; i < metricLabels.length; ++i) { - if (typeof labels[i] !== "string") { + let label = labels[i]; + if (typeof label !== "string") { throw new Error( `Expected value for label '${metricLabels[i]}' to be a string` ); } - items.push(`${metricLabels[i]}="${escapeLabelValue(labels[i])}"`); + items.push(`${metricLabels[i]}="${escapeLabelValue(label)}"`); } } @@ -281,8 +267,8 @@ function labelsToKey(labels, metricLabels) { return items.join(","); } -function keyToLabels(key) { - let labels = new Map(); +function keyToLabels(key: string) { + let labels = new Map(); if (key !== "") { for (let pair of key.split(",")) { @@ -299,7 +285,7 @@ function keyToLabels(key) { return labels; } -function removeMatchingLabels(mapping, labels) { +function removeMatchingLabels(mapping: Map, labels: Record) { for (let key of mapping.keys()) { let candidate = keyToLabels(key); let hasLabels = Object.entries(labels).every(([name, value]) => ( @@ -318,55 +304,57 @@ function removeMatchingLabels(mapping, labels) { * set is a child to the collector that can be retrieved with the .labels() * method. * - * @extends module:lib.Collector - * @static + * @extends Collector */ -class LabeledCollector extends Collector { +export class LabeledCollector extends Collector { + callback: (collector: LabeledCollector) => void | Promise; + metric: Metric; + + private _children: Map; + private _childClass: { new(collector: LabeledCollector, key: string): Child }; + /** * Create optionally labeled collector * - * @param {string} type - Type for metric. - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object} options - options for collector. - * @param {Array=} options.labels - + * @param type - Type for metric. + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. + * @param options.labels - * Labels for this metric, defaults to no labels. - * @param {Array=} options._reservedLabels - + * @param options._reservedLabels - * Labels which may not be used. Is passed to Metric constructor. - * @param {boolean=} options.register - + * @param options.register - * If true registers this collector with the default registry. * Defaults to true. - * @param {function()=} options.callback - + * @param options.callback - * Possibly async function that is called before the metric is * collected. The collector being collected is passed as the * argument. - * @param { - * function(module:lib.LabeledCollector,string) - * } childClass - + * @param childClass - * Constructor taking instance of collector and label key as * arguments and returns a child instance. */ - constructor(type, name, help, options, childClass) { - let labels = []; - let reservedLabels = []; - let register = true; - let callback = null; - for (let [key, value] of Object.entries(options)) { - if (key === "labels") { - labels = value; - } else if (key === "_reservedLabels") { - reservedLabels = value; - } else if (key === "register") { - register = value; - } else if (key === "callback") { - callback = value; - } else { - throw new Error(`Unrecognized option '${key}'`); - } + constructor( + type: MetricType, + name: string, + help: string, + options: { + labels?: string[], + _reservedLabels?: string[], + register?: boolean, + callback?: (collector: LabeledCollector) => void | Promise, + }, + childClass: { new(collector: LabeledCollector, key: string): Child } + ) { + const { labels = [], _reservedLabels = [], register = true, callback, ...rest } = options; + + for (let key of Object.keys(rest)) { + throw new Error(`Unrecognized option '${key}'`); } // Make sure we don't register to the default registry if metric throws. - let metric = new Metric(type, name, help, labels, reservedLabels); + let metric = new Metric(type, name, help, labels, _reservedLabels); super(register); @@ -387,13 +375,13 @@ class LabeledCollector extends Collector { * exported from the collector until it's removed explicitly with * the .remove() or .removeAll() methods. * - * @param {...string|Object} labels - + * @param labels - * A string value passed for each label defined on the metric in the * same order as the labels option given to the collector, or an * object mapping label name to label value. - * @returns {*} Child collector for the given labels. + * @returns Child collector for the given labels. */ - labels(...labels) { + labels(...labels: LabelValues): Child { let key = labelsToKey(labels, this.metric.labels); let child = this._children.get(key); if (child === undefined) { @@ -409,13 +397,13 @@ class LabeledCollector extends Collector { * Remove the child collector and the value it stores from the collector * itself. This will remove the entry exported for the given labels. * - * @param {...string|Object} labels - + * @param labels - * A string value passed for each label defined on the metric in the * same order as the labels option given to the collector, or an * object mapping label name to label value. - * @returns {string} key for labels to remove (for use in subclasses). + * @returns key for labels to remove (for use in subclasses). */ - remove(...labels) { + remove(...labels: LabelValues) { let key = labelsToKey(labels, this.metric.labels); if (key === "") { throw new Error("labels cannot be empty"); @@ -436,7 +424,7 @@ class LabeledCollector extends Collector { * Object mapping with label name to label values that should be * matches. */ - removeAll(labels) { + removeAll(labels: Record) { if (!Object.keys(labels).length) { throw new Error("labels cannot be empty"); } @@ -461,39 +449,46 @@ class LabeledCollector extends Collector { /** * Base class for implementing single value per label collectors * - * @extends module:lib.LabeledCollector - * @static + * @extends LabeledCollector */ -class ValueCollector extends LabeledCollector { +export class ValueCollector extends LabeledCollector { + _values: Map; + protected _defaultChild: Child | null; + /** * Create optionally labeled value collector * - * @param {string} type - Type for metric. - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object} options - options for collector. - * @param {Array=} options.labels - + * @param type - Type for metric. + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. + * @param options.labels - * Labels for this metric, defaults to no labels. - * @param {Array=} options._reservedLabels - + * @param options._reservedLabels - * Labels which may not be used. Is passed to Metric constructor. - * @param {boolean=} options.register - + * @param options.register - * If true registers this collector with the default registry. * Defaults to true. - * @param {function()=} options.callback - + * @param options.callback - * Possibly async function that is called before the collector value * is collected. The collector being collected is passed as the * argument. - * @param { - * function( - * new:module:lib.ValueCollectorChild, - * module:lib.LabeledCollector, - * string - * ) - * } childClass - + * @param childClass - * Constructor taking instance of collector and label key as * arguments and returns a child instance. */ - constructor(type, name, help, options, childClass) { + constructor( + type: MetricType, + name: string, + help: string, + options: { + labels?: string[], + _reservedLabels?: string[], + register?: boolean, + callback?: (collector: ValueCollector) => void | Promise, + }, + childClass: { new(collector: ValueCollector, key: string): Child } + ) { super(type, name, help, options, childClass); this._values = new Map(); @@ -509,18 +504,18 @@ class ValueCollector extends LabeledCollector { * Get the current value for this collector. * * Note: Only works if this is an unlabeled collector. - * @returns {number} value stored. + * @returns value stored. */ get() { return this._defaultChild.get(); } - remove(...labels) { + remove(...labels: LabelValues) { let key = super.remove(...labels); this._values.delete(key); } - removeAll(labels) { + removeAll(labels: Record) { super.removeAll(labels); removeMatchingLabels(this._values, labels); } @@ -535,7 +530,10 @@ class ValueCollector extends LabeledCollector { * Child collector representing the value of a single label set */ class ValueCollectorChild { - constructor(collector, key) { + protected _values: Map; + protected _key: string; + + constructor(collector: ValueCollector, key: string) { this._values = collector._values; this._key = key; @@ -545,8 +543,7 @@ class ValueCollectorChild { /** * Returns the current value of label set * - * Note: Only works if this is an unlabeled collector. - * @returns {number} value stored. + * @returns value stored. */ get() { return this._values.get(this._key); @@ -555,12 +552,12 @@ class ValueCollectorChild { /** * Child counter holding the value for a single label set. - * @extends module:lib~ValueCollectorChild + * @extends ValueCollectorChild */ class CounterChild extends ValueCollectorChild { /** * Increment counter for label set - * @param {number} value - Positive number to increment by. + * @param value - Positive number to increment by. */ inc(value = 1) { // Note: Inverted to also catch NaN @@ -583,7 +580,7 @@ class CounterChild extends ValueCollectorChild { * Counters should be created at module load time and referenced in the * functions that increment them, for example: * - * ```js + * ```ts * const totalRequests = new Counter( * "app_request_count_total", * "Total requests handled.", @@ -596,10 +593,9 @@ class CounterChild extends ValueCollectorChild { * ``` * * The `totalRequests` counter will register with the default registry and - * provided exposition is set up (see {@link - * module:lib.exposition}) the counter will be exported to - * Prometheus starting out with a value of 0. And that is all there is to - * it. + * provided exposition is set up (see {@link exposition}) the counter will + * be exported to Prometheus starting out with a value of 0. And that is + * all there is to it. * * It is sometimes useful however to divide a metric up into diffrent * sections, for example to have a different count for each endpoint handled @@ -607,7 +603,7 @@ class CounterChild extends ValueCollectorChild { * an error. For this Prometheus provides labels, and to use them pass the * labels option as the third argument to Counter: * - * ```js + * ```ts * const totalRequests = new Counter( * "app_request_count_total", * "Total requests handled.", @@ -639,7 +635,7 @@ class CounterChild extends ValueCollectorChild { * that will be used. This is done by calling `.labels()` for each * combination, for example: * - * ```js + * ```ts * for (let endpoint of allEndpoints) { * for (let status of ["ok", "err"]) { * totalRequests.labels(endpoint, status); @@ -652,16 +648,15 @@ class CounterChild extends ValueCollectorChild { * labels you actually need as resource usage for a metric increases * exponentially with the number of labels used. * - * @extends module:lib.ValueCollector - * @static + * @extends ValueCollector */ -class Counter extends ValueCollector { +export class Counter extends ValueCollector { /** * Create optionally labeled counter * - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object=} options - options for collector. + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. * @param {Array=} options.labels - * Labels for this metric, defaults to no labels. * @param {boolean=} options.register - @@ -672,7 +667,15 @@ class Counter extends ValueCollector { * collected. The collector being collected is passed as the * argument. */ - constructor(name, help, options = {}) { + constructor( + name: string, + help: string, + options: { + labels?: string[], + register?: boolean, + callback?: (collector: Counter) => void | Promise, + } = {}, + ) { super("counter", name, help, options, CounterChild); } @@ -680,7 +683,7 @@ class Counter extends ValueCollector { * Increment counter value * * Note: Only works if this is an unlabeled collector. - * @param {number} value - Positive number to increment by. + * @param value - Positive number to increment by. */ inc(value = 1) { this._defaultChild.inc(value); @@ -689,13 +692,13 @@ class Counter extends ValueCollector { /** * Child gauge holding the value for a single label set. - * @extends module:lib~ValueCollectorChild + * @extends ValueCollectorChild */ class GaugeChild extends ValueCollectorChild { /** * Increment gague for label set * - * @param {number} value - number to increment by. + * @param value - number to increment by. */ inc(value = 1) { this._values.set(this._key, this._values.get(this._key) + value); @@ -704,7 +707,7 @@ class GaugeChild extends ValueCollectorChild { /** * Decrement gague for label set * - * @param {number} value - number to decrease by. + * @param value - number to decrease by. */ dec(value = 1) { this._values.set(this._key, this._values.get(this._key) - value); @@ -713,9 +716,9 @@ class GaugeChild extends ValueCollectorChild { /** * Set gague for label set * - * @param {number} value - number to set gauge to. + * @param value - number to set gauge to. */ - set(value) { + set(value: number) { this._values.set(this._key, value); } @@ -729,7 +732,7 @@ class GaugeChild extends ValueCollectorChild { /** * Start a timer for setting a duration for label set * - * @returns {function()} + * @returns * function that when called will set the guage to the duration in * seconds from when the timer was started */ @@ -753,7 +756,7 @@ class GaugeChild extends ValueCollectorChild { * Gauges should be created at module load time and referenced in the * functions that modify them, for example: * - * ```js + * ```ts * const activeRequests = new Gauge( * "app_active_request_count", * "Number of requests in-flight.", @@ -770,16 +773,15 @@ class GaugeChild extends ValueCollectorChild { * ``` * * The `activeRequests` gauge will register with the default registry and - * provided exposition is set up (see {@link - * module:lib.exposition}) the gauge will be exported to - * Prometheus starting out with a value of 0. + * provided exposition is set up (see {@link exposition}) the gauge will be + * exported to Prometheus starting out with a value of 0. * * Sometimes keeping track of the value measured is impractical or * prohibitly expensive. In those cases you can update the value * of the collector as it's being collected for export with a callback * function passed as one of the options. * - * ```js + * ```ts * const userCount = new Gauge( * "app_user_count", * "Number of users in the app.", @@ -805,7 +807,7 @@ class GaugeChild extends ValueCollectorChild { * directly on the counter itself, instead a child counter with label values * set has to be retrieved with the `.labels()` method. For example: * - * ```js + * ```ts * const userCount = new Gauge( * "app_user_count", * "Number of users in the app", @@ -831,7 +833,7 @@ class GaugeChild extends ValueCollectorChild { * label values that will be used. This is done by calling `.labels()` for * each combination, for example: * - * ```js + * ```ts * for (let role of ["system", "admin", "normal") { * userCount.labels(role); * } @@ -842,27 +844,34 @@ class GaugeChild extends ValueCollectorChild { * labels you actually need as resource usage for a metric increases * exponentially with the number of labels used. * - * @extends module:lib.ValueCollector - * @static + * @extends ValueCollector */ -class Gauge extends ValueCollector { +export class Gauge extends ValueCollector { /** * Create optionally labeled gauge * - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object=} options - options for collector. - * @param {Array=} options.labels - + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. + * @param options.labels - * Labels for this metric, defaults to no labels. - * @param {boolean=} options.register - + * @param options.register - * If true registers this collector with the default registry. * Defaults to true. - * @param {function()=} options.callback - + * @param options.callback - * Possibly async function that is called when the metric is * collected. The collector being collected is passed as the * argument. */ - constructor(name, help, options = {}) { + constructor( + name: string, + help: string, + options: { + labels?: string[], + register?: boolean, + callback?: (collector: Gauge) => void | Promise, + } = {}, + ) { super("gauge", name, help, options, GaugeChild); } @@ -870,7 +879,7 @@ class Gauge extends ValueCollector { * Increment gague value * * Note: Only works if this is an unlabeled collector. - * @param {number} value - number to increment by. + * @param value - number to increment by. */ inc(value = 1) { this._defaultChild.inc(value); @@ -880,7 +889,7 @@ class Gauge extends ValueCollector { * Decrement gague value * * Note: Only works if this is an unlabeled collector. - * @param {number} value - number to decrease by. + * @param value - number to decrease by. */ dec(value = 1) { this._defaultChild.dec(value); @@ -890,9 +899,9 @@ class Gauge extends ValueCollector { * Set gague value * * Note: Only works if this is an unlabeled collector. - * @param {number} value - number to set gauge to. + * @param value - number to set gauge to. */ - set(value) { + set(value: number) { this._defaultChild.set(value); } @@ -909,7 +918,7 @@ class Gauge extends ValueCollector { * Start a timer for setting a duration * * Note: Only works if this is an unlabeled collector. - * @returns {function()} + * @returns * function that when called will set the guage to the duration in * seconds from when the timer was started */ @@ -922,7 +931,11 @@ class Gauge extends ValueCollector { * Child Summary holding the sum and count for a single label set. */ class SummaryChild { - constructor(collector, key) { + protected _sumValues: Map; + protected _countValues: Map; + protected _key: string; + + constructor(collector: Summary, key: string) { this._sumValues = collector._sumValues; this._countValues = collector._countValues; this._key = key; @@ -933,7 +946,6 @@ class SummaryChild { /** * Sum of all observations for label set - * @type {number} */ get sum() { return this._sumValues.get(this._key); @@ -941,7 +953,6 @@ class SummaryChild { /** * Count of observations for label set - * @type {number} */ get count() { return this._countValues.get(this._key); @@ -950,9 +961,9 @@ class SummaryChild { /** * Observe a given value and increment matching buckets for label set * - * @param {number} value - number to count into summary. + * @param value - number to count into summary. */ - observe(value) { + observe(value: number) { this._sumValues.set(this._key, this._sumValues.get(this._key) + value); this._countValues.set(this._key, this._countValues.get(this._key) + 1); } @@ -960,7 +971,7 @@ class SummaryChild { /** * Start a timer for observing a duration for label set * - * @returns {function()} + * @returns * function that when called will store the duration in seconds from * when the timer was started into the metric. */ @@ -981,7 +992,7 @@ class SummaryChild { * and average size of values for. A common case for this is request * duration, for example: * - * ```js + * ```ts * const requestDuration = new Summary( * "app_request_duration_seconds", * "Time to process app requests", @@ -1010,7 +1021,7 @@ class SummaryChild { * child summary with label values set has to be retrieved with the * `.labels()` method. For example: * - * ```js + * ```ts * const requestDuration = new Summary( * "app_request_duration_seconds", * "Time to process app requests", @@ -1034,7 +1045,7 @@ class SummaryChild { * that will be used. This is done by calling `.labels()` for each * combination, for example: * - * ```js + * ```ts * for (let endpoint of ["/status", "/api", ...) { * requestDuration.labels(endpoint); * } @@ -1044,27 +1055,37 @@ class SummaryChild { * series that need to be stored and processed. You should carefully * evaluate which labels you actually need as resource usage for a metric * increases exponentially with the number of labels used. - * - * @static */ -class Summary extends LabeledCollector { +export class Summary extends LabeledCollector { + _sumValues: Map; + _countValues: Map; + protected _defaultChild: SummaryChild | null; + /** * Create optionally labeled summary * - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object=} options - options for collector. - * @param {Array=} options.labels - + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. + * @param options.labels - * Labels for this metric, defaults to no labels. - * @param {boolean=} options.register - + * @param options.register - * If true registers this collector with the default registry. * Defaults to true. - * @param {function()=} options.callback - + * @param options.callback - * Possibly async function that is called when the metric is * collected. The collector being collected is passed as the * argument. */ - constructor(name, help, options = {}) { + constructor( + name: string, + help: string, + options: { + labels?: string[], + register?: boolean, + callback?: (collector: Summary) => void | Promise, + } = {}, + ) { super("summary", name, help, options, SummaryChild); this._sumValues = new Map(); @@ -1107,9 +1128,9 @@ class Summary extends LabeledCollector { * Observe a given value and increment matching buckets * * Note: Only works if this is an unlabeled collector. - * @param {number} value - number to count into histogram buckets. + * @param value - number to count into histogram buckets. */ - observe(value) { + observe(value: number) { this._defaultChild.observe(value); } @@ -1117,7 +1138,7 @@ class Summary extends LabeledCollector { * Start a timer for observing a duration * * Note: Only works if this is an unlabeled collector. - * @returns {function()} + * @returns * function that when called will store the duration in seconds from * when the timer was started into the metric. */ @@ -1125,13 +1146,13 @@ class Summary extends LabeledCollector { return this._defaultChild.startTimer(); } - remove(...labels) { + remove(...labels: LabelValues) { let key = super.remove(...labels); this._sumValues.delete(key); this._countValues.delete(key); } - removeAll(labels) { + removeAll(labels: Record) { super.removeAll(labels); removeMatchingLabels(this._sumValues, labels); removeMatchingLabels(this._countValues, labels); @@ -1144,7 +1165,7 @@ class Summary extends LabeledCollector { } } -function formatValue(value) { +function formatValue(value: number) { if (value === Infinity) { return "+Inf"; } @@ -1154,7 +1175,7 @@ function formatValue(value) { return value.toString(); } -function formatBucketKey(bucket, key) { +function formatBucketKey(bucket: number, key: string) { return `${key === "" ? "" : `${key},`}le="${formatValue(bucket)}"`; } @@ -1162,7 +1183,13 @@ function formatBucketKey(bucket, key) { * Child histogram holding the buckets for a single label set. */ class HistogramChild { - constructor(collector, key) { + protected _bucketValues: Map; + protected _sumValues: Map; + protected _countValues: Map; + protected _key: string + protected _bucketKeys: Map; + + constructor(collector: Histogram, key: string) { this._bucketValues = collector._bucketValues; this._sumValues = collector._sumValues; this._countValues = collector._countValues; @@ -1182,7 +1209,6 @@ class HistogramChild { /** * Mapping of bucket upper bounds to count of observations for label set - * @type {Map} */ get buckets() { return new Map( @@ -1194,7 +1220,6 @@ class HistogramChild { /** * Sum of all observations for label set - * @type {number} */ get sum() { return this._sumValues.get(this._key); @@ -1202,7 +1227,6 @@ class HistogramChild { /** * Count of observations for label set - * @type {number} */ get count() { return this._countValues.get(this._key); @@ -1211,9 +1235,9 @@ class HistogramChild { /** * Observe a given value and increment matching buckets for label set * - * @param {number} value - number to count into histogram buckets. + * @param value - number to count into histogram buckets. */ - observe(value) { + observe(value: number) { for (let [bound, key] of this._bucketKeys) { if (value <= bound) { this._bucketValues.set(key, this._bucketValues.get(key) + 1); @@ -1227,7 +1251,7 @@ class HistogramChild { /** * Start a timer for observing a duration for label set * - * @returns {function()} + * @returns * function that when called will store the duration in seconds from * when the timer was started into the metric. */ @@ -1248,7 +1272,7 @@ class HistogramChild { * reports a metric that you want insight into the distribution of values * for. A common case for this is request duration, for example: * - * ```js + * ```ts * const requestDuration = new Histogram( * "app_request_duration_seconds", * "Time to process app requests", @@ -1276,7 +1300,7 @@ class HistogramChild { * child histogram with label values set has to be retrieved with the * `.labels()` method. For example: * - * ```js + * ```ts * const requestDuration = new Histogram( * "app_request_duration_seconds", * "Time to process app requests", @@ -1300,7 +1324,7 @@ class HistogramChild { * will be used. This is done by calling `.labels()` for each combination, * for example: * - * ```js + * ```ts * for (let endpoint of ["/status", "/api", ...) { * requestDuration.labels(endpoint); * } @@ -1312,44 +1336,51 @@ class HistogramChild { * time series being made.) You should carefully evaluate which labels you * actually need as resource usage for a metric increases exponentially with * the number of labels used. - * - * @static */ -class Histogram extends LabeledCollector { +export class Histogram extends LabeledCollector { + _bucketValues: Map; + _sumValues: Map; + _countValues: Map; + _buckets: number[]; + protected _defaultChild: HistogramChild | null; + /** * Create optionally labeled histogram * - * @param {string} name - Name of the metric. - * @param {string} help - Help text for metric. - * @param {Object=} options - options for collector. - * @param {Array=} options.buckets - + * @param name - Name of the metric. + * @param help - Help text for metric. + * @param options - options for collector. + * @param options.buckets - * Buckets to use for the histogram. This is an array of inclusive * upper bounds. Values observed will increment a bucket if it is * less than or equal to the upper bound. - * @param {Array=} options.labels - + * @param options.labels - * Labels for this metric, defaults to no labels. - * @param {boolean=} options.register - + * @param options.register - * If true registers this collector with the default registry. * Defaults to true. - * @param {function()=} options.callback - + * @param options.callback - * Possibly async function that is called when the metric is * collected. The collector being collected is passed as the * argument. */ - constructor(name, help, options = {}) { + constructor( + name: string, + help: string, + options: { + buckets?: number[], + labels?: string[], + register?: boolean, + callback?: (collector: Histogram) => void | Promise, + } = {}, + ) { // These defaults are taken from the Python Prometheus client - let buckets = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, Infinity]; + let { + buckets = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, Infinity], + ...rest + } = options; - let parentOptions = { _reservedLabels: ["le"] }; - for (let [key, value] of Object.entries(options)) { - if (key === "buckets") { - buckets = value; - } else { - parentOptions[key] = value; - } - } - - super("histogram", name, help, parentOptions, HistogramChild); + super("histogram", name, help, { ...rest, _reservedLabels: ["le"] }, HistogramChild); if (buckets.slice(-1)[0] !== Infinity) { buckets = [...buckets, Infinity]; @@ -1378,7 +1409,6 @@ class Histogram extends LabeledCollector { * Mapping of bucket upper bounds to count of observations for label set * * Note: Only available if this is an unlabeled collector. - * @type {Map} */ get buckets() { return this._defaultChild.buckets; @@ -1388,7 +1418,6 @@ class Histogram extends LabeledCollector { * Sum of all observations for label set * * Note: Only available if this is an unlabeled collector. - * @type {number} */ get sum() { return this._defaultChild.sum; @@ -1398,7 +1427,6 @@ class Histogram extends LabeledCollector { * Count of observations for label set * * Note: Only available if this is an unlabeled collector. - * @type {number} */ get count() { return this._defaultChild.count; @@ -1408,9 +1436,8 @@ class Histogram extends LabeledCollector { * Observe a given value and increment matching buckets * * Note: Only works if this is an unlabeled collector. - * @param {number} value - number to count into histogram buckets. */ - observe(value) { + observe(value: number) { this._defaultChild.observe(value); } @@ -1418,7 +1445,7 @@ class Histogram extends LabeledCollector { * Start a timer for observing a duration * * Note: Only works if this is an unlabeled collector. - * @returns {function()} + * @returns * function that when called will store the duration in seconds from * when the timer was started into the metric. */ @@ -1426,7 +1453,7 @@ class Histogram extends LabeledCollector { return this._defaultChild.startTimer(); } - remove(...labels) { + remove(...labels: LabelValues) { let key = super.remove(...labels); for (let bucket of this._buckets) { let bucketKey = formatBucketKey(bucket, key); @@ -1436,7 +1463,7 @@ class Histogram extends LabeledCollector { this._countValues.delete(key); } - removeAll(labels) { + removeAll(labels: Record) { super.removeAll(labels); for (let bucket of this._buckets) { removeMatchingLabels( @@ -1462,13 +1489,13 @@ class Histogram extends LabeledCollector { * `start` and with each subsequent number being equal to the previous * one plus `width`. * - * @param {number} start - Number to start buckets at. - * @param {number} width - Distance between ecah bucket. - * @param {number} count - Number of buckets. - * @returns {Array} array of buckets. + * @param start - Number to start buckets at. + * @param width - Distance between ecah bucket. + * @param count - Number of buckets. + * @returns array of buckets. */ - static linear(start, width, count) { - let buckets = []; + static linear(start: number, width: number, count: number) { + let buckets: number[] = []; for (let i = 0; i < count; i++) { buckets.push(start); start += width; @@ -1484,13 +1511,13 @@ class Histogram extends LabeledCollector { * `start` and with each subsequent number being equal to the previous * multiplied with `factor`. * - * @param {number} start - Number to start buckets at. - * @param {number} factor - Ratio between each bucket. - * @param {number} count - Number of buckets. - * @returns {Array} array of buckets. + * @param start - Number to start buckets at. + * @param factor - Ratio between each bucket. + * @param count - Number of buckets. + * @returns array of buckets. */ - static exponential(start, factor, count) { - let buckets = []; + static exponential(start: number, factor: number, count: number) { + let buckets: number[] = []; for (let i = 0; i < count; i++) { buckets.push(start); start *= factor; @@ -1511,7 +1538,7 @@ class Histogram extends LabeledCollector { * registring them to the default registry and then adding them to your own * registry, for example: * - * ```js + * ```ts * const myRegistry = new CollectorRegistry(); * const myCounter = new Counter( "a_counter", "A counter.", { register: false }); * myRegistry.register(myCounter); @@ -1523,20 +1550,14 @@ class Histogram extends LabeledCollector { * The same collector can be registered to multiple registries. This may be * used to implement responding with different sets of metrics depending on * what is requested. - * - * @static */ -class CollectorRegistry { - constructor() { - this.collectors = []; - } +export class CollectorRegistry { + collectors: Collector[] = []; /** * Collect metrics from all registered collectors. - * - * @returns {*} Async iterator of {@link CollectorResult}. */ - async* collect() { + async* collect(): AsyncIterable { for (let collector of this.collectors) { for await (let result of collector.collect()) { yield result; @@ -1547,9 +1568,9 @@ class CollectorRegistry { /** * Add collector to the registry. * - * @param {lib/prometheus.Collector} collector - Collector to add. + * @param collector - Collector to add. */ - register(collector) { + register(collector: Collector) { let index = this.collectors.lastIndexOf(collector); if (index !== -1) { throw new Error( @@ -1563,9 +1584,9 @@ class CollectorRegistry { /** * Remove collector from the registry. * - * @param {lib/prometheus.Collector} collector - Collector to remove. + * @param collector - Collector to remove. */ - unregister(collector) { + unregister(collector: Collector) { let index = this.collectors.lastIndexOf(collector); if (index === -1) { throw new Error( @@ -1579,14 +1600,14 @@ class CollectorRegistry { defaultRegistry = new CollectorRegistry(); -function escapeHelp(help) { +function escapeHelp(help: string) { return help .replace(/\\/g, "\\\\") .replace(/\n/g, "\\n") ; } -async function* expositionLines(resultsIterator) { +async function* expositionLines(resultsIterator: AsyncIterable) { let first = true; for await (let result of resultsIterator) { if (first) { @@ -1634,46 +1655,49 @@ async function* expositionLines(resultsIterator) { * app.get("/metrics", (req, res, next) => getMetrics(req, res).catch(next)); * app.listen(9100); * - * @param {*} resultsIterator - + * @param resultsIterator - * Asynchronously itreable of {@link CollectorResult} results to create * exposition for. Defaults to collecting results from {@link - * module:lib.defaultRegistry}. - * @returns {string} Prometheus exposition. + * defaultRegistry}. + * @returns Prometheus exposition. * - * @property {string} exposition.contentType + * @property exposition.contentType * HTTP Content-Type for the exposition format that's implemented - * - * @static */ -async function exposition(resultsIterator = defaultRegistry.collect()) { +export async function exposition(resultsIterator = defaultRegistry.collect()) { let lines = ""; for await (let line of expositionLines(resultsIterator)) { lines += line; } return lines; } - exposition.contentType = "text/plain; version=0.0.4"; /** * Serialize CollectorResult into a plain object * - * Converts a {@link module:lib~CollectorResult} into a plain + * Converts a {@link CollectorResult} into a plain * object form that can be stringified to JSON. * - * @param {module:lib~CollectorResult} result - + * @param result - * Result to serialize into plain object form. - * @param {Object} options - Options for controlling the serialization. - * @param {string} options.addLabels - + * @param options - Options for controlling the serialization. + * @param options.addLabels - * Additional labels to append to each value. This may be used if * multiple sources are combined have the same metric and a qualifier is * needed to make sure the label sets are unique. - * @param {string} options.metricName - Override metric name of the result. - * @param {string} options.metricHelp - Override metric help of the result. - * @returns {Object} plain object form of the result. - * @static + * @param options.metricName - Override metric name of the result. + * @param options.metricHelp - Override metric help of the result. + * @returns plain object form of the result. */ -function serializeResult(result, options = {}) { +export function serializeResult( + result: CollectorResult, + options: { + addLabels?: string, + metricName?: string, + metricHelp?: string, + } = {} +) { let addLabels = null; let metricName = result.metric.name; let metricHelp = result.metric.help; @@ -1689,7 +1713,7 @@ function serializeResult(result, options = {}) { } } - let samples; + let samples: Map>; if (addLabels === null) { samples = result.samples; addLabels = {}; @@ -1717,20 +1741,23 @@ function serializeResult(result, options = {}) { help: metricHelp, labels: [...result.metric.labels, ...Object.keys(addLabels)], }, - samples: [...samples].map(([name, metricSamples]) => [name, [...metricSamples]]), + samples: [...samples].map( + ([name, metricSamples]) => [name, [...metricSamples]] as [string, [string, number][]] + ), }; } /** * Deserialize CollectorResult from plain object * - * Reverse counterpart to {@link module:lib.serializeResult}. + * Reverse counterpart to {@link serializeResult}. * - * @param {Object} serializedResult - Previously serialized result object. - * @returns {module:lib~CollectorResult} deserialized result. - * @static + * @param serializedResult - Previously serialized result object. + * @returns deserialized result. */ -function deserializeResult(serializedResult) { +export function deserializeResult( + serializedResult: ReturnType +): CollectorResult { return { metric: new Metric( serializedResult.metric.type, @@ -1748,61 +1775,43 @@ function deserializeResult(serializedResult) { /** * Default collectors provided by this library - * @type {Object} */ -let defaultCollectors = {}; -defaultCollectors.processCpuSecondsTotal = new Gauge( - "process_cpu_seconds_total", - "Total user and system CPU time spent in seconds.", - { - callback: async function(collector) { - let usage = process.cpuUsage(); - collector.set((usage.user + usage.system) / 1e6); +export const defaultCollectors = { + processCpuSecondsTotal: new Gauge( + "process_cpu_seconds_total", + "Total user and system CPU time spent in seconds.", + { + callback: function(collector) { + let usage = process.cpuUsage(); + collector.set((usage.user + usage.system) / 1e6); + }, }, - }, -); + ), -defaultCollectors.processResidentMemoryBytes = new Gauge( - "process_resident_memory_bytes", - "Resident memory size in bytes.", - { - callback: function(collector) { - collector.set(process.memoryUsage().rss); + processResidentMemoryBytes: new Gauge( + "process_resident_memory_bytes", + "Resident memory size in bytes.", + { + callback: function(collector) { + collector.set(process.memoryUsage().rss); + }, }, - }, -); + ), -defaultCollectors.processHeapBytes = new Gauge( - "process_heap_bytes", - "Process heap size in bytes.", - { - callback: function(collector) { - collector.set(process.memoryUsage().heapUsed); + processHeapBytes: new Gauge( + "process_heap_bytes", + "Process heap size in bytes.", + { + callback: function(collector) { + collector.set(process.memoryUsage().heapUsed); + }, }, - }, -); + ), + + processStartTimeSeconds: new Gauge( + "process_start_time_seconds", + "Start time of the process since unix epoch in seconds." + ), +} as const; -defaultCollectors.processStartTimeSeconds = new Gauge( - "process_start_time_seconds", - "Start time of the process since unix epoch in seconds." -); defaultCollectors.processStartTimeSeconds.setToCurrentTime(); - - -module.exports = { - Counter, - Gauge, - Summary, - Histogram, - CollectorRegistry, - Collector, - LabeledCollector, - ValueCollector, - Metric, - - exposition, - defaultRegistry, - defaultCollectors, - serializeResult, - deserializeResult, -}; diff --git a/packages/lib/src/schema.js b/packages/lib/src/schema.ts similarity index 59% rename from packages/lib/src/schema.js rename to packages/lib/src/schema.ts index 63c2f5c9..d06fdfc2 100644 --- a/packages/lib/src/schema.js +++ b/packages/lib/src/schema.ts @@ -2,8 +2,7 @@ * JSON schemas used for validating link messages * @module lib/schema */ -"use strict"; -const Ajv = require("ajv"); +import Ajv, { JSONSchemaType, ValidateFunction } from "ajv"; const ajv = new Ajv({ // These are duplicated in scripts/compile_validator allowUnionTypes: true, @@ -14,17 +13,16 @@ const ajv = new Ajv({ /** * Compile JSON schema into validator - * @function - * @param {Object} schema - JSON schema to create validator for. - * @returns {Function} Validator for the schema. + * @param schema - JSON schema to create validator for. + * @returns Validator for the schema. */ -function compile(schema) { +export function compile(schema: JSONSchemaType) { if (typeof global !== "object" || !global.lazySchemaCompilation) { - return ajv.compile(schema); + return ajv.compile(schema); } - let doValidate; - function validate(data) { + let doValidate: ValidateFunction; + const validate = >function (data) { if (!doValidate) { doValidate = ajv.compile(schema); } @@ -32,9 +30,5 @@ function compile(schema) { validate.errors = doValidate.errors; return result; } - return validate; + return validate } - -module.exports = { - compile, -}; diff --git a/packages/lib/src/shared_commands.js b/packages/lib/src/shared_commands.ts similarity index 72% rename from packages/lib/src/shared_commands.js rename to packages/lib/src/shared_commands.ts index c641d1df..ada6ac4a 100644 --- a/packages/lib/src/shared_commands.js +++ b/packages/lib/src/shared_commands.ts @@ -3,15 +3,15 @@ * @module lib/shared_commands */ "use strict"; -const path = require("path"); +import path from "path"; -const libConfig = require("./config"); -const libFileOps = require("./file_ops"); -const { logger } = require("./logging"); -const libHelpers = require("./helpers"); +import * as libConfig from "./config"; +import * as libFileOps from "./file_ops"; +import { logger } from "./logging"; +import * as libHelpers from "./helpers"; -function print(...content) { +function print(...content: any[]) { // eslint-disable-next-line no-console console.log(...content); } @@ -22,9 +22,9 @@ function print(...content) { * Can be passed to yargs.command to implement a plugin list management * command. Use handlePluginCommand to do the requested action. * - * @param {Object} yargs - yargs command builder. + * @param yargs - yargs command builder. */ -function pluginCommand(yargs) { +export function pluginCommand(yargs: any) { yargs .command("add ", "Add plugin by require path") .command("remove ", "Remove plugin by name") @@ -40,20 +40,24 @@ function pluginCommand(yargs) { * * Handle the actions that are made available by pluginCommand. * - * @param {Object} args - yargs args object. - * @param {Map} pluginList - Current list of plugins. - * @param {string} pluginListPath - Path to plugin list config file. + * @param args - yargs args object. + * @param pluginList - Current list of plugins. + * @param pluginListPath - Path to plugin list config file. */ -async function handlePluginCommand(args, pluginList, pluginListPath) { +export async function handlePluginCommand( + args: Record, + pluginList: Map, + pluginListPath: string +) { let command = args._[1]; if (command === "add") { - let pluginPath = args.path; + let pluginPath = args.path as string; if (/^\.\.?[\/\\]/.test(pluginPath)) { pluginPath = path.resolve(pluginPath); } - let pluginInfo; + let pluginInfo: { name: string }; try { // eslint-disable-next-line node/global-require pluginInfo = require(path.posix.join(pluginPath, "info")); @@ -74,7 +78,7 @@ async function handlePluginCommand(args, pluginList, pluginListPath) { print(`Added ${pluginInfo.name}`); } else if (command === "remove") { - if (!pluginList.delete(args.name)) { + if (!pluginList.delete(args.name as string)) { logger.error(`Plugin with name ${args.name} does not exist`); process.exitCode = 1; return; @@ -97,11 +101,11 @@ async function handlePluginCommand(args, pluginList, pluginListPath) { * Can be passed to yargs.command to implement a config command. Use * handleConfigCommand to do the requested action. * - * @param {Object} yargs - yargs command builder. + * @param yargs - yargs command builder. */ -function configCommand(yargs) { +export function configCommand(yargs: any) { yargs - .command("set [value]", "Set config field", yargs => { + .command("set [value]", "Set config field", (yargs: any) => { yargs.options({ "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, }); @@ -119,15 +123,19 @@ function configCommand(yargs) { * * Handle the actions that are made available by configCommand. * - * @param {Object} args - yargs args object. - * @param {module:lib.Config} instance - Config instance. - * @param {string} configPath - Path to configuration file. + * @param args - yargs args object. + * @param instance - Config instance. + * @param configPath - Path to configuration file. */ -async function handleConfigCommand(args, instance, configPath) { +export async function handleConfigCommand( + args: Record, + instance: libConfig.Config, + configPath: string +) { let command = args._[1]; if (command === "list") { - for (let GroupClass of instance.constructor.groups.values()) { + for (let GroupClass of (instance as any).constructor.groups.values()) { for (let def of GroupClass.definitions.values()) { let value = instance.get(def.fullName); print(`${def.fullName} ${JSON.stringify(value)}`); @@ -136,7 +144,7 @@ async function handleConfigCommand(args, instance, configPath) { } else if (command === "show") { try { - print(instance.get(args.field)); + print(instance.get(args.field as string)); } catch (err) { if (err instanceof libConfig.InvalidField) { logger.error(err.message); @@ -154,7 +162,7 @@ async function handleConfigCommand(args, instance, configPath) { } try { - instance.set(args.field, args.value); + instance.set(args.field as string, args.value); await libFileOps.safeOutputFile(configPath, JSON.stringify(instance.serialize(), null, 4)); } catch (err) { if (err instanceof libConfig.InvalidField || err instanceof libConfig.InvalidValue) { @@ -165,12 +173,3 @@ async function handleConfigCommand(args, instance, configPath) { } } } - - -module.exports = { - pluginCommand, - handlePluginCommand, - - configCommand, - handleConfigCommand, -}; diff --git a/packages/lib/src/stream.ts b/packages/lib/src/stream.ts index a35185b2..5e2ab36c 100644 --- a/packages/lib/src/stream.ts +++ b/packages/lib/src/stream.ts @@ -138,7 +138,7 @@ type ReadStreamOptions = Parameters[1]; * Options to pass to fs.createReadStream. Values passed for `fd` and * `fs` will be ignored. */ -export async function createReverseReadStream(path: string, options: ReadStreamOptions) { +export async function createReverseReadStream(path: string, options?: ReadStreamOptions) { if (typeof options === "string") { options = { encoding: options }; } diff --git a/packages/lib/src/users.js b/packages/lib/src/users.ts similarity index 83% rename from packages/lib/src/users.js rename to packages/lib/src/users.ts index 5e505fc4..40d93b10 100644 --- a/packages/lib/src/users.js +++ b/packages/lib/src/users.ts @@ -7,24 +7,23 @@ * @module lib/users */ "use strict"; -const libErrors = require("./errors"); +import * as libErrors from "./errors"; -const PlayerStats = require("./PlayerStats"); +import PlayerStats from "./PlayerStats"; /** * Represents a permission that can be granted - * @static */ -class Permission { - constructor(name, title, description, grantByDefault) { - this.name = name; - this.title = title; - this.description = description; - this.grantByDefault = grantByDefault; - } +export class Permission { + constructor( + public name: string, + public title: string, + public description: string, + public grantByDefault: boolean, + ) { } } -const permissions = new Map(); +export const permissions = new Map(); /** * Define a new user permission for Clusterio @@ -33,15 +32,25 @@ const permissions = new Map(); * granted to roles, and checked on users. Name should be `lower_case` * style and start with the plugin name followed by a dot. * - * @param {Object} def - The definition for the permission. - * @param {string} def.name - The internal name for the permission. - * @param {string} def.title - User facing name for permission. - * @param {string} def.description - User facing description. - * @param {boolean} def.grantByDefault - + * @param def - The definition for the permission. + * @param def.name - The internal name for the permission. + * @param def.title - User facing name for permission. + * @param def.description - User facing description. + * @param def.grantByDefault - * If true this role is granted by the generated Player role by default. * */ -function definePermission({ name, title, description, grantByDefault=false }) { +export function definePermission({ + name, + title, + description, + grantByDefault = false, +}: { + name: string, + title: string, + description: string, + grantByDefault?: boolean, +}) { if (typeof name !== "string" || !name.length) { throw new Error("Expected name to be a non-empty string"); } @@ -64,10 +73,14 @@ function definePermission({ name, title, description, grantByDefault=false }) { /** * Represents a collection of granted permissions - * @static */ -class Role { - constructor(serializedRole) { +export class Role { + id: number; + name: string; + description: string; + permissions: Set; + + constructor(serializedRole: ReturnType) { this.id = serializedRole.id; this.name = serializedRole.name; this.description = serializedRole.description; @@ -98,14 +111,18 @@ class Role { * * Ensures the role with id 0 exisits and has the core.admin permission. * - * @param {Map} roles - role storage to modify. - * @returns {module:lib.Role} the default admin role - * @static + * @param roles - role storage to modify. + * @returns the default admin role */ -function ensureDefaultAdminRole(roles) { +export function ensureDefaultAdminRole(roles: Map) { let admin = roles.get(0); if (!admin) { - admin = new Role({ id: 0, name: "Cluster Admin", description: "Cluster wide administrator." }); + admin = new Role({ + id: 0, + name: "Cluster Admin", + description: "Cluster wide administrator.", + permissions: [], + }); roles.set(0, admin); } admin.permissions.add("core.admin"); @@ -119,13 +136,17 @@ function ensureDefaultAdminRole(roles) { * assigned by default. Note that this may not be the same role as the one * that is assigned to new users by default. * - * @param {Map} roles - role storage to modify. - * @static + * @param roles - role storage to modify. */ -function ensureDefaultPlayerRole(roles) { +export function ensureDefaultPlayerRole(roles: Map) { let player = roles.get(1); if (!player) { - player = new Role({ id: 1, name: "Player", description: "Default player role." }); + player = new Role({ + id: 1, + name: "Player", + description: "Default player role.", + permissions: [], + }); roles.set(1, player); } player.grantDefaultPermissions(); @@ -136,56 +157,42 @@ function ensureDefaultPlayerRole(roles) { * Represeents a user in the cluster * * Holds data about a Factorio user in the cluster. - * @static */ -class User { - constructor(serializedUser, loadedRoles) { - /** - * Factorio user name. - * @type {string} - */ +export class User { + /** Factorio user name. */ + name: string; + /** Instances this user is online on. */ + instances: Set; + /** Unix time in seconds the user token must be issued after to be valid. */ + tokenValidAfter: number; + /** True if the user is promoted to admin on the Factorio instances. */ + isAdmin: boolean; + /** True if the user is whitelisted on the Factorio instances. */ + isWhitelisted: boolean; + /** True if the user is banned from Factorio instances. */ + isBanned: boolean; + /** Reason for being banned. Ignored if isBanned is false. */ + banReason: string; + /** Roles this user has */ + roles: Set; + /** Per instance statistics for the player this user account is tied to. */ + instanceStats: Map; + /** Combined statistics for the player this user account is tied to. */ + playerStats: PlayerStats; + /** True if this user object has been removed from the cluster. */ + isDeleted: boolean; + + constructor( + serializedUser: ReturnType, + loadedRoles: Map + ) { this.name = serializedUser.name; - - /** - * Instances this user is online on. - * @type {Set} - */ this.instances = new Set(); - - /** - * Unix time in seconds the user token must be issued after to be valid. - * @type {number} - */ this.tokenValidAfter = serializedUser.token_valid_after || 0; - - /** - * True if the user is promoted to admin on the Factorio instances. - * @type {boolean} - */ this.isAdmin = Boolean(serializedUser.is_admin); - - /** - * True if the user is whitelisted on the Factorio instances. - * @type {boolean} - */ this.isWhitelisted = Boolean(serializedUser.is_whitelisted); - - /** - * True if the user is banned from Factorio instances. - * @type {boolean} - */ this.isBanned = Boolean(serializedUser.is_banned); - - /** - * Reason for being banned. Ignored if isBanned is false. - * @type {string} - */ this.banReason = serializedUser.ban_reason || ""; - - /** - * Roles this user has - * @type {Set} - */ this.roles = new Set(); if (serializedUser.roles) { for (let roleId of serializedUser.roles) { @@ -195,32 +202,26 @@ class User { } } } - - /** - * Per instance statistics for the player this user account is tied to. - * @type {Map} - */ this.instanceStats = new Map( (serializedUser.instance_stats ? serializedUser.instance_stats : []).map( ([id, stats]) => [id, new PlayerStats(stats)] ) ); - - /** - * Combined statistics for the player this user account is tied to. - * @type {module:lib.PlayerStats} - */ this.playerStats = this._calculatePlayerStats(); - - /** - * True if this user object has been removed from the cluster. - * @type {boolean} - */ this.isDeleted = false; } serialize() { - let serialized = { + let serialized: { + name: string, + roles?: number[], + token_valid_after?: number, + is_admin?: boolean, + is_whitelisted?: boolean, + is_banned?: boolean, + ban_reason?: string, + instance_stats?: [number, object][], + } = { name: this.name, }; @@ -272,11 +273,11 @@ class User { * permission. If the permission is not granted for the user a * "Permission denied" error is thrown. * - * @param {string} permission - The permission to check for. + * @param permission - The permission to check for. * @throws {Error} If the given permission does not exist. - * @throws {module:lib.PermissionError} if the user does noh have the given permission. + * @throws {libErrors.PermissionError} if the user does noh have the given permission. */ - checkPermission(permission) { + checkPermission(permission: string) { if (!permissions.has(permission)) { throw new Error(`permission ${permission} does not exist`); } @@ -290,12 +291,12 @@ class User { throw new libErrors.PermissionError("Permission denied"); } - notifyJoin(instance_id) { + notifyJoin(instance_id: number) { this.instances.add(instance_id); User.onlineUsers.add(this); } - notifyLeave(instance_id) { + notifyLeave(instance_id: number) { this.instances.delete(instance_id); if (!this.instances.size) { User.onlineUsers.delete(this); @@ -321,12 +322,11 @@ class User { } return playerStats; } + /** + * Set of users currently online in the cluster. + */ + static onlineUsers = new Set(); } -/** - * Set of users currently online in the cluster. - * @type {module:lib.User} - */ -User.onlineUsers = new Set(); // Definitions for the built in permissions used in Clusterio. // description should answer "this permission allows you to ___" @@ -677,14 +677,3 @@ definePermission({ title: "Dump WebSocket", description: "Dump all WebSocket communicatation from the controller.", }); - -module.exports = { - Permission, - Role, - User, - - permissions, - definePermission, - ensureDefaultAdminRole, - ensureDefaultPlayerRole, -}; diff --git a/packages/lib/src/zip_ops.js b/packages/lib/src/zip_ops.ts similarity index 75% rename from packages/lib/src/zip_ops.js rename to packages/lib/src/zip_ops.ts index 39cb1bb0..f69f63e8 100644 --- a/packages/lib/src/zip_ops.js +++ b/packages/lib/src/zip_ops.ts @@ -1,4 +1,4 @@ -"use strict"; +import type JSZip from "jszip"; /** * Returns the root folder in the zip file @@ -6,13 +6,10 @@ * Returns the name of the folder that all files in the zip file is * contained in. Throws an error if there are multiple such folders. * - * @param {JSZip} zip - Zip to search through. - * @returns {string} name of the root folder. - * @memberof module:lib - * @private - * @inner + * @param zip - Zip to search through. + * @returns name of the root folder. */ -function findRoot(zip) { +export function findRoot(zip: JSZip) { let root = null; zip.forEach((relativePath, file) => { let index = relativePath.indexOf("/"); @@ -34,7 +31,3 @@ function findRoot(zip) { return root; } - -module.exports = { - findRoot, -};