unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
194 lines • 8.36 kB
JavaScript
import { CLIENT_METRICS_ADDED, FEATURE_ARCHIVED, FEATURE_CREATED, FEATURE_REVIVED, } from '../../events/index.js';
import { FeatureCompletedEvent, FeatureUncompletedEvent, } from '../../types/index.js';
import groupBy from 'lodash.groupby';
import { STAGE_ENTERED } from '../../metric-events.js';
export class FeatureLifecycleService {
constructor({ eventStore, featureLifecycleStore, environmentStore, featureEnvironmentStore, }, { eventService, }, { flagResolver, eventBus, getLogger, }) {
this.eventStore = eventStore;
this.featureLifecycleStore = featureLifecycleStore;
this.environmentStore = environmentStore;
this.featureEnvironmentStore = featureEnvironmentStore;
this.flagResolver = flagResolver;
this.eventBus = eventBus;
this.eventService = eventService;
this.logger = getLogger('feature-lifecycle/feature-lifecycle-service.ts');
}
listen() {
this.eventStore.on(FEATURE_CREATED, async (event) => {
await this.featureInitialized(event.featureName);
});
this.eventBus.on(CLIENT_METRICS_ADDED, async (events) => {
if (events.length > 0) {
if (this.flagResolver.isEnabled('optimizeLifecycle')) {
await this.handleBulkMetrics(events);
}
else {
const groupedByEnvironment = groupBy(events, 'environment');
for (const [environment, metrics] of Object.entries(groupedByEnvironment)) {
const features = metrics.map((metric) => metric.featureName);
await this.featuresReceivedMetrics(features, environment);
}
}
}
});
this.eventStore.on(FEATURE_ARCHIVED, async (event) => {
await this.featureArchived(event.featureName);
});
this.eventStore.on(FEATURE_REVIVED, async (event) => {
await this.featureRevived(event.featureName);
});
}
async getFeatureLifecycle(feature) {
return this.featureLifecycleStore.get(feature);
}
async featureInitialized(feature) {
const result = await this.featureLifecycleStore.insert([
{ feature, stage: 'initial' },
]);
this.recordStagesEntered(result);
}
async stageReceivedMetrics(features, stage) {
const newlyEnteredStages = await this.featureLifecycleStore.insert(features.map((feature) => ({ feature, stage })));
this.recordStagesEntered(newlyEnteredStages);
}
recordStagesEntered(newlyEnteredStages) {
newlyEnteredStages.forEach(({ stage, feature }) => {
this.eventBus.emit(STAGE_ENTERED, { stage, feature });
});
}
async featuresReceivedMetrics(features, environment) {
try {
const env = await this.environmentStore.get(environment);
if (!env) {
return;
}
await this.stageReceivedMetrics(features, 'pre-live');
if (env.type === 'production') {
const featureEnv = await this.featureEnvironmentStore.getAllByFeatures(features, env.name);
const enabledFeatures = featureEnv
.filter((feature) => feature.enabled)
.map((feature) => feature.featureName);
await this.stageReceivedMetrics(enabledFeatures, 'live');
}
}
catch (e) {
this.logger.warn(`Error handling ${features.length} metrics in ${environment}`, e);
}
}
/**
* Optimized bulk processing: reduces DB calls from O(4 * environments) to O(3) by batching all data fetches and processing in-memory
*/
async handleBulkMetrics(events) {
try {
const { environments, allFeatures } = this.extractUniqueEnvironmentsAndFeatures(events);
const envMap = await this.buildEnvironmentMap();
const featureEnvMap = await this.buildFeatureEnvironmentMap(allFeatures);
const allStagesToInsert = this.determineLifecycleStages(events, environments, envMap, featureEnvMap);
if (allStagesToInsert.length > 0) {
const newlyEnteredStages = await this.featureLifecycleStore.insert(allStagesToInsert);
this.recordStagesEntered(newlyEnteredStages);
}
}
catch (e) {
this.logger.warn(`Error handling bulk metrics for ${events.length} events`, e);
}
}
extractUniqueEnvironmentsAndFeatures(events) {
const environments = [...new Set(events.map((e) => e.environment))];
const allFeatures = [...new Set(events.map((e) => e.featureName))];
return { environments, allFeatures };
}
async buildEnvironmentMap() {
const allEnvs = await this.environmentStore.getAll();
return new Map(allEnvs.map((env) => [env.name, env]));
}
async buildFeatureEnvironmentMap(allFeatures) {
const allFeatureEnvs = await this.featureEnvironmentStore.getAllByFeatures(allFeatures);
const featureEnvMap = new Map();
allFeatureEnvs.forEach((fe) => {
if (!featureEnvMap.has(fe.environment)) {
featureEnvMap.set(fe.environment, new Map());
}
const envMap = featureEnvMap.get(fe.environment);
if (envMap) {
envMap.set(fe.featureName, fe);
}
});
return featureEnvMap;
}
determineLifecycleStages(events, environments, envMap, featureEnvMap) {
const allStagesToInsert = [];
for (const environment of environments) {
const env = envMap.get(environment);
if (!env)
continue;
const envFeatures = this.getFeaturesForEnvironment(events, environment);
allStagesToInsert.push(...this.createPreLiveStages(envFeatures));
if (env.type === 'production') {
const enabledFeatures = this.getEnabledFeaturesForEnvironment(envFeatures, environment, featureEnvMap);
allStagesToInsert.push(...this.createLiveStages(enabledFeatures));
}
}
return allStagesToInsert;
}
getFeaturesForEnvironment(events, environment) {
return events
.filter((e) => e.environment === environment)
.map((e) => e.featureName);
}
createPreLiveStages(features) {
return features.map((feature) => ({
feature,
stage: 'pre-live',
}));
}
createLiveStages(features) {
return features.map((feature) => ({ feature, stage: 'live' }));
}
getEnabledFeaturesForEnvironment(features, environment, featureEnvMap) {
const envFeatureEnvs = featureEnvMap.get(environment) ?? new Map();
return features.filter((feature) => {
const fe = envFeatureEnvs.get(feature);
return fe?.enabled;
});
}
async featureCompleted(feature, projectId, status, auditUser) {
const result = await this.featureLifecycleStore.insert([
{
feature,
stage: 'completed',
status: status.status,
statusValue: status.statusValue,
},
]);
this.recordStagesEntered(result);
await this.eventService.storeEvent(new FeatureCompletedEvent({
project: projectId,
featureName: feature,
data: { ...status, kept: status.status === 'kept' },
auditUser,
}));
}
async featureUncompleted(feature, projectId, auditUser) {
await this.featureLifecycleStore.deleteStage({
feature,
stage: 'completed',
});
await this.eventService.storeEvent(new FeatureUncompletedEvent({
project: projectId,
featureName: feature,
auditUser,
}));
}
async featureArchived(feature) {
const result = await this.featureLifecycleStore.insert([
{ feature, stage: 'archived' },
]);
this.recordStagesEntered(result);
}
async featureRevived(feature) {
await this.featureLifecycleStore.delete(feature);
await this.featureInitialized(feature);
}
}
//# sourceMappingURL=feature-lifecycle-service.js.map