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:
BastiaanandClaude Opus 5.5 authored and GitHub committed 2026-10-05 01:19:53 +01:00
1 parent 757a36fc03
commit ea9bf1fb78
17 files changed
+236 -241

No files matched your search

+45 -44
View File
@@ -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
View File
@@ -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);
}