UNPKG

@signiant/media-shuttle-sdk-base

Version:

The base parent sdk behind other media shuttle sdks (e.g. media-shuttle-sdk)

417 lines (416 loc) 22.3 kB
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; 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 __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; /* eslint-disable import/first */ import { v4 as uuid } from 'uuid'; import request from '../common/request'; import * as constants from '../common/constants'; import DualChannelProviderFactory from '../common/channelproviders/factories/DualChannelProviderFactory'; import { generateEncryptionInfo } from '../common/utils'; import LogManager from './LogManager'; import Registration from './Registration'; import MessageServiceState from '../enums/MessageServiceState'; import { CodedError, ErrorCode } from '../../external'; import ErrorMessages from '../../external/common/ErrorMessages'; import MessageServiceEventType from '../enums/MessageServiceEventType'; import EventEmitter from 'events'; var MAX_RECONNECT_ATTEMPTS = 10; var MAX_RECONNECT_DELAY_MILLISECONDS = 60 * 1000; /** * This class is a bit shady since it primarily exists because of testing. An alternative might be a poor man's * version of dependency injection where our reconnecter friend is passed in as a constructor argument. */ var Reconnecter = /** @class */ (function () { function Reconnecter(messageService, maxReconnectAttempts, maxReconnectDelayMilliseconds) { this._messageService = messageService; this._maxReconnectAttempts = maxReconnectAttempts; this._maxReconnectDelayMilliseconds = maxReconnectDelayMilliseconds; } Reconnecter.prototype.beginReconnectLoop = function () { return __awaiter(this, void 0, void 0, function () { var _loop_1, this_1, attemptNum, state_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _loop_1 = function (attemptNum) { var err_1, waitTimeMilliseconds_1; return __generator(this, function (_b) { switch (_b.label) { case 0: LogManager.info("Reconnection attempt [".concat(attemptNum, "]")); _b.label = 1; case 1: _b.trys.push([1, 3, , 6]); return [4 /*yield*/, this_1._messageService.initializeApplication()]; case 2: _b.sent(); LogManager.info("Reconnection attempt [".concat(attemptNum, "] was successful")); return [2 /*return*/, { value: void 0 }]; case 3: err_1 = _b.sent(); waitTimeMilliseconds_1 = Math.min(Math.pow(2, attemptNum - 1) * 1000, //1, 2, 4 seconds, etc... this_1._maxReconnectDelayMilliseconds); LogManager.warn("Error reconnecting on attempt [".concat(attemptNum, "], waiting [").concat(waitTimeMilliseconds_1, "] seconds: ").concat(err_1.message)); if (!(attemptNum < this_1._maxReconnectAttempts)) return [3 /*break*/, 5]; return [4 /*yield*/, new Promise(function (res) { setTimeout(res, waitTimeMilliseconds_1); })]; case 4: _b.sent(); _b.label = 5; case 5: return [3 /*break*/, 6]; case 6: return [2 /*return*/]; } }); }; this_1 = this; attemptNum = 1; _a.label = 1; case 1: if (!(attemptNum < this._maxReconnectAttempts + 1)) return [3 /*break*/, 4]; return [5 /*yield**/, _loop_1(attemptNum)]; case 2: state_1 = _a.sent(); if (typeof state_1 === "object") return [2 /*return*/, state_1.value]; _a.label = 3; case 3: attemptNum++; return [3 /*break*/, 1]; case 4: throw new Error("Unable to reconnect with app after [".concat(this._maxReconnectAttempts, "] attempts")); } }); }); }; return Reconnecter; }()); export { Reconnecter }; /** * Class responsible for all direct interactions with app. */ var MessageService = /** @class */ (function (_super) { __extends(MessageService, _super); function MessageService(maxReconnectAttempts, maxReconnectDelayMilliseconds) { var _this = _super.call(this) || this; _this._state = MessageServiceState.UNINITIALIZED; _this._maxReconnectAttempts = maxReconnectAttempts; _this._maxReconnectDelayMilliseconds = maxReconnectDelayMilliseconds; _this._force = false; return _this; } MessageService.prototype.getMessageChannelFactory = function () { var encryptionInfo = generateEncryptionInfo(); return new DualChannelProviderFactory({ encryptionInfo: encryptionInfo }); }; MessageService.prototype.fetchMessageChannel = function (messageServiceUrl) { if (messageServiceUrl === void 0) { messageServiceUrl = constants.prodMessageServiceUrl; } return __awaiter(this, void 0, void 0, function () { var finalUrl, providerJson, messagingConfig, factory; return __generator(this, function (_a) { switch (_a.label) { case 0: if (this._messageChannel !== void 0) { LogManager.info('Resetting existing message channel'); this._messageChannel.reset(); } finalUrl = this.createMessageServiceUrl(messageServiceUrl); LogManager.debug("Final url of the messaging service: ".concat(finalUrl)); return [4 /*yield*/, request.get({ url: finalUrl })]; case 1: providerJson = _a.sent(); messagingConfig = providerJson; LogManager.debug('Fetching message channel for messagingConfig', messagingConfig); factory = this.getMessageChannelFactory(); return [2 /*return*/, factory.getProvider(messagingConfig)]; } }); }); }; Object.defineProperty(MessageService.prototype, "state", { get: function () { return this._state; }, enumerable: false, configurable: true }); MessageService.prototype.updateState = function (newState) { this._state = newState; this.emit(MessageServiceEventType.STATE_CHANGED, this._state); }; Object.defineProperty(MessageService.prototype, "messageChannel", { get: function () { return this._messageChannel; }, enumerable: false, configurable: true }); MessageService.prototype.createMessageServiceUrl = function (baseUrl) { return "".concat(baseUrl, "/channel?pubname=").concat(uuid(), "&subname=").concat(uuid()); }; MessageService.prototype.manipulateAppMessageSettings = function (channelParams) { var object = { apiVersion: constants.sdkVersion, pubchannel: channelParams.subchannel, subchannel: channelParams.pubchannel, }; if (Array.isArray(channelParams.providers)) { var providers = channelParams.providers.map(function (provider) { var newProvider = __assign({}, provider); if (provider.pubchannel && provider.subchannel) { newProvider.pubchannel = provider.subchannel; newProvider.subchannel = provider.pubchannel; } return newProvider; }); object.providers = providers; } LogManager.info('Updated message settings', object); return object; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars MessageService.prototype.setSessionActive = function (appData) { // Signiant.Mst.groupEnd(); LogManager.info("Received Session Ready from App (via ".concat(this._messageChannel.getProviderInfo(), ")")); // Signiant.Mst.App.setConfiguration(appData); // Signiant.Mst.googleAnalytics.updateAnalyticsAppVersion(); // Signiant.Mst.googleAnalytics.endTimer(Signiant.Mst.GoogleAnalytics.Constants.Category.appDetection, // Signiant.Mst.GoogleAnalytics.Constants.Action.receiveInteractiveResponse); // Signiant.Mst.googleAnalytics.sendEvent(Signiant.Mst.GoogleAnalytics.Constants.Category.appDetection, // Signiant.Mst.GoogleAnalytics.Constants.Action.interactiveSessionResponse, // Signiant.Mst.signiantMessageChannel.getProviderInfo()); this.updateState(MessageServiceState.INITIALIZED); localStorage.setItem(constants.SigniantAppInstalledCookieKey, 'true'); }; MessageService.prototype.waitForActiveSession = function (provider) { var _this = this; return new Promise(function (resolve) { provider.setSessionActiveEventHandler(function (appData) { _this.setSessionActive(appData); resolve(appData); }); }); }; MessageService.prototype.setupAppRegistration = function (provider) { return __awaiter(this, void 0, void 0, function () { var channelParams, waitForActiveSessionPromise, appData, err_2; return __generator(this, function (_a) { switch (_a.label) { case 0: channelParams = this.manipulateAppMessageSettings(provider.getChannelParams()); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); LogManager.info('Launching Signiant App'); waitForActiveSessionPromise = this.waitForActiveSession(provider); this._provider = provider; new Registration(channelParams).initApp(); provider.initialize(); LogManager.info('Message Channel initialization started'); return [4 /*yield*/, waitForActiveSessionPromise]; case 2: appData = _a.sent(); /** * We only set the reconnect listener after we have successfully connected for the first time. This is * because when we establish a connection for the first time we drop all the other attempted connections * (Pubnub, various websocket ports) so we should only be getting events here from the "real" connection. * If we add before the first connection we can regularly expect multiple websocket connections * to fail so we could have a situation where a failure on WS1 causes retries on WS 1,2,3,4 and failures * on WS2 does the same. Bad Mojo. */ provider.setErrorCallback(this.listenForReconnectTrigger.bind(this)); return [2 /*return*/, appData]; case 3: err_2 = _a.sent(); return [2 /*return*/, Promise.reject(new CodedError({ code: ErrorCode.ERROR_LAUNCHING_APP, message: ErrorMessages.generatedErrorMessages[ErrorCode.ERROR_LAUNCHING_APP](err_2.message), }))]; case 4: return [2 /*return*/]; } }); }); }; MessageService.prototype.listenForReconnectTrigger = function (evt) { return __awaiter(this, void 0, void 0, function () { var err_3; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!(evt instanceof CloseEvent && this._state !== MessageServiceState.INITIALIZING)) return [3 /*break*/, 4]; LogManager.warn("Connection to app lost while the messageClient was in a [".concat(this._state, "], reconnecting")); this.updateState(MessageServiceState.UNINITIALIZED); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, new Reconnecter(this, this._maxReconnectAttempts, this._maxReconnectDelayMilliseconds).beginReconnectLoop()]; case 2: _a.sent(); return [3 /*break*/, 4]; case 3: err_3 = _a.sent(); //We are out of luck at this point nothing more to be done without user intervention LogManager.error('Failed to reconnect to SigniantApp', err_3.message); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; MessageService.prototype.setupAppRegistrationOrTimeout = function (provider, errorTimeout) { var _this = this; return new Promise(function (resolve, reject) { var timeout = setTimeout(function () { reject(new CodedError({ code: ErrorCode.APP_CONNECTION_TIMEOUT, message: ErrorMessages.generatedErrorMessages[ErrorCode.APP_CONNECTION_TIMEOUT](errorTimeout), })); }, errorTimeout); _this.setupAppRegistration(provider).then(function (appData) { clearTimeout(timeout); resolve(appData); }); }); }; MessageService.prototype.doInitialization = function (_a) { var messageServiceUrl = _a.messageServiceUrl, _b = _a.force, force = _b === void 0 ? this._force : _b; return __awaiter(this, void 0, void 0, function () { var _c, err_4; return __generator(this, function (_d) { switch (_d.label) { case 0: if (this._state === MessageServiceState.INITIALIZED) { return [2 /*return*/]; } _d.label = 1; case 1: _d.trys.push([1, 4, , 5]); this.updateState(MessageServiceState.INITIALIZING); if (!force && !this.isAppMarkedAsInstalled()) { throw new CodedError({ code: ErrorCode.APP_MAY_NOT_BE_INSTALLED, message: 'App may not be installed.', }); } //Remember in case of disconnect. this._force = force; _c = this; return [4 /*yield*/, this.fetchMessageChannel(messageServiceUrl)]; case 2: _c._messageChannel = _d.sent(); return [4 /*yield*/, this.setupAppRegistrationOrTimeout(this._messageChannel, 20000)]; case 3: return [2 /*return*/, _d.sent()]; case 4: err_4 = _d.sent(); this._messageChannel = void 0; this.updateState(MessageServiceState.UNINITIALIZED); throw err_4; case 5: return [2 /*return*/]; } }); }); }; /** * When a launch happens successfully for the first time after the force flag is used we add a * SigniantAppInstalled local storage record which tells us that the app has successfully launched once * on this machine which in turn implies that the App has been installed. * * Technically a user can use the SDK once and then uninstall the app, so we are left in an inconsistent state * but the expectation is this situation should be rare. * * @private */ MessageService.prototype.isAppMarkedAsInstalled = function () { return localStorage.getItem(constants.SigniantAppInstalledCookieKey); }; /** * This slightly weird structure is to protect the message service from multiple * concurrent initialization attempts while it is initializing. This way multiple callers can wait * for the initialization to complete without triggering multiple competing initializations. */ MessageService.prototype.initializeApplication = function (initializeApplicationOptions) { var _this = this; if (initializeApplicationOptions === void 0) { initializeApplicationOptions = {}; } if (this._initPromise) { LogManager.info('Returning initPromise'); return this._initPromise; } //Need to be save in case we are disconnected this._initializeApplicationOptions = initializeApplicationOptions; LogManager.info('Setting init promise'); this._initPromise = this.doInitialization(initializeApplicationOptions); /** * We tried un-assigning the promise in the doInitialize function itself but we found in some * cases the un-assignment would happen before the assignment :-( * In theory this should guarantee the correct order of operation. The function is used twice * to cover both error and success cases. */ var unassignPromise = function (appData) { _this._initPromise = void 0; return appData; }; this._initPromise.then(unassignPromise, unassignPromise); return this._initPromise; }; return MessageService; }(EventEmitter)); export { MessageService }; export default new MessageService(MAX_RECONNECT_ATTEMPTS, MAX_RECONNECT_DELAY_MILLISECONDS);