UNPKG

unleash-server

Version:

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

219 lines • 9.54 kB
import { UPDATE_REVISION } from '../../feature-toggle/configuration-revision-service.js'; import { DeltaCache } from './delta-cache.js'; import { CLIENT_DELTA_MEMORY } from '../../../metric-events.js'; import EventEmitter from 'events'; import { DELTA_EVENT_TYPES, isDeltaFeatureRemovedEvent, isDeltaFeatureUpdatedEvent, isDeltaSegmentEvent, } from './client-feature-toggle-delta-types.js'; import { FEATURE_PROJECT_CHANGE } from '../../../events/index.js'; export const UPDATE_DELTA = 'UPDATE_DELTA'; export const filterEventsByQuery = (events, requiredRevisionId, projects, namePrefix) => { const targetedEvents = events.filter((revision) => revision.eventId > requiredRevisionId); const allProjects = projects.includes('*'); const startsWithPrefix = (revision) => { return ((isDeltaFeatureUpdatedEvent(revision) && revision.feature.name.startsWith(namePrefix)) || (isDeltaFeatureRemovedEvent(revision) && revision.featureName.startsWith(namePrefix))); }; const isInProject = (revision) => { return ((isDeltaFeatureUpdatedEvent(revision) && projects.includes(revision.feature.project)) || (isDeltaFeatureRemovedEvent(revision) && projects.includes(revision.project))); }; return targetedEvents.filter((revision) => { return (isDeltaSegmentEvent(revision) || (startsWithPrefix(revision) && (allProjects || isInProject(revision)))); }); }; export const filterHydrationEventByQuery = (event, projects, namePrefix) => { const allProjects = projects.includes('*'); const { type, features, eventId, segments } = event; return { eventId, type, segments, features: features.filter((feature) => { return (feature.name.startsWith(namePrefix) && (allProjects || projects.includes(feature.project))); }), }; }; export class ClientFeatureToggleDelta extends EventEmitter { constructor(clientFeatureToggleDeltaReadModel, segmentReadModel, eventStore, configurationRevisionService, flagResolver, config) { super(); this.delta = {}; this.currentRevisionId = 0; this.eventStore = eventStore; this.configurationRevisionService = configurationRevisionService; this.clientFeatureToggleDeltaReadModel = clientFeatureToggleDeltaReadModel; this.flagResolver = flagResolver; this.segmentReadModel = segmentReadModel; this.eventBus = config.eventBus; this.logger = config.getLogger('delta/client-feature-toggle-delta.js'); this.onUpdateRevisionEvent = this.onUpdateRevisionEvent.bind(this); this.delta = {}; this.configurationRevisionService.on(UPDATE_REVISION, this.onUpdateRevisionEvent); } static getInstance(clientFeatureToggleDeltaReadModel, segmentReadModel, eventStore, configurationRevisionService, flagResolver, config) { if (!ClientFeatureToggleDelta.instance) { ClientFeatureToggleDelta.instance = new ClientFeatureToggleDelta(clientFeatureToggleDeltaReadModel, segmentReadModel, eventStore, configurationRevisionService, flagResolver, config); } return ClientFeatureToggleDelta.instance; } async getDelta(sdkRevisionId, query) { const projects = query.project ? query.project : ['*']; const environment = query.environment ? query.environment : 'default'; const namePrefix = query.namePrefix ? query.namePrefix : ''; const requiredRevisionId = sdkRevisionId || 0; const hasDelta = this.delta[environment] !== undefined; if (!hasDelta) { await this.initEnvironmentDelta(environment); } if (requiredRevisionId >= this.currentRevisionId) { return undefined; } const delta = this.delta[environment]; if (requiredRevisionId === 0 || delta.isMissingRevision(requiredRevisionId)) { const hydrationEvent = delta.getHydrationEvent(); const filteredEvent = filterHydrationEventByQuery(hydrationEvent, projects, namePrefix); const response = { events: [filteredEvent], }; return Promise.resolve(response); } else { const environmentEvents = delta.getEvents(); const events = filterEventsByQuery(environmentEvents, requiredRevisionId, projects, namePrefix); if (events.length === 0) { return undefined; } return { events, }; } } async onUpdateRevisionEvent() { if (this.flagResolver.isEnabled('deltaApi')) { await this.updateFeaturesDelta(); this.storeFootprint(); this.emit(UPDATE_DELTA); } } /** * This is used in client-feature-delta-api.e2e.test.ts, do not remove */ resetDelta() { this.delta = {}; } async updateFeaturesDelta() { const keys = Object.keys(this.delta); if (keys.length === 0) return; const latestRevision = await this.configurationRevisionService.getMaxRevisionId(); const changeEvents = await this.eventStore.getRevisionRange(this.currentRevisionId, latestRevision); const featuresMovedEvents = changeEvents .filter((event) => event.featureName) .filter((event) => event.type === FEATURE_PROJECT_CHANGE) .map((event) => ({ eventId: latestRevision, type: DELTA_EVENT_TYPES.FEATURE_REMOVED, featureName: event.featureName, project: event.data.oldProject, })); const featuresUpdated = [ ...new Set(changeEvents .filter((event) => event.featureName) .filter((event) => event.type !== 'feature-archived') .filter((event) => event.type !== 'feature-deleted') .map((event) => event.featureName)), ]; const featuresRemovedEvents = changeEvents .filter((event) => event.featureName && event.project) .filter((event) => event.type === 'feature-archived') .map((event) => ({ eventId: latestRevision, type: DELTA_EVENT_TYPES.FEATURE_REMOVED, featureName: event.featureName, project: event.project, })); const segmentsUpdated = changeEvents .filter((event) => ['segment-created', 'segment-updated'].includes(event.type)) .map((event) => event.data.id); const segmentsRemoved = changeEvents .filter((event) => event.type === 'segment-deleted') .map((event) => event.preData.id); const segments = await this.segmentReadModel.getAllForClientIds(segmentsUpdated); const segmentsUpdatedEvents = segments.map((segment) => ({ eventId: latestRevision, type: DELTA_EVENT_TYPES.SEGMENT_UPDATED, segment, })); const segmentsRemovedEvents = segmentsRemoved.map((segmentId) => ({ eventId: latestRevision, type: DELTA_EVENT_TYPES.SEGMENT_REMOVED, segmentId, })); // TODO: we might want to only update the environments that had events changed for performance for (const environment of keys) { const newToggles = await this.getChangedToggles(environment, featuresUpdated); const featuresUpdatedEvents = newToggles.map((toggle) => ({ eventId: latestRevision, type: DELTA_EVENT_TYPES.FEATURE_UPDATED, feature: toggle, })); this.delta[environment].addEvents([ ...featuresMovedEvents, ...featuresUpdatedEvents, ...featuresRemovedEvents, ...segmentsUpdatedEvents, ...segmentsRemovedEvents, ]); } this.currentRevisionId = latestRevision; } async getChangedToggles(environment, toggles) { if (toggles.length === 0) { return []; } return this.getClientFeatures({ toggleNames: toggles, environment, }); } async initEnvironmentDelta(environment) { const baseFeatures = await this.getClientFeatures({ environment, }); const baseSegments = await this.segmentReadModel.getAllForClientIds(); this.currentRevisionId = await this.configurationRevisionService.getMaxRevisionId(); this.delta[environment] = new DeltaCache({ eventId: this.currentRevisionId, type: DELTA_EVENT_TYPES.HYDRATION, features: baseFeatures, segments: baseSegments, }); this.storeFootprint(); } async getClientFeatures(query) { const result = await this.clientFeatureToggleDeltaReadModel.getAll(query); return result; } storeFootprint() { try { const memory = this.getCacheSizeInBytes(this.delta); this.eventBus.emit(CLIENT_DELTA_MEMORY, { memory }); } catch (e) { this.logger.error('Client delta footprint error', e); } } getCacheSizeInBytes(value) { const jsonString = JSON.stringify(value); return Buffer.byteLength(jsonString, 'utf8'); } } //# sourceMappingURL=client-feature-toggle-delta.js.map