mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 07:24:52 +09:00
Merge pull request #493 from Hornwitser/typescript-lib
Port lib package to TypeScript
This commit is contained in:
85 files changed
+5449
-6616
No files matched your search
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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(`
|
||||
+------------------------------------------------------------+
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
src/data/message_validate.js
|
||||
Whitespace-only changes.
+14
-14
@@ -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";
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Configuration System
|
||||
* @author Hornwitser
|
||||
* @module lib/config
|
||||
*/
|
||||
export * from "./classes";
|
||||
export * from "./definitions";
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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");
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
+66
-64
@@ -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
@@ -0,0 +1 @@
|
||||
export default function isDeepStrictEqual(a: any, b: any): boolean
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import ws = require("ws");
|
||||
export = ws;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 {},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
)
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"entryPoints": ["packages/lib/"],
|
||||
"entryPointStrategy": "packages",
|
||||
"cleanOutputDir": false,
|
||||
"out": "docs/api"
|
||||
}
|
||||
Reference in new issue
Block a user