@atomist/automation-client
Version:
Atomist API for software low-level client
211 lines • 8.71 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const stringify = require("json-stringify-safe");
const promiseRetry = require("promise-retry");
const serializeError = require("serialize-error");
const zlib = require("zlib");
const httpClient_1 = require("../../../spi/http/httpClient");
const logger_1 = require("../../../util/logger");
const shutdown_1 = require("../../util/shutdown");
const RequestProcessor_1 = require("../RequestProcessor");
const WebSocketMessageClient_1 = require("./WebSocketMessageClient");
class WebSocketClient {
constructor(registrationCallback, configuration, requestProcessor) {
this.registrationCallback = registrationCallback;
this.configuration = configuration;
this.requestProcessor = requestProcessor;
}
start() {
const connection = register(this.registrationCallback, this.configuration, this.requestProcessor, 5)
.then(registration => connect(this.registrationCallback, registration, this.configuration, this.requestProcessor));
return connection.then(() => {
shutdown_1.registerShutdownHook(() => {
reconnect = false;
logger_1.logger.debug("Closing WebSocket connection");
ws.close();
return Promise.resolve(0);
}, 100000, "closing websocket");
}).catch(() => {
logger_1.logger.error("Persistent error registering with Atomist, exiting");
process.exit(1);
});
}
}
exports.WebSocketClient = WebSocketClient;
let reconnect = true;
let ping = 0;
let pong;
let ws;
function connect(registrationCallback, registration, configuration, requestProcessor) {
// Functions are inline to avoid "this" peculiarities
function invokeCommandHandler(chr) {
requestProcessor.processCommand(chr);
}
function invokeEventHandler(e) {
requestProcessor.processEvent(e);
}
return new Promise(resolve => {
logger_1.logger.debug(`Opening WebSocket connection`);
ws = configuration.ws.client.factory.create(registration);
let timer;
ws.on("open", function open() {
// tslint:disable-next-line:no-invalid-this
requestProcessor.onConnect(this);
resolve(ws);
// Install ping/pong timer and shutdown hooks
timer = setInterval(() => {
if (pong + 1 < ping) {
reset();
ws.terminate();
logger_1.logger.error("Missing ping/pong from the server. Closing WebSocket");
}
else {
WebSocketMessageClient_1.sendMessage({ ping }, ws, false);
ping++;
}
}, 10000);
});
ws.on("message", function incoming(data) {
function handleMessage(reqString) {
let request;
try {
request = JSON.parse(reqString);
}
catch (err) {
logger_1.logger.error(`Failed to parse incoming message: %s`, reqString);
return;
}
try {
if (isPing(request)) {
WebSocketMessageClient_1.sendMessage({ pong: request.ping }, ws, false);
}
else if (isPong(request)) {
pong = request.pong;
}
else if (isControl(request)) {
logger_1.logger.debug("WebSocket connection stopped listening for incoming messages");
}
else {
if (RequestProcessor_1.isCommandIncoming(request)) {
invokeCommandHandler(request);
}
else if (RequestProcessor_1.isEventIncoming(request)) {
invokeEventHandler(request);
}
else {
logger_1.logger.error(`Unknown message payload received: ${data}`);
}
}
}
catch (err) {
logger_1.logger.error("Failed processing of message payload with: %s", JSON.stringify(serializeError(err)));
}
}
if (configuration.ws.compress) {
zlib.gunzip(data, (err, result) => {
if (!err) {
handleMessage(result.toString());
}
else {
logger_1.logger.warn(`Failed to decompress incoming message: %s`, data);
handleMessage(data);
}
});
}
else {
handleMessage(data);
}
});
// On close this websocket is meant to reconnect
ws.on("close", (code, message) => {
if (code) {
logger_1.logger.warn(`WebSocket connection closed with ${code}: ${message}`);
}
else {
logger_1.logger.warn(`WebSocket connection closed`);
}
reset();
// Only attempt to reconnect if we aren't shutting down
if (reconnect) {
register(registrationCallback, configuration, requestProcessor)
.then(reg => connect(registrationCallback, reg, configuration, requestProcessor))
.then(() => resolve(ws));
}
});
ws.on("error", err => {
if (err) {
logger_1.logger.warn(`WebSocket error occurred: ${JSON.stringify(serializeError(err))}`);
}
});
function reset() {
requestProcessor.onDisconnect();
clearInterval(timer);
ping = 0;
pong = 0;
}
});
}
function register(registrationCallback, configuration, handler, retries = 100) {
const registrationPayload = registrationCallback();
logger_1.logger.debug(`Registering ${registrationPayload.name}:${registrationPayload.version} ` +
`with Atomist at '${configuration.endpoints.api}': ${stringify(registrationPayload)}`);
const retryOptions = {
retries,
factor: 3,
minTimeout: 1 * 500,
maxTimeout: 5 * 1000,
randomize: true,
};
return promiseRetry(retryOptions, (retry, retryCount) => {
if (retryCount > 1) {
logger_1.logger.warn("Retrying registration due to previous error");
}
const client = configuration.http.client.factory.create(configuration.endpoints.api);
const authorization = `Bearer ${configuration.apiKey}`;
return client.exchange(configuration.endpoints.api, {
body: registrationPayload,
method: httpClient_1.HttpMethod.Post,
headers: { Authorization: authorization },
options: {
timeout: configuration.ws.timeout,
},
retry: { retries: 0, log: false },
})
.then(result => {
const registration = result.body;
registration.name = registrationPayload.name;
registration.version = registrationPayload.version;
handler.onRegistration(registration);
return registration;
})
.catch(error => {
const nameVersion = `${registrationPayload.name}@${registrationPayload.version}`;
if (error.response && error.response.status === 409) {
logger_1.logger.error(`Registration failed because a session for ${nameVersion} is already active`);
retry(error);
}
else if (error.response && (error.response.status === 400
|| error.response.status === 401
|| error.response.status === 403
|| error.response.status === 500)) {
logger_1.logger.error(`Registration failed with code '%s': '%s'`, error.response.status, JSON.stringify(error.response.data));
process.exit(1);
}
else {
logger_1.logger.error("Registration failed with '%s'", error);
retry(error);
}
});
});
}
/* tslint:disable:no-null-keyword */
function isPing(a) {
return a.ping !== null && a.ping !== undefined;
}
function isPong(a) {
return a.pong !== null && a.pong !== undefined;
}
function isControl(a) {
return a.control !== null && a.control !== undefined;
}
//# sourceMappingURL=WebSocketClient.js.map