Files
factorio-clusterio/lib/link/messages.js
T
Hornwitser 039b41d3e6 Move events and requests into messages
Merge the events and requests categories in lib/link into messages and
use the class name of the message to determine which handler to call.

Also swaps the order of the arguments for forwardInstanceRequest to be
consistent with the rest of the lib/link code and fixes up the docs for
requests and events.
2019-11-30 00:08:10 +01:00

465 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
const schema = require("lib/schema");
const errors = require("lib/errors");
/**
* Represents a request sent over the link
*
* A request-response message that can be innitated by either party of the
* link.
*/
class Request {
constructor({
type, sources, targets, requestProperties = {}, responseProperties = {}
}) {
this.type = type;
this.sources = sources;
this.targets = targets;
this.requestType = type + '_request';
this.responseType = type + '_response';
this._requestValidator = schema.compile({
$schema: "http://json-schema.org/draft-07/schema#",
properties: {
"type": { const: this.requestType },
"data": {
additionalProperties: false,
required: Object.keys(requestProperties),
properties: requestProperties,
},
},
});
this._responseValidator = schema.compile({
$schema: "http://json-schema.org/draft-07/schema#",
properties: {
"type": { const: this.responseType },
"data": {
anyOf: [
{
additionalProperties: false,
required: ["seq", ...Object.keys(responseProperties)],
properties: {
"seq": { type: "integer" },
...responseProperties,
},
},
{
additionalProperties: false,
required: ["seq", "error"],
properties: {
"seq": { type: "integer" },
"error": { type: "string" },
},
},
],
},
}
});
}
/**
* Attach to a link
*
* Set a the validator for the response on the given link if it's a
* source and sets the handler for the request if it is a target.
*
* Does nothing if the link is neither a source nor a target for this
* request.
*
* @param {Link} link - The link to attach to.
* @param {Function} handler -
* Async function to invoke with link set as this to handle the
* request request. Only used on the target side.
* @throws {Error} if the handler is needed and not defined.
*/
attach(link, handler) {
// Source side.
if (this.sources.includes(link.source) && this.targets.includes(link.target)) {
link.setValidator(this.responseType, this._responseValidator);
}
// Target side. Note: Source and target is reversed here.
if (this.targets.includes(link.source) && this.sources.includes(link.target)) {
if (!handler) {
throw new Error(`Missing handler for ${this.requestType} on ${link.target}-${link.source} link`);
}
link.setHandler(this.requestType, message => {
handler.call(link, message).then(response => {
if (response === undefined) {
// XXX Should we allow implicit responses like this?
response = {};
}
if (Object.prototype.hasOwnProperty.call(response, 'seq')) {
throw new errors.RequestError("response contains reserved property 'seq'");
}
link.send(this.responseType, { seq: message.seq, ...response });
}).catch(err => {
if (!(err instanceof errors.RequestError)) {
console.error(`Unexpected error while responding to ${this.requestType}`, err);
}
link.send(this.responseType, { seq: message.seq, error: err.message });
});
}, this._requestValidator);
}
}
/**
* Send request over the given link
*
* Sends the given data over the link as the request payload and waits
* for the response.
*
* @returns {object} response data
*/
async send(link, data = {}) {
// XXX validate link target/source?
let seq = link.send(this.requestType, data);
let responseMessage = await link.waitFor(this.responseType, { seq });
if (responseMessage.data.error) {
throw new errors.RequestError(responseMessage.data.error);
}
return responseMessage.data;
}
}
/**
* Represents a request sent to the master that is forwarded to an instance
*
* This is a two part request with a master part and a slave part. The
* master part is done by the forwardInstanceRequest method on the master
* side of link which finds the slave the requuested instance is on and
* forwards it there using the foward method.
*
* The slave part is done by the forwardInstanceRequest on the slave side of
* the link and finds the instance and then calls the handler with it and
* the message.
*/
class InstanceRequest {
constructor({
type, sources, requestProperties = {}, responseProperties = {}
}) {
this._masterRequest = new Request({
type: type + '_master',
sources,
targets: ['master'],
requestProperties: {
"instance_id": { type: "integer" },
...requestProperties,
},
responseProperties,
});
this._slaveRequest = new Request({
type: type + '_slave',
sources: ['master'],
targets: ['slave'],
requestProperties: {
"instance_id": { type: "integer" },
...requestProperties,
},
responseProperties,
});
}
/**
* Attach to a link
*
* Attach master/slave part of the instance request. If this is the
* slave side the handler argument is required.
*
* Does nothing if the link is neither a source nor a target for the
* slave or master request.
*
* @param {Link} link - The link to attach to.
* @param {Function} handler -
* Async function to to handle the instance request. It is passed on
* to forwardInstanceRequest on the slave side.
* @throws {Error} if the handler is needed and not defined.
*/
attach(link, handler) {
this._masterRequest.attach(link, async (message) => {
return await link.forwardInstanceRequest(this, message);
});
if (handler) {
this._slaveRequest.attach(link, async (message) => {
return await link.forwardInstanceRequest(handler, message);
});
} else {
this._slaveRequest.attach(link);
}
}
/**
* Forward the message over the link
*
* This method is expected to be called by forwardInstanceRequest on the
* master with the slave link that the instance is on.
*/
async forward(link, message) {
let response = await this._slaveRequest.send(link, message.data);
delete response.seq;
return response;
}
/**
* Send request over the given link
*
* Sends the given data over the link to the master and waits for it to
* forward the request to the slave with the instance on and return the
* result of it.
*
* @returns {object} response data
*/
async send(link, data = {}) {
return await this._masterRequest.send(link, data);
}
}
let messages = {}
// Management requests
messages.listSlaves = new Request({
type: 'list_slaves',
sources: ['control'],
targets: ['master'],
responseProperties: {
"list": {
type: "array",
items: {
additionalProperties: false,
required: ["agent", "version", "name", "id", "connected"],
properties: {
"agent": { type: "string" },
"version": { type: "string" },
"name": { type: "string" },
"id": { type: "integer" },
"connected": { type: "boolean" },
},
},
},
},
});
messages.listInstances = new Request({
type: 'list_instances',
sources: ['control'],
targets: ['master'],
responseProperties: {
"list": {
type: "array",
items: {
additionalProperties: false,
required: ["name", "id", "slave_id"],
properties: {
"name": { type: "string" },
"id": { type: "integer" },
"slave_id": { type: "integer" },
},
},
},
},
});
messages.setInstanceOutputSubscriptions = new Request({
type: 'set_instance_output_subscriptions',
sources: ['control'],
targets: ['master'],
requestProperties: {
"instance_ids": {
type: "array",
items: { type: "integer" },
},
},
});
messages.createInstanceCommand = new Request({
type: 'create_instance',
sources: ['control'],
targets: ['master'],
requestProperties: {
"name": { type: "string" },
"slave_id": { type: "integer" },
},
});
messages.createInstance = new Request({
type: 'create_instance',
sources: ['master'],
targets: ['slave'],
requestProperties: {
"id": { type: "integer" },
"options": {
additionalProperties: false,
required: [
"name", "description", "visibility", "username", "token", "game_password",
"verify_user_identity", "allow_commands", "auto_pause",
],
properties: {
"name": { type: "string" },
"description": { type: "string" },
"visibility": { type: "object" },
"username": { type: "string" },
"token": { type: "string" },
"game_password": { type: "string" },
"verify_user_identity": { type: "boolean" },
"allow_commands": { type: "string" },
"auto_pause": { type: "boolean" },
},
},
},
});
messages.createSave = new InstanceRequest({
type: 'create_save',
sources: ['control'],
});
messages.startInstance = new InstanceRequest({
type: 'start_instance',
sources: ['control'],
});
messages.stopInstance = new InstanceRequest({
type: 'stop_instance',
sources: ['control'],
});
messages.deleteInstance = new InstanceRequest({
type: 'delete_instance',
sources: ['control'],
});
messages.sendRcon = new InstanceRequest({
type: 'send_rcon',
sources: ['control'],
requestProperties: {
"command": { type: "string" },
},
responseProperties: {
"result": { type: "string" },
}
});
class Event {
constructor({ type, sources, targets, eventProperties = {} }) {
this.type = type;
this.sources = sources;
this.targets = targets;
this.eventType = type + '_event';
this._eventValidator = schema.compile({
$schema: "http://json-schema.org/draft-07/schema#",
properties: {
"type": { const: this.eventType },
"data": {
additionalProperties: false,
required: Object.keys(eventProperties),
properties: eventProperties,
},
},
});
}
attach(link, handler) {
// Target side. Note: Source and target is reversed here.
if (this.targets.includes(link.source) && this.sources.includes(link.target)) {
if (!handler) {
throw new Error(`Missing handler for ${this.eventType} on ${link.target}-${link.source} link`);
}
link.setHandler(this.eventType, message => {
// XXX Should event handlers even be allowed to be async?
handler.call(link, message).catch(err => {
console.error(`Unexpected error while handling ${this.eventType}`, err);
});
}, this._eventValidator);
}
}
/**
* Send event over the given link
*
* Sends the given event data over the link.
*/
send(link, data = {}) {
link.send(this.eventType, data);
}
}
messages.updateInstances = new Event({
type: 'update_instances',
sources: ['slave'],
targets: ['master'],
eventProperties: {
"instances": {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: ["id", "name"],
properties: {
"id": { type: "integer" },
"name": { type: "string" },
},
},
},
},
});
messages.instanceOutput = new Event({
type: 'instance_output',
sources: ['slave', 'master'],
targets: ['master', 'control'],
eventProperties: {
"instance_id": { type: "integer" },
"output": {
type: "object",
additionalProperties: false,
required: ["source", "received", "format", "type", "message"],
properties: {
"source": { type: "string" },
"received": { type: "number" },
"format": { type: "string" },
"time": { type: "string" },
"level": { type: "string" },
"file": { type: "string" },
"type": { type: "string" },
"action": { type: "string" },
"message": { type: "string" },
},
},
},
});
/**
* Attaches all requests and events to the given link
*
* Loops through all builtin messages defined and attaches all of them
* to the link. The handler used for the messageis looked up on the
* link instance as the concatenation of the name of the message, the
* name of message class, and 'Handler'.
*/
function attachAllMessages(link) {
for (let [name, message] of Object.entries(messages)) {
message.attach(link, link[name + message.constructor.name + 'Handler']);
}
}
module.exports = {
Request,
InstanceRequest,
Event,
attachAllMessages,
messages,
}