diff --git a/exp_auto_restart/controller.ts b/exp_auto_restart/controller.ts index 130dd2b0..5695c293 100644 --- a/exp_auto_restart/controller.ts +++ b/exp_auto_restart/controller.ts @@ -16,10 +16,6 @@ export interface PendingRestart { } export class AutoRestart { - controller: Controller; - logger: lib.Logger; - name: string; - /** When each host, and the cluster as a whole, last became empty of players. Absent while players are online. */ idleSince = new Map(); /** Hosts that were told to restart and are waiting for their instances to come back */ @@ -31,32 +27,36 @@ export class AutoRestart { controllerRestarting = false; private checkInterval?: ReturnType; - constructor(context: ControllerPluginContext) { - this.controller = context.controller; - this.logger = context.logger; - this.name = context.plugin.name; - } + private constructor( + public controller: Controller, + public logger: lib.Logger, + public name: string, + ) {} - /** Attach to the controller hooks and start checking for restarts. */ - start() { - const hooks = this.controller.hooks; - hooks.shutdown.attach(this.name, () => this.stop()); - hooks.playerEvent.attach(this.name, (instance, event) => this.onPlayerEvent(instance, event)); - hooks.instanceStatusChanged.attach(this.name, (instance, prev) => this.onInstanceStatusChanged(instance, prev)); - hooks.hostConnectionEvent.attach(this.name, (connection, event) => this.onHostConnectionEvent(connection, event)); + /** Create the plugin, attach to the controller hooks and start checking for restarts. */ + static async fromContext(context: ControllerPluginContext) { + const controller = context.controller; + const plugin = new AutoRestart(controller, context.logger, context.plugin.name); - if (this.controller.config.get("controller.system_metrics_interval") <= 0) { - this.logger.warn( + const hooks = controller.hooks; + hooks.shutdown.attach(plugin.name, () => plugin.stop()); + hooks.playerEvent.attach(plugin.name, (instance, event) => plugin.onPlayerEvent(instance, event)); + hooks.instanceStatusChanged.attach(plugin.name, (instance, prev) => plugin.onInstanceStatusChanged(instance, prev)); + hooks.hostConnectionEvent.attach(plugin.name, (connection, event) => plugin.onHostConnectionEvent(connection, event)); + + if (controller.config.get("controller.system_metrics_interval") <= 0) { + plugin.logger.warn( "controller.system_metrics_interval is 0, so the controller never learns which hosts need a restart" ); } - this.refreshIdle(); - this.checkInterval = setInterval(() => { - this.check().catch((err: any) => { - this.logger.error(`Unexpected error checking for restarts:\n${err.stack ?? err.message}`); + plugin.refreshIdle(); + plugin.checkInterval = setInterval(() => { + plugin.check().catch((err: any) => { + plugin.logger.error(`Unexpected error checking for restarts:\n${err.stack ?? err.message}`); }); }, CHECK_INTERVAL_MS); - this.checkInterval.unref(); + plugin.checkInterval.unref(); + return plugin; } stop() { @@ -291,5 +291,5 @@ export class AutoRestart { } export default async function (context: ControllerPluginContext) { - new AutoRestart(context).start(); + await AutoRestart.fromContext(context); } diff --git a/exp_auto_restart/test/controller.test.js b/exp_auto_restart/test/controller.test.js index e7461f53..05ec45e9 100644 --- a/exp_auto_restart/test/controller.test.js +++ b/exp_auto_restart/test/controller.test.js @@ -35,8 +35,7 @@ async function startPlugin(t2, { config = {}, canRestart = false } = {}) { sent.push({ address, message }); }; - const plugin = new AutoRestart({ controller, logger, plugin: pluginDeclaration, metrics: undefined }); - plugin.start(); + const plugin = await AutoRestart.fromContext({ controller, logger, plugin: pluginDeclaration, metrics: undefined }); t2.teardown(() => plugin.stop()); return { plugin, controller, sent, logs }; } @@ -344,7 +343,7 @@ t.test("class AutoRestart", t2 => { t3.ok(logs.some(([level, message]) => level === "warn" && message.includes("--can-restart"))); }); - t2.test(".start() warns when system metrics are disabled", async t3 => { + t2.test(".fromContext() warns when system metrics are disabled", async t3 => { const { logs } = await startPlugin(t3, { config: { "controller.system_metrics_interval": 0 } }); t3.ok(logs.some(([level, message]) => level === "warn" && message.includes("system_metrics_interval"))); }); diff --git a/exp_groups/controller.ts b/exp_groups/controller.ts index 89f6bd1e..63c17413 100644 --- a/exp_groups/controller.ts +++ b/exp_groups/controller.ts @@ -6,81 +6,82 @@ import * as path from "node:path"; const loaded = new WeakMap(); export class ControllerPlugin { - controller: Controller; - logger: lib.Logger; - name: string; - groups!: lib.SubscribableDatastore; - roleMappings!: lib.SubscribableDatastore; - manualAssignments!: lib.SubscribableDatastore; - resolvedAssignments!: lib.SubscribableDatastore; + resolvedAssignments = new lib.SubscribableDatastore(); /** The plugin loaded on a controller, for the exp_scenario seed. */ static get(controller: Controller) { return loaded.get(controller); } - constructor(context: ControllerPluginContext) { - this.controller = context.controller; - this.logger = context.logger; - this.name = context.plugin.name; - } + private constructor( + public controller: Controller, + public logger: lib.Logger, + public name: string, + public groups: lib.SubscribableDatastore, + public roleMappings: lib.SubscribableDatastore, + public manualAssignments: lib.SubscribableDatastore, + ) {} - async init() { - loaded.set(this.controller, this); - const databaseDirectory = this.controller.config.get("controller.database_directory"); + static async fromContext(context: ControllerPluginContext) { + const controller = context.controller; + const databaseDirectory = controller.config.get("controller.database_directory"); - this.groups = new lib.SubscribableDatastore( + const groups = new lib.SubscribableDatastore( ...await new lib.JsonIdDatastoreProvider( path.join(databaseDirectory, "exp_groups", "groups.json"), messages.GroupRecord.fromJSON.bind(messages.GroupRecord), ).bootstrap() ); - this.roleMappings = new lib.SubscribableDatastore( + const roleMappings = new lib.SubscribableDatastore( ...await new lib.JsonIdDatastoreProvider( path.join(databaseDirectory, "exp_groups", "role_mappings.json"), messages.RoleMappingRecord.fromJSON.bind(messages.RoleMappingRecord), ).bootstrap() ); - this.manualAssignments = new lib.SubscribableDatastore( + const manualAssignments = new lib.SubscribableDatastore( ...await new lib.JsonIdDatastoreProvider( path.join(databaseDirectory, "exp_groups", "assignments.json"), messages.AssignmentRecord.fromJSON.bind(messages.AssignmentRecord), ).bootstrap() ); - this.resolvedAssignments = new lib.SubscribableDatastore(); + const plugin = new ControllerPlugin( + controller, context.logger, context.plugin.name, groups, roleMappings, manualAssignments, + ); + loaded.set(controller, plugin); - this.controller.subscriptions.handle(messages.GroupUpdatedEvent, this.handleGroupSubscription.bind(this)); - this.controller.subscriptions.handle(messages.RoleMappingUpdatedEvent, this.handleRoleMappingSubscription.bind(this)); - this.controller.subscriptions.handle(messages.ManualAssignmentUpdatedEvent, this.handleManualAssignmentSubscription.bind(this)); - this.controller.subscriptions.handle(messages.ResolvedAssignmentUpdatedEvent, this.handleResolvedAssignmentSubscription.bind(this)); + controller.subscriptions.handle(messages.GroupUpdatedEvent, plugin.handleGroupSubscription.bind(plugin)); + controller.subscriptions.handle(messages.RoleMappingUpdatedEvent, plugin.handleRoleMappingSubscription.bind(plugin)); + controller.subscriptions.handle(messages.ManualAssignmentUpdatedEvent, plugin.handleManualAssignmentSubscription.bind(plugin)); + controller.subscriptions.handle(messages.ResolvedAssignmentUpdatedEvent, plugin.handleResolvedAssignmentSubscription.bind(plugin)); - this.groups.on("update", this.groupsUpdated.bind(this)); - this.roleMappings.on("update", this.roleMappingsUpdated.bind(this)); - this.manualAssignments.on("update", this.manualAssignmentsUpdated.bind(this)); - this.resolvedAssignments.on("update", this.resolvedAssignmentsUpdated.bind(this)); + groups.on("update", plugin.groupsUpdated.bind(plugin)); + roleMappings.on("update", plugin.roleMappingsUpdated.bind(plugin)); + manualAssignments.on("update", plugin.manualAssignmentsUpdated.bind(plugin)); + plugin.resolvedAssignments.on("update", plugin.resolvedAssignmentsUpdated.bind(plugin)); - this.controller.handle(messages.GroupCreateRequest, this.handleGroupCreateRequest.bind(this)); - this.controller.handle(messages.GroupUpdateRequest, this.handleGroupUpdateRequest.bind(this)); - this.controller.handle(messages.GroupDeleteRequest, this.handleGroupDeleteRequest.bind(this)); - this.controller.handle(messages.GroupGetRequest, this.handleGroupGetRequest.bind(this)); - this.controller.handle(messages.GroupListRequest, this.handleGroupListRequest.bind(this)); + controller.handle(messages.GroupCreateRequest, plugin.handleGroupCreateRequest.bind(plugin)); + controller.handle(messages.GroupUpdateRequest, plugin.handleGroupUpdateRequest.bind(plugin)); + controller.handle(messages.GroupDeleteRequest, plugin.handleGroupDeleteRequest.bind(plugin)); + controller.handle(messages.GroupGetRequest, plugin.handleGroupGetRequest.bind(plugin)); + controller.handle(messages.GroupListRequest, plugin.handleGroupListRequest.bind(plugin)); - this.controller.handle(messages.AssignmentCreateRequest, this.handleAssignmentCreateRequest.bind(this)); - this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this)); - this.controller.handle(messages.AssignmentDeleteRequest, this.handleAssignmentDeleteRequest.bind(this)); - this.controller.handle(messages.AssignmentGetRequest, this.handleAssignmentGetRequest.bind(this)); - this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this)); + controller.handle(messages.AssignmentCreateRequest, plugin.handleAssignmentCreateRequest.bind(plugin)); + controller.handle(messages.AssignmentUpdateRequest, plugin.handleAssignmentUpdateRequest.bind(plugin)); + controller.handle(messages.AssignmentDeleteRequest, plugin.handleAssignmentDeleteRequest.bind(plugin)); + controller.handle(messages.AssignmentGetRequest, plugin.handleAssignmentGetRequest.bind(plugin)); + controller.handle(messages.AssignmentListRequest, plugin.handleAssignmentListRequest.bind(plugin)); - this.controller.handle(messages.RoleMappingCreateRequest, this.handleRoleMappingCreateRequest.bind(this)); - this.controller.handle(messages.RoleMappingUpdateRequest, this.handleRoleMappingUpdateRequest.bind(this)); - this.controller.handle(messages.RoleMappingDeleteRequest, this.handleRoleMappingDeleteRequest.bind(this)); - this.controller.handle(messages.RoleMappingGetRequest, this.handleRoleMappingGetRequest.bind(this)); - this.controller.handle(messages.RoleMappingListRequest, this.handleRoleMappingListRequest.bind(this)); + controller.handle(messages.RoleMappingCreateRequest, plugin.handleRoleMappingCreateRequest.bind(plugin)); + controller.handle(messages.RoleMappingUpdateRequest, plugin.handleRoleMappingUpdateRequest.bind(plugin)); + controller.handle(messages.RoleMappingDeleteRequest, plugin.handleRoleMappingDeleteRequest.bind(plugin)); + controller.handle(messages.RoleMappingGetRequest, plugin.handleRoleMappingGetRequest.bind(plugin)); + controller.handle(messages.RoleMappingListRequest, plugin.handleRoleMappingListRequest.bind(plugin)); - this.controller.hooks.shutdown.attach(this.name, this.onShutdown.bind(this)); + controller.hooks.shutdown.attach(plugin.name, plugin.onShutdown.bind(plugin)); + return plugin; } async onShutdown() { @@ -453,5 +454,5 @@ export class ControllerPlugin { } export default async function (context: ControllerPluginContext) { - await new ControllerPlugin(context).init(); + await ControllerPlugin.fromContext(context); } diff --git a/exp_groups/instance.ts b/exp_groups/instance.ts index 594f025d..88e97769 100644 --- a/exp_groups/instance.ts +++ b/exp_groups/instance.ts @@ -18,9 +18,6 @@ export type IpcPlayerAssignments = { }; export class InstancePlugin { - instance: Instance; - logger: lib.Logger; - name: string; // Once only, don't send permissions for these groups // This is used for groups created on this instance that only need the controller generated id skipSendingPermissions = new Set(); @@ -29,23 +26,27 @@ export class InstancePlugin { // Track known online players so that we only apply assignment updates for them onlinePlayers = new Set(); - constructor(context: InstancePluginContext) { - this.instance = context.instance; - this.logger = context.logger; - this.name = context.plugin.name; - } + private constructor( + public instance: Instance, + public logger: lib.Logger, + public name: string, + ) {} - async init() { - this.instance.handle(messages.GroupUpdatedEvent, this.handleGroupUpdatedEvent.bind(this)); - this.instance.handle(messages.ResolvedAssignmentUpdatedEvent, this.handleResolvedAssignmentUpdatedEvent.bind(this)); - this.instance.server.handle(`exp_group:group_updated`, this.handleGroupUpdatedIPC.bind(this)) - this.instance.server.handle(`exp_group:group_deleted`, this.handleGroupDeletedIPC.bind(this)) - this.instance.server.handle(`exp_group:player_assignments`, this.handlePlayerAssignmentsIPC.bind(this)) + static async fromContext(context: InstancePluginContext) { + const instance = context.instance; + const plugin = new InstancePlugin(instance, context.logger, context.plugin.name); - const hooks = this.instance.hooks; - hooks.instanceConfigFieldChanged.attach(this.name, this.onInstanceConfigFieldChanged.bind(this)); - hooks.start.attach(this.name, this.onStart.bind(this)); - hooks.playerEvent.attach(this.name, this.onPlayerEvent.bind(this)); + instance.handle(messages.GroupUpdatedEvent, plugin.handleGroupUpdatedEvent.bind(plugin)); + instance.handle(messages.ResolvedAssignmentUpdatedEvent, plugin.handleResolvedAssignmentUpdatedEvent.bind(plugin)); + instance.server.handle(`exp_group:group_updated`, plugin.handleGroupUpdatedIPC.bind(plugin)) + instance.server.handle(`exp_group:group_deleted`, plugin.handleGroupDeletedIPC.bind(plugin)) + instance.server.handle(`exp_group:player_assignments`, plugin.handlePlayerAssignmentsIPC.bind(plugin)) + + const hooks = instance.hooks; + hooks.instanceConfigFieldChanged.attach(plugin.name, plugin.onInstanceConfigFieldChanged.bind(plugin)); + hooks.start.attach(plugin.name, plugin.onStart.bind(plugin)); + hooks.playerEvent.attach(plugin.name, plugin.onPlayerEvent.bind(plugin)); + return plugin; } async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) { @@ -185,5 +186,5 @@ export class InstancePlugin { } export default async function (context: InstancePluginContext) { - await new InstancePlugin(context).init(); + await InstancePlugin.fromContext(context); } diff --git a/exp_reports/controller.ts b/exp_reports/controller.ts index 53c5adb3..f8b6ca59 100644 --- a/exp_reports/controller.ts +++ b/exp_reports/controller.ts @@ -4,36 +4,36 @@ import * as messages from "./messages.js"; import * as path from "node:path"; export class ControllerPlugin { - controller: Controller; - logger: lib.Logger; - name: string; - reports!: lib.SubscribableDatastore; + private constructor( + public controller: Controller, + public logger: lib.Logger, + public name: string, + public reports: lib.SubscribableDatastore, + ) {} - constructor(context: ControllerPluginContext) { - this.controller = context.controller; - this.logger = context.logger; - this.name = context.plugin.name; - } + static async fromContext(context: ControllerPluginContext) { + const controller = context.controller; + const databaseDirectory = controller.config.get("controller.database_directory"); - async init() { - const databaseDirectory = this.controller.config.get("controller.database_directory"); - - this.reports = new lib.SubscribableDatastore( + const reports = new lib.SubscribableDatastore( ...await new lib.JsonIdDatastoreProvider( path.join(databaseDirectory, "exp_reports", "reports.json"), messages.ReportRecord.fromJSON.bind(messages.ReportRecord), ).bootstrap() ); - this.controller.subscriptions.handle(messages.ReportUpdatedEvent, this.handleReportSubscription.bind(this)); - this.reports.on("update", this.reportsUpdated.bind(this)); + const plugin = new ControllerPlugin(controller, context.logger, context.plugin.name, reports); - this.controller.handle(messages.ReportListRequest, this.handleReportListRequest.bind(this)); - this.controller.handle(messages.ReportGetRequest, this.handleReportGetRequest.bind(this)); - this.controller.handle(messages.ReportCreateRequest, this.handleReportCreateRequest.bind(this)); - this.controller.handle(messages.ReportDeleteRequest, this.handleReportDeleteRequest.bind(this)); + controller.subscriptions.handle(messages.ReportUpdatedEvent, plugin.handleReportSubscription.bind(plugin)); + reports.on("update", plugin.reportsUpdated.bind(plugin)); - this.controller.hooks.shutdown.attach(this.name, this.onShutdown.bind(this)); + controller.handle(messages.ReportListRequest, plugin.handleReportListRequest.bind(plugin)); + controller.handle(messages.ReportGetRequest, plugin.handleReportGetRequest.bind(plugin)); + controller.handle(messages.ReportCreateRequest, plugin.handleReportCreateRequest.bind(plugin)); + controller.handle(messages.ReportDeleteRequest, plugin.handleReportDeleteRequest.bind(plugin)); + + controller.hooks.shutdown.attach(plugin.name, plugin.onShutdown.bind(plugin)); + return plugin; } async onShutdown() { @@ -154,5 +154,5 @@ export class ControllerPlugin { } export default async function (context: ControllerPluginContext) { - await new ControllerPlugin(context).init(); + await ControllerPlugin.fromContext(context); } diff --git a/exp_reports/instance.ts b/exp_reports/instance.ts index e5ae3a43..71c90c2b 100644 --- a/exp_reports/instance.ts +++ b/exp_reports/instance.ts @@ -30,20 +30,20 @@ export type IpcReportDelete = { * stopped by then, in which case the answer is dropped. */ export class InstancePlugin { - instance: Instance; - logger: lib.Logger; - name: string; + private constructor( + public instance: Instance, + public logger: lib.Logger, + public name: string, + ) {} - constructor(context: InstancePluginContext) { - this.instance = context.instance; - this.logger = context.logger; - this.name = context.plugin.name; - } + static async fromContext(context: InstancePluginContext) { + const instance = context.instance; + const plugin = new InstancePlugin(instance, context.logger, context.plugin.name); - async init() { - this.instance.server.handle("exp_reports:create", this.handleCreateIPC.bind(this)); - this.instance.server.handle("exp_reports:list", this.handleListIPC.bind(this)); - this.instance.server.handle("exp_reports:delete", this.handleDeleteIPC.bind(this)); + instance.server.handle("exp_reports:create", plugin.handleCreateIPC.bind(plugin)); + instance.server.handle("exp_reports:list", plugin.handleListIPC.bind(plugin)); + instance.server.handle("exp_reports:delete", plugin.handleDeleteIPC.bind(plugin)); + return plugin; } async handleCreateIPC(event: IpcReportCreate) { @@ -106,5 +106,5 @@ export class InstancePlugin { } export default async function (context: InstancePluginContext) { - await new InstancePlugin(context).init(); + await InstancePlugin.fromContext(context); } diff --git a/exp_reports/test/controller.test.js b/exp_reports/test/controller.test.js index b2dec669..7581cb42 100644 --- a/exp_reports/test/controller.test.js +++ b/exp_reports/test/controller.test.js @@ -46,10 +46,9 @@ async function startPlugin(t2, { config = {} } = {}) { broadcast(event); }; - const plugin = new ControllerPlugin({ plugin: { name: "exp_reports" }, controller, logger }); - plugin.logger = { ...logger, warn: message => state.warnings.push(message) }; + const pluginLogger = { ...logger, warn: message => state.warnings.push(message) }; + const plugin = await ControllerPlugin.fromContext({ plugin: { name: "exp_reports" }, controller, logger: pluginLogger }); plugin.postWebhook = async (url, body) => { state.posts.push({ url, body }); }; - await plugin.init(); return { plugin, controller, state }; } diff --git a/exp_reports/test/instance.test.js b/exp_reports/test/instance.test.js index e579dcec..4cfcafa5 100644 --- a/exp_reports/test/instance.test.js +++ b/exp_reports/test/instance.test.js @@ -63,8 +63,7 @@ async function startPlugin(t2, { reports = [report(1, "bob", "alice"), report(2, instance.notifyStatus("running"); state.sent.length = 0; - const plugin = new InstancePlugin({ plugin: { name: "exp_reports" }, instance, host: {}, logger }); - await plugin.init(); + const plugin = await InstancePlugin.fromContext({ plugin: { name: "exp_reports" }, instance, host: {}, logger }); return { plugin, instance, state }; } diff --git a/exp_roles/controller.ts b/exp_roles/controller.ts index 2299de23..f2ed29f3 100644 --- a/exp_roles/controller.ts +++ b/exp_roles/controller.ts @@ -6,59 +6,59 @@ import * as path from "node:path"; const loaded = new WeakMap(); export class ControllerPlugin { - controller: Controller; - logger: lib.Logger; - name: string; - roleMeta!: lib.SubscribableDatastore; - /** The plugin loaded on a controller, for the exp_scenario seed. */ static get(controller: Controller) { return loaded.get(controller); } - constructor(context: ControllerPluginContext) { - this.controller = context.controller; - this.logger = context.logger; - this.name = context.plugin.name; - } + private constructor( + public controller: Controller, + public logger: lib.Logger, + public name: string, + public roleMeta: lib.SubscribableDatastore, + ) {} - async init() { - loaded.set(this.controller, this); - const databaseDirectory = this.controller.config.get("controller.database_directory"); + static async fromContext(context: ControllerPluginContext) { + const controller = context.controller; + const databaseDirectory = controller.config.get("controller.database_directory"); - this.roleMeta = new lib.SubscribableDatastore( + const roleMeta = new lib.SubscribableDatastore( ...await new lib.JsonIdDatastoreProvider( path.join(databaseDirectory, "exp_roles", "role_meta.json"), messages.RoleMetaRecord.fromJSON.bind(messages.RoleMetaRecord), ).bootstrap() ); + const plugin = new ControllerPlugin(controller, context.logger, context.plugin.name, roleMeta); + loaded.set(controller, plugin); + // The datastore can be out of step with the roles, either because the // plugin was installed after they were created or because it was // uninstalled while they were deleted - this.ensureRoleMeta(); - this.sweepRoleMeta(); - this.applyAutoAssign(); + plugin.ensureRoleMeta(); + plugin.sweepRoleMeta(); + plugin.applyAutoAssign(); - this.controller.subscriptions.handle(messages.RoleUpdatedEvent, this.handleRoleSubscription.bind(this)); - this.controller.subscriptions.handle( - messages.AssignmentUpdatedEvent, this.handleAssignmentSubscription.bind(this) + controller.subscriptions.handle(messages.RoleUpdatedEvent, plugin.handleRoleSubscription.bind(plugin)); + controller.subscriptions.handle( + messages.AssignmentUpdatedEvent, plugin.handleAssignmentSubscription.bind(plugin) ); - this.roleMeta.on("update", this.roleMetaUpdated.bind(this)); - this.controller.roles.on("update", this.rolesUpdated.bind(this)); - this.controller.users.records.on("update", this.usersUpdated.bind(this)); + roleMeta.on("update", plugin.roleMetaUpdated.bind(plugin)); + controller.roles.on("update", plugin.rolesUpdated.bind(plugin)); + controller.users.records.on("update", plugin.usersUpdated.bind(plugin)); - this.controller.handle(messages.RoleListRequest, this.handleRoleListRequest.bind(this)); - this.controller.handle(messages.RoleMetaUpdateRequest, this.handleRoleMetaUpdateRequest.bind(this)); + controller.handle(messages.RoleListRequest, plugin.handleRoleListRequest.bind(plugin)); + controller.handle(messages.RoleMetaUpdateRequest, plugin.handleRoleMetaUpdateRequest.bind(plugin)); - this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this)); - this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this)); + controller.handle(messages.AssignmentListRequest, plugin.handleAssignmentListRequest.bind(plugin)); + controller.handle(messages.AssignmentUpdateRequest, plugin.handleAssignmentUpdateRequest.bind(plugin)); - const hooks = this.controller.hooks; - hooks.shutdown.attach(this.name, this.onShutdown.bind(this)); - hooks.controllerConfigFieldChanged.attach(this.name, this.onControllerConfigFieldChanged.bind(this)); - hooks.playerEvent.attach(this.name, this.onPlayerEvent.bind(this)); + const hooks = controller.hooks; + hooks.shutdown.attach(plugin.name, plugin.onShutdown.bind(plugin)); + hooks.controllerConfigFieldChanged.attach(plugin.name, plugin.onControllerConfigFieldChanged.bind(plugin)); + hooks.playerEvent.attach(plugin.name, plugin.onPlayerEvent.bind(plugin)); + return plugin; } async onShutdown() { @@ -335,5 +335,5 @@ export class ControllerPlugin { } export default async function (context: ControllerPluginContext) { - await new ControllerPlugin(context).init(); + await ControllerPlugin.fromContext(context); } diff --git a/exp_roles/instance.ts b/exp_roles/instance.ts index 488613d6..8cb87e22 100644 --- a/exp_roles/instance.ts +++ b/exp_roles/instance.ts @@ -10,24 +10,24 @@ export type IpcAssignmentUpdate = { }; export class InstancePlugin { - instance: Instance; - logger: lib.Logger; - name: string; + private constructor( + public instance: Instance, + public logger: lib.Logger, + public name: string, + ) {} - constructor(context: InstancePluginContext) { - this.instance = context.instance; - this.logger = context.logger; - this.name = context.plugin.name; - } + static async fromContext(context: InstancePluginContext) { + const instance = context.instance; + const plugin = new InstancePlugin(instance, context.logger, context.plugin.name); - async init() { - this.instance.handle(messages.RoleUpdatedEvent, this.handleRoleUpdatedEvent.bind(this)); - this.instance.handle(messages.AssignmentUpdatedEvent, this.handleAssignmentUpdatedEvent.bind(this)); - this.instance.server.handle("exp_roles:assignment_update", this.handleAssignmentUpdateIPC.bind(this)); + instance.handle(messages.RoleUpdatedEvent, plugin.handleRoleUpdatedEvent.bind(plugin)); + instance.handle(messages.AssignmentUpdatedEvent, plugin.handleAssignmentUpdatedEvent.bind(plugin)); + instance.server.handle("exp_roles:assignment_update", plugin.handleAssignmentUpdateIPC.bind(plugin)); - const hooks = this.instance.hooks; - hooks.instanceConfigFieldChanged.attach(this.name, this.onInstanceConfigFieldChanged.bind(this)); - hooks.start.attach(this.name, this.onStart.bind(this)); + const hooks = instance.hooks; + hooks.instanceConfigFieldChanged.attach(plugin.name, plugin.onInstanceConfigFieldChanged.bind(plugin)); + hooks.start.attach(plugin.name, plugin.onStart.bind(plugin)); + return plugin; } get syncMode() { @@ -122,5 +122,5 @@ export class InstancePlugin { } export default async function (context: InstancePluginContext) { - await new InstancePlugin(context).init(); + await InstancePlugin.fromContext(context); } diff --git a/exp_roles/test/controller.test.js b/exp_roles/test/controller.test.js index 1231d093..652dca56 100644 --- a/exp_roles/test/controller.test.js +++ b/exp_roles/test/controller.test.js @@ -44,8 +44,7 @@ async function startPlugin(t2, { roles = [] } = {}) { permissionsUpdated(user); }; - const plugin = new ControllerPlugin({ plugin: { name: "exp_roles" }, controller, logger }); - await plugin.init(); + const plugin = await ControllerPlugin.fromContext({ plugin: { name: "exp_roles" }, controller, logger }); return { plugin, controller, state }; } @@ -62,7 +61,7 @@ t.test("entrypoint", async t2 => { }); t.test("class ControllerPlugin", t2 => { - t2.test(".init() and .sweepRoleMeta() manage the role properties", async t3 => { + t2.test(".fromContext() and .sweepRoleMeta() manage the role properties", async t3 => { const { plugin } = await startPlugin(t3, { roles: [role(0, "Cluster Admin"), role(5, "Moderator")] }); t3.strictSame(plugin.roleMeta.get(0).order, 1, "the first role is ordered first"); diff --git a/exp_roles/test/instance.test.js b/exp_roles/test/instance.test.js index 302e8528..5921adc7 100644 --- a/exp_roles/test/instance.test.js +++ b/exp_roles/test/instance.test.js @@ -69,9 +69,8 @@ async function startPlugin(t2, { syncMode = "bidirectional", roles = sampleRoles return undefined; }; - const plugin = new InstancePlugin({ plugin: { name: "exp_roles" }, instance, host: {}, logger }); - plugin.logger = { ...logger, warn: message => state.warnings.push(message) }; - await plugin.init(); + const pluginLogger = { ...logger, warn: message => state.warnings.push(message) }; + const plugin = await InstancePlugin.fromContext({ plugin: { name: "exp_roles" }, instance, host: {}, logger: pluginLogger }); return { plugin, state }; } diff --git a/exp_scenario/controller.ts b/exp_scenario/controller.ts index 7c7547c4..59557a5b 100644 --- a/exp_scenario/controller.ts +++ b/exp_scenario/controller.ts @@ -10,36 +10,35 @@ import { features, pruneFeatureValues, validateFeatureValues } from "./features. import { SeedRole, SeedGroup, seedRoles, seedGroups, flattenSeedPermissions } from "./seed.js"; export class ControllerPlugin { - controller: Controller; - logger: lib.Logger; - name: string; - features!: lib.SubscribableDatastore; + private constructor( + public controller: Controller, + public logger: lib.Logger, + public name: string, + public features: lib.SubscribableDatastore, + ) {} - constructor(context: ControllerPluginContext) { - this.controller = context.controller; - this.logger = context.logger; - this.name = context.plugin.name; - } - - async init() { - const databaseDirectory = this.controller.config.get("controller.database_directory"); - this.features = new lib.SubscribableDatastore( + static async fromContext(context: ControllerPluginContext) { + const controller = context.controller; + const databaseDirectory = controller.config.get("controller.database_directory"); + const features = new lib.SubscribableDatastore( ...await new lib.JsonIdDatastoreProvider( path.join(databaseDirectory, "exp_scenario", "features.json"), messages.FeatureRecord.fromJSON.bind(messages.FeatureRecord), ).bootstrap() ); - this.reconcileFeatures(); + const plugin = new ControllerPlugin(controller, context.logger, context.plugin.name, features); + plugin.reconcileFeatures(); - this.controller.subscriptions.handle(messages.FeatureUpdatedEvent, this.handleFeatureSubscription.bind(this)); - this.features.on("update", this.featuresUpdated.bind(this)); + controller.subscriptions.handle(messages.FeatureUpdatedEvent, plugin.handleFeatureSubscription.bind(plugin)); + features.on("update", plugin.featuresUpdated.bind(plugin)); - this.controller.handle(messages.SeedRequest, this.handleSeedRequest.bind(this)); - this.controller.handle(messages.FeatureListRequest, this.handleFeatureListRequest.bind(this)); - this.controller.handle(messages.FeatureUpdateRequest, this.handleFeatureUpdateRequest.bind(this)); + controller.handle(messages.SeedRequest, plugin.handleSeedRequest.bind(plugin)); + controller.handle(messages.FeatureListRequest, plugin.handleFeatureListRequest.bind(plugin)); + controller.handle(messages.FeatureUpdateRequest, plugin.handleFeatureUpdateRequest.bind(plugin)); - this.controller.hooks.shutdown.attach(this.name, this.onShutdown.bind(this)); + controller.hooks.shutdown.attach(plugin.name, plugin.onShutdown.bind(plugin)); + return plugin; } async onShutdown() { @@ -233,7 +232,7 @@ export class ControllerPlugin { } export default async function (context: ControllerPluginContext) { - await new ControllerPlugin(context).init(); + await ControllerPlugin.fromContext(context); } function newId(datastore: { has(id: number): boolean }) { diff --git a/exp_scenario/instance.ts b/exp_scenario/instance.ts index 7a0f9f5a..0e6a9da8 100644 --- a/exp_scenario/instance.ts +++ b/exp_scenario/instance.ts @@ -5,23 +5,24 @@ import { features, FeatureValue } from "./features.js"; /** Sends feature config to the lua side, see module/features.lua. */ export class InstancePlugin { - instance: Instance; - logger: lib.Logger; - name: string; /** Rcon is up and the lua side can take updates */ started = false; - constructor(context: InstancePluginContext) { - this.instance = context.instance; - this.logger = context.logger; - this.name = context.plugin.name; - } + private constructor( + public instance: Instance, + public logger: lib.Logger, + public name: string, + ) {} - async init() { - this.instance.handle(messages.FeatureUpdatedEvent, this.handleFeatureUpdatedEvent.bind(this)); - this.instance.hooks.start.attach(this.name, this.onStart.bind(this)); - this.instance.hooks.exit.attach(this.name, this.onExit.bind(this)); - this.instance.hooks.controllerConnectionEvent.attach(this.name, this.onControllerConnectionEvent.bind(this)); + static async fromContext(context: InstancePluginContext) { + const instance = context.instance; + const plugin = new InstancePlugin(instance, context.logger, context.plugin.name); + + instance.handle(messages.FeatureUpdatedEvent, plugin.handleFeatureUpdatedEvent.bind(plugin)); + instance.hooks.start.attach(plugin.name, plugin.onStart.bind(plugin)); + instance.hooks.exit.attach(plugin.name, plugin.onExit.bind(plugin)); + instance.hooks.controllerConnectionEvent.attach(plugin.name, plugin.onControllerConnectionEvent.bind(plugin)); + return plugin; } async onStart() { @@ -81,5 +82,5 @@ export class InstancePlugin { } export default async function (context: InstancePluginContext) { - await new InstancePlugin(context).init(); + await InstancePlugin.fromContext(context); } diff --git a/exp_scenario/test/controller.test.js b/exp_scenario/test/controller.test.js index 3c9916b0..ba3d8c41 100644 --- a/exp_scenario/test/controller.test.js +++ b/exp_scenario/test/controller.test.js @@ -41,13 +41,11 @@ async function startPlugin(t2, { withPlugins = true, databaseDirectory = t2.test if (withPlugins) { for (const [name, Plugin] of [["exp_roles", RolesPlugin], ["exp_groups", GroupsPlugin]]) { - const plugin = new Plugin({ plugin: { name }, controller, logger }); - await plugin.init(); + await Plugin.fromContext({ plugin: { name }, controller, logger }); } } - const plugin = new ControllerPlugin({ plugin: { name: "exp_scenario" }, controller, logger }); - await plugin.init(); + const plugin = await ControllerPlugin.fromContext({ plugin: { name: "exp_scenario" }, controller, logger }); return { plugin, controller }; } @@ -136,7 +134,7 @@ t.test("class ControllerPlugin", t2 => { t3.notOk([...first.values()].filter(priority => priority === 2).length > 1, "seed priorities skip taken ones"); }); - t2.test(".init() adds every feature with its defaults", async t3 => { + t2.test(".fromContext() adds every feature with its defaults", async t3 => { const { plugin } = await startPlugin(t3, { withPlugins: false }); const records = await plugin.handleFeatureListRequest(); t3.strictSame(records.map(record => record.id), features.map(feature => feature.name), "one record per feature"); @@ -181,7 +179,7 @@ t.test("class ControllerPlugin", t2 => { ); }); - t2.test(".init() keeps stored features across restarts", async t3 => { + t2.test(".fromContext() keeps stored features across restarts", async t3 => { const databaseDirectory = t3.testdir(); const { plugin } = await startPlugin(t3, { withPlugins: false, databaseDirectory }); await plugin.handleFeatureUpdateRequest(new messages.FeatureUpdateRequest("afk_kick", false, { afk_minutes: 5 })); diff --git a/exp_scenario/test/instance.test.js b/exp_scenario/test/instance.test.js index 31beee6d..22f59639 100644 --- a/exp_scenario/test/instance.test.js +++ b/exp_scenario/test/instance.test.js @@ -47,8 +47,7 @@ async function startPlugin(t2, features = [new messages.FeatureRecord("death_mar return request instanceof messages.FeatureListRequest ? features : undefined; }; - const plugin = new InstancePlugin({ plugin: { name: "exp_scenario" }, instance, host: {}, logger }); - await plugin.init(); + const plugin = await InstancePlugin.fromContext({ plugin: { name: "exp_scenario" }, instance, host: {}, logger }); return { plugin, instance, state }; } diff --git a/exp_server_ups/instance.ts b/exp_server_ups/instance.ts index 4ae128e6..b9a04207 100644 --- a/exp_server_ups/instance.ts +++ b/exp_server_ups/instance.ts @@ -2,24 +2,25 @@ import * as lib from "@clusterio/lib"; import type { Instance, InstancePluginContext } from "@clusterio/host"; export class InstancePlugin { - instance: Instance; - logger: lib.Logger; - name: string; private updateInterval?: ReturnType; private gameTimes: number[] = []; - constructor(context: InstancePluginContext) { - this.instance = context.instance; - this.logger = context.logger; - this.name = context.plugin.name; - } + private constructor( + public instance: Instance, + public logger: lib.Logger, + public name: string, + ) {} - init() { - const hooks = this.instance.hooks; - hooks.start.attach(this.name, this.onStart.bind(this)); - hooks.exit.attach(this.name, this.onExit.bind(this)); - hooks.instanceConfigFieldChanged.attach(this.name, this.onInstanceConfigFieldChanged.bind(this)); - hooks.playerEvent.attach(this.name, this.onPlayerEvent.bind(this)); + static async fromContext(context: InstancePluginContext) { + const instance = context.instance; + const plugin = new InstancePlugin(instance, context.logger, context.plugin.name); + + const hooks = instance.hooks; + hooks.start.attach(plugin.name, plugin.onStart.bind(plugin)); + hooks.exit.attach(plugin.name, plugin.onExit.bind(plugin)); + hooks.instanceConfigFieldChanged.attach(plugin.name, plugin.onInstanceConfigFieldChanged.bind(plugin)); + hooks.playerEvent.attach(plugin.name, plugin.onPlayerEvent.bind(plugin)); + return plugin; } async onStart() { @@ -86,5 +87,5 @@ export class InstancePlugin { } export default async function (context: InstancePluginContext) { - new InstancePlugin(context).init(); + await InstancePlugin.fromContext(context); }