unleash-client
Version:
Unleash Client for Node
270 lines • 12 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Unleash = exports.UnleashEvents = exports.Strategy = void 0;
const os_1 = require("os");
const events_1 = require("events");
const client_1 = require("./client");
const repository_1 = require("./repository");
const metrics_1 = require("./metrics");
const strategy_1 = require("./strategy");
Object.defineProperty(exports, "Strategy", { enumerable: true, get: function () { return strategy_1.Strategy; } });
const variant_1 = require("./variant");
const helpers_1 = require("./helpers");
const bootstrap_provider_1 = require("./repository/bootstrap-provider");
const events_2 = require("./events");
Object.defineProperty(exports, "UnleashEvents", { enumerable: true, get: function () { return events_2.UnleashEvents; } });
const storage_provider_file_1 = require("./repository/storage-provider-file");
const uuidv4_1 = require("./uuidv4");
const metric_types_1 = require("./impact-metrics/metric-types");
const BACKUP_PATH = (0, os_1.tmpdir)();
class Unleash extends events_1.EventEmitter {
constructor({ appName, environment = 'default', projectName, instanceId, url, refreshInterval = 15 * 1000, metricsInterval = 60 * 1000, metricsJitter = 0, disableMetrics = false, backupPath = BACKUP_PATH, strategies = [], repository, namePrefix, customHeaders, customHeadersFunction, timeout, httpOptions, tags, bootstrap = {}, bootstrapOverride, storageProvider, disableAutoStart = false, skipInstanceCountWarning = false, experimentalMode = { type: 'polling', format: 'full' }, }) {
super();
this.synchronized = false;
this.ready = false;
this.started = false;
this.metricRegistry = new metric_types_1.InMemoryMetricRegistry();
Unleash.instanceCount++;
this.on(events_2.UnleashEvents.Error, (error) => {
// Only if there does not exist other listeners for this event.
if (this.listenerCount(events_2.UnleashEvents.Error) === 1) {
console.error(error);
}
});
if (!skipInstanceCountWarning && Unleash.instanceCount > 10) {
process.nextTick(() => {
const error = new Error('The unleash SDK has been initialized more than 10 times');
this.emit(events_2.UnleashEvents.Error, error);
});
}
if (!url) {
throw new Error('Unleash API "url" is required');
}
if (!appName) {
throw new Error('Unleash client "appName" is required');
}
const unleashUrl = this.cleanUnleashUrl(url);
const unleashInstanceId = (0, helpers_1.generateInstanceId)(instanceId);
const unleashConnectionId = (0, uuidv4_1.uuidv4)();
this.staticContext = { appName, environment };
const bootstrapProvider = (0, bootstrap_provider_1.resolveBootstrapProvider)(bootstrap, appName, unleashInstanceId);
this.repository =
repository ||
new repository_1.default({
projectName,
url: unleashUrl,
appName,
instanceId: unleashInstanceId,
connectionId: unleashConnectionId,
refreshInterval,
headers: customHeaders,
customHeadersFunction,
timeout,
httpOptions,
namePrefix,
tags,
bootstrapProvider,
bootstrapOverride,
mode: experimentalMode,
storageProvider: storageProvider || new storage_provider_file_1.default(backupPath),
});
this.repository.on(events_2.UnleashEvents.Ready, () => {
this.ready = true;
process.nextTick(() => {
this.emit(events_2.UnleashEvents.Ready);
});
});
this.repository.on(events_2.UnleashEvents.Error, (err) => {
// eslint-disable-next-line no-param-reassign
err.message = `Unleash Repository error: ${err.message}`;
this.emit(events_2.UnleashEvents.Error, err);
});
this.repository.on(events_2.UnleashEvents.Warn, (msg) => this.emit(events_2.UnleashEvents.Warn, msg));
this.repository.on(events_2.UnleashEvents.Unchanged, (msg) => this.emit(events_2.UnleashEvents.Unchanged, msg));
this.repository.on(events_2.UnleashEvents.Changed, (data) => {
this.emit(events_2.UnleashEvents.Changed, data);
// Only emit the fully synchronized event the first time.
if (!this.synchronized) {
this.synchronized = true;
process.nextTick(() => this.emit(events_2.UnleashEvents.Synchronized));
}
});
// setup client
const supportedStrategies = strategies.concat(strategy_1.defaultStrategies);
this.client = new client_1.default(this.repository, supportedStrategies);
this.client.on(events_2.UnleashEvents.Error, (err) => this.emit(events_2.UnleashEvents.Error, err));
this.client.on(events_2.UnleashEvents.Impression, (e) => this.emit(events_2.UnleashEvents.Impression, e));
this.metrics = new metrics_1.default({
disableMetrics,
appName,
instanceId: unleashInstanceId,
connectionId: unleashConnectionId,
strategies: supportedStrategies.map((strategy) => strategy.name),
metricsInterval,
metricsJitter,
url: unleashUrl,
headers: customHeaders,
customHeadersFunction,
timeout,
httpOptions,
metricRegistry: this.metricRegistry,
});
this.metrics.on(events_2.UnleashEvents.Error, (err) => {
// eslint-disable-next-line no-param-reassign
err.message = `Unleash Metrics error: ${err.message}`;
this.emit(events_2.UnleashEvents.Error, err);
});
this.metrics.on(events_2.UnleashEvents.Warn, (msg) => this.emit(events_2.UnleashEvents.Warn, msg));
this.metrics.on(events_2.UnleashEvents.Sent, (payload) => this.emit(events_2.UnleashEvents.Sent, payload));
this.metrics.on(events_2.UnleashEvents.Count, (name, enabled) => {
this.emit(events_2.UnleashEvents.Count, name, enabled);
});
this.metrics.on(events_2.UnleashEvents.Registered, (payload) => {
this.emit(events_2.UnleashEvents.Registered, payload);
});
if (!disableAutoStart) {
process.nextTick(async () => this.start());
}
}
/**
* Will only give you an instance the first time you call the method,
* and then return the same instance.
* @param config The Unleash Config.
* @returns the Unleash instance
*/
static getInstance(config) {
const cleanConfig = {
...config,
// Remove complex objects
repository: undefined,
customHeadersFunction: undefined,
storageProvider: undefined,
};
const configSignature = (0, helpers_1.generateHashOfConfig)(cleanConfig);
if (Unleash.instance) {
if (configSignature !== Unleash.configSignature) {
throw new Error('You already have an Unleash instance with a different configuration.');
}
return Unleash.instance;
}
const instance = new Unleash(config);
Unleash.instance = instance;
Unleash.configSignature = configSignature;
return instance;
}
cleanUnleashUrl(url) {
let unleashUrl = url;
if (unleashUrl.endsWith('/features')) {
const oldUrl = unleashUrl;
process.nextTick(() => this.emit(events_2.UnleashEvents.Warn, `Unleash server URL "${oldUrl}" should no longer link directly to /features`));
unleashUrl = unleashUrl.replace(/\/features$/, '');
}
if (!unleashUrl.endsWith('/')) {
unleashUrl += '/';
}
return unleashUrl;
}
isSynchronized() {
return this.synchronized;
}
async start() {
if (this.started)
return;
this.started = true;
await Promise.all([this.repository.start(), this.metrics.start()]);
}
destroy() {
this.repository.stop();
this.metrics.stop();
Unleash.instance = undefined;
Unleash.configSignature = undefined;
Unleash.instanceCount--;
}
isEnabled(name, context = {}, fallback) {
const enhancedContext = { ...this.staticContext, ...context };
const fallbackFunc = (0, helpers_1.createFallbackFunction)(name, enhancedContext, fallback);
let result;
if (this.ready) {
result = this.client.isEnabled(name, enhancedContext, fallbackFunc);
}
else {
result = fallbackFunc();
this.emit(events_2.UnleashEvents.Warn, `Unleash has not been initialized yet. isEnabled(${name}) defaulted to ${result}`);
}
this.count(name, result);
return result;
}
getVariant(name, context = {}, fallbackVariant) {
const enhancedContext = { ...this.staticContext, ...context };
let variant;
if (this.ready) {
variant = this.client.getVariant(name, enhancedContext, fallbackVariant);
}
else {
variant =
typeof fallbackVariant !== 'undefined'
? { ...fallbackVariant, feature_enabled: false, featureEnabled: false }
: { ...variant_1.defaultVariant, featureEnabled: variant_1.defaultVariant.feature_enabled };
this.emit(events_2.UnleashEvents.Warn, `Unleash has not been initialized yet. isEnabled(${name}) defaulted to ${variant}`);
}
if (variant.name) {
this.countVariant(name, variant.name);
}
this.count(name, Boolean(variant.feature_enabled));
return variant;
}
forceGetVariant(name, context = {}, fallbackVariant) {
const enhancedContext = { ...this.staticContext, ...context };
let variant;
if (this.ready) {
variant = this.client.forceGetVariant(name, enhancedContext, fallbackVariant);
}
else {
variant =
typeof fallbackVariant !== 'undefined'
? { ...fallbackVariant, feature_enabled: false }
: variant_1.defaultVariant;
this.emit(events_2.UnleashEvents.Warn, `Unleash has not been initialized yet. isEnabled(${name}) defaulted to ${variant}`);
}
if (variant.name) {
this.countVariant(name, variant.name);
}
this.count(name, variant.feature_enabled || false);
return variant;
}
getFeatureToggleDefinition(toggleName) {
return this.repository.getToggle(toggleName);
}
getFeatureToggleDefinitions(withFullSegments) {
if (withFullSegments === true) {
return this.repository.getTogglesWithSegmentData();
}
return this.repository.getToggles();
}
count(toggleName, enabled) {
this.metrics.count(toggleName, enabled);
}
countVariant(toggleName, variantName) {
this.metrics.countVariant(toggleName, variantName);
}
flushMetrics() {
return this.metrics.sendMetrics();
}
async destroyWithFlush() {
await this.flushMetrics();
this.destroy();
}
/**
* Experimental: Change the fetching mode at runtime
* @param mode The new mode to switch to ('polling' or 'streaming')
*/
async setExperimentalMode(mode) {
if (this.repository.setMode) {
return this.repository.setMode(mode);
}
throw new Error('setMode is not supported by this repository implementation');
}
}
exports.Unleash = Unleash;
Unleash.instanceCount = 0;
//# sourceMappingURL=unleash.js.map