Files
factorio-clusterio/test/lib/link/connector.js
T
Hornwitser de48d807f1 Refactor connector logic
Refine the state flow used in the connector abstractions to better
handle close while connecting and resuming.  The new logic is to close
the connection immediately in these two states instead of continuing
trying to connect and then giving up some point later.

For the connectors the handshake state is split into connecting
and resuming, new is replaced by closed, and the closing state is turned
into a boolean flag as it's possible that closing is desired while
connecting, connected or resuming.  The connecting state is not present
in the server connector because there's nothing for the server to do in
that connector state.

The timeout logic is completely removed, once a connection attempt is
started for the client it will continue to attempt to connect, attempt
to resume dropped connections and attempt reconnects until either an
authentication error occurs or the client explicitly requests to close
the connection.

A new session timeout is added to control how long to attempt to resume
the connection until it's considered dead and invalidated.  This allows
stale connections to give up earlier, and swallow up less data while in
the dropped but not yet invalidated state.

For the client side sending messages while it's connecting has been
changed to throw an error instead of queuing up the message and
(possibly) sending it after the connection is established.  Code
shouldn't try to send messages without knowing if the connection it's
trying to send them over is active or not.
2021-08-30 20:20:49 +02:00

136 lines
4.6 KiB
JavaScript

"use strict";
const assert = require("assert").strict;
const events = require("events");
const mock = require("../../mock");
const libLink = require("@clusterio/lib/link");
describe("lib/link/connectors", function() {
describe("class WebSocketBaseConnector", function() {
let testConnector = new libLink.WebSocketBaseConnector();
describe("._dropSendBufferSeq()", function() {
beforeEach(function() {
testConnector._sendBuffer = [
{ seq: 1, type: "test", data: {} },
{ seq: 2, type: "test", data: {} },
{ seq: 4, type: "test", data: {} },
];
});
it("should do nothing if passed null", function() {
testConnector._dropSendBufferSeq(null);
assert(testConnector._sendBuffer.length === 3, "entries were dropped");
});
it("should drop up to sequence", function() {
testConnector._dropSendBufferSeq(2);
assert.equal(testConnector._sendBuffer.length, 1, "incorrect number of entries dropped");
});
it("should ignore holes", function() {
testConnector._dropSendBufferSeq(3);
assert.equal(testConnector._sendBuffer.length, 1, "incorrect number of entries dropped");
});
});
});
describe("class WebSocketClientConnector", function() {
let testConnector = new libLink.WebSocketClientConnector("url", 1);
testConnector._socket = new mock.MockSocket();
describe(".register()", function() {
it("is abstract", function() {
assert.throws(
() => testConnector.register(),
new Error("Abstract function")
);
});
});
describe(".sendHandshake()", function() {
it("calls send on the socket", function() {
testConnector._state = "connecting";
testConnector._socket.sentMessages = [];
testConnector.sendHandshake("test", { test: true });
assert.deepEqual(
testConnector._socket.sentMessages.map(JSON.parse),
[{ seq: null, type: "test", data: { test: true }}]
);
});
});
describe(".send()", function() {
it("calls send on the socket", function() {
testConnector._state = "connected";
testConnector._socket.sentMessages = [];
let seq = testConnector.send("test", { test: true });
assert.deepEqual(
testConnector._socket.sentMessages.map(JSON.parse),
[{ seq, type: "test", data: { test: true }}]
);
});
});
describe(".close()", function() {
it("should call close", function() {
testConnector._socket.closeCalled = false;
testConnector.close("test reason");
assert(testConnector._socket.closeCalled, "Close was not called on the socket");
});
});
describe(".processHandshake()", function() {
it("should close on invalid message", function() {
testConnector._socket.closeCalled = false;
testConnector._processHandshake({ data: "invalid message" });
assert(testConnector._socket.closeCalled, "Close was not called on the socket");
});
it("should call register on hello", function() {
let called = false;
testConnector.register = () => { called = true; };
testConnector._processHandshake({ seq: 1, type: "hello", data: { version: "test", plugins: {} }});
assert(called, "register was not called");
});
it("should emit connect on ready", async function() {
let result = events.once(testConnector, "connect");
testConnector._processHandshake(
{ seq: 1, type: "ready", data: {
session_token: "x",
session_timeout: 20,
heartbeat_interval: 10,
}}
);
await result;
clearInterval(testConnector._heartbeatId);
});
});
describe("._attachSocketHandlers()", function() {
it("should attach handlers", function() {
testConnector._attachSocketHandlers();
assert(testConnector._socket.events.size > 0, "No handlers were attached");
});
it("should throw on message received in invalid state", function() {
testConnector._state = "closed";
assert.throws(
() => testConnector._socket.events.get("message")("{}"),
new Error("Received message in unexpected state closed")
);
});
it("should call _processHandshake on message in handshake state", function() {
testConnector._state = "connecting";
let called = false;
testConnector._processHandshake = () => { called = true; };
testConnector._socket.events.get("message")("{}");
assert(called, "_processHandshake was not called");
});
it("should emit message on message in connected state", async function() {
testConnector._socket.sentMessages = [];
testConnector._state = "connected";
let result = events.once(testConnector, "message");
testConnector._socket.events.get("message")('{"message":true}');
assert.deepEqual(await result, [{ message: true }]);
});
});
});
});