UNPKG

box-ui-elements-mlh

Version:
239 lines (201 loc) 8.26 kB
// @flow import LocalStore from '../../utils/LocalStore'; import Channel from './Channel'; import { get as getCookie, set as setCookie } from './cookies'; const GET = 'GET'; const POST = 'POST'; const YEAR_MS = 31536000000; const DEFAULT_PRIMARY_PORT = 17223; const DEFAULT_FALLBACK_PORT = 17224; const CONTENT_TYPE_HEADER = 'Content-Type'; const CONTENT_TYPE_VALUE = 'text/plain; charset=UTF-8'; const STATUS_NOT_RUNNING = 'notrunning'; const localStore = new LocalStore(); function shouldUseFallbackFirst() { return !!localStore.getItem('comUseFallback'); } function saveFallbackPortPreference() { localStore.setItem('comUseFallback', 1); } function clearFallbackPortPreference() { localStore.removeItem('comUseFallback'); } function getPreferredPortOrdering() { if (shouldUseFallbackFirst()) { return { primary: DEFAULT_FALLBACK_PORT, fallback: DEFAULT_PRIMARY_PORT, }; } return { primary: DEFAULT_PRIMARY_PORT, fallback: DEFAULT_FALLBACK_PORT, }; } class HTTPChannel extends Channel { url: string; channelName: string; comChannelName: string; comServerInstallationPromise: ?Promise<any>; retryCounter: number; sendCount: number; currentPort: number; comServerInstallationPromiseRejected: boolean; window: any; constructor(appName: string, url: string, channelName: string) { super(appName); this.url = url; this.comChannelName = this.setComChannel(appName); this.channelName = channelName; this.comServerInstallationPromise = null; this.comServerInstallationPromiseRejected = false; this.retryCounter = 0; this.sendCount = 0; this.currentPort = getPreferredPortOrdering().primary; this.window = window; } createCORSRequest = (method: string, url: string): XMLHttpRequest => { let xhr; try { const { XMLHttpRequest } = this.window; xhr = new XMLHttpRequest(); xhr.open(method, url, true); return xhr; } catch (ex) { if (this.retryCounter < 3) { this.retryCounter += 1; return this.createCORSRequest(method, url); } throw new Error('could not create xhr'); } }; getComServerStatusInstallationPromise = (timeoutMS: number): Promise<any> => { let resolve; let reject; const comServerInstallationPromise = new Promise((res, rej) => { resolve = res; reject = rej; }); const resolveWithValidPort = (port, res) => { this.currentPort = port; if (port === DEFAULT_PRIMARY_PORT) { clearFallbackPortPreference(); } else { saveFallbackPortPreference(); } this.comServerInstallationPromiseRejected = false; resolve(res); }; const portPreferences = getPreferredPortOrdering(); const primaryPort = portPreferences.primary; const fallbackPort = portPreferences.fallback; // Try primary port first this.checkInstallStatus(primaryPort, timeoutMS) .then(resolveWithValidPort.bind(this, primaryPort)) .catch(err => { // If com server returned that the port is available but the app is not running, reject if (err === STATUS_NOT_RUNNING) { this.comServerInstallationPromiseRejected = true; reject(); return; } // Only check secondary, if necessary, otherwise this throws a browser error in console this.checkInstallStatus(fallbackPort, timeoutMS) .then(resolveWithValidPort.bind(this, fallbackPort)) .catch(() => { this.comServerInstallationPromiseRejected = true; reject(); }); }); return comServerInstallationPromise; }; sendComServerRequest = (method: string, url: string, data: ?Object | string, timeoutMS: number): Promise<any> => { return new Promise((resolve, reject) => { try { const request = this.createCORSRequest(method, url); request.setRequestHeader(CONTENT_TYPE_HEADER, CONTENT_TYPE_VALUE); request.onload = () => { resolve(request); }; request.onerror = () => { reject(request); }; if (timeoutMS > 0) { request.timeout = timeoutMS; request.ontimeout = () => { reject(request); }; } setTimeout(() => { request.send(data); }, 0); } catch (err) { reject(); } }); }; checkInstallStatus = (port: number, timeoutMS: number): Promise<any> => { return this.sendComServerRequest(GET, `${this.url}${port}/status`, null, timeoutMS).then(request => { const response = JSON.parse(request.responseText); if (response.running) { return response; } this.comServerInstallationPromiseRejected = true; throw new Error(STATUS_NOT_RUNNING); }); }; getComChannel = (appName: string) => getCookie(`${appName}-bgp-id`); setComChannel = (appName: string) => { const bgpId = this.getComChannel(appName) || this.generateId(); const comChannelName = `bgp-${bgpId}`; const aYearFromNow = new Date().getTime() + YEAR_MS; setCookie(`${appName}-bgp-id`, bgpId, aYearFromNow); setCookie(`bgp-${bgpId}`, 'generic', aYearFromNow); return comChannelName; }; getComServerStatus = (timeoutMS: number) => { if (!this.comServerInstallationPromise || this.comServerInstallationPromiseRejected) { // Null out the promise and fetch status again (handles the case where user installed box edit within the same page load) this.comServerInstallationPromise = null; this.comServerInstallationPromise = this.getComServerStatusInstallationPromise(timeoutMS); return this.comServerInstallationPromise; } return this.comServerInstallationPromise; }; sendRequest = ( data: Object | string, browserToComServerTimeoutMS: number, comServerToApplicationTimeoutSec: number, ): Promise<any> => { const url = `${this.url}${this.currentPort}/application_request?application=${this.appName}&com=${this.comChannelName}&timeout=${comServerToApplicationTimeoutSec}`; return this.sendComServerRequest(POST, url, data, browserToComServerTimeoutMS).then(results => { // TODO: does the error object need to be richer? const response = JSON.parse(results.responseText); if (response.response_type && response.response_type === 'error') { throw new Error(`Communication error: ${response.message}`); } return response; }); }; sendCommand = ( data: Object | string, browserToComServerTimeoutMS: number, comServerToApplicationTimeoutSec: number, ): Promise<any> => { const url = `${this.url}${this.currentPort}/application_command?application=${this.appName}&com=${this.comChannelName}&timeout=${comServerToApplicationTimeoutSec}`; return this.sendComServerRequest(POST, url, data, browserToComServerTimeoutMS).then(results => { return JSON.parse(results.responseText); }); }; generateId = () => { const time = new Date().getTime(); return 'xxxxxxxx'.replace(/x/g, () => { // Generate random number between 1 and 16. // Using time for added entropy. const rand = Math.floor((time + Math.random() * 16) % 16); // Convert number to a HEX return rand.toString(16); }); }; } export default HTTPChannel;