unleash-client
Version:
Unleash Client for Node
230 lines • 8.57 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SUPPORTED_SPEC_VERSION = void 0;
const events_1 = require("events");
const events_2 = require("../events");
const adaptive_fetcher_1 = require("./adaptive-fetcher");
exports.SUPPORTED_SPEC_VERSION = '5.2.0';
class Repository extends events_1.EventEmitter {
// Etag property for backward compatibility
get etag() {
var _a, _b;
return ((_b = (_a = this.fetcher).getEtag) === null || _b === void 0 ? void 0 : _b.call(_a)) || undefined;
}
set etag(value) {
var _a, _b;
(_b = (_a = this.fetcher).setEtag) === null || _b === void 0 ? void 0 : _b.call(_a, value);
}
constructor({ url, appName, instanceId, connectionId, projectName, refreshInterval = 15000, timeout, headers, customHeadersFunction, httpOptions, namePrefix, tags, bootstrapProvider, bootstrapOverride = true, storageProvider, eventSource, mode, }) {
super();
this.ready = false;
this.connected = false;
this.stopped = false;
this.data = {};
this.enhanceStrategies = (strategies) => {
return strategies === null || strategies === void 0 ? void 0 : strategies.map((strategy) => {
const { segments, ...restOfStrategy } = strategy;
const enhancedSegments = segments === null || segments === void 0 ? void 0 : segments.map((segment) => this.getSegment(segment));
return { ...restOfStrategy, segments: enhancedSegments };
});
};
this.appName = appName;
this.url = url;
this.projectName = projectName;
this.bootstrapProvider = bootstrapProvider;
this.bootstrapOverride = bootstrapOverride;
this.storageProvider = storageProvider;
this.segments = new Map();
this.fetcher = new adaptive_fetcher_1.AdaptiveFetcher({
url,
appName,
instanceId,
connectionId,
refreshInterval,
timeout,
headers,
customHeadersFunction,
httpOptions,
namePrefix,
tags,
projectName,
mode,
eventSource,
onSave: this.save.bind(this),
onSaveDelta: this.saveDelta.bind(this),
});
this.setupFetchingStrategyEvents();
}
setupFetchingStrategyEvents() {
this.fetcher.on(events_2.UnleashEvents.Error, (err) => this.emit(events_2.UnleashEvents.Error, err));
this.fetcher.on(events_2.UnleashEvents.Warn, (msg) => this.emit(events_2.UnleashEvents.Warn, msg));
this.fetcher.on(events_2.UnleashEvents.Unchanged, () => this.emit(events_2.UnleashEvents.Unchanged));
this.fetcher.on(events_2.UnleashEvents.Mode, (data) => this.emit(events_2.UnleashEvents.Mode, data));
}
validateFeature(feature) {
const errors = [];
if (!Array.isArray(feature.strategies)) {
errors.push(`feature.strategies should be an array, but was ${typeof feature.strategies}`);
}
if (feature.variants && !Array.isArray(feature.variants)) {
errors.push(`feature.variants should be an array, but was ${typeof feature.variants}`);
}
if (typeof feature.enabled !== 'boolean') {
errors.push(`feature.enabled should be an boolean, but was ${typeof feature.enabled}`);
}
if (errors.length > 0) {
const err = new Error(errors.join(', '));
this.emit(events_2.UnleashEvents.Error, err);
}
}
async start() {
await Promise.all([this.fetcher.start(), this.loadBackup(), this.loadBootstrap()]);
}
async loadBackup() {
try {
const content = await this.storageProvider.get(this.appName);
if (this.ready) {
return;
}
if (content && this.notEmpty(content)) {
this.data = this.convertToMap(content.features);
this.segments = this.createSegmentLookup(content.segments);
this.setReady();
}
}
catch (err) {
this.emit(events_2.UnleashEvents.Warn, err);
}
}
setReady() {
const doEmitReady = this.ready === false;
this.ready = true;
if (doEmitReady) {
process.nextTick(() => {
this.emit(events_2.UnleashEvents.Ready);
});
}
}
createSegmentLookup(segments) {
if (!segments) {
return new Map();
}
return new Map(segments.map((segment) => [segment.id, segment]));
}
async save(response, fromApi) {
if (this.stopped) {
return;
}
if (fromApi) {
this.connected = true;
this.data = this.convertToMap(response.features);
this.segments = this.createSegmentLookup(response.segments);
}
else if (!this.connected) {
// Only allow bootstrap if not connected
this.data = this.convertToMap(response.features);
this.segments = this.createSegmentLookup(response.segments);
}
this.setReady();
this.emit(events_2.UnleashEvents.Changed, [...response.features]);
await this.storageProvider.set(this.appName, response);
}
async saveDelta(delta) {
if (this.stopped) {
return;
}
this.connected = true;
delta.events.forEach((event) => {
if (event.type === 'feature-updated') {
this.data[event.feature.name] = event.feature;
}
else if (event.type === 'feature-removed') {
delete this.data[event.featureName];
}
else if (event.type === 'segment-updated') {
this.segments.set(event.segment.id, event.segment);
}
else if (event.type === 'segment-removed') {
this.segments.delete(event.segmentId);
}
else if (event.type === 'hydration') {
this.data = this.convertToMap(event.features);
this.segments = this.createSegmentLookup(event.segments);
}
});
this.setReady();
this.emit(events_2.UnleashEvents.Changed, Object.values(this.data));
await this.storageProvider.set(this.appName, {
features: Object.values(this.data),
segments: [...this.segments.values()],
version: 0,
});
}
notEmpty(content) {
return content.features.length > 0;
}
async loadBootstrap() {
try {
const content = await this.bootstrapProvider.readBootstrap();
if (!this.bootstrapOverride && this.ready) {
// early exit if we already have backup data and should not override it.
return;
}
if (content && this.notEmpty(content)) {
await this.save(content, false);
}
}
catch (err) {
this.emit(events_2.UnleashEvents.Warn, `Unleash SDK was unable to load bootstrap.
Message: ${err.message}`);
}
}
convertToMap(features) {
const obj = features.reduce((o, feature) => {
const a = { ...o };
this.validateFeature(feature);
a[feature.name] = feature;
return a;
}, {});
return obj;
}
stop() {
this.stopped = true;
this.fetcher.stop();
this.removeAllListeners();
}
getSegment(segmentId) {
return this.segments.get(segmentId);
}
getToggle(name) {
return this.data[name];
}
getToggles() {
return Object.keys(this.data).map((key) => this.data[key]);
}
getTogglesWithSegmentData() {
const toggles = this.getToggles();
return toggles.map((toggle) => {
const { strategies, ...restOfToggle } = toggle;
return { ...restOfToggle, strategies: this.enhanceStrategies(strategies) };
});
}
getMode() {
return this.fetcher.getMode();
}
async setMode(mode) {
await this.fetcher.setMode(mode);
}
// Compatibility methods for tests - delegate to fetching strategy
getFailures() {
return this.fetcher.getFailures();
}
nextFetch() {
return this.fetcher.nextFetch();
}
async fetch() {
return this.fetcher.fetch();
}
}
exports.default = Repository;
//# sourceMappingURL=index.js.map