Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4d52b90
Improve linting (#116)
apucacao Oct 17, 2018
fdd57e4
fix TypeScript def for event name and add documentation
eli-darkly Nov 9, 2018
6b43e9a
comment edit
eli-darkly Nov 9, 2018
d6c0f62
Merge pull request #121 from launchdarkly/eb/ch26618/event-name-ts
eli-darkly Nov 9, 2018
65e44c1
replace Base64 dependency with a package that has a lowercase name
eli-darkly Nov 21, 2018
6baa06e
use a different package due to import problems
eli-darkly Nov 21, 2018
d5228b6
linter
eli-darkly Nov 21, 2018
b3c1778
Merge pull request #124 from launchdarkly/eb/ch26918/base64-dep
eli-darkly Nov 21, 2018
c1ef0cb
override short default timeout in one EventSource polyfill
eli-darkly Nov 21, 2018
6bdeb15
linter
eli-darkly Nov 21, 2018
7246704
misc fixes
eli-darkly Nov 22, 2018
aaa3a0f
Merge pull request #125 from launchdarkly/eb/ch27026/eventsource-config
eli-darkly Nov 22, 2018
0101947
Merge branch 'master' of github.com:launchdarkly/js-client
eli-darkly Nov 22, 2018
d6b2d68
Merge branch 'master' of github.com:launchdarkly/js-client
eli-darkly Nov 22, 2018
800999b
Merge branch 'master' of github.com:launchdarkly/js-client
eli-darkly Nov 22, 2018
0786a48
allow streaming mode to be decoupled from event subscription
eli-darkly Nov 24, 2018
bb29194
misc cleanup
eli-darkly Nov 24, 2018
624afa8
expand unit test to make sure off() really works
eli-darkly Nov 27, 2018
24bb4c4
linter
eli-darkly Nov 27, 2018
4bba83f
simplify code using spread operator
eli-darkly Nov 27, 2018
e15b202
Merge pull request #127 from launchdarkly/eb/ch25474-ch18940/change-e…
eli-darkly Nov 27, 2018
525ca37
fire change event when updating flags after bootstrap from localstorage
eli-darkly Nov 27, 2018
af0fcfc
linter
eli-darkly Nov 27, 2018
45fc561
Merge pull request #131 from launchdarkly/eb/ch27394/change-event-aft…
eli-darkly Nov 27, 2018
41b1b2e
fix typos
brooswit Nov 29, 2018
84441c2
Merge pull request #135 from launchdarkly/jw/ch27493/readme-typos
Nov 29, 2018
a4e07d4
use deep compare to decide whether a flag value has changed
eli-darkly Nov 29, 2018
097896e
Merge pull request #137 from launchdarkly/eb/ch27500/deep-equals
eli-darkly Nov 30, 2018
7ae2cfa
version 2.8.0
eli-darkly Dec 3, 2018
b18507a
rm mistakenly checked-in files
eli-darkly Dec 3, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
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.8.0] - 2018-12-03
### Added:
- The use of a streaming connection to LaunchDarkly for receiving live updates can now be controlled with the new `client.setStreaming()` method, or the equivalent boolean `streaming` property in the client configuration. If you set this to `false`, the client will not open a streaming connection even if you subscribe to change events (you might want to do this if, for instance, you just want to be notified when the client gets new flag values due to having switched users). If you set it to `true`, the client will open a streaming connection regardless of whether you subscribe to change events or not (the flag values will simply be updated in the background). If you don't set it either way then the default behavior still applies, i.e. the client opens a streaming connection if and only if you subscribe to change events.

### Fixed:
- If the client opened a streaming connection because you called `on('change', ...)` one or more times, it will not close the connection until you call `off()` for _all_ of your event listeners. Previously, it was closing the connection whenever `off('change')` was called, even if you still had a listener for `'change:specific-flag-key'`.
- The client's logic for signaling a `change` event was using a regular Javascript `===` comparison, so it could incorrectly decide that a flag had changed if its value was a JSON object or an array. This has been fixed to use deep equality checking for object and array values.

## [2.7.5] - 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.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The LaunchDarkly client-side JavaScript SDK supports the following browsers:

* Chrome (any recent)
* Firefox (any recent)
* Safari (any recent)\*
* Safari (any recent)
* Internet Explorer (IE10+)\*
* Edge (any recent)\*
* Opera (any recent)\*
Expand Down Expand Up @@ -67,7 +67,7 @@ Then import it before the module that initializes the LaunchDarkly client:

### Promise polyfill

