From 4d52b90b0c099c05b795af13ab15f3b01005fcec Mon Sep 17 00:00:00 2001 From: Alexis Georges Date: Wed, 17 Oct 2018 13:38:00 -0700 Subject: [PATCH 01/10] Improve linting (#116) --- .eslintrc.yaml | 60 +++++++++++++++++++++++++++++-- src/EventProcessor.js | 2 +- src/EventSender.js | 6 ++-- src/GoalTracker.js | 2 +- src/Requestor.js | 2 +- src/Store.js | 6 ++-- src/Stream.js | 4 +-- src/__tests__/EventSource-mock.js | 2 +- src/__tests__/LDClient-test.js | 4 +-- src/__tests__/Store-test.js | 4 +-- src/index.js | 16 ++++----- src/messages.js | 2 ++ 12 files changed, 83 insertions(+), 27 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 2d50f0b6..c4b241e4 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -5,51 +5,105 @@ extends: env: es6: true node: true - browser: true plugins: - babel - prettier globals: VERSION: true + describe: true + it: true + expect: true + jest: true + beforeEach: true + afterEach: true + window: true + document: true rules: + # https://github.com/prettier/eslint-plugin-prettier prettier/prettier: - error + + # https://github.com/babel/eslint-plugin-babel + babel/semi: error + + # https://eslint.org/docs/rules/array-callback-return array-callback-return: error + + # https://eslint.org/docs/rules/curly curly: - error - all + + # https://eslint.org/docs/rules/no-implicit-coercion no-implicit-coercion: - 'off' - boolean: false number: true string: true allow: [] + + # https://eslint.org/docs/rules/no-eval no-eval: error + + # https://eslint.org/docs/rules/no-implied-eval no-implied-eval: error + + # https://eslint.org/docs/rules/no-param-reassign no-param-reassign: - error - props: true + + # https://eslint.org/docs/rules/no-return-assign no-return-assign: error + + # https://eslint.org/docs/rules/no-self-compare no-self-compare: error + + # https://eslint.org/docs/rules/radix radix: error + + # https://eslint.org/docs/rules/no-array-constructor no-array-constructor: error + + # https://eslint.org/docs/rules/no-new-wrappers no-new-wrappers: error + + # https://eslint.org/docs/rules/no-cond-assign no-cond-assign: error + + # https://eslint.org/docs/rules/no-use-before-define no-use-before-define: - error - functions: false + + # https://eslint.org/docs/rules/eqeqeq eqeqeq: error # Deprecations are required to turn enforce this camelcase: warn - + + # https://eslint.org/docs/rules/no-new-object no-new-object: error + + # https://eslint.org/docs/rules/no-nested-ternary no-nested-ternary: error + + # https://eslint.org/docs/rules/no-unused-vars no-unused-vars: error + + # https://eslint.org/docs/rules/no-var no-var: error + + # https://eslint.org/docs/rules/prefer-const prefer-const: error + + # https://eslint.org/docs/rules/prefer-arrow-callback prefer-arrow-callback: error + + # https://eslint.org/docs/rules/arrow-body-style arrow-body-style: - error - as-needed - babel/semi: error \ No newline at end of file + + # https://eslint.org/docs/rules/no-undef + no-undef: error diff --git a/src/EventProcessor.js b/src/EventProcessor.js index 155a3205..6df2416d 100644 --- a/src/EventProcessor.js +++ b/src/EventProcessor.js @@ -31,7 +31,7 @@ export default function EventProcessor(eventsUrl, environmentId, options = {}, e samplingInterval = options.samplingInterval || 0; } - if (options.flushInterval !== undefined && (isNan(options.flushInterval) || options.flushInterval < 2000)) { + if (options.flushInterval !== undefined && (isNaN(options.flushInterval) || options.flushInterval < 2000)) { flushInterval = 2000; reportArgumentError('Invalid flush interval configured. Must be an integer >= 2000 (milliseconds).'); } else { diff --git a/src/EventSender.js b/src/EventSender.js index 8a4b7951..14149ae2 100644 --- a/src/EventSender.js +++ b/src/EventSender.js @@ -10,7 +10,7 @@ export default function EventSender(eventsUrl, environmentId, forceHasCors, imag const sender = {}; function loadUrlUsingImage(src, onDone) { - const img = new Image(); + const img = new window.Image(); if (onDone) { img.addEventListener('load', onDone); } @@ -34,7 +34,7 @@ export default function EventSender(eventsUrl, environmentId, forceHasCors, imag const jsonBody = JSON.stringify(events); const send = onDone => { function createRequest(canRetry) { - const xhr = new XMLHttpRequest(); + const xhr = new window.XMLHttpRequest(); xhr.open('POST', postUrl, !sync); utils.addLDHeaders(xhr); xhr.setRequestHeader('Content-Type', 'application/json'); @@ -76,7 +76,7 @@ export default function EventSender(eventsUrl, environmentId, forceHasCors, imag // Detect browser support for CORS (can be overridden by tests) if (hasCors === undefined) { if (forceHasCors === undefined) { - hasCors = 'withCredentials' in new XMLHttpRequest(); + hasCors = 'withCredentials' in new window.XMLHttpRequest(); } else { hasCors = forceHasCors; } diff --git a/src/GoalTracker.js b/src/GoalTracker.js index a384f663..7c949543 100644 --- a/src/GoalTracker.js +++ b/src/GoalTracker.js @@ -60,7 +60,7 @@ export default function GoalTracker(goals, onEvent) { const urls = goal.urls || []; for (let j = 0; j < urls.length; j++) { - if (doesUrlMatch(urls[j], location.href, location.search, location.hash)) { + if (doesUrlMatch(urls[j], window.location.href, window.location.search, window.location.hash)) { if (goal.kind === 'pageview') { onEvent('pageview', goal); } else { diff --git a/src/Requestor.js b/src/Requestor.js index 1ca30e6f..ced85ee9 100644 --- a/src/Requestor.js +++ b/src/Requestor.js @@ -5,7 +5,7 @@ import * as messages from './messages'; const json = 'application/json'; function fetchJSON(endpoint, body, callback, sendLDHeaders) { - const xhr = new XMLHttpRequest(); + const xhr = new window.XMLHttpRequest(); let data = undefined; xhr.addEventListener('load', () => { diff --git a/src/Store.js b/src/Store.js index 4a7cde19..f682d206 100644 --- a/src/Store.js +++ b/src/Store.js @@ -17,7 +17,7 @@ export default function Store(environment, hash, ident) { const key = getFlagsKey(); let dataStr, data; try { - dataStr = localStorage.getItem(key); + dataStr = window.localStorage.getItem(key); } catch (ex) { console.warn(messages.localStorageUnavailable()); return null; @@ -41,7 +41,7 @@ export default function Store(environment, hash, ident) { const key = getFlagsKey(); const data = utils.extend({}, flags, { $schema: 1 }); try { - localStorage.setItem(key, JSON.stringify(data)); + window.localStorage.setItem(key, JSON.stringify(data)); } catch (ex) { console.warn(messages.localStorageUnavailable()); } @@ -50,7 +50,7 @@ export default function Store(environment, hash, ident) { store.clearFlags = function() { const key = getFlagsKey(); try { - localStorage.removeItem(key); + window.localStorage.removeItem(key); } catch (ex) {} }; diff --git a/src/Stream.js b/src/Stream.js index d5527a7a..07548c1f 100644 --- a/src/Stream.js +++ b/src/Stream.js @@ -24,7 +24,7 @@ export default function Stream(baseUrl, environment, hash, config) { }; stream.isConnected = function() { - return es && (es.readyState === EventSource.OPEN || es.readyState === EventSource.CONNECTING); + return es && (es.readyState === window.EventSource.OPEN || es.readyState === window.EventSource.CONNECTING); }; function reconnect() { @@ -45,7 +45,7 @@ export default function Stream(baseUrl, environment, hash, config) { function openConnection() { let url; let query = ''; - if (typeof EventSource !== 'undefined') { + if (typeof window.EventSource !== 'undefined') { if (useReport) { // we don't yet have an EventSource implementation that supports REPORT, so // fall back to the old ping-based stream diff --git a/src/__tests__/EventSource-mock.js b/src/__tests__/EventSource-mock.js index 724cf11a..6e100933 100644 --- a/src/__tests__/EventSource-mock.js +++ b/src/__tests__/EventSource-mock.js @@ -44,7 +44,7 @@ export default function EventSource(url) { } } - function mockOpen() { + function mockOpen(error) { if (this.readyState === EventSource.CONNECTING) { this.readyState = EventSource.OPEN; this.onopen && this.onopen(error); diff --git a/src/__tests__/LDClient-test.js b/src/__tests__/LDClient-test.js index bcf02833..1688a160 100644 --- a/src/__tests__/LDClient-test.js +++ b/src/__tests__/LDClient-test.js @@ -239,7 +239,7 @@ describe('LDClient', () => { // sandbox.restore(window.localStorage.__proto__, 'getItem'); // sandbox.stub(window.localStorage.__proto__, 'getItem').throws(); - localStorage.getItem.mockImplementationOnce(() => { + window.localStorage.getItem.mockImplementationOnce(() => { throw new Error(); }); @@ -256,7 +256,7 @@ describe('LDClient', () => { }); it('should handle localStorage setItem throwing an exception', done => { - localStorage.setItem.mockImplementationOnce(() => { + window.localStorage.setItem.mockImplementationOnce(() => { throw new Error(); }); diff --git a/src/__tests__/Store-test.js b/src/__tests__/Store-test.js index 9da5efbf..649681f2 100644 --- a/src/__tests__/Store-test.js +++ b/src/__tests__/Store-test.js @@ -7,7 +7,7 @@ describe('Store', () => { it('should handle localStorage getItem throwing an exception', () => { const store = Store('env', 'hash', ident); - const getItemSpy = jest.spyOn(localStorage, 'getItem').mockImplementation(() => { + const getItemSpy = jest.spyOn(window.localStorage, 'getItem').mockImplementation(() => { throw new Error('localstorage getitem error'); }); @@ -22,7 +22,7 @@ describe('Store', () => { it('should handle localStorage setItem throwing an exception', () => { const store = Store('env', 'hash', ident); - const setItemSpy = jest.spyOn(localStorage, 'setItem').mockImplementation(() => { + const setItemSpy = jest.spyOn(window.localStorage, 'setItem').mockImplementation(() => { throw new Error('localstorage getitem error'); }); diff --git a/src/index.js b/src/index.js index 55130941..09a32950 100644 --- a/src/index.js +++ b/src/index.js @@ -237,10 +237,10 @@ export function initialize(env, user, options = {}) { function doNotTrack() { let flag; - if (navigator && navigator.doNotTrack !== undefined) { - flag = navigator.doNotTrack; // FF, Chrome - } else if (navigator && navigator.msDoNotTrack !== undefined) { - flag = navigator.msDoNotTrack; // IE 9/10 + if (window.navigator && window.navigator.doNotTrack !== undefined) { + flag = window.navigator.doNotTrack; // FF, Chrome + } else if (window.navigator && window.navigator.msDoNotTrack !== undefined) { + flag = window.navigator.msDoNotTrack; // IE 9/10 } else { flag = window.doNotTrack; // IE 11+, Safari } @@ -460,7 +460,7 @@ export function initialize(env, user, options = {}) { } else if ( typeof options.bootstrap === 'string' && options.bootstrap.toUpperCase() === 'LOCALSTORAGE' && - !!localStorage + !!window.localStorage ) { useLocalStorage = true; @@ -520,11 +520,11 @@ export function initialize(env, user, options = {}) { } function watchLocation(interval, callback) { - let previousUrl = location.href; + let previousUrl = window.location.href; let currentUrl; function checkUrl() { - currentUrl = location.href; + currentUrl = window.location.href; if (currentUrl !== previousUrl) { previousUrl = currentUrl; @@ -541,7 +541,7 @@ export function initialize(env, user, options = {}) { poll(checkUrl, interval); - if (!!(window.history && history.pushState)) { + if (!!(window.history && window.history.pushState)) { window.addEventListener('popstate', checkUrl); } else { window.addEventListener('hashchange', checkUrl); diff --git a/src/messages.js b/src/messages.js index 772c9c4c..fc472f6a 100644 --- a/src/messages.js +++ b/src/messages.js @@ -1,3 +1,5 @@ +import * as errors from './errors'; + const docLink = ' Please see https://docs.launchdarkly.com/docs/js-sdk-reference#section-initializing-the-client for instructions on SDK initialization.'; From fdd57e4466bfc84f08f18465d744d8bdffebc6b3 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 9 Nov 2018 13:20:52 -0800 Subject: [PATCH 02/10] fix TypeScript def for event name and add documentation --- typings.d.ts | 59 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/typings.d.ts b/typings.d.ts index 9d6ba3cf..af774b7f 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -18,11 +18,6 @@ declare module 'ldclient-js' { export default LaunchDarkly; - /** - * The names of events to which users of the client can subscribe. - */ - export type LDEventName = 'ready' | 'change'; - /** * The types of values a feature flag can have. * @@ -48,12 +43,42 @@ declare module 'ldclient-js' { }; /** - * The parameters required to (un)subscribe to/from LaunchDarkly events. + * The parameters required to (un)subscribe to/from LaunchDarkly events. See + * LDClient#on and LDClient#off. + * + * The following event names (keys) are used by the cliet: + * + * "ready": The client has finished starting up. This event will be sent regardless + * of whether it successfully connected to LaunchDarkly, or encountered an error + * and had to give up; to distinguish between these cases, see below. + * + * "initialized": The client successfully started up and has valid feature flag + * data. This will always be accompanied by "ready". + * + * "failed": The client encountered an error that prevented it from connecting to + * LaunchDarkly, such as an invalid environment ID. This will always be accompanied + * by "ready". + * + * "error": General event for any kind of error condition during client operation. + * The callback parameter is an Error object. If you do not listen for "error" + * events, then the errors will be logged with console.log(). + * + * "change": The client has received new feature flag data. This can happen either + * because you have switched users with identify(), or because the client has a + * stream connection and has received a live change to a flag value (see below). + * The callback parameter is an LDFlagChangeset. + * + * "change:FLAG-KEY": The client has received a new value for a specific flag + * whose key is FLAG-KEY. The callback receives two parameters: the current (new) + * flag value, and the previous value. This is always accompanied by a general + * "change" event as described above; you can listen for either or both. * - * See LDClient#on and LDClient#off. + * The "change" and "change:FLAG-KEY" events have special behavior: the client + * will open a streaming connection to receive live changes if and only if you + * are listening for one of these events. */ type LDEventSignature = ( - key: LDEventName, + key: string, callback: (current?: LDFlagValue | LDFlagChangeset, previous?: LDFlagValue) => void, context?: any ) => void; @@ -400,28 +425,24 @@ declare module 'ldclient-js' { variationDetail: (key: string, defaultValue?: LDFlagValue) => LDEvaluationDetail; /** - * Registers an event listener. + * Registers an event listener. See LDEventSignature for the available event types + * and the data that can be associated with them. * * @param key - * The name of the event for which to listen. This can be "ready", - * "change", or "change:FLAG-KEY". + * The name of the event for which to listen. * @param callback - * The function to execute when the event fires. For the "change" - * event, the callback receives one parameter: an LDFlagChangeset - * describing the changes. For "change:FLAG-KEY" events, the callback - * receives two parameters: the current (new) value and the previous - * value of the relevant flag. + * The function to execute when the event fires. The callback may or may not + * receive parameters, depending on the type of event; see LDEventSignature. * @param context * The "this" context to use for the callback. */ on: LDEventSignature; /** - * Deregisters an event listener. + * Deregisters an event listener. See LDEventSignature for the available event types. * * @param key - * The name of the event for which to stop listening. This can be - * "ready", "change", or "change:FLAG-KEY". + * The name of the event for which to stop listening. * @param callback * The function to deregister. * @param context From 6b43e9adbf86df50d5600504c645dffe2c59e4e4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 9 Nov 2018 13:23:10 -0800 Subject: [PATCH 03/10] comment edit --- typings.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings.d.ts b/typings.d.ts index af774b7f..efebf536 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -56,8 +56,8 @@ declare module 'ldclient-js' { * data. This will always be accompanied by "ready". * * "failed": The client encountered an error that prevented it from connecting to - * LaunchDarkly, such as an invalid environment ID. This will always be accompanied - * by "ready". + * LaunchDarkly, such as an invalid environment ID. All flag evaluations will + * therefore receive default values. This will always be accompanied by "ready". * * "error": General event for any kind of error condition during client operation. * The callback parameter is an Error object. If you do not listen for "error" From 65e44c16beba441ea38fce9825d8d26bff8a0801 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 20 Nov 2018 16:14:52 -0800 Subject: [PATCH 04/10] replace Base64 dependency with a package that has a lowercase name --- package-lock.json | 10 +++++----- package.json | 4 ++-- src/__tests__/EventSender-test.js | 4 ++-- src/utils.js | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4009ca3b..ce506020 100644 --- a/package-lock.json +++ b/package-lock.json @@ -233,11 +233,6 @@ "integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==", "dev": true }, - "Base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/Base64/-/Base64-1.0.1.tgz", - "integrity": "sha1-3vRcxQyWG8yb8jIdD1K8v+wfG7E=" - }, "abab": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", @@ -5891,6 +5886,11 @@ "merge-stream": "^1.0.1" } }, + "js-base64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz", + "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==" + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", diff --git a/package.json b/package.json index 7c65fc23..b9497c86 100755 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "typescript": "3.0.1" }, "dependencies": { - "Base64": "1.0.1", - "escape-string-regexp": "1.0.5" + "escape-string-regexp": "1.0.5", + "js-base64": "2.4.9" }, "repository": { "type": "git", diff --git a/src/__tests__/EventSender-test.js b/src/__tests__/EventSender-test.js index ca5ac683..670b78f6 100644 --- a/src/__tests__/EventSender-test.js +++ b/src/__tests__/EventSender-test.js @@ -1,4 +1,4 @@ -import Base64 from 'Base64'; +import { Base64 } from 'js-base64'; import sinon from 'sinon'; import EventSender from '../EventSender'; @@ -44,7 +44,7 @@ describe('EventSender', () => { s = s + '='; } s = s.replace(/_/g, '/').replace(/-/g, '+'); - return decodeURIComponent(escape(Base64.atob(s))); + return decodeURIComponent(escape(Base64.decode(s))); } function decodeOutputFromUrl(url) { diff --git a/src/utils.js b/src/utils.js index c9edeaba..beec17b5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,8 +1,8 @@ -import Base64 from 'Base64'; +import { Base64 } from 'js-base64'; // See http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html export function btoa(s) { - return Base64.btoa(unescape(encodeURIComponent(s))); + return Base64.encode(unescape(encodeURIComponent(s))); } export function base64URLEncode(s) { From 6baa06e3a4bc2d7669b136003823e9cc1801ea26 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 20 Nov 2018 16:43:53 -0800 Subject: [PATCH 05/10] use a different package due to import problems --- package-lock.json | 10 +++++----- package.json | 4 ++-- src/__tests__/EventSender-test.js | 6 ++++-- src/utils.js | 13 +++++++++++-- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce506020..736da68a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1486,6 +1486,11 @@ } } }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -5886,11 +5891,6 @@ "merge-stream": "^1.0.1" } }, - "js-base64": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz", - "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==" - }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", diff --git a/package.json b/package.json index b9497c86..d1c6c00f 100755 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "typescript": "3.0.1" }, "dependencies": { - "escape-string-regexp": "1.0.5", - "js-base64": "2.4.9" + "base64-js": "1.3.0", + "escape-string-regexp": "1.0.5" }, "repository": { "type": "git", diff --git a/src/__tests__/EventSender-test.js b/src/__tests__/EventSender-test.js index 670b78f6..c130de76 100644 --- a/src/__tests__/EventSender-test.js +++ b/src/__tests__/EventSender-test.js @@ -1,4 +1,4 @@ -import { Base64 } from 'js-base64'; +import * as base64 from 'base64-js'; import sinon from 'sinon'; import EventSender from '../EventSender'; @@ -44,7 +44,9 @@ describe('EventSender', () => { s = s + '='; } s = s.replace(/_/g, '/').replace(/-/g, '+'); - return decodeURIComponent(escape(Base64.decode(s))); + const decodedBytes = base64.toByteArray(s); + const decodedStr = String.fromCharCode.apply(String, decodedBytes); + return decodeURIComponent(escape(decodedStr)); } function decodeOutputFromUrl(url) { diff --git a/src/utils.js b/src/utils.js index beec17b5..98d0f2cc 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,8 +1,17 @@ -import { Base64 } from 'js-base64'; +import * as base64 from 'base64-js'; // See http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html export function btoa(s) { - return Base64.encode(unescape(encodeURIComponent(s))); + const escaped = unescape(encodeURIComponent(s)); + return base64.fromByteArray(stringToBytes(escaped)); +} + +function stringToBytes(s) { + var b = []; + for (var i = 0; i < s.length; i++) { + b.push(s.charCodeAt(i)); + } + return b; } export function base64URLEncode(s) { From d5228b683292c928c2004626e7bff4b96b0eca3a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 20 Nov 2018 16:45:45 -0800 Subject: [PATCH 06/10] linter --- src/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.js b/src/utils.js index 98d0f2cc..c716232a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -7,8 +7,8 @@ export function btoa(s) { } function stringToBytes(s) { - var b = []; - for (var i = 0; i < s.length; i++) { + const b = []; + for (let i = 0; i < s.length; i++) { b.push(s.charCodeAt(i)); } return b; From c1ef0cb2243d26a1a4d993ed2a45079cb46d8e83 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 20 Nov 2018 17:11:41 -0800 Subject: [PATCH 07/10] override short default timeout in one EventSource polyfill --- src/Stream.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Stream.js b/src/Stream.js index 07548c1f..d993554e 100644 --- a/src/Stream.js +++ b/src/Stream.js @@ -62,7 +62,17 @@ export default function Stream(baseUrl, environment, hash, config) { url = url + (query ? '?' : '') + query; closeConnection(); - es = new window.EventSource(url); + + // The standard EventSource constructor doesn't take any options, just a URL. However, there's + // a known issue with one of the EventSource polyfills, Yaffle, which has a fairly short + // default timeout - much shorter than our heartbeat interval - causing unnecessary reconnect + // attempts and error logging. Yaffle allows us to override this with the "heartbeatTimeout" + // property. This should be ignored by other implementations that don't have such an option. + const options = { + heartbeatTimeout: 300000 // 5-minute timeout; LD stream sends heartbeats every 3 min + }; + + es = new window.EventSource(url, options); for (const key in handlers) { if (handlers.hasOwnProperty(key)) { es.addEventListener(key, handlers[key]); From 6bdeb156ef95f1fb68713e57a7716021ed86170f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 20 Nov 2018 17:13:51 -0800 Subject: [PATCH 08/10] linter --- src/Stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stream.js b/src/Stream.js index d993554e..4ad9e510 100644 --- a/src/Stream.js +++ b/src/Stream.js @@ -69,7 +69,7 @@ export default function Stream(baseUrl, environment, hash, config) { // attempts and error logging. Yaffle allows us to override this with the "heartbeatTimeout" // property. This should be ignored by other implementations that don't have such an option. const options = { - heartbeatTimeout: 300000 // 5-minute timeout; LD stream sends heartbeats every 3 min + heartbeatTimeout: 300000, // 5-minute timeout; LD stream sends heartbeats every 3 min }; es = new window.EventSource(url, options); From 7246704cdd21272ca696b64011ba5eed0c12a90f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 21 Nov 2018 16:10:21 -0800 Subject: [PATCH 09/10] misc fixes --- README.md | 2 +- src/Stream.js | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e2214596..e1216d72 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ when `client.on('change')` is called. If you need streaming support, and you wish to support browsers that do not support `EventSource` natively, you can install a polyfill such as -[EventSource](https://github.com/Yaffle/EventSource). +[event-source-polyfill](https://github.com/Yaffle/EventSource). #### CDN diff --git a/src/Stream.js b/src/Stream.js index 4ad9e510..8476c4a7 100644 --- a/src/Stream.js +++ b/src/Stream.js @@ -6,6 +6,7 @@ export default function Stream(baseUrl, environment, hash, config) { const useReport = (config && config.useReport) || false; const withReasons = (config && config.evaluationReasons) || false; const streamReconnectDelay = (config && config.streamReconnectDelay) || 1000; + const timeoutMillis = 300000; // 5 minutes (same as other SDKs) - note, this only has an effect on polyfills let es = null; let reconnectTimeoutReference = null; let user = null; @@ -63,13 +64,13 @@ export default function Stream(baseUrl, environment, hash, config) { closeConnection(); - // The standard EventSource constructor doesn't take any options, just a URL. However, there's - // a known issue with one of the EventSource polyfills, Yaffle, which has a fairly short - // default timeout - much shorter than our heartbeat interval - causing unnecessary reconnect - // attempts and error logging. Yaffle allows us to override this with the "heartbeatTimeout" - // property. This should be ignored by other implementations that don't have such an option. + // The standard EventSource constructor doesn't take any options, just a URL. However, some + // EventSource polyfills allow us to specify a timeout interval, and in some cases they will + // default to a too-short timeout if we don't specify one. So, here, we are setting the + // timeout properties that are used by several popular polyfills. const options = { - heartbeatTimeout: 300000, // 5-minute timeout; LD stream sends heartbeats every 3 min + heartbeatTimeout: timeoutMillis, // used by "event-source-polyfill" package + silentTimeout: timeoutMillis, // used by "eventsource-polyfill" package }; es = new window.EventSource(url, options); From ff3448cfc2046c20b54c4d44f8e5311768c50d6e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 21 Nov 2018 16:15:00 -0800 Subject: [PATCH 10/10] version 2.7.3 --- CHANGELOG.md | 5 +++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b02d15..b5cde27b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to the LaunchDarkly client-side JavaScript SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.7.3] - 2018-11-21 +### Fixed: +- When using the [`event-source-polyfill`](https://github.com/Yaffle/EventSource) package to allow streaming mode in browsers with no native EventSource support, the polyfill was using a default read timeout of 45 seconds, so if no updates arrived within 45 seconds it would log an error and reconnect the stream. The SDK now sets its own timeout (5 minutes) which will be used if this particular polyfill is active. LaunchDarkly normally sends a heartbeat every 3 minutes, so you should not see a timeout happen unless the connection has been lost. +- The SDK's use of the "Base64" package caused problems for build tools that strictly enforce the lowercase package name rule. It now uses the "base64-js" package instead. ([#124](https://github.com/launchdarkly/js-client/issues/124)) + ## [2.7.2] - 2018-10-17 ### Fixed: - Disconnecting from the stream does not close the browser tab anymore. diff --git a/package-lock.json b/package-lock.json index 736da68a..e5262fe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ldclient-js", - "version": "2.7.2", + "version": "2.7.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d1c6c00f..3df583a2 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ldclient-js", - "version": "2.7.2", + "version": "2.7.3", "description": "LaunchDarkly SDK for JavaScript", "author": "LaunchDarkly ", "license": "Apache-2.0",