mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-11 15:54:52 +09:00
Construct plugins with a static fromContext (#487)
Each plugin class now has a private constructor and a static async fromContext(context) that loads its datastores, constructs the instance and attaches its handlers and hooks. This replaces the public constructor followed by init(), and AutoRestart's start(). Datastores are loaded before construction and passed to the constructor, so the definite assignment assertions on them are gone. Tests build the plugins through fromContext and pass a wrapped logger in the context instead of overwriting plugin.logger before init(). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
757a36fc03
commit
ea9bf1fb78
17 files changed
+236
-241
No files matched your search
@@ -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<number | "cluster", number>();
|
||||
/** 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<typeof setInterval>;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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")));
|
||||
});
|
||||
|
||||
+45
-44
@@ -6,81 +6,82 @@ import * as path from "node:path";
|
||||
const loaded = new WeakMap<Controller, ControllerPlugin>();
|
||||
|
||||
export class ControllerPlugin {
|
||||
controller: Controller;
|
||||
logger: lib.Logger;
|
||||
name: string;
|
||||
groups!: lib.SubscribableDatastore<messages.GroupRecord>;
|
||||
roleMappings!: lib.SubscribableDatastore<messages.RoleMappingRecord>;
|
||||
manualAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
|
||||
resolvedAssignments!: lib.SubscribableDatastore<messages.AssignmentRecord>;
|
||||
resolvedAssignments = new lib.SubscribableDatastore<messages.AssignmentRecord>();
|
||||
|
||||
/** 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<messages.GroupRecord>,
|
||||
public roleMappings: lib.SubscribableDatastore<messages.RoleMappingRecord>,
|
||||
public manualAssignments: lib.SubscribableDatastore<messages.AssignmentRecord>,
|
||||
) {}
|
||||
|
||||
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);
|
||||
}
|
||||
+20
-19
@@ -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<string>();
|
||||
@@ -29,23 +26,27 @@ export class InstancePlugin {
|
||||
// Track known online players so that we only apply assignment updates for them
|
||||
onlinePlayers = new Set<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
+21
-21
@@ -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<messages.ReportRecord>;
|
||||
private constructor(
|
||||
public controller: Controller,
|
||||
public logger: lib.Logger,
|
||||
public name: string,
|
||||
public reports: lib.SubscribableDatastore<messages.ReportRecord>,
|
||||
) {}
|
||||
|
||||
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);
|
||||
}
|
||||
+13
-13
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
+32
-32
@@ -6,59 +6,59 @@ import * as path from "node:path";
|
||||
const loaded = new WeakMap<Controller, ControllerPlugin>();
|
||||
|
||||
export class ControllerPlugin {
|
||||
controller: Controller;
|
||||
logger: lib.Logger;
|
||||
name: string;
|
||||
roleMeta!: lib.SubscribableDatastore<messages.RoleMetaRecord>;
|
||||
|
||||
/** 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<messages.RoleMetaRecord>,
|
||||
) {}
|
||||
|
||||
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);
|
||||
}
|
||||
+16
-16
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
+20
-21
@@ -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<messages.FeatureRecord>;
|
||||
private constructor(
|
||||
public controller: Controller,
|
||||
public logger: lib.Logger,
|
||||
public name: string,
|
||||
public features: lib.SubscribableDatastore<messages.FeatureRecord>,
|
||||
) {}
|
||||
|
||||
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 }) {
|
||||
|
||||
+15
-14
@@ -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);
|
||||
}
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
+16
-15
@@ -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<typeof setInterval>;
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user