import EventEmitter from 'events'; import { Segment } from 'unleash-client/lib/strategy/strategy'; import { FeatureInterface } from 'unleash-client/lib/feature'; import { IApiUser } from '../../types/api-user'; import { IFeatureToggleClient, ISegmentReadModel, IUnleashConfig, } from '../../types'; import { mapFeatureForClient, mapSegmentsForClient, } from '../playground/offline-unleash-client'; import { ALL_ENVS } from '../../util/constants'; import { Logger } from '../../logger'; import { UPDATE_REVISION } from '../feature-toggle/configuration-revision-service'; import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-model-type'; type Config = Pick; type FrontendApiFeatureCache = Record>; export type GlobalFrontendApiCacheState = 'starting' | 'ready' | 'updated'; export class GlobalFrontendApiCache extends EventEmitter { private readonly config: Config; private readonly logger: Logger; private readonly clientFeatureToggleReadModel: IClientFeatureToggleReadModel; private readonly segmentReadModel: ISegmentReadModel; private readonly configurationRevisionService: EventEmitter; private featuresByEnvironment: FrontendApiFeatureCache = {}; private segments: Segment[] = []; private status: GlobalFrontendApiCacheState = 'starting'; constructor( config: Config, segmentReadModel: ISegmentReadModel, clientFeatureToggleReadModel: IClientFeatureToggleReadModel, configurationRevisionService: EventEmitter, ) { super(); this.config = config; this.logger = config.getLogger('global-frontend-api-cache.ts'); this.clientFeatureToggleReadModel = clientFeatureToggleReadModel; this.configurationRevisionService = configurationRevisionService; this.segmentReadModel = segmentReadModel; this.onUpdateRevisionEvent = this.onUpdateRevisionEvent.bind(this); this.refreshData(); this.configurationRevisionService.on( UPDATE_REVISION, this.onUpdateRevisionEvent, ); } getSegment(id: number): Segment | undefined { return this.segments.find((segment) => segment.id === id); } getToggle(name: string, token: IApiUser): FeatureInterface { const features = this.getTogglesByEnvironment( this.environmentNameForToken(token), ); return features[name]; } getToggles(token: IApiUser): FeatureInterface[] { const features = this.getTogglesByEnvironment( this.environmentNameForToken(token), ); return this.filterTogglesByProjects(features, token.projects); } private filterTogglesByProjects( features: Record, projects: string[], ): FeatureInterface[] { if (projects.includes('*')) { return Object.values(features); } return Object.values(features).filter( (feature) => feature.project && projects.includes(feature.project), ); } private getTogglesByEnvironment( environment: string, ): Record { const features = this.featuresByEnvironment[environment]; if (features == null) return {}; return features; } // TODO: fetch only relevant projects/environments based on tokens // TODO: also consider not fetching disabled features, because those are not returned by frontend API private async refreshData() { try { this.featuresByEnvironment = await this.getAllFeatures(); this.segments = await this.getAllSegments(); if (this.status === 'starting') { this.status = 'ready'; this.emit('ready'); } else if (this.status === 'ready' || this.status === 'updated') { this.status = 'updated'; this.emit('updated'); } } catch (e) { this.logger.error('Cannot load data for token', e); } } private async getAllFeatures(): Promise { const features = await this.clientFeatureToggleReadModel.getAll(); return this.mapFeatures(features); } private async getAllSegments(): Promise { return mapSegmentsForClient(await this.segmentReadModel.getAll()); } private async onUpdateRevisionEvent() { if (this.config.flagResolver.isEnabled('globalFrontendApiCache')) { await this.refreshData(); } } private environmentNameForToken(token: IApiUser): string { if (token.environment === ALL_ENVS) { return 'default'; } return token.environment; } private mapFeatures( features: Record>, ): FrontendApiFeatureCache { const entries = Object.entries(features).map(([key, value]) => [ key, Object.fromEntries( Object.entries(value).map(([innerKey, innerValue]) => [ innerKey, mapFeatureForClient(innerValue), ]), ), ]); return Object.fromEntries(entries); } }