UNPKG

@atomist/automation-client

Version:

Atomist API for software low-level client

211 lines • 8.71 kB
"use strict"; 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