UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

194 lines 8.36 kB
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