unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
219 lines • 9.54 kB
JavaScript
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