infobip-rtc
Version:
Infobip RTC JavaScript SDK - Infobip WebRTC API Implementation
186 lines • 7.12 kB
JavaScript
import Properties from "./Properties";
import Status from "./Status";
import { version } from "../Version";
import Retry from '../util/Retry';
import Browser from '../util/Browser';
import { PortunusHostEvaluator } from "./PortunusHostEvaluator";
export class InfobipGatewayImpl {
constructor(eventEmitter, logger, accessToken) {
this.eventEmitter = eventEmitter;
this.logger = logger;
this.status = Status.OFFLINE;
this.accessToken = accessToken;
this.portunusHostEvaluator = new PortunusHostEvaluator(this.logger);
this.deviceInfo = this.encodeDeviceInfo();
this.initRetry();
}
async connect(isReconnect = false) {
if (!this.portunusInstanceHash) {
this.portunusHost = await this.portunusHostEvaluator.evaluate(this.accessToken);
}
this.ws = new WebSocket(this.generatePortunusUrl(), [this.accessToken.jwt, this.deviceInfo]);
this.status = isReconnect ? Status.RECONNECTING : Status.CONNECTING;
this.ws.onopen = this.onOpen.bind(this);
this.ws.onclose = this.onClose.bind(this);
this.ws.onmessage = this.onMessage.bind(this);
this.ws.onerror = this.onError.bind(this);
}
disconnect() {
if ([Status.RECONNECTING, Status.CONNECTING, Status.CONNECTED].indexOf(this.status) >= 0) {
this.ws.close(4000, "USER_DISCONNECT");
}
if (this.retry.cancel()) {
this.eventEmitter.emit('disconnected', ({ code: 4000, reason: 'USER_DISCONNECT' }));
}
this.status = Status.OFFLINE;
}
;
send(data) {
if (this.status !== Status.CONNECTED) {
this.logger?.error(`Socket to Infobip WebRTC Gateway not opened, could not send ${JSON.stringify(data)}!`);
return;
}
this.logSendingMessage(data);
let message = JSON.stringify(data);
this.ws.send(message);
}
setLogger(logger) {
this.logger = logger;
}
encodeDeviceInfo() {
let generatedDeviceInfo = JSON.stringify(this.generateDeviceInfo());
return this.base64EncodeUrl(window.btoa(generatedDeviceInfo));
}
generatePortunusUrl() {
let url = `wss://${this.portunusHost}`;
if (this.portunusInstanceHash) {
url += `?instance=${this.portunusInstanceHash}`;
}
return url;
}
logSendingMessage(message) {
if (message.action !== 'heartbeat' && message.action !== 'heartbeat_resp') {
this.logger?.debug(`Sending ${JSON.stringify(message)} to Infobip WebRTC Gateway...`);
}
}
onOpen() {
this.logger?.info('Socket to Infobip WebRTC Gateway opened.');
this.eventEmitter.once("registered", event => {
this.portunusInstanceHash = event.instanceHash;
});
let reconnected = this.status === Status.RECONNECTING;
if (reconnected) {
this.eventEmitter.once("registered", () => {
this.eventEmitter.emit('reconnected');
});
}
this.status = Status.CONNECTED;
this.scheduleHeartbeat();
this.initRetry();
}
onMessage(message) {
let data = JSON.parse(message.data);
this.logReceivedMessage(data);
if (data.event === 'heartbeat_resp') {
this.measureHeartbeatRate();
this.cancelHeartbeatCheck();
return;
}
if (data.event === 'heartbeat') {
this.sendHeartbeatResponse();
return;
}
if (data.event === 'registration_failed') {
this.disconnect();
}
this.eventEmitter.emit(data.event, data);
}
sendHeartbeatResponse() {
this.send({ action: 'heartbeat_resp' });
}
logReceivedMessage(message) {
if (message.event !== 'heartbeat_resp' && message.event !== 'heartbeat') {
this.logger?.debug(`Received message from Infobip Gateway: ${JSON.stringify(message)}.`);
}
}
onClose(event) {
this.logger?.info(`Socket closed, code ${event.code}, reason: ${event.reason}.`);
if (event.code === 1013) {
this.portunusInstanceHash = null;
}
const prevStatus = this.status;
this.status = Status.OFFLINE;
this.cleanup();
if (prevStatus !== Status.OFFLINE && event.code < 4000) {
this.retry.retry();
if (prevStatus === Status.CONNECTED) {
this.eventEmitter.emit('reconnecting');
}
}
else {
this.eventEmitter.emit('disconnected', event);
}
}
cleanup() {
clearInterval(this.heartbeat);
this.cancelHeartbeatCheck();
this.ws.onopen = null;
this.ws.onclose = null;
this.ws.onmessage = null;
this.ws.onerror = null;
delete this.ws;
}
onError(event) {
this.logger?.error(`Socket error: ${JSON.stringify(event)}.`);
}
scheduleHeartbeat() {
this.heartbeat = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.send({ 'action': 'heartbeat' });
this.scheduleHeartbeatCheck();
}
}, Properties.HEARTBEAT_PERIOD);
}
scheduleHeartbeatCheck() {
if (this.heartbeatCheck) {
this.logger?.warn("Did not receive previous heartbeat response, skipping new heartbeat check.");
return;
}
this.heartbeatCheck = setTimeout(() => {
this.logger?.info(`No heartbeat response in ${Properties.HEARTBEAT_TIMEOUT} milliseconds. Closing socket...`);
this.onClose({ code: 3000, reason: 'Heartbeat timeout.' });
}, Properties.HEARTBEAT_TIMEOUT);
this.heartbeatStartTime = performance.now();
}
initRetry() {
this.retry?.cancel();
this.retry = new Retry(async (iteration) => {
this.logger?.info(`Reconnect attempt ${iteration}...`);
await this.connect(true);
}, Retry.exponentialDelay(Properties.RECONNECT_INITIAL_DELAY), Properties.RECONNECT_MAX_DELAY, Properties.RECONNECT_MAX).catch(reason => {
this.logger?.info(`Reconnecting failed, reason: ${reason}`);
this.eventEmitter.emit('disconnected', { reason: 'Connection error.' });
});
}
generateDeviceInfo() {
let browser = new Browser();
return {
sdk: { type: 'js', version: version },
device: { browser: browser.getBrowser(), os: browser.getOS() }
};
}
base64EncodeUrl(value) {
return value.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
cancelHeartbeatCheck() {
clearTimeout(this.heartbeatCheck);
this.heartbeatCheck = undefined;
}
measureHeartbeatRate() {
const durationMs = performance.now() - this.heartbeatStartTime;
if (durationMs > Properties.HEARTBEAT_SLOW_THRESHOLD) {
this.logger?.warn(`Heartbeat check took too long: ${durationMs} ms`);
}
}
}
//# sourceMappingURL=InfobipGateway.js.map