mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
200 lines
5.4 KiB
JavaScript
200 lines
5.4 KiB
JavaScript
"use strict";
|
|
|
|
const schema = require("lib/schema");
|
|
const errors = require("lib/errors");
|
|
const messages = require("lib/link/messages");
|
|
|
|
// Some definitions for the terminology used here:
|
|
// link: Either side of a master - client connection
|
|
// connector: Adapter between a link and a socket connection
|
|
// connection: the master server side of a link
|
|
// client: the side that is not the master server 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
|
|
*/
|
|
class Link {
|
|
constructor(source, target, connector) {
|
|
this.source = source;
|
|
this.target = target;
|
|
this.connector = connector;
|
|
|
|
this._waiters = new Map();
|
|
this._handlers = new Map();
|
|
this._validators = new Map();
|
|
|
|
// Process messages received by the connector
|
|
connector.on('message', payload => {
|
|
try {
|
|
this.processMessage(payload);
|
|
} catch (err) {
|
|
if (err instanceof errors.InvalidMessage) {
|
|
this.connector.close(`Invalid message: ${err.message}`);
|
|
} else {
|
|
this.connector.emit('error', err);
|
|
}
|
|
}
|
|
});
|
|
|
|
this.setHandler('close', message => {
|
|
console.log(`SOCKET | Received close: ${message.data.reason}`);
|
|
this.connector.disconnect();
|
|
}, schema.close);
|
|
}
|
|
|
|
/**
|
|
* Process a received message on the link
|
|
*
|
|
* Validates and invokes the handler and/or waiters for a message that has
|
|
* been received. An unhandled message is considered to be an error.
|
|
*
|
|
* @throws {InvalidMessage}
|
|
* if validation failed, is missing, the message is invalid, or no
|
|
* handlers/waiters were invoked to handle the message.
|
|
*/
|
|
processMessage(message) {
|
|
if (!schema.message(message)) {
|
|
throw new errors.InvalidMessage("Malformed");
|
|
}
|
|
|
|
let validator = this._validators.get(message.type);
|
|
if (!validator) {
|
|
throw new errors.InvalidMessage(`No validator for ${message.type} on ${this.source}-${this.target}`);
|
|
}
|
|
|
|
if (!validator(message)) {
|
|
// TODO better logging of errors encountered validating
|
|
console.error(validator.errors);
|
|
throw new errors.InvalidMessage(`Validation failed for ${message.type}`);
|
|
}
|
|
|
|
let hadHandlers = this._processHandler(message);
|
|
let hadWaiters = this._processWaiters(message);
|
|
|
|
if (!hadWaiters && !hadHandlers) {
|
|
// XXX console.error(`Unhandled message: ${JSON.stringify(message, null, 4)}`);
|
|
throw new errors.InvalidMessage(`Unhandled message ${message.type}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoke the handler for this message if it exists
|
|
*
|
|
* @returns {Boolean} true if a handler was invoked.
|
|
*/
|
|
_processHandler(message) {
|
|
let handler = this._handlers.get(message.type);
|
|
if (handler) {
|
|
handler(message);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invokes the waiters for this message if any
|
|
*
|
|
* @returns {Boolean} true if a waiter was triggered.
|
|
*/
|
|
_processWaiters(message) {
|
|
let { seq, type, data } = message;
|
|
let waiters = this._waiters.get(type);
|
|
if (!waiters || !waiters.length) {
|
|
return false;
|
|
}
|
|
|
|
let matched = [];
|
|
waitersLoop: for (let index = 0; index < waiters.length; index++) {
|
|
let waiter = waiters[index];
|
|
for (let [key, value] of Object.entries(waiter.data)) {
|
|
if (data[key] !== value) {
|
|
continue waitersLoop;
|
|
}
|
|
}
|
|
|
|
waiter.resolve(message);
|
|
matched.push(index);
|
|
}
|
|
|
|
matched.reverse();
|
|
for (let index of matched) {
|
|
waiters.splice(index, 1);
|
|
}
|
|
|
|
return matched.length > 0;
|
|
}
|
|
|
|
/**
|
|
* Set handler for a message type
|
|
*
|
|
* Set function called upon receiving a message of the given type.
|
|
*
|
|
* @param {string} type - Type of message to listen to
|
|
* @param {function} handler -
|
|
* Callack to invoke on messages of the given type.
|
|
* @param {function} validator -
|
|
* Function validating the message have the correct format.
|
|
*/
|
|
setHandler(type, handler, validator) {
|
|
if (this._handlers.has(type)) {
|
|
throw new Error(`${type} already has a handler`);
|
|
}
|
|
|
|
if (!validator) {
|
|
throw new Error("validator is required");
|
|
}
|
|
|
|
this.setValidator(type, validator);
|
|
this._handlers.set(type, handler);
|
|
}
|
|
|
|
/**
|
|
* Set validator for a message type
|
|
*
|
|
* Set function called upon receiving a message of the given type in
|
|
* order to validate the message. The function should return true if
|
|
* the message is valid and false otherwise.
|
|
*
|
|
* @param {string} type - Type of message to validate
|
|
* @param {function} validator - Validation function.
|
|
*/
|
|
setValidator(type, validator) {
|
|
if (this._validators.has(type)) {
|
|
throw new Error(`${type} already has a validator`);
|
|
}
|
|
|
|
this._validators.set(type, validator);
|
|
}
|
|
|
|
/**
|
|
* Wait for a message matching given type and data
|
|
*
|
|
* Waits for a message over the link that matches the type given as well
|
|
* as any properties specified in data. For example specifying { foo:
|
|
* 21 } as the data will cause it to wait until a message with the right
|
|
* type and a foo property value of 21 in the data payload.
|
|
*/
|
|
async waitFor(type, data) {
|
|
if (!this._validators.has(type)) {
|
|
throw new Error(`No validator for ${type} on ${this.source}-${this.target}`);
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
if (!this._waiters.has(type)) {
|
|
this._waiters.set(type, [{ data, resolve }]);
|
|
} else {
|
|
this._waiters.get(type).push({ data, resolve });
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
module.exports = {
|
|
Link,
|
|
}
|