@skyway-sdk/analytics-client
Version:
The official Next Generation JavaScript SDK for SkyWay
253 lines • 10.8 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Socket = void 0;
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
const payloadTypes_1 = require("./payloadTypes");
const event_1 = require("./utils/event");
const ServerEventType = ['Open', 'Acknowledge'];
const getReconnectWaitTime = (reconnectCount) => {
return (Math.pow(2, reconnectCount) + Math.random()) * 1000;
};
class Socket {
constructor({ sessionEndpoint, token, logger, sdkVersion, contextId }) {
this._isOpen = false;
this._isClosed = false;
this._reconnectCount = 0;
this.connectionState = 'connecting'; // コンストラクタ作成時点で繋ぎにいくので初期値はconnecting
this.onConnectionStateChanged = new event_1.Event();
this.onOpened = new event_1.Event();
this.onTokenExpired = new event_1.Event();
this.onEventReceived = new event_1.Event();
this.onConnectionFailed = new event_1.Event();
this._resendClientEvents = [];
this._sessionEndpoint = sessionEndpoint;
this._token = token;
this._logger = logger;
this._sdkVersion = sdkVersion;
this._contextId = contextId;
this._connect();
}
_setConnectionState(state) {
if (this.connectionState === state)
return;
this._logger.debug(`connectionState changed : ${state}`);
this.connectionState = state;
this.onConnectionStateChanged.emit(state);
}
_connect() {
let ws;
try {
// We use the SubProtocol header to send the token.
// This is because the browser's WebSocket class does not allow the header to be changed freely.
const subProtocol = `SkyWayAuthToken!${this._token}`;
const wsProperties = {
sdkPlatform: 'js',
sdkVersion: this._sdkVersion,
contextId: this._contextId,
};
const queryString = Object.entries(wsProperties)
.filter(([_, v]) => v !== undefined)
.map((pair) => pair.join('='))
.join('&');
const wsURL = `${this._sessionEndpoint}?${queryString}`;
ws = new isomorphic_ws_1.default(wsURL, subProtocol);
this._logger.debug(`Connecting to analytics-logging-server: ${this._sessionEndpoint}`);
ws.onerror = (event) => {
this._logger.error('WebSocket error occurred', event.error);
ws.close(4202);
};
}
catch (err) {
const error = err instanceof Error ? err : new Error();
this._logger.error('Failed to create WebSocket instance', error);
this.reconnect();
return;
}
ws.onopen = () => {
this._logger.debug('Connected to analytics-logging-server');
};
ws.onclose = (event) => {
const logMessage = 'Close event fired: ' + JSON.stringify({ code: event.code, reason: event.reason, type: event.type });
// 1000, 4000~4099: normal case (should not reconnect)
// 1009, 4100~4199: non-normal case (should not reconnect)
// 4200~4299: non-normal case (should reconnect)
// others: unexpected case (should reconnect)
if ((4100 <= event.code && event.code <= 4199) || event.code === 1009) {
this._logger.error(logMessage, new Error());
}
else {
this._logger.debug(logMessage);
}
if (event.code !== 1000 && event.code !== 1009 && !(4000 <= event.code && event.code <= 4199)) {
if (4200 === event.code) {
this.onTokenExpired.emit();
}
else {
this.reconnect();
}
return;
}
this._logger.debug('Closed the connection to analytics-logging-server');
this.onConnectionFailed.emit({ code: event.code, reason: event.reason });
this.close();
};
ws.onmessage = (event) => {
this._messageHandler(event.data);
};
this._ws = ws;
}
updateAuthToken(token) {
this._token = token;
}
reconnect() {
if (this._ws !== undefined) {
this._ws.close(4000);
}
this._ws = undefined;
this._isOpen = false;
// getReconnectWaitTime により30秒程まで再試行するため5を指定している
if (this._reconnectCount >= 5) {
this.onConnectionFailed.emit({});
this.close();
this._logger.error('Failed to reconnect for five times', new Error());
}
else {
this._setConnectionState('reconnecting');
const waitTime = getReconnectWaitTime(this._reconnectCount);
this._reconnectTimer = setTimeout(() => {
this._connect();
this._reconnectCount++;
this._logger.debug(`Try to reconnect: count = ${this._reconnectCount}`);
}, waitTime);
}
}
close() {
this._isClosed = true;
this.destroy();
}
destroy() {
this._setConnectionState('closed');
this.onConnectionStateChanged.removeAllListeners();
this.onOpened.removeAllListeners();
this.onEventReceived.removeAllListeners();
this.onConnectionFailed.removeAllListeners();
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer);
}
if (this._ws !== undefined) {
this._ws.close(1000);
}
}
send(clientEvent) {
return __awaiter(this, void 0, void 0, function* () {
if (this._ws === undefined || !this._isOpen || this._ws.readyState !== isomorphic_ws_1.default.OPEN) {
this._logger.debug('Try to reconnect because connection is lost');
this.resendAfterReconnect(clientEvent);
return;
}
const data = JSON.stringify(clientEvent.toJSON());
this._ws.send(data, (err) => {
if (err) {
this._logger.debug(`Try to reconnect because failed to send: ${err.message}`);
this.resendAfterReconnect(clientEvent);
return;
}
});
});
}
resendAfterReconnect(data) {
const isEventExist = this._resendClientEvents.some((event) => event.id === data.id);
if (!isEventExist)
this._resendClientEvents.push(data);
// この関数が複数回呼ばれた際に再接続の試行が重複しないよう、connectionStateを確認してから再接続する
if (this.connectionState !== 'reconnecting') {
this.reconnect();
}
}
pushResendClientEventsQueue(data) {
this._resendClientEvents.push(data);
}
isClosed() {
return this._isClosed;
}
_messageHandler(data) {
if (typeof data !== 'string') {
this._logger.error('Received invalid message: not string', new Error());
return;
}
let parsedData;
try {
parsedData = JSON.parse(data);
}
catch (err) {
const error = err instanceof Error ? err : new Error();
this._logger.error('Received invalid message: parse error', error);
return;
}
if (!isServerEvent(parsedData)) {
this._logger.error(`Received invalid message: ${JSON.stringify(parsedData)}`, new Error());
return;
}
if (parsedData.type === 'Open') {
if (!(0, payloadTypes_1.isOpenServerEventPayload)(parsedData.payload)) {
this._logger.error(`Received invalid message: ${JSON.stringify(parsedData.payload)}`, new Error());
return;
}
this._logger.debug('Received a open event');
this._isOpen = true;
this._setConnectionState('connected');
if (this._reconnectCount !== 0) {
this._reconnectCount = 0;
this._logger.debug('Succeeded to reconnect');
}
if (this._resendClientEvents.length > 0) {
for (const event of this._resendClientEvents) {
if (this._ws === undefined || !this._isOpen || this._ws.readyState !== isomorphic_ws_1.default.OPEN) {
this._logger.error(`Failed to resend event because connection lost after reconnect: ${event}`, new Error());
continue;
}
const data = JSON.stringify(event.toJSON());
this._ws.send(data, (err) => {
if (err) {
this._logger.error(`Failed to resend event: ${event}`, err);
return;
}
this._logger.debug(`Succeed to resend ClientEvent: ${event}`);
});
}
this._logger.debug('Process of resending ClientEvents is completed');
this._resendClientEvents = [];
}
this.onOpened.emit(parsedData.payload);
}
else {
this._logger.debug(`Received the event: ${parsedData.type}, payload: ${JSON.stringify(parsedData.payload)}`);
this.onEventReceived.emit(parsedData);
}
}
}
exports.Socket = Socket;
function isServerEvent(data) {
if (!data || typeof data !== 'object')
return false;
if (typeof data.type !== 'string' || !ServerEventType.includes(data.type))
return false;
if (typeof data.id !== 'string')
return false;
if (data.payload && typeof data.payload !== 'object')
return false;
return true;
}
//# sourceMappingURL=socket.js.map