From 7f8a700716671b270fb6824dcca714f9e02572bb Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 3 Jun 2023 04:07:07 +0200 Subject: [PATCH] Refactor message registration and validation Split up the action of registring an event or request data class to be decoded by the Link class and the action of registring a handler for those requests and events. This moves the validation work from Link instantiation time to startup initialisation which makes logic errors fail on startup instead of failing instantiation of the link when a connection attempt is made. Additionally implement a partial validation logic for messages received using the newly refactored message registration. This unfortunately ended up merged with the registration refactor. --- packages/controller/controller.js | 2 + packages/controller/src/BaseConnection.js | 8 +- packages/controller/src/ControlConnection.js | 104 ++--- packages/controller/src/Controller.js | 23 +- packages/controller/src/HostConnection.js | 10 +- packages/ctl/ctl.js | 5 +- packages/host/host.js | 1 + packages/host/src/Host.js | 22 +- packages/host/src/Instance.js | 30 +- packages/host/src/InstanceConnection.js | 2 +- packages/lib/data/messages_controller.js | 4 +- packages/lib/data/messages_core.js | 2 +- packages/lib/data/messages_instance.js | 18 +- packages/lib/data/messages_user.js | 20 +- packages/lib/link/connectors.js | 10 +- packages/lib/link/index.js | 2 + packages/lib/link/link.js | 384 +++++++++++++------ packages/lib/link/messages.js | 111 ++++++ packages/lib/plugin_loader.js | 7 + packages/web_ui/src/bootstrap.jsx | 3 +- packages/web_ui/src/util/websocket.jsx | 18 +- plugins/global_chat/info.js | 5 + plugins/global_chat/instance.js | 2 +- plugins/global_chat/test/plugin.js | 2 + plugins/inventory_sync/controller.js | 10 +- plugins/inventory_sync/info.js | 8 + plugins/player_auth/controller.js | 4 +- plugins/player_auth/info.js | 6 + plugins/research_sync/controller.js | 6 +- plugins/research_sync/info.js | 8 + plugins/research_sync/instance.js | 4 +- plugins/research_sync/messages.js | 10 +- plugins/subspace_storage/controller.js | 8 +- plugins/subspace_storage/info.js | 8 + plugins/subspace_storage/instance.js | 2 +- test/integration/index.js | 14 +- test/lib/command.js | 13 +- test/lib/link/link.js | 81 ++-- test/mock.js | 3 +- 39 files changed, 648 insertions(+), 332 deletions(-) create mode 100644 packages/lib/link/messages.js diff --git a/packages/controller/controller.js b/packages/controller/controller.js index 86f7cedd..dc9c1365 100755 --- a/packages/controller/controller.js +++ b/packages/controller/controller.js @@ -28,6 +28,7 @@ const jwt = require("jsonwebtoken"); // homebrew modules const libErrors = require("@clusterio/lib/errors"); const libFileOps = require("@clusterio/lib/file_ops"); +const libLink = require("@clusterio/lib/link"); const libPluginLoader = require("@clusterio/lib/plugin_loader"); const libPrometheus = require("@clusterio/lib/prometheus"); const libConfig = require("@clusterio/lib/config"); @@ -290,6 +291,7 @@ async function initialize() { logger.info("Loading Plugin info"); parameters.pluginInfos = await libPluginLoader.loadPluginInfos(pluginList); + libLink.registerPluginMessages(parameters.pluginInfos); libConfig.registerPluginConfigGroups(parameters.pluginInfos); libConfig.finalizeConfigs(); diff --git a/packages/controller/src/BaseConnection.js b/packages/controller/src/BaseConnection.js index fa0e46ba..40fddbba 100644 --- a/packages/controller/src/BaseConnection.js +++ b/packages/controller/src/BaseConnection.js @@ -19,13 +19,13 @@ class BaseConnection extends libLink.Link { super(connector); this.router = new ControllerRouter(controller); this._controller = controller; - for (let [Request, handler] of controller._registeredRequests) { this.register(Request, handler); } + for (let [Request, handler] of controller._registeredRequests) { this.handle(Request, handler); } for (let [Request, handler] of controller._fallbackedRequests) { this.fallbackRequest(Request, handler); } - for (let [Event, handler] of controller._registeredEvents) { this.register(Event, handler); } + for (let [Event, handler] of controller._registeredEvents) { this.handle(Event, handler); } for (let [Event, handler] of controller._snoopedEvents) { this.snoopEvent(Event, handler); } - this.register(libData.ModPackGetRequest, this.handleModPackGetRequest.bind(this)); - this.register(libData.ModPackGetDefaultRequest, this.handleModPackGetDefaultRequest.bind(this)); + this.handle(libData.ModPackGetRequest, this.handleModPackGetRequest.bind(this)); + this.handle(libData.ModPackGetDefaultRequest, this.handleModPackGetDefaultRequest.bind(this)); } async disconnect(code, reason) { diff --git a/packages/controller/src/ControlConnection.js b/packages/controller/src/ControlConnection.js index 776ae636..d65350c5 100644 --- a/packages/controller/src/ControlConnection.js +++ b/packages/controller/src/ControlConnection.js @@ -104,69 +104,69 @@ class ControlConnection extends BaseConnection { }); } - this.register(libData.ControllerConfigGetRequest, this.handleControllerConfigGetRequest.bind(this)); - this.register(libData.ControllerConfigSetFieldRequest, this.handleControllerConfigSetFieldRequest.bind(this)); - this.register(libData.ControllerConfigSetPropRequest, this.handleControllerConfigSetPropRequest.bind(this)); - this.register(libData.HostListRequest, this.handleHostListRequest.bind(this)); - this.register(libData.HostSetSubscriptionsRequest, this.handleHostSetSubscriptionsRequest.bind(this)); - this.register(libData.HostGenerateTokenRequest, this.handleHostGenerateTokenRequest.bind(this)); - this.register(libData.HostConfigCreateRequest, this.handleHostConfigCreateRequest.bind(this)); - this.register(libData.InstanceDetailsGetRequest, this.handleInstanceDetailsGetRequest.bind(this)); - this.register(libData.InstanceDetailsListRequest, this.handleInstanceDetailsListRequest.bind(this)); - this.register( + this.handle(libData.ControllerConfigGetRequest, this.handleControllerConfigGetRequest.bind(this)); + this.handle(libData.ControllerConfigSetFieldRequest, this.handleControllerConfigSetFieldRequest.bind(this)); + this.handle(libData.ControllerConfigSetPropRequest, this.handleControllerConfigSetPropRequest.bind(this)); + this.handle(libData.HostListRequest, this.handleHostListRequest.bind(this)); + this.handle(libData.HostSetSubscriptionsRequest, this.handleHostSetSubscriptionsRequest.bind(this)); + this.handle(libData.HostGenerateTokenRequest, this.handleHostGenerateTokenRequest.bind(this)); + this.handle(libData.HostConfigCreateRequest, this.handleHostConfigCreateRequest.bind(this)); + this.handle(libData.InstanceDetailsGetRequest, this.handleInstanceDetailsGetRequest.bind(this)); + this.handle(libData.InstanceDetailsListRequest, this.handleInstanceDetailsListRequest.bind(this)); + this.handle( libData.InstanceDetailsSetSubscriptionsRequest, this.handleInstanceDetailsSetSubscriptionsRequest.bind(this) ); - this.register(libData.InstanceCreateRequest, this.handleInstanceCreateRequest.bind(this)); - this.register(libData.InstanceDeleteRequest, this.handleInstanceDeleteRequest.bind(this)); - this.register(libData.InstanceConfigGetRequest, this.handleInstanceConfigGetRequest.bind(this)); - this.register(libData.InstanceConfigSetFieldRequest, this.handleInstanceConfigSetFieldRequest.bind(this)); - this.register(libData.InstanceConfigSetPropRequest, this.handleInstanceConfigSetPropRequest.bind(this)); - this.register(libData.InstanceAssignRequest, this.handleInstanceAssignRequest.bind(this)); - this.register( + this.handle(libData.InstanceCreateRequest, this.handleInstanceCreateRequest.bind(this)); + this.handle(libData.InstanceDeleteRequest, this.handleInstanceDeleteRequest.bind(this)); + this.handle(libData.InstanceConfigGetRequest, this.handleInstanceConfigGetRequest.bind(this)); + this.handle(libData.InstanceConfigSetFieldRequest, this.handleInstanceConfigSetFieldRequest.bind(this)); + this.handle(libData.InstanceConfigSetPropRequest, this.handleInstanceConfigSetPropRequest.bind(this)); + this.handle(libData.InstanceAssignRequest, this.handleInstanceAssignRequest.bind(this)); + this.handle( libData.InstanceSetSaveListSubscriptionsRequest, this.handleInstanceSetSaveListSubscriptionsRequest.bind(this) ); - this.register( + this.handle( libData.InstanceRenameSaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller) ); - this.register(libData.InstanceCopySaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller)); - this.register( + this.handle(libData.InstanceCopySaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller)); + this.handle( libData.InstanceDeleteSaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller) ); - this.register(libData.InstanceDownloadSaveRequest, this.handleInstanceDownloadSaveRequest.bind(this)); - this.register(libData.InstanceTransferSaveRequest, this.handleInstanceTransferSaveRequest.bind(this)); - this.register(libData.ModPackListRequest, this.handleModPackListRequest.bind(this)); - this.register(libData.ModPackSetSubscriptionsRequest, this.handleModPackSetSubscriptionsRequest.bind(this)); - this.register(libData.ModPackCreateRequest, this.handleModPackCreateRequest.bind(this)); - this.register(libData.ModPackUpdateRequest, this.handleModPackUpdateRequest.bind(this)); - this.register(libData.ModPackDeleteRequest, this.handleModPackDeleteRequest.bind(this)); - this.register(libData.ModGetRequest, this.handleModGetRequest.bind(this)); - this.register(libData.ModListRequest, this.handleModListRequest.bind(this)); - this.register(libData.ModSearchRequest, this.handleModSearchRequest.bind(this)); - this.register(libData.ModSetSubscriptionsRequest, this.handleModSetSubscriptionsRequest.bind(this)); - this.register(libData.ModDownloadRequest, this.handleModDownloadRequest.bind(this)); - this.register(libData.ModDeleteRequest, this.handleModDeleteRequest.bind(this)); - this.register(libData.LogSetSubscriptionsRequest, this.handleLogSetSubscriptionsRequest.bind(this)); - this.register(libData.LogQueryRequest, this.handleLogQueryRequest.bind(this)); - this.register(libData.PermissionListRequest, this.handlePermissionListRequest.bind(this)); - this.register(libData.RoleListRequest, this.handleRoleListRequest.bind(this)); - this.register(libData.RoleCreateRequest, this.handleRoleCreateRequest.bind(this)); - this.register(libData.RoleUpdateRequest, this.handleRoleUpdateRequest.bind(this)); - this.register( + this.handle(libData.InstanceDownloadSaveRequest, this.handleInstanceDownloadSaveRequest.bind(this)); + this.handle(libData.InstanceTransferSaveRequest, this.handleInstanceTransferSaveRequest.bind(this)); + this.handle(libData.ModPackListRequest, this.handleModPackListRequest.bind(this)); + this.handle(libData.ModPackSetSubscriptionsRequest, this.handleModPackSetSubscriptionsRequest.bind(this)); + this.handle(libData.ModPackCreateRequest, this.handleModPackCreateRequest.bind(this)); + this.handle(libData.ModPackUpdateRequest, this.handleModPackUpdateRequest.bind(this)); + this.handle(libData.ModPackDeleteRequest, this.handleModPackDeleteRequest.bind(this)); + this.handle(libData.ModGetRequest, this.handleModGetRequest.bind(this)); + this.handle(libData.ModListRequest, this.handleModListRequest.bind(this)); + this.handle(libData.ModSearchRequest, this.handleModSearchRequest.bind(this)); + this.handle(libData.ModSetSubscriptionsRequest, this.handleModSetSubscriptionsRequest.bind(this)); + this.handle(libData.ModDownloadRequest, this.handleModDownloadRequest.bind(this)); + this.handle(libData.ModDeleteRequest, this.handleModDeleteRequest.bind(this)); + this.handle(libData.LogSetSubscriptionsRequest, this.handleLogSetSubscriptionsRequest.bind(this)); + this.handle(libData.LogQueryRequest, this.handleLogQueryRequest.bind(this)); + this.handle(libData.PermissionListRequest, this.handlePermissionListRequest.bind(this)); + this.handle(libData.RoleListRequest, this.handleRoleListRequest.bind(this)); + this.handle(libData.RoleCreateRequest, this.handleRoleCreateRequest.bind(this)); + this.handle(libData.RoleUpdateRequest, this.handleRoleUpdateRequest.bind(this)); + this.handle( libData.RoleGrantDefaultPermissionsRequest, this.handleRoleGrantDefaultPermissionsRequest.bind(this) ); - this.register(libData.RoleDeleteRequest, this.handleRoleDeleteRequest.bind(this)); - this.register(libData.UserGetRequest, this.handleUserGetRequest.bind(this)); - this.register(libData.UserListRequest, this.handleUserListRequest.bind(this)); - this.register(libData.UserSetSubscriptionsRequest, this.handleUserSetSubscriptionsRequest.bind(this)); - this.register(libData.UserCreateRequest, this.handleUserCreateRequest.bind(this)); - this.register(libData.UserRevokeTokenRequest, this.handleUserRevokeTokenRequest.bind(this)); - this.register(libData.UserUpdateRolesRequest, this.handleUserUpdateRolesRequest.bind(this)); - this.register(libData.UserSetAdminRequest, this.handleUserSetAdminRequest.bind(this)); - this.register(libData.UserSetBannedRequest, this.handleUserSetBannedRequest.bind(this)); - this.register(libData.UserSetWhitelistedRequest, this.handleUserSetWhitelistedRequest.bind(this)); - this.register(libData.UserDeleteRequest, this.handleUserDeleteRequest.bind(this)); - this.register(libData.DebugDumpWsRequest, this.handleDebugDumpWsRequest.bind(this)); + this.handle(libData.RoleDeleteRequest, this.handleRoleDeleteRequest.bind(this)); + this.handle(libData.UserGetRequest, this.handleUserGetRequest.bind(this)); + this.handle(libData.UserListRequest, this.handleUserListRequest.bind(this)); + this.handle(libData.UserSetSubscriptionsRequest, this.handleUserSetSubscriptionsRequest.bind(this)); + this.handle(libData.UserCreateRequest, this.handleUserCreateRequest.bind(this)); + this.handle(libData.UserRevokeTokenRequest, this.handleUserRevokeTokenRequest.bind(this)); + this.handle(libData.UserUpdateRolesRequest, this.handleUserUpdateRolesRequest.bind(this)); + this.handle(libData.UserSetAdminRequest, this.handleUserSetAdminRequest.bind(this)); + this.handle(libData.UserSetBannedRequest, this.handleUserSetBannedRequest.bind(this)); + this.handle(libData.UserSetWhitelistedRequest, this.handleUserSetWhitelistedRequest.bind(this)); + this.handle(libData.UserDeleteRequest, this.handleUserDeleteRequest.bind(this)); + this.handle(libData.DebugDumpWsRequest, this.handleDebugDumpWsRequest.bind(this)); } async handleControllerConfigGetRequest() { diff --git a/packages/controller/src/Controller.js b/packages/controller/src/Controller.js index 53636cf1..26db2fa4 100644 --- a/packages/controller/src/Controller.js +++ b/packages/controller/src/Controller.js @@ -13,6 +13,7 @@ const libConfig = require("@clusterio/lib/config"); const libData = require("@clusterio/lib/data"); const libErrors = require("@clusterio/lib/errors"); const libFileOps = require("@clusterio/lib/file_ops"); +const libLink = require("@clusterio/lib/link"); const libLoggingUtils = require("@clusterio/lib/logging_utils"); const libPlugin = require("@clusterio/lib/plugin"); const libPluginLoader = require("@clusterio/lib/plugin_loader"); @@ -970,17 +971,20 @@ class Controller { }; } - register(Class, handler) { + handle(Class, handler) { if (Class.type === "request") { - this.registerRequest(Class, handler); + this.handleRequest(Class, handler); } else if (Class.type === "event") { - this.registerEvent(Class, handler); + this.handleEvent(Class, handler); } else { throw new Error(`Class ${Class.name} has unrecognized type ${Class.type}`); } } - registerRequest(Request, handler) { + handleRequest(Request, handler) { + if (!libLink.Link._requestsByClass.has(Request)) { + throw new Error(`Unregistered Request class ${Request.name}`); + } if (this._registeredRequests.has(Request)) { throw new Error(`Request ${Request.name} is already registered`); } @@ -988,13 +992,19 @@ class Controller { } fallbackRequest(Request, handler) { + if (!libLink.Link._requestsByClass.has(Request)) { + throw new Error(`Unregistered Request class ${Request.name}`); + } if (this._fallbackedRequests.has(Request)) { throw new Error(`Request ${Request.name} is already fallbacked`); } this._fallbackedRequests.set(Request, handler); } - registerEvent(Event, handler) { + handleEvent(Event, handler) { + if (!libLink.Link._eventsByClass.has(Event)) { + throw new Error(`Unregistered Event class ${Event.name}`); + } if (this._registeredEvents.has(Event)) { throw new Error(`Event ${Event.name} is already registered`); } @@ -1002,6 +1012,9 @@ class Controller { } snoopEvent(Event, handler) { + if (!libLink.Link._eventsByClass.has(Event)) { + throw new Error(`Unregistered Event class ${Event.name}`); + } if (this._snoopedEvents.has(Event)) { throw new Error(`Event ${Event.name} is already snooped`); } diff --git a/packages/controller/src/HostConnection.js b/packages/controller/src/HostConnection.js index aa14e366..b5c626f3 100644 --- a/packages/controller/src/HostConnection.js +++ b/packages/controller/src/HostConnection.js @@ -58,11 +58,11 @@ class HostConnection extends BaseConnection { } }); - this.register(libData.InstanceStatusChangedEvent, this.handleInstanceStatusChangedEvent.bind(this)); - this.register(libData.InstancesUpdateRequest, this.handleInstancesUpdateRequest.bind(this)); - this.register(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this)); - this.register(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this)); - this.register(libData.InstancePlayerUpdateEvent, this.handleInstancePlayerUpdateEvent.bind(this)); + this.handle(libData.InstanceStatusChangedEvent, this.handleInstanceStatusChangedEvent.bind(this)); + this.handle(libData.InstancesUpdateRequest, this.handleInstancesUpdateRequest.bind(this)); + this.handle(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this)); + this.handle(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this)); + this.handle(libData.InstancePlayerUpdateEvent, this.handleInstancePlayerUpdateEvent.bind(this)); } _checkPluginVersions() { diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index b1ee3472..e02dd2a5 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -76,8 +76,8 @@ class Control extends libLink.Link { */ this.keepOpen = false; - this.register(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this)); - this.register(libData.DebugWsMessageEvent, this.handleDebugWsMessageEvent.bind(this)); + this.handle(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this)); + this.handle(libData.DebugWsMessageEvent, this.handleDebugWsMessageEvent.bind(this)); } async setLogSubscriptions({ @@ -114,6 +114,7 @@ class Control extends libLink.Link { async function loadPlugins(pluginList) { let pluginInfos = await libPluginLoader.loadPluginInfos(pluginList); + libLink.registerPluginMessages(pluginInfos); libConfig.registerPluginConfigGroups(pluginInfos); libConfig.finalizeConfigs(); diff --git a/packages/host/host.js b/packages/host/host.js index cee1f688..159e1aac 100755 --- a/packages/host/host.js +++ b/packages/host/host.js @@ -141,6 +141,7 @@ async function startHost() { logger.info("Loading Plugin info"); let pluginInfos = await libPluginLoader.loadPluginInfos(pluginList); + libLink.registerPluginMessages(pluginInfos); libConfig.registerPluginConfigGroups(pluginInfos); libConfig.finalizeConfigs(); diff --git a/packages/host/src/Host.js b/packages/host/src/Host.js index da87711a..3b7eea31 100644 --- a/packages/host/src/Host.js +++ b/packages/host/src/Host.js @@ -307,21 +307,21 @@ class Host extends libLink.Link { }); } - this.register(libData.SyncUserListsEvent, this.handleSyncUserListsEvent.bind(this)); + this.handle(libData.SyncUserListsEvent, this.handleSyncUserListsEvent.bind(this)); this.snoopEvent(libData.InstanceAdminlistUpdateEvent, this.handleAdminlistUpdateEvent.bind(this)); this.snoopEvent(libData.InstanceBanlistUpdateEvent, this.handleBanlistUpdateEvent.bind(this)); this.snoopEvent(libData.InstanceWhitelistUpdateEvent, this.handleWhitelistUpdateEvent.bind(this)); - this.register(libData.InstanceAssignInternalRequest, this.handleInstanceAssignInternalRequest.bind(this)); - this.register(libData.InstanceUnassignInternalRequest, this.handleInstanceUnassignInternalRequest.bind(this)); - this.register(libData.HostMetricsRequest, this.handleHostMetricsRequest.bind(this)); + this.handle(libData.InstanceAssignInternalRequest, this.handleInstanceAssignInternalRequest.bind(this)); + this.handle(libData.InstanceUnassignInternalRequest, this.handleInstanceUnassignInternalRequest.bind(this)); + this.handle(libData.HostMetricsRequest, this.handleHostMetricsRequest.bind(this)); this.fallbackRequest(libData.InstanceListSavesRequest, this.fallbackInstanceListSavesRequest.bind(this)); - this.register(libData.InstanceRenameSaveRequest, this.handleInstanceRenameSaveRequest.bind(this)); - this.register(libData.InstanceCopySaveRequest, this.handleInstanceCopySaveRequest.bind(this)); - this.register(libData.InstanceTransferSaveRequest, this.handleInstanceTransferSaveRequest.bind(this)); - this.register(libData.InstanceDeleteSaveRequest, this.handleInstanceDeleteSaveRequest.bind(this)); - this.register(libData.InstancePullSaveRequest, this.handleInstancePullSaveRequest.bind(this)); - this.register(libData.InstancePushSaveRequest, this.handleInstancePushSaveRequest.bind(this)); - this.register(libData.InstanceDeleteInternalRequest, this.handleInstanceDeleteInternalRequest.bind(this)); + this.handle(libData.InstanceRenameSaveRequest, this.handleInstanceRenameSaveRequest.bind(this)); + this.handle(libData.InstanceCopySaveRequest, this.handleInstanceCopySaveRequest.bind(this)); + this.handle(libData.InstanceTransferSaveRequest, this.handleInstanceTransferSaveRequest.bind(this)); + this.handle(libData.InstanceDeleteSaveRequest, this.handleInstanceDeleteSaveRequest.bind(this)); + this.handle(libData.InstancePullSaveRequest, this.handleInstancePullSaveRequest.bind(this)); + this.handle(libData.InstancePushSaveRequest, this.handleInstancePushSaveRequest.bind(this)); + this.handle(libData.InstanceDeleteInternalRequest, this.handleInstanceDeleteInternalRequest.bind(this)); } async _createNewInstanceDir(name) { diff --git a/packages/host/src/Instance.js b/packages/host/src/Instance.js index f442565f..b9d9c2ff 100644 --- a/packages/host/src/Instance.js +++ b/packages/host/src/Instance.js @@ -211,23 +211,23 @@ class Instance extends libLink.Link { this._hadPlayersOnline = false; this._playerAutosaveSlot = 1; - this.register(libData.InstanceExtractPlayersRequest, this.handleInstanceExtractPlayersRequest.bind(this)); - this.register(libData.InstanceAdminlistUpdateEvent, this.handleInstanceAdminlistUpdateEvent.bind(this)); - this.register(libData.InstanceBanlistUpdateEvent, this.handleInstanceBanlistUpdateEvent.bind(this)); - this.register(libData.InstanceWhitelistUpdateEvent, this.handleInstanceWhitelistUpdateEvent.bind(this)); - this.register(libData.ControllerConnectionEvent, this.handleControllerConnectionEvent.bind(this)); - this.register( + this.handle(libData.InstanceExtractPlayersRequest, this.handleInstanceExtractPlayersRequest.bind(this)); + this.handle(libData.InstanceAdminlistUpdateEvent, this.handleInstanceAdminlistUpdateEvent.bind(this)); + this.handle(libData.InstanceBanlistUpdateEvent, this.handleInstanceBanlistUpdateEvent.bind(this)); + this.handle(libData.InstanceWhitelistUpdateEvent, this.handleInstanceWhitelistUpdateEvent.bind(this)); + this.handle(libData.ControllerConnectionEvent, this.handleControllerConnectionEvent.bind(this)); + this.handle( libData.PrepareControllerDisconnectRequest, this.handlePrepareControllerDisconnectRequest.bind(this) ); - this.register(libData.InstanceMetricsRequest, this.handleInstanceMetricsRequest.bind(this)); - this.register(libData.InstanceStartRequest, this.handleInstanceStartRequest.bind(this)); - this.register(libData.InstanceLoadScenarioRequest, this.handleInstanceLoadScenarioRequest.bind(this)); - this.register(libData.InstanceListSavesRequest, this.handleInstanceListSavesRequest.bind(this)); - this.register(libData.InstanceCreateSaveRequest, this.handleInstanceCreateSaveRequest.bind(this)); - this.register(libData.InstanceExportDataRequest, this.handleInstanceExportDataRequest.bind(this)); - this.register(libData.InstanceStopRequest, this.handleInstanceStopRequest.bind(this)); - this.register(libData.InstanceKillRequest, this.handleInstanceKillRequest.bind(this)); - this.register(libData.InstanceSendRconRequest, this.handleInstanceSendRconRequest.bind(this)); + this.handle(libData.InstanceMetricsRequest, this.handleInstanceMetricsRequest.bind(this)); + this.handle(libData.InstanceStartRequest, this.handleInstanceStartRequest.bind(this)); + this.handle(libData.InstanceLoadScenarioRequest, this.handleInstanceLoadScenarioRequest.bind(this)); + this.handle(libData.InstanceListSavesRequest, this.handleInstanceListSavesRequest.bind(this)); + this.handle(libData.InstanceCreateSaveRequest, this.handleInstanceCreateSaveRequest.bind(this)); + this.handle(libData.InstanceExportDataRequest, this.handleInstanceExportDataRequest.bind(this)); + this.handle(libData.InstanceStopRequest, this.handleInstanceStopRequest.bind(this)); + this.handle(libData.InstanceKillRequest, this.handleInstanceKillRequest.bind(this)); + this.handle(libData.InstanceSendRconRequest, this.handleInstanceSendRconRequest.bind(this)); } _watchPlayerJoinsByChat() { diff --git a/packages/host/src/InstanceConnection.js b/packages/host/src/InstanceConnection.js index 51e82d72..223863fa 100644 --- a/packages/host/src/InstanceConnection.js +++ b/packages/host/src/InstanceConnection.js @@ -14,7 +14,7 @@ class InstanceConnection extends libLink.Link { this.plugins = new Map(); this.status = "stopped"; - this.register(libData.InstanceInitialisedEvent, this.handleInstanceInitialisedEvent.bind(this)); + this.handle(libData.InstanceInitialisedEvent, this.handleInstanceInitialisedEvent.bind(this)); this.snoopEvent(libData.InstanceStatusChangedEvent, this.snoopInstanceStatusChangedEvent.bind(this)); } diff --git a/packages/lib/data/messages_controller.js b/packages/lib/data/messages_controller.js index f35b6640..0280320f 100644 --- a/packages/lib/data/messages_controller.js +++ b/packages/lib/data/messages_controller.js @@ -301,8 +301,8 @@ LogQueryRequest.Response = class Response { /** @memberof module:lib/data */ class LogMessageEvent { static type = "event"; - static src = "host"; - static dst = "controller"; + static src = ["host", "controller"]; + static dst = ["controller", "control"]; /** @type {object} */ info; diff --git a/packages/lib/data/messages_core.js b/packages/lib/data/messages_core.js index 3028ad43..037e4c88 100644 --- a/packages/lib/data/messages_core.js +++ b/packages/lib/data/messages_core.js @@ -848,7 +848,7 @@ Message.validate = libSchema.compile(Message.jsonSchema); class PingRequest { static type = "request"; static src = ["controller", "host", "control"]; - static dest = ["controller", "host", "control"]; + static dst = ["controller", "host", "control"]; static permission = null; } diff --git a/packages/lib/data/messages_instance.js b/packages/lib/data/messages_instance.js index f7ace486..c288edbc 100644 --- a/packages/lib/data/messages_instance.js +++ b/packages/lib/data/messages_instance.js @@ -461,7 +461,7 @@ class InstanceSetSaveListSubscriptionsRequest { /** @memberof module:lib/data */ class InstanceSaveListUpdateEvent { static type = "event"; - static src = ["instance", "host"]; + static src = ["instance", "host", "controller"]; static dst = ["controller", "control"]; /** @type {number} */ @@ -533,8 +533,8 @@ class InstanceCreateSaveRequest { /** @memberof module:lib/data */ class InstanceRenameSaveRequest { static type = "request"; - static src = "control"; - static dst = "host"; + static src = ["control", "controller"]; + static dst = ["controller", "host"]; static permission = "core.instance.save.rename"; /** @type {number} */ @@ -568,8 +568,8 @@ class InstanceRenameSaveRequest { /** @memberof module:lib/data */ class InstanceCopySaveRequest { static type = "request"; - static src = "control"; - static dst = "host"; + static src = ["control", "controller"]; + static dst = ["controller", "host"]; static permission = "core.instance.save.copy"; /** @type {number} */ @@ -603,8 +603,8 @@ class InstanceCopySaveRequest { /** @memberof module:lib/data */ class InstanceDeleteSaveRequest { static type = "request"; - static src = "control"; - static dst = "host"; + static src = ["control", "controller"]; + static dst = ["controller", "host"]; static permission = "core.instance.save.delete"; /** @type {number} */ @@ -667,7 +667,7 @@ class InstanceDownloadSaveRequest { /** @memberof module:lib/data */ class InstanceTransferSaveRequest { static type = "request"; - static src = "control"; + static src = ["control", "controller"]; static dst = ["controller", "host"]; static permission = "core.instance.save.transfer"; @@ -1081,7 +1081,7 @@ class InstanceInitialisedEvent { /** @memberof module:lib/data */ class InstanceStatusChangedEvent { static type = "event"; - static src = "instance"; + static src = ["instance", "host"]; static dst = "controller"; /** @type {integer} */ diff --git a/packages/lib/data/messages_user.js b/packages/lib/data/messages_user.js index c657a698..e45e1b4d 100644 --- a/packages/lib/data/messages_user.js +++ b/packages/lib/data/messages_user.js @@ -266,11 +266,11 @@ class RawUser { // TODO refactor into lib/user.User properties: { name: { type: "string" }, roles: { type: "array", items: { type: "integer" } }, + instances: { type: "array", items: { type: "integer" } }, isAdmin: { type: "boolean" }, isBanned: { type: "boolean" }, isWhitelisted: { type: "boolean" }, banReason: { type: "string" }, - instances: { type: "array", items: { type: "integer" } }, isDeleted: { type: "boolean" }, playerStats: PlayerStats.jsonSchema, instanceStats: { @@ -299,6 +299,24 @@ class RawUser { // TODO refactor into lib/user.User json.isDeleted, playerStats, instanceStats ); } + + toJSON() { + const json = { + name: this.name, + roles: this.roles, + instances: this.instances, + isAdmin: this.isAdmin, + isBanned: this.isBanned, + isWhitelisted: this.isWhitelisted, + banReason: this.banReason, + isDeleted: this.isDeleted, + playerStats: this.playerStats, + }; + if (this.instanceStats) { + json.instanceStats = [...this.instanceStats]; + } + return json; + } } /* @memberof module:lib/data */ diff --git a/packages/lib/link/connectors.js b/packages/lib/link/connectors.js index 45159a2f..39a7ed69 100644 --- a/packages/lib/link/connectors.js +++ b/packages/lib/link/connectors.js @@ -36,8 +36,9 @@ class BaseConnector extends events.EventEmitter { let seq = this._seq; this._seq += 1; let src = new libData.Address(this.src.type, this.src.id, requestId); - let name = request.constructor.name; - let hasData = Boolean(request.constructor.jsonSchema); + let Request = request.constructor; + let name = Request.plugin ? `${Request.plugin}:${Request.name}` : Request.name; + let hasData = Boolean(Request.jsonSchema); const message = new libData.MessageRequest(seq, src, dst, name, hasData ? request : undefined); this.send(message); return seq; @@ -62,8 +63,9 @@ class BaseConnector extends events.EventEmitter { sendEvent(event, dst) { let seq = this._seq; this._seq += 1; - let name = event.constructor.name; - let hasData = Boolean(event.constructor.jsonSchema); + let Event = event.constructor; + let name = Event.plugin ? `${Event.plugin}:${Event.name}` : Event.name; + let hasData = Boolean(Event.jsonSchema); const message = new libData.MessageEvent(seq, this.src, dst, name, hasData ? event : undefined); this.send(message); return seq; diff --git a/packages/lib/link/index.js b/packages/lib/link/index.js index dcc14b7f..132e6150 100644 --- a/packages/lib/link/index.js +++ b/packages/lib/link/index.js @@ -10,6 +10,8 @@ const connectors = require("./connectors"); module.exports = { ...link, ...connectors, + + // migrate: Allow info for plugins from before link refactor to load. Event: class Event {}, Request: class Request {}, }; diff --git a/packages/lib/link/link.js b/packages/lib/link/link.js index f49cf253..5895074c 100644 --- a/packages/lib/link/link.js +++ b/packages/lib/link/link.js @@ -5,6 +5,7 @@ const libData = require("../data"); const libErrors = require("../errors"); const { logger } = require("../logging"); const libSchema = require("../schema"); +const { dataClasses } = require("./messages"); // Some definitions for the terminology used here: // link: Either side of a controller - client connection @@ -12,8 +13,6 @@ const libSchema = require("../schema"); // connection: the controller side of a link // client: the side that is not the controller of a link // message: the complete object sent using the 'message' event -// data: the data property of a message, essentially the payload. -// type: the type property of a message, identifies what to expect in data. /** * Common interface for server and client connections @@ -26,15 +25,14 @@ class Link { this.router = undefined; this.validateSent = true; - this._registeredRequests = new Map(); - this._fallbackedRequests = new Map(); - this._registeredEvents = new Map(); - this._snoopedEvents = new Map(); + this._requestHandlers = new Map(); + this._requestFallbacks = new Map(); this._eventHandlers = new Map(); + this._eventSnoopers = new Map(); this._pendingRequests = new Map(); this._nextRequestId = 1; - this.register(libData.PingRequest, () => {}); + this.handle(libData.PingRequest, () => {}); // Process messages received by the connector connector.on("message", payload => { @@ -97,28 +95,22 @@ class Link { throw new libErrors.InvalidMessage(`Unhandled message type ${message.type}`); } - if (message.type === "event" && this._snoopedEvents.has(message.name)) { - let event = this._snoopedEvents.get(message.name); - event.handler(event.eventFromJSON(message.data), message.src, message.dst).catch(err => { + let entry = this._validateMessage(message); + + if (message.type === "event" && this._eventSnoopers.has(entry.Event)) { + let handler = this._eventSnoopers.get(entry.Event); + handler(entry.eventFromJSON(message.data), message.src, message.dst).catch(err => { logger.error(`Unexpected error snooping ${message.name}:\n${err.stack}`); }); } if (!message.dst.addressedTo(this.connector.src)) { - this._routeMessage(message); + this._routeMessage(message, entry); return; } if (message.type === "request") { - let request = this._registeredRequests.get(message.name); - if (!request) { - this.connector.sendResponseError( - new libData.ResponseError(`Unrecognized request ${message.name}`), - message.src, - ); - return; - } - this._processRequest(message, request); + this._processRequest(message, entry, this._requestHandlers.get(entry.Request)); } else if (message.type === "response") { this._processResponse(message); @@ -127,11 +119,61 @@ class Link { this._processResponseError(message); } else if (message.type === "event") { - this._processEvent(message); + this._processEvent(message, entry); } } - _routeMessage(message) { + _validateMessage(message) { + if (message.type === "request") { + let entry = this.constructor._requestsByName.get(message.name); + if (!entry) { + let err = new libErrors.InvalidMessage(`Unrecognized request ${message.name}`); + this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src); + throw err; + } + if (!entry.allowedSrcTypes.has(message.src.type)) { + let err = new libErrors.InvalidMessage(`Source ${message.src} is not allowed for ${message.name}`); + this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src); + throw err; + } + if (!entry.allowedDstTypes.has(message.dst.type)) { + let err = new libErrors.InvalidMessage(`Destination ${message.dst} is not allowed for ${message.name}`); + this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src); + throw err; + } + return entry; + + } else if (message.type === "response") { + return undefined; + + } else if (message.type === "responseError") { + return undefined; + + } else if (message.type === "event") { + let entry = this.constructor._eventsByName.get(message.name); + if (!entry) { + throw new libErrors.InvalidMessage(`Unrecognized event ${message.name}`); + } + if (!entry.allowedSrcTypes.has(message.src.type)) { + throw new libErrors.InvalidMessage(`Source ${message.src} is not allowed for ${message.name}`); + } + if (message.dst.type === libData.Address.broadcast) { + if (!entry.allowedDstTypes.has(message.dst.id)) { + throw new libErrors.InvalidMessage(`Destination ${message.dst} is not allowed for ${message.name}`); + } + } else { + // eslint-disable-next-line no-lonely-if + if (!entry.allowedDstTypes.has(message.dst.type)) { + throw new libErrors.InvalidMessage(`Destination ${message.dst} is not allowed for ${message.name}`); + } + } + return entry; + } + + throw new Error("Should be unreachable"); + } + + _routeMessage(message, entry) { if (!this.router) { let err = new libErrors.InvalidMessage( `Received message addressed to ${message.dst} but this link does not route messages` @@ -144,7 +186,7 @@ class Link { throw err; } - let fallback = this._fallbackedRequests.get(message.name); + let fallback = message.type === "request" ? this._requestFallbacks.get(entry.Request) : undefined; if (this.router.forwardMessage(this, message, Boolean(fallback))) { return; } @@ -154,20 +196,20 @@ class Link { if (message.type !== "request") { throw new Error(`Router requested fallback handling of unsupported message type ${message.type}`); } - this._processRequest(message, fallback); + this._processRequest(message, entry, fallback); } - _processRequest(message, request) { - if (!request.handler) { + _processRequest(message, entry, handler) { + if (!handler) { this.connector.sendResponseError( - new libData.ResponseError(`No handler registered for ${request.Request.name}`), message.src + new libData.ResponseError(`No handler for ${entry.Request.name}`), message.src ); return; } let response; try { - response = request.handler(request.requestFromJSON(message.data), message.src, message.dst); + response = handler(entry.requestFromJSON(message.data), message.src, message.dst); } catch (err) { if (err.errors) { logger.error(JSON.stringify(err.errors, null, 4)); @@ -185,17 +227,22 @@ class Link { response.then( result => { if (this.validateSent) { - if (request.Response) { - request.responseFromJSON(JSON.parse(JSON.stringify(result))); + if (entry.Response) { + entry.responseFromJSON(JSON.parse(JSON.stringify(result))); } else if (result !== undefined) { - throw new Error(`Expected empty response from ${request.Request.name} handler`); + throw new Error(`Expected empty response from ${entry.Request.name} handler`); } } this.connector.sendResponse(result, message.src); } ).catch( err => { - if (!(err instanceof libErrors.RequestError)) { + if (err instanceof libErrors.InvalidMessage) { + logger.error(err.message); + if (err.errors) { + logger.error(JSON.stringify(err.errors, null, 4)); + } + } else if (!(err instanceof libErrors.RequestError)) { logger.error(`Unexpected error responding to ${message.name}:\n${err.stack}`); } this.connector.sendResponseError( @@ -232,16 +279,13 @@ class Link { pending.reject(new libErrors.RequestError(message.data.message, message.data.code, message.data.stack)); } - _processEvent(message) { - let event = this._registeredEvents.get(message.name); - if (!event) { - throw new libErrors.InvalidMessage(`Unrecognized event ${message.name}`); + _processEvent(message, entry) { + let handler = this._eventHandlers.get(entry.Event); + if (!handler) { + throw new libErrors.InvalidMessage(`Unhandled event ${message.name}`); } - if (!event.handler) { - return; - } - event.handler(event.eventFromJSON(message.data), message.src, message.dst).catch(err => { + handler(entry.eventFromJSON(message.data), message.src, message.dst).catch(err => { logger.error(`Unexpected error handling ${message.name}:\n${err.stack}`); }); } @@ -296,18 +340,16 @@ class Link { } sendRequest(request, dst) { - let name = request.constructor.name; - if (!this._registeredRequests.has(name)) { - // XXX should this be allowed? - this.registerRequest(request.constructor); + let entry = this.constructor._requestsByClass.get(request.constructor); + if (!entry) { + throw new Error(`Attempt to send unregistered Request ${request.constructor.name}`); } - let registeredRequest = this._registeredRequests.get(name); if (this.validateSent) { - registeredRequest.requestFromJSON(JSON.parse(JSON.stringify(request))); + entry.requestFromJSON(JSON.parse(JSON.stringify(request))); } let pending = { - request: registeredRequest, + request: entry, }; pending.promise = new Promise((resolve, reject) => { pending.resolve = resolve; @@ -321,141 +363,231 @@ class Link { } sendEvent(event, dst) { + let entry = this.constructor._eventsByClass.get(event.constructor); + if (!entry) { + throw new Error(`Attempt to send unregistered Event ${event.constructor.name}`); + } if (this.validateSent) { - let name = event.constructor.name; - if (!this._registeredEvents.has(name)) { - // XXX should this be allowed? - this.registerEvent(event.constructor); - } - this._registeredEvents.get(name).eventFromJSON(JSON.parse(JSON.stringify(event))); + entry.eventFromJSON(JSON.parse(JSON.stringify(event))); } this.connector.sendEvent(event, dst); } - register(Class, handler) { + handle(Class, handler) { if (Class.type === "request") { - this.registerRequest(Class, handler); + this.handleRequest(Class, handler); } else if (Class.type === "event") { - this.registerEvent(Class, handler); + this.handleEvent(Class, handler); } else { throw new Error(`Class ${Class.name} has unrecognized type ${Class.type}`); } } - _requestEntry(Request, handler) { - let name = Request.name; - let entry = { - Request, - handler, - }; + handleRequest(Request, handler) { + let entry = this.constructor._requestsByClass.get(Request); + if (!entry) { + throw new Error(`Unregistered Request class ${Request.name}`); + } + if (this._requestHandlers.has(Request)) { + throw new Error(`Request ${entry.name} is already registered`); + } + this._requestHandlers.set(Request, handler); + } + + fallbackRequest(Request, handler) { + let entry = this.constructor._requestsByClass.get(Request); + if (!entry) { + throw new Error(`Unregistered Request class ${Request.name}`); + } + if (this._requestFallbacks.has(Request)) { + throw new Error(`Request ${entry.name} is already fallbacked`); + } + this._requestFallbacks.set(Request, handler); + } + + handleEvent(Event, handler) { + let entry = this.constructor._eventsByClass.get(Event); + if (!entry) { + throw new Error(`Unregistered Event class ${Event.name}`); + } + if (this._eventHandlers.has(Event)) { + throw new Error(`Event ${entry.name} is already registered`); + } + this._eventHandlers.set(Event, handler); + } + + snoopEvent(Event, handler) { + let entry = this.constructor._eventsByClass.get(Event); + if (!entry) { + throw new Error(`Unregistered Event class ${Event.name}`); + } + if (this._eventSnoopers.has(Event)) { + throw new Error(`Event ${entry.name} is already snooped`); + } + this._eventSnoopers.set(Event, handler); + } + + static register(Class) { + if (Class.type === "request") { + this.registerRequest(Class); + } else if (Class.type === "event") { + this.registerEvent(Class); + } else { + throw new Error(`Data class ${Class.name} has unknown type ${Class.type}`); + } + } + + static requestFromJSON(Request, name) { if (Request.fromJSON) { if (!Request.jsonSchema) { throw new Error(`Request ${name} has static fromJSON but is missing static jsonSchema`); } let validate = libSchema.compile(Request.jsonSchema); - entry.requestFromJSON = (json) => { + return (json) => { if (!validate(json)) { throw new libErrors.InvalidMessage(`Request ${name} failed validation`, validate.errors); } return Request.fromJSON(json); }; - } else if (Request.jsonSchema) { - throw new Error(`Request ${name} has static jsonSchema but is missing static fromJSON`); - } else { - entry.requestFromJSON = () => new Request(); } + if (Request.jsonSchema) { + throw new Error(`Request ${name} has static jsonSchema but is missing static fromJSON`); + } + return () => new Request(); + } + + static responseFromJSON(Response, name) { + if (!Response.jsonSchema) { + throw new Error(`Response for Request ${name} is missing static jsonSchema`); + } + if (!Response.fromJSON) { + throw new Error(`Response for Request ${name} is missing static fromJSON`); + } + + let validate = libSchema.compile(Response.jsonSchema); + return (json) => { + if (!validate(json)) { + throw new libErrors.InvalidMessage( + `Response for request ${name} failed validation`, validate.errors + ); + } + return Response.fromJSON(json); + }; + } + + static allowedTypes(types, name, side) { + if (types === undefined) { + throw new Error(`Missing ${side} specification in ${name}`); + } + + if (!(types instanceof Array)) { + types = [types]; + } + + let allowed = new Set(); + for (let type of types) { + let id = libData.Address[type]; + if (typeof id !== "number") { + throw new Error(`Invalid type ${type} in ${side} specification of ${name}`); + } + if (id === libData.Address.broadcast) { + throw new Error(`${side} specification may not use the broadcast type in ${name}`); + } + allowed.add(id); + } + return allowed; + } + + static _requestsByName = new Map(); + static _requestsByClass = new Map(); + + static registerRequest(Request) { + const name = Request.plugin ? `${Request.plugin}:${Request.name}` : Request.name; + if (this._requestsByName.has(name)) { + throw new Error(`Request ${name} is already registered`); + } + + let entry = { + Request, + name, + requestFromJSON: this.requestFromJSON(Request, name), + allowedSrcTypes: this.allowedTypes(Request.src, name, "src"), + allowedDstTypes: this.allowedTypes(Request.dst, name, "dst"), + }; let Response = Request.Response; if (Response) { - if (!Response.jsonSchema) { - throw new Error(`Response for Request ${name} is missing static jsonSchema`); - } - if (!Response.fromJSON) { - throw new Error(`Response for Request ${name} is missing static fromJSON`); - } - - let validate = libSchema.compile(Response.jsonSchema); - entry.responseFromJSON = (json) => { - if (!validate(json)) { - throw new libErrors.InvalidMessage( - `Response for request ${name} failed validation`, validate.errors - ); - } - return Response.fromJSON(json); - }; - entry.Response = Response; + entry.responseFromJSON = this.responseFromJSON(Response, name); } else { entry.responseFromJSON = () => undefined; } - return entry; + + this._requestsByName.set(name, entry); + this._requestsByClass.set(Request, entry); } - registerRequest(Request, handler) { - let name = Request.name; - if (this._registeredRequests.has(name)) { - throw new Error(`Request ${name} is already registered`); - } - let request = this._requestEntry(Request, handler); - this._registeredRequests.set(name, request); - } - - fallbackRequest(Request, handler) { - let name = Request.name; - if (this._fallbackedRequests.has(name)) { - throw new Error(`Request ${name} is already fallbacked`); - } - let request = this._requestEntry(Request, handler); - this._fallbackedRequests.set(name, request); - } - - _createEventFromJSON(Event) { + static eventFromJSON(Event, name) { if (Event.fromJSON) { if (!Event.jsonSchema) { - throw new Error(`Event ${Event.name} has static fromJSON but is missing static jsonSchema`); + throw new Error(`Event ${name} has static fromJSON but is missing static jsonSchema`); } let validate = libSchema.compile(Event.jsonSchema); return (json) => { if (!validate(json)) { - throw new libErrors.InvalidMessage(`Event ${Event.name} failed validation`, validate.errors); + throw new libErrors.InvalidMessage(`Event ${name} failed validation`, validate.errors); } return Event.fromJSON(json); }; } if (Event.jsonSchema) { - throw new Error(`Event ${Event.name} has static jsonSchema but is missing static fromJSON`); + throw new Error(`Event ${name} has static jsonSchema but is missing static fromJSON`); } return () => new Event(); } - registerEvent(Event, handler) { - if (this._registeredEvents.has(Event.name)) { - throw new Error(`Event ${Event.name} is already registered`); - } - let event = { - Event, - handler, - eventFromJSON: this._createEventFromJSON(Event), - }; - this._registeredEvents.set(Event.name, event); - } + static _eventsByName = new Map(); + static _eventsByClass = new Map(); - snoopEvent(Event, handler) { - let name = Event.name; - if (this._snoopedEvents.has(name)) { - throw new Error(`Event ${name} is already snooped`); + static registerEvent(Event) { + const name = Event.plugin ? `${Event.plugin}:${Event.name}` : Event.name; + if (this._eventsByName.has(name)) { + throw new Error(`Event ${name} is already registered`); } - let event = { + + let entry = { Event, - handler, - eventFromJSON: this._createEventFromJSON(Event), + name, + eventFromJSON: this.eventFromJSON(Event, name), + allowedSrcTypes: this.allowedTypes(Event.src, name, "src"), + allowedDstTypes: this.allowedTypes(Event.dst, name, "dst"), }; - this._snoopedEvents.set(Event.name, event); + this._eventsByName.set(name, entry); + this._eventsByClass.set(Event, entry); + } +} + +for (let Class of dataClasses) { + Link.register(Class); +} + +function registerPluginMessages(pluginInfos) { + for (let pluginInfo of pluginInfos) { + for (let Class of pluginInfo.messages || []) { + if (Class.plugin !== pluginInfo.name) { + throw new Error( + `Expected message ${Class.name} from ${pluginInfo.name} to have a static ` + + `plugin property set to "${pluginInfo.name}"` + ); + } + Link.register(Class); + } } } module.exports = { Link, + registerPluginMessages, }; diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js new file mode 100644 index 00000000..6a6acf5c --- /dev/null +++ b/packages/lib/link/messages.js @@ -0,0 +1,111 @@ +"use strict"; +const core = require("../data/messages_core"); +const controller = require("../data/messages_controller"); +const host = require("../data/messages_host"); +const instance = require("../data/messages_instance"); +const mod = require("../data/messages_mod"); +const user = require("../data/messages_user"); + +// Event and Request payloads registered with the Link class. Used to decode +// the data payload of incomming requsts and event messages into instances of +// their proper classes. +const dataClasses = [ + core.PingRequest, + + controller.ControllerConfigGetRequest, + controller.ControllerConfigSetFieldRequest, + controller.ControllerConfigSetPropRequest, + controller.HostGenerateTokenRequest, + controller.HostConfigCreateRequest, + controller.LogSetSubscriptionsRequest, + controller.LogQueryRequest, + controller.LogMessageEvent, + controller.DebugDumpWsRequest, + controller.DebugWsMessageEvent, + + host.HostListRequest, + host.HostSetSubscriptionsRequest, + host.HostUpdateEvent, + host.HostMetricsRequest, + host.ControllerConnectionEvent, + host.PrepareControllerDisconnectRequest, + host.SyncUserListsEvent, + + instance.InstanceDetailsGetRequest, + instance.InstanceDetailsListRequest, + instance.InstanceDetailsSetSubscriptionsRequest, + instance.InstanceDetailsUpdateEvent, + instance.InstanceCreateRequest, + instance.InstanceConfigGetRequest, + instance.InstanceConfigSetFieldRequest, + instance.InstanceConfigSetPropRequest, + instance.InstanceAssignRequest, + instance.InstanceMetricsRequest, + instance.InstanceStartRequest, + instance.InstanceListSavesRequest, + instance.InstanceSetSaveListSubscriptionsRequest, + instance.InstanceSaveListUpdateEvent, + instance.InstanceCreateSaveRequest, + instance.InstanceRenameSaveRequest, + instance.InstanceCopySaveRequest, + instance.InstanceDeleteSaveRequest, + instance.InstanceDownloadSaveRequest, + instance.InstanceTransferSaveRequest, + instance.InstancePullSaveRequest, + instance.InstancePushSaveRequest, + instance.InstanceLoadScenarioRequest, + instance.InstanceExportDataRequest, + instance.InstanceExtractPlayersRequest, + instance.InstanceStopRequest, + instance.InstanceKillRequest, + instance.InstanceDeleteRequest, + instance.InstanceDeleteInternalRequest, + instance.InstanceSendRconRequest, + instance.InstancesUpdateRequest, + instance.InstanceAssignInternalRequest, + instance.InstanceUnassignInternalRequest, + instance.InstanceInitialisedEvent, + instance.InstanceStatusChangedEvent, + instance.InstanceDetailsChangedEvent, + instance.InstanceBanlistUpdateEvent, + instance.InstanceAdminlistUpdateEvent, + instance.InstanceWhitelistUpdateEvent, + instance.InstancePlayerUpdateEvent, + + mod.ModPackGetRequest, + mod.ModPackGetDefaultRequest, + mod.ModPackListRequest, + mod.ModPackCreateRequest, + mod.ModPackUpdateRequest, + mod.ModPackDeleteRequest, + mod.ModPackSetSubscriptionsRequest, + mod.ModGetRequest, + mod.ModListRequest, + mod.ModSearchRequest, + mod.ModSetSubscriptionsRequest, + mod.ModDownloadRequest, + mod.ModDeleteRequest, + mod.ModPackUpdateEvent, + mod.ModUpdateEvent, + + user.PermissionListRequest, + user.RoleListRequest, + user.RoleCreateRequest, + user.RoleUpdateRequest, + user.RoleGrantDefaultPermissionsRequest, + user.RoleDeleteRequest, + user.UserGetRequest, + user.UserListRequest, + user.UserSetSubscriptionsRequest, + user.UserCreateRequest, + user.UserRevokeTokenRequest, + user.UserUpdateRolesRequest, + user.UserSetAdminRequest, + user.UserSetWhitelistedRequest, + user.UserSetBannedRequest, + user.UserDeleteRequest, + user.AccountUpdateEvent, + user.UserUpdateEvent, +]; + +module.exports = { dataClasses }; diff --git a/packages/lib/plugin_loader.js b/packages/lib/plugin_loader.js index e1f03ca0..e831e5db 100644 --- a/packages/lib/plugin_loader.js +++ b/packages/lib/plugin_loader.js @@ -7,6 +7,7 @@ const libErrors = require("./errors"); const libPlugin = require("./plugin"); +const { logger } = require("./logging"); const path = require("path"); @@ -42,6 +43,12 @@ async function loadPluginInfos(pluginList) { ); } + // migrate: ignore incompatible old plugins + if (pluginInfo.messages && !(pluginInfo.messages instanceof Array)) { + logger.warn(`Ignoring incompatible pre alpha.14 plugin ${pluginName}`); + continue; + } + pluginInfo.requirePath = pluginPath; pluginInfo.version = pluginPackage.version; plugins.push(pluginInfo); diff --git a/packages/web_ui/src/bootstrap.jsx b/packages/web_ui/src/bootstrap.jsx index e6f3bbb0..d07d1ed3 100644 --- a/packages/web_ui/src/bootstrap.jsx +++ b/packages/web_ui/src/bootstrap.jsx @@ -3,7 +3,7 @@ import "./index.css"; import React from "react"; import ReactDOM from "react-dom"; -import { libConfig, libLogging, libPlugin } from "@clusterio/lib"; +import { libConfig, libLink, libLogging, libPlugin } from "@clusterio/lib"; const { ConsoleTransport, WebConsoleFormat, logger } = libLogging; import App from "./components/App"; @@ -102,6 +102,7 @@ export default async function bootstrap() { format: new WebConsoleFormat(), })); let pluginInfos = await loadPluginInfos(); + libLink.registerPluginMessages(pluginInfos); libConfig.registerPluginConfigGroups(pluginInfos); libConfig.finalizeConfigs(); let plugins = await loadPlugins(pluginInfos); diff --git a/packages/web_ui/src/util/websocket.jsx b/packages/web_ui/src/util/websocket.jsx index 2d1b43c8..314745d0 100644 --- a/packages/web_ui/src/util/websocket.jsx +++ b/packages/web_ui/src/util/websocket.jsx @@ -114,15 +114,15 @@ export class Control extends libLink.Link { }); } - this.register(libData.AccountUpdateEvent, this.handleAccountUpdateEvent.bind(this)); - this.register(libData.HostUpdateEvent, this.handleHostUpdateEvent.bind(this)); - this.register(libData.InstanceDetailsUpdateEvent, this.handleInstanceDetailsUpdateEvent.bind(this)); - this.register(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this)); - this.register(libData.ModPackUpdateEvent, this.handleModPackUpdateEvent.bind(this)); - this.register(libData.ModUpdateEvent, this.handleModUpdateEvent.bind(this)); - this.register(libData.UserUpdateEvent, this.handleUserUpdateEvent.bind(this)); - this.register(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this)); - this.register(libData.DebugWsMessageEvent, this.handleDebugWsMessageEvent.bind(this)); + this.handle(libData.AccountUpdateEvent, this.handleAccountUpdateEvent.bind(this)); + this.handle(libData.HostUpdateEvent, this.handleHostUpdateEvent.bind(this)); + this.handle(libData.InstanceDetailsUpdateEvent, this.handleInstanceDetailsUpdateEvent.bind(this)); + this.handle(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this)); + this.handle(libData.ModPackUpdateEvent, this.handleModPackUpdateEvent.bind(this)); + this.handle(libData.ModUpdateEvent, this.handleModUpdateEvent.bind(this)); + this.handle(libData.UserUpdateEvent, this.handleUserUpdateEvent.bind(this)); + this.handle(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this)); + this.handle(libData.DebugWsMessageEvent, this.handleDebugWsMessageEvent.bind(this)); } async handleAccountUpdateEvent(event) { diff --git a/plugins/global_chat/info.js b/plugins/global_chat/info.js index fee0683f..875dec62 100644 --- a/plugins/global_chat/info.js +++ b/plugins/global_chat/info.js @@ -1,4 +1,5 @@ "use strict"; +const messages = require("./messages"); module.exports = { name: "global_chat", @@ -6,4 +7,8 @@ module.exports = { description: "Forwards chat between instances.", instanceEntrypoint: "instance", controlEntrypoint: "control", + + messages: [ + messages.ChatEvent, + ], }; diff --git a/plugins/global_chat/instance.js b/plugins/global_chat/instance.js index 01ba87a6..a334b230 100644 --- a/plugins/global_chat/instance.js +++ b/plugins/global_chat/instance.js @@ -20,7 +20,7 @@ function removeTags(content) { class InstancePlugin extends libPlugin.BaseInstancePlugin { async init() { this.messageQueue = []; - this.instance.register(ChatEvent, this.handleChatEvent.bind(this)); + this.instance.handle(ChatEvent, this.handleChatEvent.bind(this)); } onControllerConnectionEvent(event) { diff --git a/plugins/global_chat/test/plugin.js b/plugins/global_chat/test/plugin.js index 4029d659..4b147d92 100644 --- a/plugins/global_chat/test/plugin.js +++ b/plugins/global_chat/test/plugin.js @@ -1,5 +1,6 @@ "use strict"; const assert = require("assert").strict; +const libLink = require("@clusterio/lib/link"); const mock = require("../../../test/mock"); const lines = require("../../../test/lib/factorio/lines"); @@ -25,6 +26,7 @@ describe("global_chat plugin", function() { let instancePlugin; before(async function() { + libLink.Link.register(ChatEvent); instancePlugin = new instance.InstancePlugin(info, new mock.MockInstance(), new mock.MockHost()); await instancePlugin.init(); }); diff --git a/plugins/inventory_sync/controller.js b/plugins/inventory_sync/controller.js index e78d659e..34cc2bb0 100644 --- a/plugins/inventory_sync/controller.js +++ b/plugins/inventory_sync/controller.js @@ -48,11 +48,11 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin { }); }, this.controller.config.get("inventory_sync.autosave_interval") * 1000); - this.controller.register(AcquireRequest, this.handleAcquireRequest.bind(this)); - this.controller.register(ReleaseRequest, this.handleReleaseRequest.bind(this)); - this.controller.register(UploadRequest, this.handleUploadRequest.bind(this)); - this.controller.register(DownloadRequest, this.handleDownloadRequest.bind(this)); - this.controller.register(DatabaseStatsRequest, this.handleDatabaseStatsRequest.bind(this)); + this.controller.handle(AcquireRequest, this.handleAcquireRequest.bind(this)); + this.controller.handle(ReleaseRequest, this.handleReleaseRequest.bind(this)); + this.controller.handle(UploadRequest, this.handleUploadRequest.bind(this)); + this.controller.handle(DownloadRequest, this.handleDownloadRequest.bind(this)); + this.controller.handle(DatabaseStatsRequest, this.handleDatabaseStatsRequest.bind(this)); } async onInstanceStatusChanged(instance) { diff --git a/plugins/inventory_sync/info.js b/plugins/inventory_sync/info.js index 72f69ccb..9b341fcd 100644 --- a/plugins/inventory_sync/info.js +++ b/plugins/inventory_sync/info.js @@ -1,5 +1,6 @@ "use strict"; const { libConfig, libUsers } = require("@clusterio/lib"); +let messages = require("./messages"); class ControllerConfigGroup extends libConfig.PluginConfigGroup { } ControllerConfigGroup.defaultAccess = ["controller", "host", "control"]; @@ -51,6 +52,13 @@ module.exports = { controllerEntrypoint: "controller", ControllerConfigGroup, + messages: [ + messages.AcquireRequest, + messages.ReleaseRequest, + messages.UploadRequest, + messages.DownloadRequest, + messages.DatabaseStatsRequest, + ], webEntrypoint: "./web", routes: ["/inventory"], }; diff --git a/plugins/player_auth/controller.js b/plugins/player_auth/controller.js index 96afe5c8..aab1ad2c 100644 --- a/plugins/player_auth/controller.js +++ b/plugins/player_auth/controller.js @@ -63,8 +63,8 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin { this.handleVerify(req, res).catch(next); }); - this.controller.register(FetchPlayerCodeRequest, this.handleFetchPlayerCodeRequest.bind(this)); - this.controller.register(SetVerifyCodeRequest, this.handleSetVerifyCodeRequest.bind(this)); + this.controller.handle(FetchPlayerCodeRequest, this.handleFetchPlayerCodeRequest.bind(this)); + this.controller.handle(SetVerifyCodeRequest, this.handleSetVerifyCodeRequest.bind(this)); } async handlePlayerCode(req, res) { diff --git a/plugins/player_auth/info.js b/plugins/player_auth/info.js index 2f937b37..ebe16f55 100644 --- a/plugins/player_auth/info.js +++ b/plugins/player_auth/info.js @@ -1,5 +1,6 @@ "use strict"; let { libConfig } = require("@clusterio/lib"); +let messages = require("./messages"); class ControllerConfigGroup extends libConfig.PluginConfigGroup {} @@ -29,4 +30,9 @@ module.exports = { instanceEntrypoint: "instance", webEntrypoint: "./web", ControllerConfigGroup, + + messages: [ + messages.FetchPlayerCodeRequest, + messages.SetVerifyCodeRequest, + ], }; diff --git a/plugins/research_sync/controller.js b/plugins/research_sync/controller.js index e182badb..3375306e 100644 --- a/plugins/research_sync/controller.js +++ b/plugins/research_sync/controller.js @@ -49,9 +49,9 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin { this.progressBroadcastId = null; this.progressToBroadcast = new Set(); - this.controller.register(ContributionEvent, this.handleContributionEvent.bind(this)); - this.controller.register(FinishedEvent, this.handleFinishedEvent.bind(this)); - this.controller.register(SyncTechnologiesRequest, this.handleSyncTechnologiesRequest.bind(this)); + this.controller.handle(ContributionEvent, this.handleContributionEvent.bind(this)); + this.controller.handle(FinishedEvent, this.handleFinishedEvent.bind(this)); + this.controller.handle(SyncTechnologiesRequest, this.handleSyncTechnologiesRequest.bind(this)); } async onShutdown() { diff --git a/plugins/research_sync/info.js b/plugins/research_sync/info.js index 2b895a22..c84a2053 100644 --- a/plugins/research_sync/info.js +++ b/plugins/research_sync/info.js @@ -1,4 +1,5 @@ "use strict"; +const messages = require("./messages"); module.exports = { name: "research_sync", @@ -6,4 +7,11 @@ module.exports = { description: "Synchronises technology research progress between instances.", instanceEntrypoint: "instance", controllerEntrypoint: "controller", + + messages: [ + messages.ContributionEvent, + messages.ProgressEvent, + messages.FinishedEvent, + messages.SyncTechnologiesRequest, + ], }; diff --git a/plugins/research_sync/instance.js b/plugins/research_sync/instance.js index b37892c4..d106b508 100644 --- a/plugins/research_sync/instance.js +++ b/plugins/research_sync/instance.js @@ -28,8 +28,8 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin { }); this.syncStarted = false; - this.instance.register(ProgressEvent, this.handleProgressEvent.bind(this)); - this.instance.register(FinishedEvent, this.handleFinishedEvent.bind(this)); + this.instance.handle(ProgressEvent, this.handleProgressEvent.bind(this)); + this.instance.handle(FinishedEvent, this.handleFinishedEvent.bind(this)); } async researchContribution(tech) { diff --git a/plugins/research_sync/messages.js b/plugins/research_sync/messages.js index 85670154..8d5e839b 100644 --- a/plugins/research_sync/messages.js +++ b/plugins/research_sync/messages.js @@ -5,7 +5,7 @@ class ContributionEvent { static type = "event"; static src = "instance"; static dst = "controller"; - static json = "research_sync"; + static plugin = "research_sync"; /** @type {string} */ name; @@ -39,7 +39,7 @@ class ProgressEvent { static type = "event"; static src = "controller"; static dst = "instance"; - static json = "research_sync"; + static plugin = "research_sync"; /** @type {Array<>} */ technologies; @@ -74,9 +74,9 @@ class ProgressEvent { class FinishedEvent { static type = "event"; - static src = "instance"; + static src = ["instance", "controller"]; static dst = "instance"; - static json = "research_sync"; + static plugin = "research_sync"; /** @type {string} */ name; @@ -144,7 +144,7 @@ class SyncTechnologiesRequest { static type = "request"; static src = "instance"; static dst = "controller"; - static json = "research_sync"; + static plugin = "research_sync"; /** @type {Array} */ technologies; diff --git a/plugins/subspace_storage/controller.js b/plugins/subspace_storage/controller.js index aaedf1c9..f062a28d 100644 --- a/plugins/subspace_storage/controller.js +++ b/plugins/subspace_storage/controller.js @@ -97,10 +97,10 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin { routes.addApiRoutes(this.controller.app, this.items); - this.controller.register(GetStorageRequest, this.handleGetStorageRequest.bind(this)); - this.controller.register(PlaceEvent, this.handlePlaceEvent.bind(this)); - this.controller.register(RemoveRequest, this.handleRemoveRequest.bind(this)); - this.controller.register(SetStorageSubscriptionRequest, this.handleSetStorageSubscriptionRequest.bind(this)); + this.controller.handle(GetStorageRequest, this.handleGetStorageRequest.bind(this)); + this.controller.handle(PlaceEvent, this.handlePlaceEvent.bind(this)); + this.controller.handle(RemoveRequest, this.handleRemoveRequest.bind(this)); + this.controller.handle(SetStorageSubscriptionRequest, this.handleSetStorageSubscriptionRequest.bind(this)); } updateStorage() { diff --git a/plugins/subspace_storage/info.js b/plugins/subspace_storage/info.js index bcc421b5..394bb4e9 100644 --- a/plugins/subspace_storage/info.js +++ b/plugins/subspace_storage/info.js @@ -1,5 +1,6 @@ "use strict"; const { libConfig, libUsers } = require("@clusterio/lib"); +const messages = require("./messages"); class ControllerConfigGroup extends libConfig.PluginConfigGroup {} ControllerConfigGroup.defaultAccess = ["controller", "host", "control"]; @@ -57,6 +58,13 @@ module.exports = { controllerEntrypoint: "controller", ControllerConfigGroup, + messages: [ + messages.PlaceEvent, + messages.RemoveRequest, + messages.GetStorageRequest, + messages.UpdateStorageEvent, + messages.SetStorageSubscriptionRequest, + ], webEntrypoint: "./web", routes: ["/storage"], }; diff --git a/plugins/subspace_storage/instance.js b/plugins/subspace_storage/instance.js index a2ac7b6b..cd0a5fb3 100644 --- a/plugins/subspace_storage/instance.js +++ b/plugins/subspace_storage/instance.js @@ -31,7 +31,7 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin { task.finally(() => { this.pendingTasks.delete(task); }); }); - this.instance.register(UpdateStorageEvent, this.handleUpdateStorageEvent.bind(this)); + this.instance.handle(UpdateStorageEvent, this.handleUpdateStorageEvent.bind(this)); } async onStart() { diff --git a/test/integration/index.js b/test/integration/index.js index ae759f8f..3f6c4eeb 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -53,13 +53,13 @@ class TestControl extends libLink.Link { ).catch(err => logger.error(`Error setting user subscriptions:\n${err.stack}`)); }); - this.register(libData.AccountUpdateEvent); - this.register(libData.HostUpdateEvent, this.handleHostUpdateEvent.bind(this)); - this.register(libData.InstanceDetailsUpdateEvent, this.handleInstanceDetailsUpdateEvent.bind(this)); - this.register(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this)); - this.register(libData.ModUpdateEvent, this.handleModUpdateEvent.bind(this)); - this.register(libData.ModPackUpdateEvent, this.handleModPackUpdateEvent.bind(this)); - this.register(libData.UserUpdateEvent, this.handleUserUpdateEvent.bind(this)); + this.handle(libData.AccountUpdateEvent); + this.handle(libData.HostUpdateEvent, this.handleHostUpdateEvent.bind(this)); + this.handle(libData.InstanceDetailsUpdateEvent, this.handleInstanceDetailsUpdateEvent.bind(this)); + this.handle(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this)); + this.handle(libData.ModUpdateEvent, this.handleModUpdateEvent.bind(this)); + this.handle(libData.ModPackUpdateEvent, this.handleModPackUpdateEvent.bind(this)); + this.handle(libData.UserUpdateEvent, this.handleUserUpdateEvent.bind(this)); } async handleHostUpdateEvent(event) { diff --git a/test/lib/command.js b/test/lib/command.js index 2d879e83..38d6f635 100644 --- a/test/lib/command.js +++ b/test/lib/command.js @@ -13,15 +13,18 @@ const addr = libData.Address.fromShorthand; describe("lib/command", function() { let testRole = libData.RawRole.fromJSON({ id: 28, name: "Test Role", description: "Test", permissions: [] }); - let loopbackConnector = new libLink.VirtualConnector(addr("controller"), addr("controller")); - let testControl = new mock.MockControl(loopbackConnector); - testControl.register( + let [controlConnector, controllerConnector] = libLink.VirtualConnector.makePair( + addr({ controlId: 1}), addr("controller") + ); + let testControl = new mock.MockControl(controlConnector); + let testController = new libLink.Link(controllerConnector); + testController.handle( libData.HostListRequest, () => [new libData.HostDetails("test", "0.1", "Test Host", 11, false)] ); - testControl.register( + testController.handle( libData.InstanceDetailsListRequest, () => [new libData.InstanceDetails("Test Instance", 57, 4, null, "stopped")] ); - testControl.register(libData.RoleListRequest, () => [testRole]); + testController.handle(libData.RoleListRequest, () => [testRole]); describe("resolveHost", function() { it("should pass an integer like string back", async function() { diff --git a/test/lib/link/link.js b/test/lib/link/link.js index c67e98f9..484029bf 100644 --- a/test/lib/link/link.js +++ b/test/lib/link/link.js @@ -27,6 +27,7 @@ describe("lib/link/link", function() { static src = ["controller", "control"]; static dst = ["controller", "control"]; } + libLink.Link.register(SimpleRequest); class NumberRequest { static type = "request"; static src = ["controller", "control"]; @@ -42,11 +43,13 @@ describe("lib/link/link", function() { toJSON() { return this.value; } static fromJSON(json) { return new this(json); } }; + libLink.Link.register(NumberRequest); class SimpleEvent { static type = "event"; static src = ["controller", "control"]; static dst = ["controller", "control"]; } + libLink.Link.register(SimpleEvent); class NumberEvent { static type = "event"; static src = ["controller", "control"]; @@ -56,6 +59,7 @@ describe("lib/link/link", function() { toJSON() { return this.value; } static fromJSON(json) { return new this(json); } } + libLink.Link.register(NumberEvent); beforeEach(function() { testConnector = new mock.MockConnector(src, dst); @@ -82,14 +86,14 @@ describe("lib/link/link", function() { static src = "controller"; static dst = "control"; } - testLink.register(UnhandledRequest); + libLink.Link.register(UnhandledRequest); testConnector.emit("message", new libData.MessageRequest(1, dst, src, "UnhandledRequest")); assert.deepEqual(testConnector.sentMessages, [ new libData.MessageResponseError( testConnector._seq - 1, src, dst, - new libData.ResponseError("No handler registered for UnhandledRequest") + new libData.ResponseError("No handler for UnhandledRequest") ), ]); }); @@ -190,18 +194,18 @@ describe("lib/link/link", function() { ); }); }); - describe(".register()", function() { - it("should register a request", function() { + describe(".handle()", function() { + it("should register a request handler", function() { let handled = false; - testLink.register(SimpleRequest, async () => { handled = true; }); - assert(testLink._registeredRequests.has("SimpleRequest"), "request was not registered"); + testLink.handle(SimpleRequest, async () => { handled = true; }); + assert(testLink._requestHandlers.has(SimpleRequest), "request handler was not registered"); testConnector.emit("message", new libData.MessageRequest(1, dst, src, "SimpleRequest")); assert(handled, "request was not handled"); }); it("should send response error from request handler throwing", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); - testLink.register(SimpleRequest, async () => { throwSimple("Error"); }); + testLink.handle(SimpleRequest, async () => { throwSimple("Error"); }); testConnector.emit("message", new libData.MessageRequest(1, dst, src, "SimpleRequest")); assert.deepEqual( await message, @@ -213,7 +217,7 @@ describe("lib/link/link", function() { it("should send response error on request validation failing", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); - testLink.register(NumberRequest, async () => 1); + testLink.handle(NumberRequest, async () => 1); testConnector.emit("message", new libData.MessageRequest(1, dst, src, "NumberRequest", "not a number")); let response = (await message)[0]; assert.deepEqual( @@ -228,7 +232,7 @@ describe("lib/link/link", function() { it("should send response error on response validation failing", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); - testLink.register(NumberRequest, async () => "not a number"); + testLink.handle(NumberRequest, async () => "not a number"); testConnector.emit("message", new libData.MessageRequest(1, dst, src, "NumberRequest", 1)); let response = (await message)[0]; assert.deepEqual( @@ -243,7 +247,7 @@ describe("lib/link/link", function() { it("should send value returned from request handler", async function() { let message = events.once(testConnector, "send"); message.catch(() => {}); - testLink.register(NumberRequest, async (request) => request.value + 4); + testLink.handle(NumberRequest, async (request) => request.value + 4); testConnector.emit("message", new libData.MessageRequest(1, dst, src, "NumberRequest", 1)); let response = (await message)[0]; assert.deepEqual( @@ -251,41 +255,43 @@ describe("lib/link/link", function() { new libData.MessageResponse(1, src, dst, 5) ); }); - it("should register an event", function() { + it("should register an event handler", function() { let handled = false; - testLink.register(SimpleEvent, async () => { handled = true; }); - assert(testLink._registeredEvents.has("SimpleEvent"), "event was not registered"); + testLink.handle(SimpleEvent, async () => { handled = true; }); + assert(testLink._eventHandlers.has(SimpleEvent), "event handler was not registered"); testConnector.emit("message", new libData.MessageEvent(1, dst, src, "SimpleEvent")); assert(handled, "event was not handled"); }); it("should pass value to event handler", async function() { let value; - testLink.register(NumberEvent, async (event) => { value = event.value; }); + testLink.handle(NumberEvent, async (event) => { value = event.value; }); testConnector.emit("message", new libData.MessageEvent(1, dst, src, "NumberEvent", 9)); assert.deepEqual(value, 9); }); it("should log errors from event handler", function() { - testLink.register(SimpleEvent, async () => { throwSimple("Error"); }); + testLink.handle(SimpleEvent, async () => { throwSimple("Error"); }); testConnector.emit("message", new libData.MessageEvent(1, dst, src, "SimpleEvent")); }); it("should throw on unknown type", function() { assert.throws( - () => testLink.register({ name: "Bad", type: "bad" }), + () => testLink.handle({ name: "Bad", type: "bad" }), { message: "Class Bad has unrecognized type bad" } ); }); it("should throw on double registration", function() { - testLink.register(SimpleRequest); + testLink.handle(SimpleRequest); assert.throws( - () => testLink.register(SimpleRequest), + () => testLink.handle(SimpleRequest), new Error("Request SimpleRequest is already registered") ); - testLink.register(SimpleEvent); + testLink.handle(SimpleEvent); assert.throws( - () => testLink.register(SimpleEvent), + () => testLink.handle(SimpleEvent), new Error("Event SimpleEvent is already registered") ); }); + }); + describe("static .register()", function() { it("should throw if Request has only one of jsonSchema and fromJSON", function() { class BadRequest1 { static type = "request"; @@ -294,7 +300,7 @@ describe("lib/link/link", function() { static jsonSchema = {}; } assert.throws( - () => testLink.register(BadRequest1), + () => libLink.Link.register(BadRequest1), new Error("Request BadRequest1 has static jsonSchema but is missing static fromJSON") ); class BadRequest2 { @@ -304,7 +310,7 @@ describe("lib/link/link", function() { static fromJSON() {}; } assert.throws( - () => testLink.register(BadRequest2), + () => libLink.Link.register(BadRequest2), new Error("Request BadRequest2 has static fromJSON but is missing static jsonSchema") ); }); @@ -316,7 +322,7 @@ describe("lib/link/link", function() { static jsonSchema = {}; } assert.throws( - () => testLink.register(BadEvent1), + () => libLink.Link.register(BadEvent1), new Error("Event BadEvent1 has static jsonSchema but is missing static fromJSON") ); class BadEvent2 { @@ -326,7 +332,7 @@ describe("lib/link/link", function() { static fromJSON() {}; } assert.throws( - () => testLink.register(BadEvent2), + () => libLink.Link.register(BadEvent2), new Error("Event BadEvent2 has static fromJSON but is missing static jsonSchema") ); }); @@ -348,7 +354,8 @@ describe("lib/link/link", function() { static jsonSchema = { type: "string" }; static fromJSON(json) { return new this(json); }; } - testLink.register(StringEvent, () => {}); + libLink.Link.register(StringEvent); + testLink.handle(StringEvent, () => {}); assert.throws( () => testLink._processMessage(new libData.MessageEvent(1, dst, src, "StringEvent", 99)), { message: "Event StringEvent failed validation" } @@ -360,7 +367,7 @@ describe("lib/link/link", function() { it("should snoop an event", function() { let handled = false; testLink.snoopEvent(SimpleEvent, async () => { handled = true; }); - assert(testLink._snoopedEvents.has("SimpleEvent"), "event was not snooped"); + assert(testLink._eventSnoopers.has(SimpleEvent), "event was not snooped"); testConnector.emit("message", new libData.MessageEvent(1, dst, src, "SimpleEvent")); assert(handled, "event was not handled"); }); @@ -375,28 +382,6 @@ describe("lib/link/link", function() { new Error("Event SimpleEvent is already snooped") ); }); - it("should throw if Event has only one of jsonSchema and fromJSON", function() { - class BadEvent1 { - static type = "event"; - static src = "controller"; - static dst = "control"; - static jsonSchema = {}; - } - assert.throws( - () => testLink.snoopEvent(BadEvent1), - new Error("Event BadEvent1 has static jsonSchema but is missing static fromJSON") - ); - class BadEvent2 { - static type = "event"; - static src = "controller"; - static dst = "control"; - static fromJSON() {}; - } - assert.throws( - () => testLink.snoopEvent(BadEvent2), - new Error("Event BadEvent2 has static fromJSON but is missing static jsonSchema") - ); - }); }); }); }); diff --git a/test/mock.js b/test/mock.js index d6226245..2006e360 100644 --- a/test/mock.js +++ b/test/mock.js @@ -185,7 +185,7 @@ class MockController { this.hosts = new Map(); } - register() { + handle() { } getControllerUrl() { @@ -210,6 +210,7 @@ class MockController { } async function createControllerPlugin(ControllerPluginClass, info) { + libLink.registerPluginMessages([info]); let controller = new MockController(); let metrics = {}; let logger = new MockLogger();