diff --git a/packages/lib/package.json b/packages/lib/package.json index 40f530a5..519495da 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -5,7 +5,7 @@ "repository": "https://github.com/clusterio/clusterio", "license": "MIT", "scripts": { - "prepare": "pnpm run compile_validator && tsc || true", + "prepare": "pnpm run compile_validator && tsc", "compile_validator": "node scripts/compile_validator.js" }, "main": "dist/index.js", diff --git a/packages/lib/src/command.ts b/packages/lib/src/command.ts index f0caf2f5..1577b486 100644 --- a/packages/lib/src/command.ts +++ b/packages/lib/src/command.ts @@ -162,6 +162,7 @@ export async function resolveHost(client: Link, hostName: string) { hostId = parseInt(hostName, 10); } else { let hosts = await client.sendTo("controller", new libData.HostListRequest()); + // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let host of hosts) { if (host.name === hostName) { hostId = host.id; @@ -193,6 +194,7 @@ export async function resolveInstance(client: Link, instanceName: string) { instanceId = parseInt(instanceName, 10); } else { let instances = await client.sendTo("controller", new libData.InstanceDetailsListRequest()); + // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let instance of instances) { if (instance.name === instanceName) { instanceId = instance.id; @@ -225,6 +227,7 @@ export async function resolveModPack(client: Link, modPackName: string) { modPackId = parseInt(modPackName, 10); } else { let modPacks = await client.sendTo("controller", new libData.ModPackListRequest()); + // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let modPack of modPacks) { if (modPack.name === modPackName) { modPackId = modPack.id; @@ -256,6 +259,7 @@ export async function retrieveRole(client: Link, roleName: string) { let resolvedRole: libData.RawRole; if (/^-?\d+$/.test(roleName)) { let roleId = parseInt(roleName, 10); + // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let role of roles) { if (role.id === roleId) { resolvedRole = role; @@ -264,6 +268,7 @@ export async function retrieveRole(client: Link, roleName: string) { } } else { + // @ts-ignore: TODO Remove when lib/data is migrated to ts for (let role of roles) { if (role.name === roleName) { resolvedRole = role; diff --git a/packages/lib/src/config/classes.js b/packages/lib/src/config/classes.ts similarity index 70% rename from packages/lib/src/config/classes.js rename to packages/lib/src/config/classes.ts index 1855b584..a05bff29 100644 --- a/packages/lib/src/config/classes.js +++ b/packages/lib/src/config/classes.ts @@ -1,11 +1,11 @@ // Configuration classes -"use strict"; +import events from "events"; -const events = require("events"); +import isDeepStrictEqual from "../is_deep_strict_equal"; +import { basicType } from"../helpers"; -const isDeepStrictEqual = require("../is_deep_strict_equal"); -const { basicType } = require("../helpers"); +export type ConfigLocation = "controller" | "host" | "instance" | "control"; /** * Invalid Access exception @@ -14,9 +14,8 @@ const { basicType } = require("../helpers"); * does not permitt this access is done. * * @extends Error - * @memberof module:lib */ -class InvalidAccess extends Error { }; +export class InvalidAccess extends Error { }; /** * Invalid Value exception @@ -25,9 +24,8 @@ class InvalidAccess extends Error { }; * field. * * @extends Error - * @memberof module:lib */ -class InvalidValue extends Error { }; +export class InvalidValue extends Error { }; /** * Invalid Field exception @@ -36,9 +34,8 @@ class InvalidValue extends Error { }; * attempted to be accessed. * * @extends Error - * @memberof module:lib */ -class InvalidField extends Error { }; +export class InvalidField extends Error { }; /** * Split the given string on the first instance of separator @@ -48,14 +45,12 @@ class InvalidField extends Error { }; * after the separator. Returns an array with the string and an empty * string if the separator is not present. * - * @param {string} separator - Separator to split string by. - * @param {string} string - String to split - * @returns {Array} string split on separator. - * @memberof module:lib - * @private - * @inner + * @param separator - Separator to split string by. + * @param string - String to split + * @returns string split on separator. + * @internal */ -function splitOn(separator, string) { +function splitOn(separator: string, string: string) { let index = string.indexOf(separator); if (index === -1) { return [string, ""]; @@ -63,6 +58,9 @@ function splitOn(separator, string) { return [string.slice(0, index), string.slice(index + separator.length)]; } +type SerializedConfig = ReturnType; +type SerializedGroup = ReturnType; + /** * Collection of config groups * @@ -76,34 +74,38 @@ function splitOn(separator, string) { * prev. * * @extends events.EventEmitter - * - * @memberof module:lib */ -class Config extends events.EventEmitter { +export class Config extends events.EventEmitter { + declare static _finalized: boolean; + declare static _initialized: boolean; + declare static _groups: Map; + + _initialized = false; + _groups = new Map(); + _unknownGroups = new Map(); + /** * Create a new instance of the given config * - * @param {string} location - + * @param location - * Location to evaluate access for this instance from */ - constructor(location) { + constructor( + public location: ConfigLocation, + ) { super(); - if (!this.constructor._finalized) { + if (!(this.constructor as typeof Config)._finalized) { throw new Error(`Cannot instantiate incomplete Config class ${this.constructor.name}`); } if (typeof location !== "string") { throw new Error("location must be a string"); } - - this.location = location; - this._groups = new Map(); - this._unknownGroups = new Map(); } async init() { - for (let [name, GroupClass] of this.constructor._groups) { + for (let [name, GroupClass] of (this.constructor as typeof Config)._groups) { if (!this._groups.has(name)) { let group = new GroupClass(this); await group.init(); @@ -113,7 +115,7 @@ class Config extends events.EventEmitter { this._initialized = true; } - _validate(serializedConfig) { + _validate(serializedConfig: SerializedConfig) { if (basicType(serializedConfig) !== "object") { throw new Error(`Expected object, not ${basicType(serializedConfig)} for config`); } @@ -126,13 +128,16 @@ class Config extends events.EventEmitter { /** * Load config from a serialized object * - * @param {object} serializedConfig - Serialized config to load. - * @param {string=} location - Location used for access control. + * @param serializedConfig - Serialized config to load. + * @param location - Location used for access control. */ - async load(serializedConfig, location = this.location) { + async load( + serializedConfig: SerializedConfig, + location = this.location + ) { this._validate(serializedConfig); for (let serializedGroup of serializedConfig.groups) { - let GroupClass = this.constructor._groups.get(serializedGroup.name); + let GroupClass = (this.constructor as typeof Config)._groups.get(serializedGroup.name); if (!GroupClass) { this._unknownGroups.set(serializedGroup.name, serializedGroup); continue; @@ -149,11 +154,11 @@ class Config extends events.EventEmitter { /** * Serialize the config to a plain JavaScript object * - * @param {string=} location - Location used for access control. - * @returns {Object} JSON serializable representation of the config. + * @param location - Location used for access control. + * @returns JSON serializable representation of the config. */ serialize(location = this.location) { - let groups = [...this._unknownGroups.values()]; + let groups: SerializedGroup[] = [...this._unknownGroups.values()]; for (let group of this._groups.values()) { groups.push(group.serialize(location)); } @@ -167,11 +172,15 @@ class Config extends events.EventEmitter { * Updates all groups in the config with groups in the serialized * config passed. * - * @param {Object} serializedConfig - Output from .serialize(). - * @param {boolean} notify - Invoke fieldChanged events if true. - * @param {string=} location - Location used for access control. + * @param serializedConfig - Output from .serialize(). + * @param notify - Invoke fieldChanged events if true. + * @param location - Location used for access control. */ - update(serializedConfig, notify, location = this.location) { + update( + serializedConfig: SerializedConfig, + notify: boolean, + location = this.location + ) { this._validate(serializedConfig); for (let serializedGroup of serializedConfig.groups) { let group = this._groups.get(serializedGroup.name); @@ -186,11 +195,11 @@ class Config extends events.EventEmitter { /** * Check if field can be accessed from the given location * - * @param {string} name - Name of the field to check. - * @param {string=} location - Location used for access control. - * @returns {boolean} true if field is accessible + * @param name - Name of the field to check. + * @param location - Location used for access control. + * @returns true if field is accessible */ - canAccess(name, location = this.location) { + canAccess(name: string, location = this.location) { let [group, field] = splitOn(".", name); return this.group(group).canAccess(field, location); } @@ -198,11 +207,11 @@ class Config extends events.EventEmitter { /** * Get the value for a config field * - * @param {string} name - Name of field to get. - * @param {string=} location - Location used for access control. - * @returns {*} the value for the field. + * @param name - Name of field to get. + * @param location - Location used for access control. + * @returns the value for the field. */ - get(name, location = this.location) { + get(name: string, location = this.location) { let [group, field] = splitOn(".", name); return this.group(group).get(field, location); } @@ -210,11 +219,11 @@ class Config extends events.EventEmitter { /** * Set the value for a config field * - * @param {string} name - Name of field to set. - * @param {*} value - the value to set on field - * @param {string=} location - Location used for access control. + * @param name - Name of field to set. + * @param value - the value to set on field + * @param location - Location used for access control. */ - set(name, value, location = this.location) { + set(name: string, value: unknown, location = this.location) { let [group, field] = splitOn(".", name); this.group(group).set(field, value, location); } @@ -222,12 +231,12 @@ class Config extends events.EventEmitter { /** * Set property of an object value for a config field * - * @param {string} name - Name of field to set property on. - * @param {string} prop - Name of property to set on field. - * @param {*} value - the value to set the property to. - * @param {string=} location - Location used for access control. + * @param name - Name of field to set property on. + * @param prop - Name of property to set on field. + * @param value - the value to set the property to. + * @param location - Location used for access control. */ - setProp(name, prop, value, location = this.location) { + setProp(name: string, prop: string, value: unknown, location = this.location) { let [group, field] = splitOn(".", name); this.group(group).setProp(field, prop, value, location); } @@ -235,10 +244,10 @@ class Config extends events.EventEmitter { /** * Get the config group instance with the given name * - * @param {string} name - Name of group to get. - * @returns {module:lib.ConfigGroup} config group. + * @param name - Name of group to get. + * @returns config group. */ - group(name) { + group(name: string) { if (!this._initialized) { throw new Error(`${this.constructor.name} instance is uninitialized`); } @@ -266,7 +275,7 @@ class Config extends events.EventEmitter { * mave been registered with this config. Note that mutating this * object will lead to unexpected behaviour. * - * @returns {Map} config groups + * @returns config groups */ static get groups() { this._initSubclass(); @@ -287,9 +296,9 @@ class Config extends events.EventEmitter { * Adds the ConfigGroup to this config as a group that will get * initialized, loaded and serialized with this config class. * - * @param {module:lib.ConfigGroup} group - class to register. + * @param group - class to register. */ - static registerGroup(group) { + static registerGroup(group: typeof ConfigGroup) { this._initSubclass(); if (this._finalized) { @@ -308,6 +317,20 @@ class Config extends events.EventEmitter { } } +type FieldType = "boolean" | "string" | "number" | "object"; +interface FieldDefinition { + type: FieldType; + name: string; + fullName: string; + title?: string; + description?: string; + optional: boolean; + initial_value?: unknown | (() => unknown); + enum?: unknown[]; + restartRequired: boolean; + restartRequiredProps?: string[]; + access: ConfigLocation[]; +} /** * Collection of related config entries @@ -316,41 +339,47 @@ class Config extends events.EventEmitter { * to set invalid values for fields will result in an error thrown while * loading a config with invalid values will result in those being replaced * by the innitial value for those fields. - * - * @memberof module:lib */ -class ConfigGroup { +export class ConfigGroup { + declare static _finalized: boolean; + declare static _initialized: boolean; + declare static _definitions: Map; + declare static groupName: string; + declare static defaultAccess: ConfigLocation[]; + + _fields = new Map(); + _unknownFields = new Map(); + + /** * Creates a new config group. * * After the creation of the new config group you have to call * either .init() or .load() on it in order to fully initialize it. * - * @param {module:lib.Config} config - + * @param config - * Parent config for this group instance. */ - constructor(config) { - if (!this.constructor._finalized) { + constructor( + public config: Config + ) { + if (!(this.constructor as typeof ConfigGroup)._finalized) { throw new Error(`Cannot instantiate incomplete ConfigGroup class ${this.constructor.name}`); } - - this._fields = new Map(); - this._unknownFields = new Map(); - this.config = config; } /** * Check access for a field * - * @param {object} def - definition for for the field to check. - * @param {string} remote - location of remote access to check for. - * @param {boolean} error - throw on failure to pass check. - * @throws {module:lib.InvalidAccess} + * @param def - definition for for the field to check. + * @param remote - location of remote access to check for. + * @param error - throw on failure to pass check. + * @throws {InvalidAccess} * if access is not granted and error is true. - * @returns {boolean} true if access is granted + * @returns true if access is granted * @private */ - _checkAccess(def, remote, error) { + _checkAccess(def: FieldDefinition, remote: ConfigLocation, error: boolean) { for (let loc of [remote, this.config.location]) { if (!def.access.includes(loc)) { if (error) { @@ -370,7 +399,7 @@ class ConfigGroup { * config group. */ async init() { - for (let [name, def] of this.constructor._definitions) { + for (let [name, def] of (this.constructor as typeof ConfigGroup)._definitions) { if (this._fields.has(name)) { continue; } @@ -397,11 +426,11 @@ class ConfigGroup { * group and then initializes any possible missing fields from it to * their default values. * - * @param {Object} serializedGroup - + * @param serializedGroup - * Result from a previous call to .serialize(). - * @param {string=} location - Location used for access control. + * @param location - Location used for access control. */ - async load(serializedGroup, location = this.config.location) { + async load(serializedGroup: SerializedGroup, location = this.config.location) { this.update(serializedGroup, false, location); await this.init(); } @@ -412,18 +441,18 @@ class ConfigGroup { * Shortcut for `group.constructor.groupName` */ get name() { - return this.constructor.groupName; + return (this.constructor as typeof ConfigGroup).groupName; } /** * Check if field can be accessed from the given location * - * @param {string} name - Name of field to check. - * @param {string=} location - Location used for access control. - * @returns {boolean} true if field is accessible + * @param name - Name of field to check. + * @param location - Location used for access control. + * @returns true if field is accessible */ - canAccess(name, location = this.config.location) { - let def = this.constructor._definitions.get(name); + canAccess(name: string, location = this.config.location) { + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!def) { throw new InvalidField(`No field named '${name}'`); } @@ -433,12 +462,12 @@ class ConfigGroup { /** * Get value for field * - * @param {string} name - Name of field to get. - * @param {string=} location - Location used for access control. - * @returns {*} Value stored for the field. + * @param name - Name of field to get. + * @param location - Location used for access control. + * @returns Value stored for the field. */ - get(name, location = this.config.location) { - let def = this.constructor._definitions.get(name); + get(name: string, location = this.config.location) { + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!def) { throw new InvalidField(`No field named '${name}'`); } @@ -453,14 +482,14 @@ class ConfigGroup { * The field must be defined for the config group and the value is * type checked against the field definition. * - * @param {string} name - Name of field to set. - * @param {*} value - Value to set for field. - * @param {string=} location - Location used for access control. - * @throws {module:lib.InvalidField} if field is not defined. - * @throws {module:lib.InvalidValue} if value is not allowed for the field. + * @param name - Name of field to set. + * @param value - Value to set for field. + * @param location - Location used for access control. + * @throws {InvalidField} if field is not defined. + * @throws {InvalidValue} if value is not allowed for the field. */ - set(name, value, location = this.config.location) { - let def = this.constructor._definitions.get(name); + set(name: string, value: unknown, location = this.config.location) { + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!def) { throw new InvalidField(`No field named '${name}'`); } @@ -475,7 +504,7 @@ class ConfigGroup { throw new InvalidValue(`Expected one of [${def.enum.join(", ")}], not ${value}`); } - if (basicType(value) === "string") { + if (typeof value === "string") { if (def.type === "boolean") { if (value === "true") { value = true; @@ -520,15 +549,15 @@ class ConfigGroup { * Update value of stored object field by setting the specified property * on it. The field must be defined as an object for the config group. * - * @param {string} name - Name of field to set property on. - * @param {string} prop - Name of property to set on field. - * @param {*} value - the value to set the property to. - * @param {string=} location - Location used for access control. - * @throws {module:lib.InvalidField} if field is not defined. - * @throws {module:lib.InvalidValue} if field is not an object. + * @param name - Name of field to set property on. + * @param prop - Name of property to set on field. + * @param value - the value to set the property to. + * @param location - Location used for access control. + * @throws {InvalidField} if field is not defined. + * @throws {InvalidValue} if field is not an object. */ - setProp(name, prop, value, location = this.config.location) { - let def = this.constructor._definitions.get(name); + setProp(name: string, prop: string, value: unknown, location = this.config.location) { + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!def) { throw new InvalidField(`No field named '${name}'`); } @@ -538,7 +567,7 @@ class ConfigGroup { throw new InvalidValue(`Cannot set property on non-object field '${name}'`); } - let prev = this._fields.get(name); + let prev = this._fields.get(name) as object; let updated = {...prev || {}}; if (value !== undefined) { @@ -558,12 +587,16 @@ class ConfigGroup { * Overwrites the values of all the fields in this config group that * has a valid value in the passed serialized group with that value. * - * @param {Object} serializedGroup - + * @param serializedGroup - * Result from a previous call to .serialize(). - * @param {boolean} notify - Invoke fieldChanged events if true. - * @param {string=} location - Location used for access control. + * @param notify - Invoke fieldChanged events if true. + * @param location - Location used for access control. */ - update(serializedGroup, notify, location = this.config.location) { + update( + serializedGroup: SerializedGroup, + notify: boolean, + location = this.config.location + ) { if (basicType(serializedGroup) !== "object") { throw new Error(`Expected object, not ${basicType(serializedGroup)} for ConfigGroup`); } @@ -577,7 +610,7 @@ class ConfigGroup { } for (let [name, value] of Object.entries(serializedGroup.fields)) { - let def = this.constructor._definitions.get(name); + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!def) { this._unknownFields.set(name, value); continue; @@ -611,13 +644,13 @@ class ConfigGroup { /** * Serialize the config to a plain JavaScript object * - * @param {string=} location - Location used for access control. - * @returns {Object} JSON serializable representation of the group. + * @param location - Location used for access control. + * @returns JSON serializable representation of the group. */ - serialize(location = this.config.location) { + serialize(location: ConfigLocation = this.config.location) { let fields = {}; for (let [name, value] of this._fields) { - let def = this.constructor._definitions.get(name); + let def = (this.constructor as typeof ConfigGroup)._definitions.get(name); if (!this._checkAccess(def, location, false)) { continue; } @@ -649,8 +682,6 @@ class ConfigGroup { * * Returns the internal meta data for the fields of this class. * Note that mutating this object will lead to unexpected behaviour. - * - * @returns {Map} field meta data */ static get definitions() { this._initSubclass(); @@ -660,37 +691,48 @@ class ConfigGroup { /** * Define a new configuration field * - * @param {Object} item - Field definition. - * @param {Array=} item.access - + * @param item - Field definition. + * @param item.access - * Array of strings declaring where this config entry can be read * and modified from. - * @param {string} item.type - + * @param item.type - * string declaring the type of the config value, supports * boolean, string, number and object. - * @param {string} item.name - + * @param item.name - * Name of the configuration field. Should follow the * lower_case_underscore style. - * @param {string} item.title - + * @param item.title - * Text used to identify the config field in user interfaces. - * @param {string} item.description - + * @param item.description - * Text used to describe the config field in user interfaces. - * @param {Array=} item.enum - + * @param item.enum - * Array of all values that are acceptible. Useful for making * enumerated values. - * @param {boolean=} item.restartRequired - + * @param item.restartRequired - * True if a restart of the entity this config is attached to is * required for changes to take effect. Informative only, defaults * to false. - * @param {Array=} item.restartRequiredProps - + * @param item.restartRequiredProps - * Properties to invert the value of restartRequired for if present. - * @param {boolean=} item.optional - + * @param item.optional - * True if this field can be set to null. Defaults to false. - * @param {*=} item.initial_value - + * @param item.initial_value - * Value this config field should take in a newly initialized * config. This can also be an async function returning the * value to use. */ - static define(item) { + static define(item: { + access?: ConfigLocation[], + type: FieldType, + name: string, + title?: string, + description?: string, + enum?: unknown[], + restartRequired?: boolean, + restartRequiredProps?: string[], + optional?: boolean, + initial_value?: unknown | (() => unknown), + }) { if (this._finalized) { throw new Error(`Cannot define field for ConfigGroup class ${this.name} after it has been finalized`); } @@ -786,7 +828,7 @@ class ConfigGroup { throw new Error(`Config field ${item.name} has already been defined`); } - let computed = { + let computed: FieldDefinition = { access: this.defaultAccess, optional: false, fullName: `${this.groupName}.${item.name}`, @@ -829,10 +871,9 @@ class ConfigGroup { * plugins. This is currently only the enabled field. It is otherwise * identical to the ConfigGroup class. * - * @extends module:lib.ConfigGroup - * @memberof module:lib + * @extends ConfigGroup */ -class PluginConfigGroup extends ConfigGroup { +export class PluginConfigGroup extends ConfigGroup { static _initSubclass() { if (!this._initialized) { super._initSubclass(); @@ -846,14 +887,3 @@ class PluginConfigGroup extends ConfigGroup { } } } - - -module.exports = { - InvalidAccess, - InvalidField, - InvalidValue, - - Config, - ConfigGroup, - PluginConfigGroup, -}; diff --git a/packages/lib/src/config/definitions.js b/packages/lib/src/config/definitions.ts similarity index 92% rename from packages/lib/src/config/definitions.js rename to packages/lib/src/config/definitions.ts index a21bf40b..2d25e542 100644 --- a/packages/lib/src/config/definitions.js +++ b/packages/lib/src/config/definitions.ts @@ -1,13 +1,11 @@ // Core definitions for the configuration system -"use strict"; - -const classes = require("./classes"); +import * as classes from "./classes"; +import type { PluginInfo } from "../plugin"; /** - * Controller config group for {@link module:lib.ControllerConfig} - * @extends module:lib.ConfigGroup - * @memberof module:lib + * Controller config group for {@link ControllerConfig} + * @extends classes.ConfigGroup */ class ControllerGroup extends classes.ConfigGroup { } ControllerGroup.defaultAccess = ["controller", "host", "control"]; @@ -140,17 +138,15 @@ ControllerGroup.finalize(); /** * Controller Config class - * @extends module:lib.Config - * @memberof module:lib + * @extends classes.Config */ class ControllerConfig extends classes.Config { } ControllerConfig.registerGroup(ControllerGroup); /** - * Host config group for {@link module:lib.HostConfig} - * @extends module:lib.ConfigGroup - * @memberof module:lib + * Host config group for {@link HostConfig} + * @extends classes.ConfigGroup */ class HostGroup extends classes.ConfigGroup {} HostGroup.defaultAccess = ["controller", "host", "control"]; @@ -232,17 +228,15 @@ HostGroup.finalize(); /** * Host Config class - * @extends module:lib.Config - * @memberof module:lib + * @extends classes.Config */ class HostConfig extends classes.Config { } HostConfig.registerGroup(HostGroup); /** - * Instance config group for {@link module:lib.InstanceConfig} - * @extends module:lib.ConfigGroup - * @memberof module:lib + * Instance config group for {@link classes.InstanceConfig} + * @extends classes.ConfigGroup */ class InstanceGroup extends classes.ConfigGroup { } InstanceGroup.defaultAccess = ["controller", "host", "control"]; @@ -272,9 +266,8 @@ InstanceGroup.define({ InstanceGroup.finalize(); /** - * Factorio config group for {@link module:lib.InstanceConfig} - * @extends module:lib.ConfigGroup - * @memberof module:lib + * Factorio config group for {@link classes.InstanceConfig} + * @extends classes.ConfigGroup */ class FactorioGroup extends classes.ConfigGroup { } FactorioGroup.defaultAccess = ["controller", "host", "control"]; @@ -398,8 +391,7 @@ FactorioGroup.finalize(); /** * Instance config class - * @extends module:lib.Config - * @memberof module:lib + * @extends classes.Config */ class InstanceConfig extends classes.Config { } InstanceConfig.registerGroup(InstanceGroup); @@ -407,9 +399,8 @@ InstanceConfig.registerGroup(FactorioGroup); /** - * Control config group for {@link module:lib.ControlConfig} - * @extends module:lib.ConfigGroup - * @memberof module:lib + * Control config group for {@link classes.ControlConfig} + * @extends classes.ConfigGroup */ class ControlGroup extends classes.ConfigGroup {} ControlGroup.defaultAccess = ["control"]; @@ -443,14 +434,13 @@ ControlGroup.finalize(); /** * Control config class - * @extends module:lib.Config - * @memberof module:lib + * @extends classes.Config */ class ControlConfig extends classes.Config { } ControlConfig.registerGroup(ControlGroup); -function validateGroup(pluginInfo, groupName) { +function validateGroup(pluginInfo: PluginInfo, groupName: string) { if (!(pluginInfo[groupName].prototype instanceof classes.PluginConfigGroup)) { throw new Error( `Expected ${groupName} for ${pluginInfo.name} to be a subclass of PluginConfigGroup` @@ -468,9 +458,8 @@ function validateGroup(pluginInfo, groupName) { * Registers the config groups for the provided plugin infos * * @param {Array} pluginInfos - Array of plugin info objects. - * @memberof module:lib */ -function registerPluginConfigGroups(pluginInfos) { +function registerPluginConfigGroups(pluginInfos: PluginInfo[]) { for (let pluginInfo of pluginInfos) { if (pluginInfo.ControllerConfigGroup) { validateGroup(pluginInfo, "ControllerConfigGroup"); @@ -502,8 +491,6 @@ function registerPluginConfigGroups(pluginInfos) { /** * Lock configs from adding more groups and make them usable - * - * @memberof module:lib */ function finalizeConfigs() { ControllerConfig.finalize(); diff --git a/packages/lib/src/data/index.ts b/packages/lib/src/data/index.ts index 46c1ed2a..5fb50974 100644 --- a/packages/lib/src/data/index.ts +++ b/packages/lib/src/data/index.ts @@ -14,3 +14,40 @@ export * from "./messages_instance"; export * from "./messages_mod"; export * from "./messages_user"; export * from "./version"; + +// TODO: Remove/migrate after lib/data is migrated to ts +import type { User } from "../users"; +export type AddressType = "controller" | "host" | "instance" | "control" | "broadcast"; +export interface Serialisable { + jsonSchema: object, + fromJSON(json: any): object, +} + +import type { Address } from "./messages_core"; +export type AddressShorthand = + | "controller" + | { instanceId: number } + | { hostId: number } + | { controlId: number } + | Address + +export class Request { + static type: "request"; + static src: AddressType | AddressType[]; + static dst: AddressType | AddressType[]; + static permission: null | "string" | ((user: User, message: Request) => void); + static plugin?: string; + static jsonSchema?: object; + static fromJSON?: (json: any) => Request; + static Response?: Serialisable; +} + +export class Event { + static type: "event"; + static src: AddressType | AddressType[]; + static dst: AddressType | AddressType[]; + static permission: null | "string" | ((user: User, message: Event) => void); + static plugin?: string; + static jsonSchema?: object; + static fromJSON?: (json: any) => Event; +} diff --git a/packages/lib/src/data/messages_core.js b/packages/lib/src/data/messages_core.js index beda4502..d418a60f 100644 --- a/packages/lib/src/data/messages_core.js +++ b/packages/lib/src/data/messages_core.js @@ -19,10 +19,10 @@ class Address { /** @type {number} */ type; - /** @type {string} */ + /** @type {number} */ id; - /** @type {string|undefined} */ + /** @type {number|undefined} */ requestId; constructor(type, id, requestId) { diff --git a/packages/lib/src/errors.ts b/packages/lib/src/errors.ts index 30b26cf3..54c307c8 100644 --- a/packages/lib/src/errors.ts +++ b/packages/lib/src/errors.ts @@ -6,7 +6,9 @@ /** * Thrown by commands when they fail */ -export class CommandError extends Error { } +export class CommandError extends Error { + code: "CommandError"; +} /** * Thrown from requests sent when an error occured handling it @@ -14,7 +16,7 @@ export class CommandError extends Error { } export class RequestError extends Error { constructor( message: string, - public code?: string, + public code = "RequestError", public stack?: string, ) { super(message); @@ -28,12 +30,22 @@ export class RequestError extends Error { * requests fail due to permission denied. * */ -export class PermissionError extends RequestError { } +export class PermissionError extends RequestError { + constructor( + message: string, + code = "PermissionError", + stack?: string, + ) { + super(message, code, stack); + } + +} /** * Signal for messages that fail validation */ export class InvalidMessage extends Error { + code: "InvalidMessage"; constructor( message: string, public errors: object = null, @@ -45,27 +57,36 @@ export class InvalidMessage extends Error { /** * Thrown from requests when the session it was sent on was lost */ -export class SessionLost extends Error { } +export class SessionLost extends Error { + code: "SessionLost"; +} /** * Thrown when WebSocket authentication failed */ -export class AuthenticationFailed extends Error { } +export class AuthenticationFailed extends Error { + code: "AuthenticationFailed"; +} /** * Errror class for known errors occuring during startup */ -export class StartupError extends Error { } +export class StartupError extends Error { + code: "StartupError"; +} /** * Errors outside of our control */ -export class EnvironmentError extends Error { } +export class EnvironmentError extends Error { + code: "EnvironmentError"; +} /** * Errors caused by plugins */ export class PluginError extends Error { + code: "PluginError"; constructor( public pluginName: string, public original: Error diff --git a/packages/lib/src/factorio/exchange_string.js b/packages/lib/src/factorio/exchange_string.ts similarity index 79% rename from packages/lib/src/factorio/exchange_string.js rename to packages/lib/src/factorio/exchange_string.ts index 249cd3d1..a0d9606f 100644 --- a/packages/lib/src/factorio/exchange_string.js +++ b/packages/lib/src/factorio/exchange_string.ts @@ -1,51 +1,50 @@ "use strict"; -const zlib = require("zlib"); - +import zlib from "zlib"; class MapReaderState { - constructor(buf) { - this.pos = 0; - this.buf = buf; - this.last_position = { x: 0, y: 0 }; - } + pos = 0; + last_position = { x: 0, y: 0 }; + constructor( + public buf: Buffer + ) { } } -function readUInt8(state) { +function readUInt8(state: MapReaderState) { let value = state.buf.readUInt8(state.pos); state.pos += 1; return value; } -function readBool(state) { +function readBool(state: MapReaderState) { let value = readUInt8(state) !== 0; return value; } -function readInt16(state) { +function readInt16(state: MapReaderState) { let value = state.buf.readInt16LE(state.pos); state.pos += 2; return value; } -function readUInt16(state) { +function readUInt16(state: MapReaderState) { let value = state.buf.readUInt16LE(state.pos); state.pos += 2; return value; } -function readInt32(state) { +function readInt32(state: MapReaderState) { let value = state.buf.readInt32LE(state.pos); state.pos += 4; return value; } -function readUInt32(state) { +function readUInt32(state: MapReaderState) { let value = state.buf.readUInt32LE(state.pos); state.pos += 4; return value; } -function readUInt32so(state) { +function readUInt32so(state: MapReaderState) { let value = readUInt8(state); if (value === 0xff) { return readUInt32(state); @@ -54,26 +53,26 @@ function readUInt32so(state) { return value; } -function readFloat(state) { +function readFloat(state: MapReaderState) { let value = state.buf.readFloatLE(state.pos); state.pos += 4; return value; } -function readDouble(state) { +function readDouble(state: MapReaderState) { let value = state.buf.readDoubleLE(state.pos); state.pos += 8; return value; } -function readString(state) { +function readString(state: MapReaderState) { let size = readUInt32so(state); - let data = state.buf.slice(state.pos, state.pos + size).toString("utf-8"); + let data = state.buf.subarray(state.pos, state.pos + size).toString("utf-8"); state.pos += size; return data; } -function readOptional(state, readValue) { +function readOptional(state: MapReaderState, readValue: (state: MapReaderState) => T) { let load = readUInt8(state) !== 0; if (!load) { return null; @@ -81,10 +80,10 @@ function readOptional(state, readValue) { return readValue(state); } -function readArray(state, readItem) { +function readArray(state: MapReaderState, readItem: (state: MapReaderState) => T) { let size = readUInt32so(state); - let array = []; + let array: T[] = []; for (let i = 0; i < size; i++) { let item = readItem(state); array.push(item); @@ -93,10 +92,14 @@ function readArray(state, readItem) { return array; } -function readDict(state, readKey, readValue) { +function readDict( + state: MapReaderState, + readKey: (state: MapReaderState) => K, + readValue: (state: MapReaderState) => V +) { let size = readUInt32so(state); - let mapping = new Map(); + let mapping = new Map(); for (let i = 0; i < size; i++) { let key = readKey(state); let value = readValue(state); @@ -106,7 +109,7 @@ function readDict(state, readKey, readValue) { return mapping; } -function readVersion(state) { +function readVersion(state: MapReaderState) { let major = readUInt16(state); let minor = readUInt16(state); let patch = readUInt16(state); @@ -114,7 +117,7 @@ function readVersion(state) { return [major, minor, patch, developer]; } -function readFrequencySizeRichness(state) { +function readFrequencySizeRichness(state: MapReaderState) { return { frequency: readFloat(state), size: readFloat(state), @@ -122,15 +125,15 @@ function readFrequencySizeRichness(state) { }; } -function readAutoplaceSetting(state) { +function readAutoplaceSetting(state: MapReaderState) { return { treat_missing_as_default: readBool(state), settings: Object.fromEntries(readDict(state, readString, readFrequencySizeRichness)), }; } -function readMapPosition(state) { - let x, y; +function readMapPosition(state: MapReaderState) { + let x: number, y: number; let x_diff = readInt16(state) / 256; if (x_diff === 0x7fff / 256) { x = readInt32(state) / 256; @@ -145,7 +148,7 @@ function readMapPosition(state) { return { x, y }; } -function readBoundingBox(state) { +function readBoundingBox(state: MapReaderState) { return { left_top: readMapPosition(state), right_bottom: readMapPosition(state), @@ -156,7 +159,7 @@ function readBoundingBox(state) { }; } -function readCliffSettings(state) { +function readCliffSettings(state: MapReaderState) { return { name: readString(state), elevation_0: readFloat(state), @@ -165,7 +168,7 @@ function readCliffSettings(state) { }; } -function readMapGenSettings(state) { +function readMapGenSettings(state: MapReaderState) { return { terrain_segmentation: readFloat(state), water: readFloat(state), @@ -184,7 +187,7 @@ function readMapGenSettings(state) { }; } -function readPollution(state) { +function readPollution(state: MapReaderState) { return { enabled: readOptional(state, readBool), diffusion_ratio: readOptional(state, readDouble), @@ -201,24 +204,23 @@ function readPollution(state) { }; } -function readSteeringValue(state) { +function readSteeringValue(state: MapReaderState) { return { radius: readOptional(state, readDouble), separation_factor: readOptional(state, readDouble), separation_force: readOptional(state, readDouble), force_unit_fuzzy_goto_behavior: readOptional(state, readBool), }; - } -function readSteering(state) { +function readSteering(state: MapReaderState) { return { default: readSteeringValue(state), moving: readSteeringValue(state), }; } -function readEnemyEvolution(state) { +function readEnemyEvolution(state: MapReaderState) { return { enabled: readOptional(state, readBool), time_factor: readOptional(state, readDouble), @@ -227,7 +229,7 @@ function readEnemyEvolution(state) { }; } -function readEnemyExpansion(state) { +function readEnemyExpansion(state: MapReaderState) { return { enabled: readOptional(state, readBool), max_expansion_distance: readOptional(state, readUInt32), @@ -245,7 +247,7 @@ function readEnemyExpansion(state) { }; } -function readUnitGroup(state) { +function readUnitGroup(state: MapReaderState) { return { min_group_gathering_time: readOptional(state, readUInt32), max_group_gathering_time: readOptional(state, readUInt32), @@ -263,7 +265,7 @@ function readUnitGroup(state) { }; } -function readPathFinder(state) { +function readPathFinder(state: MapReaderState) { return { fwd2bwd_ratio: readOptional(state, readInt32), goal_pressure_ratio: readOptional(state, readDouble), @@ -301,7 +303,7 @@ function readPathFinder(state) { }; } -function readDifficultySettings(state) { +function readDifficultySettings(state: MapReaderState) { return { recipe_difficulty: readUInt8(state), technology_difficulty: readUInt8(state), @@ -310,7 +312,7 @@ function readDifficultySettings(state) { }; } -function readMapSettings(state) { +function readMapSettings(state: MapReaderState) { return { pollution: readPollution(state), steering: readSteering(state), @@ -323,18 +325,23 @@ function readMapSettings(state) { }; } -/** - * @typedef {object} MapExchangeResult - * @property {array} version - - * Version of Factorio the string was created with. - * @property {object} map_gen_settings - - * Decoded map generator settings in the format the --map-gen-settings - * command line option to Factorio expect. - * @property {object} map_settings - - * Decoded map settings in the format the --map-settings command line option - * to Factorio expects. - * @property {number} checksum - checksum for the exchange string. - */ +export interface MapExchangeData { + /** Version of Factorio the string was created with. */ + version: ReturnType; + unknown: number; + /** + * Decoded map generator settings in the format the --map-gen-settings + * command line option to Factorio expect. + */ + map_gen_settings: ReturnType; + /** + * Decoded map settings in the format the --map-settings command line + * option to Factorio expects. + */ + map_settings: ReturnType; + /** CRC32 checksum for the exchange string. */ + checksum: number; +} /** * Parse a Map Exchange String @@ -343,11 +350,10 @@ function readMapSettings(state) { * data structures that can be fed into the Factorio server when creating a * save to set the map gen settings and the map settings for the save. * - * @param {string} exchangeString - Max Exchange String to parse. - * @returns {MapExchangeResult} Parsed result. - * @memberof module:lib + * @param exchangeString - Max Exchange String to parse. + * @returns Parsed result. */ -function readMapExchangeString(exchangeString) { +export function readMapExchangeString(exchangeString: string) { exchangeString = exchangeString.replace(/[ \t\n\r]+/g, ""); if (!/>>>[0-9a-zA-Z\/+]+={0,3}<<; /** * Gracefully disconnect @@ -272,7 +282,7 @@ class WebSocketBaseConnector extends BaseConnector { this.setClosing(); this.send(new libData.MessageDisconnect("prepare")); - let timeout; + let timeout: NodeJS.Timeout; let waitTimeout = new Promise(resolve => { timeout = setTimeout(resolve, 10000); }); // Make Configurable? try { await Promise.race([ @@ -314,25 +324,27 @@ class WebSocketBaseConnector extends BaseConnector { * Connector for controller clients * * @extends module:lib.WebSocketBaseConnector - * @memberof module:lib */ -class WebSocketClientConnector extends WebSocketBaseConnector { - constructor(url, maxReconnectDelay, tlsCa = null) { - super(undefined, new libData.Address(libData.Address.controller, 0)); +export abstract class WebSocketClientConnector extends WebSocketBaseConnector { + _reconnectId: NodeJS.Timeout | null = null; + _sessionToken: string | null = null; + _sessionTimeout: number | null = null; + _startedResuming: number | null = null; + _backoff: ExponentialBackoff; - this._url = url; + constructor( + protected _url: string, + maxReconnectDelay: number, + protected _tlsCa = null + ) { + super(undefined, new libData.Address(libData.Address.controller, 0)); this._backoff = new ExponentialBackoff({ max: maxReconnectDelay }); - this._tlsCa = tlsCa; // The following states are used in the client connector // closed: Not connected // connecting: Attempting to connect to server. // resuming: Attempting to resume an existing connection to the server. // connected: Connection is online - this._reconnectId = null; - this._sessionToken = null; - this._sessionTimeout = null; - this._startedResuming = null; } _reset() { @@ -360,9 +372,9 @@ class WebSocketClientConnector extends WebSocketBaseConnector { * Used in the register function in order to send the handshake messages * over the WebSocket. * - * @param {module:lib.Message} message - Message to send. + * @param message - Message to send. */ - sendHandshake(message) { + sendHandshake(message: libData.Message) { this._check("connecting", "resuming"); this._sendInternal(message); } @@ -372,10 +384,10 @@ class WebSocketClientConnector extends WebSocketBaseConnector { * * Sends a close frame and disconnects the connector. * - * @param {number} code - WebSocket close code. - * @param {string} reason - WebSocket close reason. + * @param code - WebSocket close code. + * @param reason - WebSocket close reason. */ - async close(code, reason) { + async close(code: number, reason: string) { if (this._state === "closed") { return; } @@ -443,7 +455,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { this._socket = new WebSocket(url); } else { - let options = {}; + let options: { ca?: string } = {}; if (this._tlsCa) { options.ca = this._tlsCa; } this._socket = new WebSocket(url, options); } @@ -470,7 +482,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { ) { logger.error("Connector | Session timed out trying to resume"); this._reset(); - if (this._disconnecting) { + if (this._closing) { this._reset(); this.emit("close"); } else { @@ -485,11 +497,11 @@ class WebSocketClientConnector extends WebSocketBaseConnector { this._reconnectId = null; this._doConnect(); }, delay); - this._reconnectTime = Date.now() + delay; } _attachSocketHandlers() { - this._socket.onclose = event => { + // TODO type event + this._socket.onclose = (event: any) => { const previousState = this._state; // Authentication failed @@ -532,7 +544,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { } }; - this._socket.onerror = event => { + this._socket.onerror = (event: any) => { // It's assumed that close is always called by ws let code = !event.error ? "" : `, code: ${event.error.code}`; let message = `Connector | Socket error: ${event.message || "unknown error"}${code}`; @@ -561,7 +573,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { }; // Handle messages - this._socket.onmessage = event => { + this._socket.onmessage = (event: any) => { let message = this._parseMessage(event.data); if (!message) { return; @@ -578,9 +590,9 @@ class WebSocketClientConnector extends WebSocketBaseConnector { }; } - _processHandshake(message) { - - let { type, data } = message; + _processHandshake(message: libData.Message) { + // TODO infer data from Message type. + let { type, data } = message as libData.Message & { data: any }; if (type === "hello") { logger.verbose(`Connector | received hello from controller version ${data.version}`); this.emit("hello", data); @@ -621,7 +633,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector { } else if (type === "invalidate") { logger.warn("Connector | session invalidated by controller"); - if (this._disconnecting) { + if (this._closing) { this._reset(); this.emit("close"); } else { @@ -638,19 +650,18 @@ class WebSocketClientConnector extends WebSocketBaseConnector { * This function is expected to be overriden by sub-classes and send the * register message over the socket when invoked. */ - register() { - throw Error("Abstract function"); - } + abstract register(): void; } /** * Connector for in-memory local links * * @extends events.EventEmitter - * @memberof module:lib */ -class VirtualConnector extends BaseConnector { - constructor(src, dst) { +export class VirtualConnector extends BaseConnector { + other: VirtualConnector; + + constructor(src: libData.Address, dst: libData.Address) { super(src, dst); this.other = this; @@ -662,11 +673,14 @@ class VirtualConnector extends BaseConnector { * Creates two virtual connectors that are liked to each othes such that * a message sent on one is received by the other. * - * @param {module:lib.Address} src - Source for obverse connector - * @param {module:lib.Address} dst - destination for obverse connector - * @returns {Array} two virtuarl connectors. + * @param src - Source for obverse connector + * @param dst - destination for obverse connector + * @returns two virtuarl connectors. */ - static makePair(src, dst) { + static makePair( + src: libData.Address, + dst: libData.Address, + ): [VirtualConnector, VirtualConnector] { let obverse = new this(src, dst); let reverse = new this(dst, src); obverse.other = reverse; @@ -677,16 +691,9 @@ class VirtualConnector extends BaseConnector { /** * Send a message to the other end of the connector * - * @param {module:lib.Message} message - Message type to send. + * @param message - Message type to send. */ - send(message) { + send(message: libData.Message) { this.other.emit("message", message); } } - -module.exports = { - BaseConnector, - WebSocketBaseConnector, - WebSocketClientConnector, - VirtualConnector, -}; diff --git a/packages/lib/src/link/index.ts b/packages/lib/src/link/index.ts index 5f873f85..e13d1d8b 100644 --- a/packages/lib/src/link/index.ts +++ b/packages/lib/src/link/index.ts @@ -5,7 +5,3 @@ */ export * from "./link" export * from "./connectors" - -// migrate: Allow info for plugins from before link refactor to load. -export class Event {} -export class Request {} diff --git a/packages/lib/src/link/link.js b/packages/lib/src/link/link.ts similarity index 65% rename from packages/lib/src/link/link.js rename to packages/lib/src/link/link.ts index a9aacc42..f62411a6 100644 --- a/packages/lib/src/link/link.js +++ b/packages/lib/src/link/link.ts @@ -1,11 +1,45 @@ // Implementation of Link class -"use strict"; +import * as libData from "../data"; +import * as libErrors from "../errors"; +import { logger } from "../logging"; +import * as libSchema from "../schema"; +import { dataClasses } from "./messages"; +import { BaseConnector, WebSocketBaseConnector } from "./connectors"; +import { strict as assert } from "assert"; +import type { PluginInfo } from "../plugin"; -const libData = require("../data"); -const libErrors = require("../errors"); -const { logger } = require("../logging"); -const libSchema = require("../schema"); -const { dataClasses } = require("./messages"); +interface RequestEntry { + Request: typeof libData.Request; + name: string, + allowedSrcTypes: Set; + allowedDstTypes: Set; + requestFromJSON: (json: any) => libData.Request; + Response?: any; + responseFromJSON: (json: any) => any; +} + +interface PendingRequest { + promise: Promise; + request: RequestEntry; + resolve: (result: any) => void; + reject: (err: Error) => void; + dst: libData.Address; +} +interface ForwardedRequest { + origin: Link; + src: libData.Address; + dst: libData.Address; +} + +interface EventEntry { + Event: typeof libData.Event; + name: string, + allowedSrcTypes: Set; + allowedDstTypes: Set; + eventFromJSON: (json: any) => libData.Event; +} +type RequestHandler = (request: libData.Request, src: libData.Address, dst: libData.Address) => any; +type EventHandler = (request: libData.Event, src: libData.Address, dst: libData.Address) => any; // Some definitions for the terminology used here: // link: Either side of a controller - client connection @@ -16,23 +50,23 @@ const { dataClasses } = require("./messages"); /** * Common interface for server and client connections - * - * @memberof module:lib */ -class Link { - constructor(connector) { - this.connector = connector; - this.router = undefined; - this.validateSent = true; +export class Link { + router = undefined; + validateSent = true; - this._requestHandlers = new Map(); - this._requestFallbacks = new Map(); - this._eventHandlers = new Map(); - this._eventSnoopers = new Map(); - this._pendingRequests = new Map(); - this._forwardedRequests = new Map(); - this._nextRequestId = 1; + _requestHandlers = new Map(); + _requestFallbacks = new Map(); + _eventHandlers = new Map(); + _eventSnoopers = new Map(); + _pendingRequests = new Map(); + _forwardedRequests = new Map(); + _nextRequestId = 1; + constructor( + public connector: BaseConnector + ) { + // @ts-ignore: TODO Remove when lib/data is migrated to ts this.handle(libData.PingRequest, () => {}); // Process messages received by the connector @@ -74,11 +108,12 @@ class Link { }); } - _clearPendingRequests(err) { + _clearPendingRequests(err: Error & { code?: string }) { for (let pending of this._pendingRequests.values()) { pending.reject(err); } for (let pending of this._forwardedRequests.values()) { + assert(pending.origin.connector instanceof WebSocketBaseConnector); if (pending.origin.connector.hasSession) { pending.origin.connector.sendResponseError( new libData.ResponseError(err.message, err.code), pending.src, pending.dst @@ -95,10 +130,12 @@ class Link { * Validates and invokes the handler and/or waiters for a message that has * been received. An unhandled message is considered to be an error. * - * @param {module:lib.Message} message - Message to process. - * @throws {module:lib.InvalidMessage} if the message is invalid or not handled. + * @param message - Message to process. + * @throws {libErrors.InvalidMessage} if the message is invalid or not handled. */ - _processMessage(message) { + _processMessage( + message: libData.MessageRequest | libData.MessageResponse | libData.MessageResponseError | libData.MessageEvent + ) { if (!["request", "response", "responseError", "event"].includes(message.type)) { throw new libErrors.InvalidMessage(`Unhandled message type ${message.type}`); } @@ -115,10 +152,11 @@ class Link { } } - if (message.type === "event" && this._eventSnoopers.has(entry.Event)) { - let handler = this._eventSnoopers.get(entry.Event); - handler(entry.eventFromJSON(message.data), message.src, message.dst).catch(err => { - logger.error(`Unexpected error snooping ${message.name}:\n${err.stack}`); + if (message.type === "event" && this._eventSnoopers.has((entry as EventEntry).Event)) { + let handler = this._eventSnoopers.get((entry as EventEntry).Event); + let event = message as libData.MessageEvent; + handler((entry as EventEntry).eventFromJSON(event.data), event.src, event.dst).catch((err: Error) => { + logger.error(`Unexpected error snooping ${event.name}:\n${err.stack}`); }); } @@ -131,7 +169,11 @@ class Link { } if (message.type === "request") { - this._processRequest(message, entry, this._requestHandlers.get(entry.Request)); + this._processRequest( + message as libData.MessageRequest, + entry as RequestEntry, + this._requestHandlers.get((entry as RequestEntry).Request) + ); } else if (message.type === "response") { this._processResponse(message); @@ -140,7 +182,10 @@ class Link { this._processResponseError(message); } else if (message.type === "event") { - this._processEvent(message, entry); + this._processEvent( + message as libData.MessageEvent, + entry as EventEntry + ); } } @@ -149,12 +194,14 @@ class Link { * * Should be overridden by sub-classes to validate messages received. * - * @param {module:lib.Message} message - Message to check. - * @throws {module:lib.InvalidMessage} if the message is invalid. + * @param message - Message to check. + * @throws {libErrors.InvalidMessage} if the message is invalid. */ - validateIngress(message) { } + validateIngress(message: libData.Message) { } - _validateMessage(message) { + _validateMessage( + message: libData.MessageRequest | libData.MessageResponse | libData.MessageResponseError | libData.MessageEvent + ) { try { if (message.src.type === libData.Address.broadcast) { throw new libErrors.InvalidMessage("Message src may not be broadcast"); @@ -168,7 +215,8 @@ class Link { } if (message.type === "request") { - let entry = this.constructor._requestsByName.get(message.name); + assert(message instanceof libData.MessageRequest); + let entry = Link._requestsByName.get(message.name); if (!entry) { let err = new libErrors.InvalidMessage(`Unrecognized request ${message.name}`); this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src); @@ -193,7 +241,8 @@ class Link { return undefined; } else if (message.type === "event") { - let entry = this.constructor._eventsByName.get(message.name); + assert(message instanceof libData.MessageEvent); + let entry = Link._eventsByName.get(message.name); if (!entry) { throw new libErrors.InvalidMessage(`Unrecognized event ${message.name}`); } @@ -222,26 +271,29 @@ class Link { * Called when an request or event is received by a control connector. * Should be overridden by sub-classes to validate messages received. * - * @param {module:lib.Message} message - Message to check. - * @param {object} entry - Request or Event entry for this Message. - * @throws {module:lib.PermissionError} if unauthorized. + * @param message - Message to check. + * @param entry - Request or Event entry for this Message. + * @throws {libErrors.PermissionError} if unauthorized. */ - validatePermission(message, entry) { } + validatePermission(message: libData.Message, entry: RequestEntry | EventEntry) { } - _routeMessage(message, entry) { + _routeMessage(message: libData.Message, entry?: RequestEntry | EventEntry) { if (!this.router) { let err = new libErrors.InvalidMessage( - `Received message addressed to ${message.dst} but this link does not route messages` + `Received message addressed to ${(message as libData.MessageRequest).dst} but this link `+ + `does not route messages` ); if (message.type === "request") { this.connector.sendResponseError( - new libData.ResponseError(err.message, err.code), message.src + new libData.ResponseError(err.message, err.code), (message as libData.MessageRequest).src ); } throw err; } - let fallback = message.type === "request" ? this._requestFallbacks.get(entry.Request) : undefined; + let fallback = + message.type === "request" ? this._requestFallbacks.get((entry as RequestEntry).Request) : undefined + ; if (this.router.forwardMessage(this, message, Boolean(fallback))) { return; } @@ -251,10 +303,20 @@ class Link { if (message.type !== "request") { throw new Error(`Router requested fallback handling of unsupported message type ${message.type}`); } - this._processRequest(message, entry, fallback, message.dst); + this._processRequest( + message as libData.MessageRequest, + entry as RequestEntry, + fallback, + (message as libData.MessageRequest).dst + ); } - _processRequest(message, entry, handler, spoofedSrc) { + _processRequest( + message: libData.MessageRequest, + entry: RequestEntry, + handler: RequestHandler, + spoofedSrc?: libData.Address + ) { if (!handler) { this.connector.sendResponseError( new libData.ResponseError(`No handler for ${entry.Request.name}`), message.src @@ -262,7 +324,7 @@ class Link { return; } - let response; + let response: any; try { response = handler(entry.requestFromJSON(message.data), message.src, message.dst); } catch (err) { @@ -280,7 +342,7 @@ class Link { } response.then( - result => { + (result: any) => { if (this.validateSent) { if (entry.Response) { entry.responseFromJSON(JSON.parse(JSON.stringify(result))); @@ -291,7 +353,7 @@ class Link { this.connector.sendResponse(result, message.src, spoofedSrc); } ).catch( - err => { + (err: Error & { code?: string }) => { if (err instanceof libErrors.InvalidMessage) { logger.error(err.message); if (err.errors) { @@ -307,7 +369,7 @@ class Link { ); } - _processResponse(message) { + _processResponse(message: libData.MessageResponse) { let pending = this._pendingRequests.get(message.dst.requestId); if (!pending) { throw new libErrors.InvalidMessage( @@ -327,7 +389,7 @@ class Link { } } - _processResponseError(message) { + _processResponseError(message: libData.MessageResponseError) { let pending = this._pendingRequests.get(message.dst.requestId); if (!pending) { throw new libErrors.InvalidMessage( @@ -338,13 +400,15 @@ class Link { pending.reject(new libErrors.RequestError(message.data.message, message.data.code, message.data.stack)); } - _processEvent(message, entry) { + _processEvent(message: libData.MessageEvent, entry: EventEntry) { let handler = this._eventHandlers.get(entry.Event); if (!handler) { throw new libErrors.InvalidMessage(`Unhandled event ${message.name}`); } - handler(entry.eventFromJSON(message.data), message.src, message.dst).catch(err => { + handler( + entry.eventFromJSON(message.data), message.src, message.dst + ).catch((err: Error) => { logger.error(`Unexpected error handling ${message.name}:\n${err.stack}`); }); } @@ -368,38 +432,46 @@ class Link { /** * Send a request or event to the other side of this link - * @param {*} requestOrEvent - Request or event to send - * @returns {Promise<*>|undefined} + * @param requestOrEvent - Request or event to send + * @returns * Promise that resolves to the response if a request was sent or * undefined if it was an event. */ - send(requestOrEvent) { + send(requestOrEvent: libData.Request): Promise; + send(requestOrEvent: libData.Event): void; + send(requestOrEvent: libData.Request | libData.Event): Promise | void { return this.sendTo(this.connector.dst, requestOrEvent); } /** * Send a request or event to the given address * - * @param {*|module:lib.Address} destination - Where to send it - * @param {*} requestOrEvent - Request or event to send - * @returns {Promise<*>|undefined} + * @param destination - Where to send it + * @param requestOrEvent - Request or event to send + * @returns * Promise that resolves to the response if a request was sent or * undefined if it was an event. */ - sendTo(destination, requestOrEvent) { + sendTo(destination: libData.AddressShorthand, requestOrEvent: libData.Request): Promise; + sendTo(destination: libData.AddressShorthand, requestOrEvent: libData.Event): void; + sendTo( + destination: libData.AddressShorthand, + requestOrEvent: libData.Request | libData.Event, + ): Promise | void { let dst = libData.Address.fromShorthand(destination); - if (requestOrEvent.constructor.type === "request") { + // TODO investigate if these types can be inferred + if ((requestOrEvent.constructor as any).type === "request") { return this.sendRequest(requestOrEvent, dst); } - if (requestOrEvent.constructor.type === "event") { + if ((requestOrEvent.constructor as any).type === "event") { return this.sendEvent(requestOrEvent, dst); } - throw Error(`Expected request or event but got type ${requestOrEvent.constructor.type}`); + throw Error(`Expected request or event but got type ${(requestOrEvent.constructor as any).type}`); } - sendRequest(request, dst) { - let entry = this.constructor._requestsByClass.get(request.constructor); + sendRequest(request: libData.Request, dst: libData.Address) { + let entry = Link._requestsByClass.get(request.constructor as typeof libData.Request); if (!entry) { throw new Error(`Attempt to send unregistered Request ${request.constructor.name}`); } @@ -407,22 +479,22 @@ class Link { entry.requestFromJSON(JSON.parse(JSON.stringify(request))); } - let pending = { - request: entry, - dst, - }; - pending.promise = new Promise((resolve, reject) => { - pending.resolve = resolve; - pending.reject = reject; + let resolve: (value: unknown) => void; + let reject: (reason: any) => void; + let promise = new Promise((res, rej) => { + resolve = res; + reject = rej; }); let requestId = this._nextRequestId; this._nextRequestId += 1; - this._pendingRequests.set(requestId, pending); + this._pendingRequests.set( + requestId, { request: entry, promise, resolve, reject, dst, } + ); this.connector.sendRequest(request, requestId, dst); - return pending.promise; + return promise; } - forwardRequest(message, origin) { + forwardRequest(message: libData.MessageRequest, origin: Link) { let pending = { origin, src: message.src, @@ -432,8 +504,8 @@ class Link { this.connector.send(message); } - sendEvent(event, dst) { - let entry = this.constructor._eventsByClass.get(event.constructor); + sendEvent(event: libData.Event, dst: libData.Address) { + let entry = Link._eventsByClass.get(event.constructor as typeof libData.Event); if (!entry) { throw new Error(`Attempt to send unregistered Event ${event.constructor.name}`); } @@ -444,18 +516,23 @@ class Link { this.connector.sendEvent(event, dst); } - handle(Class, handler) { + handle(Class: typeof libData.Request, handler: RequestHandler): void; + handle(Class: typeof libData.Event, handler: EventHandler): void; + handle( + Class: typeof libData.Request | typeof libData.Event, + handler: RequestHandler | EventHandler, + ) { if (Class.type === "request") { this.handleRequest(Class, handler); } else if (Class.type === "event") { this.handleEvent(Class, handler); } else { - throw new Error(`Class ${Class.name} has unrecognized type ${Class.type}`); + throw new Error(`Class ${(Class as any).name} has unrecognized type ${(Class as any).type}`); } } - handleRequest(Request, handler) { - let entry = this.constructor._requestsByClass.get(Request); + handleRequest(Request: typeof libData.Request, handler: RequestHandler) { + let entry = Link._requestsByClass.get(Request); if (!entry) { throw new Error(`Unregistered Request class ${Request.name}`); } @@ -465,8 +542,8 @@ class Link { this._requestHandlers.set(Request, handler); } - fallbackRequest(Request, handler) { - let entry = this.constructor._requestsByClass.get(Request); + fallbackRequest(Request: typeof libData.Request, handler: RequestHandler) { + let entry = Link._requestsByClass.get(Request); if (!entry) { throw new Error(`Unregistered Request class ${Request.name}`); } @@ -476,8 +553,8 @@ class Link { this._requestFallbacks.set(Request, handler); } - handleEvent(Event, handler) { - let entry = this.constructor._eventsByClass.get(Event); + handleEvent(Event: typeof libData.Event, handler: EventHandler) { + let entry = Link._eventsByClass.get(Event); if (!entry) { throw new Error(`Unregistered Event class ${Event.name}`); } @@ -487,8 +564,8 @@ class Link { this._eventHandlers.set(Event, handler); } - snoopEvent(Event, handler) { - let entry = this.constructor._eventsByClass.get(Event); + snoopEvent(Event: typeof libData.Event, handler: EventHandler) { + let entry = Link._eventsByClass.get(Event); if (!entry) { throw new Error(`Unregistered Event class ${Event.name}`); } @@ -498,23 +575,23 @@ class Link { this._eventSnoopers.set(Event, handler); } - static register(Class) { + static register(Class: typeof libData.Request | typeof libData.Event) { if (Class.type === "request") { this.registerRequest(Class); } else if (Class.type === "event") { this.registerEvent(Class); } else { - throw new Error(`Data class ${Class.name} has unknown type ${Class.type}`); + throw new Error(`Data class ${(Class as any).name} has unknown type ${(Class as any).type}`); } } - static requestFromJSON(Request, name) { + static requestFromJSON(Request: typeof libData.Request, name: string) { if (Request.fromJSON) { if (!Request.jsonSchema) { throw new Error(`Request ${name} has static fromJSON but is missing static jsonSchema`); } - let validate = libSchema.compile(Request.jsonSchema); - return (json) => { + let validate = libSchema.compile(Request.jsonSchema as any); + return (json: any) => { if (!validate(json)) { throw new libErrors.InvalidMessage(`Request ${name} failed validation`, validate.errors); } @@ -527,7 +604,7 @@ class Link { return () => new Request(); } - static responseFromJSON(Response, name) { + static responseFromJSON(Response: libData.Serialisable, name: string) { if (!Response.jsonSchema) { throw new Error(`Response for Request ${name} is missing static jsonSchema`); } @@ -535,8 +612,8 @@ class Link { throw new Error(`Response for Request ${name} is missing static fromJSON`); } - let validate = libSchema.compile(Response.jsonSchema); - return (json) => { + let validate = libSchema.compile(Response.jsonSchema as any); + return (json: any) => { if (!validate(json)) { throw new libErrors.InvalidMessage( `Response for request ${name} failed validation`, validate.errors @@ -546,7 +623,7 @@ class Link { }; } - static allowedTypes(types, name, side) { + static allowedTypes(types: libData.AddressType | libData.AddressType[], name: string, side: "src" | "dst") { if (types === undefined) { throw new Error(`Missing ${side} specification in ${name}`); } @@ -555,7 +632,7 @@ class Link { types = [types]; } - let allowed = new Set(); + let allowed = new Set(); for (let type of types) { let id = libData.Address[type]; if (typeof id !== "number") { @@ -569,21 +646,22 @@ class Link { return allowed; } - static _requestsByName = new Map(); - static _requestsByClass = new Map(); + static _requestsByName = new Map(); + static _requestsByClass = new Map(); - static registerRequest(Request) { + static registerRequest(Request: typeof libData.Request) { const name = Request.plugin ? `${Request.plugin}:${Request.name}` : Request.name; if (this._requestsByName.has(name)) { throw new Error(`Request ${name} is already registered`); } - let entry = { + let entry: RequestEntry = { Request, name, requestFromJSON: this.requestFromJSON(Request, name), allowedSrcTypes: this.allowedTypes(Request.src, name, "src"), allowedDstTypes: this.allowedTypes(Request.dst, name, "dst"), + responseFromJSON: (_json: any) => undefined, }; if ( @@ -597,21 +675,19 @@ class Link { if (Response) { entry.Response = Response; entry.responseFromJSON = this.responseFromJSON(Response, name); - } else { - entry.responseFromJSON = () => undefined; } this._requestsByName.set(name, entry); this._requestsByClass.set(Request, entry); } - static eventFromJSON(Event, name) { + static eventFromJSON(Event: typeof libData.Event, name: string) { if (Event.fromJSON) { if (!Event.jsonSchema) { throw new Error(`Event ${name} has static fromJSON but is missing static jsonSchema`); } - let validate = libSchema.compile(Event.jsonSchema); - return (json) => { + let validate = libSchema.compile(Event.jsonSchema as any); + return (json: any) => { if (!validate(json)) { throw new libErrors.InvalidMessage(`Event ${name} failed validation`, validate.errors); } @@ -621,13 +697,13 @@ class Link { if (Event.jsonSchema) { throw new Error(`Event ${name} has static jsonSchema but is missing static fromJSON`); } - return () => new Event(); + return (json: any) => new Event(); } - static _eventsByName = new Map(); - static _eventsByClass = new Map(); + static _eventsByName = new Map(); + static _eventsByClass = new Map(); - static registerEvent(Event) { + static registerEvent(Event: typeof libData.Event) { const name = Event.plugin ? `${Event.plugin}:${Event.name}` : Event.name; if (this._eventsByName.has(name)) { throw new Error(`Event ${name} is already registered`); @@ -654,10 +730,11 @@ class Link { } for (let Class of dataClasses) { + // @ts-ignore: TODO Remove when lib/data is migrated to ts Link.register(Class); } -function registerPluginMessages(pluginInfos) { +export function registerPluginMessages(pluginInfos: PluginInfo[]) { for (let pluginInfo of pluginInfos) { for (let Class of pluginInfo.messages || []) { if (Class.plugin !== pluginInfo.name) { @@ -670,9 +747,3 @@ function registerPluginMessages(pluginInfos) { } } } - - -module.exports = { - Link, - registerPluginMessages, -}; diff --git a/packages/lib/src/link/messages.js b/packages/lib/src/link/messages.ts similarity index 85% rename from packages/lib/src/link/messages.js rename to packages/lib/src/link/messages.ts index 6a6acf5c..1566c1de 100644 --- a/packages/lib/src/link/messages.js +++ b/packages/lib/src/link/messages.ts @@ -1,15 +1,18 @@ "use strict"; -const core = require("../data/messages_core"); -const controller = require("../data/messages_controller"); -const host = require("../data/messages_host"); -const instance = require("../data/messages_instance"); -const mod = require("../data/messages_mod"); -const user = require("../data/messages_user"); +import * as core from "../data/messages_core"; +import * as controller from "../data/messages_controller"; +import * as host from "../data/messages_host"; +import * as instance from "../data/messages_instance"; +import * as mod from "../data/messages_mod"; +import * as user from "../data/messages_user"; -// Event and Request payloads registered with the Link class. Used to decode -// the data payload of incomming requsts and event messages into instances of -// their proper classes. -const dataClasses = [ +/** + * Event and Request payloads registered with the Link class. Used to decode + * the data payload of incomming requsts and event messages into instances of + * their proper classes. + * @internal + */ +export const dataClasses = [ core.PingRequest, controller.ControllerConfigGetRequest, @@ -107,5 +110,3 @@ const dataClasses = [ user.AccountUpdateEvent, user.UserUpdateEvent, ]; - -module.exports = { dataClasses }; diff --git a/packages/lib/src/shared_commands.ts b/packages/lib/src/shared_commands.ts index ada6ac4a..5635f1ad 100644 --- a/packages/lib/src/shared_commands.ts +++ b/packages/lib/src/shared_commands.ts @@ -135,7 +135,7 @@ export async function handleConfigCommand( let command = args._[1]; if (command === "list") { - for (let GroupClass of (instance as any).constructor.groups.values()) { + for (let GroupClass of (instance.constructor as typeof libConfig.Config).groups.values()) { for (let def of GroupClass.definitions.values()) { let value = instance.get(def.fullName); print(`${def.fullName} ${JSON.stringify(value)}`); diff --git a/test/lib/link/connector.js b/test/lib/link/connector.js index e589af21..bbfbf39c 100644 --- a/test/lib/link/connector.js +++ b/test/lib/link/connector.js @@ -37,15 +37,6 @@ describe("lib/link/connectors", function() { let testConnector = new lib.WebSocketClientConnector("url", 1); testConnector._socket = new mock.MockSocket(); - describe(".register()", function() { - it("is abstract", function() { - assert.throws( - () => testConnector.register(), - new Error("Abstract function") - ); - }); - }); - describe(".sendHandshake()", function() { it("calls send on the socket", function() { testConnector._state = "connecting";