Commit 5e1ebc60 authored by Nick Stenning's avatar Nick Stenning

Add a minimal feature client to the front-end

In order to respond to feature flag changes on the frontend, we need a
way of knowing what the current feature flag values are on the backend.

This commit adds a basic feature flag client that retrieves current
feature flag values by making an ajax request to the server. The client
also includes a cache, and supports retrieving new value from the server
when the cache expires.
parent 2ae0d7ec
/**
* Feature flag client.
*
* This is a small utility which will periodically retrieve the application
* feature flags from a JSON endpoint in order to expose these to the
* client-side application.
*
* All feature flags implicitly start toggled off. When `flagEnabled` is first
* called (or alternatively when `fetch` is called explicitly) an XMLHTTPRequest
* will be made to retrieve the current feature flag values from the server.
* Once these are retrieved, `flagEnabled` will return current values.
*
* If `flagEnabled` is called and the cache is more than `CACHE_TTL`
* milliseconds old, then it will trigger a new fetch of the feature flag
* values. Note that this is again done asynchronously, so it is only later
* calls to `flagEnabled` that will return the updated values.
*
* Users of this service should assume that the value of any given flag can
* change at any time and should write code accordingly. Feature flags should
* not be cached, and should not be interrogated only at setup time.
*/
"use strict";
var CACHE_TTL = 5 * 60 * 1000; // 5 minutes
function features ($document, $http, $log) {
var cache = null;
var featuresURL = new URL('/app/features', $document.prop('baseURI'));
function fetch() {
$http.get(featuresURL)
.success(function(data) {
cache = [Date.now(), data];
})
.error(function() {
$log.warn('features service: failed to load features data');
});
}
function flagEnabled(name) {
// Trigger a fetch if the cache is more than CACHE_TTL milliseconds old.
// We don't wait for the fetch to complete, so it's not this call that
// will see new data.
if (cache === null || (Date.now() - cache[0]) > CACHE_TTL) {
fetch();
}
if (cache === null) {
return false;
}
var flags = cache[1];
if (!flags.hasOwnProperty(name)) {
$log.warn('features service: looked up unknown feature:', name);
return false;
}
return flags[name];
}
return {
fetch: fetch,
flagEnabled: flagEnabled
};
}
module.exports = ['$document', '$http', '$log', features];
"use strict";
var mock = require('angular-mock');
var assert = chai.assert;
sinon.assert.expose(assert, {prefix: null});
describe('h:features', function () {
var $httpBackend;
var httpHandler;
var features;
var sandbox;
before(function () {
angular.module('h', [])
.service('features', require('../features'));
});
beforeEach(mock.module('h'));
beforeEach(mock.module(function ($provide) {
sandbox = sinon.sandbox.create();
var fakeDocument = {
prop: sandbox.stub()
};
fakeDocument.prop.withArgs('baseURI').returns('http://foo.com/');
$provide.value('$document', fakeDocument);
}));
beforeEach(mock.inject(function ($injector) {
$httpBackend = $injector.get('$httpBackend');
features = $injector.get('features');
httpHandler = $httpBackend.when('GET', 'http://foo.com/app/features');
httpHandler.respond(200, {foo: true, bar: false});
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
sandbox.restore();
});
it('fetch should retrieve features data', function () {
$httpBackend.expect('GET', 'http://foo.com/app/features');
features.fetch();
$httpBackend.flush();
});
it('fetch should not explode for errors fetching features data', function () {
httpHandler.respond(500, "ASPLODE!");
features.fetch();
$httpBackend.flush();
});
it('flagEnabled should retrieve features data', function () {
$httpBackend.expect('GET', 'http://foo.com/app/features');
features.flagEnabled('foo');
$httpBackend.flush();
});
it('flagEnabled should return false initially', function () {
var result = features.flagEnabled('foo');
$httpBackend.flush();
assert.isFalse(result);
});
it('flagEnabled should return flag values when data is loaded', function () {
features.fetch();
$httpBackend.flush();
var foo = features.flagEnabled('foo');
assert.isTrue(foo);
var bar = features.flagEnabled('bar');
assert.isFalse(bar);
});
it('flagEnabled should return false for unknown flags', function () {
features.fetch();
$httpBackend.flush();
var baz = features.flagEnabled('baz');
assert.isFalse(baz);
});
it('flagEnabled should trigger a new fetch after cache expiry', function () {
var clock = sandbox.useFakeTimers();
$httpBackend.expect('GET', 'http://foo.com/app/features');
features.flagEnabled('foo');
$httpBackend.flush();
clock.tick(301 * 1000);
$httpBackend.expect('GET', 'http://foo.com/app/features');
features.flagEnabled('foo');
$httpBackend.flush();
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment