diff --git a/packages/datafile-manager/.eslintrc.js b/packages/datafile-manager/.eslintrc.js index b91853e52..f99c80bd9 100644 --- a/packages/datafile-manager/.eslintrc.js +++ b/packages/datafile-manager/.eslintrc.js @@ -23,5 +23,7 @@ module.exports = { '@typescript-eslint/ban-ts-ignore': 'off', '@typescript-eslint/camelcase': 'off', '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off' }, }; diff --git a/packages/datafile-manager/src/backoffController.ts b/packages/datafile-manager/src/backoffController.ts index a19650d2f..db45fddfa 100644 --- a/packages/datafile-manager/src/backoffController.ts +++ b/packages/datafile-manager/src/backoffController.ts @@ -14,33 +14,33 @@ * limitations under the License. */ -import { BACKOFF_BASE_WAIT_SECONDS_BY_ERROR_COUNT } from './config' +import { BACKOFF_BASE_WAIT_SECONDS_BY_ERROR_COUNT } from './config'; -function randomMilliseconds() { - return Math.round(Math.random() * 1000) +function randomMilliseconds(): number { + return Math.round(Math.random() * 1000); } export default class BackoffController { - private errorCount = 0 + private errorCount = 0; getDelay(): number { if (this.errorCount === 0) { - return 0 + return 0; } const baseWaitSeconds = BACKOFF_BASE_WAIT_SECONDS_BY_ERROR_COUNT[ Math.min(BACKOFF_BASE_WAIT_SECONDS_BY_ERROR_COUNT.length - 1, this.errorCount) - ] - return baseWaitSeconds * 1000 + randomMilliseconds() + ]; + return baseWaitSeconds * 1000 + randomMilliseconds(); } countError(): void { if (this.errorCount < BACKOFF_BASE_WAIT_SECONDS_BY_ERROR_COUNT.length - 1) { - this.errorCount++ + this.errorCount++; } } reset(): void { - this.errorCount = 0 + this.errorCount = 0; } } diff --git a/packages/datafile-manager/src/browserDatafileManager.ts b/packages/datafile-manager/src/browserDatafileManager.ts index 3c23f059a..229a804b0 100644 --- a/packages/datafile-manager/src/browserDatafileManager.ts +++ b/packages/datafile-manager/src/browserDatafileManager.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { makeGetRequest } from './browserRequest' -import HttpPollingDatafileManager from './httpPollingDatafileManager' +import { makeGetRequest } from './browserRequest'; +import HttpPollingDatafileManager from './httpPollingDatafileManager'; import { Headers, AbortableRequest } from './http'; import { DatafileManagerConfig } from './datafileManager'; export default class BrowserDatafileManager extends HttpPollingDatafileManager { protected makeGetRequest(reqUrl: string, headers: Headers): AbortableRequest { - return makeGetRequest(reqUrl, headers) + return makeGetRequest(reqUrl, headers); } protected getConfigDefaults(): Partial { return { autoUpdate: false, - } + }; } } diff --git a/packages/datafile-manager/src/browserRequest.ts b/packages/datafile-manager/src/browserRequest.ts index 0dbbcef78..72c53697e 100644 --- a/packages/datafile-manager/src/browserRequest.ts +++ b/packages/datafile-manager/src/browserRequest.ts @@ -14,83 +14,83 @@ * limitations under the License. */ -import { AbortableRequest, Response, Headers } from './http' -import { REQUEST_TIMEOUT_MS } from './config' -import { getLogger } from '@optimizely/js-sdk-logging' +import { AbortableRequest, Response, Headers } from './http'; +import { REQUEST_TIMEOUT_MS } from './config'; +import { getLogger } from '@optimizely/js-sdk-logging'; -const logger = getLogger('DatafileManager') +const logger = getLogger('DatafileManager'); -const GET_METHOD = 'GET' -const READY_STATE_DONE = 4 +const GET_METHOD = 'GET'; +const READY_STATE_DONE = 4; function parseHeadersFromXhr(req: XMLHttpRequest): Headers { - const allHeadersString = req.getAllResponseHeaders() + const allHeadersString = req.getAllResponseHeaders(); if (allHeadersString === null) { - return {} + return {}; } - const headerLines = allHeadersString.split('\r\n') - const headers: Headers = {} + const headerLines = allHeadersString.split('\r\n'); + const headers: Headers = {}; headerLines.forEach(headerLine => { - const separatorIndex = headerLine.indexOf(': ') + const separatorIndex = headerLine.indexOf(': '); if (separatorIndex > -1) { - const headerName = headerLine.slice(0, separatorIndex) - const headerValue = headerLine.slice(separatorIndex + 2) + const headerName = headerLine.slice(0, separatorIndex); + const headerValue = headerLine.slice(separatorIndex + 2); if (headerValue.length > 0) { - headers[headerName] = headerValue + headers[headerName] = headerValue; } } - }) - return headers + }); + return headers; } function setHeadersInXhr(headers: Headers, req: XMLHttpRequest): void { Object.keys(headers).forEach(headerName => { - const header = headers[headerName] - req.setRequestHeader(headerName, header!) - }) + const header = headers[headerName]; + req.setRequestHeader(headerName, header!); + }); } export function makeGetRequest(reqUrl: string, headers: Headers): AbortableRequest { - const req = new XMLHttpRequest() + const req = new XMLHttpRequest(); const responsePromise: Promise = new Promise((resolve, reject) => { - req.open(GET_METHOD, reqUrl, true) + req.open(GET_METHOD, reqUrl, true); - setHeadersInXhr(headers, req) + setHeadersInXhr(headers, req); - req.onreadystatechange = () => { + req.onreadystatechange = (): void => { if (req.readyState === READY_STATE_DONE) { - const statusCode = req.status + const statusCode = req.status; if (statusCode === 0) { - reject(new Error('Request error')) - return + reject(new Error('Request error')); + return; } - const headers = parseHeadersFromXhr(req) + const headers = parseHeadersFromXhr(req); const resp: Response = { statusCode: req.status, body: req.responseText, headers, - } - resolve(resp) + }; + resolve(resp); } - } + }; - req.timeout = REQUEST_TIMEOUT_MS + req.timeout = REQUEST_TIMEOUT_MS; - req.ontimeout = () => { - logger.error('Request timed out') - } + req.ontimeout = (): void => { + logger.error('Request timed out'); + }; - req.send() - }) + req.send(); + }); return { responsePromise, - abort() { - req.abort() + abort(): void { + req.abort(); }, - } + }; } diff --git a/packages/datafile-manager/src/config.ts b/packages/datafile-manager/src/config.ts index 8cc9faf54..900bb8d0b 100644 --- a/packages/datafile-manager/src/config.ts +++ b/packages/datafile-manager/src/config.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -export const DEFAULT_UPDATE_INTERVAL = 5 * 60 * 1000 // 5 minutes +export const DEFAULT_UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes -export const MIN_UPDATE_INTERVAL = 1000 +export const MIN_UPDATE_INTERVAL = 1000; -export const DEFAULT_URL_TEMPLATE = `https://cdn.optimizely.com/datafiles/%s.json` +export const DEFAULT_URL_TEMPLATE = `https://cdn.optimizely.com/datafiles/%s.json`; -export const BACKOFF_BASE_WAIT_SECONDS_BY_ERROR_COUNT = [0, 8, 16, 32, 64, 128, 256, 512] +export const BACKOFF_BASE_WAIT_SECONDS_BY_ERROR_COUNT = [0, 8, 16, 32, 64, 128, 256, 512]; -export const REQUEST_TIMEOUT_MS = 60 * 1000 // 1 minute +export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute diff --git a/packages/datafile-manager/src/datafileManager.ts b/packages/datafile-manager/src/datafileManager.ts index 7ec2c2f2f..0cc8920bb 100644 --- a/packages/datafile-manager/src/datafileManager.ts +++ b/packages/datafile-manager/src/datafileManager.ts @@ -13,34 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import PersistentKeyValueCache from './persistentKeyValueCache' +import PersistentKeyValueCache from './persistentKeyValueCache'; export interface DatafileUpdate { - datafile: object + datafile: object; } export interface DatafileUpdateListener { - (datafileUpdate: DatafileUpdate): void + (datafileUpdate: DatafileUpdate): void; } // TODO: Replace this with the one from js-sdk-models interface Managed { - start(): void + start(): void; - stop(): Promise + stop(): Promise; } export interface DatafileManager extends Managed { - get: () => object | null - on: (eventName: string, listener: DatafileUpdateListener) => () => void - onReady: () => Promise + get: () => object | null; + on: (eventName: string, listener: DatafileUpdateListener) => () => void; + onReady: () => Promise; } export interface DatafileManagerConfig { - autoUpdate?: boolean - datafile?: object - sdkKey: string - updateInterval?: number - urlTemplate?: string - cache?: PersistentKeyValueCache + autoUpdate?: boolean; + datafile?: object; + sdkKey: string; + updateInterval?: number; + urlTemplate?: string; + cache?: PersistentKeyValueCache; } diff --git a/packages/datafile-manager/src/eventEmitter.ts b/packages/datafile-manager/src/eventEmitter.ts index f8bc4ef78..1b0d193b4 100644 --- a/packages/datafile-manager/src/eventEmitter.ts +++ b/packages/datafile-manager/src/eventEmitter.ts @@ -14,49 +14,49 @@ * limitations under the License. */ -export type Disposer = () => void +export type Disposer = () => void; -export type Listener = (arg?: any) => void +export type Listener = (arg?: any) => void; interface Listeners { - [index: string]: { // index is event name - [index: string]: Listener // index is listener id - } + [index: string]: { + // index is event name + [index: string]: Listener; // index is listener id + }; } export default class EventEmitter { - private listeners: Listeners = {} + private listeners: Listeners = {}; - private listenerId = 1 + private listenerId = 1; on(eventName: string, listener: Listener): Disposer { if (!this.listeners[eventName]) { - this.listeners[eventName] = {} + this.listeners[eventName] = {}; } - const currentListenerId = String(this.listenerId) - this.listenerId++ - this.listeners[eventName][currentListenerId] = listener - return () => { + const currentListenerId = String(this.listenerId); + this.listenerId++; + this.listeners[eventName][currentListenerId] = listener; + return (): void => { if (this.listeners[eventName]) { - delete this.listeners[eventName][currentListenerId] + delete this.listeners[eventName][currentListenerId]; } - } + }; } - emit(eventName: string, arg?: any) { - const listeners = this.listeners[eventName] + emit(eventName: string, arg?: any): void { + const listeners = this.listeners[eventName]; if (listeners) { Object.keys(listeners).forEach(listenerId => { - const listener = listeners[listenerId] - listener(arg) - }) + const listener = listeners[listenerId]; + listener(arg); + }); } } removeAllListeners(): void { - this.listeners = {} + this.listeners = {}; } } - // TODO: Create a typed event emitter for use in TS only (not JS) diff --git a/packages/datafile-manager/src/http.ts b/packages/datafile-manager/src/http.ts index fb4dece7f..526da3029 100644 --- a/packages/datafile-manager/src/http.ts +++ b/packages/datafile-manager/src/http.ts @@ -22,16 +22,16 @@ * to work with multiple values per header name. */ export interface Headers { - [header: string]: string | undefined + [header: string]: string | undefined; } export interface Response { - statusCode?: number - body: string - headers: Headers + statusCode?: number; + body: string; + headers: Headers; } export interface AbortableRequest { - abort(): void - responsePromise: Promise + abort(): void; + responsePromise: Promise; } diff --git a/packages/datafile-manager/src/httpPollingDatafileManager.ts b/packages/datafile-manager/src/httpPollingDatafileManager.ts index 2a1fc3ea9..82bf12f84 100644 --- a/packages/datafile-manager/src/httpPollingDatafileManager.ts +++ b/packages/datafile-manager/src/httpPollingDatafileManager.ts @@ -14,98 +14,98 @@ * limitations under the License. */ -import { getLogger } from '@optimizely/js-sdk-logging' +import { getLogger } from '@optimizely/js-sdk-logging'; import { sprintf } from '@optimizely/js-sdk-utils'; -import { DatafileManager, DatafileManagerConfig, DatafileUpdate } from './datafileManager' -import EventEmitter from './eventEmitter' -import { AbortableRequest, Response, Headers } from './http' -import { DEFAULT_UPDATE_INTERVAL, MIN_UPDATE_INTERVAL, DEFAULT_URL_TEMPLATE } from './config' -import BackoffController from './backoffController' -import PersistentKeyValueCache from './persistentKeyValueCache' +import { DatafileManager, DatafileManagerConfig, DatafileUpdate } from './datafileManager'; +import EventEmitter, { Disposer } from './eventEmitter'; +import { AbortableRequest, Response, Headers } from './http'; +import { DEFAULT_UPDATE_INTERVAL, MIN_UPDATE_INTERVAL, DEFAULT_URL_TEMPLATE } from './config'; +import BackoffController from './backoffController'; +import PersistentKeyValueCache from './persistentKeyValueCache'; -const logger = getLogger('DatafileManager') +const logger = getLogger('DatafileManager'); -const UPDATE_EVT = 'update' +const UPDATE_EVT = 'update'; function isValidUpdateInterval(updateInterval: number): boolean { - return updateInterval >= MIN_UPDATE_INTERVAL + return updateInterval >= MIN_UPDATE_INTERVAL; } function isSuccessStatusCode(statusCode: number): boolean { - return statusCode >= 200 && statusCode < 400 + return statusCode >= 200 && statusCode < 400; } const noOpKeyValueCache: PersistentKeyValueCache = { - get(key: string): Promise { - return Promise.resolve(null) + get(): Promise { + return Promise.resolve(null); }, - set(key: string, val: any): Promise { - return Promise.resolve() + set(): Promise { + return Promise.resolve(); }, - contains(key: string): Promise { - return Promise.resolve(false) + contains(): Promise { + return Promise.resolve(false); }, - remove(key: string): Promise { - return Promise.resolve() - } -} + remove(): Promise { + return Promise.resolve(); + }, +}; export default abstract class HttpPollingDatafileManager implements DatafileManager { // Make an HTTP get request to the given URL with the given headers // Return an AbortableRequest, which has a promise for a Response. // If we can't get a response, the promise is rejected. // The request will be aborted if the manager is stopped while the request is in flight. - protected abstract makeGetRequest(reqUrl: string, headers: Headers): AbortableRequest + protected abstract makeGetRequest(reqUrl: string, headers: Headers): AbortableRequest; // Return any default configuration options that should be applied - protected abstract getConfigDefaults(): Partial + protected abstract getConfigDefaults(): Partial; - private currentDatafile: object | null + private currentDatafile: object | null; - private readonly readyPromise: Promise + private readonly readyPromise: Promise; - private isReadyPromiseSettled: boolean + private isReadyPromiseSettled: boolean; - private readyPromiseResolver: () => void + private readyPromiseResolver: () => void; - private readyPromiseRejecter: (err: Error) => void + private readyPromiseRejecter: (err: Error) => void; - private readonly emitter: EventEmitter + private readonly emitter: EventEmitter; - private readonly autoUpdate: boolean + private readonly autoUpdate: boolean; - private readonly updateInterval: number + private readonly updateInterval: number; - private currentTimeout: any + private currentTimeout: any; - private isStarted: boolean + private isStarted: boolean; - private lastResponseLastModified?: string + private lastResponseLastModified?: string; - private datafileUrl: string + private datafileUrl: string; - private currentRequest: AbortableRequest | null + private currentRequest: AbortableRequest | null; - private backoffController: BackoffController + private backoffController: BackoffController; - private cacheKey: string + private cacheKey: string; - private cache: PersistentKeyValueCache + private cache: PersistentKeyValueCache; // When true, this means the update interval timeout fired before the current // sync completed. In that case, we should sync again immediately upon // completion of the current request, instead of waiting another update // interval. - private syncOnCurrentRequestComplete: boolean + private syncOnCurrentRequestComplete: boolean; constructor(config: DatafileManagerConfig) { const configWithDefaultsApplied: DatafileManagerConfig = { ...this.getConfigDefaults(), - ...config - } + ...config, + }; const { datafile, autoUpdate = false, @@ -113,246 +113,244 @@ export default abstract class HttpPollingDatafileManager implements DatafileMana updateInterval = DEFAULT_UPDATE_INTERVAL, urlTemplate = DEFAULT_URL_TEMPLATE, cache = noOpKeyValueCache, - } = configWithDefaultsApplied + } = configWithDefaultsApplied; - this.cache = cache - this.cacheKey = 'opt-datafile-' + sdkKey - this.isReadyPromiseSettled = false - this.readyPromiseResolver = () => {} - this.readyPromiseRejecter = () => {} + this.cache = cache; + this.cacheKey = 'opt-datafile-' + sdkKey; + this.isReadyPromiseSettled = false; + this.readyPromiseResolver = (): void => {}; + this.readyPromiseRejecter = (): void => {}; this.readyPromise = new Promise((resolve, reject) => { - this.readyPromiseResolver = resolve - this.readyPromiseRejecter = reject - }) + this.readyPromiseResolver = resolve; + this.readyPromiseRejecter = reject; + }); if (datafile) { - this.currentDatafile = datafile + this.currentDatafile = datafile; if (!sdkKey) { - this.resolveReadyPromise() + this.resolveReadyPromise(); } } else { - this.currentDatafile = null + this.currentDatafile = null; } - this.isStarted = false + this.isStarted = false; - this.datafileUrl = sprintf(urlTemplate, sdkKey) + this.datafileUrl = sprintf(urlTemplate, sdkKey); - this.emitter = new EventEmitter() - this.autoUpdate = autoUpdate + this.emitter = new EventEmitter(); + this.autoUpdate = autoUpdate; if (isValidUpdateInterval(updateInterval)) { - this.updateInterval = updateInterval + this.updateInterval = updateInterval; } else { - logger.warn('Invalid updateInterval %s, defaulting to %s', updateInterval, DEFAULT_UPDATE_INTERVAL) - this.updateInterval = DEFAULT_UPDATE_INTERVAL + logger.warn('Invalid updateInterval %s, defaulting to %s', updateInterval, DEFAULT_UPDATE_INTERVAL); + this.updateInterval = DEFAULT_UPDATE_INTERVAL; } - this.currentTimeout = null - this.currentRequest = null - this.backoffController = new BackoffController() - this.syncOnCurrentRequestComplete = false + this.currentTimeout = null; + this.currentRequest = null; + this.backoffController = new BackoffController(); + this.syncOnCurrentRequestComplete = false; } get(): object | null { - return this.currentDatafile + return this.currentDatafile; } start(): void { if (!this.isStarted) { - logger.debug('Datafile manager started') - this.isStarted = true - this.backoffController.reset() - this.setDatafileFromCacheIfAvailable() - this.syncDatafile() + logger.debug('Datafile manager started'); + this.isStarted = true; + this.backoffController.reset(); + this.setDatafileFromCacheIfAvailable(); + this.syncDatafile(); } } stop(): Promise { - logger.debug('Datafile manager stopped') - this.isStarted = false + logger.debug('Datafile manager stopped'); + this.isStarted = false; if (this.currentTimeout) { - clearTimeout(this.currentTimeout) - this.currentTimeout = null + clearTimeout(this.currentTimeout); + this.currentTimeout = null; } - this.emitter.removeAllListeners() + this.emitter.removeAllListeners(); if (this.currentRequest) { - this.currentRequest.abort() - this.currentRequest = null + this.currentRequest.abort(); + this.currentRequest = null; } - return Promise.resolve() + return Promise.resolve(); } onReady(): Promise { - return this.readyPromise + return this.readyPromise; } - on(eventName: string, listener: (datafileUpdate: DatafileUpdate) => void) { - return this.emitter.on(eventName, listener) + on(eventName: string, listener: (datafileUpdate: DatafileUpdate) => void): Disposer { + return this.emitter.on(eventName, listener); } private onRequestRejected(err: any): void { if (!this.isStarted) { - return + return; } - this.backoffController.countError() + this.backoffController.countError(); if (err instanceof Error) { - logger.error('Error fetching datafile: %s', err.message, err) + logger.error('Error fetching datafile: %s', err.message, err); } else if (typeof err === 'string') { - logger.error('Error fetching datafile: %s', err) + logger.error('Error fetching datafile: %s', err); } else { - logger.error('Error fetching datafile') + logger.error('Error fetching datafile'); } } private onRequestResolved(response: Response): void { if (!this.isStarted) { - return + return; } - if (typeof response.statusCode !== 'undefined' && - isSuccessStatusCode(response.statusCode)) { - this.backoffController.reset() + if (typeof response.statusCode !== 'undefined' && isSuccessStatusCode(response.statusCode)) { + this.backoffController.reset(); } else { - this.backoffController.countError() + this.backoffController.countError(); } - this.trySavingLastModified(response.headers) + this.trySavingLastModified(response.headers); - const datafile = this.getNextDatafileFromResponse(response) + const datafile = this.getNextDatafileFromResponse(response); if (datafile !== null) { - logger.info('Updating datafile from response') - this.currentDatafile = datafile - this.cache.set(this.cacheKey, datafile) + logger.info('Updating datafile from response'); + this.currentDatafile = datafile; + this.cache.set(this.cacheKey, datafile); if (!this.isReadyPromiseSettled) { - this.resolveReadyPromise() + this.resolveReadyPromise(); } else { const datafileUpdate: DatafileUpdate = { datafile, - } - this.emitter.emit(UPDATE_EVT, datafileUpdate) + }; + this.emitter.emit(UPDATE_EVT, datafileUpdate); } } } private onRequestComplete(this: HttpPollingDatafileManager): void { if (!this.isStarted) { - return + return; } - this.currentRequest = null + this.currentRequest = null; if (!this.isReadyPromiseSettled && !this.autoUpdate) { // We will never resolve ready, so reject it - this.rejectReadyPromise(new Error('Failed to become ready')) + this.rejectReadyPromise(new Error('Failed to become ready')); } if (this.autoUpdate && this.syncOnCurrentRequestComplete) { - this.syncDatafile() + this.syncDatafile(); } - this.syncOnCurrentRequestComplete = false + this.syncOnCurrentRequestComplete = false; } private syncDatafile(): void { - const headers: Headers = {} + const headers: Headers = {}; if (this.lastResponseLastModified) { - headers['if-modified-since'] = this.lastResponseLastModified + headers['if-modified-since'] = this.lastResponseLastModified; } - logger.debug('Making datafile request to url %s with headers: %s', this.datafileUrl, () => JSON.stringify(headers)) - this.currentRequest = this.makeGetRequest(this.datafileUrl, headers) - - const onRequestComplete = () => { - this.onRequestComplete() - } - const onRequestResolved = (response: Response) => { - this.onRequestResolved(response) - } - const onRequestRejected = (err: any) => { - this.onRequestRejected(err) - } + logger.debug('Making datafile request to url %s with headers: %s', this.datafileUrl, () => JSON.stringify(headers)); + this.currentRequest = this.makeGetRequest(this.datafileUrl, headers); + + const onRequestComplete = (): void => { + this.onRequestComplete(); + }; + const onRequestResolved = (response: Response): void => { + this.onRequestResolved(response); + }; + const onRequestRejected = (err: any): void => { + this.onRequestRejected(err); + }; this.currentRequest.responsePromise .then(onRequestResolved, onRequestRejected) - .then(onRequestComplete, onRequestComplete) + .then(onRequestComplete, onRequestComplete); if (this.autoUpdate) { - this.scheduleNextUpdate() + this.scheduleNextUpdate(); } } private resolveReadyPromise(): void { - this.readyPromiseResolver() - this.isReadyPromiseSettled = true + this.readyPromiseResolver(); + this.isReadyPromiseSettled = true; } private rejectReadyPromise(err: Error): void { - this.readyPromiseRejecter(err) - this.isReadyPromiseSettled = true + this.readyPromiseRejecter(err); + this.isReadyPromiseSettled = true; } private scheduleNextUpdate(): void { - const currentBackoffDelay = this.backoffController.getDelay() - const nextUpdateDelay = Math.max(currentBackoffDelay, this.updateInterval) - logger.debug('Scheduling sync in %s ms', nextUpdateDelay) + const currentBackoffDelay = this.backoffController.getDelay(); + const nextUpdateDelay = Math.max(currentBackoffDelay, this.updateInterval); + logger.debug('Scheduling sync in %s ms', nextUpdateDelay); this.currentTimeout = setTimeout(() => { if (this.currentRequest) { - this.syncOnCurrentRequestComplete = true + this.syncOnCurrentRequestComplete = true; } else { - this.syncDatafile() + this.syncDatafile(); } - }, nextUpdateDelay) + }, nextUpdateDelay); } private getNextDatafileFromResponse(response: Response): object | null { - logger.debug('Response status code: %s', response.statusCode) + logger.debug('Response status code: %s', response.statusCode); if (typeof response.statusCode === 'undefined') { - return null + return null; } if (response.statusCode === 304) { - return null + return null; } if (isSuccessStatusCode(response.statusCode)) { return this.tryParsingBodyAsJSON(response.body); } - return null + return null; } private tryParsingBodyAsJSON(body: string): object | null { - let parseResult: any + let parseResult: any; try { - parseResult = JSON.parse(body) + parseResult = JSON.parse(body); } catch (err) { - logger.error('Error parsing response body: %s', err.message, err) - return null + logger.error('Error parsing response body: %s', err.message, err); + return null; } - let datafileObj: object | null = null + let datafileObj: object | null = null; if (typeof parseResult === 'object' && parseResult !== null) { - datafileObj = parseResult + datafileObj = parseResult; } else { - logger.error('Error parsing response body: was not an object') + logger.error('Error parsing response body: was not an object'); } - return datafileObj + return datafileObj; } private trySavingLastModified(headers: Headers): void { - const lastModifiedHeader = headers['last-modified'] || headers['Last-Modified'] + const lastModifiedHeader = headers['last-modified'] || headers['Last-Modified']; if (typeof lastModifiedHeader !== 'undefined') { - this.lastResponseLastModified = lastModifiedHeader - logger.debug('Saved last modified header value from response: %s', this.lastResponseLastModified) + this.lastResponseLastModified = lastModifiedHeader; + logger.debug('Saved last modified header value from response: %s', this.lastResponseLastModified); } } setDatafileFromCacheIfAvailable(): void { - this.cache.get(this.cacheKey) - .then(datafile => { - if (this.isStarted && !this.isReadyPromiseSettled && datafile) { - logger.debug('Using datafile from cache') - this.currentDatafile = datafile - this.resolveReadyPromise() - } - }) + this.cache.get(this.cacheKey).then(datafile => { + if (this.isStarted && !this.isReadyPromiseSettled && datafile) { + logger.debug('Using datafile from cache'); + this.currentDatafile = datafile; + this.resolveReadyPromise(); + } + }); } } diff --git a/packages/datafile-manager/src/index.browser.ts b/packages/datafile-manager/src/index.browser.ts index 205e24a5c..6c501d2e3 100644 --- a/packages/datafile-manager/src/index.browser.ts +++ b/packages/datafile-manager/src/index.browser.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export * from './datafileManager' -export { default as HttpPollingDatafileManager } from './browserDatafileManager' +export * from './datafileManager'; +export { default as HttpPollingDatafileManager } from './browserDatafileManager'; diff --git a/packages/datafile-manager/src/index.node.ts b/packages/datafile-manager/src/index.node.ts index 76b76ba14..28e044ede 100644 --- a/packages/datafile-manager/src/index.node.ts +++ b/packages/datafile-manager/src/index.node.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export * from './datafileManager' -export { default as HttpPollingDatafileManager } from './nodeDatafileManager' +export * from './datafileManager'; +export { default as HttpPollingDatafileManager } from './nodeDatafileManager'; diff --git a/packages/datafile-manager/src/index.react_native.ts b/packages/datafile-manager/src/index.react_native.ts index 555bf0c2b..e6706908d 100644 --- a/packages/datafile-manager/src/index.react_native.ts +++ b/packages/datafile-manager/src/index.react_native.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export * from './datafileManager' -export { default as HttpPollingDatafileManager } from './reactNativeDatafileManager' +export * from './datafileManager'; +export { default as HttpPollingDatafileManager } from './reactNativeDatafileManager'; diff --git a/packages/datafile-manager/src/nodeDatafileManager.ts b/packages/datafile-manager/src/nodeDatafileManager.ts index 6672bc2dc..b0c6ef6fe 100644 --- a/packages/datafile-manager/src/nodeDatafileManager.ts +++ b/packages/datafile-manager/src/nodeDatafileManager.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { makeGetRequest } from './nodeRequest' -import HttpPollingDatafileManager from './httpPollingDatafileManager' +import { makeGetRequest } from './nodeRequest'; +import HttpPollingDatafileManager from './httpPollingDatafileManager'; import { Headers, AbortableRequest } from './http'; import { DatafileManagerConfig } from './datafileManager'; export default class NodeDatafileManager extends HttpPollingDatafileManager { protected makeGetRequest(reqUrl: string, headers: Headers): AbortableRequest { - return makeGetRequest(reqUrl, headers) + return makeGetRequest(reqUrl, headers); } protected getConfigDefaults(): Partial { return { autoUpdate: true, - } + }; } } diff --git a/packages/datafile-manager/src/nodeRequest.ts b/packages/datafile-manager/src/nodeRequest.ts index aa56d5eeb..8390e5cd4 100644 --- a/packages/datafile-manager/src/nodeRequest.ts +++ b/packages/datafile-manager/src/nodeRequest.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import http from 'http' -import https from 'https' -import url from 'url' -import { Headers, AbortableRequest, Response } from './http' +import http from 'http'; +import https from 'https'; +import url from 'url'; +import { Headers, AbortableRequest, Response } from './http'; import { REQUEST_TIMEOUT_MS } from './config'; // Shared signature between http.request and https.request -type ClientRequestCreator = (options: http.RequestOptions) => http.ClientRequest +type ClientRequestCreator = (options: http.RequestOptions) => http.ClientRequest; function getRequestOptionsFromUrl(url: url.UrlWithStringQuery): http.RequestOptions { return { @@ -29,7 +29,7 @@ function getRequestOptionsFromUrl(url: url.UrlWithStringQuery): http.RequestOpti path: url.path, port: url.port, protocol: url.protocol, - } + }; } /** @@ -41,24 +41,23 @@ function getRequestOptionsFromUrl(url: url.UrlWithStringQuery): http.RequestOpti * per header name. * */ -function createHeadersFromNodeIncomingMessage( - incomingMessage: http.IncomingMessage, -): Headers { - const headers: Headers = {} +function createHeadersFromNodeIncomingMessage(incomingMessage: http.IncomingMessage): Headers { + const headers: Headers = {}; Object.keys(incomingMessage.headers).forEach(headerName => { - const headerValue = incomingMessage.headers[headerName] + const headerValue = incomingMessage.headers[headerName]; if (typeof headerValue === 'string') { - headers[headerName] = headerValue + headers[headerName] = headerValue; } else if (typeof headerValue === 'undefined') { + // no value provided for this header } else { // array if (headerValue.length > 0) { // We don't care about multiple values - just take the first one - headers[headerName] = headerValue[0] + headers[headerName] = headerValue[0]; } } - }) - return headers + }); + return headers; } function getResponseFromRequest(request: http.ClientRequest): Promise { @@ -66,86 +65,84 @@ function getResponseFromRequest(request: http.ClientRequest): Promise // constructing own Promise return new Promise((resolve, reject) => { const timeout = setTimeout(() => { - request.abort() - reject(new Error('Request timed out')) - }, REQUEST_TIMEOUT_MS) + request.abort(); + reject(new Error('Request timed out')); + }, REQUEST_TIMEOUT_MS); request.once('response', (incomingMessage: http.IncomingMessage) => { if (request.aborted) { - return + return; } - incomingMessage.setEncoding('utf8') + incomingMessage.setEncoding('utf8'); - let responseData = '' + let responseData = ''; incomingMessage.on('data', (chunk: string) => { if (!request.aborted) { - responseData += chunk + responseData += chunk; } - }) + }); incomingMessage.on('end', () => { if (request.aborted) { - return + return; } - clearTimeout(timeout) + clearTimeout(timeout); resolve({ statusCode: incomingMessage.statusCode, body: responseData, headers: createHeadersFromNodeIncomingMessage(incomingMessage), - }) - }) - }) + }); + }); + }); request.on('error', (err: any) => { - clearTimeout(timeout) + clearTimeout(timeout); if (err instanceof Error) { - reject(err) + reject(err); } else if (typeof err === 'string') { - reject(new Error(err)) + reject(new Error(err)); } else { - reject(new Error('Request error')) + reject(new Error('Request error')); } - }) - }) + }); + }); } export function makeGetRequest(reqUrl: string, headers: Headers): AbortableRequest { // TODO: Use non-legacy URL parsing when we drop support for Node 6 - const parsedUrl = url.parse(reqUrl) + const parsedUrl = url.parse(reqUrl); - let requester: ClientRequestCreator + let requester: ClientRequestCreator; if (parsedUrl.protocol === 'http:') { - requester = http.request + requester = http.request; } else if (parsedUrl.protocol === 'https:') { - requester = https.request + requester = https.request; } else { return { - responsePromise: Promise.reject( - new Error(`Unsupported protocol: ${parsedUrl.protocol}`), - ), - abort() {}, - } + responsePromise: Promise.reject(new Error(`Unsupported protocol: ${parsedUrl.protocol}`)), + abort(): void {}, + }; } const requestOptions: http.RequestOptions = { ...getRequestOptionsFromUrl(parsedUrl), method: 'GET', headers, - } + }; - const request = requester(requestOptions) - const responsePromise = getResponseFromRequest(request) + const request = requester(requestOptions); + const responsePromise = getResponseFromRequest(request); - request.end() + request.end(); return { - abort() { - request.abort() + abort(): void { + request.abort(); }, responsePromise, - } + }; } diff --git a/packages/datafile-manager/src/persistentKeyValueCache.ts b/packages/datafile-manager/src/persistentKeyValueCache.ts index 9c989f44f..9b3ef9fac 100644 --- a/packages/datafile-manager/src/persistentKeyValueCache.ts +++ b/packages/datafile-manager/src/persistentKeyValueCache.ts @@ -19,37 +19,36 @@ * and JSON Object as value. */ export default interface PersistentKeyValueCache { - /** * Returns value stored against a key or null if not found. - * @param key - * @returns + * @param key + * @returns * Resolves promise with * 1. Object if value found was stored as a JSON Object. * 2. null if the key does not exist in the cache. * Rejects the promise in case of an error */ - get(key: string): Promise + get(key: string): Promise; /** * Stores Object in the persistent cache against a key - * @param key - * @param val - * @returns + * @param key + * @param val + * @returns * Resolves promise without a value if successful * Rejects the promise in case of an error */ - set(key: string, val: any): Promise + set(key: string, val: any): Promise; /** * Checks if a key exists in the cache * @param key - * Resolves promise with + * Resolves promise with * 1. true if the key exists * 2. false if the key does not exist * Rejects the promise in case of an error */ - contains(key: string): Promise + contains(key: string): Promise; /** * Removes the key value pair from cache. @@ -57,5 +56,5 @@ export default interface PersistentKeyValueCache { * Resolves promise without a value if successful * Rejects the promise in case of an error */ - remove(key: string): Promise + remove(key: string): Promise; } diff --git a/packages/datafile-manager/src/reactNativeAsyncStorageCache.ts b/packages/datafile-manager/src/reactNativeAsyncStorageCache.ts index eae674798..73f0bb364 100644 --- a/packages/datafile-manager/src/reactNativeAsyncStorageCache.ts +++ b/packages/datafile-manager/src/reactNativeAsyncStorageCache.ts @@ -14,43 +14,42 @@ * limitations under the License. */ -import { getLogger } from '@optimizely/js-sdk-logging' -import AsyncStorage from '@react-native-community/async-storage' +import { getLogger } from '@optimizely/js-sdk-logging'; +import AsyncStorage from '@react-native-community/async-storage'; -import PersistentKeyValueCache from './persistentKeyValueCache' +import PersistentKeyValueCache from './persistentKeyValueCache'; -const logger = getLogger('DatafileManager') +const logger = getLogger('DatafileManager'); export default class ReactNativeAsyncStorageCache implements PersistentKeyValueCache { get(key: string): Promise { - return AsyncStorage.getItem(key) - .then((val: string | null) => { - if (!val) { - return null - } - try { - return JSON.parse(val); - } catch (ex) { - logger.error('Error Parsing Object from cache - %s', ex) - throw ex - } - }) + return AsyncStorage.getItem(key).then((val: string | null) => { + if (!val) { + return null; + } + try { + return JSON.parse(val); + } catch (ex) { + logger.error('Error Parsing Object from cache - %s', ex); + throw ex; + } + }); } - set(key: string, val: any): Promise { + set(key: string, val: any): Promise { try { - return AsyncStorage.setItem(key, JSON.stringify(val)) + return AsyncStorage.setItem(key, JSON.stringify(val)); } catch (ex) { - logger.error('Error stringifying Object to Json - %s', ex) - return Promise.reject(ex) + logger.error('Error stringifying Object to Json - %s', ex); + return Promise.reject(ex); } } - + contains(key: string): Promise { - return AsyncStorage.getItem(key).then((val: string | null) => (val !== null)) + return AsyncStorage.getItem(key).then((val: string | null) => val !== null); } - + remove(key: string): Promise { - return AsyncStorage.removeItem(key) + return AsyncStorage.removeItem(key); } } diff --git a/packages/datafile-manager/src/reactNativeDatafileManager.ts b/packages/datafile-manager/src/reactNativeDatafileManager.ts index 4949ee09a..a7a2ac44e 100644 --- a/packages/datafile-manager/src/reactNativeDatafileManager.ts +++ b/packages/datafile-manager/src/reactNativeDatafileManager.ts @@ -14,22 +14,21 @@ * limitations under the License. */ -import { makeGetRequest } from './browserRequest' -import HttpPollingDatafileManager from './httpPollingDatafileManager' -import { Headers, AbortableRequest } from './http' -import { DatafileManagerConfig } from './datafileManager' -import ReactNativeAsyncStorageCache from './reactNativeAsyncStorageCache' +import { makeGetRequest } from './browserRequest'; +import HttpPollingDatafileManager from './httpPollingDatafileManager'; +import { Headers, AbortableRequest } from './http'; +import { DatafileManagerConfig } from './datafileManager'; +import ReactNativeAsyncStorageCache from './reactNativeAsyncStorageCache'; export default class ReactNativeDatafileManager extends HttpPollingDatafileManager { - protected makeGetRequest(reqUrl: string, headers: Headers): AbortableRequest { - return makeGetRequest(reqUrl, headers) + return makeGetRequest(reqUrl, headers); } protected getConfigDefaults(): Partial { return { autoUpdate: true, cache: new ReactNativeAsyncStorageCache(), - } + }; } }