Skip to content

Commit

Permalink
Merge pull request #16 from achambers/support-streaming-flag-updates
Browse files Browse the repository at this point in the history
Support streaming of flag changes
  • Loading branch information
achambers authored Oct 6, 2017
2 parents 75e022c + 2269d31 commit 33c0b9d
Show file tree
Hide file tree
Showing 7 changed files with 348 additions and 43 deletions.
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ A list of initial values for your feature flags. This property is only used when

_Default_: `null`

### `streaming`

Streaming options for the feature flags for which you'd like to subscribe to realtime updates. See the [Streaming Feature Flags section](#streaming-feature-flags) for more detailed info on what the possible options are for streaming flags.

_Default_: `false`

## Content Security Policy

If you have CSP enabled in your ember application, you will need to add Launch Darkly to the `connect-src` like so:
Expand Down Expand Up @@ -266,6 +272,50 @@ When `local: true`, the Launch Darkly feature service is available in the browse
> ld.user() // return the user that the client has been initialized with
```

## Streaming Feature Flags

Launch Darkly supports the ability to subsribe to changes to feature flags so that apps can react in realtime to these changes. The [`streaming` configuration option](#streaming) allows you to specify, in a couple of ways, which flags you'd like to stream.

To disable streaming completely, use the following configuration:

```js
launchDarkly: {
streaming: false
}
```

_Note, this is the default behaviour if the `streaming` option is not specified._

To stream all flags, use the following configuration:

```
launchDarkly: {
streaming: true
}
```

To get more specific, you can select to stream all flags except those specified:

```
launchDarkly: {
streaming: {
allExcept: ['apply-discount', 'new-login']
}
}
```

And, finally, you can specify only which flags you would like to stream:

```
launchDarkly: {
streaming: {
'apply-discount': true
}
}
```

As Launch Darkly's realtime updates to flags uses the [Event Source API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), certain browsers will require a polyfill to be included. ember-launch-darkly uses [EmberCLI targets](http://rwjblue.com/2017/04/21/ember-cli-targets/) to automatically decide whether or not to include the polyfill. Ensure your project contains a valid `config/targets.js` file if you require this functionality.

## Test Helpers

### Acceptance Tests
Expand Down Expand Up @@ -349,6 +399,5 @@ test('new pricing', function(assert) {
## TODO

- Implement support for `secure` mode ([#9](https://github.com/kayako/ember-launch-darkly/issues/9))
- Implement event source polyfill ([#8](https://github.com/kayako/ember-launch-darkly/issues/8))

<p align="center"><sub>Made with :heart: by The Kayako Engineering Team</sub></p>
34 changes: 29 additions & 5 deletions addon/services/launch-darkly-client-remote.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export default Service.extend(Evented, {
this._super(...arguments);
},

initialize(user = {}/*, options = {}*/) {
let { clientSideId } = this._config();
initialize(user = {}) {
let { clientSideId, streaming = false } = this._config();

assert('ENV.launchDarkly.clientSideId must be specified in config/environment.js', clientSideId);

Expand Down Expand Up @@ -46,7 +46,7 @@ export default Service.extend(Evented, {
return RSVP.resolve();
}

return this._initialize(clientSideId, user);
return this._initialize(clientSideId, user, streaming);
},

identify(user) {
Expand All @@ -67,15 +67,23 @@ export default Service.extend(Evented, {
return appConfig.launchDarkly || {};
},

_initialize(id, user/*, options*/) {
_initialize(id, user, streamingOptions) {
return new RSVP.Promise((resolve, reject) => {
let client = window.LDClient.initialize(id, user/*, options*/);
let client = window.LDClient.initialize(id, user);

client.on('ready', () => {
this.set('_client', client);
run(null, resolve);
});

client.on('change', settings => {
Object.keys(settings).forEach(key => {
if (this._shouldTriggerEvent(key, streamingOptions)) {
this.trigger(key);
}
});
});

run.later(this, () => {
if (!this.get('_client')) {
run(null, reject);
Expand All @@ -88,5 +96,21 @@ export default Service.extend(Evented, {
return new RSVP.Promise(resolve => {
this.get('_client').identify(user, null, resolve);
})
},

_shouldTriggerEvent(key, streamingOptions) {
if (streamingOptions === true) {
return true;
}

if (typeof streamingOptions === 'object') {
if (streamingOptions.allExcept && Array.isArray(streamingOptions.allExcept)) {
return streamingOptions.allExcept.indexOf(key) === -1;
}

return streamingOptions[key];
}

return false;
}
});
50 changes: 45 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
/* eslint-env node */
'use strict';

var path = require('path');
var Funnel = require('broccoli-funnel');
var MergeTrees = require('broccoli-merge-trees');
const path = require('path');
const Funnel = require('broccoli-funnel');
const MergeTrees = require('broccoli-merge-trees');
const browserslist = require('browserslist');

const EVENT_SOURCE_NON_SUPPORTED_BROWSERS = [
'Chrome < 6',
'Edge > 0',
'Firefox < 6.0',
'ie > 0',
'Safari < 5'
];

module.exports = {
name: 'ember-launch-darkly',
Expand All @@ -26,13 +35,44 @@ module.exports = {
}

this.import('vendor/ldclient.js');

if (this._shouldIncludePolyfill()) {
this.import('vendor/eventsource.js');
}
},

treeForVendor(vendorTree) {
var ldTree = new Funnel(path.dirname(require.resolve('ldclient-js/dist/ldclient.js')), {
let trees = vendorTree ? [vendorTree] : [];

trees.push(this._launchDarklyTree());

if (this._shouldIncludePolyfill()) {
trees.push(this._eventSourceTree());
}

return new MergeTrees(trees);
},

_launchDarklyTree() {
return new Funnel(path.dirname(require.resolve('ldclient-js/dist/ldclient.js')), {
files: ['ldclient.js'],
});
},

_eventSourceTree() {
return new Funnel(path.dirname(require.resolve('event-source-polyfill/eventsource.js')), {
files: ['eventsource.js'],
});
},

_shouldIncludePolyfill() {
if (this.project.targets && this.project.targets.browsers) {
let browsers = browserslist(this.project.targets.browsers);
let prohibitedBrowsers = browserslist(EVENT_SOURCE_NON_SUPPORTED_BROWSERS);

return prohibitedBrowsers.filter(version => browsers.includes(version)).length;
}

return vendorTree ? new MergeTrees([vendorTree, ldTree]) : ldTree;
return false;
}
};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"dependencies": {
"ember-cli-babel": "^6.3.0",
"event-source-polyfill": "^0.0.9",
"ldclient-js": "1.1.12"
},
"devDependencies": {
Expand All @@ -31,9 +32,11 @@
"broccoli-asset-rev": "^2.4.5",
"broccoli-funnel": "^2.0.0",
"broccoli-merge-trees": "^2.0.0",
"browserslist": "^2.4.0",
"chai": "^4.1.1",
"ember-ajax": "^3.0.0",
"ember-cli": "~2.14.0",
"ember-cli-babili": "^0.2.0",
"ember-cli-dependency-checker": "^1.3.0",
"ember-cli-eslint": "^3.0.0",
"ember-cli-htmlbars": "^2.0.1",
Expand All @@ -43,7 +46,6 @@
"ember-cli-release": "^1.0.0-beta.2",
"ember-cli-shims": "^1.1.0",
"ember-cli-sri": "^2.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-code-snippet": "^2.0.0",
"ember-disable-prototype-extensions": "^1.1.2",
"ember-export-application-global": "^2.0.0",
Expand Down
8 changes: 8 additions & 0 deletions tests/dummy/config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ module.exports = function(environment) {

launchDarkly: {
clientSideId: 'xxx',
streaming: true,
//streaming: {
//allExcept: ['apply-discount']
//},
//streaming: {
//'apply-discount': true,
//'some-other-flag': true
//},
localFeatureFlags: {
'apply-discount': true
}
Expand Down
9 changes: 5 additions & 4 deletions tests/dummy/config/targets.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/* eslint-env node */
module.exports = {
browsers: [
'ie 9',
'last 1 Chrome versions',
'last 1 Firefox versions',
'last 1 Safari versions'
'ie 10',
'last 2 Edge versions',
'last 2 Chrome versions',
'last 2 Firefox versions',
'last 2 Safari versions'
]
};
Loading

0 comments on commit 33c0b9d

Please sign in to comment.