The newer versions of the use `Promise`. If you need to support older browsers, you will
Newer versions of the SDK use `Promise`. If you need to support older browsers, you will
need to install a polyfill for it, such as [es6-promise](https://github.com/stefanpenner/es6-promise).

#### CDN
Expand Down
17 changes: 12 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ldclient-js",
"version": "2.7.5",
"version": "2.8.0",
"description": "LaunchDarkly SDK for JavaScript",
"author": "LaunchDarkly <team@launchdarkly.com>",
"license": "Apache-2.0",
Expand Down Expand Up @@ -79,7 +79,8 @@
},
"dependencies": {
"base64-js": "1.3.0",
"escape-string-regexp": "1.0.5"
"escape-string-regexp": "1.0.5",
"fast-deep-equal": "2.0.1"
},
"repository": {
"type": "git",
Expand Down
8 changes: 8 additions & 0 deletions src/EventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export default function EventEmitter() {
}
};

emitter.getEvents = function() {
return Object.keys(events);
};

emitter.getEventListenerCount = function(event) {
return events[event] ? events[event].length : 0;
};

emitter.maybeReportError = function(error) {
if (!error) {
return;
Expand Down
203 changes: 184 additions & 19 deletions src/__tests__/LDClient-streaming-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,37 +44,181 @@ describe('LDClient', () => {

describe('streaming/event listening', () => {
const streamUrl = 'https://clientstream.launchdarkly.com';
const fullStreamUrlWithUser = streamUrl + '/eval/' + envName + '/' + encodedUser;

function streamEvents() {
return sources[`${streamUrl}/eval/${envName}/${encodedUser}`].__emitter._events;
return sources[fullStreamUrlWithUser].__emitter._events;
}

function expectStreamUrlIsOpen(url) {
expect(Object.keys(sources)).toEqual([url]);
}

function expectNoStreamIsOpen() {
expect(sources).toMatchObject({});
}

it('does not connect to the stream by default', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {} });

client.on('ready', () => {
expect(sources).toMatchObject({});
expectNoStreamIsOpen();
done();
});
});

it('connects to the stream when listening to global change events', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {} });
it('connects to the stream if options.streaming is true', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {}, streaming: true });

client.on('ready', () => {
client.on('change', () => {});
expect(Object.keys(sources)).toEqual([streamUrl + '/eval/' + envName + '/' + encodedUser]);
expectStreamUrlIsOpen(fullStreamUrlWithUser);
done();
});
});

it('connects to the stream when listening to change event for one flag', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {} });
describe('setStreaming()', () => {
it('can connect to the stream', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {} });

client.on('ready', () => {
client.on('change:flagkey', () => {});
expect(Object.keys(sources)).toEqual([streamUrl + '/eval/' + envName + '/' + encodedUser]);
done();
client.on('ready', () => {
client.setStreaming(true);
expectStreamUrlIsOpen(fullStreamUrlWithUser);
done();
});
});

it('can disconnect from the stream', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {} });

client.on('ready', () => {
client.setStreaming(true);
expectStreamUrlIsOpen(fullStreamUrlWithUser);
client.setStreaming(false);
expectNoStreamIsOpen();
done();
});
});
});

describe('on("change")', () => {
it('connects to the stream if not otherwise overridden', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {} });

client.on('ready', () => {
client.on('change', () => {});
expectStreamUrlIsOpen(fullStreamUrlWithUser);
done();
});
});

it('also connects if listening for a specific flag', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {} });

client.on('ready', () => {
client.on('change:flagkey', () => {});
expectStreamUrlIsOpen(fullStreamUrlWithUser);
done();
});
});

it('does not connect if some other kind of event was specified', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {} });

client.on('ready', () => {
client.on('error', () => {});
expectNoStreamIsOpen();
done();
});
});

it('does not connect if options.streaming is explicitly set to false', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {}, streaming: false });

client.on('ready', () => {
client.on('change', () => {});
expectNoStreamIsOpen();
done();
});
});

it('does not connect if setStreaming(false) was called', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {} });

client.on('ready', () => {
client.setStreaming(false);
client.on('change', () => {});
expectNoStreamIsOpen();
done();
});
});
});

