mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 23:24:52 +09:00
Add hosts, instances, systems
This commit is contained in:
10 files changed
+135
-203
No files matched your search
@@ -66,9 +66,9 @@ void new lib.Gauge(
|
||||
if (!controller || !controller.hosts) {
|
||||
return;
|
||||
}
|
||||
for (let [id, host] of controller.hosts) {
|
||||
for (const host of controller.hosts.values()) {
|
||||
gauge.labels({
|
||||
host_id: String(id),
|
||||
host_id: String(host.id),
|
||||
host_name: host.name,
|
||||
}).set(1);
|
||||
};
|
||||
@@ -86,9 +86,9 @@ void new lib.Gauge(
|
||||
if (!controller || !controller.instances) {
|
||||
return;
|
||||
}
|
||||
for (let [id, instance] of controller.instances) {
|
||||
for (const instance of controller.instances.values()) {
|
||||
gauge.labels({
|
||||
instance_id: String(id),
|
||||
instance_id: String(instance.id),
|
||||
instance_name: String(instance.config.get("instance.name")),
|
||||
host_id: String(instance.config.get("instance.assigned_host")),
|
||||
}).set(1);
|
||||
|
||||
@@ -195,7 +195,7 @@ export default class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async handleHostRevokeTokensRequest(request: lib.HostRevokeTokensRequest) {
|
||||
const host = this._controller.hosts.get(request.hostId);
|
||||
const host = this._controller.hosts.getMutable(request.hostId);
|
||||
if (!host) {
|
||||
throw new Error(`Unknown host id (${request.hostId})`);
|
||||
}
|
||||
@@ -207,7 +207,7 @@ export default class ControlConnection extends BaseConnection {
|
||||
hostConnection.connector.terminate();
|
||||
}
|
||||
|
||||
this._controller.hostsUpdated([host]);
|
||||
this._controller.hosts.set(host);
|
||||
}
|
||||
|
||||
async handleHostListRequest(): Promise<lib.HostDetails[]> {
|
||||
@@ -410,7 +410,7 @@ export default class ControlConnection extends BaseConnection {
|
||||
|
||||
async handleModPackDeleteRequest(request: lib.ModPackDeleteRequest) {
|
||||
let { id } = request;
|
||||
let modPack = this._controller.modPacks.getCopy(id);
|
||||
let modPack = this._controller.modPacks.getMutable(id);
|
||||
if (!modPack) {
|
||||
throw new lib.RequestError(`Mod pack with ID ${id} does not exist`);
|
||||
}
|
||||
|
||||
@@ -55,13 +55,6 @@ export default class Controller {
|
||||
config: lib.ControllerConfig;
|
||||
app: Application;
|
||||
|
||||
/** True if (@link Controller.systems} has changed since last time it was saved */
|
||||
systemsDirty = false;
|
||||
/** True if {@link Controller.hosts} has changed since last time it was saved */
|
||||
hostsDirty = false;
|
||||
/** True if {@link Controller.instances} has changed since last time it was saved */
|
||||
instancesDirty = false;
|
||||
|
||||
httpServer: http.Server | null = null;
|
||||
httpServerCloser: HttpCloser | null = null;
|
||||
httpsServer: https.Server | null = null;
|
||||
@@ -107,16 +100,31 @@ export default class Controller {
|
||||
let databaseDirectory = config.get("controller.database_directory");
|
||||
await fs.ensureDir(databaseDirectory);
|
||||
|
||||
const systems = await Controller.loadSystems(path.join(databaseDirectory, "systems.json"));
|
||||
const hosts = await Controller.loadHosts(path.join(databaseDirectory, "hosts.json"));
|
||||
const instances = await Controller.loadInstances(path.join(databaseDirectory, "instances.json"));
|
||||
const systems = new lib.SubscribableDatastore(...await new lib.JsonIdDatastoreProvider(
|
||||
path.join(databaseDirectory, "systems.json"),
|
||||
lib.SystemInfo.fromJSON
|
||||
).bootstrap());
|
||||
|
||||
const hosts = new lib.SubscribableDatastore(...await new lib.JsonIdDatastoreProvider(
|
||||
path.join(databaseDirectory, "hosts.json"),
|
||||
HostInfo.fromJSON,
|
||||
this.migrateHosts, this.finaliseHosts,
|
||||
).bootstrap());
|
||||
|
||||
const instances = new lib.SubscribableDatastore(...await new lib.JsonIdDatastoreProvider(
|
||||
path.join(databaseDirectory, "instances.json"),
|
||||
json => InstanceInfo.fromJSON(json, "controller"),
|
||||
this.migrateInstances, this.finaliseInstances,
|
||||
).bootstrap());
|
||||
|
||||
const saves = new lib.SubscribableDatastore(...await new lib.JsonIdDatastoreProvider(
|
||||
path.join(databaseDirectory, "saves.json"), lib.SaveDetails
|
||||
path.join(databaseDirectory, "saves.json"),
|
||||
lib.SaveDetails.fromJSON
|
||||
).bootstrap());
|
||||
|
||||
const modPacks = new lib.SubscribableDatastore(...await new lib.JsonIdDatastoreProvider(
|
||||
path.join(databaseDirectory, "mod-packs.json"), lib.ModPack
|
||||
path.join(databaseDirectory, "mod-packs.json"),
|
||||
lib.ModPack.fromJSON
|
||||
).bootstrap());
|
||||
|
||||
const userManager = new UserManager(config);
|
||||
@@ -148,11 +156,11 @@ export default class Controller {
|
||||
* will restart the controller on non-zero exit codes.
|
||||
*/
|
||||
public canRestart: boolean = false,
|
||||
public systems = new Map<lib.SystemInfo["id"], lib.SystemInfo>(),
|
||||
public systems = new lib.SubscribableDatastore<lib.SystemInfo>(),
|
||||
/** Mapping of host id to host info */
|
||||
public hosts = new Map<number, HostInfo>(),
|
||||
public hosts = new lib.SubscribableDatastore<HostInfo>(),
|
||||
/** Mapping of instance id to instance info */
|
||||
public instances = new Map<number, InstanceInfo>(),
|
||||
public instances = new lib.SubscribableDatastore<InstanceInfo>(),
|
||||
/** Mapping of save id to save details */
|
||||
public saves = new lib.SubscribableDatastore<lib.SaveDetails>(),
|
||||
/** Mapping of mod pack id to mod pack */
|
||||
@@ -182,17 +190,18 @@ export default class Controller {
|
||||
this.subscriptions.handle(lib.SystemInfoUpdateEvent, this.handleSystemInfoSubscription.bind(this));
|
||||
this.subscriptions.handle(lib.HostUpdatesEvent, this.handleHostSubscription.bind(this));
|
||||
this.subscriptions.handle(lib.InstanceDetailsUpdatesEvent, this.handleInstanceDetailsSubscription.bind(this));
|
||||
|
||||
this.subscriptions.handle(
|
||||
lib.InstanceSaveDetailsUpdatesEvent, this.handleInstanceSaveDetailsSubscription.bind(this)
|
||||
);
|
||||
this.saves.on("update", this.savesUpdated.bind(this));
|
||||
|
||||
this.subscriptions.handle(lib.ModPackUpdatesEvent, this.handleModPackSubscription.bind(this));
|
||||
this.modPacks.on("update", this.modPacksUpdated.bind(this));
|
||||
|
||||
this.subscriptions.handle(lib.ModUpdatesEvent, this.handleModSubscription.bind(this));
|
||||
this.subscriptions.handle(lib.UserUpdatesEvent, this.handleUserSubscription.bind(this));
|
||||
|
||||
// Handle updates for datastores
|
||||
this.hosts.on("update", this.hostsUpdated.bind(this));
|
||||
this.instances.on("update", this.instanceDetailsUpdated.bind(this));
|
||||
this.saves.on("update", this.savesUpdated.bind(this));
|
||||
this.modPacks.on("update", this.modPacksUpdated.bind(this));
|
||||
}
|
||||
|
||||
async start(args: ControllerArgs) {
|
||||
@@ -509,9 +518,8 @@ export default class Controller {
|
||||
requests.push(lib.gatherSystemInfo("controller", this.canRestart));
|
||||
const newMetrics = await Promise.all(requests);
|
||||
for (const metric of newMetrics) {
|
||||
this.systems.set(metric.id, metric);
|
||||
this.systems.set(metric);
|
||||
}
|
||||
this.systemsDirty = true;
|
||||
this.subscriptions.broadcast(new lib.SystemInfoUpdateEvent(newMetrics));
|
||||
} catch (err: any) {
|
||||
logger.error(`Unexpected error updating system infos:\n${err.stack ?? err.message}`);
|
||||
@@ -554,21 +562,10 @@ export default class Controller {
|
||||
}
|
||||
|
||||
let databaseDirectory = this.config.get("controller.database_directory");
|
||||
if (this.systemsDirty) {
|
||||
this.systemsDirty = false;
|
||||
await Controller.saveSystems(path.join(databaseDirectory, "systems.json"), this.systems);
|
||||
}
|
||||
|
||||
if (this.hostsDirty) {
|
||||
this.hostsDirty = false;
|
||||
await Controller.saveHosts(path.join(databaseDirectory, "hosts.json"), this.hosts);
|
||||
}
|
||||
|
||||
if (this.instancesDirty) {
|
||||
this.instancesDirty = false;
|
||||
await Controller.saveInstances(path.join(databaseDirectory, "instances.json"), this.instances);
|
||||
}
|
||||
|
||||
await this.systems.save();
|
||||
await this.hosts.save();
|
||||
await this.instances.save();
|
||||
await this.saves.save();
|
||||
await this.modPacks.save();
|
||||
|
||||
@@ -579,92 +576,42 @@ export default class Controller {
|
||||
await lib.invokeHook(this.plugins, "onSaveData");
|
||||
}
|
||||
|
||||
static async loadSystems(filePath: string): Promise<Map<lib.SystemInfo["id"], lib.SystemInfo>> {
|
||||
let json: Static<typeof lib.SystemInfo.jsonSchema>[];
|
||||
try {
|
||||
json = JSON.parse(await fs.readFile(filePath, { encoding: "utf8" }));
|
||||
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
|
||||
return new Map(json.map(m => lib.SystemInfo.fromJSON(m)).map(m => [m.id, m]));
|
||||
}
|
||||
|
||||
static async saveSystems(filePath: string, systems: Map<lib.SystemInfo["id"], lib.SystemInfo>) {
|
||||
await lib.safeOutputFile(filePath, JSON.stringify([...systems.values()], null, "\t"));
|
||||
}
|
||||
|
||||
static async loadHosts(filePath: string): Promise<Map<number, HostInfo>> {
|
||||
let serialized: [number, Static<typeof HostInfo.jsonSchema>][];
|
||||
try {
|
||||
serialized = JSON.parse(await fs.readFile(filePath, { encoding: "utf8" }));
|
||||
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
static migrateHosts(rawJson: unknown[]): Static<typeof HostInfo.jsonSchema>[] {
|
||||
const serialized = rawJson as Static<typeof HostInfo.jsonSchema>[];
|
||||
|
||||
// TODO: migrate remove after release.
|
||||
if (serialized.length && !(serialized[0] instanceof Array)) {
|
||||
return new Map(); // Discard old format.
|
||||
return []; // Discard old format.
|
||||
}
|
||||
|
||||
const hosts = new Map();
|
||||
for (let [id, json] of serialized) {
|
||||
const host = HostInfo.fromJSON(json);
|
||||
if (host.connected) {
|
||||
host.connected = false;
|
||||
host.updatedAtMs = Date.now();
|
||||
}
|
||||
hosts.set(id, host);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
static finaliseHosts(host: HostInfo): HostInfo {
|
||||
if (host.connected) {
|
||||
host.connected = false;
|
||||
host.updatedAtMs = Date.now();
|
||||
}
|
||||
return hosts;
|
||||
return host;
|
||||
}
|
||||
|
||||
static async saveHosts(filePath: string, hosts: Map<number, HostInfo>) {
|
||||
await lib.safeOutputFile(filePath, JSON.stringify([...hosts.entries()], null, "\t"));
|
||||
static migrateInstances(rawJson: unknown[]): Static<typeof InstanceInfo.jsonSchema>[] {
|
||||
const serialized = rawJson as Static<typeof InstanceInfo.jsonSchema>[];
|
||||
return serialized.map(json => {
|
||||
if (!json.config) { // migrate: from pre Alpha 14 format.
|
||||
return { config: json as any, status: "running" }; // Use running to force updatedAtMs
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
|
||||
static async loadInstances(filePath: string): Promise<Map<number, InstanceInfo>> {
|
||||
logger.info(`Loading ${filePath}`);
|
||||
|
||||
let instances = new Map();
|
||||
try {
|
||||
let serialized = JSON.parse(
|
||||
await fs.readFile(filePath, { encoding: "utf8" })
|
||||
) as Static<typeof InstanceInfo.jsonSchema>[];
|
||||
for (let json of serialized) {
|
||||
if (!json.config) { // migrate: from pre Alpha 14 format.
|
||||
json = { config: json as any, status: "running" }; // Use running to force updatedAtMs
|
||||
}
|
||||
const instance = InstanceInfo.fromJSON(json, "controller");
|
||||
const status = instance.config.get("instance.assigned_host") === null ? "unassigned" : "unknown";
|
||||
if (instance.status !== status) {
|
||||
instance.status = status;
|
||||
instance.updatedAtMs = Date.now();
|
||||
}
|
||||
instances.set(instance.id, instance);
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
return new Map();
|
||||
static finaliseInstances(instance: InstanceInfo): InstanceInfo {
|
||||
const status = instance.config.get("instance.assigned_host") === null ? "unassigned" : "unknown";
|
||||
if (instance.status !== status) {
|
||||
instance.status = status;
|
||||
instance.updatedAtMs = Date.now();
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
|
||||
static async saveInstances(filePath: string, instances: Map<number, InstanceInfo>) {
|
||||
let serialized = [...instances.values()];
|
||||
await lib.safeOutputFile(filePath, JSON.stringify(serialized, null, "\t"));
|
||||
return instance;
|
||||
}
|
||||
|
||||
static async loadJsonObject(filePath: string, throwOnMissing: boolean = false): Promise<any> {
|
||||
@@ -782,11 +729,6 @@ export default class Controller {
|
||||
}
|
||||
|
||||
hostsUpdated(hosts: HostInfo[]) {
|
||||
const now = Date.now();
|
||||
for (const host of hosts) {
|
||||
host.updatedAtMs = now;
|
||||
}
|
||||
this.hostsDirty = true;
|
||||
let updates = hosts.map(host => host.toHostDetails());
|
||||
this.subscriptions.broadcast(new lib.HostUpdatesEvent(updates));
|
||||
}
|
||||
@@ -863,11 +805,9 @@ export default class Controller {
|
||||
instanceConfig.set("factorio.settings", settings);
|
||||
|
||||
let instance = new InstanceInfo(instanceConfig, "unassigned", undefined, Date.now());
|
||||
this.instances.set(instanceId, instance);
|
||||
this.instancesDirty = true;
|
||||
this.instances.set(instance);
|
||||
await lib.invokeHook(this.plugins, "onInstanceStatusChanged", instance);
|
||||
this.addInstanceHooks(instance);
|
||||
this.instanceDetailsUpdated([instance]);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -924,7 +864,6 @@ export default class Controller {
|
||||
|
||||
// Assign to target
|
||||
instance.config.set("instance.assigned_host", hostId ?? null);
|
||||
// "fieldChanged" event handler will set this.instancesDirty
|
||||
if (hostId !== undefined && newHostConnection) {
|
||||
await newHostConnection.send(
|
||||
new lib.InstanceAssignInternalRequest(instanceId, instance.config.toRemote("host"))
|
||||
@@ -932,8 +871,7 @@ export default class Controller {
|
||||
} else {
|
||||
instance.status = "unassigned";
|
||||
}
|
||||
instance.updatedAtMs = Date.now();
|
||||
this.instanceDetailsUpdated([instance]);
|
||||
this.instances.set(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -950,16 +888,13 @@ export default class Controller {
|
||||
if (hostId !== null) {
|
||||
await this.sendTo({ hostId }, new lib.InstanceDeleteInternalRequest(instanceId));
|
||||
}
|
||||
this.instances.delete(instanceId);
|
||||
this.instancesDirty = true;
|
||||
|
||||
let prev = instance.status;
|
||||
this.instances.delete(instance);
|
||||
|
||||
this.clearSavesOfInstance(instanceId);
|
||||
this.userManager.clearStatsOfInstance(instanceId);
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = "deleted";
|
||||
instance.updatedAtMs = Date.now();
|
||||
this.instanceDetailsUpdated([instance]);
|
||||
await lib.invokeHook(this.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
|
||||
@@ -988,10 +923,9 @@ export default class Controller {
|
||||
instance.config.on("fieldChanged", (field: string, curr: any, prev: any) => {
|
||||
instance.updatedAtMs = Date.now();
|
||||
if (field === "instance.name") {
|
||||
this.instanceDetailsUpdated([instance]);
|
||||
this.instances.set(instance); // Trigger updated logic
|
||||
}
|
||||
|
||||
this.instancesDirty = true;
|
||||
lib.invokeHook(this.plugins, "onInstanceConfigFieldChanged", instance, field, curr, prev);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,8 +42,7 @@ export default class HostConnection extends BaseConnection {
|
||||
0,
|
||||
false,
|
||||
);
|
||||
this._controller.hosts.set(this.id, this.info);
|
||||
this._controller.hostsUpdated([this.info]);
|
||||
this._controller.hosts.set(this.info);
|
||||
|
||||
this._checkPluginVersions();
|
||||
|
||||
@@ -57,10 +56,9 @@ export default class HostConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
this.connector.on("close", () => {
|
||||
const now = Date.now();
|
||||
// Update status to unknown for instances on this host.
|
||||
const instances: InstanceInfo[] = [];
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
for (let instance of this._controller.instances.valuesMutable()) {
|
||||
if (instance.config.get("instance.assigned_host") !== this.id) {
|
||||
continue;
|
||||
}
|
||||
@@ -68,13 +66,12 @@ export default class HostConnection extends BaseConnection {
|
||||
instances.push(instance);
|
||||
let prev = instance.status;
|
||||
instance.status = "unknown";
|
||||
instance.updatedAtMs = now;
|
||||
lib.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
this._controller.instanceDetailsUpdated(instances);
|
||||
this._controller.instances.setMany(instances);
|
||||
|
||||
this.info.connected = false;
|
||||
this._controller.hostsUpdated([this.info]);
|
||||
this._controller.hosts.set(this.info);
|
||||
});
|
||||
|
||||
this.handle(lib.HostInfoUpdateEvent, this.handleHostInfoUpdateEvent.bind(this));
|
||||
@@ -158,11 +155,11 @@ export default class HostConnection extends BaseConnection {
|
||||
async handleHostInfoUpdateEvent(event: lib.HostInfoUpdateEvent) {
|
||||
this.info.name = event.update.name;
|
||||
this.info.publicAddress = event.update.publicAddress;
|
||||
this._controller.hostsUpdated([this.info]);
|
||||
this._controller.hosts.set(this.info);
|
||||
}
|
||||
|
||||
async handleInstanceStatusChangedEvent(request: lib.InstanceStatusChangedEvent) {
|
||||
let instance = this._controller.instances.get(request.instanceId);
|
||||
let instance = this._controller.instances.getMutable(request.instanceId);
|
||||
|
||||
// It's possible to get updates from an instance that does not exist
|
||||
// or is not assigned to the host it originated from if it was
|
||||
@@ -188,9 +185,8 @@ export default class HostConnection extends BaseConnection {
|
||||
let prev = instance.status;
|
||||
instance.status = request.status;
|
||||
instance.gamePort = request.gamePort;
|
||||
instance.updatedAtMs = Date.now();
|
||||
this._controller.instances.set(instance);
|
||||
logger.verbose(`Instance ${instance.config.get("instance.name")} State: ${instance.status}`);
|
||||
this._controller.instanceDetailsUpdated([instance]);
|
||||
await lib.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
|
||||
@@ -210,7 +206,7 @@ export default class HostConnection extends BaseConnection {
|
||||
const instanceConfig = new lib.InstanceConfig("controller");
|
||||
instanceConfig.update(instanceData.config, false, "host");
|
||||
|
||||
let controllerInstance = this._controller.instances.get(instanceConfig.get("instance.id"));
|
||||
let controllerInstance = this._controller.instances.getMutable(instanceConfig.get("instance.id"));
|
||||
if (controllerInstance) {
|
||||
// Check if this instance is assigned somewhere else.
|
||||
if (controllerInstance.config.get("instance.assigned_host") !== this.id) {
|
||||
@@ -225,9 +221,8 @@ export default class HostConnection extends BaseConnection {
|
||||
let prev = controllerInstance.status;
|
||||
controllerInstance.status = instanceData.status;
|
||||
controllerInstance.gamePort = instanceData.gamePort;
|
||||
controllerInstance.updatedAtMs = Date.now();
|
||||
logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`);
|
||||
instanceUpdates.push(controllerInstance);
|
||||
logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`);
|
||||
await lib.invokeHook(
|
||||
this._controller.plugins, "onInstanceStatusChanged", controllerInstance, prev
|
||||
);
|
||||
@@ -242,21 +237,17 @@ export default class HostConnection extends BaseConnection {
|
||||
instanceData.gamePort,
|
||||
Date.now(),
|
||||
);
|
||||
this._controller.instances.set(instanceConfig.get("instance.id"), newInstance);
|
||||
this._controller.instancesDirty = true;
|
||||
instanceUpdates.push(newInstance);
|
||||
this._controller.addInstanceHooks(newInstance);
|
||||
await this.send(
|
||||
new lib.InstanceAssignInternalRequest(
|
||||
instanceConfig.get("instance.id"), instanceConfig.toRemote("host")
|
||||
)
|
||||
);
|
||||
instanceUpdates.push(newInstance);
|
||||
await lib.invokeHook(this._controller.plugins, "onInstanceStatusChanged", newInstance);
|
||||
}
|
||||
|
||||
if (instanceUpdates.length) {
|
||||
this._controller.instanceDetailsUpdated(instanceUpdates);
|
||||
}
|
||||
this._controller.instances.setMany(instanceUpdates);
|
||||
|
||||
// Push lists to make sure they are in sync.
|
||||
let adminlist: Set<string> = new Set();
|
||||
|
||||
@@ -48,6 +48,13 @@ export default class InstanceInfo {
|
||||
return this.status === "deleted";
|
||||
}
|
||||
|
||||
set isDeleted(value: boolean) {
|
||||
if (value === false) {
|
||||
throw new Error("Setting isDeleted to false is not supported, use status instead.");
|
||||
}
|
||||
this.status = "deleted";
|
||||
}
|
||||
|
||||
/** Shorthand for `instance.config.get("instance.id")` */
|
||||
get id():number {
|
||||
return this.config.get("instance.id");
|
||||
|
||||
@@ -218,7 +218,7 @@ async function uploadExport(req: Request, res: Response) {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
}
|
||||
let modPack = res.app.locals.controller.modPacks.getCopy(modPackId);
|
||||
let modPack = res.app.locals.controller.modPacks.getMutable(modPackId);
|
||||
if (!modPack) {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
|
||||
@@ -2,23 +2,11 @@ import fs from "fs-extra";
|
||||
|
||||
import { safeOutputFile } from "./file_ops";
|
||||
import { SubscribableValue } from "./subscriptions";
|
||||
import { JSONDeserialisable } from "./data/composites";
|
||||
import { EventEmitter } from "stream";
|
||||
|
||||
type DatastoreKey = string | number;
|
||||
type DatastoreValue = string | number | boolean | object;
|
||||
|
||||
// Deep copy required to support the read only property of datastore values
|
||||
function deepCopy<V extends DatastoreValue>(value: V): V {
|
||||
if (value instanceof Object) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([k, v]) => [k, deepCopy(v)])
|
||||
) as V;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Abstract class which can provide saving and loading capabilities to a datastore
|
||||
export abstract class DatastoreProvider<
|
||||
K extends DatastoreKey,
|
||||
@@ -50,11 +38,13 @@ export class MemoryDatastoreProvider<
|
||||
export class JsonDatastoreProvider<
|
||||
K extends DatastoreKey,
|
||||
V extends DatastoreValue,
|
||||
J, // Intermediate type returned from migrations
|
||||
> extends DatastoreProvider<K, V> {
|
||||
constructor(
|
||||
private filePath: string,
|
||||
private dataClass: JSONDeserialisable<V>,
|
||||
private migrations: (rawJson: unknown) => Array<[K, unknown]> = v => v as any,
|
||||
private fromJson: (json: J) => V,
|
||||
private migrations: (rawJson: Record<DatastoreKey, unknown>) => Record<DatastoreKey, J> = v => v as any,
|
||||
private finalise: (obj: V) => V = v => v,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -81,7 +71,7 @@ export class JsonDatastoreProvider<
|
||||
const serialized = this.migrations(rawJson);
|
||||
|
||||
// Convert to data class objects
|
||||
return new Map(serialized.map((k, v) => [k, this.dataClass.fromJSON(v)]));
|
||||
return new Map(Object.entries(serialized).map(([k, v]) => [k, this.finalise(this.fromJson(v))]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,11 +79,13 @@ export class JsonDatastoreProvider<
|
||||
export class JsonIdDatastoreProvider<
|
||||
K extends DatastoreKey,
|
||||
V extends DatastoreValue & { id: K },
|
||||
J, // Intermediate type returned from migrations
|
||||
> extends DatastoreProvider<K, V> {
|
||||
constructor(
|
||||
private filePath: string,
|
||||
private dataClass: JSONDeserialisable<V>,
|
||||
private migrations: (rawJson: unknown) => Array<{ id: K }> = v => v as any,
|
||||
private fromJson: (json: J) => V,
|
||||
private migrations: (rawJson: Array<unknown>) => Array<J> = v => v as any,
|
||||
private finalise: (obj: V) => V = v => v,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -121,7 +113,7 @@ export class JsonIdDatastoreProvider<
|
||||
|
||||
// Convert to data class objects
|
||||
return new Map(serialized.map((e) => {
|
||||
const v = this.dataClass.fromJSON(e);
|
||||
const v = this.finalise(this.fromJson(e));
|
||||
return [v.id, v];
|
||||
}));
|
||||
}
|
||||
@@ -132,10 +124,10 @@ export abstract class BaseDatastore<
|
||||
K extends DatastoreKey,
|
||||
V extends DatastoreValue,
|
||||
> extends EventEmitter {
|
||||
protected dirty = false;
|
||||
private dirty = false;
|
||||
|
||||
constructor(
|
||||
protected provider: DatastoreProvider<K, V> = new MemoryDatastoreProvider(),
|
||||
private provider: DatastoreProvider<K, V> = new MemoryDatastoreProvider(),
|
||||
protected data = new Map<K, V>(),
|
||||
) {
|
||||
super();
|
||||
@@ -155,6 +147,14 @@ export abstract class BaseDatastore<
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
// Set the dirty flag and call update handlers
|
||||
protected touch(updates: any[]) {
|
||||
if (updates.length) {
|
||||
this.dirty = true;
|
||||
this.emit("update", updates);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if the datastore has the value, false otherwise
|
||||
has(key: K) {
|
||||
return this.data.has(key);
|
||||
@@ -165,10 +165,8 @@ export abstract class BaseDatastore<
|
||||
return this.data.get(key) as Readonly<V> ?? undefined;
|
||||
}
|
||||
|
||||
// Get a copy of a value from the datastore
|
||||
getCopy(key: K) {
|
||||
// const v = this.data.get(key);
|
||||
// return v === undefined ? v : deepCopy(v);
|
||||
// Get a mutable reference to a value in the datastore, call set after use
|
||||
getMutable(key: K) {
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
@@ -176,6 +174,11 @@ export abstract class BaseDatastore<
|
||||
values() {
|
||||
return this.data.values() as IterableIterator<Readonly<V>>;
|
||||
}
|
||||
|
||||
// Returns all values in the datastore as mutable references, call setMany after use
|
||||
valuesMutable() {
|
||||
return this.data.values();
|
||||
}
|
||||
}
|
||||
|
||||
// General key value mapping which can be saved and loaded with the file system
|
||||
@@ -186,35 +189,33 @@ export class Datastore<
|
||||
// Set the value in the datastore, be careful of race conditions if you await any functions before calling set
|
||||
set(key: K, value: V) {
|
||||
this.data.set(key, value);
|
||||
this.dirty = true;
|
||||
this.emit("update", [key, value]);
|
||||
this.touch([[key, value]]);
|
||||
}
|
||||
|
||||
// Set many values at once from an array of key value pairs
|
||||
setMany(pairs: [K, V][]) {
|
||||
this.dirty ||= pairs.length > 0;
|
||||
for (const [k, v] of pairs) {
|
||||
this.data.set(k, v);
|
||||
}
|
||||
this.emit("update", pairs);
|
||||
this.touch(pairs);
|
||||
}
|
||||
|
||||
// Delete a value in the datastore
|
||||
delete(key: K) {
|
||||
const value = this.data.get(key);
|
||||
this.data.delete(key);
|
||||
this.dirty = true;
|
||||
this.emit("update", [key, value, true]);
|
||||
this.touch([[key, value!, true]]);
|
||||
}
|
||||
|
||||
// Delete many values at once from an array of keys
|
||||
deleteMany(keys: K[]) {
|
||||
this.dirty ||= keys.length > 0;
|
||||
const updates = [] as [K, V, true][];
|
||||
for (const key of keys) {
|
||||
const value = this.data.get(key);
|
||||
updates.push([key, value!, true]);
|
||||
this.data.delete(key);
|
||||
}
|
||||
this.emit("update", keys);
|
||||
this.touch(updates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,39 +226,36 @@ export class SubscribableDatastore<
|
||||
> extends BaseDatastore<V["id"], V> {
|
||||
// Set the value in the datastore, be careful of race conditions if you await any functions before calling set
|
||||
set(value: V) {
|
||||
value.updatedAtMs = Date.now();
|
||||
this.data.set(value.id, value);
|
||||
this.dirty = true;
|
||||
this.emit("update", [value]);
|
||||
this.touch([value]);
|
||||
}
|
||||
|
||||
// Set many values at once from an array of key value pairs
|
||||
setMany(values: V[]) {
|
||||
const nowMs = Date.now();
|
||||
this.dirty ||= values.length > 0;
|
||||
for (const value of values) {
|
||||
value.updatedAtMs = nowMs;
|
||||
this.data.set(value.id, value);
|
||||
}
|
||||
this.emit("update", values);
|
||||
this.touch(values);
|
||||
}
|
||||
|
||||
// Delete a value in the datastore
|
||||
delete(value: V) {
|
||||
this.data.delete(value.id);
|
||||
value.isDeleted = true;
|
||||
value.updatedAtMs = Date.now();
|
||||
this.emit("update", [value]);
|
||||
value.isDeleted = true;
|
||||
this.touch([value]);
|
||||
}
|
||||
|
||||
// Delete many values at once from an array of values
|
||||
deleteMany(values: V[]) {
|
||||
this.dirty ||= values.length > 0;
|
||||
const nowMs = Date.now();
|
||||
for (const value of values) {
|
||||
value.isDeleted = true;
|
||||
value.updatedAtMs = Date.now();
|
||||
value.updatedAtMs = nowMs;
|
||||
this.data.delete(value.id);
|
||||
}
|
||||
this.emit("update", values);
|
||||
this.touch(values);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ describe("player_auth", function() {
|
||||
return load;
|
||||
} else if (field === "factorio.settings") {
|
||||
return { name };
|
||||
} else if (field === "instance.id") {
|
||||
return id;
|
||||
}
|
||||
throw new Error(`field ${field} not implemented`);
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@ function connectInstance(controller, host, instanceId) {
|
||||
instanceConfig.set("instance.id", instanceId);
|
||||
instanceConfig.set("instance.assigned_host", host.config.get("host.id"));
|
||||
host.instanceInfos.set(instanceId, { path: "invalid", config: instanceConfig });
|
||||
controller.instances.set(instanceId, new InstanceInfo(instanceConfig, "stopped"));
|
||||
controller.instances.set(new InstanceInfo(instanceConfig, "stopped"));
|
||||
const instance = new Instance(host, instanceSide, "invalid", "invalid", instanceConfig);
|
||||
const instanceConnection = new InstanceConnection(hostSide, host, instance);
|
||||
host.instanceConnections.set(instanceId, instanceConnection);
|
||||
|
||||
+2
-2
@@ -182,8 +182,8 @@ class MockController {
|
||||
["test", ControllerUser.fromJSON({ name: "test", roles: [0, 1] }, this.userManager)],
|
||||
["player", ControllerUser.fromJSON({ name: "player", roles: [1] }, this.userManager)],
|
||||
]);
|
||||
this.instances = new Map();
|
||||
this.hosts = new Map();
|
||||
this.instances = new lib.Datastore();
|
||||
this.hosts = new lib.Datastore();
|
||||
this.handles = new Map();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user