diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index b4161f65..29692954 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -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); diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index 20651843..ba59deaf 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -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 { @@ -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`); } diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index 2badf027..4c767d9c 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -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(), + public systems = new lib.SubscribableDatastore(), /** Mapping of host id to host info */ - public hosts = new Map(), + public hosts = new lib.SubscribableDatastore(), /** Mapping of instance id to instance info */ - public instances = new Map(), + public instances = new lib.SubscribableDatastore(), /** Mapping of save id to save details */ public saves = new lib.SubscribableDatastore(), /** 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> { - let json: Static[]; - 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) { - await lib.safeOutputFile(filePath, JSON.stringify([...systems.values()], null, "\t")); - } - - static async loadHosts(filePath: string): Promise> { - let serialized: [number, Static][]; - 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[] { + const serialized = rawJson as Static[]; // 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) { - await lib.safeOutputFile(filePath, JSON.stringify([...hosts.entries()], null, "\t")); + static migrateInstances(rawJson: unknown[]): Static[] { + const serialized = rawJson as Static[]; + 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> { - logger.info(`Loading ${filePath}`); - - let instances = new Map(); - try { - let serialized = JSON.parse( - await fs.readFile(filePath, { encoding: "utf8" }) - ) as Static[]; - 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) { - let serialized = [...instances.values()]; - await lib.safeOutputFile(filePath, JSON.stringify(serialized, null, "\t")); + return instance; } static async loadJsonObject(filePath: string, throwOnMissing: boolean = false): Promise { @@ -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); }); } diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index 7a82b233..7d31d979 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -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 = new Set(); diff --git a/packages/controller/src/InstanceInfo.ts b/packages/controller/src/InstanceInfo.ts index 0534c20d..b88c6b5b 100644 --- a/packages/controller/src/InstanceInfo.ts +++ b/packages/controller/src/InstanceInfo.ts @@ -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"); diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index a3a54253..068bf3e5 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -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; diff --git a/packages/lib/src/datastore.ts b/packages/lib/src/datastore.ts index 9f3aa3fb..3624efee 100644 --- a/packages/lib/src/datastore.ts +++ b/packages/lib/src/datastore.ts @@ -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(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 { constructor( private filePath: string, - private dataClass: JSONDeserialisable, - private migrations: (rawJson: unknown) => Array<[K, unknown]> = v => v as any, + private fromJson: (json: J) => V, + private migrations: (rawJson: Record) => Record = 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 { constructor( private filePath: string, - private dataClass: JSONDeserialisable, - private migrations: (rawJson: unknown) => Array<{ id: K }> = v => v as any, + private fromJson: (json: J) => V, + private migrations: (rawJson: Array) => Array = 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 = new MemoryDatastoreProvider(), + private provider: DatastoreProvider = new MemoryDatastoreProvider(), protected data = new Map(), ) { 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 ?? 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>; } + + // 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 { // 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); } } diff --git a/plugins/player_auth/test/plugin.js b/plugins/player_auth/test/plugin.js index 6b90348e..3865fcdb 100644 --- a/plugins/player_auth/test/plugin.js +++ b/plugins/player_auth/test/plugin.js @@ -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`); }, diff --git a/test/integration/routing.js b/test/integration/routing.js index 7c5b204c..3b423786 100644 --- a/test/integration/routing.js +++ b/test/integration/routing.js @@ -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); diff --git a/test/mock.js b/test/mock.js index 1c1fb84b..3eaabea2 100644 --- a/test/mock.js +++ b/test/mock.js @@ -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(); }