@microsoft/refreshing-config
Version:
Configuration library that can dynamically refresh configuration values.
241 lines (221 loc) • 6.55 kB
JavaScript
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const debug = require('debug')('dynamic-config');
const EventEmitter = require('events');
const extend = require('extend');
const Q = require('q');
const patch = require('fast-json-patch');
const moment = require('moment');
const InMemoryConfigStore = require('./inmemoryStore').InMemoryConfigStore;
const InMemoryPubSubRefreshPolicyAndChangePublisher = require('./inmemoryStore').InMemoryPubSubRefreshPolicyAndChangePublisher;
// TODO: Support patching config
// TODO: Can we be fancier with data types?
// TODO: Review failure conditions with refreshPolicies and changePublishers
class RefreshingConfig extends EventEmitter {
constructor(store) {
super();
if (!store) {
debug('Missing store');
throw new Error('Missing store');
}
this.store = store;
this.refreshPolicies = [];
this.changePublishers = [];
this.values = {};
this.values._config = this;
this.firstTime = true;
}
get(name) {
if (!name) {
throw new Error('Missing name');
}
return this.refreshIfNeeded().then(() => {
return this.values[name];
});
}
getAll() {
return this.refreshIfNeeded().then(() => {
return this.values;
});
}
set(name, value) {
const self = this;
return this.store.set(name, value)
.then(() => {
this.emit('set', name, value);
this.changePublishers.forEach(publisher => {
try {
publisher.publish('set', name, value);
}
catch (e) {
// Empty block
}
});
})
.then(this.refresh.bind(self));
}
delete(name) {
const self = this;
return this.store.delete(name)
.then(() => {
this.emit('delete', name);
this.changePublishers.forEach(publisher => {
try {
publisher.publish('delete', name);
}
catch (e) {
// Empty block
}
});
})
.then(this.refresh.bind(self));
}
apply(patches) {
// Snapshot the property names and apply the patches. If a changed property is still present,
// it was changed so set. If it is now missing, delete it.
return this.refreshIfNeeded().then(() => {
const newValues = extend(true, {}, this.values);
patch.apply(newValues, patches);
const affected = this._getAffectedProperties(patches);
const oldKeys = Object.getOwnPropertyNames(this.values);
return Q.all(affected.map(key => {
if (newValues[key] !== undefined) {
return this.set(key, newValues[key]);
}
if (oldKeys.includes(key)) {
return this.delete(key);
}
return Q();
}));
});
}
_getAffectedProperties(patches) {
return Array.from(patches.reduce((result, patch) => {
result.add(patch.path.split('/')[1]);
return result;
}, new Set()));
}
withExtension(extension) {
if (!extension) {
return this;
}
if (typeof (extension['subscribe']) === 'function') {
extension.subscribe(this);
}
if (typeof (extension['shouldRefresh']) === 'function') {
this.refreshPolicies.push(extension);
}
if (typeof (extension['publish']) === 'function') {
this.changePublishers.push(extension);
}
return this;
}
refresh() {
if (this.refreshPromise) {
return this.refreshPromise;
}
const self = this;
this.refreshPromise = this.store.getAll()
.then(newValues => {
self.refreshPromise = null;
const configPatch = patch.compare(self.values, newValues);
const emitterPatchIndex = configPatch.findIndex(patch => patch.path === '/_config');
/* istanbul ignore else */
if (emitterPatchIndex >= 0) {
configPatch.splice(emitterPatchIndex, 1);
}
if (configPatch.length !== 0) {
patch.apply(self.values, configPatch);
self.emit('changed', self.values, configPatch);
}
self.emit('refresh', self.values);
return self.values;
});
return this.refreshPromise;
}
refreshIfNeeded() {
let shouldRefresh = this.firstTime;
this.firstTime = false;
if (!shouldRefresh) {
for (let i = 0; i < this.refreshPolicies.length; i++) {
const refreshPolicy = this.refreshPolicies[i];
if (refreshPolicy.shouldRefresh()) {
shouldRefresh = true;
break;
}
}
}
if (shouldRefresh) {
return this.refresh();
}
return Q(this.values);
}
}
// Refresh on every get/set
class AlwaysRefreshPolicy {
shouldRefresh() {
return true;
}
}
// Never refresh automatically
class NeverRefreshPolicy {
shouldRefresh() {
return false;
}
}
// Refresh if we haven't refreshed recently
class StaleRefreshPolicy {
constructor(duration) {
if (typeof duration !== 'number' || duration <= 0) {
throw new Error('Invalid duration');
}
this.duration = duration;
}
shouldRefresh() {
if (!this.lastRefresh || moment() > this.lastRefresh.add(this.duration, 'ms')) {
this.lastRefresh = moment();
return true;
}
return false;
}
}
// Refresh periodically
class IntervalRefreshPolicy {
constructor(duration) {
if (typeof duration !== 'number' || duration <= 0) {
throw new Error('Invalid duration');
}
this.duration = duration;
}
subscribe(subscriber) {
if (this.subscriber) {
throw new Error('Already subscribed');
}
this.subscriber = subscriber;
this.interval = setInterval(() => {
try {
subscriber.refresh();
}
catch (e) {
// Empty block
}
}, this.duration);
}
unsubscribe() {
if (this.interval) {
clearInterval(this.interval);
this.subscriber = null;
}
}
}
module.exports = {
RefreshingConfig: RefreshingConfig,
RefreshPolicy: {
AlwaysRefreshPolicy: AlwaysRefreshPolicy,
NeverRefreshPolicy: NeverRefreshPolicy,
StaleRefreshPolicy: StaleRefreshPolicy,
IntervalRefreshPolicy: IntervalRefreshPolicy
},
InMemoryConfigStore: InMemoryConfigStore,
InMemoryPubSubRefreshPolicyAndChangePublisher: InMemoryPubSubRefreshPolicyAndChangePublisher
};