describe('off("change")', () => {
it('disconnects from the stream if all event listeners are removed', done => {
const client = LDClient.initialize(envName, user, { bootstrap: {} });
const listener1 = () => {};
const listener2 = () => {};

client.on('ready', () => {
client.on('change', listener1);
client.on('change:flagkey', listener2);
client.on('error', () => {});
expectStreamUrlIsOpen(fullStreamUrlWithUser);

client.off('change', listener1);
expectStreamUrlIsOpen(fullStreamUrlWithUser);

client.off('change:flagkey', listener2);
expectNoStreamIsOpen();

done();
});
});

it('does not disconnect if setStreaming(true) was called, but still removes event listener', done => {
const changes1 = [];
const changes2 = [];

const client = LDClient.initialize(envName, user, { bootstrap: {} });
const listener1 = allValues => changes1.push(allValues);
const listener2 = newValue => changes2.push(newValue);

client.on('ready', () => {
client.setStreaming(true);

client.on('change', listener1);
client.on('change:flag', listener2);
expectStreamUrlIsOpen(fullStreamUrlWithUser);

streamEvents().put({
data: '{"flag":{"value":"a","version":1}}',
});

expect(changes1).toEqual([{ flag: { current: 'a', previous: undefined } }]);
expect(changes2).toEqual(['a']);

client.off('change', listener1);
expectStreamUrlIsOpen(fullStreamUrlWithUser);

streamEvents().put({
data: '{"flag":{"value":"b","version":1}}',
});

expect(changes1).toEqual([{ flag: { current: 'a', previous: undefined } }]);
expect(changes2).toEqual(['a', 'b']);

client.off('change:flag', listener2);
expectStreamUrlIsOpen(fullStreamUrlWithUser);

streamEvents().put({
data: '{"flag":{"value":"c","version":1}}',
});

expect(changes1).toEqual([{ flag: { current: 'a', previous: undefined } }]);
expect(changes2).toEqual(['a', 'b']);

done();
});
});
});

Expand All @@ -83,7 +227,7 @@ describe('LDClient', () => {

client.on('ready', () => {
client.on('change:flagkey', () => {});
expect(Object.keys(sources)).toEqual([streamUrl + '/eval/' + envName + '/' + encodedUser + '?h=' + hash]);
expectStreamUrlIsOpen(fullStreamUrlWithUser + '?h=' + hash);
done();
});
});
Expand All @@ -93,9 +237,7 @@ describe('LDClient', () => {

client.on('ready', () => {
client.on('change', () => {});
expect(Object.keys(sources)).toEqual([
streamUrl + '/eval/' + envName + '/' + encodedUser + '?withReasons=true',
]);
expectStreamUrlIsOpen(fullStreamUrlWithUser + '?withReasons=true');
done();
});
});
Expand All @@ -105,9 +247,7 @@ describe('LDClient', () => {

client.on('ready', () => {
client.on('change', () => {});
expect(Object.keys(sources)).toEqual([
streamUrl + '/eval/' + envName + '/' + encodedUser + '?h=' + hash + '&withReasons=true',
]);
expectStreamUrlIsOpen(fullStreamUrlWithUser + '?h=' + hash + '&withReasons=true');
done();
});
});
Expand Down Expand Up @@ -181,6 +321,31 @@ describe('LDClient', () => {
});
});

it('does not fire change event if new and old values are equivalent JSON objects', done => {
const client = LDClient.initialize(envName, user, {
bootstrap: {
'will-change': 3,
'wont-change': { a: 1, b: 2 },
},
});

client.on('ready', () => {
client.on('change', changes => {
expect(changes).toEqual({
'will-change': { current: 4, previous: 3 },
});

done();
});

const putData = {
'will-change': { value: 4, version: 2 },
'wont-change': { value: { b: 2, a: 1 }, version: 2 },
};
streamEvents().put({ data: JSON.stringify(putData) });
});
});

it('fires individual change event when flags are updated from put event', done => {
const client = LDClient.initialize(envName, user, { bootstrap: { 'enable-foo': false } });

Expand Down
19 changes: 19 additions & 0 deletions src/__tests__/LDClient-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,25 @@ describe('LDClient', () => {
.catch(() => {});
});

it('should load flags from local storage and then request newer ones', done => {
const json = '{"flag": "a"}';

window.localStorage.setItem(lsKey, json);

const client = LDClient.initialize(envName, user, { bootstrap: 'localstorage', streaming: false });

client.waitForInitialization().then(() => {
expect(client.variation('flag')).toEqual('a');

client.on('change:flag', newValue => {
expect(newValue).toEqual('b');
done();
});

requests[0].respond(200, { 'Content-Type': 'application/json' }, '{"flag": {"value": "b", "version": 2}}');
});
});

it('should start with empty flags if we tried to use cached settings and there are none', done => {
window.localStorage.removeItem(lsKey);

Expand Down
Loading