Merge pull request #493 from Hornwitser/typescript-lib

Port lib package to TypeScript
This commit is contained in:
Hornwitser authored and GitHub committed 2023-08-12 14:44:59 +02:00
commit 6d523096ce
85 files changed
+5449 -6616

No files matched your search

-25
View File
@@ -1,25 +0,0 @@
{
"plugins": [
"plugins/markdown"
],
"tags": {
"allowUnknownTags": false
},
"opts": {
"destination": "docs/api",
"recurse": true
},
"source": {
"include": [
"packages/controller/src/",
"packages/controller/controller.js",
"packages/host/src/",
"packages/host/host.js",
"packages/ctl/src/",
"packages/ctl/ctl.js",
"packages/lib/",
"plugins/"
],
"excludePattern": "node_modules"
}
}
+3 -3
View File
@@ -9,7 +9,7 @@
"ci-cover": "nyc --reporter=lcovonly pnpm run-script test",
"lint": "eslint \"*.js\" packages plugins test",
"lint-fix": "eslint --fix \"*.js\" packages plugins test",
"docs": "jsdoc --configure docs/jsdoc.json",
"docs": "typedoc",
"bundle-dependencies": "bundle-dependencies"
},
"nyc": {
@@ -31,11 +31,11 @@
"express": "^4.18.2",
"form-data": "^4.0.0",
"fs-extra": "^11.1.1",
"jsdoc": "^4.0.2",
"jsonwebtoken": "^9.0.1",
"jszip": "^3.10.0",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"phin": "^3.7.0"
"phin": "^3.7.0",
"typedoc": "^0.24.8"
}
}
+6 -6
View File
@@ -248,7 +248,7 @@ class ControlConnection extends BaseConnection {
async handleHostGenerateTokenRequest(message) {
let hostId = message.host_id;
if (hostId === null) {
if (hostId === undefined) {
hostId = Math.random() * 2**31 | 0;
}
return this._controller.generateHostToken(hostId);
@@ -279,7 +279,7 @@ class ControlConnection extends BaseConnection {
instance.config.get("instance.name"),
instance.id,
instance.config.get("instance.assigned_host"),
instance.game_port || null,
instance.game_port || undefined,
instance.status,
);
}
@@ -290,8 +290,8 @@ class ControlConnection extends BaseConnection {
list.push(new lib.InstanceDetails(
instance.config.get("instance.name"),
instance.id,
instance.config.get("instance.assigned_host"),
instance.game_port || null,
instance.config.get("instance.assigned_host") || undefined,
instance.game_port || undefined,
instance.status,
));
}
@@ -311,8 +311,8 @@ class ControlConnection extends BaseConnection {
new lib.InstanceDetails(
instance.config.get("instance.name"),
instance.id,
instance.config.get("instance.assigned_host"),
instance.game_port || null,
instance.config.get("instance.assigned_host") || undefined,
instance.game_port || undefined,
instance.status,
)
));
+9 -2
View File
@@ -83,7 +83,7 @@ class Control extends lib.Link {
controller = false,
hostIds = [],
instanceIds = [],
maxLevel = null,
maxLevel = undefined,
}) {
await this.send(
new lib.LogSetSubscriptionsRequest(
@@ -286,7 +286,11 @@ async function startControl() {
process.exitCode = 1;
} else if (err instanceof lib.RequestError) {
logger.error(`Error sending request:\n${err.stack}`);
if (err.stack) {
logger.error(`Error sending request:\n${err.stack}`);
} else {
logger.error(`Error sending request: ${err.message}`);
}
process.exitCode = 1;
} else {
@@ -315,6 +319,9 @@ I version of clusterio. Expect things to break. I
`
);
startControl().catch(err => {
if (err.errors) {
logger.fatal(JSON.stringify(err.errors, null, 4));
}
if (!(err instanceof lib.StartupError)) {
logger.fatal(`
+------------------------------------------------------------+
+6 -6
View File
@@ -237,9 +237,9 @@ hostCommands.add(new lib.Command({
yargs.option("id", { type: "number", nargs: 1, describe: "Host id" });
}],
handler: async function(args, control) {
let hostId = typeof args.id === "number" ? args.id : null;
let hostId = typeof args.id === "number" ? args.id : undefined;
let response = await control.send(new lib.HostGenerateTokenRequest(hostId));
print(response.token);
print(response);
},
}));
@@ -480,9 +480,9 @@ instanceCommands.add(new lib.Command({
}));
async function loadMapSettings(args) {
let seed = args.seed !== undefined ? args.seed : null;
let mapGenSettings = null;
let mapSettings = null;
let seed = args.seed;
let mapGenSettings;
let mapSettings;
if (args.mapExchangeString) {
let parsed = lib.readMapExchangeString(args.mapExchangeString);
mapGenSettings = parsed.map_gen_settings;
@@ -1551,7 +1551,7 @@ logCommands.add(new lib.Command({
"controller": { describe: "Query log of the controller", nargs: 0, type: "boolean", default: false },
"host": { describe: "Query log of given host", nargs: 1, type: "string", default: null },
"instance": { describe: "Query log of given instance", nargs: 1, type: "string", default: null },
"max-level": { describe: "Maximum log level to return", nargs: 1, type: "string", default: null },
"max-level": { describe: "Maximum log level to return", nargs: 1, type: "string", default: undefined },
"limit": { describe: "Max number of entries to return", nargs: 1, type: "number", default: 1000 },
"start": { describe: "Limit from the start instead of the end", nargs: 0, type: "boolean", default: false },
});
+1 -1
View File
@@ -457,7 +457,7 @@ class Host extends lib.Link {
let instanceConnection = this.instanceConnections.get(instanceId);
this.send(
new lib.InstanceStatusChangedEvent(
instanceId, instanceConnection ? instanceConnection.status : "stopped", null
instanceId, instanceConnection ? instanceConnection.status : "stopped", undefined
)
);
+1 -1
View File
@@ -438,7 +438,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
new lib.InstanceStatusChangedEvent(
this.id,
status,
this.server && this.server.gamePort || this.config.get("factorio.game_port") || null,
this.server && this.server.gamePort || this.config.get("factorio.game_port") || undefined,
),
);
}
+10 -6
View File
@@ -796,9 +796,11 @@ class FactorioServer extends events.EventEmitter {
[
"--config", this.writePath("config.ini"),
"--create", this.writePath("saves", name),
...(seed !== null ? ["--map-gen-seed", seed] : []),
...(mapGenSettings !== null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []),
...(mapSettings !== null ? ["--map-settings", this.writePath("map-settings.json")] : []),
/* eslint-disable eqeqeq, no-eq-null */
...(seed != null ? ["--map-gen-seed", seed] : []),
...(mapGenSettings != null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []),
...(mapSettings != null ? ["--map-settings", this.writePath("map-settings.json")] : []),
/* eslint-enable eqeqeq, no-eq-null */
...(this.verboseLogging ? ["--verbose"] : []),
],
{
@@ -886,9 +888,11 @@ class FactorioServer extends events.EventEmitter {
[
"--config", this.writePath("config.ini"),
"--start-server-load-scenario", scenario,
...(seed !== null ? ["--map-gen-seed", seed] : []),
...(mapGenSettings !== null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []),
...(mapSettings !== null ? ["--map-settings", this.writePath("map-settings.json")] : []),
/* eslint-disable eqeqeq, no-eq-null */
...(seed != null ? ["--map-gen-seed", seed] : []),
...(mapGenSettings != null ? ["--map-gen-settings", this.writePath("map-gen-settings.json")] : []),
...(mapSettings != null ? ["--map-settings", this.writePath("map-settings.json")] : []),
/* eslint-enable eqeqeq, no-eq-null */
"--port", this.gamePort,
"--rcon-port", this.rconPort,
"--rcon-password", this.rconPassword,
-1
View File
@@ -1 +0,0 @@
src/data/message_validate.js
View File
Whitespace-only changes.
+14 -14
View File
@@ -1,15 +1,15 @@
export * from "./src/config";
export * from "./src/data";
export * from "./src/errors";
export * from "./src/factorio/exchange_string";
export * from "./src/helpers";
export * from "./src/ini";
export * from "./src/link";
export * from "./src/logging";
export * from "./src/plugin";
export * from "./src/schema";
export * from "./src/users";
export * from "./dist/src/config";
export * from "./dist/src/data";
export * from "./dist/src/errors";
export * from "./dist/src/factorio/exchange_string";
export * from "./dist/src/helpers";
export * from "./dist/src/ini";
export * from "./dist/src/link";
export * from "./dist/src/logging";
export * from "./dist/src/plugin";
export * from "./dist/src/schema";
export * from "./dist/src/users";
export { default as ExponentialBackoff } from "./src/ExponentialBackoff";
export { default as PlayerStats } from "./src/PlayerStats";
export { default as RateLimiter } from "./src/RateLimiter";
export { default as ExponentialBackoff } from "./dist/src/ExponentialBackoff";
export { default as PlayerStats } from "./dist/src/PlayerStats";
export { default as RateLimiter } from "./dist/src/RateLimiter";
-63
View File
@@ -1,63 +0,0 @@
/**
* Shared library for Clusterio
* @module lib
*/
"use strict";
const libBuildMod = require("./build_mod");
const libCommand = require("./src/command");
const libConfig = require("./src/config");
const libData = require("./src/data");
const libDatabase = require("./src/database");
const libErrors = require("./src/errors");
const libFactorio = require("./src/factorio");
const libFileOps = require("./src/file_ops");
const libHash = require("./src/hash");
const libHelpers = require("./src/helpers");
const libIni = require("./src/ini");
const libLink = require("./src/link");
const libLogging = require("./src/logging");
const libLoggingUtils = require("./src/logging_utils");
const libLuaTools = require("./src/lua_tools");
const libPlugin = require("./src/plugin");
const libPluginLoader = require("./src/plugin_loader");
const libPrometheus = require("./src/prometheus");
const libSchema = require("./src/schema");
const libSharedCommands = require("./src/shared_commands");
const libStream = require("./src/stream");
const libUsers = require("./src/users");
const libZipOps = require("./src/zip_ops");
const ExponentialBackoff = require("./src/ExponentialBackoff");
const PlayerStats = require("./src/PlayerStats");
const RateLimiter = require("./src/RateLimiter");
module.exports = {
...libBuildMod,
...libCommand,
...libConfig,
...libData,
...libDatabase,
...libErrors,
...libFactorio,
...libFileOps,
...libHash,
...libHelpers,
...libIni,
...libLink,
...libLogging,
...libLoggingUtils,
...libLuaTools,
...libPlugin,
...libPluginLoader,
...libPrometheus,
...libSchema,
...libSharedCommands,
...libStream,
...libUsers,
...libZipOps,
ExponentialBackoff,
PlayerStats,
RateLimiter,
};
+31
View File
@@ -0,0 +1,31 @@
/**
* Shared library for Clusterio
* @module lib
*/
export * from "./build_mod";
export * from "./src/command";
export * from "./src/config";
export * from "./src/data";
export * from "./src/database";
export * from "./src/errors";
export * from "./src/factorio";
export * from "./src/file_ops";
export * from "./src/hash";
export * from "./src/helpers";
export * from "./src/ini";
export * from "./src/link";
export * from "./src/logging";
export * from "./src/logging_utils";
export * from "./src/lua_tools";
export * from "./src/plugin";
export * from "./src/plugin_loader";
export * from "./src/prometheus";
export * from "./src/schema";
export * from "./src/shared_commands";
export * from "./src/stream";
export * from "./src/users";
export * from "./src/zip_ops";
export { default as ExponentialBackoff } from "./src/ExponentialBackoff";
export { default as PlayerStats } from "./src/PlayerStats";
export { default as RateLimiter } from "./src/RateLimiter";
+10 -1
View File
@@ -5,9 +5,10 @@
"repository": "https://github.com/clusterio/clusterio",
"license": "MIT",
"scripts": {
"prepare": "pnpm run compile_validator",
"prepare": "tsc --build && pnpm run compile_validator",
"compile_validator": "node scripts/compile_validator.js"
},
"main": "dist/index.js",
"browser": "browser.js",
"keywords": [
"factorio"
@@ -16,6 +17,7 @@
"node": ">=12"
},
"dependencies": {
"@sinclair/typebox": "^0.30.4",
"ajv": "^8.12.0",
"chalk": "^4.1.2",
"fast-deep-equal": "^3.1.3",
@@ -31,5 +33,12 @@
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/triple-beam": "^1.3.2",
"@types/ws": "^8.5.5",
"typescript": "^5.1.6"
}
}
+4 -4
View File
@@ -7,9 +7,9 @@ const standaloneCode = require("ajv/dist/standalone").default;
/* eslint-disable node/no-sync */
// Provide an empty file for message_core to load
fs.outputFileSync(path.join(__dirname, "../src/data/message_validate.js"), "");
fs.outputFileSync(path.join(__dirname, "../dist/src/data/message_validate.js"), "");
const { Message } = require("../src/data/messages_core");
const { Message } = require("../dist/src/data/messages_core");
const ajv = new Ajv({
allowUnionTypes: true,
@@ -23,6 +23,6 @@ const ajv = new Ajv({
const validate = ajv.compile(Message.jsonSchema);
let moduleCode = standaloneCode(ajv, validate);
fs.outputFileSync(
path.join(__dirname, "../src/data/message_validate.js"),
`// generated by scripts/compile_validator.js\n/* eslint-disable */\n${moduleCode}`
path.join(__dirname, "../dist/src/data/message_validate.js"),
`// generated by scripts/compile_validator.js\n// @ts-nocheck\n/* eslint-disable */\n${moduleCode}`
);
@@ -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 *
* 2<sup>retries</sup>, optinos.max).
*
* @returns {number} time in milliseconds to wait.
* @returns time in milliseconds to wait.
*/
delay() {
let invocationTime = Date.now();
@@ -1,52 +1,43 @@
"use strict";
import { Type, Static } from "@sinclair/typebox";
/**
* Stastics collected about a player.
* @alias module:lib.PlayerStats
*/
class PlayerStats {
export default class PlayerStats {
/**
* Count of the number of times this player has been seen joining this server.
* @type {number}
*/
joinCount = 0;
/**
* Time in ms this player has been seen online on this instance.
* @type {number}
*/
onlineTimeMs = 0;
/**
* Timestamp the player was last seen joining.
* @type {Date=}
*/
lastJoinAt;
lastJoinAt?: Date;
/**
* Timestamp the player was last seen leaving.
* @type {Date=}
*/
lastLeaveAt;
lastLeaveAt?: Date;
/**
* Reason the player was last seen leaving with.
* @type {string=}
*/
lastLeaveReason;
lastLeaveReason?: string;
static jsonSchema = {
type: "object",
properties: {
"join_count": { type: "integer" },
"online_time_ms": { type: "number" },
"last_join_at": { type: "number" },
"last_leave_at": { type: "number" },
"last_leave_reason": { type: "string" },
},
};
static jsonSchema = Type.Object({
"join_count": Type.Optional(Type.Integer()),
"online_time_ms": Type.Optional(Type.Number()),
"last_join_at": Type.Optional(Type.Number()),
"last_leave_at": Type.Optional(Type.Number()),
"last_leave_reason": Type.Optional(Type.String()),
});
constructor(json = {}) {
constructor(json: Static<typeof PlayerStats.jsonSchema> = {}) {
if (json["join_count"]) {
this.joinCount = json["join_count"];
}
@@ -62,12 +53,12 @@ class PlayerStats {
}
}
static fromJSON(json) {
static fromJSON(json: Static<typeof PlayerStats.jsonSchema>) {
return new this(json);
}
toJSON() {
let json = {};
let json: Static<typeof PlayerStats.jsonSchema> = {};
if (this.joinCount) {
json["join_count"] = this.joinCount;
}
@@ -84,5 +75,3 @@ class PlayerStats {
return json;
}
}
module.exports = PlayerStats;
@@ -1,6 +1,3 @@
"use strict";
/**
* Helper class for rate limiting actions
*
@@ -8,6 +5,7 @@
* limited on invocations.
*
* @example
* ```ts
* const rateLimiter = RateLimiter({
* action: () => { console.log("action"); },
* maxRate: 1,
@@ -19,36 +17,35 @@
* // and one more time 1 second later for 99 other activations that were
* // exceeding the rate limit.
* }
* @alias module:lib.RateLimiter
* ```
*/
class RateLimiter {
export default class RateLimiter {
/**
* Maximum rate of activations per second.
*/
maxRate: number;
/**
* Callback invoked at most `maxRate` per second on activation.
*/
action?: Function;
private _lastRun = 0;
private _runTimeout?: NodeJS.Timeout;
/**
* Construct RateLimiter helper
*
* The options passed may also be modified on the instance at runtime.
*
* @param {object=} options - Options for this rate limiter
* @param {number=} options.maxRate -
* @param options - Options for this rate limiter
* @param options.maxRate -
* Maximum rate of activations per second.
* @param {function()=} options.action -
* @param options.action -
* Callback invoked at maste `maxRate` per second on activation.
*/
constructor(options = {}) {
/**
* Maximum rate of activations per second.
* @type {number}
*/
constructor(options: { maxRate?: number, action?: Function } = {}) {
this.maxRate = options.maxRate || 1;
/**
* Callback invoked at most `maxRate` per second on activation.
* @type {function()}
*/
this.action = options.action || null;
this._lastRun = 0;
this._runTimeout = null;
this.lastProgressBroadcast = Date.now();
this.action = options.action || undefined;
}
/**
@@ -59,16 +56,18 @@ class RateLimiter {
* invoked immediatly, otherwise a timeout is registered to invoke the
* action at next point in time where it will be within the rate limit.
*
* @returns {boolean} true if activation was within the rate limit.
* @returns true if activation was within the rate limit.
*/
activate() {
let now = Date.now();
if (now < this._lastRun + 1000 / this.maxRate) {
if (!this._runTimeout && this.action !== null) {
if (!this._runTimeout && this.action) {
this._runTimeout = setTimeout(() => {
this._runTimeout = null;
this._runTimeout = undefined;
this._lastRun = Date.now();
this.action();
if (this.action) {
this.action();
}
}, this._lastRun + 1000 / this.maxRate - now);
}
return false;
@@ -77,11 +76,11 @@ class RateLimiter {
// We may end up activating before the timeout does.
if (this._runTimeout) {
clearTimeout(this._runTimeout);
this._runTimeout = null;
this._runTimeout = undefined;
}
this._lastRun = now;
if (this.action !== null) {
if (this.action) {
this.action();
}
return true;
@@ -100,5 +99,3 @@ class RateLimiter {
}
}
}
module.exports = RateLimiter;
@@ -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<string, unknown>, control: Link) => Promise<void>;
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<string, unknown>, 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<string, Command | CommandTree>;
/**
* Define a command containing sub commands.
*
* @param {Object} cmd - Command definition
* @param {string} cmd.name - Name of the command.
* @param {Array<string>} 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<number>} host ID.
* @static
* @param client - link to controller to query host on.
* @param hostName - string with name or id of host.
* @returns host ID.
*/
async function resolveHost(client, hostName) {
let hostId;
export async function resolveHost(client: Link, hostName: string) {
let hostId: number | undefined;
if (/^-?\d+$/.test(hostName)) {
hostId = parseInt(hostName, 10);
} else {
@@ -173,13 +183,12 @@ async function resolveHost(client, hostName) {
* Resolves a string with either an instance name or an id into an integer
* with the instance ID.
*
* @param {module:lib.Link} client - link to controller to query instance on.
* @param {string} instanceName - string with name or id of instance.
* @returns {Promise<number>} instance ID.
* @static
* @param client - link to controller to query instance on.
* @param instanceName - string with name or id of instance.
* @returns instance ID.
*/
async function resolveInstance(client, instanceName) {
let instanceId;
export async function resolveInstance(client: Link, instanceName: string) {
let instanceId: number | undefined;
if (/^-?\d+$/.test(instanceName)) {
instanceId = parseInt(instanceName, 10);
} else {
@@ -205,14 +214,13 @@ async function resolveInstance(client, instanceName) {
* Resolevs a string with either a mod pack name or an id into an integer
* with the mod pack ID.
*
* @param {module:lib.Link} client -
* @param client -
* link to controller to query mod pack on.
* @param {string} modPackName - string with name or id of mod pack.
* @returns {Promise<number>} mod pack ID.
* @static
* @param modPackName - string with name or id of mod pack.
* @returns mod pack ID.
*/
async function resolveModPack(client, modPackName) {
let modPackId;
export async function resolveModPack(client: Link, modPackName: string) {
let modPackId: number | undefined;
if (/^-?\d+/.test(modPackName)) {
modPackId = parseInt(modPackName, 10);
} else {
@@ -238,15 +246,14 @@ async function resolveModPack(client, modPackName) {
* Resolves a string with either a role name or an id into an object
* representing the role.
*
* @param {module:lib.Link} client - link to controller to query role on.
* @param {string} roleName - string with name or id of role.
* @returns {Promise<object>} Role info.
* @static
* @param client - link to controller to query role on.
* @param roleName - string with name or id of role.
* @returns Role info.
*/
async function retrieveRole(client, roleName) {
export async function retrieveRole(client: Link, roleName: string) {
let roles = await client.sendTo("controller", new libData.RoleListRequest());
let resolvedRole;
let resolvedRole: libData.RawRole | undefined;
if (/^-?\d+$/.test(roleName)) {
let roleId = parseInt(roleName, 10);
for (let role of roles) {
@@ -271,13 +278,3 @@ async function retrieveRole(client, roleName) {
return resolvedRole;
}
module.exports = {
Command,
CommandTree,
resolveHost,
resolveInstance,
resolveModPack,
retrieveRole,
};
@@ -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<Config["serialize"]>;
type SerializedGroup = ReturnType<ConfigGroup["serialize"]>;
/**
* 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<string, typeof ConfigGroup>;
_initialized = false;
_groups = new Map<string, ConfigGroup>();
_unknownGroups = new Map<string, SerializedGroup>();
/**
* 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<string, GroupClass>} 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<string, FieldDefinition>;
declare static groupName: string;
declare static defaultAccess: ConfigLocation[];
_fields = new Map<string, unknown>();
_unknownFields = new Map<string, unknown>();
/**
* Creates a new config group.
*
* After the creation of the new config group you have to call
* either .init() or .load() on it in order to fully initialize it.
*
* @param {module:lib.Config} config -
* @param config -
* Parent config for this group instance.
*/
constructor(config) {
if (!this.constructor._finalized) {
constructor(
public config: Config
) {
if (!(this.constructor as typeof ConfigGroup)._finalized) {
throw new Error(`Cannot instantiate incomplete ConfigGroup class ${this.constructor.name}`);
}
this._fields = new Map();
this._unknownFields = new Map();
this.config = config;
}
/**
* Check access for a field
*
* @param {object} def - definition for for the field to check.
* @param {string} remote - location of remote access to check for.
* @param {boolean} error - throw on failure to pass check.
* @throws {module:lib.InvalidAccess}
* @param def - definition for for the field to check.
* @param remote - location of remote access to check for.
* @param error - throw on failure to pass check.
* @throws {InvalidAccess}
* if access is not granted and error is true.
* @returns {boolean} true if access is granted
* @returns true if access is granted
* @private
*/
_checkAccess(def, remote, error) {
_checkAccess(def: FieldDefinition, remote: ConfigLocation, error: boolean) {
for (let loc of [remote, this.config.location]) {
if (!def.access.includes(loc)) {
if (error) {
@@ -370,7 +399,7 @@ class ConfigGroup {
* config group.
*/
async init() {
for (let [name, def] of this.constructor._definitions) {
for (let [name, def] of (this.constructor as typeof ConfigGroup)._definitions) {
if (this._fields.has(name)) {
continue;
}
@@ -397,11 +426,11 @@ class ConfigGroup {
* group and then initializes any possible missing fields from it to
* their default values.
*
* @param {Object} serializedGroup -
* @param serializedGroup -
* Result from a previous call to .serialize().
* @param {string=} location - Location used for access control.
* @param location - Location used for access control.
*/
async load(serializedGroup, location = this.config.location) {
async load(serializedGroup: SerializedGroup, location = this.config.location) {
this.update(serializedGroup, false, location);
await this.init();
}
@@ -412,18 +441,18 @@ class ConfigGroup {
* Shortcut for `group.constructor.groupName`
*/
get name() {
return this.constructor.groupName;
return (this.constructor as typeof ConfigGroup).groupName;
}
/**
* Check if field can be accessed from the given location
*
* @param {string} name - Name of field to check.
* @param {string=} location - Location used for access control.
* @returns {boolean} true if field is accessible
* @param name - Name of field to check.
* @param location - Location used for access control.
* @returns true if field is accessible
*/
canAccess(name, location = this.config.location) {
let def = this.constructor._definitions.get(name);
canAccess(name: string, location = this.config.location) {
let def = (this.constructor as typeof ConfigGroup)._definitions.get(name);
if (!def) {
throw new InvalidField(`No field named '${name}'`);
}
@@ -433,12 +462,12 @@ class ConfigGroup {
/**
* Get value for field
*
* @param {string} name - Name of field to get.
* @param {string=} location - Location used for access control.
* @returns {*} Value stored for the field.
* @param name - Name of field to get.
* @param location - Location used for access control.
* @returns Value stored for the field.
*/
get(name, location = this.config.location) {
let def = this.constructor._definitions.get(name);
get(name: string, location = this.config.location) {
let def = (this.constructor as typeof ConfigGroup)._definitions.get(name);
if (!def) {
throw new InvalidField(`No field named '${name}'`);
}
@@ -453,14 +482,14 @@ class ConfigGroup {
* The field must be defined for the config group and the value is
* type checked against the field definition.
*
* @param {string} name - Name of field to set.
* @param {*} value - Value to set for field.
* @param {string=} location - Location used for access control.
* @throws {module:lib.InvalidField} if field is not defined.
* @throws {module:lib.InvalidValue} if value is not allowed for the field.
* @param name - Name of field to set.
* @param value - Value to set for field.
* @param location - Location used for access control.
* @throws {InvalidField} if field is not defined.
* @throws {InvalidValue} if value is not allowed for the field.
*/
set(name, value, location = this.config.location) {
let def = this.constructor._definitions.get(name);
set(name: string, value: unknown, location = this.config.location) {
let def = (this.constructor as typeof ConfigGroup)._definitions.get(name);
if (!def) {
throw new InvalidField(`No field named '${name}'`);
}
@@ -475,7 +504,7 @@ class ConfigGroup {
throw new InvalidValue(`Expected one of [${def.enum.join(", ")}], not ${value}`);
}
if (basicType(value) === "string") {
if (typeof value === "string") {
if (def.type === "boolean") {
if (value === "true") {
value = true;
@@ -492,7 +521,7 @@ class ConfigGroup {
} else if (def.type === "object") {
try {
value = JSON.parse(value);
} catch (err) {
} catch (err: any) {
throw new InvalidValue(`Error parsing value for ${name}: ${err.message}`);
}
}
@@ -520,15 +549,15 @@ class ConfigGroup {
* Update value of stored object field by setting the specified property
* on it. The field must be defined as an object for the config group.
*
* @param {string} name - Name of field to set property on.
* @param {string} prop - Name of property to set on field.
* @param {*} value - the value to set the property to.
* @param {string=} location - Location used for access control.
* @throws {module:lib.InvalidField} if field is not defined.
* @throws {module:lib.InvalidValue} if field is not an object.
* @param name - Name of field to set property on.
* @param prop - Name of property to set on field.
* @param value - the value to set the property to.
* @param location - Location used for access control.
* @throws {InvalidField} if field is not defined.
* @throws {InvalidValue} if field is not an object.
*/
setProp(name, prop, value, location = this.config.location) {
let def = this.constructor._definitions.get(name);
setProp(name: string, prop: string, value: unknown, location = this.config.location) {
let def = (this.constructor as typeof ConfigGroup)._definitions.get(name);
if (!def) {
throw new InvalidField(`No field named '${name}'`);
}
@@ -538,7 +567,7 @@ class ConfigGroup {
throw new InvalidValue(`Cannot set property on non-object field '${name}'`);
}
let prev = this._fields.get(name);
let prev = this._fields.get(name) as Record<string, unknown>;
let updated = {...prev || {}};
if (value !== undefined) {
@@ -558,12 +587,16 @@ class ConfigGroup {
* Overwrites the values of all the fields in this config group that
* has a valid value in the passed serialized group with that value.
*
* @param {Object} serializedGroup -
* @param serializedGroup -
* Result from a previous call to .serialize().
* @param {boolean} notify - Invoke fieldChanged events if true.
* @param {string=} location - Location used for access control.
* @param notify - Invoke fieldChanged events if true.
* @param location - Location used for access control.
*/
update(serializedGroup, notify, location = this.config.location) {
update(
serializedGroup: SerializedGroup,
notify: boolean,
location = this.config.location
) {
if (basicType(serializedGroup) !== "object") {
throw new Error(`Expected object, not ${basicType(serializedGroup)} for ConfigGroup`);
}
@@ -577,7 +610,7 @@ class ConfigGroup {
}
for (let [name, value] of Object.entries(serializedGroup.fields)) {
let def = this.constructor._definitions.get(name);
let def = (this.constructor as typeof ConfigGroup)._definitions.get(name);
if (!def) {
this._unknownFields.set(name, value);
continue;
@@ -611,13 +644,13 @@ class ConfigGroup {
/**
* Serialize the config to a plain JavaScript object
*
* @param {string=} location - Location used for access control.
* @returns {Object} JSON serializable representation of the group.
* @param location - Location used for access control.
* @returns JSON serializable representation of the group.
*/
serialize(location = this.config.location) {
let fields = {};
serialize(location: ConfigLocation = this.config.location) {
let fields: Record<string, unknown> = {};
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<string, Object>} 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<string>=} item.access -
* @param item - Field definition.
* @param item.access -
* Array of strings declaring where this config entry can be read
* and modified from.
* @param {string} item.type -
* @param item.type -
* string declaring the type of the config value, supports
* boolean, string, number and object.
* @param {string} item.name -
* @param item.name -
* Name of the configuration field. Should follow the
* lower_case_underscore style.
* @param {string} item.title -
* @param item.title -
* Text used to identify the config field in user interfaces.
* @param {string} item.description -
* @param item.description -
* Text used to describe the config field in user interfaces.
* @param {Array=} item.enum -
* @param item.enum -
* Array of all values that are acceptible. Useful for making
* enumerated values.
* @param {boolean=} item.restartRequired -
* @param item.restartRequired -
* True if a restart of the entity this config is attached to is
* required for changes to take effect. Informative only, defaults
* to false.
* @param {Array=} item.restartRequiredProps -
* @param item.restartRequiredProps -
* Properties to invert the value of restartRequired for if present.
* @param {boolean=} item.optional -
* @param item.optional -
* True if this field can be set to null. Defaults to false.
* @param {*=} item.initial_value -
* @param item.initial_value -
* Value this config field should take in a newly initialized
* config. This can also be an async function returning the
* value to use.
*/
static define(item) {
static define(item: {
access?: ConfigLocation[],
type: FieldType,
name: string,
title?: string,
description?: string,
enum?: unknown[],
restartRequired?: boolean,
restartRequiredProps?: string[],
optional?: boolean,
initial_value?: unknown | (() => unknown),
}) {
if (this._finalized) {
throw new Error(`Cannot define field for ConfigGroup class ${this.name} after it has been finalized`);
}
@@ -711,82 +753,83 @@ class ConfigGroup {
});
// Validate properties
let validTypes = ["boolean", "string", "number", "object"];
// eslint-disable-next-line complexity
Object.keys(item).forEach(key => {
let value = item[key];
let valueType = basicType(value);
switch (key) {
case "type":
if (!validTypes.includes(value)) {
throw new Error(`${value} is not a valid type`);
}
break;
{
const {
type, access, enum: enum_, name, title, description, restartRequired,
restartRequiredProps, optional, initial_value, ...rest
} = item;
case "access":
if (valueType !== "array") {
throw new Error("access must be an array");
}
break;
case "enum":
if (valueType !== "array") {
throw new Error("enum must be an array");
}
break;
case "name":
if (valueType !== "string") {
throw new Error("name must be a string");
}
break;
case "title":
if (valueType !== "string") {
throw new Error("title must be a string");
}
break;
case "description":
if (valueType !== "string") {
throw new Error("description must be a string");
}
break;
case "restartRequired":
if (valueType !== "boolean") {
throw new Error("restartRequired must be a boolean");
}
break;
case "restartRequiredProps":
if (valueType !== "array") {
throw new Error("restartRequiredProps must be a array");
}
break;
case "optional":
if (valueType !== "boolean") {
throw new Error("optional must be a boolean");
}
break;
case "initial_value":
if (valueType !== item.type && valueType !== "function") {
throw new Error("initial_value must match the type or be a function");
}
break;
default:
throw new Error(`Unknown property ${key}`);
let validTypes = ["boolean", "string", "number", "object"];
if (type !== undefined) {
if (!validTypes.includes(type)) {
throw new Error(`${type} is not a valid type`);
}
}
});
if (access !== undefined) {
if (basicType(access) !== "array") {
throw new Error("access must be an array");
}
}
if (enum_ !== undefined) {
if (basicType(enum_) !== "array") {
throw new Error("enum must be an array");
}
}
if (name !== undefined) {
if (typeof name !== "string") {
throw new Error("name must be a string");
}
}
if (title !== undefined) {
if (typeof title !== "string") {
throw new Error("title must be a string");
}
}
if (description !== undefined) {
if (typeof description !== "string") {
throw new Error("description must be a string");
}
}
if (restartRequired !== undefined) {
if (typeof restartRequired !== "boolean") {
throw new Error("restartRequired must be a boolean");
}
}
if (restartRequiredProps !== undefined) {
if (basicType(restartRequiredProps) !== "array") {
throw new Error("restartRequiredProps must be a array");
}
}
if (optional !== undefined) {
if (typeof optional !== "boolean") {
throw new Error("optional must be a boolean");
}
}
if (initial_value !== undefined) {
if (basicType(initial_value) !== item.type && basicType(initial_value) !== "function") {
throw new Error("initial_value must match the type or be a function");
}
}
for (let key of Object.keys(rest)) {
throw new Error(`Unknown property ${key}`);
}
}
if (this._definitions.has(item.name)) {
throw new Error(`Config field ${item.name} has already been defined`);
}
let computed = {
let computed: FieldDefinition = {
access: this.defaultAccess,
optional: false,
fullName: `${this.groupName}.${item.name}`,
@@ -829,10 +872,9 @@ class ConfigGroup {
* plugins. This is currently only the enabled field. It is otherwise
* identical to the ConfigGroup class.
*
* @extends module:lib.ConfigGroup
* @memberof module:lib
* @extends ConfigGroup
*/
class PluginConfigGroup extends ConfigGroup {
export class PluginConfigGroup extends ConfigGroup {
static _initSubclass() {
if (!this._initialized) {
super._initSubclass();
@@ -846,14 +888,3 @@ class PluginConfigGroup extends ConfigGroup {
}
}
}
module.exports = {
InvalidAccess,
InvalidField,
InvalidValue,
Config,
ConfigGroup,
PluginConfigGroup,
};
@@ -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<Object>} 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();
-13
View File
@@ -1,13 +0,0 @@
/**
* Configuration System
* @author Hornwitser
* @module lib/config
*/
"use strict";
const classes = require("./classes");
const definitions = require("./definitions");
module.exports = {
...classes,
...definitions,
};
+7
View File
@@ -0,0 +1,7 @@
/**
* Configuration System
* @author Hornwitser
* @module lib/config
*/
export * from "./classes";
export * from "./definitions";
-46
View File
@@ -1,46 +0,0 @@
"use strict";
/**
* Export manifest for a mod pack
*
* Tracks files stored on disk for data that extracted from Factorio using a
* given mod pack.
* @alias module:lib.ExportManifest
*/
class ExportManifest {
/**
* Mapping between known assets and their file names.
*
* Currently exported assets are
* - settings: JSON of mod settings prototypes.
* - prototypes: JSON of all game prototypes except settings.
* - item-spritesheet: PNG containing icons from the game.
* - item-metadata: JSON of Map entries describing icons in item-spritesheet
* - locale: JSON of flattened Map entries of en locale strings from the game.
* @type {Object<string, string>}
*/
assets = {};
constructor(assets) {
if (assets) { this.assets = assets; }
}
static jsonSchema = {
type: "object",
additionalProperties: false,
required: ["assets"],
properties: {
"assets": {
type: "object",
additionalProperties: { type: "string" },
},
},
};
static fromJSON(json) {
return new this(json.assets);
}
}
module.exports = ExportManifest;
+33
View File
@@ -0,0 +1,33 @@
import { Type, Static } from "@sinclair/typebox";
/**
* Export manifest for a mod pack
*
* Tracks files stored on disk for data that extracted from Factorio using a
* given mod pack.
*/
export default class ExportManifest {
constructor(
/**
* Mapping between known assets and their file names.
*
* Currently exported assets are
* - settings: JSON of mod settings prototypes.
* - prototypes: JSON of all game prototypes except settings.
* - item-spritesheet: PNG containing icons from the game.
* - item-metadata: JSON of Map entries describing icons in item-spritesheet
* - locale: JSON of flattened Map entries of en locale strings from the game.
*/
public assets: Record<string, string>
) {
if (assets) { this.assets = assets; }
}
static jsonSchema = Type.Object({
"assets": Type.Record(Type.String(), Type.String()),
});
static fromJSON(json: Static<typeof ExportManifest.jsonSchema>) {
return new this(json.assets);
}
}
@@ -1,13 +1,12 @@
"use strict";
const fs = require("fs-extra");
const JSZip = require("jszip");
const path = require("path");
import fs from "fs-extra";
import JSZip from "jszip";
import { Type, Static } from "@sinclair/typebox";
const libHash = require("../hash");
const libSchema = require("../schema");
const { findRoot } = require("../zip_ops");
import * as libHash from "../hash";
import * as libSchema from "../schema";
import { findRoot } from "../zip_ops";
const { integerModVersion, integerFactorioVersion, modVersionRegExp } = require("./version");
import { integerModVersion, integerFactorioVersion, modVersionRegExp } from "./version";
/**
@@ -15,27 +14,23 @@ const { integerModVersion, integerFactorioVersion, modVersionRegExp } = require(
*
* See https://wiki.factorio.com/Tutorial:Mod_structure#info.json for
* details of the invidiual fields sourced from info.json.
* @alias module:lib.ModInfo
*/
class ModInfo {
export default class ModInfo {
/**
* Internal name of this mod.
* This is the name of the zip file as well as the name that appears in
* info.json.
* @type {string}
*/
name = "";
/**
* Version of the mod.
* Sourced from info.json.
* @type {string}
*/
version = "";
/**
* Integer representation of the version
* @type {number}
*/
get integerVersion() {
return integerModVersion(this.version);
@@ -44,42 +39,36 @@ class ModInfo {
/**
* Display name of the mod.
* Sourced from info.json.
* @type {string}
*/
title = "";
/**
* Author of the mod.
* Sourced from info.json.
* @type {string}
*/
author = "";
/**
* Contact field for mod.
* Sourced from info.json.
* @type {string}
*/
contact = "";
/**
* Homepage of the mod.
* Sourced from info.json.
* @type {string}
*/
homepage = "";
/**
* Description of the mod.
* Sourced from info.json.
* @type {string}
*/
description = "";
/**
* Major version of Factorio this mod supports.
* Sourced from info.json.
* @type {string}
*/
factorioVersion = "0.12";
@@ -94,13 +83,11 @@ class ModInfo {
/**
* Dependiences for this mod.
* Sourced from info.json.
* @type {Array<string>}
*/
dependencies = ["base"];
/**
* Expected name of zip file containing this mod.
* @type {number}
*/
get filename() {
return `${this.name}_${this.version}.zip`;
@@ -108,54 +95,44 @@ class ModInfo {
/**
* Size of the mod in bytes
* @type {number}
*/
size = 0;
/**
* SHA1 hash of this mod
* @type {string=}
*/
sha1;
sha1?: string;
/**
* True if this mod has been deleted
* @type {boolean}
*/
isDeleted = false;
// Content of info.json found in mod files
static infoJsonSchema = {
type: "object",
required: ["name", "version", "title", "author"],
properties: {
"name": { type: "string" },
"version": { type: "string" },
"title": { type: "string" },
"author": { type: "string" },
"contact": { type: "string" },
"homepage": { type: "string" },
"description": { type: "string" },
"factorio_version": { type: "string" },
"dependencies": { type: "array", items: { type: "string" } },
},
};
static infoJsonSchema = Type.Object({
"name": Type.String(),
"version": Type.String(),
"title": Type.String(),
"author": Type.String(),
"contact": Type.Optional(Type.String()),
"homepage": Type.Optional(Type.String()),
"description": Type.Optional(Type.String()),
"factorio_version": Type.Optional(Type.String()),
"dependencies": Type.Optional(Type.Array(Type.String())),
});
static validateInfo = libSchema.compile(this.infoJsonSchema);
static validateInfo = libSchema.compile<Static<typeof this.infoJsonSchema>>(this.infoJsonSchema as any);
static jsonSchema = {
type: "object",
properties: {
...this.infoJsonSchema.properties,
"size": { type: "integer" },
"sha1": { type: "string" },
"is_deleted": { type: "boolean" },
},
};
static jsonSchema = Type.Object({
...this.infoJsonSchema.properties,
"size": Type.Optional(Type.Integer()),
"sha1": Type.Optional(Type.String()),
"is_deleted": Type.Optional(Type.Boolean()),
});
static validate = libSchema.compile(this.jsonSchema);
static validate = libSchema.compile(this.jsonSchema as any);
static fromJSON(json) {
static fromJSON(json: Static<typeof ModInfo.jsonSchema>) {
const modInfo = new this();
// info.json fields
@@ -178,7 +155,7 @@ class ModInfo {
}
toJSON() {
let json = {
let json: Static<typeof ModInfo.jsonSchema> = {
name: this.name,
version: this.version,
title: this.title,
@@ -187,7 +164,7 @@ class ModInfo {
if (this.contact) { json.contact = this.contact; }
if (this.homepage) { json.homepage = this.homepage; }
if (this.description) { json.description = this.description; }
if (this.factorio_version !== "0.12") { json.factorio_version = this.factorioVersion; }
if (this.factorioVersion !== "0.12") { json.factorio_version = this.factorioVersion; }
if (this.dependencies.length !== 1 || this.dependencies[0] !== "base") {
json.dependencies = this.dependencies;
}
@@ -197,13 +174,13 @@ class ModInfo {
return json;
}
static async fromModFile(modPath) {
let modInfo;
static async fromModFile(modPath: string) {
let modInfo: Static<typeof ModInfo.jsonSchema>;
{
// XXX: JSZip needs the whole archive loaded in memory to work.
// This is clearly untenable and will be replaced later.
let zip = await JSZip.loadAsync(await fs.readFile(modPath));
let root = zip.folder(findRoot(zip));
let root = zip.folder(findRoot(zip))!;
let infoFile = root.file("info.json");
if (!infoFile) {
@@ -231,5 +208,3 @@ class ModInfo {
});
}
}
module.exports = ModInfo;
@@ -1,84 +1,81 @@
"use strict";
const zlib = require("zlib");
import zlib from "zlib";
import { Type, Static } from "@sinclair/typebox";
const libSchema = require("../schema");
import * as libSchema from "../schema";
const ExportManifest = require("./ExportManifest");
const { integerFactorioVersion } = require("./version");
import ExportManifest from "./ExportManifest";
import { integerFactorioVersion } from "./version";
import type { Logger } from "../logging";
/**
* A setting for a mod.
* @typedef {object} module:lib.ModPack~ModSetting
* @property {boolean|number|string} value - Value of the given mod setting.
*/
const ModSettingJsonSchema = {
type: "object",
additionalProperties: false,
required: ["value"],
properties: {
"value": { type: ["boolean", "number", "string"] },
},
};
interface ModSetting {
/** Value of the given mod setting. */
value: boolean | number | string;
}
const ModSettingsJsonSchema = {
type: "object",
additionalProperties: ModSettingJsonSchema,
};
const ModSettingJsonSchema = Type.Object({
"value": Type.Union([Type.Boolean(), Type.Number(), Type.String()]),
});
const ModSettingsJsonSchema = Type.Record(Type.String(), ModSettingJsonSchema);
/**
* A mod in a mod pack
* @typedef {object} module:lib.ModPack~ModRecord
* @property {string} name - name of the mod.
* @property {boolean} enabled - if mod is to be loaded.
* @property {string} version - version of the mod.
* @property {string=} sha1 - SHA1 hash of the zip file.
*/
const ModRecordJsonSchema = {
type: "object",
required: ["name", "enabled", "version"],
properties: {
"name": { type: "string" },
"enabled": { type: "boolean" },
"version": { type: "string" },
"sha1": { type: "string" },
},
};
interface ModRecord {
/** name of the mod. */
name: string,
/** if mod is to be loaded. */
enabled: boolean,
/** version of the mod. */
version: string,
/** SHA1 hash of the zip file. */
sha1?: string,
}
const ModRecordJsonSchema = Type.Object({
"name": Type.String(),
"enabled": Type.Boolean(),
"version": Type.String(),
"sha1": Type.Optional(Type.String()),
});
type PropertyType = null | boolean | number | string | PropertyType[] | { [key: string]: PropertyType };
/**
* Factorio Mod Pack
*
* Tracks mods and settings for a collection of Factorio mods.
* @alias module:lib.ModPack
*/
class ModPack {
export default class ModPack {
declare ["constructor"]: typeof ModPack;
/**
* Id of this mod pack
* @type {number}
*/
id;
id?: number;
/**
* Name of this mod pack.
* @type {string}
*/
name = "";
/**
* Description for this modpcak.
* @type {string}
*/
description = "";
/**
* Version of Factorio this mod pack is for
* @type {string}
*/
factorioVersion = "1.1.0";
/**
* Integer representation of the factorioVersion
* @type {number}
*/
get integerFactorioVersion() {
return integerFactorioVersion(this.factorioVersion);
@@ -86,29 +83,25 @@ class ModPack {
/**
* Mods included in this mod pack
* @type {Map<string, module:lib.ModPack~ModRecord>}
*/
mods = new Map();
mods = new Map<string, ModRecord>();
/**
* Mod settings for this mod pack
* @type {Object<string, Map<string, module:lib.ModPack~ModSetting>>=}
*/
settings = {
"startup": new Map(),
"runtime-global": new Map(),
"runtime-per-user": new Map(),
"startup": new Map<string, ModSetting>(),
"runtime-global": new Map<string, ModSetting>(),
"runtime-per-user": new Map<string, ModSetting>(),
};
/**
* Mapping to files containing exported data for this modpack
* @type {module:lib.ExportManifest|undefined}
*/
exportManifest;
exportManifest?: ExportManifest;
/**
* True if this mod pack has been deleted from the list of mod packs.
* @type {boolean}
*/
isDeleted = false;
@@ -125,37 +118,24 @@ class ModPack {
return clone;
}
static jsonSchema = {
type: "object",
additionalProperties: false,
required: ["name", "description", "factorio_version", "mods", "settings"],
properties: {
"id": { type: "integer" },
"name": { type: "string" },
"description": { type: "string" },
"factorio_version": { type: "string" },
"mods": {
type: "array",
items: ModRecordJsonSchema,
},
"settings": {
type: "object",
additionalProperties: false,
required: ["startup", "runtime-global", "runtime-per-user"],
properties: {
"startup": ModSettingsJsonSchema,
"runtime-global": ModSettingsJsonSchema,
"runtime-per-user": ModSettingsJsonSchema,
},
},
"export_manifest": ExportManifest.jsonSchema,
"is_deleted": { type: "boolean" },
},
};
static jsonSchema = Type.Object({
"id": Type.Optional(Type.Integer()),
"name": Type.String(),
"description": Type.String(),
"factorio_version": Type.String(),
"mods": Type.Array(ModRecordJsonSchema),
"settings": Type.Object({
"startup": ModSettingsJsonSchema,
"runtime-global": ModSettingsJsonSchema,
"runtime-per-user": ModSettingsJsonSchema,
}),
"export_manifest": Type.Optional(ExportManifest.jsonSchema),
"is_deleted": Type.Optional(Type.Boolean()),
});
static validate = libSchema.compile(this.jsonSchema);
static validate = libSchema.compile<Static<typeof ModPack.jsonSchema>>(this.jsonSchema as any);
static fromJSON(json) {
static fromJSON(json: Static<typeof ModPack.jsonSchema>) {
const modPack = new this();
if (json.id) {
@@ -185,7 +165,7 @@ class ModPack {
}
toJSON() {
let json = {
let json: Static<typeof ModPack.jsonSchema> = {
id: this.id,
name: this.name,
description: this.description,
@@ -212,21 +192,21 @@ class ModPack {
return buf.toString("base64");
}
static fromModPackString(modPackString) {
static fromModPackString(modPackString: string) {
let buf = Buffer.from(modPackString, "base64");
try {
// eslint-disable-next-line node/no-sync
buf = zlib.inflateSync(buf);
} catch (err) {
} catch (err: any) {
if (err.code.startsWith("Z_")) {
throw new Error("Malformed mod pack string: zlib inflate failed");
}
}
let json;
let json: unknown;
try {
json = JSON.parse(buf);
} catch (err) {
json = JSON.parse(buf.toString());
} catch (err: any) {
throw new Error(`Malformed mod pack string: ${err.message}`);
}
@@ -238,16 +218,16 @@ class ModPack {
}
toModSettingsDat() {
function uint8Byte(int) {
function uint8Byte(int: number) {
return Buffer.from(Uint8Array.from([int]).buffer);
}
function int16Bytes(int) {
function int16Bytes(int: number) {
return Buffer.from(Int16Array.from([int]).buffer);
}
function uint32Bytes(int) {
function uint32Bytes(int: number) {
return Buffer.from(Uint32Array.from([int]).buffer);
}
function uint32SpaceOptimizedBytes(int) {
function uint32SpaceOptimizedBytes(int: number) {
if (0 <= int && int < 0xff) {
return uint8Byte(int);
}
@@ -256,17 +236,17 @@ class ModPack {
uint32Bytes(int),
]);
}
function doubleBytes(double) {
function doubleBytes(double: number) {
return Buffer.from(Float64Array.from([double]).buffer);
}
function stringBytes(str) {
function stringBytes(str: string) {
const buf = Buffer.from(str, "utf8");
return Buffer.concat([
uint32SpaceOptimizedBytes(buf.length),
buf,
]);
}
function immutableStringBytes(str) {
function immutableStringBytes(str?: string) {
if (str === undefined) {
return uint8Byte(1); // empty
}
@@ -275,7 +255,7 @@ class ModPack {
stringBytes(str),
]);
}
function versionBytes(version) {
function versionBytes(version: string) {
const [main, major, minor] = version.split(".").map(n => Number.parseInt(n, 10));
return Buffer.concat([
int16Bytes(main),
@@ -285,7 +265,7 @@ class ModPack {
Buffer.alloc(1), // reserved byte
]);
}
function propertyTreeListBytes(entries) {
function propertyTreeListBytes(entries: [string | undefined, PropertyType][]) {
const sizeBytes = uint32Bytes(entries.length);
const itemBytes = entries.flatMap(([key, item]) => [
immutableStringBytes(key),
@@ -297,15 +277,15 @@ class ModPack {
...itemBytes,
]);
}
function propertyTreeBytes(element) {
let type;
let dataBytes;
function propertyTreeBytes(element: PropertyType) {
let type: number;
let dataBytes: Buffer;
if (typeof element === null) {
type = 1;
dataBytes = Buffer.alloc(0);
} else if (typeof element === "boolean") {
type = 1;
dataBytes = uint8Byte(element);
dataBytes = uint8Byte(Number(element));
} else if (typeof element === "number") {
type = 2;
dataBytes = doubleBytes(element);
@@ -315,7 +295,7 @@ class ModPack {
} else if (element instanceof Array) {
type = 4;
dataBytes = propertyTreeListBytes(element.map(item => [undefined, item]));
} else if (typeof element === "object") {
} else if (typeof element === "object" && element !== null) {
type = 5;
dataBytes = propertyTreeListBytes(Object.entries(element));
} else {
@@ -340,11 +320,11 @@ class ModPack {
* Uses the provided setting prototypes to add any missing mod settings
* in the mod pack with the default value from the prototype.
*
* @param {Object<string, object>} settingPrototypes -
* @param settingPrototypes -
* Setting prototypes exported from the game.
* @param {Logger} logger - Logger used to report warnings on.
* @param logger - Logger used to report warnings on.
*/
fillDefaultSettings(settingPrototypes, logger) {
fillDefaultSettings(settingPrototypes: Record<string, object>, logger: Logger) {
const knownTypes = ["bool-setting", "int-setting", "double-setting", "string-setting"];
let prototypes = Object.entries(settingPrototypes)
.filter(([type, _]) => knownTypes.includes(type))
@@ -352,21 +332,20 @@ class ModPack {
;
for (let prototype of prototypes) {
if (!["startup", "runtime-global", "runtime-per-user"].includes(prototype.setting_type)) {
logger.warn(`Ignoring ${prototype.name} with unknown setting_type ${prototype.setting_type}`);
const settingType = prototype.setting_type as keyof typeof this.settings;
if (!["startup", "runtime-global", "runtime-per-user"].includes(settingType)) {
logger.warn(`Ignoring ${prototype.name} with unknown setting_type ${settingType}`);
continue;
}
if (prototype.default_value === undefined) {
logger.warn(`Ignoring ${prototype.name} with missing default_value`);
continue;
}
if (this.settings[prototype.setting_type].get(prototype.name)) {
if (this.settings[settingType].get(prototype.name)) {
continue;
}
this.settings[prototype.setting_type].set(prototype.name, { value: prototype.default_value });
this.settings[settingType].set(prototype.name, { value: prototype.default_value });
}
}
}
module.exports = ModPack;
-32
View File
@@ -1,32 +0,0 @@
"use strict";
function jsonArray(ItemClass) {
return {
jsonSchema: {
type: "array",
items: ItemClass.jsonSchema,
},
fromJSON(json) {
return json.map(i => ItemClass.fromJSON(i));
},
};
}
function jsonPrimitive(type) {
return {
jsonSchema: { type },
fromJSON(json) {
return json;
},
};
}
module.exports = {
JsonBoolean: jsonPrimitive("boolean"),
JsonNumber: jsonPrimitive("number"),
JsonString: jsonPrimitive("string"),
jsonArray,
jsonPrimitive,
};
+34
View File
@@ -0,0 +1,34 @@
import { Type } from "@sinclair/typebox";
export function StringEnum<T extends string[]>(values: [...T]) {
return Type.Unsafe<T[number]>({ type: 'string', enum: values })
}
export interface JSONDeserialisable<T> {
fromJSON(json: unknown): T;
jsonSchema: object;
}
export function jsonArray<T>(ItemClass: JSONDeserialisable<T>) {
return {
jsonSchema: {
type: "array",
items: ItemClass.jsonSchema,
},
fromJSON(json: Array<T>) {
return json.map(i => ItemClass.fromJSON(i));
},
};
}
export function jsonPrimitive<T, U>(type: U) {
return {
jsonSchema: { type },
fromJSON(json: T) {
return json;
},
};
}
export const JsonBoolean = jsonPrimitive<boolean, "boolean">("boolean");
export const JsonNumber = jsonPrimitive<number, "number">("number");
export const JsonString = jsonPrimitive<string, "string">("string");
-31
View File
@@ -1,31 +0,0 @@
/**
* Shared data types used in Clusterio
* @module lib/data
* @author Hornwitser
*/
"use strict";
const ExportManifest = require("./ExportManifest");
const ModInfo = require("./ModInfo");
const ModPack = require("./ModPack");
const composites = require("./composites");
const messagesCore = require("./messages_core");
const messagesController = require("./messages_controller");
const messagesHost = require("./messages_host");
const messagesInstance = require("./messages_instance");
const messagesMod = require("./messages_mod");
const messagesUser = require("./messages_user");
const version = require("./version");
module.exports = {
ExportManifest,
ModInfo,
ModPack,
...composites,
...messagesCore,
...messagesController,
...messagesHost,
...messagesInstance,
...messagesMod,
...messagesUser,
...version,
};
+16
View File
@@ -0,0 +1,16 @@
/**
* Shared data types used in Clusterio
* @module lib/data
* @author Hornwitser
*/
export { default as ExportManifest } from "./ExportManifest";
export { default as ModInfo } from "./ModInfo";
export { default as ModPack } from "./ModPack";
export * from "./composites";
export * from "./messages_core";
export * from "./messages_controller";
export * from "./messages_host";
export * from "./messages_instance";
export * from "./messages_mod";
export * from "./messages_user";
export * from "./version";
+4
View File
@@ -0,0 +1,4 @@
import type { ValidateFunction } from "ajv";
import type { Message } from "./messages_core";
var message_validate: ValidateFunction<Message>;
export default message_validate;
@@ -1,384 +0,0 @@
"use strict";
const { JsonString } = require("./composites");
// TODO: remove after config refactor
/** @memberof module:lib */
class RawConfig {
/** @type {object} */
serializedConfig;
constructor(serializedConfig) {
this.serializedConfig = serializedConfig;
}
static jsonSchema = {
type: "object",
required: ["serializedConfig"],
properties: {
"serializedConfig": { type: "object" },
},
};
static fromJSON(json) {
return new this(json.serializedConfig);
}
}
/* @memberof module:lib */
class ControllerConfigGetRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.controller.get_config";
static Response = RawConfig;
}
/** @memberof module:lib */
class ControllerConfigSetFieldRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.controller.update_config";
/** @type {string} */
field;
/** @type {string} */
value;
constructor(field, value) {
this.field = field;
this.value = value;
}
static jsonSchema = {
type: "object",
required: ["field", "value"],
properties: {
"field": { type: "string" },
"value": { type: "string" },
},
};
static fromJSON(json) {
return new this(json.field, json.value);
}
}
/* @memberof module:lib */
class ControllerConfigSetPropRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.controller.update_config";
/** @type {string} */
field;
/** @type {string} */
prop;
/** @type {undefined} */
value;
constructor(field, prop, value) {
this.field = field;
this.prop = prop;
if (value) { this.value = value; }
}
static jsonSchema = {
type: "object",
required: ["field", "prop"],
properties: {
field: { type: "string" },
prop: { type: "string" },
value: {},
},
};
static fromJSON(json) {
return new this(json.field, json.prop, json.value);
}
}
/* @memberof module:lib */
class HostGenerateTokenRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.host.generate_token";
/** @type {?number} */
hostId;
constructor(hostId) {
this.hostId = hostId;
}
static jsonSchema = {
type: "object",
required: ["hostId"],
properties: {
hostId: { type: ["integer", "null"] },
},
};
static fromJSON(json) {
return new this(json.hostId);
}
static Response = JsonString;
}
/** @memberof module:lib */
class HostConfigCreateRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.host.create_config";
/** @type {?number} */
id;
/** @type {?string} */
name;
/** @type {boolean} */
generateToken;
constructor(id, name, generateToken) {
this.id = id;
this.name = name;
this.generateToken = generateToken;
}
static jsonSchema = {
type: "object",
required: ["id", "name", "generateToken"],
properties: {
"id": { type: ["integer", "null"] },
"name": { type: ["string", "null"] },
"generateToken": { type: "boolean" },
},
};
static fromJSON(json) {
return new this(json.id, json.name, json.generateToken);
}
static Response = RawConfig;
}
/* @memberof module:lib */
class LogSetSubscriptionsRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.log.follow";
/** @type {boolean} */
all;
/** @type {boolean} */
controller;
/** @type {array} */
hostIds;
/** @type {array} */
instanceIds;
/** @type {?string} */
maxLevel;
constructor(all, controller, hostIds, instanceIds, maxLevel) {
this.all = all;
this.controller = controller;
this.hostIds = hostIds;
this.instanceIds = instanceIds;
this.maxLevel = maxLevel;
}
static jsonSchema = {
type: "object",
required: ["all", "controller", "hostIds", "instanceIds", "maxLevel"],
properties: {
all: { type: "boolean" },
controller: { type: "boolean" },
hostIds: {
type: "array",
items: { type: "integer" },
},
instanceIds: {
type: "array",
items: { type: "integer" },
},
maxLevel: { type: ["string", "null"] },
},
};
static fromJSON(json) {
return new this(json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel);
}
}
/* @memberof module:lib */
class LogQueryRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.log.query";
/** @type {boolean} */
all;
/** @type {boolean} */
controller;
/** @type {Array<number>} */
hostIds;
/** @type {Array<number>} */
instanceIds;
/** @type {?string} */
maxLevel;
/** @type {integer} */
limit;
/** @type {undefined} */
order;
constructor(all, controller, hostIds, instanceIds, maxLevel, limit, order) {
this.all = all;
this.controller = controller;
this.hostIds = hostIds;
this.instanceIds = instanceIds;
this.maxLevel = maxLevel;
this.limit = limit;
this.order = order;
}
static jsonSchema = {
type: "object",
required: ["all", "controller", "hostIds", "instanceIds", "maxLevel", "limit", "order"],
properties: {
all: { type: "boolean" },
controller: { type: "boolean" },
hostIds: {
type: "array",
items: { type: "integer" },
},
instanceIds: {
type: "array",
items: { type: "integer" },
},
maxLevel: { type: ["string", "null"] },
limit: { type: "integer" },
order: {
enum: ["asc", "desc"],
},
},
};
static fromJSON(json) {
return new this(
json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel, json.limit, json.order
);
}
}
LogQueryRequest.Response = class Response {
/** @type {Array<object>} */
log;
constructor(log) {
this.log = log;
}
static jsonSchema = {
type: "object",
required: ["log"],
properties: {
log: {
type: "array",
items: { type: "object" },
},
},
};
static fromJSON(json) {
return new this(json.log);
}
};
/** @memberof module:lib */
class LogMessageEvent {
static type = "event";
static src = ["host", "controller"];
static dst = ["controller", "control"];
/** @type {object} */
info;
constructor(info) {
this.info = info;
}
static jsonSchema = {
type: "object",
required: ["info"],
properties: {
"info": {
type: "object",
required: ["level", "message"],
properties: {
"level": { type: "string" },
"message": { type: "string" },
},
},
},
};
static fromJSON(json) {
return new this(json.info);
}
}
/* @memberof module:lib */
class DebugDumpWsRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.debug.dump_ws";
}
/* @memberof module:lib */
class DebugWsMessageEvent {
static type = "event";
static src = "controller";
static dst = "control";
/** @type {string} */
direction;
/** @type {string} */
content;
constructor(direction, content) {
this.direction = direction;
this.content = content;
}
static jsonSchema = {
type: "object",
required: ["direction", "content"],
properties: {
direction: { type: "string" },
content: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.direction, json.content);
}
}
module.exports = {
RawConfig,
ControllerConfigGetRequest,
ControllerConfigSetFieldRequest,
ControllerConfigSetPropRequest,
HostGenerateTokenRequest,
HostConfigCreateRequest,
LogSetSubscriptionsRequest,
LogQueryRequest,
LogMessageEvent,
DebugDumpWsRequest,
DebugWsMessageEvent,
};
@@ -0,0 +1,247 @@
import { Type, Static } from "@sinclair/typebox";
import { JsonString, StringEnum } from "./composites";
import { levels } from "../logging";
// TODO: remove after config refactor
export class RawConfig {
constructor(
public serializedConfig: object
) { }
static jsonSchema = Type.Object({
"serializedConfig": Type.Object({}),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.serializedConfig);
}
}
export class ControllerConfigGetRequest {
declare ["constructor"]: typeof ControllerConfigGetRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.controller.get_config" as const;
static Response = RawConfig;
}
export class ControllerConfigSetFieldRequest {
declare ["constructor"]: typeof ControllerConfigSetFieldRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.controller.update_config" as const;
constructor(
public field: string,
public value: string,
) { }
static jsonSchema = Type.Object({
"field": Type.String(),
"value": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.field, json.value);
}
}
export class ControllerConfigSetPropRequest {
declare ["constructor"]: typeof ControllerConfigSetPropRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.controller.update_config" as const;
constructor(
public field: string,
public prop: string,
public value?: unknown,
) { }
static jsonSchema = Type.Object({
"field": Type.String(),
"prop": Type.String(),
"value": Type.Optional(Type.Unknown()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.field, json.prop, json.value);
}
}
export class HostGenerateTokenRequest {
declare ["constructor"]: typeof HostGenerateTokenRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.host.generate_token" as const;
constructor(
public hostId?: number,
) { }
static jsonSchema = Type.Object({
"hostId": Type.Optional(Type.Integer()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.hostId);
}
static Response = JsonString;
}
export class HostConfigCreateRequest {
declare ["constructor"]: typeof HostConfigCreateRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.host.create_config" as const;
constructor(
public id: number | undefined,
public name: string | undefined,
public generateToken: boolean,
) { }
static jsonSchema = Type.Object({
"id": Type.Optional(Type.Integer()),
"name": Type.Optional(Type.String()),
"generateToken": Type.Boolean(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.id, json.name, json.generateToken);
}
static Response = RawConfig;
}
export class LogSetSubscriptionsRequest {
declare ["constructor"]: typeof LogSetSubscriptionsRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.log.follow" as const;
constructor(
public all?: boolean,
public controller?: boolean,
public hostIds?: number[],
public instanceIds?: number[],
public maxLevel?: keyof typeof levels,
) { }
static jsonSchema = Type.Object({
all: Type.Optional(Type.Boolean()),
controller: Type.Optional(Type.Boolean()),
hostIds: Type.Array(Type.Integer()),
instanceIds: Type.Array(Type.Integer()),
maxLevel: Type.Optional(StringEnum(Object.keys(levels) as (keyof typeof levels)[])),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel);
}
}
export class LogQueryRequest {
declare ["constructor"]: typeof LogQueryRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.log.query" as const;
constructor(
public all: boolean,
public controller: boolean,
public hostIds: number[],
public instanceIds: number[],
public maxLevel: undefined | keyof typeof levels,
public limit: number,
public order: "asc" | "desc",
) { }
static jsonSchema = Type.Object({
all: Type.Boolean(),
controller: Type.Boolean(),
hostIds: Type.Array(Type.Integer()),
instanceIds: Type.Array(Type.Integer()),
maxLevel: Type.Optional(StringEnum(Object.keys(levels) as (keyof typeof levels)[])),
limit: Type.Integer(),
order: StringEnum(["asc", "desc"]),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel, json.limit, json.order
);
}
static Response = class Response {
constructor(
public log: object[],
) { }
static jsonSchema = Type.Object({
"log": Type.Array(Type.Object({})),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.log);
}
};
}
export class LogMessageEvent {
declare ["constructor"]: typeof LogMessageEvent;
static type = "event" as const;
static src = ["host", "controller"] as const;
static dst = ["controller", "control"] as const;
constructor(
public info: object,
) { }
static jsonSchema = Type.Object({
"info": Type.Object({
"level": Type.String(),
"message": Type.String(),
}),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.info);
}
}
export class DebugDumpWsRequest {
declare ["constructor"]: typeof DebugDumpWsRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.debug.dump_ws" as const;
}
export class DebugWsMessageEvent {
declare ["constructor"]: typeof DebugWsMessageEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "control" as const;
constructor(
public direction: string,
public content: string,
) { }
static jsonSchema = Type.Object({
"direction": Type.String(),
"content": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.direction, json.content);
}
}
-890
View File
@@ -1,890 +0,0 @@
// Note: Run compile_validator script after making changes to this file.
"use strict";
const message_validate = require("./message_validate.js");
/** @memberof module:lib */
class Address {
/** Controller address */
static controller = 0;
/** Instance address */
static instance = 1;
/** Host address */
static host = 2;
/** Control address */
static control = 3;
/** Broadcast */
static broadcast = 4;
/** @type {number} */
type;
/** @type {string} */
id;
/** @type {string|undefined} */
requestId;
constructor(type, id, requestId) {
if (typeof type !== "number") {
throw Error(`Invalid type ${type}`);
}
this.type = type;
this.id = id;
this.requestId = requestId;
}
/**
* Convert from convenient shorthand notation to address.
*
* Supported notations and their corresponding address:
* - `"controller"`: new Address(Address.controller, 0)
* - `{ hostId: id }`: new Address(Address.host, id)
* - `{ instanceId: id }`: new Address(Address.instance, id)
* - `{ controlId: id }`: new Address(Address.control, id)
* `"allHosts"`: new Address(Address.broadcast, Address.host)
* `"allInstances"`: new Address(Address.broadcast, Address.instance)
* `"allControls"`: new Address(Address.broadcast, Address.control)
* @param {module:lib.Address|string|object} shorthand -
* Shorthand to translate with.
* @returns {module:lib.Address} Translated address.
*/
static fromShorthand(shorthand) {
if (shorthand instanceof Address) { return shorthand; }
if (shorthand === "controller") { return new Address(Address.controller, 0); }
if (shorthand === "allHosts") { return new Address(Address.broadcast, Address.host); }
if (shorthand === "allInstances") { return new Address(Address.broadcast, Address.instance); }
if (shorthand === "allControls") { return new Address(Address.broadcast, Address.control); }
let entries = Object.entries(shorthand);
if (entries.length === 1) {
let [name, value] = entries[0];
if (name === "hostId") { return new Address(Address.host, value); }
if (name === "instanceId") { return new Address(Address.instance, value); }
if (name === "controlId") { return new Address(Address.control, value); }
}
throw Error("Unrecognized shorthand");
}
static jsonSchema = {
type: "array",
minItems: 2,
items: [
{ type: "integer", minimum: 0, maximum: 4 },
{ type: "integer" },
{ type: "integer" },
],
};
static fromJSON(json) {
return new this(json[0], json[1], json[2]);
}
toJSON() {
if (this.requestId !== undefined) {
return [this.type, this.id, this.requestId];
}
return [this.type, this.id];
}
toString() {
const typeMap = ["controller", "instance", "host", "control"];
const type = typeMap[this.type] || this.type;
if (this.type === Address.broadcast) {
return `[Address ${type}:${typeMap[this.id] || this.id}]`;
}
if (this.requestId === undefined) {
return `[Address ${type}:${this.id}]`;
}
return `[Address ${type}:${this.id}:${this.requestId}]`;
}
/**
* Returns a string suitable for indexing the address in a Map
* @returns {string} unique string for this address
*/
index() {
return `${this.type}:${this.id}:${this.requestId}`;
}
/**
* Returns true if this address targets the given destination
* @param {module:lib.Address} dst - Destination to check
* @returns {boolean} true if this address is addressed to the given destination
*/
addressedTo(dst) {
if (this.type !== Address.broadcast) {
return (
this.type === dst.type
&& this.id === dst.id
);
}
return dst.type !== Address.broadcast && this.id === dst.type;
}
equals(other) {
return (
this.type === other.type
&& this.id === other.id
&& this.requestId === other.requestId
);
}
}
/** @memberof module:lib */
class Message {
/** @type {string} */
type;
constructor(type) {
this.type = type;
}
static jsonSchema;
static validate;
static fromJSON(json) {
/* eslint-disable no-use-before-define */
if (json.type === "hello") { return MessageHello.fromJSON(json); }
if (json.type === "registerHost") { return MessageRegisterHost.fromJSON(json); }
if (json.type === "registerControl") { return MessageRegisterControl.fromJSON(json); }
if (json.type === "ready") { return MessageReady.fromJSON(json); }
if (json.type === "resume") { return MessageResume.fromJSON(json); }
if (json.type === "continue") { return MessageContinue.fromJSON(json); }
if (json.type === "invalidate") { return MessageInvalidate.fromJSON(json); }
if (json.type === "heartbeat") { return MessageHeartbeat.fromJSON(json); }
if (json.type === "request") { return MessageRequest.fromJSON(json); }
if (json.type === "response") { return MessageResponse.fromJSON(json); }
if (json.type === "responseError") { return MessageResponseError.fromJSON(json); }
if (json.type === "event") { return MessageEvent.fromJSON(json); }
if (json.type === "disconnect") { return MessageDisconnect.fromJSON(json); }
/* eslint-enable no-use-before-define */
throw new Error(`Unrecognized message type ${json.type}`);
}
}
/** @memberof module:lib */
class HelloData {
/** @type {string} */
version;
/** @type {object} */
plugins;
constructor(version, plugins) {
this.version = version;
this.plugins = plugins;
}
static jsonSchema = {
type: "object",
required: ["version", "plugins"],
properties: {
"version": { type: "string" },
"plugins": {
type: "object",
additionalProperties: { type: "string" },
},
},
};
static fromJSON(json) {
return new this(json.version, json.plugins);
}
}
/** @memberof module:lib */
class MessageHello extends Message {
/** @type {module:lib.HelloData} */
data;
constructor(data) {
super("hello");
this.data = data;
}
static jsonSchema = {
type: "object",
required: ["type", "data"],
properties: {
"type": { const: "hello" },
"data": HelloData.jsonSchema,
},
};
static fromJSON(json) {
return new this(HelloData.fromJSON(json.data));
}
};
/** @memberof module:lib */
class RegisterHostData {
/** @type {string} */
token;
/** @type {string} */
agent;
/** @type {string} */
version;
/** @type {string} */
name;
/** @type {string} */
id;
/** @type {string|undefined} */
publicAddress;
/** @type {Object<string, string>} */
plugins;
constructor(token, agent, version, name, id, publicAddress, plugins) {
this.token = token;
this.agent = agent;
this.version = version;
this.name = name;
this.id = id;
this.publicAddress = publicAddress;
this.plugins = plugins;
}
static jsonSchema = {
type: "object",
required: ["token", "agent", "version", "name", "id", "plugins"],
properties: {
"token": { type: "string" },
"agent": { type: "string" },
"version": { type: "string" },
"name": { type: "string" },
"id": { type: "integer" },
"publicAddress": { type: "string" },
"plugins": {
type: "object",
additionalProperties: { type: "string" },
},
},
};
static fromJSON(json) {
return new this(json.token, json.agent, json.version, json.name, json.id, json.publicAddress, json.plugins);
}
}
/** @memberof module:lib */
class MessageRegisterHost extends Message {
/** @type {module:lib.RegisterHostData} */
data;
constructor(data) {
super("registerHost");
this.data = data;
}
static jsonSchema = {
type: "object",
required: ["type", "data"],
properties: {
"type": { const: "registerHost" },
"data": RegisterHostData.jsonSchema,
},
};
static fromJSON(json) {
return new this(RegisterHostData.fromJSON(json.data));
}
};
/** @memberof module:lib */
class RegisterControlData {
/** @type {string} */
token;
/** @type {string} */
agent;
/** @type {string} */
version;
constructor(token, agent, version) {
this.token = token;
this.agent = agent;
this.version = version;
}
static jsonSchema = {
type: "object",
required: ["token", "agent", "version"],
properties: {
"token": { type: "string" },
"agent": { type: "string" },
"version": { type: "string" },
},
};
static fromJSON(json) {
return new this(json.token, json.agent, json.version);
}
}
/** @memberof module:lib */
class MessageRegisterControl extends Message {
/** @type {module:lib.RegisterControlData} */
data;
constructor(data) {
super("registerControl");
this.data = data;
}
static jsonSchema = {
type: "object",
required: ["data"],
properties: {
"type": { const: "registerControl" },
"data": RegisterControlData.jsonSchema,
},
};
static fromJSON(json) {
return new this(RegisterControlData.fromJSON(json.data));
}
};
/** @memberof module:lib */
class AccountDetails {
/** @type {string} */
name;
/** @type {Array} */
roles;
constructor(name, roles) {
this.name = name;
this.roles = roles;
}
static jsonSchema = {
type: "object",
required: ["name", "roles"],
properties: {
"name": { type: "string" },
"roles": {
type: "array",
items: {
type: "object",
required: ["name", "id", "permissions"],
properties: {
"name": { type: "string" },
"id": { type: "integer" },
"permissions": {
type: "array",
items: { type: "string" },
},
},
},
},
},
};
static fromJSON(json) {
return new this(json.name, json.roles);
}
}
/** @memberof module:lib */
class ReadyData {
/** @type {module:lib.Address} */
src;
/** @type {string} */
sessionToken;
/** @type {number} */
sessionTimeout;
/** @type {number} */
heartbeatInterval;
/** @type {AccountDetails|undefined} */
account;
constructor(src, sessionToken, sessionTimeout, heartbeatInterval, account) {
this.src = src;
this.sessionToken = sessionToken;
this.sessionTimeout = sessionTimeout;
this.heartbeatInterval = heartbeatInterval;
if (account) { this.account = account; }
}
static jsonSchema = {
type: "object",
required: ["src", "sessionToken", "sessionTimeout", "heartbeatInterval"],
properties: {
"src": Address.jsonSchema,
"sessionToken": { type: "string" },
"sessionTimeout": { type: "number" },
"heartbeatInterval": { type: "number" },
"account": AccountDetails.jsonSchema,
},
};
static fromJSON(json) {
let account;
if (json.account) { account = AccountDetails.fromJSON(json.account); }
return new this(
Address.fromJSON(json.src),
json.sessionToken,
json.sessionTimeout,
json.heartbeatInterval,
account
);
}
}
/** @memberof module:lib */
class MessageReady extends Message {
/** @type {module:lib.ReadyData} */
data;
constructor(data) {
super("ready");
this.data = data;
}
static jsonSchema = {
type: "object",
required: ["data"],
properties: {
"type": { const: "ready" },
"data": ReadyData.jsonSchema,
},
};
static fromJSON(json) {
return new this(ReadyData.fromJSON(json.data));
}
}
/** @memberof module:lib */
class ResumeData {
/** @type {string} */
sessionToken;
/** @type {integer|undefined} */
lastSeq;
constructor(sessionToken, lastSeq) {
this.sessionToken = sessionToken;
if (lastSeq !== undefined) { this.lastSeq = lastSeq; }
}
static jsonSchema = {
type: "object",
required: ["sessionToken"],
properties: {
"sessionToken": { type: "string" },
"lastSeq": { type: "integer" },
},
};
static fromJSON(json) {
return new this(json.sessionToken, json.lastSeq);
}
}
/** @memberof module:lib */
class MessageResume extends Message {
/** @type {module:lib.ResumeData} */
data;
constructor(data) {
super("resume");
this.data = data;
}
static jsonSchema = {
type: "object",
required: ["data"],
properties: {
"type": { const: "resume" },
"data": ResumeData.jsonSchema,
},
};
static fromJSON(json) {
return new this(ResumeData.fromJSON(json.data));
}
}
/** @memberof module:lib */
class ContinueData {
/** @type {number} */
sessionTimeout;
/** @type {number} */
heartbeatInterval;
/** @type {integer|undefined} */
lastSeq;
constructor(sessionTimeout, heartbeatInterval, lastSeq) {
this.sessionTimeout = sessionTimeout;
this.heartbeatInterval = heartbeatInterval;
if (lastSeq !== undefined) { this.lastSeq = lastSeq; }
}
static jsonSchema = {
type: "object",
required: ["sessionTimeout", "heartbeatInterval"],
properties: {
"sessionTimeout": { type: "number" },
"heartbeatInterval": { type: "number" },
"lastSeq": { type: "integer" },
},
};
static fromJSON(json) {
return new this(json.sessionTimeout, json.heartbeatInterval, json.lastSeq);
}
}
/** @memberof module:lib */
class MessageContinue extends Message {
/** @type {module:lib.ContinueData} */
data;
constructor(data) {
super("continue");
this.data = data;
}
static jsonSchema = {
type: "object",
required: ["data"],
properties: {
"type": { const: "continue" },
"data": ContinueData.jsonSchema,
},
};
static fromJSON(json) {
return new this(ContinueData.fromJSON(json.data));
}
}
/** @memberof module:lib */
class MessageInvalidate extends Message {
constructor() {
super("invalidate");
}
static jsonSchema = {
type: "object",
properties: {
"type": { const: "invalidate" },
},
};
static fromJSON(json) {
return new this();
}
}
/** @memberof module:lib */
class MessageHeartbeat extends Message {
/** @type {integer|undefined} */
seq;
constructor(seq) {
super("heartbeat");
this.seq = seq;
}
static jsonSchema = {
type: "object",
required: ["type"],
properties: {
"type": { const: "heartbeat" },
"seq": { type: "integer" },
},
};
static fromJSON(json) {
return new this(json.seq);
}
}
/** @memberof module:lib */
class MessageRequest extends Message {
/** @type {number} */
seq;
/** @type {Address} */
src;
/** @type {Address} */
dst;
/** @type {string} */
name;
/** @type {*} */
data;
constructor(seq, src, dst, name, data) {
super("request");
this.seq = seq;
this.src = src;
this.dst = dst;
this.name = name;
this.data = data;
}
static jsonSchema = {
type: "object",
required: ["type", "seq", "src", "dst", "name"],
properties: {
"type": { const: "request" },
"seq": { type: "integer" },
"src": Address.jsonSchema,
"dst": Address.jsonSchema,
"name": { type: "string" },
"data": {},
},
};
static fromJSON(json) {
return new this(
json.seq,
Address.fromJSON(json.src),
Address.fromJSON(json.dst),
json.name,
json.data,
);
}
};
/** @memberof module:lib */
class MessageResponse extends Message {
/** @type {number} */
seq;
/** @type {Address} */
src;
/** @type {Address} */
dst;
/** @type {*} */
data;
constructor(seq, src, dst, data) {
super("response");
this.seq = seq;
this.src = src;
this.dst = dst;
this.data = data;
}
static jsonSchema = {
type: "object",
required: ["type", "seq", "src", "dst"],
properties: {
"type": { const: "response" },
"seq": { type: "integer" },
"src": Address.jsonSchema,
"dst": Address.jsonSchema,
"data": {},
},
};
static fromJSON(json) {
return new this(
json.seq,
Address.fromJSON(json.src),
Address.fromJSON(json.dst),
json.data,
);
}
};
/** @memberof module:lib */
class ResponseError {
/** @type {string} */
message;
/** @type {string|undefined} */
code;
/** @type {string|undefined} */
stack;
constructor(message, code, stack) {
this.message = message;
if (code) { this.code = code; }
if (stack) { this.stack = stack; }
}
static jsonSchema = {
type: "object",
required: ["message"],
properties: {
"message": { type: "string" },
"code": { type: "string" },
"stack": { type: "string" },
},
};
static fromJSON(json) {
return new this(json.message, json.code, json.stack);
}
}
/** @memberof module:lib */
class MessageResponseError extends Message {
/** @type {number} */
seq;
/** @type {Address} */
src;
/** @type {Address} */
dst;
/** @type {module:lib.ResponseError} */
data;
constructor(seq, src, dst, data) {
super("responseError");
this.seq = seq;
this.src = src;
this.dst = dst;
this.data = data;
}
static jsonSchema = {
type: "object",
required: ["type", "seq", "src", "dst", "data"],
properties: {
"type": { const: "responseError" },
"seq": { type: "integer" },
"src": Address.jsonSchema,
"dst": Address.jsonSchema,
"data": ResponseError.jsonSchema,
},
};
static fromJSON(json) {
return new this(
json.seq,
Address.fromJSON(json.src),
Address.fromJSON(json.dst),
ResponseError.fromJSON(json.data),
);
}
};
/** @memberof module:lib */
class MessageEvent extends Message {
/** @type {number} */
seq;
/** @type {Address} */
src;
/** @type {Address} */
dst;
/** @type {string} */
name;
/** @type {*} */
data;
constructor(seq, src, dst, name, data) {
super("event");
this.seq = seq;
this.src = src;
this.dst = dst;
this.name = name;
this.data = data;
}
static jsonSchema = {
type: "object",
required: ["type", "seq", "src", "dst", "name"],
properties: {
"type": { const: "event" },
"seq": { type: "integer" },
"src": Address.jsonSchema,
"dst": Address.jsonSchema,
"name": { type: "string" },
"data": {},
},
};
static fromJSON(json) {
return new this(
json.seq,
Address.fromJSON(json.src),
Address.fromJSON(json.dst),
json.name,
json.data,
);
}
};
/** @memberof module:lib */
class MessageDisconnect extends Message {
/** @type {string} */
data;
constructor(stage) {
super("disconnect");
this.data = stage;
}
static jsonSchema = {
type: "object",
required: ["type"],
properties: {
"type": { const: "disconnect" },
"data": { enum: ["prepare", "ready"] },
},
};
static fromJSON(json) {
return new this(json.data);
}
};
Message.jsonSchema = {
type: "object",
required: ["type"],
properties: {
"type": { enum: [
"hello", "registerHost", "registerControl", "ready", "resume", "continue", "invalidate",
"heartbeat", "request", "response", "responseError", "event", "disconnect",
] },
},
oneOf: [
MessageHello.jsonSchema,
MessageRegisterHost.jsonSchema,
MessageRegisterControl.jsonSchema,
MessageReady.jsonSchema,
MessageResume.jsonSchema,
MessageContinue.jsonSchema,
MessageInvalidate.jsonSchema,
MessageHeartbeat.jsonSchema,
MessageRequest.jsonSchema,
MessageResponse.jsonSchema,
MessageResponseError.jsonSchema,
MessageEvent.jsonSchema,
MessageDisconnect.jsonSchema,
],
};
Message.validate = message_validate;
/** @memberof module:lib */
class PingRequest {
static type = "request";
static src = ["controller", "host", "control"];
static dst = ["controller", "host", "control"];
static permission = null;
}
module.exports = {
Message,
HelloData,
MessageHello,
RegisterHostData,
MessageRegisterHost,
RegisterControlData,
MessageRegisterControl,
AccountDetails,
ReadyData,
MessageReady,
ResumeData,
MessageResume,
ContinueData,
MessageContinue,
MessageInvalidate,
MessageHeartbeat,
Address,
MessageRequest,
MessageResponse,
ResponseError,
MessageResponseError,
MessageEvent,
MessageDisconnect,
PingRequest,
};
+630
View File
@@ -0,0 +1,630 @@
// Note: Run compile_validator script after making changes to this file.
import { TSchema, Type, Static } from "@sinclair/typebox";
import messageValidate from "./message_validate";
import { StringEnum } from "./composites";
export type AddressType = "controller" | "host" | "instance" | "control" | "broadcast";
export type AddressShorthand =
| "controller"
| "allHosts"
| "allInstances"
| "allControls"
| { instanceId: number }
| { hostId: number }
| { controlId: number }
| Address
;
export class Address {
/** Controller address */
static controller = 0;
/** Instance address */
static instance = 1;
/** Host address */
static host = 2;
/** Control address */
static control = 3;
/** Broadcast */
static broadcast = 4;
constructor(
public type: number,
public id: number,
public requestId?: number,
) { }
/**
* Convert from convenient shorthand notation to address.
*
* Supported notations and their corresponding address:
* - `"controller"`: new Address(Address.controller, 0)
* - `{ hostId: id }`: new Address(Address.host, id)
* - `{ instanceId: id }`: new Address(Address.instance, id)
* - `{ controlId: id }`: new Address(Address.control, id)
* `"allHosts"`: new Address(Address.broadcast, Address.host)
* `"allInstances"`: new Address(Address.broadcast, Address.instance)
* `"allControls"`: new Address(Address.broadcast, Address.control)
* @param shorthand -
* Shorthand to translate with.
* @returns Translated address.
*/
static fromShorthand(shorthand: AddressShorthand) {
if (shorthand instanceof Address) { return shorthand; }
if (shorthand === "controller") { return new Address(Address.controller, 0); }
if (shorthand === "allHosts") { return new Address(Address.broadcast, Address.host); }
if (shorthand === "allInstances") { return new Address(Address.broadcast, Address.instance); }
if (shorthand === "allControls") { return new Address(Address.broadcast, Address.control); }
let entries = Object.entries(shorthand);
if (entries.length === 1) {
let [name, value] = entries[0];
if (name === "hostId") { return new Address(Address.host, value); }
if (name === "instanceId") { return new Address(Address.instance, value); }
if (name === "controlId") { return new Address(Address.control, value); }
}
throw Error("Unrecognized shorthand");
}
static jsonSchema = Type.Unsafe<[number, number, number?]>({
type: "array",
minItems: 2,
items: [
{ type: "integer", minimum: 0, maximum: 4 },
Type.Integer(),
Type.Integer(),
],
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json[0], json[1], json[2]);
}
toJSON() {
if (this.requestId !== undefined) {
return [this.type, this.id, this.requestId];
}
return [this.type, this.id];
}
toString() {
const typeMap = ["controller", "instance", "host", "control"];
const type = typeMap[this.type] || this.type;
if (this.type === Address.broadcast) {
return `[Address ${type}:${typeMap[this.id] || this.id}]`;
}
if (this.requestId === undefined) {
return `[Address ${type}:${this.id}]`;
}
return `[Address ${type}:${this.id}:${this.requestId}]`;
}
/**
* Returns a string suitable for indexing the address in a Map
* @returns unique string for this address
*/
index() {
return `${this.type}:${this.id}:${this.requestId}`;
}
/**
* Returns true if this address targets the given destination
* @param dst - Destination to check
* @returns true if this address is addressed to the given destination
*/
addressedTo(dst: Address) {
if (this.type !== Address.broadcast) {
return (
this.type === dst.type
&& this.id === dst.id
);
}
return dst.type !== Address.broadcast && this.id === dst.type;
}
equals(other: Address) {
return (
this.type === other.type
&& this.id === other.id
&& this.requestId === other.requestId
);
}
}
type MessageType =
| "hello"
| "registerHost"
| "registerControl"
| "ready"
| "resume"
| "continue"
| "invalidate"
| "heartbeat"
| "request"
| "response"
| "responseError"
| "event"
| "disconnect"
;
export class Message {
constructor(
public type: MessageType,
) { }
static jsonSchema: TSchema;
static validate = messageValidate;
static fromJSON(json: any) {
/* eslint-disable no-use-before-define */
if (json.type === "hello") { return MessageHello.fromJSON(json); }
if (json.type === "registerHost") { return MessageRegisterHost.fromJSON(json); }
if (json.type === "registerControl") { return MessageRegisterControl.fromJSON(json); }
if (json.type === "ready") { return MessageReady.fromJSON(json); }
if (json.type === "resume") { return MessageResume.fromJSON(json); }
if (json.type === "continue") { return MessageContinue.fromJSON(json); }
if (json.type === "invalidate") { return MessageInvalidate.fromJSON(json); }
if (json.type === "heartbeat") { return MessageHeartbeat.fromJSON(json); }
if (json.type === "request") { return MessageRequest.fromJSON(json); }
if (json.type === "response") { return MessageResponse.fromJSON(json); }
if (json.type === "responseError") { return MessageResponseError.fromJSON(json); }
if (json.type === "event") { return MessageEvent.fromJSON(json); }
if (json.type === "disconnect") { return MessageDisconnect.fromJSON(json); }
/* eslint-enable no-use-before-define */
throw new Error(`Unrecognized message type ${json.type}`);
}
}
export class HelloData {
constructor(
public version: string,
public plugins: Record<string, string>,
) { }
static jsonSchema = Type.Object({
"version": Type.String(),
"plugins": Type.Record(Type.String(), Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.version, json.plugins);
}
}
export class MessageHello extends Message {
constructor(
public data: HelloData,
) {
super("hello");
}
static jsonSchema = Type.Object({
"type": Type.Literal("hello"),
"data": HelloData.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(HelloData.fromJSON(json.data));
}
};
export class RegisterHostData {
constructor(
public token: string,
public agent: string,
public version: string,
public name: string,
public id: number,
public publicAddress: string | undefined,
public plugins: Record<string, string>,
) { }
static jsonSchema = Type.Object({
"token": Type.String(),
"agent": Type.String(),
"version": Type.String(),
"name": Type.String(),
"id": Type.Integer(),
"publicAddress": Type.Optional(Type.String()),
"plugins": Type.Record(Type.String(), Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.token, json.agent, json.version, json.name, json.id, json.publicAddress, json.plugins);
}
}
export class MessageRegisterHost extends Message {
constructor(
public data: RegisterHostData,
) {
super("registerHost");
}
static jsonSchema = Type.Object({
"type": Type.Literal("registerHost"),
"data": RegisterHostData.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(RegisterHostData.fromJSON(json.data));
}
};
export class RegisterControlData {
constructor(
public token: string,
public agent: string,
public version: string,
) { }
static jsonSchema = Type.Object({
"token": Type.String(),
"agent": Type.String(),
"version": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.token, json.agent, json.version);
}
}
export class MessageRegisterControl extends Message {
constructor(
public data: RegisterControlData,
) {
super("registerControl");
}
static jsonSchema = Type.Object({
"type": Type.Literal("registerControl"),
"data": RegisterControlData.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(RegisterControlData.fromJSON(json.data));
}
};
export class AccountDetails {
constructor(
public name: string,
public roles: { name: string, id: number, permissions: string[] }[],
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"roles": Type.Array(
Type.Object({
"name": Type.String(),
"id": Type.Integer(),
"permissions": Type.Array(Type.String()),
}),
),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.roles);
}
}
export class ReadyData {
constructor(
public src: Address,
public sessionToken: string,
public sessionTimeout: number,
public heartbeatInterval: number,
public account?: AccountDetails,
) { }
static jsonSchema = Type.Object({
"src": Address.jsonSchema,
"sessionToken": Type.String(),
"sessionTimeout": Type.Number(),
"heartbeatInterval": Type.Number(),
"account": Type.Optional(AccountDetails.jsonSchema),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
let account: AccountDetails | undefined;
if (json.account) { account = AccountDetails.fromJSON(json.account); }
return new this(
Address.fromJSON(json.src),
json.sessionToken,
json.sessionTimeout,
json.heartbeatInterval,
account
);
}
}
export class MessageReady extends Message {
constructor(
public data: ReadyData,
) {
super("ready");
}
static jsonSchema = Type.Object({
"type": Type.Literal("ready"),
"data": ReadyData.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(ReadyData.fromJSON(json.data));
}
}
export class ResumeData {
constructor(
public sessionToken: string,
public lastSeq?: number,
) { }
static jsonSchema = Type.Object({
"sessionToken": Type.String(),
"lastSeq": Type.Optional(Type.Integer()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.sessionToken, json.lastSeq);
}
}
export class MessageResume extends Message {
constructor(
public data: ResumeData,
) {
super("resume");
}
static jsonSchema = Type.Object({
"type": Type.Literal("resume"),
"data": ResumeData.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(ResumeData.fromJSON(json.data));
}
}
export class ContinueData {
constructor(
public sessionTimeout: number,
public heartbeatInterval: number,
public lastSeq?: number,
) { }
static jsonSchema = Type.Object({
"sessionTimeout": Type.Number(),
"heartbeatInterval": Type.Number(),
"lastSeq": Type.Optional(Type.Integer()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.sessionTimeout, json.heartbeatInterval, json.lastSeq);
}
}
export class MessageContinue extends Message {
constructor(
public data: ContinueData,
) {
super("continue");
}
static jsonSchema = Type.Object({
"type": Type.Literal("continue"),
"data": ContinueData.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(ContinueData.fromJSON(json.data));
}
}
export class MessageInvalidate extends Message {
constructor() {
super("invalidate");
}
static jsonSchema = Type.Object({
"type": Type.Literal("invalidate"),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this();
}
}
export class MessageHeartbeat extends Message {
constructor(
public seq?: number,
) {
super("heartbeat");
}
static jsonSchema = Type.Object({
"type": Type.Literal("heartbeat"),
"seq": Type.Optional(Type.Integer()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.seq);
}
}
export class MessageRequest<T = unknown> extends Message {
constructor(
public seq: number,
public src: Address,
public dst: Address,
public name: string,
public data?: T,
) {
super("request");
}
static jsonSchema = Type.Object({
"type": Type.Literal("request"),
"seq": Type.Integer(),
"src": Address.jsonSchema,
"dst": Address.jsonSchema,
"name": Type.String(),
"data": Type.Optional(Type.Unknown()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.seq,
Address.fromJSON(json.src),
Address.fromJSON(json.dst),
json.name,
json.data,
);
}
};
export class MessageResponse<T = unknown> extends Message {
constructor(
public seq: number,
public src: Address,
public dst: Address,
public data?: T,
) {
super("response");
}
static jsonSchema = Type.Object({
"type": Type.Literal("response"),
"seq": Type.Integer(),
"src": Address.jsonSchema,
"dst": Address.jsonSchema,
"data": Type.Optional(Type.Unknown()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.seq,
Address.fromJSON(json.src),
Address.fromJSON(json.dst),
json.data,
);
}
};
export class ResponseError {
constructor(
public message: string,
public code?: string,
public stack?: string,
) { }
static jsonSchema = Type.Object({
"message": Type.String(),
"code": Type.Optional(Type.String()),
"stack": Type.Optional(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.message, json.code, json.stack);
}
}
export class MessageResponseError extends Message {
constructor(
public seq: number,
public src: Address,
public dst: Address,
public data: ResponseError,
) {
super("responseError");
}
static jsonSchema = Type.Object({
"type": Type.Literal("responseError"),
"seq": Type.Integer(),
"src": Address.jsonSchema,
"dst": Address.jsonSchema,
"data": ResponseError.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.seq,
Address.fromJSON(json.src),
Address.fromJSON(json.dst),
ResponseError.fromJSON(json.data),
);
}
};
export class MessageEvent<T = unknown> extends Message {
constructor(
public seq: number,
public src: Address,
public dst: Address,
public name: string,
public data?: T,
) {
super("event");
}
static jsonSchema = Type.Object({
"type": Type.Literal("event"),
"seq": Type.Integer(),
"src": Address.jsonSchema,
"dst": Address.jsonSchema,
"name": Type.String(),
"data": Type.Optional(Type.Unknown()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.seq,
Address.fromJSON(json.src),
Address.fromJSON(json.dst),
json.name,
json.data,
);
}
};
export class MessageDisconnect extends Message {
constructor(
public data: string,
) {
super("disconnect");
}
static jsonSchema = Type.Object({
"type": Type.Literal("disconnect"),
"data": StringEnum(["prepare", "ready"]),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.data);
}
};
Message.jsonSchema = Type.Union([
MessageHello.jsonSchema,
MessageRegisterHost.jsonSchema,
MessageRegisterControl.jsonSchema,
MessageReady.jsonSchema,
MessageResume.jsonSchema,
MessageContinue.jsonSchema,
MessageInvalidate.jsonSchema,
MessageHeartbeat.jsonSchema,
MessageRequest.jsonSchema,
MessageResponse.jsonSchema,
MessageResponseError.jsonSchema,
MessageEvent.jsonSchema,
MessageDisconnect.jsonSchema,
]);
export class PingRequest {
declare ["constructor"]: typeof PingRequest;
static type = "request" as const;
static src = ["controller", "host", "control"] as const;
static dst = ["controller", "host", "control"] as const;
static permission = null;
}
-243
View File
@@ -1,243 +0,0 @@
"use strict";
const { jsonArray } = require("./composites");
/** @memberof module:lib */
class HostDetails {
/** @type {string} */
agent;
/** @type {string} */
version;
/** @type {string} */
name;
/** @type {number} */
id;
/** @type {boolean} */
connected;
/** @type {string|undefined} */
publicAddress;
constructor(agent, version, name, id, connected, publicAddress) {
this.agent = agent;
this.version = version;
this.name = name;
this.id = id;
this.connected = connected;
if (publicAddress !== undefined) { this.publicAddress = publicAddress; }
}
static jsonSchema = {
type: "object",
required: ["agent", "version", "name", "id", "connected"],
properties: {
"agent": { type: "string" },
"version": { type: "string" },
"name": { type: "string" },
"id": { type: "integer" },
"connected": { type: "boolean" },
"publicAddress": { type: "string" },
},
};
static fromJSON(json) {
return new this(json.agent, json.version, json.name, json.id, json.connected, json.publicAddress);
}
}
/** @memberof module:lib */
class HostListRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.host.list";
static Response = jsonArray(HostDetails);
}
/* @memberof module:lib */
class HostSetSubscriptionsRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.host.subscribe";
/** @type {boolean} */
all;
/** @type {Array<number>} */
hostIds;
constructor(all, hostIds) {
this.all = all;
this.hostIds = hostIds;
}
static jsonSchema = {
type: "object",
required: ["all", "hostIds"],
properties: {
all: { type: "boolean" },
hostIds: {
type: "array",
items: { type: "integer" },
},
},
};
static fromJSON(json) {
return new this(json.all, json.hostIds);
}
}
/* @memberof module:lib */
class HostUpdateEvent {
static type = "event";
static src = "controller";
static dst = "control";
/** @type {module:lib.HostDetails} */
update;
constructor(update) {
this.update = update;
}
static jsonSchema = {
type: "object",
required: ["update"],
properties: {
update: HostDetails.jsonSchema,
},
};
static fromJSON(json) {
return new this(HostDetails.fromJSON(json.update));
}
}
/** @memberof module:lib */
class HostMetricsRequest {
static type = "request";
static src = "controller";
static dst = "host";
}
HostMetricsRequest.Response = class Response { // TODO: Use JSON class pattern in Prometheus
/** @type {array} */
results;
constructor(results) {
this.results = results;
}
static jsonSchema = {
type: "object",
required: ["results"],
properties: {
"results": {
type: "array",
},
},
};
static fromJSON(json) {
return new this(json.results);
}
};
/** @memberof module:lib */
class ControllerConnectionEvent {
static type = "event";
static src = "host";
static dst = "instance";
/** @type {string} */
event;
constructor(event) {
this.event = event;
}
static jsonSchema = {
type: "object",
required: ["event"],
properties: {
"event": { type: "string" },
},
};
static fromJSON(json) {
return new this(json.event);
}
}
/* @memberof module:lib */
class PrepareControllerDisconnectRequest {
static type = "request";
static src = "host";
static dst = "instance";
}
/** @memberof module:lib */
class SyncUserListsEvent {
static type = "event";
static src = "controller";
static dst = "host";
/** @type {Set<string>} */
adminlist;
/** @type {Map<string, string>} */
banlist;
/** @type {Set<string>} */
whitelist;
constructor(adminlist, banlist, whitelist) {
this.adminlist = adminlist;
this.banlist = banlist;
this.whitelist = whitelist;
}
static jsonSchema = {
type: "object",
required: ["adminlist", "banlist", "whitelist"],
properties: {
"adminlist": {
type: "array",
items: { type: "string" },
},
"banlist": {
type: "array",
items: {
type: "array",
minItems: 2,
items: [{ type: "string" }, { type: "string" }],
},
},
"whitelist": {
type: "array",
items: { type: "string" },
},
},
};
static fromJSON(json) {
return new this(new Set(json.adminlist), new Set(json.banlist), new Set(json.whitelist));
}
static toJSON() {
return {
adminlist: [...this.adminlist],
banlist: [...this.banlist],
whitelist: [...this.whitelist],
};
}
}
module.exports = {
HostDetails,
HostListRequest,
HostSetSubscriptionsRequest,
HostUpdateEvent,
HostMetricsRequest,
ControllerConnectionEvent,
PrepareControllerDisconnectRequest,
SyncUserListsEvent,
};
+157
View File
@@ -0,0 +1,157 @@
import { Type, Static } from "@sinclair/typebox";
import { jsonArray } from "./composites";
export class HostDetails {
constructor(
public agent: string,
public version: string,
public name: string,
public id: number,
public connected: boolean,
public publicAddress?: string,
) { }
static jsonSchema = Type.Object({
"agent": Type.String(),
"version": Type.String(),
"name": Type.String(),
"id": Type.Integer(),
"connected": Type.Boolean(),
"publicAddress": Type.Optional(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.agent, json.version, json.name, json.id, json.connected, json.publicAddress);
}
}
export class HostListRequest {
declare ["constructor"]: typeof HostListRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.host.list" as const;
static Response = jsonArray(HostDetails);
}
export class HostSetSubscriptionsRequest {
declare ["constructor"]: typeof HostSetSubscriptionsRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.host.subscribe" as const;
constructor(
public all: boolean,
public hostIds: number[],
) { }
static jsonSchema = Type.Object({
"all": Type.Boolean(),
"hostIds": Type.Array(Type.Integer()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.all, json.hostIds);
}
}
export class HostUpdateEvent {
declare ["constructor"]: typeof HostUpdateEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "control" as const;
constructor(
public update: HostDetails,
) { }
static jsonSchema = Type.Object({
"update": HostDetails.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(HostDetails.fromJSON(json.update));
}
}
export class HostMetricsRequest {
declare ["constructor"]: typeof HostMetricsRequest;
static type = "request" as const;
static src = "controller" as const;
static dst = "host" as const;
static Response = class Response { // TODO: Use JSON class pattern in Prometheus
constructor(
public results: object[],
) { }
static jsonSchema = Type.Object({
"results": Type.Array(Type.Object({})),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.results);
}
};
}
export class ControllerConnectionEvent {
declare ["constructor"]: typeof ControllerConnectionEvent;
static type = "event" as const;
static src = "host" as const;
static dst = "instance" as const;
constructor(
public event: string,
) { }
static jsonSchema = Type.Object({
"event": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.event);
}
}
export class PrepareControllerDisconnectRequest {
declare ["constructor"]: typeof PrepareControllerDisconnectRequest;
static type = "request" as const;
static src = "host" as const;
static dst = "instance" as const;
}
export class SyncUserListsEvent {
declare ["constructor"]: typeof SyncUserListsEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "host" as const;
constructor(
public adminlist: Set<string>,
public banlist: Map<string, string>,
public whitelist: Set<string>,
) { }
static jsonSchema = Type.Object({
"adminlist": Type.Array(Type.String()),
"banlist": Type.Array(
Type.Tuple([Type.String(), Type.String()])
),
"whitelist": Type.Array(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(new Set(json.adminlist), new Map(json.banlist), new Set(json.whitelist));
}
toJSON() {
return {
adminlist: [...this.adminlist],
banlist: [...this.banlist],
whitelist: [...this.whitelist],
};
}
}
File diff suppressed because it is too large. Load diff
+916
View File
@@ -0,0 +1,916 @@
import { Type, Static } from "@sinclair/typebox";
import PlayerStats from "../PlayerStats";
import { JsonString, StringEnum, jsonArray } from "./composites";
import type { User } from "../users";
import type { MessageRequest } from "./messages_core";
export type InstanceStatus =
"unknown" | "unassigned" | "stopped" | "starting" | "running"
| "stopping" | "creating_save" | "exporting_data" | "deleted"
;
export class InstanceDetails {
constructor(
public name: string,
public id: number,
public assignedHost: number | undefined,
public gamePort: number | undefined,
public status: InstanceStatus,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"id": Type.Integer(),
"assignedHost": Type.Optional(Type.Integer()),
"gamePort": Type.Optional(Type.Integer()),
"status": StringEnum([
"unknown", "unassigned", "stopped", "starting", "running", "stopping",
"creating_save", "exporting_data", "deleted",
]),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.id, json.assignedHost, json.gamePort, json.status);
}
}
export class InstanceDetailsGetRequest {
declare ["constructor"]: typeof InstanceDetailsGetRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.get" as const;
constructor(
public instanceId: number,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId);
}
static Response = InstanceDetails;
}
export class InstanceDetailsListRequest {
declare ["constructor"]: typeof InstanceDetailsListRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.list" as const;
static Response = jsonArray(InstanceDetails);
};
export class InstanceDetailsSetSubscriptionsRequest {
declare ["constructor"]: typeof InstanceDetailsSetSubscriptionsRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.subscribe" as const;
constructor(
public all: boolean = false,
public instanceIds: number[] = [],
) { }
static jsonSchema = Type.Object({
"all": Type.Boolean(),
"instanceIds": Type.Array(Type.Integer()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.all, json.instanceIds);
}
};
export class InstanceDetailsUpdateEvent {
declare ["constructor"]: typeof InstanceDetailsUpdateEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "control" as const;
constructor(
public details: InstanceDetails,
) { }
static jsonSchema = InstanceDetails.jsonSchema;
toJSON() {
return this.details;
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(InstanceDetails.fromJSON(json));
}
};
export class InstanceCreateRequest {
declare ["constructor"]: typeof InstanceCreateRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.create" as const;
constructor(
public config: object,
) { }
static jsonSchema = Type.Object({
"config": Type.Object({}),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
// TODO deserialise config here after config refactor
return new this(json.config);
}
}
export class InstanceConfigGetRequest {
declare ["constructor"]: typeof InstanceConfigGetRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.get_config" as const;
constructor(
public instanceId: number,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId);
}
// TODO replace with InstanceConfig after config refactor
static Response = class Response {
constructor(
public config: object,
) { }
static jsonSchema = Type.Object({
"config": Type.Object({}),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.config);
}
};
}
export class InstanceConfigSetFieldRequest {
declare ["constructor"]: typeof InstanceConfigSetFieldRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.update_config" as const;
constructor(
public instanceId: number,
public field: string,
public value: string,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"field": Type.String(),
"value": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.field, json.value);
}
}
export class InstanceConfigSetPropRequest {
declare ["constructor"]: typeof InstanceConfigSetPropRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.update_config" as const;
constructor(
public instanceId: number,
public field: string,
public prop: string,
public value?: unknown,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"field": Type.String(),
"prop": Type.String(),
"value": Type.Unknown(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.field, json.prop, json.value);
}
}
export class InstanceAssignRequest {
declare ["constructor"]: typeof InstanceAssignRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.assign" as const;
constructor(
public instanceId: number,
public hostId?: number,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Number(),
"hostId": Type.Optional(Type.Number()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.hostId);
}
}
export class InstanceMetricsRequest {
declare ["constructor"]: typeof InstanceMetricsRequest;
static type = "request" as const;
static src = "host" as const;
static dst = "instance" as const;
static Response = class Response { // TODO: Use JSON class pattern in Prometheus
constructor(
public results: object[],
) { }
static jsonSchema = Type.Object({
"results": Type.Array(Type.Object({})),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.results);
}
};
}
export class InstanceStartRequest {
declare ["constructor"]: typeof InstanceStartRequest;
static type = "request" as const;
static src = ["control", "controller"] as const;
static dst = "instance" as const;
static permission = "core.instance.start" as const;
constructor(
public save?: string,
) { }
static jsonSchema = Type.Object({
"save": Type.Optional(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.save);
}
}
export class SaveDetails {
constructor(
public type: string,
public name: string,
public size: number,
public mtimeMs: number,
public loaded: boolean,
public loadByDefault: boolean,
) { }
static jsonSchema = Type.Object({
"type": StringEnum(["file", "directory", "special"]),
"name": Type.String(),
"size": Type.Integer(),
"mtimeMs": Type.Number(),
"loaded": Type.Boolean(),
"loadByDefault": Type.Boolean(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.type, json.name, json.size, json.mtimeMs, json.loaded, json.loadByDefault);
}
}
export class InstanceListSavesRequest {
declare ["constructor"]: typeof InstanceListSavesRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "instance" as const;
static permission = "core.instance.save.list" as const;
static Response = jsonArray(SaveDetails);
}
export class InstanceSetSaveListSubscriptionsRequest {
declare ["constructor"]: typeof InstanceSetSaveListSubscriptionsRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.save.list_subscribe" as const;
constructor(
public all: boolean = false,
public instanceIds: number[] = [],
) { }
static jsonSchema = Type.Object({
"all": Type.Boolean(),
"instanceIds": Type.Array(Type.Integer()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.all, json.instanceIds);
}
};
export class InstanceSaveListUpdateEvent {
declare ["constructor"]: typeof InstanceSaveListUpdateEvent;
static type = "event" as const;
static src = ["instance", "host", "controller"] as const;
static dst = ["controller", "control"] as const;
constructor(
public instanceId: number,
public saves: SaveDetails[],
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"saves": Type.Array(SaveDetails.jsonSchema),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.saves.map(i => SaveDetails.fromJSON(i)));
}
}
export class InstanceCreateSaveRequest {
declare ["constructor"]: typeof InstanceCreateSaveRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "instance" as const;
static permission = "core.instance.save.create" as const;
constructor(
public name: string,
public seed?: number,
public mapGenSettings?: object,
public mapSettings?: object,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"seed": Type.Optional(Type.Integer()),
"mapGenSettings": Type.Optional(Type.Object({})),
"mapSettings": Type.Optional(Type.Object({})),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.seed, json.mapGenSettings, json.mapSettings);
}
}
export class InstanceRenameSaveRequest {
declare ["constructor"]: typeof InstanceRenameSaveRequest;
static type = "request" as const;
static src = ["control", "controller"] as const;
static dst = ["controller", "host"] as const;
static permission = "core.instance.save.rename" as const;
constructor(
public instanceId: number,
public oldName: string,
public newName: string,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"oldName": Type.String(),
"newName": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.oldName, json.newName);
}
}
export class InstanceCopySaveRequest {
declare ["constructor"]: typeof InstanceCopySaveRequest;
static type = "request" as const;
static src = ["control", "controller"] as const;
static dst = ["controller", "host"] as const;
static permission = "core.instance.save.copy" as const;
constructor(
public instanceId: number,
public source: string,
public destination: string,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"source": Type.String(),
"destination": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.source, json.destination);
}
}
export class InstanceDeleteSaveRequest {
declare ["constructor"]: typeof InstanceDeleteSaveRequest;
static type = "request" as const;
static src = ["control", "controller"] as const;
static dst = ["controller", "host"] as const;
static permission = "core.instance.save.delete" as const;
constructor(
public instanceId: number,
public name: string,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"name": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.name);
}
}
export class InstanceDownloadSaveRequest {
declare ["constructor"]: typeof InstanceDownloadSaveRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.save.download" as const;
constructor(
public instanceId: number,
public name: string,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"name": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.name);
}
static Response = JsonString;
}
export class InstanceTransferSaveRequest {
declare ["constructor"]: typeof InstanceTransferSaveRequest;
static type = "request" as const;
static src = ["control", "controller"] as const;
static dst = ["controller", "host"] as const;
static permission(user: User, message: MessageRequest) {
user.checkPermission("core.instance.save.transfer");
if (typeof message.data === "object" && message.data !== null) {
const data = message.data as { copy: boolean, sourceName: string, targetName: string };
if (data.copy) {
user.checkPermission("core.instance.save.copy");
} else if (data.sourceName !== data.targetName) {
user.checkPermission("core.instance.save.rename");
}
}
}
constructor(
public sourceInstanceId: number,
public sourceName: string,
public targetInstanceId: number,
public targetName: string,
public copy: boolean,
) { }
static jsonSchema = Type.Object({
"sourceInstanceId": Type.Number(),
"sourceName": Type.String(),
"targetInstanceId": Type.Number(),
"targetName": Type.String(),
"copy": Type.Boolean(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.sourceInstanceId, json.sourceName, json.targetInstanceId, json.targetName, json.copy
);
}
static Response = JsonString;
}
export class InstancePullSaveRequest {
declare ["constructor"]: typeof InstancePullSaveRequest;
static type = "request" as const;
static src = "controller" as const;
static dst = "host" as const;
constructor(
public instanceId: number,
public streamId: string,
public name: string,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"streamId": Type.String(),
"name": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.streamId, json.name);
}
static Response = JsonString;
}
export class InstancePushSaveRequest {
declare ["constructor"]: typeof InstancePushSaveRequest;
static type = "request" as const;
static src = "controller" as const;
static dst = "host" as const;
constructor(
public instanceId: number,
public streamId: string,
public name: string,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"streamId": Type.String(),
"name": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.streamId, json.name);
}
}
export class InstanceLoadScenarioRequest {
declare ["constructor"]: typeof InstanceLoadScenarioRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "instance" as const;
static permission = "core.instance.load_scenario" as const;
constructor(
public scenario: string,
public seed?: number,
public mapGenSettings?: object,
public mapSettings?: object,
) { }
static jsonSchema = Type.Object({
"scenario": Type.String(),
"seed": Type.Optional(Type.Integer()),
"mapGenSettings": Type.Optional(Type.Object({})),
"mapSettings": Type.Optional(Type.Object({})),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.scenario, json.seed, json.mapGenSettings, json.mapSettings);
}
}
export class InstanceExportDataRequest {
declare ["constructor"]: typeof InstanceExportDataRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "instance" as const;
static permission = "core.instance.export_data" as const;
}
export class InstanceExtractPlayersRequest {
declare ["constructor"]: typeof InstanceExtractPlayersRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "instance" as const;
static permission = "core.instance.extract_players" as const;
}
export class InstanceStopRequest {
declare ["constructor"]: typeof InstanceStopRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "instance" as const;
static permission = "core.instance.stop" as const;
}
export class InstanceKillRequest {
declare ["constructor"]: typeof InstanceKillRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "instance" as const;
static permission = "core.instance.kill" as const;
}
export class InstanceDeleteRequest {
declare ["constructor"]: typeof InstanceDeleteRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.instance.delete" as const;
constructor(
public instanceId: number,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId);
}
}
export class InstanceDeleteInternalRequest {
declare ["constructor"]: typeof InstanceDeleteInternalRequest;
static type = "request" as const;
static src = "controller" as const;
static dst = "host" as const;
constructor(
public instanceId: number,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId);
}
}
export class InstanceSendRconRequest {
declare ["constructor"]: typeof InstanceSendRconRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "instance" as const;
static permission = "core.instance.send_rcon" as const;
constructor(
public command: string,
) { }
static jsonSchema = Type.Object({
"command": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.command);
}
static Response = JsonString;
}
// TODO remove this after config refactor
export class RawInstanceInfo {
constructor(
public config: object,
public status: string,
) { }
static jsonSchema = Type.Object({
"config": Type.Object({}),
"status": StringEnum([
"stopped", "starting", "running", "stopping", "creating_save", "exporting_data",
]),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.config, json.status);
}
}
export class InstancesUpdateRequest {
declare ["constructor"]: typeof InstancesUpdateRequest;
static type = "request" as const;
static src = "host" as const;
static dst = "controller" as const;
constructor(
public instances: RawInstanceInfo[],
) { }
static jsonSchema = Type.Array(RawInstanceInfo.jsonSchema);
toJSON() {
return this.instances;
}
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.map(i => RawInstanceInfo.fromJSON(i)));
}
}
export class InstanceAssignInternalRequest {
declare ["constructor"]: typeof InstanceAssignInternalRequest;
static type = "request" as const;
static src = "controller" as const;
static dst = "host" as const;
constructor(
public instanceId: number,
public config: object,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"config": Type.Object({}),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
// TODO deserialise config here after config refactor
return new this(json.instanceId, json.config);
}
}
export class InstanceUnassignInternalRequest {
declare ["constructor"]: typeof InstanceUnassignInternalRequest;
static type = "request" as const;
static src = "controller" as const;
static dst = "host" as const;
constructor(
public instanceId: number,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId);
}
}
export class InstanceInitialisedEvent {
declare ["constructor"]: typeof InstanceInitialisedEvent;
static type = "event" as const;
static src = "instance" as const;
static dst = "host" as const;
constructor(
public plugins: Record<string, string>,
) { }
static jsonSchema = Type.Object({
"plugins": Type.Record(Type.String(), Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.plugins);
}
}
export class InstanceStatusChangedEvent {
declare ["constructor"]: typeof InstanceStatusChangedEvent;
static type = "event" as const;
static src = ["instance", "host"] as const;
static dst = "controller" as const;
constructor(
public instanceId: number,
public status: string,
public gamePort?: number,
) { }
static jsonSchema = Type.Object({
"instanceId": Type.Integer(),
"status": StringEnum([
"stopped", "starting", "running", "stopping", "creating_save", "exporting_data",
]),
"gamePort": Type.Optional(Type.Integer()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.instanceId, json.status, json.gamePort);
}
}
export class InstanceDetailsChangedEvent {
declare ["constructor"]: typeof InstanceDetailsChangedEvent;
static type = "event" as const;
static src = "instance" as const;
static dst = "controller" as const;
constructor(
public details: InstanceDetails,
) { }
static jsonSchema = InstanceDetails.jsonSchema;
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(InstanceDetails.fromJSON(json));
}
}
export class InstanceBanlistUpdateEvent {
declare ["constructor"]: typeof InstanceBanlistUpdateEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "instance" as const;
constructor(
public name: string,
public banned: boolean,
public reason: string,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"banned": Type.Boolean(),
"reason": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.banned, json.reason);
}
}
export class InstanceAdminlistUpdateEvent {
declare ["constructor"]: typeof InstanceAdminlistUpdateEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "instance" as const;
constructor(
public name: string,
public admin: boolean,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"admin": Type.Boolean(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.admin);
}
}
export class InstanceWhitelistUpdateEvent {
declare ["constructor"]: typeof InstanceWhitelistUpdateEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "instance" as const;
constructor(
public name: string,
public whitelisted: boolean,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"whitelisted": Type.Boolean(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.whitelisted);
}
}
export class InstancePlayerUpdateEvent {
declare ["constructor"]: typeof InstancePlayerUpdateEvent;
static type = "event" as const;
static src = "instance" as const;
static dst = "controller" as const;
constructor(
public type: string,
public name: string,
public reason?: string,
public stats?: PlayerStats,
) { }
static jsonSchema = Type.Object({
"type": StringEnum(["join", "leave", "import"]),
"name": Type.String(),
"reason": Type.Optional(Type.String()),
"stats": Type.Unsafe<object>(PlayerStats.jsonSchema),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.name, json.reason, PlayerStats.fromJSON(json.stats));
}
}
-474
View File
@@ -1,474 +0,0 @@
"use strict";
const ModPack = require("./ModPack");
const ModInfo = require("./ModInfo");
const { jsonArray, JsonString } = require("./composites");
/* @memberof module:lib */
class ModPackGetRequest {
static type = "request";
static src = ["instance", "control"];
static dst = "controller";
static permission = "core.mod_pack.get";
/** @type {number} */
id;
constructor(id) {
this.id = id;
}
static jsonSchema = {
type: "object",
required: ["id"],
properties: {
id: { type: "integer" },
},
};
static fromJSON(json) {
return new this(json.id);
}
static Response = ModPack;
}
/* @memberof module:lib */
class ModPackGetDefaultRequest {
static type = "request";
static src = ["instance", "control"];
static dst = "controller";
static permission = "core.mod_pack.get";
static Response = ModPack;
}
/* @memberof module:lib */
class ModPackListRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod_pack.list";
static Response = jsonArray(ModPack);
}
/** @memberof module:lib */
class ModPackCreateRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod_pack.create";
/** @type {module:lib.ModPack} */
modPack;
constructor(modPack) {
this.modPack = modPack;
}
static jsonSchema = {
type: "object",
required: ["modPack"],
properties: {
"modPack": ModPack.jsonSchema,
},
};
static fromJSON(json) {
return new this(ModPack.fromJSON(json.modPack));
}
}
/* @memberof module:lib */
class ModPackUpdateRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod_pack.update";
/** @type {module:lib.ModPack} */
modPack;
constructor(modPack) {
this.modPack = modPack;
}
static jsonSchema = {
type: "object",
required: ["modPack"],
properties: {
"modPack": ModPack.jsonSchema,
},
};
static fromJSON(json) {
return new this(ModPack.fromJSON(json.modPack));
}
}
/* @memberof module:lib */
class ModPackDeleteRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod_pack.delete";
/** @type {number} */
id;
constructor(id) {
this.id = id;
}
static jsonSchema = {
type: "object",
required: ["id"],
properties: {
id: { type: "integer" },
},
};
static fromJSON(json) {
return new this(json.id);
}
}
/* @memberof module:lib */
class ModPackSetSubscriptionsRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod_pack.subscribe";
/** @type {boolean} */
all;
/** @type {Array<number>} */
modPackIds;
constructor(all, modPackIds) {
this.all = all;
this.modPackIds = modPackIds;
}
static jsonSchema = {
type: "object",
required: ["all", "modPackIds"],
properties: {
all: { type: "boolean" },
modPackIds: {
type: "array",
items: { type: "integer" },
},
},
};
static fromJSON(json) {
return new this(json.all, json.modPackIds);
}
}
/* @memberof module:lib */
class ModGetRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod.get";
/** @type {string} */
name;
/** @type {string} */
version;
constructor(name, version) {
this.name = name;
this.version = version;
}
static jsonSchema = {
type: "object",
required: ["name", "version"],
properties: {
name: { type: "string" },
version: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.name, json.version);
}
static Response = ModInfo;
}
/* @memberof module:lib */
class ModListRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod.list";
static Response = jsonArray(ModInfo);
}
/* @memberof module:lib */
class ModSearchRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod.search";
/** @type {string} */
query;
/** @type {string} */
factorioVersion;
/** @type {number} */
page;
/** @type {number|undefined} */
pageSize;
/** @type {string|undefined} */
sort;
/** @type {string|undefined} */
sortOrder;
constructor(query, factorioVersion, page, pageSize, sort, sortOrder) {
this.query = query;
this.factorioVersion = factorioVersion;
this.page = page;
if (pageSize) { this.pageSize = pageSize; }
if (sort) { this.sort = sort; }
if (sortOrder) { this.sortOrder = sortOrder; }
}
static jsonSchema = {
type: "object",
required: ["query", "factorioVersion", "page"],
properties: {
query: { type: "string" },
factorioVersion: { type: "string" },
pageSize: { type: "integer" },
page: { type: "integer" },
sort: { type: "string" },
sortOrder: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.query, json.factorioVersion, json.page, json.pageSize, json.sort, json.sortOrder);
}
}
ModSearchRequest.Response = class Response {
/** @type {Array<string>} */
queryIssues;
/** @type {number} */
pageCount;
/** @type {number} */
resultCount;
/** @type {Array<object>} */
results;
constructor(queryIssues, pageCount, resultCount, results) {
this.queryIssues = queryIssues;
this.pageCount = pageCount;
this.resultCount = resultCount;
this.results = results;
}
static jsonSchema = {
type: "object",
required: ["queryIssues", "pageCount", "resultCount", "results"],
properties: {
queryIssues: {
type: "array",
items: { type: "string" },
},
pageCount: { type: "integer" },
resultCount: { type: "integer" },
results: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
versions: {
type: "array",
items: ModInfo.jsonSchema,
},
},
},
},
},
};
static fromJSON(json) {
let results = json.results.map(
({ name, versions }) => ({ name, versions: versions.map(mod => ModInfo.fromJSON(mod)) })
);
return new this(json.queryIssues, json.pageCount, json.resultCount, results);
}
};
/* @memberof module:lib */
class ModSetSubscriptionsRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod.subscribe";
/** @type {boolean} */
all;
/** @type {Array<string>} */
modNames;
constructor(all, modNames) {
this.all = all;
this.modNames = modNames;
}
static jsonSchema = {
type: "object",
required: ["all", "modNames"],
properties: {
all: { type: "boolean" },
modNames: {
type: "array",
items: { type: "string" },
},
},
};
static fromJSON(json) {
return new this(json.all, json.modNames);
}
}
/* @memberof module:lib */
class ModDownloadRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod.download";
/** @type {string} */
name;
/** @type {string} */
version;
constructor(name, version) {
this.name = name;
this.version = version;
}
static jsonSchema = {
type: "object",
required: ["name", "version"],
properties: {
name: { type: "string" },
version: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.name, json.version);
}
static Response = JsonString;
}
/* @memberof module:lib */
class ModDeleteRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.mod.delete";
/** @type {string} */
name;
/** @type {string} */
version;
constructor(name, version) {
this.name = name;
this.version = version;
}
static jsonSchema = {
type: "object",
required: ["name", "version"],
properties: {
name: { type: "string" },
version: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.name, json.version);
}
}
/* @memberof module:lib */
class ModPackUpdateEvent {
static type = "event";
static src = "controller";
static dst = "control";
/** @type {module:lib.ModPack} */
modPack;
constructor(modPack) {
this.modPack = modPack;
}
static jsonSchema = {
type: "object",
required: ["modPack"],
properties: {
modPack: ModPack.jsonSchema,
},
};
static fromJSON(json) {
return new this(ModPack.fromJSON(json.modPack));
}
}
/* @memberof module:lib */
class ModUpdateEvent {
static type = "event";
static src = "controller";
static dst = "control";
/** @type {module:lib.ModInfo} */
mod;
constructor(mod) {
this.mod = mod;
}
static jsonSchema = {
type: "object",
required: ["mod"],
properties: {
mod: ModInfo.jsonSchema,
},
};
static fromJSON(json) {
return new this(ModInfo.fromJSON(json.mod));
}
}
module.exports = {
ModPackGetRequest,
ModPackGetDefaultRequest,
ModPackListRequest,
ModPackCreateRequest,
ModPackUpdateRequest,
ModPackDeleteRequest,
ModPackSetSubscriptionsRequest,
ModGetRequest,
ModListRequest,
ModSearchRequest,
ModSetSubscriptionsRequest,
ModDownloadRequest,
ModDeleteRequest,
ModPackUpdateEvent,
ModUpdateEvent,
};
+324
View File
@@ -0,0 +1,324 @@
import { Type, Static } from "@sinclair/typebox";
import ModInfo from "./ModInfo";
import ModPack from "./ModPack";
import { JsonString, jsonArray } from "./composites";
export class ModPackGetRequest {
declare ["constructor"]: typeof ModPackGetRequest;
static type = "request" as const;
static src = ["instance", "control"] as const;
static dst = "controller" as const;
static permission = "core.mod_pack.get" as const;
constructor(
public id: number,
) { }
static jsonSchema = Type.Object({
"id": Type.Integer(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.id);
}
static Response = ModPack;
}
export class ModPackGetDefaultRequest {
declare ["constructor"]: typeof ModPackGetDefaultRequest;
static type = "request" as const;
static src = ["instance", "control"] as const;
static dst = "controller" as const;
static permission = "core.mod_pack.get" as const;
static Response = ModPack;
}
export class ModPackListRequest {
declare ["constructor"]: typeof ModPackListRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod_pack.list" as const;
static Response = jsonArray(ModPack);
}
export class ModPackCreateRequest {
declare ["constructor"]: typeof ModPackCreateRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod_pack.create" as const;
constructor(
public modPack: ModPack,
) { }
static jsonSchema = Type.Object({
"modPack": ModPack.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(ModPack.fromJSON(json.modPack));
}
}
export class ModPackUpdateRequest {
declare ["constructor"]: typeof ModPackUpdateRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod_pack.update" as const;
constructor(
public modPack: ModPack,
) { }
static jsonSchema = Type.Object({
"modPack": ModPack.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(ModPack.fromJSON(json.modPack));
}
}
export class ModPackDeleteRequest {
declare ["constructor"]: typeof ModPackDeleteRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod_pack.delete" as const;
constructor(
public id: number,
) { }
static jsonSchema = Type.Object({
"id": Type.Integer(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.id);
}
}
export class ModPackSetSubscriptionsRequest {
declare ["constructor"]: typeof ModPackSetSubscriptionsRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod_pack.subscribe" as const;
constructor(
public all: boolean,
public modPackIds: number[],
) { }
static jsonSchema = Type.Object({
"all": Type.Boolean(),
"modPackIds": Type.Array(Type.Integer()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.all, json.modPackIds);
}
}
export class ModGetRequest {
declare ["constructor"]: typeof ModGetRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod.get" as const;
constructor(
public name: string,
public version: string,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"version": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.version);
}
static Response = ModInfo;
}
export class ModListRequest {
declare ["constructor"]: typeof ModListRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod.list" as const;
static Response = jsonArray(ModInfo);
}
export class ModSearchRequest {
declare ["constructor"]: typeof ModSearchRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod.search" as const;
constructor(
public query: string,
public factorioVersion: string,
public page: number,
public pageSize?: number,
public sort?: string,
public sortOrder?: string,
) { }
static jsonSchema = Type.Object({
"query": Type.String(),
"factorioVersion": Type.String(),
"page": Type.Integer(),
"pageSize": Type.Optional(Type.Integer()),
"sort": Type.Optional(Type.String()),
"sortOrder": Type.Optional(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.query, json.factorioVersion, json.page, json.pageSize, json.sort, json.sortOrder);
}
static Response = class Response {
constructor(
public queryIssues: string[],
public pageCount: number,
public resultCount: number,
public results: object[],
) { }
static jsonSchema = Type.Object({
"queryIssues": Type.Array(Type.String()),
"pageCount": Type.Integer(),
"resultCount": Type.Integer(),
"results": Type.Array(
Type.Object({
"name": Type.String(),
"versions": Type.Array(ModInfo.jsonSchema),
}),
),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
let results = json.results.map(
({ name, versions }) => ({ name, versions: versions.map(mod => ModInfo.fromJSON(mod)) })
);
return new this(json.queryIssues, json.pageCount, json.resultCount, results);
}
};
}
export class ModSetSubscriptionsRequest {
declare ["constructor"]: typeof ModSetSubscriptionsRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod.subscribe" as const;
constructor(
public all: boolean,
public modNames: string[],
) { }
static jsonSchema = Type.Object({
"all": Type.Boolean(),
"modNames": Type.Array(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.all, json.modNames);
}
}
export class ModDownloadRequest {
declare ["constructor"]: typeof ModDownloadRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod.download" as const;
constructor(
public name: string,
public version: string,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"version": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.version);
}
static Response = JsonString;
}
export class ModDeleteRequest {
declare ["constructor"]: typeof ModDeleteRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.mod.delete" as const;
constructor(
public name: string,
public version: string,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"version": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.version);
}
}
export class ModPackUpdateEvent {
declare ["constructor"]: typeof ModPackUpdateEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "control" as const;
constructor(
public modPack: ModPack,
) { }
static jsonSchema = Type.Object({
"modPack": ModPack.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(ModPack.fromJSON(json.modPack));
}
}
export class ModUpdateEvent {
declare ["constructor"]: typeof ModUpdateEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "control" as const;
constructor(
public mod: ModInfo,
) { }
static jsonSchema = Type.Object({
"mod": ModInfo.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(ModInfo.fromJSON(json.mod));
}
}
-708
View File
@@ -1,708 +0,0 @@
"use strict";
const PlayerStats = require("../PlayerStats");
const { jsonArray, JsonNumber } = require("./composites");
/* @memberof module:lib */
class RawPermission { // TODO refactor into lib/user.Permission
/** @type {string} */
name;
/** @type {string} */
title;
/** @type {string} */
description;
constructor(name, title, description) {
this.name = name;
this.title = title;
this.description = description;
}
static jsonSchema = {
type: "object",
required: ["name", "title", "description"],
properties: {
name: { type: "string" },
title: { type: "string" },
description: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.name, json.title, json.description);
}
}
/* @memberof module:lib */
class PermissionListRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.permission.list";
static Response = jsonArray(RawPermission);
}
class RawRole { // TODO refactor into lib/user.Role
/** @type {number} */
id;
/** @type {string} */
name;
/** @type {string} */
description;
/** @type {Array<string>} */
permissions;
constructor(id, name, description, permissions) {
this.id = id;
this.name = name;
this.description = description;
this.permissions = permissions;
}
static jsonSchema = {
type: "object",
required: ["id", "name", "description", "permissions"],
properties: {
id: { type: "integer" },
name: { type: "string" },
description: { type: "string" },
permissions: {
type: "array",
items: { type: "string" },
},
},
};
static fromJSON(json) {
return new this(json.id, json.name, json.description, json.permissions);
}
}
/* @memberof module:lib */
class RoleListRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.role.list";
static Response = jsonArray(RawRole);
}
/* @memberof module:lib */
class RoleCreateRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.role.create";
/** @type {string} */
name;
/** @type {string} */
description;
/** @type {Array<string>} */
permissions;
constructor(name, description, permissions) {
this.name = name;
this.description = description;
this.permissions = permissions;
}
static jsonSchema = {
type: "object",
required: ["name", "description", "permissions"],
properties: {
name: { type: "string" },
description: { type: "string" },
permissions: {
type: "array",
items: { type: "string" },
},
},
};
static fromJSON(json) {
return new this(json.name, json.description, json.permissions);
}
static Response = JsonNumber;
}
/* @memberof module:lib */
class RoleUpdateRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.role.update";
/** @type {number} */
id;
/** @type {string} */
name;
/** @type {string} */
description;
/** @type {Array<string>} */
permissions;
constructor(id, name, description, permissions) {
this.id = id;
this.name = name;
this.description = description;
this.permissions = permissions;
}
static jsonSchema = {
type: "object",
required: ["id", "name", "description", "permissions"],
properties: {
id: { type: "integer" },
name: { type: "string" },
description: { type: "string" },
permissions: {
type: "array",
items: { type: "string" },
},
},
};
static fromJSON(json) {
return new this(json.id, json.name, json.description, json.permissions);
}
}
/* @memberof module:lib */
class RoleGrantDefaultPermissionsRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.role.update";
/** @type {number} */
id;
constructor(id) {
this.id = id;
}
static jsonSchema = {
type: "object",
required: ["id"],
properties: {
id: { type: "integer" },
},
};
static fromJSON(json) {
return new this(json.id);
}
}
/* @memberof module:lib */
class RoleDeleteRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.role.delete";
/** @type {number} */
id;
constructor(id) {
this.id = id;
}
static jsonSchema = {
type: "object",
required: ["id"],
properties: {
id: { type: "integer" },
},
};
static fromJSON(json) {
return new this(json.id);
}
}
/* @memberof module:lib */
class RawUser { // TODO refactor into lib/user.User
/** @type {string} */
name;
/** @type {Array<number>} */
roles;
/** @type {Array<number>} */
instances;
/** @type {boolean|undefined} */
isAdmin;
/** @type {boolean|undefined} */
isBanned;
/** @type {boolean|undefined} */
isWhitelisted;
/** @type {string|undefined} */
banReason;
/** @type {boolean|undefined} */
isDeleted;
/** @type {module:lib.PlayerStats} */
playerStats;
/** @type {Map<number, module:lib.PlayerStats>|undefined} */
instanceStats;
constructor(
name, roles, instances, isAdmin, isBanned, isWhitelisted, banReason,
isDeleted, playerStats, instanceStats
) {
this.name = name;
this.roles = roles;
this.instances = instances;
if (isAdmin !== undefined) { this.isAdmin = isAdmin; }
if (isBanned !== undefined) { this.isBanned = isBanned; }
if (isWhitelisted !== undefined) { this.isWhitelisted = isWhitelisted; }
if (banReason !== undefined) { this.banReason = banReason; }
if (isDeleted !== undefined) { this.isDeleted = isDeleted; }
if (playerStats !== undefined) { this.playerStats = playerStats; }
if (instanceStats !== undefined) { this.instanceStats = instanceStats; }
}
static jsonSchema = {
type: "object",
required: ["name", "roles", "instances"],
properties: {
name: { type: "string" },
roles: { type: "array", items: { type: "integer" } },
instances: { type: "array", items: { type: "integer" } },
isAdmin: { type: "boolean" },
isBanned: { type: "boolean" },
isWhitelisted: { type: "boolean" },
banReason: { type: "string" },
isDeleted: { type: "boolean" },
playerStats: PlayerStats.jsonSchema,
instanceStats: {
type: "array",
items: {
type: "array",
minItems: 2,
items: [{ type: "integer" }, PlayerStats.jsonSchema],
},
},
},
};
static fromJSON(json) {
let playerStats;
if (json.playerStats) {
playerStats = PlayerStats.fromJSON(json.playerStats);
}
let instanceStats;
if (json.instanceStats) {
instanceStats = new Map(
json.instanceStats.map(([id, stats]) => [id, PlayerStats.fromJSON(stats)])
);
}
return new this(
json.name, json.roles, json.instances, json.isAdmin, json.isBanned, json.isWhitelisted, json.banReason,
json.isDeleted, playerStats, instanceStats
);
}
toJSON() {
const json = {
name: this.name,
roles: this.roles,
instances: this.instances,
isAdmin: this.isAdmin,
isBanned: this.isBanned,
isWhitelisted: this.isWhitelisted,
banReason: this.banReason,
isDeleted: this.isDeleted,
playerStats: this.playerStats,
};
if (this.instanceStats) {
json.instanceStats = [...this.instanceStats];
}
return json;
}
}
/* @memberof module:lib */
class UserGetRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.user.get";
/** @type {string} */
name;
constructor(name) {
this.name = name;
}
static jsonSchema = {
type: "object",
required: ["name"],
properties: {
name: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.name);
}
static Response = RawUser;
}
/* @memberof module:lib */
class UserListRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.user.list";
static Response = jsonArray(RawUser);
}
/* @memberof module:lib */
class UserSetSubscriptionsRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.user.subscribe";
/** @type {boolean} */
all;
/** @type {Array<string>} */
names;
constructor(all, names) {
this.all = all;
this.names = names;
}
static jsonSchema = {
type: "object",
required: ["all", "names"],
properties: {
all: { type: "boolean" },
names: {
type: "array",
items: { type: "string" },
},
},
};
static fromJSON(json) {
return new this(json.all, json.names);
}
}
/* @memberof module:lib */
class UserCreateRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.user.create";
/** @type {string} */
name;
constructor(name) {
this.name = name;
}
static jsonSchema = {
type: "object",
required: ["name"],
properties: {
name: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.name);
}
}
/* @memberof module:lib */
class UserRevokeTokenRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.user.revoke_token";
/** @type {string} */
name;
constructor(name) {
this.name = name;
}
static jsonSchema = {
type: "object",
required: ["name"],
properties: {
name: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.name);
}
}
/* @memberof module:lib */
class UserUpdateRolesRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.user.update_roles";
/** @type {string} */
name;
/** @type {Array<number>} */
roles;
constructor(name, roles) {
this.name = name;
this.roles = roles;
}
static jsonSchema = {
type: "object",
required: ["name", "roles"],
properties: {
name: { type: "string" },
roles: {
type: "array",
items: { type: "integer" },
},
},
};
static fromJSON(json) {
return new this(json.name, json.roles);
}
}
/* @memberof module:lib */
class UserSetAdminRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.user.set_admin";
/** @type {string} */
name;
/** @type {boolean} */
create;
/** @type {boolean} */
admin;
constructor(name, create, admin) {
this.name = name;
this.create = create;
this.admin = admin;
}
static jsonSchema = {
type: "object",
required: ["name", "create", "admin"],
properties: {
name: { type: "string" },
create: { type: "boolean" },
admin: { type: "boolean" },
},
};
static fromJSON(json) {
return new this(json.name, json.create, json.admin);
}
}
/* @memberof module:lib */
class UserSetWhitelistedRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.user.set_whitelisted";
/** @type {string} */
name;
/** @type {boolean} */
create;
/** @type {boolean} */
whitelisted;
constructor(name, create, whitelisted) {
this.name = name;
this.create = create;
this.whitelisted = whitelisted;
}
static jsonSchema = {
type: "object",
required: ["name", "create", "whitelisted"],
properties: {
name: { type: "string" },
create: { type: "boolean" },
whitelisted: { type: "boolean" },
},
};
static fromJSON(json) {
return new this(json.name, json.create, json.whitelisted);
}
}
/* @memberof module:lib */
class UserSetBannedRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.user.set_banned";
/** @type {string} */
name;
/** @type {boolean} */
create;
/** @type {boolean} */
banned;
/** @type {string} */
reason;
constructor(name, create, banned, reason) {
this.name = name;
this.create = create;
this.banned = banned;
this.reason = reason;
}
static jsonSchema = {
type: "object",
required: ["name", "create", "banned", "reason"],
properties: {
name: { type: "string" },
create: { type: "boolean" },
banned: { type: "boolean" },
reason: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.name, json.create, json.banned, json.reason);
}
}
/* @memberof module:lib */
class UserDeleteRequest {
static type = "request";
static src = "control";
static dst = "controller";
static permission = "core.user.delete";
/** @type {string} */
name;
constructor(name) {
this.name = name;
}
static jsonSchema = {
type: "object",
required: ["name"],
properties: {
name: { type: "string" },
},
};
static fromJSON(json) {
return new this(json.name);
}
}
/* @memberof module:lib */
class AccountUpdateEvent {
static type = "event";
static src = "controller";
static dst = "control";
/** @type {Array<object>|undefined} */
roles;
constructor(roles) {
if (roles) { this.roles = roles; }
}
static jsonSchema = {
type: "object",
properties: {
roles: {
type: "array",
items: {
type: "object",
required: ["name", "id", "permissions"],
properties: {
name: { type: "string" },
id: { type: "integer" },
permissions: {
type: "array",
items: { type: "string" },
},
},
},
},
},
};
static fromJSON(json) {
return new this(json.roles);
}
}
/* @memberof module:lib */
class UserUpdateEvent {
static type = "event";
static src = "controller";
static dst = "control";
/** @type {module:lib.RawUser} */
user;
constructor(user) {
this.user = user;
}
static jsonSchema = {
type: "object",
required: ["user"],
properties: {
user: RawUser.jsonSchema,
},
};
static fromJSON(json) {
return new this(RawUser.fromJSON(json.user));
}
}
module.exports = {
RawPermission,
PermissionListRequest,
RawRole,
RoleListRequest,
RoleCreateRequest,
RoleUpdateRequest,
RoleGrantDefaultPermissionsRequest,
RoleDeleteRequest,
RawUser,
UserGetRequest,
UserListRequest,
UserSetSubscriptionsRequest,
UserCreateRequest,
UserRevokeTokenRequest,
UserUpdateRolesRequest,
UserSetAdminRequest,
UserSetWhitelistedRequest,
UserSetBannedRequest,
UserDeleteRequest,
AccountUpdateEvent,
UserUpdateEvent,
};
+474
View File
@@ -0,0 +1,474 @@
import { Type, Static } from "@sinclair/typebox";
import PlayerStats from "../PlayerStats";
import { JsonNumber, jsonArray } from "./composites";
export class RawPermission { // TODO refactor into lib/user.Permission
constructor(
public name: string,
public title: string,
public description: string,
) { }
static jsonSchema = Type.Object({
name: Type.String(),
title: Type.String(),
description: Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.title, json.description);
}
}
export class PermissionListRequest {
declare ["constructor"]: typeof PermissionListRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.permission.list" as const;
static Response = jsonArray(RawPermission);
}
export class RawRole { // TODO refactor into lib/user.Role
constructor(
public id: number,
public name: string,
public description: string,
public permissions: string[],
) { }
static jsonSchema = Type.Object({
id: Type.Integer(),
name: Type.String(),
description: Type.String(),
permissions: Type.Array(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.id, json.name, json.description, json.permissions);
}
}
export class RoleListRequest {
declare ["constructor"]: typeof RoleListRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.role.list" as const;
static Response = jsonArray(RawRole);
}
export class RoleCreateRequest {
declare ["constructor"]: typeof RoleCreateRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.role.create" as const;
constructor(
public name: string,
public description: string,
public permissions: string[],
) { }
static jsonSchema = Type.Object({
name: Type.String(),
description: Type.String(),
permissions: Type.Array(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.description, json.permissions);
}
static Response = JsonNumber;
}
export class RoleUpdateRequest {
declare ["constructor"]: typeof RoleUpdateRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.role.update" as const;
constructor(
public id: number,
public name: string,
public description: string,
public permissions: string[],
) { }
static jsonSchema = Type.Object({
id: Type.Integer(),
name: Type.String(),
description: Type.String(),
permissions: Type.Array(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.id, json.name, json.description, json.permissions);
}
}
export class RoleGrantDefaultPermissionsRequest {
declare ["constructor"]: typeof RoleGrantDefaultPermissionsRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.role.update" as const;
constructor(
public id: number,
) { }
static jsonSchema = Type.Object({
id: Type.Integer(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.id);
}
}
export class RoleDeleteRequest {
declare ["constructor"]: typeof RoleDeleteRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.role.delete" as const;
constructor(
public id: number,
) { }
static jsonSchema = Type.Object({
id: Type.Integer(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.id);
}
}
export class RawUser { // TODO refactor into lib/user.User
constructor(
public name: string,
public roles: number[],
public instances: number[],
public isAdmin?: boolean,
public isBanned?: boolean,
public isWhitelisted?: boolean,
public banReason?: string,
public isDeleted?: boolean,
public playerStats?: PlayerStats,
public instanceStats?: Map<number, PlayerStats>,
) { }
static jsonSchema = Type.Object({
name: Type.String(),
roles: Type.Array(Type.Integer()),
instances: Type.Array(Type.Integer()),
isAdmin: Type.Optional(Type.Boolean()),
isBanned: Type.Optional(Type.Boolean()),
isWhitelisted: Type.Optional(Type.Boolean()),
banReason: Type.Optional(Type.String()),
isDeleted: Type.Optional(Type.Boolean()),
playerStats: Type.Optional(PlayerStats.jsonSchema),
instanceStats: Type.Optional(
Type.Array(
Type.Tuple([Type.Integer(), PlayerStats.jsonSchema]),
)
),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
let playerStats: PlayerStats | undefined;
if (json.playerStats) {
playerStats = PlayerStats.fromJSON(json.playerStats);
}
let instanceStats: Map<number, PlayerStats> | undefined;
if (json.instanceStats) {
instanceStats = new Map(
json.instanceStats.map(([id, stats]) => [id, PlayerStats.fromJSON(stats)])
);
}
return new this(
json.name, json.roles, json.instances, json.isAdmin, json.isBanned, json.isWhitelisted, json.banReason,
json.isDeleted, playerStats, instanceStats
);
}
toJSON() {
const json: Static<typeof RawUser.jsonSchema> = {
name: this.name,
roles: this.roles,
instances: this.instances,
isAdmin: this.isAdmin,
isBanned: this.isBanned,
isWhitelisted: this.isWhitelisted,
banReason: this.banReason,
isDeleted: this.isDeleted,
};
if (this.playerStats) {
json.playerStats = this.playerStats.toJSON()
}
if (this.instanceStats) {
json.instanceStats = [...this.instanceStats].map(([k, v]) => [k, v.toJSON()]);
}
return json;
}
}
export class UserGetRequest {
declare ["constructor"]: typeof UserGetRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.user.get" as const;
constructor(
public name: string,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name);
}
static Response = RawUser;
}
export class UserListRequest {
declare ["constructor"]: typeof UserListRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.user.list" as const;
static Response = jsonArray(RawUser);
}
export class UserSetSubscriptionsRequest {
declare ["constructor"]: typeof UserSetSubscriptionsRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.user.subscribe" as const;
constructor(
public all: boolean,
public names: string[],
) { }
static jsonSchema = Type.Object({
"all": Type.Boolean(),
"names": Type.Array(Type.String()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.all, json.names);
}
}
export class UserCreateRequest {
declare ["constructor"]: typeof UserCreateRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.user.create" as const;
constructor(
public name: string,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name);
}
}
export class UserRevokeTokenRequest {
declare ["constructor"]: typeof UserRevokeTokenRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.user.revoke_token" as const;
constructor(
public name: string,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name);
}
}
export class UserUpdateRolesRequest {
declare ["constructor"]: typeof UserUpdateRolesRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.user.update_roles" as const;
constructor(
public name: string,
public roles: number[],
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"roles": Type.Array(Type.Integer()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.roles);
}
}
export class UserSetAdminRequest {
declare ["constructor"]: typeof UserSetAdminRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.user.set_admin" as const;
constructor(
public name: string,
public create: boolean,
public admin: boolean,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"create": Type.Boolean(),
"admin": Type.Boolean(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.create, json.admin);
}
}
export class UserSetWhitelistedRequest {
declare ["constructor"]: typeof UserSetWhitelistedRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.user.set_whitelisted" as const;
constructor(
public name: string,
public create: boolean,
public whitelisted: boolean,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"create": Type.Boolean(),
"whitelisted": Type.Boolean(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.create, json.whitelisted);
}
}
export class UserSetBannedRequest {
declare ["constructor"]: typeof UserSetBannedRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.user.set_banned" as const;
constructor(
public name: string,
public create: boolean,
public banned: boolean,
public reason: string,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
"create": Type.Boolean(),
"banned": Type.Boolean(),
"reason": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name, json.create, json.banned, json.reason);
}
}
export class UserDeleteRequest {
declare ["constructor"]: typeof UserDeleteRequest;
static type = "request" as const;
static src = "control" as const;
static dst = "controller" as const;
static permission = "core.user.delete" as const;
constructor(
public name: string,
) { }
static jsonSchema = Type.Object({
"name": Type.String(),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.name);
}
}
export class AccountUpdateEvent {
declare ["constructor"]: typeof AccountUpdateEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "control" as const;
constructor(
public roles?: { name: string, id: number, permissions: string[] }[],
) { }
static jsonSchema = Type.Object({
roles: Type.Array(
Type.Object({
name: Type.String(),
id: Type.Integer(),
permissions: Type.Array(Type.String()),
}),
),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.roles);
}
}
export class UserUpdateEvent {
declare ["constructor"]: typeof UserUpdateEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "control" as const;
constructor(
public user: RawUser,
) { }
static jsonSchema = Type.Object({
"user": RawUser.jsonSchema,
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(RawUser.fromJSON(json.user));
}
}
@@ -1,5 +1,4 @@
// Helper functions for dealing with factorio version numbers
"use strict";
/**
@@ -7,11 +6,10 @@
*
* Suitable for sorting and comparing version numbers which may have different
* number of leading zeros in them.
* @param {string} version - string representation of version.
* @returns {number} numeric representation.
* @alias module:lib.integerModVersion
* @param version - string representation of version.
* @returns numeric representation.
*/
function integerModVersion(version) {
export function integerModVersion(version: string) {
const [major, minor, sub] = version.split(".").map(n => Number.parseInt(n, 10));
return major * 0x100000000 + minor * 0x10000 + sub; // Can't use bitwise here because this is 48-bits.
}
@@ -21,24 +19,15 @@ function integerModVersion(version) {
*
* Suitable for sorting and comparing version numbers which may have different
* number of leading zeros in them.
* @param {string} version - string representation of version.
* @returns {number} numeric representation.
* @alias module:lib.integerFactorioVersion
* @param version - string representation of version.
* @returns numeric representation.
*/
function integerFactorioVersion(version) {
export function integerFactorioVersion(version: string) {
const [main, major, minor] = version.split(".").map(n => Number.parseInt(n, 10));
return main * 0x100000000 + major * 0x10000 + (minor || 0); // Can't use bitwise here because this is 48-bits.
}
/**
* Matches valid mod versions.
* @type {RegExp}
* @alias module:lib.modVersionRegExp
*/
const modVersionRegExp = /^\d+\.\d+\.\d+$/;
module.exports = {
integerModVersion,
integerFactorioVersion,
modVersionRegExp,
};
export const modVersionRegExp = /^\d+\.\d+\.\d+$/;
@@ -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<T>(map: Map<string, T>) {
let obj: Record<string, T> = {};
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<Map<string, unknown>> {
try {
let content = await fs.readFile(filePath);
let parsed = JSON.parse(content);
let parsed = JSON.parse(content.toString());
if (basicType(parsed) !== "object") {
throw new Error(`Expected object but got ${basicType(parsed)}`);
@@ -55,7 +54,7 @@ async function loadJsonAsMap(filePath) {
return new Map(Object.entries(parsed));
} catch (err) {
} catch (err: any) {
if (err.code === "ENOENT") {
// File does not exist, return empty map
return new Map();
@@ -72,12 +71,12 @@ async function loadJsonAsMap(filePath) {
* If the directory the file is to be saved into doesn't exist it will be
* created.
*
* @param {string} filePath - The path to the JSON file that will be saved.
* @param {Map} map - Map with only string keys to save.
* @param filePath - The path to the JSON file that will be saved.
* @param map - Map with only string keys to save.
* @throws {Error} if there are non-string keys in map.
* @throws {Error} if an error occured writing to the file.
*/
async function saveMapAsJson(filePath, map) {
export async function saveMapAsJson(filePath: string, map: Map<string, unknown>) {
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<Map<unknown, unknown>> {
try {
let content = await fs.readFile(filePath);
let parsed = JSON.parse(content);
let parsed = JSON.parse(content.toString());
if (basicType(parsed) !== "array") {
throw new Error(`Expected array but got ${basicType(parsed)}`);
@@ -116,7 +115,7 @@ async function loadJsonArrayAsMap(filePath) {
}
return map;
} catch (err) {
} catch (err: any) {
if (err.code === "ENOENT") {
// File does not exist, return empty map
return new Map();
@@ -132,24 +131,24 @@ async function loadJsonArrayAsMap(filePath) {
* Save the values of a Map with objects containing an id property that is
* equal to the key the objects are stored at into a JSON file.
*
* @param {string} filePath - The path to the JSON file that will be saved.
* @param {Map} map -
* @param filePath - The path to the JSON file that will be saved.
* @param map -
* Map with objects containing an id property equal to the key they are
* stored under.
* @throws {Error} if an error occured writing to the file.
*/
async function saveMapAsJsonArray(filePath, map) {
export async function saveMapAsJsonArray(filePath: string, map: Map<unknown, { id: unknown }>) {
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<string, number> = 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<string, number> = {};
for (let [name, count] of this._items) {
if (count !== 0) {
obj[name] = count;
@@ -223,10 +221,10 @@ class ItemDatabase {
* database. If the item has not been previously stored into the database
* then 0 is returned.
*
* @param {string} name - The name of the item to get the count of.
* @returns {number} The count of the item stored.
* @param name - The name of the item to get the count of.
* @returns The count of the item stored.
*/
getItemCount(name) {
getItemCount(name: string) {
checkName(name);
if (!this._items.has(name)) {
@@ -241,15 +239,15 @@ class ItemDatabase {
*
* Add count copies of the item specified by name to the item database.
*
* @param {string} name - The name of the item to add.
* @param {number} count - The count of item to remove.
* @param name - The name of the item to add.
* @param count - The count of item to remove.
*/
addItem(name, count) {
addItem(name: string, count: number) {
checkName(name);
checkCount(count);
if (this._items.has(name)) {
count += this._items.get(name);
count += this._items.get(name)!;
}
this._items.set(name, count);
@@ -262,27 +260,18 @@ class ItemDatabase {
* If count is greater than the currently stored count the stored item
* count will become negative.
*
* @param {string} name - The name of the item to remove.
* @param {number} count - The count of items to remove.
* @param name - The name of the item to remove.
* @param count - The count of items to remove.
*/
removeItem(name, count) {
removeItem(name: string, count: number) {
checkName(name);
checkCount(count);
count = -count;
if (this._items.has(name)) {
count += this._items.get(name);
count += this._items.get(name)!;
}
this._items.set(name, count);
};
}
module.exports = {
mapToObject,
loadJsonAsMap,
saveMapAsJson,
loadJsonArrayAsMap,
saveMapAsJsonArray,
ItemDatabase,
};
-92
View File
@@ -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,
};
+96
View File
@@ -0,0 +1,96 @@
/**
* Errors thrown in Clusterio
* @module lib/errors
*/
/**
* Thrown by commands when they fail
*/
export class CommandError extends Error {
code = "CommandError";
}
/**
* Thrown from requests sent when an error occured handling it
*/
export class RequestError extends Error {
constructor(
message: string,
public code = "RequestError",
public stack?: string,
) {
super(message);
}
}
/**
* Thrown when a permission check fails.
*
* Is a subclass of RequestError to prevent logging stack traces when
* requests fail due to permission denied.
*
*/
export class PermissionError extends RequestError {
constructor(
message: string,
code = "PermissionError",
stack?: string,
) {
super(message, code, stack);
}
}
/**
* Signal for messages that fail validation
*/
export class InvalidMessage extends Error {
code = "InvalidMessage";
constructor(
message: string,
public errors?: object | null,
) {
super(message);
}
}
/**
* Thrown from requests when the session it was sent on was lost
*/
export class SessionLost extends Error {
code = "SessionLost";
}
/**
* Thrown when WebSocket authentication failed
*/
export class AuthenticationFailed extends Error {
code = "AuthenticationFailed";
}
/**
* Errror class for known errors occuring during startup
*/
export class StartupError extends Error {
code = "StartupError";
}
/**
* Errors outside of our control
*/
export class EnvironmentError extends Error {
code = "EnvironmentError";
}
/**
* Errors caused by plugins
*/
export class PluginError extends Error {
code = "PluginError";
constructor(
public pluginName: string,
public original: Error
) {
super(`PluginError: ${original.message}`);
}
}
@@ -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<T>(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<T>(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<K, V>(
state: MapReaderState,
readKey: (state: MapReaderState) => K,
readValue: (state: MapReaderState) => V
) {
let size = readUInt32so(state);
let mapping = new Map();
let mapping = new Map<K, V>();
for (let i = 0; i < size; i++) {
let key = readKey(state);
let value = readValue(state);
@@ -106,7 +109,7 @@ function readDict(state, readKey, readValue) {
return mapping;
}
function readVersion(state) {
function readVersion(state: MapReaderState) {
let major = readUInt16(state);
let minor = readUInt16(state);
let patch = readUInt16(state);
@@ -114,7 +117,7 @@ function readVersion(state) {
return [major, minor, patch, developer];
}
function readFrequencySizeRichness(state) {
function readFrequencySizeRichness(state: MapReaderState) {
return {
frequency: readFloat(state),
size: readFloat(state),
@@ -122,15 +125,15 @@ function readFrequencySizeRichness(state) {
};
}
function readAutoplaceSetting(state) {
function readAutoplaceSetting(state: MapReaderState) {
return {
treat_missing_as_default: readBool(state),
settings: Object.fromEntries(readDict(state, readString, readFrequencySizeRichness)),
};
}
function readMapPosition(state) {
let x, y;
function readMapPosition(state: MapReaderState) {
let x: number, y: number;
let x_diff = readInt16(state) / 256;
if (x_diff === 0x7fff / 256) {
x = readInt32(state) / 256;
@@ -145,7 +148,7 @@ function readMapPosition(state) {
return { x, y };
}
function readBoundingBox(state) {
function readBoundingBox(state: MapReaderState) {
return {
left_top: readMapPosition(state),
right_bottom: readMapPosition(state),
@@ -156,7 +159,7 @@ function readBoundingBox(state) {
};
}
function readCliffSettings(state) {
function readCliffSettings(state: MapReaderState) {
return {
name: readString(state),
elevation_0: readFloat(state),
@@ -165,7 +168,7 @@ function readCliffSettings(state) {
};
}
function readMapGenSettings(state) {
function readMapGenSettings(state: MapReaderState) {
return {
terrain_segmentation: readFloat(state),
water: readFloat(state),
@@ -184,7 +187,7 @@ function readMapGenSettings(state) {
};
}
function readPollution(state) {
function readPollution(state: MapReaderState) {
return {
enabled: readOptional(state, readBool),
diffusion_ratio: readOptional(state, readDouble),
@@ -201,24 +204,23 @@ function readPollution(state) {
};
}
function readSteeringValue(state) {
function readSteeringValue(state: MapReaderState) {
return {
radius: readOptional(state, readDouble),
separation_factor: readOptional(state, readDouble),
separation_force: readOptional(state, readDouble),
force_unit_fuzzy_goto_behavior: readOptional(state, readBool),
};
}
function readSteering(state) {
function readSteering(state: MapReaderState) {
return {
default: readSteeringValue(state),
moving: readSteeringValue(state),
};
}
function readEnemyEvolution(state) {
function readEnemyEvolution(state: MapReaderState) {
return {
enabled: readOptional(state, readBool),
time_factor: readOptional(state, readDouble),
@@ -227,7 +229,7 @@ function readEnemyEvolution(state) {
};
}
function readEnemyExpansion(state) {
function readEnemyExpansion(state: MapReaderState) {
return {
enabled: readOptional(state, readBool),
max_expansion_distance: readOptional(state, readUInt32),
@@ -245,7 +247,7 @@ function readEnemyExpansion(state) {
};
}
function readUnitGroup(state) {
function readUnitGroup(state: MapReaderState) {
return {
min_group_gathering_time: readOptional(state, readUInt32),
max_group_gathering_time: readOptional(state, readUInt32),
@@ -263,7 +265,7 @@ function readUnitGroup(state) {
};
}
function readPathFinder(state) {
function readPathFinder(state: MapReaderState) {
return {
fwd2bwd_ratio: readOptional(state, readInt32),
goal_pressure_ratio: readOptional(state, readDouble),
@@ -301,7 +303,7 @@ function readPathFinder(state) {
};
}
function readDifficultySettings(state) {
function readDifficultySettings(state: MapReaderState) {
return {
recipe_difficulty: readUInt8(state),
technology_difficulty: readUInt8(state),
@@ -310,7 +312,7 @@ function readDifficultySettings(state) {
};
}
function readMapSettings(state) {
function readMapSettings(state: MapReaderState) {
return {
pollution: readPollution(state),
steering: readSteering(state),
@@ -323,18 +325,23 @@ function readMapSettings(state) {
};
}
/**
* @typedef {object} module:lib.MapExchangeResult
* @property {array} version -
* Version of Factorio the string was created with.
* @property {object} map_gen_settings -
* Decoded map generator settings in the format the --map-gen-settings
* command line option to Factorio expect.
* @property {object} map_settings -
* Decoded map settings in the format the --map-settings command line option
* to Factorio expects.
* @property {number} checksum - checksum for the exchange string.
*/
export interface MapExchangeData {
/** Version of Factorio the string was created with. */
version: ReturnType<typeof readVersion>;
unknown: number;
/**
* Decoded map generator settings in the format the --map-gen-settings
* command line option to Factorio expect.
*/
map_gen_settings: ReturnType<typeof readMapGenSettings>;
/**
* Decoded map settings in the format the --map-settings command line
* option to Factorio expects.
*/
map_settings: ReturnType<typeof readMapSettings>;
/** CRC32 checksum for the exchange string. */
checksum: number;
}
/**
* Parse a Map Exchange String
@@ -343,11 +350,10 @@ function readMapSettings(state) {
* data structures that can be fed into the Factorio server when creating a
* save to set the map gen settings and the map settings for the save.
*
* @param {string} exchangeString - Max Exchange String to parse.
* @returns {module:lib.MapExchangeResult} Parsed result.
* @memberof module:lib
* @param exchangeString - Max Exchange String to parse.
* @returns Parsed result.
*/
function readMapExchangeString(exchangeString) {
export function readMapExchangeString(exchangeString: string) {
exchangeString = exchangeString.replace(/[ \t\n\r]+/g, "");
if (!/>>>[0-9a-zA-Z\/+]+={0,3}<<</.test(exchangeString)) {
throw new Error("Not a map exchange string");
@@ -357,14 +363,14 @@ function readMapExchangeString(exchangeString) {
try {
// eslint-disable-next-line node/no-sync
buf = zlib.inflateSync(buf);
} catch (err) {
} catch (err: any) {
if (err.code.startsWith("Z_")) {
throw new Error("Malformed map exchange string: zlib inflate failed");
}
}
let state = new MapReaderState(buf);
let data;
let data: MapExchangeData;
try {
data = {
@@ -374,7 +380,7 @@ function readMapExchangeString(exchangeString) {
map_settings: readMapSettings(state),
checksum: readUInt32(state),
};
} catch (err) {
} catch (err: any) {
if (err.code === "ERR_OUT_OF_RANGE") {
throw new Error("Malformed map exchange string: reached end before finishing parsing");
}
@@ -387,7 +393,3 @@ function readMapExchangeString(exchangeString) {
return data;
}
module.exports = {
readMapExchangeString,
};
@@ -3,9 +3,4 @@
* @module lib/factorio
* @author Hornwitser
*/
"use strict";
const exchange_string = require("./exchange_string");
module.exports = {
...exchange_string,
};
export * from "./exchange_string";
@@ -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<string|undefined>}
* @returns
* Name of the file with the newest timestamp or null if the
* directory contains no files.
*/
async function getNewestFile(directory, filter = (name) => true) {
export async function getNewestFile(directory: string, filter = (_name: string) => true) {
let newestTime = new Date(0);
let newestFile;
let newestFile: string | undefined;
for (let entry of await fs.readdir(directory, { withFileTypes: true })) {
if (entry.isFile()) {
let stat = await fs.stat(path.join(directory, entry.name));
@@ -40,20 +39,20 @@ async function getNewestFile(directory, filter = (name) => true) {
* Error reading the size of files are ignored, and error reading the
* directory will result in 0 being returned.
*
* @param {string} directory - The directory to sum files in.
* @returns {Promise<number>} The size in bytes of all files in the directory.
* @param directory - The directory to sum files in.
* @returns The size in bytes of all files in the directory.
*/
async function directorySize(directory) {
let dirEntries;
export async function directorySize(directory: string) {
let dirEntries: fs.Dirent[];
try {
dirEntries = await fs.readdir(directory, { withFileTypes: true });
} catch (err) {
} catch (err: any) {
if (err.code === "ENOENT") {
return 0;
}
throw err;
}
let statTasks = [];
let statTasks: Promise<number>[] = [];
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<string>} modified name with extension that likely does
* @param directory - directory to check in.
* @param name - file name to check for, may have extension.
* @param extension - dot extension used for the file name.
* @returns modified name with extension that likely does
* not exist in the folder
*/
async function findUnusedName(directory, name, extension = "") {
export async function findUnusedName(directory: string, name: string, extension = "") {
if (extension && name.endsWith(extension)) {
name = name.slice(0, -extension.length);
}
@@ -93,7 +92,7 @@ async function findUnusedName(directory, name, extension = "") {
return `${name}${extension}`;
}
let match = /^(.*?)(-(\d+))?$/.exec(name);
let match = /^(.*?)(-(\d+))?$/.exec(name)!;
if (!match[2]) {
name = `${match[1]}-2`;
} else {
@@ -101,25 +100,27 @@ async function findUnusedName(directory, name, extension = "") {
}
}
}
/**
* Generate collision resistante temporary file name
*
* Warning: this function should not be relied upon to be accurate for
* security sensitive applications. The selection process is inherently
* racy and a file or folder may have been created in the folder by the time
* this function returns.
*
* @param {string} prefix - Prefix for file
* @param {string} suffix - Suffix for file
* @param {string} tmpdir - Directory for temp file
* @param prefix - Prefix for file
* @param suffix - Suffix for file
* @param tmpdir - Directory for temp file
* @returns Temporary file name
*/
async function getTempFile(prefix, suffix, tmpdir) {
prefix = (typeof prefix !== "undefined") ? prefix : "tmp.";
suffix = (typeof suffix !== "undefined") ? suffix : "";
tmpdir = (typeof tmpdir !== "undefined") ? tmpdir : "./";
export async function getTempFile(prefix = "tmp.", suffix = "", tmpdir = "./") {
let fileName = path.join(prefix + crypto.randomBytes(16).toString("hex") + suffix);
let freeFile = await findUnusedName(tmpdir, fileName);
let fullPath = path.join(tmpdir, freeFile);
return fullPath;
}
/**
* Safely write data to a file
*
@@ -131,11 +132,11 @@ async function getTempFile(prefix, suffix, tmpdir) {
* should not be too much of an issue as the next time the same file is
* written the temporary will be overwritten and renamed to the target file.
*
* @param {string} file - Path to file to write.
* @param {string|Buffer} data - Content to write.
* @param {object|string} options - see fs.writeFile, `flag` must not be set.
* @param file - Path to file to write.
* @param data - Content to write.
* @param options - see fs.writeFile, `flag` must not be set.
*/
async function safeOutputFile(file, data, options={}) {
export async function safeOutputFile(file: string, data: string | Buffer, options: fs.WriteFileOptions ={}) {
let directory = path.dirname(file);
if (!await fs.pathExists(directory)) {
await fs.mkdirs(directory);
@@ -153,7 +154,7 @@ async function safeOutputFile(file, data, options={}) {
}
// Reserved names by allmost all filesystems
// Reserved names by almost all filesystems
const badNames = [".", ".."];
// Reserved namespaces in Windows
@@ -167,10 +168,10 @@ const badWinNamespaces = [
/**
* Check if a string is a valid file name
*
* @param {string} name - Name to check
* @param name - Name to check
* @throws Error if the name is unsuitable.
*/
function checkFilename(name) {
export function checkFilename(name: string) {
// All of these are bad in Windows only, except for /, . and ..
// See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
const badChars = /[<>:"\/\\|?*\x00-\x1f]/g;
@@ -208,10 +209,10 @@ function checkFilename(name) {
/**
* Clean up string to be suitable for use as filename
*
* @param {string} name - Arbitrary name string
* @returns {string} Filename suitable to use in the filesystem
* @param name - Arbitrary name string
* @returns Filename suitable to use in the filesystem
*/
function cleanFilename(name) {
export function cleanFilename(name: string) {
// copied from checkFilename due to RegExp with global flag containing state.
const badChars = /[<>:"\/\\|?*\x00-\x1f]/g;
const badEnd = /[. ]$/;
@@ -233,14 +234,3 @@ function cleanFilename(name) {
return name;
}
module.exports = {
getNewestFile,
directorySize,
findUnusedName,
getTempFile,
safeOutputFile,
checkFilename,
cleanFilename,
};
@@ -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<string>} 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<string> {
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<string>} 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,
};
@@ -2,7 +2,6 @@
* Collection of small utilites that are useful in multiple places.
* @module lib/helpers
*/
"use strict";
/**
* Return a string describing the type of the value passed
@@ -10,11 +9,10 @@
* Works the same as typeof, excpet that null and array types get their
* own string.
*
* @param {*} value - value to return the type of.
* @returns {string} basic type of the value passed.
* @static
* @param value - value to return the type of.
* @returns basic type of the value passed.
*/
function basicType(value) {
export function basicType(value: unknown) {
if (value === null) { return "null"; }
if (value instanceof Array) { return "array"; }
return typeof value;
@@ -24,10 +22,9 @@ function basicType(value) {
/**
* Asynchronously wait for the given duration
*
* @param {number} duration - Time to wait for in milliseconds.
* @static
* @param duration - Time to wait for in milliseconds.
*/
async function wait(duration) {
export async function wait(duration: number) {
await new Promise(resolve => { setTimeout(resolve, duration); });
}
@@ -38,10 +35,9 @@ async function wait(duration) {
* @param {Promise} promise - Promise to wait for.
* @param {number} time - Maximum time im milliseconds to wait for.
* @param {*=} timeoutResult - Value to return if the operation timed out.
* @static
*/
async function timeout(promise, time, timeoutResult) {
let timer;
export async function timeout<T>(promise: Promise<T>, time: number, timeoutResult: T) {
let timer: NodeJS.Timer | undefined;
try {
return await Promise.race([
promise,
@@ -62,18 +58,17 @@ async function timeout(promise, time, timeoutResult) {
* Reads the stream given asynchronously until the end is reached and
* returns all the data which was read from the stream.
*
* @param {Readable} stream - byte stream to read to the end.
* @returns {Promise<Buffer>} content of the stream.
* @static
* @param stream - byte stream to read to the end.
* @returns content of the stream.
*/
async function readStream(stream) {
let chunks = [];
export async function readStream(stream: NodeJS.ReadableStream & { isTTY?: boolean }) {
let chunks: Buffer[] = [];
for await (let chunk of stream) {
// Support using ^Z to end input on Windows
if (process.platform === "win32" && stream.isTTY && chunk.toString() === "\x1a\r\n") {
break;
}
chunks.push(chunk);
chunks.push(chunk as Buffer);
}
return Buffer.concat(chunks);
}
@@ -87,11 +82,10 @@ async function readStream(stream) {
*
* See https://stackoverflow.com/a/9310752
*
* @param {string} text - Text to escape RegExp meta chars in.
* @returns {string} escaped text.
* @static
* @param text - Text to escape RegExp meta chars in.
* @returns escaped text.
*/
function escapeRegExp(text) {
export function escapeRegExp(text: string) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
@@ -99,11 +93,10 @@ function escapeRegExp(text) {
* Format byte count for human readable display
*
* Shortens a large number of bytes using the kB/MB/GB/TB prefixes.
* @param {number} bytes - Count of bytes to format.
* @returns {string} formatted text.
* @static
* @param bytes - Count of bytes to format.
* @returns formatted text.
*/
function formatBytes(bytes) {
export function formatBytes(bytes: number) {
if (bytes === 0) {
return "0\u{A0}Bytes"; // No-break space
}
@@ -114,7 +107,7 @@ function formatBytes(bytes) {
return (power > 0 ? (bytes / factor ** power).toFixed(2) : bytes) + units[power];
}
function parseSearchIdentifier(pos, input) {
function parseSearchIdentifier(pos: number, input: string): [number, string] {
// character = ? all characters excluding space : " ?
// identifier = 1*character
let startPos = pos;
@@ -124,15 +117,14 @@ function parseSearchIdentifier(pos, input) {
return [pos, input.slice(startPos, pos)];
}
function parseSearchWord(pos, input) {
function parseSearchWord(pos: number, input: string): [number, ParsedWord] {
// non quote character = ? all characters excluding " ?
// word op = "-"
// word = [ word op ], ( identifier | '"', *non quote character, '"' )
let word = {
type: "word",
};
let exclude = false;
let value: string;
if (input.charAt(pos) === "-") {
word.exclude = true;
exclude = true;
pos += 1;
}
if (input.charAt(pos) === '"') {
@@ -146,21 +138,33 @@ function parseSearchWord(pos, input) {
if (input.charAt(pos) === '"') {
pos += 1;
}
word.value = input.slice(startPos, endPos);
value = input.slice(startPos, endPos);
} else {
([pos, word.value] = parseSearchIdentifier(pos, input));
([pos, value] = parseSearchIdentifier(pos, input));
}
let word: ParsedWord = {
type: "word",
value,
};
if (exclude) {
word.exclude = true;
}
return [pos, word];
}
function parseSearchTerm(pos, input, attributes, issues) {
function parseSearchTerm(
pos: number,
input: string,
attributes: Record<string, string>,
issues: Array<string>
): [number, ParsedTerm | undefined] {
// attribute = identifier, ':', word
// term = word | attribute
let term;
let term: ParsedTerm | undefined;
if (["-", '"'].includes(input.charAt(pos))) {
([pos, term] = parseSearchWord(pos, input));
} else {
let identifier;
let identifier: string;
([pos, identifier] = parseSearchIdentifier(pos, input));
if (input.charAt(pos) === ":") {
pos += 1;
@@ -168,7 +172,7 @@ function parseSearchTerm(pos, input, attributes, issues) {
if (!Object.prototype.hasOwnProperty.call(attributes, identifier)) {
issues.push(`Unregonized attribute "${identifier}", use quotes to escape colons`);
} else if (attributes[identifier] === "word") {
let value;
let value: ParsedWord;
([pos, value] = parseSearchWord(pos, input));
term = { type: "attribute", name: identifier, value };
} else {
@@ -188,17 +192,17 @@ function parseSearchTerm(pos, input, attributes, issues) {
* of the passed text snippets. If the word mode is exclude then returns
* true if none of the passed text snippets match.
*
* @param {lib/helpers.ParsedTerm} word - Word to match.
* @param {...string} texts - Text to match word in.
* @returns {boolean} true if the word matches.
* @param word - Word to match.
* @param texts - Text to match word in.
* @returns true if the word matches.
*/
function wordMatches(word, ...texts) {
export function wordMatches(word: ParsedTerm, ...texts: string[]) {
if (word.type !== "word") {
throw Error("wordMatches: parameter is not a word");
}
let matches = false;
for (let text of texts) {
if (text.includes(word.value)) {
if (text.includes(word.value as string)) {
matches = true;
break;
}
@@ -209,23 +213,35 @@ function wordMatches(word, ...texts) {
return matches;
}
/**
* @typedef {object} module:lib~ParsedTerm
* @property {string} type - Type of term, either attribute or word.
* @property {string=} name - attribute only: Name of attribute.
* @property {boolean=} exclude - word only: exclude results with this word.
* @property {string | module:lib.ParsedTerm} value -
* Parsed value of this term. Is a string if type is word, and a
* ParsedTerm if type is attribute.
*/
export interface ParsedAttribute {
/** Type of term, either attribute or word. */
type: "attribute"
/** Name of attribute. */
name: string;
/** Parsed value of this attribute. */
value: ParsedWord;
}
export interface ParsedWord {
/** Type of term, either attribute or word. */
type: "word";
/** Exclude results with this word. */
exclude?: boolean;
/** Parsed text of this word. */
value: string;
}
export type ParsedTerm = ParsedAttribute | ParsedWord;
/**
* @typedef {object} module:lib~ParsedSearch
* @property {Array<module:lib~ParsedTerm>} terms -
* Parsed result of search terms.
* @property {Array<string>} issues -
* Issues detected while parsing the seach string.
* Result from {@link parseSearchString}.
*/
export interface ParsedSearch {
/** Parsed result of search terms. */
terms: Array<ParsedTerm>;
/** Issues detected while parsing the seach string. */
issues: Array<string>;
};
/**
* Parse a search string with optional attributes
@@ -244,24 +260,26 @@ function wordMatches(word, ...texts) {
* The issues should be shown to the end user so that they can correct their
* search.
*
* @param {string} input - Search expression to parse.
* @param {Object<string, string>} attributes -
* @param input - Search expression to parse.
* @param attributes -
* Recognized attributes and their format. Currently only word is
* supported.
* @returns {module:lib~ParsedSearch} parsed terms of the search.
* @static
* @returns parsed terms of the search.
*/
function parseSearchString(input, attributes = {}) {
export function parseSearchString(
input: string,
attributes: Record<string, string> = {}
): ParsedSearch {
// whitespace = 1*" "
// search = [ term, *( [ whitespace ], term ) ]
input = input.trim();
let parsed = {
terms: [],
issues: [],
terms: [] as ParsedTerm[],
issues: [] as string[],
};
let pos = 0;
while (pos < input.length) {
let term;
let term: ParsedTerm | undefined;
([pos, term] = parseSearchTerm(pos, input, attributes, parsed.issues));
if (term) {
parsed.terms.push(term);
@@ -276,15 +294,3 @@ function parseSearchString(input, attributes = {}) {
}
return parsed;
}
module.exports = {
basicType,
wait,
timeout,
readStream,
escapeRegExp,
formatBytes,
wordMatches,
parseSearchString,
};
@@ -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<string, object<string, string>>}
* 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<string, string | Record<string, string>> = {};
let currentSection: Record<string, string> = sections as Record<string, string>;
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<string, object<string, string>>} 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<string, string | Record<string, string>>) {
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,
};
+1
View File
@@ -0,0 +1 @@
export default function isDeepStrictEqual(a: any, b: any): boolean
+2
View File
@@ -0,0 +1,2 @@
import ws = require("ws");
export = ws;
+3
View File
@@ -0,0 +1,3 @@
"use strict";
// eslint-disable-next-line node/no-process-env
module.exports = process.env.APP_ENV === "browser" ? window.WebSocket : require("ws");
@@ -1,28 +1,29 @@
// Connection adapters for Links
"use strict";
const assert = require("assert").strict;
const events = require("events");
// eslint-disable-next-line node/no-process-env
const WebSocket = process.env.APP_ENV === "browser" ? window.WebSocket : require("ws");
import { strict as assert } from "assert";
import events from "events";
import WebSocket from "./WebSocket";
const libData = require("../data");
const libErrors = require("../errors");
const { logger } = require("../logging");
const ExponentialBackoff = require("../ExponentialBackoff");
import * as libData from "../data";
import * as libErrors from "../errors";
import { logger } from "../logging";
import ExponentialBackoff from "../ExponentialBackoff";
import type { Request, Event } from "./link";
/**
* Base connector for links
*
* @extends events.EventEmitter
* @memberof module:lib
*/
class BaseConnector extends events.EventEmitter {
constructor(src, dst) {
export abstract class BaseConnector extends events.EventEmitter {
protected _seq = 1;
constructor(
public src: libData.Address,
public dst: libData.Address,
) {
super();
this._seq = 1;
this.src = src;
this.dst = dst;
}
_reset() {
@@ -33,7 +34,13 @@ class BaseConnector extends events.EventEmitter {
this._seq = 1;
}
sendRequest(request, requestId, dst) {
abstract send(message: libData.Message): void;
sendRequest<Req, Res>(
request: Request<Req, Res>,
requestId: number,
dst: libData.Address
) {
let seq = this._seq;
this._seq += 1;
let src = new libData.Address(this.src.type, this.src.id, requestId);
@@ -45,7 +52,7 @@ class BaseConnector extends events.EventEmitter {
return seq;
}
sendResponse(response, dst, src = this.src) {
sendResponse(response: unknown, dst: libData.Address, src = this.src) {
let seq = this._seq;
this._seq += 1;
const message = new libData.MessageResponse(seq, src, dst, response);
@@ -53,7 +60,7 @@ class BaseConnector extends events.EventEmitter {
return seq;
}
sendResponseError(error, dst, src = this.src) {
sendResponseError(error: libData.ResponseError, dst: libData.Address, src = this.src) {
let seq = this._seq;
this._seq += 1;
const message = new libData.MessageResponseError(seq, src, dst, error);
@@ -61,7 +68,7 @@ class BaseConnector extends events.EventEmitter {
return seq;
}
sendEvent(event, dst) {
sendEvent<T>(event: Event<T>, dst: libData.Address) {
let seq = this._seq;
this._seq += 1;
let Event = event.constructor;
@@ -73,25 +80,32 @@ class BaseConnector extends events.EventEmitter {
}
}
type ConnectorState = "closed" | "connecting" | "connected" | "resuming";
type ReliableMessages =
| libData.MessageRequest
| libData.MessageResponse
| libData.MessageResponseError
| libData.MessageEvent
;
/**
* Base connector for links
*
* @extends module:lib.BaseConnector
* @memberof module:lib
*/
class WebSocketBaseConnector extends BaseConnector {
constructor(src, dst) {
super(src, dst);
export abstract class WebSocketBaseConnector extends BaseConnector {
// One of closed, connecting (client only), connected and resuming.
_state: ConnectorState = "closed";
_closing = false;
_socket: WebSocket | null = null;
_lastHeartbeat: number | null = null;
_heartbeatId: NodeJS.Timer | null = null;
_heartbeatInterval: number | null = null;
_lastReceivedSeq = undefined;
_sendBuffer: (ReliableMessages)[] = [];
// One of closed, connecting (client only), connected and resuming.
this._state = "closed";
this._closing = false;
this._socket = null;
this._lastHeartbeat = null;
this._heartbeatId = null;
this._heartbeatInterval = null;
this._lastReceivedSeq = undefined;
this._sendBuffer = [];
constructor(src: libData.Address, dst: libData.Address) {
super(src, dst);
}
_reset() {
@@ -118,7 +132,7 @@ class WebSocketBaseConnector extends BaseConnector {
super._invalidate();
}
_check(...expectedStates) {
_check(...expectedStates: ConnectorState[]) {
if (!expectedStates.includes(this._state)) {
throw new Error(
`Expected state ${expectedStates} but state is ${this._state}`
@@ -126,7 +140,7 @@ class WebSocketBaseConnector extends BaseConnector {
}
}
_dropSendBufferSeq(seq) {
_dropSendBufferSeq(seq?: number) {
if (seq === undefined) {
return;
}
@@ -142,8 +156,8 @@ class WebSocketBaseConnector extends BaseConnector {
this._sendBuffer.splice(0, dropCount);
}
_parseMessage(text) {
let json;
_parseMessage(text: string) {
let json: object;
try {
json = JSON.parse(text);
} catch (err) {
@@ -159,22 +173,24 @@ class WebSocketBaseConnector extends BaseConnector {
return libData.Message.fromJSON(json);
}
_processMessage(message) {
_processMessage(message: libData.Message) {
// TODO check if this can be inferred from Message type
if (message.type === "heartbeat") {
this._processHeartbeat(message);
this._processHeartbeat(message as libData.MessageHeartbeat);
} else if (message.type === "disconnect") {
if (message.data === "ready") {
const data = (message as libData.MessageDisconnect).data;
if (data === "ready") {
this.emit("disconnectReady");
} else if (message.data === "prepare") {
} else if (data === "prepare") {
this.setClosing();
this.emit("disconnectPrepare");
}
} else {
if (message.seq !== undefined) {
this._lastReceivedSeq = message.seq;
if ((message as any).seq !== undefined) {
this._lastReceivedSeq = (message as any).seq;
}
this.emit("message", message);
@@ -183,13 +199,13 @@ class WebSocketBaseConnector extends BaseConnector {
_doHeartbeat() {
this._check("connected");
if (Date.now() - this._lastHeartbeat > 2000 * this._heartbeatInterval) {
if (Date.now() - this._lastHeartbeat! > 2000 * this._heartbeatInterval!) {
logger.verbose("Connector | closing after heartbeat timed out");
// eslint-disable-next-line node/no-process-env
if (process.env.APP_ENV === "browser") {
this._socket.close(4008, "Heartbeat timeout");
this._socket!.close(4008, "Heartbeat timeout");
} else {
this._socket.terminate();
this._socket!.terminate();
}
} else {
@@ -197,7 +213,7 @@ class WebSocketBaseConnector extends BaseConnector {
}
}
_processHeartbeat(message) {
_processHeartbeat(message: libData.MessageHeartbeat) {
this._lastHeartbeat = Date.now();
this._dropSendBufferSeq(message.seq);
}
@@ -212,7 +228,7 @@ class WebSocketBaseConnector extends BaseConnector {
this._lastHeartbeat = Date.now();
this._heartbeatId = setInterval(() => {
this._doHeartbeat();
}, this._heartbeatInterval * 1000);
}, this._heartbeatInterval! * 1000);
}
/**
@@ -231,9 +247,9 @@ class WebSocketBaseConnector extends BaseConnector {
* This is a low level method that should only be used for implementing
* links. See sendTo for sending requests and events.
*
* @param {module:lib.Message} message - Message to send.
* @param message - Message to send.
*/
send(message) {
send(message: ReliableMessages) {
if (!["connected", "resuming"].includes(this._state)) {
throw new libErrors.SessionLost("No session");
}
@@ -244,8 +260,8 @@ class WebSocketBaseConnector extends BaseConnector {
}
}
_sendInternal(message) {
this._socket.send(JSON.stringify(message));
_sendInternal(message: libData.Message) {
this._socket!.send(JSON.stringify(message));
}
/**
@@ -254,9 +270,8 @@ class WebSocketBaseConnector extends BaseConnector {
* Signal the connection is about to close and that once the close
* frame is received it should be considered finished.
*/
setClosing() {
throw new Error("Abstract function");
}
abstract setClosing(): void;
abstract close(code: number, reason: string): Promise<void>;
/**
* Gracefully disconnect
@@ -270,9 +285,9 @@ class WebSocketBaseConnector extends BaseConnector {
}
this.setClosing();
this.send(new libData.MessageDisconnect("prepare"));
this._sendInternal(new libData.MessageDisconnect("prepare"));
let timeout;
let timeout: NodeJS.Timeout | undefined;
let waitTimeout = new Promise(resolve => { timeout = setTimeout(resolve, 10000); }); // Make Configurable?
try {
await Promise.race([
@@ -314,44 +329,46 @@ class WebSocketBaseConnector extends BaseConnector {
* Connector for controller clients
*
* @extends module:lib.WebSocketBaseConnector
* @memberof module:lib
*/
class WebSocketClientConnector extends WebSocketBaseConnector {
constructor(url, maxReconnectDelay, tlsCa = null) {
super(undefined, new libData.Address(libData.Address.controller, 0));
export abstract class WebSocketClientConnector extends WebSocketBaseConnector {
_reconnectId?: NodeJS.Timeout;
_sessionToken: string | null = null;
_sessionTimeout: number | null = null;
_startedResuming: number | null = null;
_backoff: ExponentialBackoff;
this._url = url;
constructor(
protected _url: string,
maxReconnectDelay: number,
protected _tlsCa = null
) {
super(undefined as any, new libData.Address(libData.Address.controller, 0));
this._backoff = new ExponentialBackoff({ max: maxReconnectDelay });
this._tlsCa = tlsCa;
// The following states are used in the client connector
// closed: Not connected
// connecting: Attempting to connect to server.
// resuming: Attempting to resume an existing connection to the server.
// connected: Connection is online
this._reconnectId = null;
this._sessionToken = null;
this._sessionTimeout = null;
this._startedResuming = null;
}
_reset() {
clearTimeout(this._reconnectId);
this._reconnectId = null;
this._reconnectId = undefined;
this._sessionToken = null;
this._sessionTimeout = null;
this._startedResuming = null;
super._reset();
this.src = undefined;
this.src = undefined as any;
}
_invalidate() {
assert(this._reconnectId === null);
assert(this._reconnectId === undefined);
this._sessionToken = null;
this._sessionTimeout = null;
this._startedResuming = null;
super._invalidate();
this.src = undefined;
this.src = undefined as any;
}
/**
@@ -360,9 +377,9 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
* Used in the register function in order to send the handshake messages
* over the WebSocket.
*
* @param {module:lib.Message} message - Message to send.
* @param message - Message to send.
*/
sendHandshake(message) {
sendHandshake(message: libData.Message) {
this._check("connecting", "resuming");
this._sendInternal(message);
}
@@ -372,17 +389,17 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
*
* Sends a close frame and disconnects the connector.
*
* @param {number} code - WebSocket close code.
* @param {string} reason - WebSocket close reason.
* @param code - WebSocket close code.
* @param reason - WebSocket close reason.
*/
async close(code, reason) {
async close(code: number, reason: string) {
if (this._state === "closed") {
return;
}
this._closing = true;
if (this._state === "connected" || this._socket) {
this._socket.close(code, reason);
this._socket!.close(code, reason);
await events.once(this, "close");
} else {
@@ -409,7 +426,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
}
if (this._socket) {
this._socket.close(1000, "Connector closing");
this._socket!.close(1000, "Connector closing");
} else {
this._reset();
@@ -443,7 +460,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
this._socket = new WebSocket(url);
} else {
let options = {};
let options: { ca?: string } = {};
if (this._tlsCa) { options.ca = this._tlsCa; }
this._socket = new WebSocket(url, options);
}
@@ -459,18 +476,18 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
throw new Error("Cannot reconnect while socket is open");
}
if (this._reconnectId !== null) {
if (this._reconnectId !== undefined) {
logger.error("Unexpected double call to reconnect");
}
let delay = this._backoff.delay();
if (
this._state === "resuming"
&& this._startedResuming + this._sessionTimeout * 1000 < Date.now() + delay
&& this._startedResuming! + this._sessionTimeout! * 1000 < Date.now() + delay
) {
logger.error("Connector | Session timed out trying to resume");
this._reset();
if (this._disconnecting) {
if (this._closing) {
this._reset();
this.emit("close");
} else {
@@ -482,14 +499,13 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
`Connector | waiting ${(Math.round(delay / 10) / 100)} seconds for reconnect`
);
this._reconnectId = setTimeout(() => {
this._reconnectId = null;
this._reconnectId = undefined;
this._doConnect();
}, delay);
this._reconnectTime = Date.now() + delay;
}
_attachSocketHandlers() {
this._socket.onclose = event => {
this._socket!.onclose = (event) => {
const previousState = this._state;
// Authentication failed
@@ -532,7 +548,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
}
};
this._socket.onerror = event => {
this._socket!.onerror = (event) => {
// It's assumed that close is always called by ws
let code = !event.error ? "" : `, code: ${event.error.code}`;
let message = `Connector | Socket error: ${event.message || "unknown error"}${code}`;
@@ -556,13 +572,13 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
}
};
this._socket.onopen = () => {
this._socket!.onopen = () => {
logger.verbose("Connector | Open");
};
// Handle messages
this._socket.onmessage = event => {
let message = this._parseMessage(event.data);
this._socket!.onmessage = (event) => {
let message = this._parseMessage(event.data as string);
if (!message) {
return;
}
@@ -578,9 +594,9 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
};
}
_processHandshake(message) {
let { type, data } = message;
_processHandshake(message: libData.Message) {
// TODO infer data from Message type.
let { type, data } = message as libData.Message & { data: any };
if (type === "hello") {
logger.verbose(`Connector | received hello from controller version ${data.version}`);
this.emit("hello", data);
@@ -621,7 +637,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
} else if (type === "invalidate") {
logger.warn("Connector | session invalidated by controller");
if (this._disconnecting) {
if (this._closing) {
this._reset();
this.emit("close");
} else {
@@ -638,19 +654,18 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
* This function is expected to be overriden by sub-classes and send the
* register message over the socket when invoked.
*/
register() {
throw Error("Abstract function");
}
abstract register(): void;
}
/**
* Connector for in-memory local links
*
* @extends events.EventEmitter
* @memberof module:lib
*/
class VirtualConnector extends BaseConnector {
constructor(src, dst) {
export class VirtualConnector extends BaseConnector {
other: VirtualConnector;
constructor(src: libData.Address, dst: libData.Address) {
super(src, dst);
this.other = this;
@@ -662,11 +677,14 @@ class VirtualConnector extends BaseConnector {
* Creates two virtual connectors that are liked to each othes such that
* a message sent on one is received by the other.
*
* @param {module:lib.Address} src - Source for obverse connector
* @param {module:lib.Address} dst - destination for obverse connector
* @returns {Array} two virtuarl connectors.
* @param src - Source for obverse connector
* @param dst - destination for obverse connector
* @returns two virtuarl connectors.
*/
static makePair(src, dst) {
static makePair(
src: libData.Address,
dst: libData.Address,
): [VirtualConnector, VirtualConnector] {
let obverse = new this(src, dst);
let reverse = new this(dst, src);
obverse.other = reverse;
@@ -677,16 +695,9 @@ class VirtualConnector extends BaseConnector {
/**
* Send a message to the other end of the connector
*
* @param {module:lib.Message} message - Message type to send.
* @param message - Message type to send.
*/
send(message) {
send(message: libData.Message) {
this.other.emit("message", message);
}
}
module.exports = {
BaseConnector,
WebSocketBaseConnector,
WebSocketClientConnector,
VirtualConnector,
};
-17
View File
@@ -1,17 +0,0 @@
/**
* Library for message based communication between nodes
* @module lib/link
* @author Hornwitser
*/
"use strict";
const link = require("./link");
const connectors = require("./connectors");
module.exports = {
...link,
...connectors,
// migrate: Allow info for plugins from before link refactor to load.
Event: class Event {},
Request: class Request {},
};
+7
View File
@@ -0,0 +1,7 @@
/**
* Library for message based communication between nodes
* @module lib/link
* @author Hornwitser
*/
export * from "./link"
export * from "./connectors"
@@ -1,11 +1,78 @@
// Implementation of Link class
"use strict";
import * as libData from "../data";
import * as libErrors from "../errors";
import { logger } from "../logging";
import * as libSchema from "../schema";
import { dataClasses } from "./messages";
import { BaseConnector, WebSocketBaseConnector } from "./connectors";
import { strict as assert } from "assert";
import type { PluginInfo } from "../plugin";
import type { User } from "../users";
import type { AddressType, JSONDeserialisable, Message, MessageRequest, MessageEvent } from "../data";
const libData = require("../data");
const libErrors = require("../errors");
const { logger } = require("../logging");
const libSchema = require("../schema");
const { dataClasses } = require("./messages");
export interface Request<Req, Res> {
constructor: Partial<JSONDeserialisable<Request<Req, Res>>> & {
new (...args: any): Request<Req, Res>,
name: string,
type: "request";
src: AddressType | readonly AddressType[];
dst: AddressType | readonly AddressType[];
permission?: null | string | ((user: User, message: MessageRequest) => void);
plugin?: string;
Response?: JSONDeserialisable<Res>,
}
}
export type RequestClass<Req, Res> = Request<Req, Res>["constructor"];
export interface Event<T> {
constructor: Partial<JSONDeserialisable<Event<T>>> & {
new (...args: any): Event<T>,
name: string,
type: "event";
src: AddressType | readonly AddressType[];
dst: AddressType | readonly AddressType[];
permission?: null | string | ((user: User, message: MessageEvent) => void);
plugin?: string;
}
}
export type EventClass<T> = Event<T>["constructor"];
interface RequestEntry {
Request: RequestClass<unknown, unknown>;
name: string,
allowedSrcTypes: Set<number>;
allowedDstTypes: Set<number>;
requestFromJSON: (json: any) => Request<unknown, unknown>;
Response?: any;
responseFromJSON: (json: any) => any;
}
interface PendingRequest {
promise: Promise<any>;
request: RequestEntry;
resolve: (result: any) => void;
reject: (err: Error) => void;
dst: libData.Address;
}
interface ForwardedRequest {
origin: Link;
src: libData.Address;
dst: libData.Address;
}
interface EventEntry {
Event: EventClass<unknown>;
name: string,
allowedSrcTypes: Set<number>;
allowedDstTypes: Set<number>;
eventFromJSON: (json: any) => Event<unknown>;
}
type RequestHandler<Req, Res> = (request: Request<Req, Res>, src: libData.Address, dst: libData.Address) => any;
type EventHandler<T> = (event: Event<T>, src: libData.Address, dst: libData.Address) => any;
interface Router {
forwardMessage(origin: Link, message: Message, fallback: boolean): boolean,
}
// Some definitions for the terminology used here:
// link: Either side of a controller - client connection
@@ -16,23 +83,22 @@ const { dataClasses } = require("./messages");
/**
* Common interface for server and client connections
*
* @memberof module:lib
*/
class Link {
constructor(connector) {
this.connector = connector;
this.router = undefined;
this.validateSent = true;
export class Link {
router?: Router = undefined;
validateSent = true;
this._requestHandlers = new Map();
this._requestFallbacks = new Map();
this._eventHandlers = new Map();
this._eventSnoopers = new Map();
this._pendingRequests = new Map();
this._forwardedRequests = new Map();
this._nextRequestId = 1;
_requestHandlers = new Map();
_requestFallbacks = new Map();
_eventHandlers = new Map();
_eventSnoopers = new Map();
_pendingRequests = new Map<number, PendingRequest>();
_forwardedRequests = new Map<string, ForwardedRequest>();
_nextRequestId = 1;
constructor(
public connector: BaseConnector
) {
this.handle(libData.PingRequest, () => {});
// Process messages received by the connector
@@ -74,11 +140,12 @@ class Link {
});
}
_clearPendingRequests(err) {
_clearPendingRequests(err: Error & { code?: string }) {
for (let pending of this._pendingRequests.values()) {
pending.reject(err);
}
for (let pending of this._forwardedRequests.values()) {
assert(pending.origin.connector instanceof WebSocketBaseConnector);
if (pending.origin.connector.hasSession) {
pending.origin.connector.sendResponseError(
new libData.ResponseError(err.message, err.code), pending.src, pending.dst
@@ -95,10 +162,12 @@ class Link {
* Validates and invokes the handler and/or waiters for a message that has
* been received. An unhandled message is considered to be an error.
*
* @param {module:lib.Message} message - Message to process.
* @throws {module:lib.InvalidMessage} if the message is invalid or not handled.
* @param message - Message to process.
* @throws {libErrors.InvalidMessage} if the message is invalid or not handled.
*/
_processMessage(message) {
_processMessage(
message: libData.MessageRequest | libData.MessageResponse | libData.MessageResponseError | libData.MessageEvent
) {
if (!["request", "response", "responseError", "event"].includes(message.type)) {
throw new libErrors.InvalidMessage(`Unhandled message type ${message.type}`);
}
@@ -115,10 +184,11 @@ class Link {
}
}
if (message.type === "event" && this._eventSnoopers.has(entry.Event)) {
let handler = this._eventSnoopers.get(entry.Event);
handler(entry.eventFromJSON(message.data), message.src, message.dst).catch(err => {
logger.error(`Unexpected error snooping ${message.name}:\n${err.stack}`);
if (message.type === "event" && this._eventSnoopers.has((entry as EventEntry).Event)) {
let handler = this._eventSnoopers.get((entry as EventEntry).Event);
let event = message as libData.MessageEvent;
handler((entry as EventEntry).eventFromJSON(event.data), event.src, event.dst).catch((err: Error) => {
logger.error(`Unexpected error snooping ${event.name}:\n${err.stack}`);
});
}
@@ -131,16 +201,23 @@ class Link {
}
if (message.type === "request") {
this._processRequest(message, entry, this._requestHandlers.get(entry.Request));
this._processRequest(
message as libData.MessageRequest,
entry as RequestEntry,
this._requestHandlers.get((entry as RequestEntry).Request)
);
} else if (message.type === "response") {
this._processResponse(message);
this._processResponse(message as libData.MessageResponse);
} else if (message.type === "responseError") {
this._processResponseError(message);
this._processResponseError(message as libData.MessageResponseError);
} else if (message.type === "event") {
this._processEvent(message, entry);
this._processEvent(
message as libData.MessageEvent,
entry as EventEntry
);
}
}
@@ -149,18 +226,20 @@ class Link {
*
* Should be overridden by sub-classes to validate messages received.
*
* @param {module:lib.Message} message - Message to check.
* @throws {module:lib.InvalidMessage} if the message is invalid.
* @param message - Message to check.
* @throws {libErrors.InvalidMessage} if the message is invalid.
*/
validateIngress(message) { }
validateIngress(message: libData.Message) { }
_validateMessage(message) {
_validateMessage(
message: libData.MessageRequest | libData.MessageResponse | libData.MessageResponseError | libData.MessageEvent
) {
try {
if (message.src.type === libData.Address.broadcast) {
throw new libErrors.InvalidMessage("Message src may not be broadcast");
}
this.validateIngress(message);
} catch (err) {
} catch (err: any) {
if (message.type === "request") {
this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src);
}
@@ -168,7 +247,8 @@ class Link {
}
if (message.type === "request") {
let entry = this.constructor._requestsByName.get(message.name);
assert(message instanceof libData.MessageRequest);
let entry = Link._requestsByName.get(message.name);
if (!entry) {
let err = new libErrors.InvalidMessage(`Unrecognized request ${message.name}`);
this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src);
@@ -193,7 +273,8 @@ class Link {
return undefined;
} else if (message.type === "event") {
let entry = this.constructor._eventsByName.get(message.name);
assert(message instanceof libData.MessageEvent);
let entry = Link._eventsByName.get(message.name);
if (!entry) {
throw new libErrors.InvalidMessage(`Unrecognized event ${message.name}`);
}
@@ -222,26 +303,29 @@ class Link {
* Called when an request or event is received by a control connector.
* Should be overridden by sub-classes to validate messages received.
*
* @param {module:lib.Message} message - Message to check.
* @param {object} entry - Request or Event entry for this Message.
* @throws {module:lib.PermissionError} if unauthorized.
* @param message - Message to check.
* @param entry - Request or Event entry for this Message.
* @throws {libErrors.PermissionError} if unauthorized.
*/
validatePermission(message, entry) { }
validatePermission(message: libData.Message, entry: RequestEntry | EventEntry) { }
_routeMessage(message, entry) {
_routeMessage(message: libData.Message, entry?: RequestEntry | EventEntry) {
if (!this.router) {
let err = new libErrors.InvalidMessage(
`Received message addressed to ${message.dst} but this link does not route messages`
`Received message addressed to ${(message as libData.MessageRequest).dst} but this link `+
`does not route messages`
);
if (message.type === "request") {
this.connector.sendResponseError(
new libData.ResponseError(err.message, err.code), message.src
new libData.ResponseError(err.message, err.code), (message as libData.MessageRequest).src
);
}
throw err;
}
let fallback = message.type === "request" ? this._requestFallbacks.get(entry.Request) : undefined;
let fallback =
message.type === "request" ? this._requestFallbacks.get((entry as RequestEntry).Request) : undefined
;
if (this.router.forwardMessage(this, message, Boolean(fallback))) {
return;
}
@@ -251,10 +335,20 @@ class Link {
if (message.type !== "request") {
throw new Error(`Router requested fallback handling of unsupported message type ${message.type}`);
}
this._processRequest(message, entry, fallback, message.dst);
this._processRequest(
message as libData.MessageRequest,
entry as RequestEntry,
fallback,
(message as libData.MessageRequest).dst
);
}
_processRequest(message, entry, handler, spoofedSrc) {
_processRequest(
message: libData.MessageRequest,
entry: RequestEntry,
handler: RequestHandler<unknown, unknown>,
spoofedSrc?: libData.Address
) {
if (!handler) {
this.connector.sendResponseError(
new libData.ResponseError(`No handler for ${entry.Request.name}`), message.src
@@ -262,10 +356,10 @@ class Link {
return;
}
let response;
let response: any;
try {
response = handler(entry.requestFromJSON(message.data), message.src, message.dst);
} catch (err) {
} catch (err: any) {
if (err.errors) {
logger.error(JSON.stringify(err.errors, null, 4));
}
@@ -280,7 +374,7 @@ class Link {
}
response.then(
result => {
(result: any) => {
if (this.validateSent) {
if (entry.Response) {
entry.responseFromJSON(JSON.parse(JSON.stringify(result)));
@@ -291,7 +385,7 @@ class Link {
this.connector.sendResponse(result, message.src, spoofedSrc);
}
).catch(
err => {
(err: Error & { code?: string }) => {
if (err instanceof libErrors.InvalidMessage) {
logger.error(err.message);
if (err.errors) {
@@ -300,15 +394,19 @@ class Link {
} else if (!(err instanceof libErrors.RequestError)) {
logger.error(`Unexpected error responding to ${message.name}:\n${err.stack}`);
}
this.connector.sendResponseError(
new libData.ResponseError(err.message, err.code, err.stack), message.src, spoofedSrc
);
try {
this.connector.sendResponseError(
new libData.ResponseError(err.message, err.code, err.stack), message.src, spoofedSrc
);
} catch (err: any) {
logger.error(`Unexpected error sending error response for ${message.name}:\n${err.stack}`);
}
}
);
}
_processResponse(message) {
let pending = this._pendingRequests.get(message.dst.requestId);
_processResponse(message: libData.MessageResponse) {
let pending = this._pendingRequests.get(message.dst.requestId!);
if (!pending) {
throw new libErrors.InvalidMessage(
`Received response ${message.dst.requestId} without a pending request`
@@ -321,14 +419,14 @@ class Link {
try {
pending.resolve(pending.request.responseFromJSON(message.data));
} catch (err) {
} catch (err: any) {
// An invalid response object was likely received
pending.reject(err);
}
}
_processResponseError(message) {
let pending = this._pendingRequests.get(message.dst.requestId);
_processResponseError(message: libData.MessageResponseError) {
let pending = this._pendingRequests.get(message.dst.requestId!);
if (!pending) {
throw new libErrors.InvalidMessage(
`Received error response ${message.dst.requestId} without a pending request`
@@ -338,13 +436,15 @@ class Link {
pending.reject(new libErrors.RequestError(message.data.message, message.data.code, message.data.stack));
}
_processEvent(message, entry) {
_processEvent(message: libData.MessageEvent, entry: EventEntry) {
let handler = this._eventHandlers.get(entry.Event);
if (!handler) {
throw new libErrors.InvalidMessage(`Unhandled event ${message.name}`);
}
handler(entry.eventFromJSON(message.data), message.src, message.dst).catch(err => {
handler(
entry.eventFromJSON(message.data), message.src, message.dst
).catch((err: Error) => {
logger.error(`Unexpected error handling ${message.name}:\n${err.stack}`);
});
}
@@ -368,38 +468,46 @@ class Link {
/**
* Send a request or event to the other side of this link
* @param {*} requestOrEvent - Request or event to send
* @returns {Promise<*>|undefined}
* @param requestOrEvent - Request or event to send
* @returns
* Promise that resolves to the response if a request was sent or
* undefined if it was an event.
*/
send(requestOrEvent) {
return this.sendTo(this.connector.dst, requestOrEvent);
send<Req, Res = void>(requestOrEvent: Request<Req, Res>): Promise<Res>;
send<T>(requestOrEvent: Event<T>): void;
send(requestOrEvent: Request<unknown, unknown> | Event<unknown>): Promise<unknown> | void {
return this.sendTo(this.connector.dst, requestOrEvent as any);
}
/**
* Send a request or event to the given address
*
* @param {*|module:lib.Address} destination - Where to send it
* @param {*} requestOrEvent - Request or event to send
* @returns {Promise<*>|undefined}
* @param destination - Where to send it
* @param requestOrEvent - Request or event to send
* @returns
* Promise that resolves to the response if a request was sent or
* undefined if it was an event.
*/
sendTo(destination, requestOrEvent) {
sendTo<Req, Res = void>(destination: libData.AddressShorthand, requestOrEvent: Request<Req, Res>): Promise<Res>;
sendTo<T>(destination: libData.AddressShorthand, requestOrEvent: Event<T>): void;
sendTo(
destination: libData.AddressShorthand,
requestOrEvent: Request<unknown, unknown> | Event<unknown>,
): Promise<unknown> | void {
let dst = libData.Address.fromShorthand(destination);
// TODO investigate if these types can be inferred
if (requestOrEvent.constructor.type === "request") {
return this.sendRequest(requestOrEvent, dst);
return this.sendRequest(requestOrEvent as Request<unknown, unknown>, dst);
}
if (requestOrEvent.constructor.type === "event") {
return this.sendEvent(requestOrEvent, dst);
return this.sendEvent(requestOrEvent as Event<unknown>, 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<Req, Res>(request: Request<Req, Res>, dst: libData.Address) {
let entry = Link._requestsByClass.get(request.constructor as RequestClass<unknown, unknown>);
if (!entry) {
throw new Error(`Attempt to send unregistered Request ${request.constructor.name}`);
}
@@ -407,30 +515,22 @@ class Link {
entry.requestFromJSON(JSON.parse(JSON.stringify(request)));
}
let pending = {
request: entry,
dst,
};
pending.promise = new Promise((resolve, reject) => {
pending.resolve = resolve;
pending.reject = reject;
let resolve: (value: unknown) => void;
let reject: (reason: any) => void;
let promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
let requestId = this._nextRequestId;
this._nextRequestId += 1;
this._pendingRequests.set(requestId, pending);
this._pendingRequests.set(
requestId, { request: entry, promise, resolve: resolve!, reject: reject!, dst, }
);
this.connector.sendRequest(request, requestId, dst);
return pending.promise;
return promise;
}
forwardRequest(message, origin) {
if (this.validateSent) {
let entry = this.constructor._requestsByName.get(message.name);
if (!entry) {
throw new Error(`Attempt to forward unregistered Request ${message.name}`);
}
entry.requestFromJSON(message.data);
}
forwardRequest(message: libData.MessageRequest, origin: Link) {
let pending = {
origin,
src: message.src,
@@ -440,8 +540,8 @@ class Link {
this.connector.send(message);
}
sendEvent(event, dst) {
let entry = this.constructor._eventsByClass.get(event.constructor);
sendEvent<T>(event: Event<T>, dst: libData.Address) {
let entry = Link._eventsByClass.get(event.constructor as EventClass<unknown>);
if (!entry) {
throw new Error(`Attempt to send unregistered Event ${event.constructor.name}`);
}
@@ -452,18 +552,23 @@ class Link {
this.connector.sendEvent(event, dst);
}
handle(Class, handler) {
handle<Req, Res>(Class: RequestClass<Req, Res>, handler: RequestHandler<Req, Res>): void;
handle<T>(Class: EventClass<T>, handler: EventHandler<T>): void;
handle(
Class: RequestClass<unknown, unknown> | EventClass<unknown>,
handler: RequestHandler<unknown, unknown> | EventHandler<unknown>,
) {
if (Class.type === "request") {
this.handleRequest(Class, handler);
this.handleRequest(Class, handler as RequestHandler<unknown, unknown>);
} else if (Class.type === "event") {
this.handleEvent(Class, handler);
this.handleEvent(Class, handler as EventHandler<unknown>);
} 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<Req, Res>(Request: RequestClass<Req, Res>, handler: RequestHandler<Req, Res>) {
let entry = Link._requestsByClass.get(Request as RequestClass<unknown, unknown>);
if (!entry) {
throw new Error(`Unregistered Request class ${Request.name}`);
}
@@ -473,8 +578,8 @@ class Link {
this._requestHandlers.set(Request, handler);
}
fallbackRequest(Request, handler) {
let entry = this.constructor._requestsByClass.get(Request);
fallbackRequest<Req, Res>(Request: RequestClass<Req, Res>, handler: RequestHandler<Req, Res>) {
let entry = Link._requestsByClass.get(Request as RequestClass<unknown, unknown>);
if (!entry) {
throw new Error(`Unregistered Request class ${Request.name}`);
}
@@ -484,8 +589,8 @@ class Link {
this._requestFallbacks.set(Request, handler);
}
handleEvent(Event, handler) {
let entry = this.constructor._eventsByClass.get(Event);
handleEvent<T>(Event: EventClass<T>, handler: EventHandler<T>) {
let entry = Link._eventsByClass.get(Event as EventClass<unknown>);
if (!entry) {
throw new Error(`Unregistered Event class ${Event.name}`);
}
@@ -495,8 +600,8 @@ class Link {
this._eventHandlers.set(Event, handler);
}
snoopEvent(Event, handler) {
let entry = this.constructor._eventsByClass.get(Event);
snoopEvent<T>(Event: EventClass<T>, handler: EventHandler<T>) {
let entry = Link._eventsByClass.get(Event as EventClass<unknown>);
if (!entry) {
throw new Error(`Unregistered Event class ${Event.name}`);
}
@@ -506,27 +611,29 @@ class Link {
this._eventSnoopers.set(Event, handler);
}
static register(Class) {
static register<Req, Res>(Class: RequestClass<Req, Res>): void;
static register<T>(Class: EventClass<T>): void;
static register(Class: RequestClass<unknown, unknown> | EventClass<unknown>) {
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<Req, Res>(Request: RequestClass<Req, Res>, name: string): (json: any) => Request<Req, Res> {
if (Request.fromJSON) {
if (!Request.jsonSchema) {
throw new Error(`Request ${name} has static fromJSON but is missing static jsonSchema`);
}
let validate = libSchema.compile(Request.jsonSchema);
return (json) => {
let validate = libSchema.compile(Request.jsonSchema as any);
return (json: any) => {
if (!validate(json)) {
throw new libErrors.InvalidMessage(`Request ${name} failed validation`, validate.errors);
}
return Request.fromJSON(json);
return Request.fromJSON!(json);
};
}
if (Request.jsonSchema) {
@@ -535,7 +642,7 @@ class Link {
return () => new Request();
}
static responseFromJSON(Response, name) {
static responseFromJSON<T>(Response: JSONDeserialisable<T>, name: string) {
if (!Response.jsonSchema) {
throw new Error(`Response for Request ${name} is missing static jsonSchema`);
}
@@ -543,8 +650,8 @@ class Link {
throw new Error(`Response for Request ${name} is missing static fromJSON`);
}
let validate = libSchema.compile(Response.jsonSchema);
return (json) => {
let validate = libSchema.compile(Response.jsonSchema as any);
return (json: any) => {
if (!validate(json)) {
throw new libErrors.InvalidMessage(
`Response for request ${name} failed validation`, validate.errors
@@ -554,7 +661,11 @@ class Link {
};
}
static allowedTypes(types, name, side) {
static allowedTypes(
types: libData.AddressType | readonly libData.AddressType[],
name: string,
side: "src" | "dst"
) {
if (types === undefined) {
throw new Error(`Missing ${side} specification in ${name}`);
}
@@ -563,7 +674,7 @@ class Link {
types = [types];
}
let allowed = new Set();
let allowed = new Set<number>();
for (let type of types) {
let id = libData.Address[type];
if (typeof id !== "number") {
@@ -577,21 +688,22 @@ class Link {
return allowed;
}
static _requestsByName = new Map();
static _requestsByClass = new Map();
static _requestsByName = new Map<string, RequestEntry>();
static _requestsByClass = new Map<RequestClass<unknown, unknown>, RequestEntry>();
static registerRequest(Request) {
static registerRequest(Request: RequestClass<unknown, unknown>) {
const name = Request.plugin ? `${Request.plugin}:${Request.name}` : Request.name;
if (this._requestsByName.has(name)) {
throw new Error(`Request ${name} is already registered`);
}
let entry = {
let entry: RequestEntry = {
Request,
name,
requestFromJSON: this.requestFromJSON(Request, name),
allowedSrcTypes: this.allowedTypes(Request.src, name, "src"),
allowedDstTypes: this.allowedTypes(Request.dst, name, "dst"),
responseFromJSON: (_json: any) => undefined,
};
if (
@@ -605,44 +717,42 @@ class Link {
if (Response) {
entry.Response = Response;
entry.responseFromJSON = this.responseFromJSON(Response, name);
} else {
entry.responseFromJSON = () => undefined;
}
this._requestsByName.set(name, entry);
this._requestsByClass.set(Request, entry);
}
static eventFromJSON(Event, name) {
static eventFromJSON<T>(Event: EventClass<T>, name: string) {
if (Event.fromJSON) {
if (!Event.jsonSchema) {
throw new Error(`Event ${name} has static fromJSON but is missing static jsonSchema`);
}
let validate = libSchema.compile(Event.jsonSchema);
return (json) => {
let validate = libSchema.compile(Event.jsonSchema as any);
return (json: any) => {
if (!validate(json)) {
throw new libErrors.InvalidMessage(`Event ${name} failed validation`, validate.errors);
}
return Event.fromJSON(json);
return Event.fromJSON!(json);
};
}
if (Event.jsonSchema) {
throw new Error(`Event ${name} has static jsonSchema but is missing static fromJSON`);
}
return () => new Event();
return (json: any) => new Event();
}
static _eventsByName = new Map();
static _eventsByClass = new Map();
static _eventsByName = new Map<string, EventEntry>();
static _eventsByClass = new Map<EventClass<unknown>, EventEntry>();
static registerEvent(Event) {
static registerEvent<T>(Event: EventClass<T>) {
const name = Event.plugin ? `${Event.plugin}:${Event.name}` : Event.name;
if (this._eventsByName.has(name)) {
throw new Error(`Event ${name} is already registered`);
}
let entry = {
Event,
let entry: EventEntry = {
Event: Event as EventClass<unknown>,
name,
eventFromJSON: this.eventFromJSON(Event, name),
allowedSrcTypes: this.allowedTypes(Event.src, name, "src"),
@@ -657,15 +767,15 @@ class Link {
}
this._eventsByName.set(name, entry);
this._eventsByClass.set(Event, entry);
this._eventsByClass.set(Event as EventClass<unknown>, entry);
}
}
for (let Class of dataClasses) {
Link.register(Class);
Link.register(Class as RequestClass<unknown, unknown>);
}
function registerPluginMessages(pluginInfos) {
export function registerPluginMessages(pluginInfos: PluginInfo[]) {
for (let pluginInfo of pluginInfos) {
for (let Class of pluginInfo.messages || []) {
if (Class.plugin !== pluginInfo.name) {
@@ -678,9 +788,3 @@ function registerPluginMessages(pluginInfos) {
}
}
}
module.exports = {
Link,
registerPluginMessages,
};
@@ -1,15 +1,19 @@
"use strict";
const core = require("../data/messages_core");
const controller = require("../data/messages_controller");
const host = require("../data/messages_host");
const instance = require("../data/messages_instance");
const mod = require("../data/messages_mod");
const user = require("../data/messages_user");
import * as core from "../data/messages_core";
import * as controller from "../data/messages_controller";
import * as host from "../data/messages_host";
import * as instance from "../data/messages_instance";
import * as mod from "../data/messages_mod";
import * as user from "../data/messages_user";
import type { RequestClass, EventClass } from "./link";
// Event and Request payloads registered with the Link class. Used to decode
// the data payload of incomming requsts and event messages into instances of
// their proper classes.
const dataClasses = [
/**
* Event and Request payloads registered with the Link class. Used to decode
* the data payload of incomming requsts and event messages into instances of
* their proper classes.
* @internal
*/
export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)[] = [
core.PingRequest,
controller.ControllerConfigGetRequest,
@@ -107,5 +111,3 @@ const dataClasses = [
user.AccountUpdateEvent,
user.UserUpdateEvent,
];
module.exports = { dataClasses };
@@ -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<string>;
warnLevels: Set<string>;
filter: (info: object) => boolean;
constructor(
options: Transport.TransportStreamOptions & {
errorLevels?: string[],
warnLevels?: string[],
filter?: ConsoleTransport["filter"]
} = {}
) {
super(options);
this.errorLevels = new Set(options.errorLevels || ["fatal", "error"]);
this.warnLevels = new Set(options.warnLevels || ["warn"]);
this.filter = options.filter || (() => true);
}
log(info, callback) {
log(info: any, callback: Function) {
if (!this.filter(info)) {
return callback();
}
@@ -56,12 +66,12 @@ class ConsoleTransport extends Transport {
/**
* Formats winston log messages for the web console
*/
class WebConsoleFormat {
constructor(options = {}) {
this.options = options;
}
export class WebConsoleFormat {
constructor(
public options = {}
) { }
transform(info, options) {
transform(info: any, _options: {}) {
let src = " ";
if (info.host_id !== undefined) {
src += `s:${info.host_name} - `;
@@ -84,33 +94,43 @@ class WebConsoleFormat {
/**
* Filter object for logs
* @typedef {Object} module:lib~LogFilter
* @property {string} [maxLevel] -
* Maximum log level to include. Higher levels are more verbose.
* @property {boolean} [all] -
* Include log entries from controller, all hosts and all instances.
* @property {boolean} [controller] -
* Include log entries from the controller.
* @property {Array<number>} [hostIds] -
* Include log entries for the given hosts and instances of those
* hosts by id.
* @property {Array<number>} [instanceIds] -
* Include log entries for the given instances by id.
*/
export interface LogFilter {
/**
* Maximum log level to include. Higher levels are more verbose.
*/
maxLevel?: keyof typeof levels;
/**
* Include log entries from controller, all hosts and all instances.
*/
all?: boolean;
/**
* Include log entries from the controller.
*/
controller?: boolean;
/**
* Include log entries for the given hosts and instances of those
* hosts by id.
*/
hostIds?: number[];
/**
* Include log entries for the given instances by id.
*/
instanceIds?: number[];
}
/**
* Create log filter by level and source.
*
* @param {module:lib~LogFilter} filter -
* @param filter -
* Filter to filter log entries by.
* @returns {function(object): boolean}
* @returns
* filter returning true for log entries that match it.
* @static
*/
function logFilter({ all, controller, hostIds, instanceIds, maxLevel }) {
return info => {
export function logFilter({ all, controller, hostIds, instanceIds, maxLevel }: LogFilter) {
return (info: any) => {
// Note: reversed to filter out undefined levels
if (maxLevel && !(levels[info.level] <= levels[maxLevel])) {
if (maxLevel && !((levels as any)[info.level] <= levels[maxLevel])) {
return false;
}
@@ -135,17 +155,23 @@ function logFilter({ all, controller, hostIds, instanceIds, maxLevel }) {
};
}
const logger = winston.createLogger({
export const logger = winston.createLogger({
level: "verbose",
levels,
format: winston.format.timestamp(),
});
}) as unknown as Logger;
module.exports = {
ConsoleTransport,
WebConsoleFormat,
colors,
levels,
logFilter,
logger,
// Who in their right mind thought that log levels should be hard coded
// into the type when you can define custom levels you want to support?
export type Logger = Omit<winston.Logger,
"error" | "warn" | "help" | "data" | "info" | "debug" | "prompt" | "http" | "verbose" | "input" | "silly" |
"emerg" | "alert" | "crit" | "warning" | "notice"
> & {
fatal: winston.LeveledLogMethod,
error: winston.LeveledLogMethod,
warn: winston.LeveledLogMethod,
audit: winston.LeveledLogMethod,
info: winston.LeveledLogMethod,
server: winston.LeveledLogMethod,
verbose: winston.LeveledLogMethod,
};
@@ -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<string, {
levels: Set<keyof typeof levels>,
controller: boolean,
hostIds: Set<number>,
instanceIds: Set<number>,
}>;
constructor(
public logDirectory: string,
serialized: any,
) {
this.index = new Map();
if (serialized.version !== logIndexVersion) {
return;
}
for (let [file, serializedEntry] of Object.entries(serialized.files)) {
for (let [file, serializedEntry] of Object.entries(serialized.files) as [string, any][]) {
this.index.set(file, {
levels: new Set(serializedEntry.levels),
controller: serializedEntry.controller,
@@ -168,7 +256,12 @@ class LogIndex {
serialize() {
let serialized = {
version: logIndexVersion,
files: {},
files: {} as Record<string, {
levels: string[],
controller: boolean,
hostIds: number[],
instanceIds: number[],
}>,
};
for (let [file, entry] of this.index) {
@@ -190,14 +283,14 @@ class LogIndex {
* data about previously indexed files saved with {@link
* module:lib.LogIndex#save}.
*
* @param {string} logDirectory - Path to directory to load from.
* @returns {Promise<module:lib.LogIndex>} loaded or created log index.
* @param logDirectory - Path to directory to load from.
* @returns loaded or created log index.
*/
static async load(logDirectory) {
static async load(logDirectory: string) {
try {
let content = await fs.readFile(path.join(logDirectory, "index.json"));
return new LogIndex(logDirectory, JSON.parse(content));
} catch (err) {
return new LogIndex(logDirectory, JSON.parse(content.toString()));
} catch (err: any) {
if (err.code !== "ENOENT") {
logger.warn(`Failed to load ${path.join(logDirectory, "index.json")}: ${err}`);
}
@@ -218,19 +311,19 @@ class LogIndex {
);
}
async indexFile(file) {
async indexFile(file: string) {
let filePath = path.join(this.logDirectory, file);
let lineStream = new libStream.LineSplitter({ readableObjectMode: true });
let fileStream = fs.createReadStream(filePath);
fileStream.pipe(lineStream);
let entry = {
levels: new Set(),
levels: new Set() as Set<keyof typeof levels>,
controller: false,
hostIds: new Set(),
instanceIds: new Set(),
hostIds: new Set() as Set<number>,
instanceIds: new Set() as Set<number>,
};
for await (let line of lineStream) {
let info;
let info: any;
try {
info = JSON.parse(line);
} catch (err) {
@@ -273,15 +366,15 @@ class LogIndex {
/**
* Check if a file includes entries for the given filter
*
* @param {string} file -
* @param file -
* Name of file in the log directory to check for.
* @param {module:lib~LogFilter} filter -
* @param filter -
* Filter to check index if file contains entries for.
* @returns {boolean}
* @returns
* true if the file may contain entries included by the filter.
*/
filterIncludesFile(file, { maxLevel, all, controller, instanceIds, hostIds }) {
let entry = this.index.get(file);
filterIncludesFile(file: string, { maxLevel, all, controller, instanceIds, hostIds }: LogFilter) {
const entry = this.index.get(file);
if (!entry) {
return true;
}
@@ -314,35 +407,51 @@ class LogIndex {
/**
* Filter object for querying logs
* @typedef {Object} module:lib~QueryLogFilter
* @property {number} [limit=100] - Maximum number of entries to return.
* @property {string} [order="asc"] -
* Return entries in ascending ("asc") date order or desceding ("desc")
* date order.
* @property {string} [maxLevel] -
* Maximum log level to include. Higher levels are more verbose.
* @property {boolean} [all] -
* Include log entries from controller, all hosts and all instances.
* @property {boolean} [controller] -
* Include log entries from the controller.
* @property {Array<number>} [hostIds] -
* Include log entries for the given hosts and instances of those
* hosts by id.
* @property {Array<number>} [instanceIds] -
* Include log entries for the given instances by id.
*/
export interface QueryLogFilter {
/**
* Maximum number of entries to return. Defaults to 100.
*/
limit?: number;
/**
* Return entries in ascending ("asc") date order or desceding ("desc")
* date order. Defaults to "asc".
*/
order?: "asc" | "desc";
/**
* Maximum log level to include. Higher levels are more verbose.
*/
maxLevel: keyof typeof levels;
/**
* Include log entries from controller, all hosts and all instances.
*/
all: boolean;
/**
* Include log entries from the controller.
*/
controller: boolean;
/**
* Include log entries for the given hosts and instances of those
* hosts by id.
*/
hostIds: number[];
/**
* Include log entries for the given instances by id.
*/
instanceIds: number[];
}
/**
* Query log directory
*
* @param {string} logDirectory - path to directory with logs.
* @param {module:lib~QueryLogFilter} filter -
* @param logDirectory - path to directory with logs.
* @param filter -
* Filter to limit logs by.
* @param {module:lib.LogIndex=} index -
* @param index -
* Index to speed up query with.
* @returns {Promise<Array<Object>>} log entries matching the filter
* @returns log entries matching the filter
*/
async function queryLog(logDirectory, filter, index) {
export async function queryLog(logDirectory: string, filter: QueryLogFilter, index: LogIndex) {
let files = (await fs.readdir(logDirectory)).filter(entry => logFileGlob.test(entry));
filter = {
@@ -357,14 +466,14 @@ async function queryLog(logDirectory, filter, index) {
}
const includeEntry = logFilter(filter);
let log = [];
let log: object[] = [];
for (let file of files) {
if (index && !index.filterIncludesFile(file, filter)) {
continue;
}
let filePath = path.join(logDirectory, file);
let fileStream;
let lineStream;
let fileStream: fs.ReadStream;
let lineStream: libStream.LineSplitter;
if (filter.order === "asc") {
lineStream = new libStream.LineSplitter({ readableObjectMode: true });
fileStream = fs.createReadStream(filePath);
@@ -373,7 +482,7 @@ async function queryLog(logDirectory, filter, index) {
fileStream = await libStream.createReverseReadStream(filePath);
}
lineStream.on("data", line => {
let info;
let info: any;
try {
info = JSON.parse(line);
} catch (err) {
@@ -381,7 +490,7 @@ async function queryLog(logDirectory, filter, index) {
}
if (includeEntry(info)) {
log.push(info);
if (log.length >= filter.limit) {
if (log.length >= Number(filter.limit)) {
lineStream.destroy();
fileStream.destroy();
}
@@ -390,40 +499,31 @@ async function queryLog(logDirectory, filter, index) {
fileStream.pipe(lineStream);
try {
await finished(lineStream);
} catch (err) {
if (log.length < filter.limit || err.code !== "ERR_STREAM_PREMATURE_CLOSE") {
} catch (err: any) {
if (log.length < Number(filter.limit) || err.code !== "ERR_STREAM_PREMATURE_CLOSE") {
throw err;
}
}
if (log.length >= filter.limit) {
if (log.length >= Number(filter.limit)) {
break;
}
}
return log;
}
function handleUnhandledErrors() {
export function handleUnhandledErrors() {
/* eslint-disable node/no-process-exit */
process.on("uncaughtException", err => {
logger.fatal(`Uncaught exception:\n${err.stack}`);
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
process.on("unhandledRejection", (reason: Error, _promise) => {
logger.fatal(`Unhandled rejection:\n${reason.stack ? reason.stack : reason}`);
process.exit(1);
});
/* eslint-enable node/no-process-exit */
}
module.exports = {
TerminalFormat,
LinkTransport,
LogIndex,
handleUnhandledErrors,
queryLog,
// for testing only
_formatServerOutput: formatServerOutput,
};
// for testing only
export const _formatServerOutput = formatServerOutput;
@@ -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,
};
@@ -2,18 +2,61 @@
* Plugin interfaces and utilities.
* @module lib/plugin
*/
"use strict";
const helpers = require("./helpers");
import * as libHelpers from "./helpers";
import type { Logger } from "./logging";
import type { ConfigGroup } from "./config";
import type { CollectorResult } from "./prometheus";
import type { ParsedFactorioOutput } from "./logging_utils";
import type { ModPack, ModInfo } from "./data";
import type { Link } from "./link";
import type { CommandTree } from "./command";
/**
* Conceptual base for controller and instance plugins.
* @typedef {
* module:lib.BaseControllerPlugin
* |module:lib.BaseInstancePlugin
* |module:lib.BaseControlPlugin
* } module:lib.BasePlugin
*/
export type BasePlugin =
| BaseControllerPlugin
| BaseInstancePlugin
| BaseControlPlugin
| BaseWebPlugin
;
// TODO Add proper typing for plugins
export type PluginInfo = any;
type Controller = any;
type Instance = any;
type Host = any;
type InstanceInfo = any;
type HostConnection = any;
type ControlConnection = any;
type Event = any;
type React = any;
/**
* Information about the event.
*/
interface PlayerEvent {
type: "join" | "leave" | "import";
/** Name of the player that joined/left */
name: string,
/**
* Only present for type "leave". Reason for player leaving the
* game, one of the possible reasons in defines.disconnect_reason
* or "server_quit" if the server exits while the player is online.
*/
reason?: string;
}
type InstanceStatus =
| "unassigned"
| "unknown"
| "stopped"
| "starting"
| "running"
| "stopping"
| "creating_save"
| "exporting_data"
| "deleted"
;
/**
* Base class for instance plugins
@@ -27,40 +70,43 @@ const helpers = require("./helpers");
* Instances may be started and stopped many times, and many instances may
* be running at the same time, each of which will have their own instance
* of the InstancePlugin class.
*
* @static
*/
class BaseInstancePlugin {
constructor(info, instance, host) {
export class BaseInstancePlugin {
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
logger: Logger;
private _pendingRconMessages: {
resolve: (result: string) => void,
reject: (err: Error) => void,
message: string,
expectEmpty: boolean,
}[] = [];
private _sendingRconMessages = false;
constructor(
/**
* The plugin's own info module
*/
this.info = info;
public info: PluginInfo,
/**
* Instance the plugin started for
* @type {module:host/src/Instance}
*/
this.instance = instance;
public instance: Instance,
/**
* Host running the instance
*
* With the exepction of accessing the host's config you should
* avoid ineracting with the host object directly.
*
* @type {module:host/src/Host}
*/
this.host = host;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
this.logger = instance.logger.child({ plugin: this.info.name });
public host: Host,
) {
this.logger = instance.logger.child({ plugin: this.info.name }) as unknown as Logger;
this._pendingRconMessages = [];
this._sendingRconMessages = false;
@@ -77,12 +123,12 @@ class BaseInstancePlugin {
* Invoked after the value of the config field given by `field` has
* changed.
*
* @param {module:lib.ConfigGroup} group -
* @param group -
* The group who's field got changed.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
* @param field - Name of the field that changed.
* @param prev - The previous value of the field.
*/
async onInstanceConfigFieldChanged(group, field, prev) { }
async onInstanceConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
/**
* Called before collecting Prometheus metrics
@@ -97,9 +143,9 @@ class BaseInstancePlugin {
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
* collecting the values.
*
* @returns {*} an async iterator of prometheus metric results or undefined.
* @returns an async iterator of prometheus metric results or undefined.
*/
async onMetrics() { }
async onMetrics(): Promise<void | AsyncIterator<CollectorResult>> { }
/**
* 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<string>} response from server.
* @returns response from server.
*/
async sendRcon(message, expectEmpty=false) {
async sendRcon(message: string, expectEmpty=false): Promise<string> {
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<string>} 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<string>((resolve, reject) => {
this._pendingRconMessages.push({resolve, reject, message, expectEmpty});
});
if (!this._sendingRconMessages) {
@@ -271,11 +297,11 @@ class BaseInstancePlugin {
async _sendPendingRconMessages() {
this._sendingRconMessages = true;
while (this._pendingRconMessages.length) {
let task = this._pendingRconMessages.shift();
let task = this._pendingRconMessages.shift()!;
try {
let result = await this.sendRcon(task.message, task.expectEmpty);
task.resolve(result);
} catch (err) {
} catch (err: any) {
task.reject(err);
}
}
@@ -291,28 +317,24 @@ class BaseInstancePlugin {
* To be discovered the class must be exported under the name `ControllerPlugin`
* in the module specified by the `controllerEntrypoint` in the plugin's info.js
* file.
*
* @static
*/
class BaseControllerPlugin {
constructor(info, controller, metrics, logger) {
this.info = info;
export class BaseControllerPlugin {
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
logger: Logger;
/**
* Controller
* @type {module:controller/src/Controller}
*/
this.controller = controller;
this.metrics = metrics;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
this.logger = logger.child({ plugin: this.info.name });
constructor(
public info: PluginInfo,
public controller: Controller,
public metrics: any[],
logger: Logger
) {
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
}
/**
@@ -354,11 +376,11 @@ class BaseControllerPlugin {
* the case of network outages if reconnect fails to re-establish the
* session between the controller and the host.
*
* @param {module:controller/src/InstanceInfo} instance -
* @param instance -
* The instance that changed.
* @param {?string} prev - the previous status of the instance.
* @param prev - the previous status of the instance.
*/
async onInstanceStatusChanged(instance, prev) { }
async onInstanceStatusChanged(instance: InstanceInfo, prev?: InstanceStatus) { }
/**
* Called when the value of a controller config field changed.
@@ -366,12 +388,12 @@ class BaseControllerPlugin {
* Invoked after the value of the config field given by `field` has
* changed on the controller.
*
* @param {module:lib.ConfigGroup} group -
* @param group -
* The group who's field got changed.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
* @param field - Name of the field that changed.
* @param prev - The previous value of the field.
*/
async onControllerConfigFieldChanged(group, field, prev) { }
async onControllerConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
/**
* Called when the value of an instance config field changed.
@@ -379,14 +401,14 @@ class BaseControllerPlugin {
* Invoked after the value of the config field given by `field` has
* changed on an instance.
*
* @param {module:controller/src/InstanceInfo} instance -
* @param instance -
* The instance the config changed on.
* @param {module:lib.ConfigGroup} group -
* @param group -
* The group who's field got changed.
* @param {string} field - Name of the field that changed.
* @param {*} prev - The previous value of the field.
* @param field - Name of the field that changed.
* @param prev - The previous value of the field.
*/
async onInstanceConfigFieldChanged(instance, group, field, prev) { }
async onInstanceConfigFieldChanged(instance: InstanceInfo, group: ConfigGroup, field: string, prev: unknown) { }
/**
* Called before collecting Prometheus metrics
@@ -401,9 +423,9 @@ class BaseControllerPlugin {
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
* collecting the values.
*
* @returns {*} an async iterator of prometheus metric results or undefined.
* @returns an async iterator of prometheus metric results or undefined.
*/
async onMetrics() { }
async onMetrics(): Promise<void | AsyncIterator<CollectorResult>> { }
/**
* Called when the controller is shutting down
@@ -439,11 +461,11 @@ class BaseControllerPlugin {
*
* Invoked when the connection to the host has been closed.
*
* @param {module:controller/src/HostConnection} connection -
* @param connection -
* The connection the event occured on.
* @param {string} event - one of connect, drop, resume and close
* @param event - one of connect, drop, resume and close
*/
onHostConnectionEvent(connection, event) { }
onHostConnectionEvent(connection: HostConnection, event: "connect" | "drop" | "resume" | "close") { }
/**
* Called when an avent on a control connection happens
@@ -475,11 +497,11 @@ class BaseControllerPlugin {
*
* Invoked when the connection to the control has been closed.
*
* @param {module:controller/src/ControlConnection} connection -
* @param connection -
* The connection the event occured on.
* @param {string} event - one of connect, drop, resume, and close.
* @param event - one of connect, drop, resume, and close.
*/
onControlConnectionEvent(connection, event) { }
onControlConnectionEvent(connection: ControlConnection, event: "connect" | "drop" | "resume" | "close") { }
/**
* Called when a host is preparing to disconnect from the controller
@@ -490,10 +512,10 @@ class BaseControllerPlugin {
* Plugins must stop sending messages to the host in question after the
* prepare disconnect has been handled.
*
* @param {module:controller/src/HostConnection} connection -
* @param connection -
* The connection to the host preparing to disconnect.
*/
async onPrepareHostDisconnect(connection) { }
async onPrepareHostDisconnect(connection: HostConnection) { }
/**
* Called when a mod pack is updated
@@ -504,9 +526,9 @@ class BaseControllerPlugin {
* If the mod pack has been deleted its `.isDeleted` property will be
* true.
*
* @param {module:lib.ModPack} modPack - Mod pack that updated.
* @param modPack - Mod pack that updated.
*/
async onModPackUpdated(modPack) { }
async onModPackUpdated(modPack: ModPack) { }
/**
* Called when a mod stored on the controller is updated
@@ -516,9 +538,9 @@ class BaseControllerPlugin {
*
* If a mod has been deleted its `.isDeleted` property will be true.
*
* @param {module:lib.ModInfo} mod - Mod that updated.
* @param mod - Mod that updated.
*/
async onModUpdated(mod) { }
async onModUpdated(mod: ModInfo) { }
/**
* Called when a player joins or leaves an instance
@@ -526,17 +548,11 @@ class BaseControllerPlugin {
* Invoked when a player either joins or leaves an instance in the
* cluster.
*
* @param {module:controller/src/InstanceInfo} instance -
* @param instance -
* The instance it occured on.
* @param {Object} event - Information about the event.
* @param {string} event.type - Either "join", "leave" or "import".
* @param {string} event.name - Name of the player that joined/left.
* @param {string=} event.reason -
* Only present for type "leave". Reason for player leaving the
* game, one of the possible reasons in defines.disconnect_reason
* or "server_quit" if the server exits while the player is online.
* @param event - Information about the event.
*/
async onPlayerEvent(instance, event) { }
async onPlayerEvent(instance: InstanceInfo, event: PlayerEvent) { }
/**
* Broadcast event to all connected hosts
@@ -545,20 +561,15 @@ class BaseControllerPlugin {
* This does not include hosts that are in the process of closing the
* connection, which typically happens when they are shutting down.
*
* @param {*} event - Event to send
* @param {Object} data - Data ta pass with the event.
* @param event - Event to send
*/
broadcastEventToHosts(event, data={}) {
if (event.type !== "event") {
this.logger.warn(`Ignoring broadcast of ${event.type}`);
return;
}
broadcastEventToHosts(event: Event) {
for (let hostConnection of this.controller.wsServer.hostConnections.values()) {
if (
!hostConnection.connector.closing
&& (!event.plugin || hostConnection.plugins.has(event.plugin))
&& (!event.constructor.plugin || hostConnection.plugins.has(event.constructor.plugin))
) {
event.send(hostConnection, data);
hostConnection.send(event);
}
}
}
@@ -571,24 +582,25 @@ class BaseControllerPlugin {
* clusterioctl in order to extend its functionallity. To be discovered the
* class must be exported under the name `ControlPlugin` in the module
* specified by the `controlEntrypoint` in the plugin's info.js file.
*
* @static
*/
class BaseControlPlugin {
constructor(info, logger) {
export class BaseControlPlugin {
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
logger: Logger;
constructor(
/**
* The plugin's own info module
*/
this.info = info;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
this.logger = logger.child({ plugin: this.info.name });
public info: PluginInfo,
logger: Logger,
) {
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
}
/**
@@ -601,136 +613,153 @@ class BaseControlPlugin {
*
* Invoked by clusterioctl to let plugins add commands. `rootCommand` is
* the top level command node which the plugin should add its own {@link
* module:lib.CommandTree} to.
* CommandTree} to.
*
* @param {module:lib.CommandTree} rootCommand -
* @param rootCommand -
* Root of the clusterioctl command tree.
*/
async addCommands(rootCommand) { }
async addCommands(rootCommand: CommandTree) { }
}
/**
* Plugin supplied login form
* @typedef {Object} module:lib~LoginForm
* @property {string} name -
* Internal name of the login form, this should start with the
* plugin name followed by a dot.
* @property {string} title -
* Name displayed above this form in the login window.
* @property {React.Component} Component -
* React component that's rendered for this login form. This is
* supplied the setToken function via its props which should be
* called when an authentication token is aquired via this
* form.
*/
export interface PluginLoginForm {
/**
* Internal name of the login form, this should start with the
* plugin name followed by a dot.
*/
name: string;
/** Name displayed above this form in the login window. */
title: string;
/**
* React component that's rendered for this login form. This is
* supplied the setToken function via its props which should be called
* when an authentication token is aquired via this form.
*/
Component: React["Component"];
};
/**
* Plugin supplied pages
* @typedef {Object} module:lib~Page
* @property {string} path - URL path to this page.
* @property {string=} sidebarPath -
* If present and this path matches one of the pages in the sidebar it
* will cause that sidebar entry to be highlighted as active.
* @property {string=} sidebarGroup -
* If present group this entry under a group of the given name in the
* sidebar.
* @property {string=} sidebarName -
* If present creates an entry in the sidebar for this page with the
* given text.
* @property {ReactNode} content -
* A react node which is rendered when this page is navigated to.
* Should render a PageLayout.
*/
export interface PluginPage {
/** URL path to this page. */
path: string;
/**
* If present and this path matches one of the pages in the sidebar it
* will cause that sidebar entry to be highlighted as active.
*/
sidebarPath?: string;
/**
* If present group this entry under a group of the given name in the
* sidebar.
*/
sidebarGroup?: string;
/**
* If present creates an entry in the sidebar for this page with the
* given text.
*/
sidebarName?: string;
/**
* A react node which is rendered when this page is navigated to.
* Should render a PageLayout.
*/
content?: React["ReactNode"];
};
/**
* Base class for web interface plugins
*
* @static
*/
class BaseWebPlugin {
constructor(container, packageData, info, logger) {
export class BaseWebPlugin {
/**
* Contents of the plugin's package.json file
*/
package: any;
/**
* Control link to the controller, not available until the
* connect event in onControllerConnectionEvent is signaled.
*/
control?: Link;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
logger: Logger;
/**
* List of login forms provided by this plugin
*/
loginForms: PluginLoginForm[] = [];
/**
* List of pages provided by this plugin
*/
pages: PluginPage[] = [];
/**
* Extra react component to add to core components
*
* Interface to augment core components of the web UI. Setting a
* component as one of the supported properties of this object will
* cause the web UI to render it when displaying that component,
* usually at the end. Each component will receive a `plugin` param
* which is the instance of the web plugin that contained the
* component extra.
*/
componentExtra: {
/** Placed at the end of the controller page. */
ControllerPage?: React["ComponentType"],
/** Placed at the end of the hosts list page. */
HostsPage?: React["ComponentType"],
/**
* Placed at the end of each host page. Takes a `host` param which
* is the host the page is displayed for.
*/
HostViewPage?: React["ComponentType"],
/** Placed at the end of the instance list page. */
InstancesPage?: React["ComponentType"],
/**
* Placed at the end of each instance page. Takes an `instance`
* param which is the instance the page is displayed for.
*/
InstanceViewPage?: React["ComponentType"],
/** Placed at the end of the users list page. */
UsersPage?: React["ComponentType"],
/**
* Placed at the end of each user page. Takes a `user` param which
* is the user object the page is displayed for.
*/
UserViewPage?: React["ComponentType"],
/** Placed at the end of the roles list page. */
RolesPage?: React["ComponentType"],
/**
* Placed at the end of each role page. Takes a `role` param which
* is the role object the page is displayed for.
*/
RoleViewPage?: React["ComponentType"],
} = {};
constructor(
/**
* Webpack container for this plugin
*/
this.container = container;
/**
* Contents of the plugin's package.json file
* @type {Object}
*/
this.package = packageData;
public container: any,
packageData: any,
/**
* The plugin's own info module
*/
this.info = info;
/**
* Control link to the controller, not available until the
* connect event in onControllerConnectionEvent is signaled.
* @type {?module:lib.Link}
*/
this.control = null;
/**
* Logger for this plugin
*
* Instance of winston Logger for sending log messages from this
* plugin. Supported methods and their corresponding log levels are
* `error`, `warn`, `audit`, `info` and `verbose`.
*/
this.logger = logger.child({ plugin: this.info.name });
/**
* List of login forms provided by this plugin
*
* @type {Array<module:lib~LoginForm>}
*/
this.loginForms = [];
/**
* List of pages provided by this plugin
*
* @type {Array<module:lib~Page>}
*/
this.pages = [];
/**
* Extra react component to add to core components
*
* Interface to augment core components of the web UI. Setting a
* component as one of the supported properties of this object will
* cause the web UI to render it when displaying that component,
* usually at the end. Each component will receive a `plugin` param
* which is the instance of the web plugin that contained the
* component extra.
*
* @type {object}
* @property {React.ComponentType} ControllerPage -
* Placed at the end of the controller page.
* @property {React.ComponentType} HostsPage -
* Placed at the end of the hosts list page.
* @property {React.ComponentType} HostViewPage -
* Placed at the end of each host page. Takes a `host` param
* which is the host the page is displayed for.
* @property {React.ComponentType} InstancesPage -
* Placed at the end of the instance list page.
* @property {React.ComponentType} InstanceViewPage -
* Placed at the end of each instance page. Takes an `instance`
* param which is the instance the page is displayed for.
* @property {React.ComponentType} UsersPage -
* Placed at the end of the users list page.
* @property {React.ComponentType} UserViewPage -
* Placed at the end of each user page. Takes a `user` param
* which is the user object the page is displayed for.
* @property {React.ComponentType} RolesPage -
* Placed at the end of the roles list page.
* @property {React.ComponentType} RoleViewPage -
* Placed at the end of each role page. Takes a `role` param
* which is the role object the page is displayed for.
*/
this.componentExtra = {};
public info: PluginInfo,
logger: Logger,
) {
this.package = packageData;
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
}
/**
@@ -770,28 +799,28 @@ class BaseWebPlugin {
* send any messages that goes to or via the controller after the
* connection has been closed and before a new one is established.
*
* @param {string} event - one of connect, drop, resume and close
* @param event - one of connect, drop, resume and close
*/
onControllerConnectionEvent(event) { }
onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { }
}
/**
* Invokes the given hook on all plugins
*
* @param {Map<string, Object>} 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<Array>} 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<string, BasePlugin>, hook: string, ...args: any[]) {
let results: unknown[] = [];
for (let [name, plugin] of plugins) {
try {
// Use an object to detect if the hook failed on timeout or tried to return a value looking like an error
const timeout = {};
let result = await helpers.timeout(
plugin[hook](...args),
let result = await libHelpers.timeout(
(plugin as any)[hook](...args),
15000,
timeout
);
@@ -800,19 +829,9 @@ async function invokeHook(plugins, hook, ...args) {
} else if (result !== undefined) {
results.push(result);
}
} catch (err) {
} catch (err: any) {
plugin.logger.error(`Ignoring error from plugin ${name} in ${hook}:\n${err.stack}`);
}
}
return results;
}
module.exports = {
BaseInstancePlugin,
BaseControllerPlugin,
BaseControlPlugin,
BaseWebPlugin,
invokeHook,
};
@@ -3,12 +3,10 @@
* Helpers for loading plugins in Node.js
* @module lib/plugin_loader
*/
"use strict";
const libErrors = require("./errors");
const libPlugin = require("./plugin");
const { logger } = require("./logging");
const path = require("path");
import path from "path";
import * as libErrors from "./errors";
import * as libPlugin from "./plugin";
import { logger } from "./logging";
/**
@@ -18,22 +16,21 @@ const path = require("path");
* loaded the info modules will not be reloaded should this function be
* called again.
*
* @param {Map<string, string>} pluginList -
* @param pluginList -
* Mapping of plugin name to require path for the plugins to load.
* @returns {Promise<Array<Object>>} Array of plugin info modules.
* @static
* @returns Array of plugin info modules.
*/
async function loadPluginInfos(pluginList) {
let plugins = [];
export async function loadPluginInfos(pluginList: Map<string, string>) {
let plugins: libPlugin.PluginInfo[] = [];
for (let [pluginName, pluginPath] of pluginList) {
let pluginInfo;
let pluginPackage;
let pluginInfo: libPlugin.PluginInfo;
let pluginPackage: { version: string };
try {
pluginInfo = require(path.posix.join(pluginPath, "info"));
pluginPackage = require(path.posix.join(pluginPath, "package.json"));
} catch (err) {
} catch (err: any) {
throw new libErrors.PluginError(pluginName, err);
}
@@ -56,7 +53,12 @@ async function loadPluginInfos(pluginList) {
return plugins;
}
function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) {
function loadPluginClass(
entrypointName: string,
className: string,
pluginClass: any,
pluginInfo: libPlugin.PluginInfo
) {
let resolvedPath = path.posix.join(pluginInfo.requirePath, pluginInfo[entrypointName]);
let entrypoint = require(resolvedPath);
if (!entrypoint[className]) {
@@ -75,45 +77,41 @@ function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) {
/**
* Load controller plugin class of a plugin
*
* @param {Object} pluginInfo -
* @param pluginInfo -
* Plugin info object returned from {@link
* module:lib.loadPluginInfos} to load class from.
* @returns {Promise<function>} plugin class
* @static
* loadPluginInfos} to load class from.
* @returns plugin class
*/
async function loadControllerPluginClass(pluginInfo) {
export async function loadControllerPluginClass(
pluginInfo: libPlugin.PluginInfo
): Promise<libPlugin.BaseControllerPlugin> {
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<function>} plugin class
* @static
* loadPluginInfos} to load class from.
* @returns plugin class
*/
async function loadInstancePluginClass(pluginInfo) {
export async function loadInstancePluginClass(
pluginInfo: libPlugin.PluginInfo
): Promise<libPlugin.BaseInstancePlugin> {
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<function>} plugin class
* @static
* loadPluginInfos} to load class from.
* @returns plugin class
*/
async function loadControlPluginClass(pluginInfo) {
export async function loadControlPluginClass(
pluginInfo: libPlugin.PluginInfo
): Promise<libPlugin.BaseControlPlugin> {
return loadPluginClass("controlEntrypoint", "ControlPlugin", libPlugin.BaseControlPlugin, pluginInfo);
}
module.exports = {
loadPluginInfos,
loadControllerPluginClass,
loadInstancePluginClass,
loadControlPluginClass,
};
File diff suppressed because it is too large. Load diff
@@ -2,8 +2,7 @@
* JSON schemas used for validating link messages
* @module lib/schema
*/
"use strict";
const Ajv = require("ajv");
import Ajv, { JSONSchemaType, ValidateFunction } from "ajv";
const ajv = new Ajv({
// These are duplicated in scripts/compile_validator
allowUnionTypes: true,
@@ -14,17 +13,16 @@ const ajv = new Ajv({
/**
* Compile JSON schema into validator
* @function
* @param {Object} schema - JSON schema to create validator for.
* @returns {Function} Validator for the schema.
* @param schema - JSON schema to create validator for.
* @returns Validator for the schema.
*/
function compile(schema) {
if (typeof global !== "object" || !global.lazySchemaCompilation) {
return ajv.compile(schema);
export function compile<T>(schema: JSONSchemaType<T>) {
if (typeof global !== "object" || !(global as any).lazySchemaCompilation) {
return ajv.compile<T>(schema);
}
let doValidate;
function validate(data) {
let doValidate: ValidateFunction<T>;
const validate = <ValidateFunction<T>>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,
};
@@ -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 <path>", "Add plugin by require path")
.command("remove <name>", "Remove plugin by name")
@@ -40,24 +40,28 @@ function pluginCommand(yargs) {
*
* Handle the actions that are made available by pluginCommand.
*
* @param {Object} args - yargs args object.
* @param {Map<string, string>} pluginList - Current list of plugins.
* @param {string} pluginListPath - Path to plugin list config file.
* @param args - yargs args object.
* @param pluginList - Current list of plugins.
* @param pluginListPath - Path to plugin list config file.
*/
async function handlePluginCommand(args, pluginList, pluginListPath) {
let command = args._[1];
export async function handlePluginCommand(
args: Record<string, unknown>,
pluginList: Map<string, string>,
pluginListPath: string
) {
let command = (args._ as string[])[1];
if (command === "add") {
let pluginPath = args.path;
let pluginPath = args.path as string;
if (/^\.\.?[\/\\]/.test(pluginPath)) {
pluginPath = path.resolve(pluginPath);
}
let pluginInfo;
let pluginInfo: { name: string };
try {
// eslint-disable-next-line node/global-require
pluginInfo = require(path.posix.join(pluginPath, "info"));
} catch (err) {
} catch (err: any) {
logger.error(`Unable to import plugin info from ${args.path}:\n${err.stack}`);
process.exitCode = 1;
return;
@@ -74,7 +78,7 @@ async function handlePluginCommand(args, pluginList, pluginListPath) {
print(`Added ${pluginInfo.name}`);
} else if (command === "remove") {
if (!pluginList.delete(args.name)) {
if (!pluginList.delete(args.name as string)) {
logger.error(`Plugin with name ${args.name} does not exist`);
process.exitCode = 1;
return;
@@ -97,11 +101,11 @@ async function handlePluginCommand(args, pluginList, pluginListPath) {
* Can be passed to yargs.command to implement a config command. Use
* handleConfigCommand to do the requested action.
*
* @param {Object} yargs - yargs command builder.
* @param yargs - yargs command builder.
*/
function configCommand(yargs) {
export function configCommand(yargs: any) {
yargs
.command("set <field> [value]", "Set config field", yargs => {
.command("set <field> [value]", "Set config field", (yargs: any) => {
yargs.options({
"stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" },
});
@@ -119,15 +123,19 @@ function configCommand(yargs) {
*
* Handle the actions that are made available by configCommand.
*
* @param {Object} args - yargs args object.
* @param {module:lib.Config} instance - Config instance.
* @param {string} configPath - Path to configuration file.
* @param args - yargs args object.
* @param instance - Config instance.
* @param configPath - Path to configuration file.
*/
async function handleConfigCommand(args, instance, configPath) {
let command = args._[1];
export async function handleConfigCommand(
args: Record<string, unknown>,
instance: libConfig.Config,
configPath: string
) {
let command = (args._ as string[])[1];
if (command === "list") {
for (let GroupClass of instance.constructor.groups.values()) {
for (let GroupClass of (instance.constructor as typeof libConfig.Config).groups.values()) {
for (let def of GroupClass.definitions.values()) {
let value = instance.get(def.fullName);
print(`${def.fullName} ${JSON.stringify(value)}`);
@@ -136,7 +144,7 @@ async function handleConfigCommand(args, instance, configPath) {
} else if (command === "show") {
try {
print(instance.get(args.field));
print(instance.get(args.field as string));
} catch (err) {
if (err instanceof libConfig.InvalidField) {
logger.error(err.message);
@@ -154,7 +162,7 @@ async function handleConfigCommand(args, instance, configPath) {
}
try {
instance.set(args.field, args.value);
instance.set(args.field as string, args.value);
await libFileOps.safeOutputFile(configPath, JSON.stringify(instance.serialize(), null, 4));
} catch (err) {
if (err instanceof libConfig.InvalidField || err instanceof libConfig.InvalidValue) {
@@ -165,12 +173,3 @@ async function handleConfigCommand(args, instance, configPath) {
}
}
}
module.exports = {
pluginCommand,
handlePluginCommand,
configCommand,
handleConfigCommand,
};
@@ -1,7 +1,7 @@
"use strict";
const assert = require("assert");
const fs = require("fs-extra");
const stream = require("stream");
import assert from "assert";
import fs from "fs-extra";
import stream from "stream";
/**
@@ -11,20 +11,17 @@ const stream = require("stream");
* one removing the line feed and optional carriage return from the lines.
*
* @example
* let lineStream = new libStream.LineSplitter({ readableObjectMode: true });
* ```ts
* let lineStream = new lib.LineSplitter({ readableObjectMode: true });
* lineStream.on("data", line => { console.log(line.toString("utf8")); });
* let fileStream = fs.createReadStream(path);
* fileStream.pipe(lineStream);
*
* @memberof module:lib
* ```
*/
class LineSplitter extends stream.Transform {
constructor(options) {
super(options);
this._partial = null;
}
export class LineSplitter extends stream.Transform {
_partial: Buffer | null = null;
_transform(chunk, encoding, callback) {
_transform(chunk: Buffer, _encoding: string, callback: stream.TransformCallback) {
if (this._partial) {
chunk = Buffer.concat([this._partial, chunk]);
this._partial = null;
@@ -43,14 +40,14 @@ class LineSplitter extends stream.Transform {
end -= 1;
}
let line = chunk.slice(0, end);
chunk = chunk.slice(next);
let line = chunk.subarray(0, end);
chunk = chunk.subarray(next);
this.push(line);
}
callback();
}
_flush(callback) {
_flush(callback: stream.TransformCallback) {
if (this._partial) {
this.push(this._partial);
this._partial = null;
@@ -63,25 +60,22 @@ class LineSplitter extends stream.Transform {
* Transform splitting a reverse stream into lines
*
* Splits up a reverse stream of chunks created by {@link
* module:lib.createReverseReadStream} by newlines and passes on the
* createReverseReadStream} by newlines and passes on the
* lines one by one in reverse order removing the line feed and optional
* carriage return from the lines.
*
* @example
* let revLineStream = new libStream.ReverseLineSplitter({ readableObjectMode: true });
* ```ts
* let revLineStream = new lib.ReverseLineSplitter({ readableObjectMode: true });
* revLineStream.on("data", line => { console.log(line.toString("utf8")); });
* let revFileStream = await libStream.createReverseReadStream(path);
* let revFileStream = await lib.createReverseReadStream(path);
* revFileStream.pipe(revLineStream);
*
* @memberof module:lib
* ```
*/
class ReverseLineSplitter extends stream.Transform {
constructor(options) {
super(options);
this._partial = null;
}
export class ReverseLineSplitter extends stream.Transform {
_partial: Buffer | null = null;
_transform(chunk, encoding, callback) {
_transform(chunk: Buffer, _encoding: string, callback: stream.TransformCallback) {
if (this._partial) {
chunk = Buffer.concat([chunk, this._partial]);
this._partial = null;
@@ -103,14 +97,14 @@ class ReverseLineSplitter extends stream.Transform {
}
}
let line = chunk.slice(next + 1, end + 1);
chunk = chunk.slice(0, next + 1);
let line = chunk.subarray(next + 1, end + 1);
chunk = chunk.subarray(0, next + 1);
this.push(line);
}
callback();
}
_flush(callback) {
_flush(callback: stream.TransformCallback) {
if (this._partial) {
let end = this._partial.length - 1;
if (this._partial[end] === "\n".charCodeAt(0)) {
@@ -120,7 +114,7 @@ class ReverseLineSplitter extends stream.Transform {
end -= 1;
}
}
this.push(this._partial.slice(0, end + 1));
this.push(this._partial.subarray(0, end + 1));
this._partial = null;
}
callback();
@@ -128,6 +122,8 @@ class ReverseLineSplitter extends stream.Transform {
}
type ReadStreamOptions = Parameters<typeof fs.createReadStream>[1];
/**
* Create a file read stream running in reverse
*
@@ -136,19 +132,28 @@ class ReverseLineSplitter extends stream.Transform {
* are not reversed, thus the file content can be recreated by concatenating
* the chunks together in the reverse order they are read.
*
* @param {string} path -
* @param path -
* Path to file to open. May be anything accepted by fs.open.
* @param {object} options -
* @param options -
* Options to pass to fs.createReadStream. Values passed for `fd` and
* `fs` will be ignored.
* @memberof module:lib
*/
async function createReverseReadStream(path, options) {
export async function createReverseReadStream(path: string, options?: ReadStreamOptions) {
if (typeof options === "string") {
options = { encoding: options };
}
const fileFd = await fs.open(path, "r");
const fileSize = (await fs.fstat(fileFd)).size;
let filePosition = fileSize;
const reverseFs = {
read(fd, buffer, offset, length, position, callback) {
read(
fd: number,
buffer: Buffer,
offset: number,
length: number,
position: number,
callback: () => void,
) {
assert(fd === fileFd);
assert(position === undefined);
length = Math.min(length, filePosition);
@@ -156,13 +161,10 @@ async function createReverseReadStream(path, options) {
return fs.read(fd, buffer, offset, length, filePosition, callback);
},
open() { assert(false); },
close(fd, callback) { return fs.close(fd, callback); },
close(fd: number, callback: () => void) { return fs.close(fd, callback); },
};
return fs.createReadStream("", { ...options, fd: fileFd, fs: reverseFs });
return fs.createReadStream(
"",
{ ...options, fd: fileFd, fs: reverseFs } as ReadStreamOptions, // type specification is missing fs prop.
);
}
module.exports = {
LineSplitter,
ReverseLineSplitter,
createReverseReadStream,
};
@@ -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<string, Permission>();
/**
* 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<string>;
constructor(serializedRole: ReturnType<Role["serialize"]>) {
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<number, module:lib.Role>} 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<number, Role>) {
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<number, module:lib.Role>} roles - role storage to modify.
* @static
* @param roles - role storage to modify.
*/
function ensureDefaultPlayerRole(roles) {
export function ensureDefaultPlayerRole(roles: Map<number, Role>) {
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<number>;
/** 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<Role>;
/** Per instance statistics for the player this user account is tied to. */
instanceStats: Map<number, PlayerStats>;
/** 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<User["serialize"]>,
loadedRoles: Map<number, Role>
) {
this.name = serializedUser.name;
/**
* Instances this user is online on.
* @type {Set<number>}
*/
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<module:lib.Role>}
*/
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<number, module:lib.PlayerStats>}
*/
this.instanceStats = new Map(
(serializedUser.instance_stats ? serializedUser.instance_stats : []).map(
([id, stats]) => [id, new PlayerStats(stats)]
)
);
/**
* Combined statistics for the player this user account is tied to.
* @type {module:lib.PlayerStats}
*/
this.playerStats = this._calculatePlayerStats();
/**
* True if this user object has been removed from the cluster.
* @type {boolean}
*/
this.isDeleted = false;
}
serialize() {
let serialized = {
let serialized: {
name: string,
roles?: number[],
token_valid_after?: number,
is_admin?: boolean,
is_whitelisted?: boolean,
is_banned?: boolean,
ban_reason?: string,
instance_stats?: [number, object][],
} = {
name: this.name,
};
@@ -272,11 +273,11 @@ class User {
* permission. If the permission is not granted for the user a
* "Permission denied" error is thrown.
*
* @param {string} permission - The permission to check for.
* @param permission - The permission to check for.
* @throws {Error} If the given permission does not exist.
* @throws {module:lib.PermissionError} if the user does noh have the given permission.
* @throws {libErrors.PermissionError} if the user does noh have the given permission.
*/
checkPermission(permission) {
checkPermission(permission: string) {
if (!permissions.has(permission)) {
throw new Error(`permission ${permission} does not exist`);
}
@@ -290,12 +291,12 @@ class User {
throw new libErrors.PermissionError("Permission denied");
}
notifyJoin(instance_id) {
notifyJoin(instance_id: number) {
this.instances.add(instance_id);
User.onlineUsers.add(this);
}
notifyLeave(instance_id) {
notifyLeave(instance_id: number) {
this.instances.delete(instance_id);
if (!this.instances.size) {
User.onlineUsers.delete(this);
@@ -309,10 +310,16 @@ class User {
_calculatePlayerStats() {
let playerStats = new PlayerStats();
for (let instanceStats of this.instanceStats.values()) {
if (!playerStats.lastJoinAt || instanceStats.lastJoinAt > playerStats.lastJoinAt) {
if (
instanceStats.lastJoinAt
&& (!playerStats.lastJoinAt || instanceStats.lastJoinAt > playerStats.lastJoinAt)
) {
playerStats.lastJoinAt = instanceStats.lastJoinAt;
}
if (!playerStats.lastLeaveAt || instanceStats.lastLeaveAt > playerStats.lastLeaveAt) {
if (
instanceStats.lastLeaveAt
&& (!playerStats.lastLeaveAt || instanceStats.lastLeaveAt > playerStats.lastLeaveAt)
) {
playerStats.lastLeaveAt = instanceStats.lastLeaveAt;
playerStats.lastLeaveReason = instanceStats.lastLeaveReason;
}
@@ -321,12 +328,11 @@ class User {
}
return playerStats;
}
/**
* Set of users currently online in the cluster.
*/
static onlineUsers = new Set<User>();
}
/**
* Set of users currently online in the cluster.
* @type {module:lib.User}
*/
User.onlineUsers = new Set();
// Definitions for the built in permissions used in Clusterio.
// description should answer "this permission allows you to ___"
@@ -677,14 +683,3 @@ definePermission({
title: "Dump WebSocket",
description: "Dump all WebSocket communicatation from the controller.",
});
module.exports = {
Permission,
Role,
User,
permissions,
definePermission,
ensureDefaultAdminRole,
ensureDefaultPlayerRole,
};
@@ -1,4 +1,4 @@
"use strict";
import type JSZip from "jszip";
/**
* Returns the root folder in the zip file
@@ -6,14 +6,11 @@
* Returns the name of the folder that all files in the zip file is
* contained in. Throws an error if there are multiple such folders.
*
* @param {JSZip} zip - Zip to search through.
* @returns {string} name of the root folder.
* @memberof module:lib
* @private
* @inner
* @param zip - Zip to search through.
* @returns name of the root folder.
*/
function findRoot(zip) {
let root = null;
export function findRoot(zip: JSZip) {
let root: undefined | string;
zip.forEach((relativePath, file) => {
let index = relativePath.indexOf("/");
if (index === -1) {
@@ -21,20 +18,16 @@ function findRoot(zip) {
}
let pathRoot = relativePath.slice(0, index);
if (root === null) {
if (root === undefined) {
root = pathRoot;
} else if (root !== pathRoot) {
throw new Error("Zip contains multiple root folders");
}
});
if (root === null) {
if (root === undefined) {
throw new Error("Empty zip file");
}
return root;
}
module.exports = {
findRoot,
};
+18
View File
@@ -0,0 +1,18 @@
{
"include": ["index.ts", "build_mod.js", "src/**/*.ts", "src/**/*.js"],
"compilerOptions": {
"lib": ["es2023"],
"module": "commonjs",
"target": "es2022",
"rootDir": ".",
"outDir": "dist",
"composite": true,
"sourceMap": true,
"strict": true,
"alwaysStrict": true,
"allowJs": true,
"checkJs": false,
"esModuleInterop": true,
"skipLibCheck": true
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"entryPoints": ["index.ts"],
"includeVersion": true
}
@@ -16,9 +16,9 @@ export default function CreateSaveModal(props) {
function createSave() {
let values = form.getFieldsValue();
let name = values.saveName || "world.zip";
let seed = values.seed === undefined ? null : Number.parseInt(values.seed, 10);
let mapGenSettings = null;
let mapSettings = null;
let seed = values.seed === undefined ? undefined : Number.parseInt(values.seed, 10);
let mapGenSettings;
let mapSettings;
if (values.exchangeString && values.exchangeString.trim()) {
let result;
try {
@@ -16,9 +16,9 @@ export default function LoadScenarioModal(props) {
function loadScenario() {
let values = form.getFieldsValue();
let scenario = values.scenario || "base/freeplay";
let seed = values.seed === undefined ? null : Number.parseInt(values.seed, 10);
let mapGenSettings = null;
let mapSettings = null;
let seed = values.seed === undefined ? undefined : Number.parseInt(values.seed, 10);
let mapGenSettings;
let mapSettings;
if (values.exchangeString && values.exchangeString.trim()) {
let result;
try {
@@ -75,7 +75,7 @@ export default function LogConsole(props) {
logFilter.controller,
logFilter.hostIds,
logFilter.instanceIds,
null,
undefined,
400,
"desc",
)).then(result => {
+1 -1
View File
@@ -565,7 +565,7 @@ export class Control extends lib.Link {
}
}
await this.send(new lib.LogSetSubscriptionsRequest(all, controller, hostIds, instanceIds, null));
await this.send(new lib.LogSetSubscriptionsRequest(all, controller, hostIds, instanceIds, undefined));
}
async handleDebugWsMessageEvent(message) {
-2
View File
@@ -44,7 +44,6 @@ class ControllerPlugin extends lib.BaseControllerPlugin {
action: () => this.broadcastProgress(),
});
this.lastProgressBroadcast = Date.now();
this.progressBroadcastId = null;
this.progressToBroadcast = new Set();
@@ -68,7 +67,6 @@ class ControllerPlugin extends lib.BaseControllerPlugin {
}
this.progressToBroadcast.clear();
this.lastProgressBroadcast = Date.now();
if (techs.length) {
this.controller.sendTo("allInstances", new ProgressEvent(techs));
}
+6 -4
View File
@@ -92,12 +92,14 @@ describe("Integration of Clusterio", function() {
describe("queryLogRequestHandler", function() {
it("should honnor the limit", async function() {
let result = await getControl().send(new lib.LogQueryRequest(true, false, [], [], null, 10, "asc"));
let result = await getControl().send(
new lib.LogQueryRequest(true, false, [], [], undefined, 10, "asc")
);
assert.equal(result.log.length, 10);
});
it("should return entries by order", async function() {
let first = await getControl().send(new lib.LogQueryRequest(true, false, [], [], null, 1, "asc"));
let last = await getControl().send(new lib.LogQueryRequest(true, false, [], [], null, 1, "desc"));
let first = await getControl().send(new lib.LogQueryRequest(true, false, [], [], undefined, 1, "asc"));
let last = await getControl().send(new lib.LogQueryRequest(true, false, [], [], undefined, 1, "desc"));
assert(first.log[0].timestamp < last.log[0].timestamp, "first log entry happened after last");
});
});
@@ -346,7 +348,7 @@ describe("Integration of Clusterio", function() {
slowTest(this);
await execCtl("instance send-rcon test technobabble");
let { log } = await getControl().send(
new lib.LogQueryRequest(false, false, [], [44], null, 10, "desc")
new lib.LogQueryRequest(false, false, [], [44], undefined, 10, "desc")
);
assert(log.some(info => /technobabble/.test(info.message)), "Command was not sent");
});
+1 -1
View File
@@ -19,7 +19,7 @@ describe("lib/command", function() {
lib.HostListRequest, () => [new lib.HostDetails("test", "0.1", "Test Host", 11, false)]
);
testController.handle(
lib.InstanceDetailsListRequest, () => [new lib.InstanceDetails("Test Instance", 57, 4, null, "stopped")]
lib.InstanceDetailsListRequest, () => [new lib.InstanceDetails("Test Instance", 57, 4, undefined, "stopped")]
);
testController.handle(lib.RoleListRequest, () => [testRole]);
-9
View File
@@ -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";
+12 -5
View File
@@ -80,7 +80,7 @@ describe("lib/link/link", function() {
testConnector._seq - 1,
src,
dst,
new lib.ResponseError("Unrecognized request UnhandledRequest")
new lib.ResponseError("Unrecognized request UnhandledRequest", "InvalidMessage")
),
]);
});
@@ -114,7 +114,10 @@ describe("lib/link/link", function() {
testConnector._seq - 1,
src,
dst,
new lib.ResponseError("Source [Address controller:0] is not allowed for InvalidSrcRequest")
new lib.ResponseError(
"Source [Address controller:0] is not allowed for InvalidSrcRequest",
"InvalidMessage"
)
),
]);
});
@@ -132,7 +135,8 @@ describe("lib/link/link", function() {
src,
dst,
new lib.ResponseError(
"Destination [Address control:1] is not allowed for InvalidDstRequest"
"Destination [Address control:1] is not allowed for InvalidDstRequest",
"InvalidMessage",
)
),
]);
@@ -144,7 +148,7 @@ describe("lib/link/link", function() {
testConnector._seq - 1,
src,
addr("allControls"),
new lib.ResponseError("Message src may not be broadcast")
new lib.ResponseError("Message src may not be broadcast", "InvalidMessage")
),
]);
});
@@ -155,7 +159,10 @@ describe("lib/link/link", function() {
testConnector._seq - 1,
src,
dst,
new lib.ResponseError("Destination [Address 4:control] is not allowed for SimpleRequest")
new lib.ResponseError(
"Destination [Address 4:control] is not allowed for SimpleRequest",
"InvalidMessage",
)
),
]);
});
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPoints": ["packages/lib/"],
"entryPointStrategy": "packages",
"cleanOutputDir": false,
"out": "docs/api"
}