mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
feat: make frontend api complexity O(n) instead of O(n2) (#6477)
This commit is contained in:
parent
6f2bd546a6
commit
2e6d91846b
@ -14,7 +14,12 @@ type NonEmptyList<T> = [T, ...T[]];
|
|||||||
export const mapFeaturesForClient = (
|
export const mapFeaturesForClient = (
|
||||||
features: FeatureConfigurationClient[],
|
features: FeatureConfigurationClient[],
|
||||||
): FeatureInterface[] =>
|
): FeatureInterface[] =>
|
||||||
features.map((feature) => ({
|
features.map((feature) => mapFeatureForClient(feature));
|
||||||
|
|
||||||
|
export const mapFeatureForClient = (
|
||||||
|
feature: FeatureConfigurationClient,
|
||||||
|
): FeatureInterface => {
|
||||||
|
return {
|
||||||
impressionData: false,
|
impressionData: false,
|
||||||
...feature,
|
...feature,
|
||||||
variants: (feature.variants || []).map((variant) => ({
|
variants: (feature.variants || []).map((variant) => ({
|
||||||
@ -47,7 +52,8 @@ export const mapFeaturesForClient = (
|
|||||||
})) || [],
|
})) || [],
|
||||||
})),
|
})),
|
||||||
dependencies: feature.dependencies,
|
dependencies: feature.dependencies,
|
||||||
}));
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const mapSegmentsForClient = (segments: ISegment[]): Segment[] =>
|
export const mapSegmentsForClient = (segments: ISegment[]): Segment[] =>
|
||||||
serializeDates(segments) as Segment[];
|
serializeDates(segments) as Segment[];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { IFeatureToggleClient } from '../types';
|
import { IFeatureToggleClient } from '../types';
|
||||||
|
|
||||||
export interface IClientFeatureToggleReadModel {
|
export interface IClientFeatureToggleReadModel {
|
||||||
getClient(): Promise<Record<string, IFeatureToggleClient[]>>;
|
getClient(): Promise<Record<string, Record<string, IFeatureToggleClient>>>;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import {
|
import { IFeatureToggleClient, IStrategyConfig, PartialDeep } from '../types';
|
||||||
IFeatureToggleClient,
|
|
||||||
IFeatureToggleQuery,
|
|
||||||
IStrategyConfig,
|
|
||||||
ITag,
|
|
||||||
PartialDeep,
|
|
||||||
} from '../types';
|
|
||||||
import { ensureStringValue, mapValues } from '../util';
|
import { ensureStringValue, mapValues } from '../util';
|
||||||
import { Db } from '../db/db';
|
import { Db } from '../db/db';
|
||||||
import FeatureToggleStore from '../features/feature-toggle/feature-toggle-store';
|
import FeatureToggleStore from '../features/feature-toggle/feature-toggle-store';
|
||||||
@ -15,12 +9,6 @@ import { DB_TIME } from '../metric-events';
|
|||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-model-type';
|
import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-model-type';
|
||||||
|
|
||||||
export interface IGetAllFeatures {
|
|
||||||
featureQuery?: IFeatureToggleQuery;
|
|
||||||
archived: boolean;
|
|
||||||
userId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ClientFeatureToggleReadModel
|
export default class ClientFeatureToggleReadModel
|
||||||
implements IClientFeatureToggleReadModel
|
implements IClientFeatureToggleReadModel
|
||||||
{
|
{
|
||||||
@ -37,7 +25,9 @@ export default class ClientFeatureToggleReadModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAll(): Promise<Record<string, IFeatureToggleClient[]>> {
|
private async getAll(): Promise<
|
||||||
|
Record<string, Record<string, IFeatureToggleClient>>
|
||||||
|
> {
|
||||||
const stopTimer = this.timer(`getAll`);
|
const stopTimer = this.timer(`getAll`);
|
||||||
const selectColumns = [
|
const selectColumns = [
|
||||||
'features.name as name',
|
'features.name as name',
|
||||||
@ -52,7 +42,6 @@ export default class ClientFeatureToggleReadModel
|
|||||||
'fe.environment as environment',
|
'fe.environment as environment',
|
||||||
'fs.id as strategy_id',
|
'fs.id as strategy_id',
|
||||||
'fs.strategy_name as strategy_name',
|
'fs.strategy_name as strategy_name',
|
||||||
'fs.title as strategy_title',
|
|
||||||
'fs.disabled as strategy_disabled',
|
'fs.disabled as strategy_disabled',
|
||||||
'fs.parameters as parameters',
|
'fs.parameters as parameters',
|
||||||
'fs.constraints as constraints',
|
'fs.constraints as constraints',
|
||||||
@ -102,23 +91,25 @@ export default class ClientFeatureToggleReadModel
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAggregatedData(rows): Record<string, IFeatureToggleClient[]> {
|
getAggregatedData(
|
||||||
const featureTogglesByEnv: Record<string, IFeatureToggleClient[]> = {};
|
rows,
|
||||||
|
): Record<string, Record<string, IFeatureToggleClient>> {
|
||||||
|
const featureTogglesByEnv: Record<
|
||||||
|
string,
|
||||||
|
Record<string, IFeatureToggleClient>
|
||||||
|
> = {};
|
||||||
|
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
const environment = row.environment;
|
const environment = row.environment;
|
||||||
|
const featureName = row.name;
|
||||||
|
|
||||||
if (!featureTogglesByEnv[environment]) {
|
if (!featureTogglesByEnv[environment]) {
|
||||||
featureTogglesByEnv[environment] = [];
|
featureTogglesByEnv[environment] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
let feature = featureTogglesByEnv[environment].find(
|
if (!featureTogglesByEnv[environment][featureName]) {
|
||||||
(f) => f.name === row.name,
|
featureTogglesByEnv[environment][featureName] = {
|
||||||
);
|
name: featureName,
|
||||||
|
|
||||||
if (!feature) {
|
|
||||||
feature = {
|
|
||||||
name: row.name,
|
|
||||||
strategies: [],
|
strategies: [],
|
||||||
variants: row.variants || [],
|
variants: row.variants || [],
|
||||||
impressionData: row.impression_data,
|
impressionData: row.impression_data,
|
||||||
@ -127,13 +118,12 @@ export default class ClientFeatureToggleReadModel
|
|||||||
project: row.project,
|
project: row.project,
|
||||||
stale: row.stale,
|
stale: row.stale,
|
||||||
type: row.type,
|
type: row.type,
|
||||||
|
dependencies: [],
|
||||||
};
|
};
|
||||||
featureTogglesByEnv[environment].push(feature);
|
|
||||||
} else {
|
|
||||||
if (this.isNewTag(feature, row)) {
|
|
||||||
this.addTag(feature, row);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const feature = featureTogglesByEnv[environment][featureName];
|
||||||
|
|
||||||
if (row.parent) {
|
if (row.parent) {
|
||||||
feature.dependencies = feature.dependencies || [];
|
feature.dependencies = feature.dependencies || [];
|
||||||
feature.dependencies.push({
|
feature.dependencies.push({
|
||||||
@ -149,30 +139,20 @@ export default class ClientFeatureToggleReadModel
|
|||||||
this.isUnseenStrategyRow(feature, row) &&
|
this.isUnseenStrategyRow(feature, row) &&
|
||||||
!row.strategy_disabled
|
!row.strategy_disabled
|
||||||
) {
|
) {
|
||||||
feature.strategies?.push(this.rowToStrategy(row));
|
feature.strategies = feature.strategies || [];
|
||||||
|
feature.strategies.push(this.rowToStrategy(row));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Object.keys(featureTogglesByEnv).forEach((envKey) => {
|
Object.values(featureTogglesByEnv).forEach((envFeatures) => {
|
||||||
featureTogglesByEnv[envKey] = featureTogglesByEnv[envKey].map(
|
Object.values(envFeatures).forEach((feature) => {
|
||||||
(featureToggle) => ({
|
if (feature.strategies) {
|
||||||
...featureToggle,
|
feature.strategies = feature.strategies
|
||||||
strategies: featureToggle.strategies
|
.sort((a, b) => {
|
||||||
?.sort((strategy1, strategy2) => {
|
return (a.sortOrder || 0) - (b.sortOrder || 0);
|
||||||
if (
|
|
||||||
typeof strategy1.sortOrder === 'number' &&
|
|
||||||
typeof strategy2.sortOrder === 'number'
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
strategy1.sortOrder - strategy2.sortOrder
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
})
|
})
|
||||||
.map(({ id, title, sortOrder, ...strategy }) => ({
|
.map(({ id, sortOrder, ...strategy }) => strategy);
|
||||||
...strategy,
|
}
|
||||||
})),
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return featureTogglesByEnv;
|
return featureTogglesByEnv;
|
||||||
@ -191,13 +171,6 @@ export default class ClientFeatureToggleReadModel
|
|||||||
return strategy;
|
return strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static rowToTag(row: Record<string, any>): ITag {
|
|
||||||
return {
|
|
||||||
value: row.tag_value,
|
|
||||||
type: row.tag_type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private isUnseenStrategyRow(
|
private isUnseenStrategyRow(
|
||||||
feature: PartialDeep<IFeatureToggleClient>,
|
feature: PartialDeep<IFeatureToggleClient>,
|
||||||
row: Record<string, any>,
|
row: Record<string, any>,
|
||||||
@ -208,30 +181,9 @@ export default class ClientFeatureToggleReadModel
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addTag(
|
async getClient(): Promise<
|
||||||
feature: Record<string, any>,
|
Record<string, Record<string, IFeatureToggleClient>>
|
||||||
row: Record<string, any>,
|
> {
|
||||||
): void {
|
|
||||||
const tags = feature.tags || [];
|
|
||||||
const newTag = ClientFeatureToggleReadModel.rowToTag(row);
|
|
||||||
feature.tags = [...tags, newTag];
|
|
||||||
}
|
|
||||||
|
|
||||||
private isNewTag(
|
|
||||||
feature: PartialDeep<IFeatureToggleClient>,
|
|
||||||
row: Record<string, any>,
|
|
||||||
): boolean {
|
|
||||||
return (
|
|
||||||
row.tag_type &&
|
|
||||||
row.tag_value &&
|
|
||||||
!feature.tags?.some(
|
|
||||||
(tag) =>
|
|
||||||
tag?.type === row.tag_type && tag?.value === row.tag_value,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getClient(): Promise<Record<string, IFeatureToggleClient[]>> {
|
|
||||||
return this.getAll();
|
return this.getAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,18 @@ import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-mode
|
|||||||
export default class FakeClientFeatureToggleReadModel
|
export default class FakeClientFeatureToggleReadModel
|
||||||
implements IClientFeatureToggleReadModel
|
implements IClientFeatureToggleReadModel
|
||||||
{
|
{
|
||||||
constructor(private value: Record<string, IFeatureToggleClient[]> = {}) {}
|
constructor(
|
||||||
|
private value: Record<
|
||||||
|
string,
|
||||||
|
Record<string, IFeatureToggleClient>
|
||||||
|
> = {},
|
||||||
|
) {}
|
||||||
|
|
||||||
getClient(): Promise<Record<string, IFeatureToggleClient[]>> {
|
getClient(): Promise<Record<string, Record<string, IFeatureToggleClient>>> {
|
||||||
return Promise.resolve(this.value);
|
return Promise.resolve(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue(value: Record<string, IFeatureToggleClient[]>) {
|
setValue(value: Record<string, Record<string, IFeatureToggleClient>>) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,9 +50,7 @@ export class FrontendApiRepository
|
|||||||
|
|
||||||
getToggle(name: string): FeatureInterface {
|
getToggle(name: string): FeatureInterface {
|
||||||
//@ts-ignore (we must update the node SDK to allow undefined)
|
//@ts-ignore (we must update the node SDK to allow undefined)
|
||||||
return this.getToggles(this.token).find(
|
return this.globalFrontendApiCache.getToggle(name, this.token);
|
||||||
(feature) => feature.name === name,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getToggles(): FeatureInterface[] {
|
getToggles(): FeatureInterface[] {
|
||||||
|
@ -46,7 +46,7 @@ const alwaysOnFlagResolver = {
|
|||||||
|
|
||||||
const createCache = (
|
const createCache = (
|
||||||
segment: ISegment = defaultSegment,
|
segment: ISegment = defaultSegment,
|
||||||
features: Record<string, IFeatureToggleClient[]> = {},
|
features: Record<string, Record<string, IFeatureToggleClient>> = {},
|
||||||
) => {
|
) => {
|
||||||
const config = { getLogger: noLogger, flagResolver: alwaysOnFlagResolver };
|
const config = { getLogger: noLogger, flagResolver: alwaysOnFlagResolver };
|
||||||
const segmentReadModel = new FakeSegmentReadModel([segment as ISegment]);
|
const segmentReadModel = new FakeSegmentReadModel([segment as ISegment]);
|
||||||
@ -82,28 +82,28 @@ test('Can read initial segment', async () => {
|
|||||||
|
|
||||||
test('Can read initial features', async () => {
|
test('Can read initial features', async () => {
|
||||||
const { cache } = createCache(defaultSegment, {
|
const { cache } = createCache(defaultSegment, {
|
||||||
development: [
|
development: {
|
||||||
{
|
featureA: {
|
||||||
...defaultFeature,
|
...defaultFeature,
|
||||||
name: 'featureA',
|
name: 'featureA',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
project: 'projectA',
|
project: 'projectA',
|
||||||
},
|
},
|
||||||
{
|
featureB: {
|
||||||
...defaultFeature,
|
...defaultFeature,
|
||||||
name: 'featureB',
|
name: 'featureB',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
project: 'projectB',
|
project: 'projectB',
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
production: [
|
production: {
|
||||||
{
|
featureA: {
|
||||||
...defaultFeature,
|
...defaultFeature,
|
||||||
name: 'featureA',
|
name: 'featureA',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
project: 'projectA',
|
project: 'projectA',
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const featuresBeforeRead = cache.getToggles({
|
const featuresBeforeRead = cache.getToggles({
|
||||||
@ -138,6 +138,18 @@ test('Can read initial features', async () => {
|
|||||||
projects: ['*'],
|
projects: ['*'],
|
||||||
} as IApiUser);
|
} as IApiUser);
|
||||||
expect(defaultProjectFeatures.length).toBe(0);
|
expect(defaultProjectFeatures.length).toBe(0);
|
||||||
|
|
||||||
|
const singleToggle = cache.getToggle('featureA', {
|
||||||
|
environment: 'development',
|
||||||
|
projects: ['*'],
|
||||||
|
} as IApiUser);
|
||||||
|
|
||||||
|
expect(singleToggle).toMatchObject({
|
||||||
|
...defaultFeature,
|
||||||
|
name: 'featureA',
|
||||||
|
enabled: true,
|
||||||
|
impressionData: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can refresh data on revision update', async () => {
|
test('Can refresh data on revision update', async () => {
|
||||||
@ -150,15 +162,15 @@ test('Can refresh data on revision update', async () => {
|
|||||||
await state(cache, 'ready');
|
await state(cache, 'ready');
|
||||||
|
|
||||||
clientFeatureToggleReadModel.setValue({
|
clientFeatureToggleReadModel.setValue({
|
||||||
development: [
|
development: {
|
||||||
{
|
featureA: {
|
||||||
...defaultFeature,
|
...defaultFeature,
|
||||||
name: 'featureA',
|
name: 'featureA',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
project: 'projectA',
|
project: 'projectA',
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
});
|
});
|
||||||
configurationRevisionService.emit(UPDATE_REVISION);
|
configurationRevisionService.emit(UPDATE_REVISION);
|
||||||
|
|
||||||
|
@ -2,19 +2,24 @@ import EventEmitter from 'events';
|
|||||||
import { Segment } from 'unleash-client/lib/strategy/strategy';
|
import { Segment } from 'unleash-client/lib/strategy/strategy';
|
||||||
import { FeatureInterface } from 'unleash-client/lib/feature';
|
import { FeatureInterface } from 'unleash-client/lib/feature';
|
||||||
import { IApiUser } from '../types/api-user';
|
import { IApiUser } from '../types/api-user';
|
||||||
import { ISegmentReadModel, IUnleashConfig } from '../types';
|
|
||||||
import {
|
import {
|
||||||
mapFeaturesForClient,
|
IFeatureToggleClient,
|
||||||
|
ISegmentReadModel,
|
||||||
|
IUnleashConfig,
|
||||||
|
} from '../types';
|
||||||
|
import {
|
||||||
|
mapFeatureForClient,
|
||||||
mapSegmentsForClient,
|
mapSegmentsForClient,
|
||||||
} from '../features/playground/offline-unleash-client';
|
} from '../features/playground/offline-unleash-client';
|
||||||
import { ALL_ENVS } from '../util/constants';
|
import { ALL_ENVS } from '../util/constants';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { UPDATE_REVISION } from '../features/feature-toggle/configuration-revision-service';
|
import { UPDATE_REVISION } from '../features/feature-toggle/configuration-revision-service';
|
||||||
import { mapValues } from '../util';
|
|
||||||
import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-model-type';
|
import { IClientFeatureToggleReadModel } from './client-feature-toggle-read-model-type';
|
||||||
|
|
||||||
type Config = Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>;
|
type Config = Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>;
|
||||||
|
|
||||||
|
type FrontendApiFeatureCache = Record<string, Record<string, FeatureInterface>>;
|
||||||
|
|
||||||
export type GlobalFrontendApiCacheState = 'starting' | 'ready' | 'updated';
|
export type GlobalFrontendApiCacheState = 'starting' | 'ready' | 'updated';
|
||||||
|
|
||||||
export class GlobalFrontendApiCache extends EventEmitter {
|
export class GlobalFrontendApiCache extends EventEmitter {
|
||||||
@ -28,7 +33,7 @@ export class GlobalFrontendApiCache extends EventEmitter {
|
|||||||
|
|
||||||
private readonly configurationRevisionService: EventEmitter;
|
private readonly configurationRevisionService: EventEmitter;
|
||||||
|
|
||||||
private featuresByEnvironment: Record<string, FeatureInterface[]> = {};
|
private featuresByEnvironment: FrontendApiFeatureCache = {};
|
||||||
|
|
||||||
private segments: Segment[] = [];
|
private segments: Segment[] = [];
|
||||||
|
|
||||||
@ -58,30 +63,40 @@ export class GlobalFrontendApiCache extends EventEmitter {
|
|||||||
return this.segments.find((segment) => segment.id === id);
|
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[] {
|
getToggles(token: IApiUser): FeatureInterface[] {
|
||||||
if (
|
const features = this.getTogglesByEnvironment(
|
||||||
this.featuresByEnvironment[this.environmentNameForToken(token)] ==
|
this.environmentNameForToken(token),
|
||||||
null
|
);
|
||||||
)
|
return this.filterTogglesByProjects(features, token.projects);
|
||||||
return [];
|
}
|
||||||
return this.featuresByEnvironment[
|
|
||||||
this.environmentNameForToken(token)
|
private filterTogglesByProjects(
|
||||||
].filter(
|
features: Record<string, FeatureInterface>,
|
||||||
(feature) =>
|
projects: string[],
|
||||||
token.projects.includes('*') ||
|
): FeatureInterface[] {
|
||||||
(feature.project && token.projects.includes(feature.project)),
|
if (projects.includes('*')) {
|
||||||
|
return Object.values(features);
|
||||||
|
}
|
||||||
|
return Object.values(features).filter(
|
||||||
|
(feature) => feature.project && projects.includes(feature.project),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAllFeatures(): Promise<
|
private getTogglesByEnvironment(
|
||||||
Record<string, FeatureInterface[]>
|
environment: string,
|
||||||
> {
|
): Record<string, FeatureInterface> {
|
||||||
const features = await this.clientFeatureToggleReadModel.getClient();
|
const features = this.featuresByEnvironment[environment];
|
||||||
return mapValues(features, mapFeaturesForClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getAllSegments(): Promise<Segment[]> {
|
if (features == null) return {};
|
||||||
return mapSegmentsForClient(await this.segmentReadModel.getAll());
|
|
||||||
|
return features;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fetch only relevant projects/environments based on tokens
|
// TODO: fetch only relevant projects/environments based on tokens
|
||||||
@ -102,6 +117,15 @@ export class GlobalFrontendApiCache extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getAllFeatures(): Promise<FrontendApiFeatureCache> {
|
||||||
|
const features = await this.clientFeatureToggleReadModel.getClient();
|
||||||
|
return this.mapFeatures(features);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAllSegments(): Promise<Segment[]> {
|
||||||
|
return mapSegmentsForClient(await this.segmentReadModel.getAll());
|
||||||
|
}
|
||||||
|
|
||||||
private async onUpdateRevisionEvent() {
|
private async onUpdateRevisionEvent() {
|
||||||
if (this.config.flagResolver.isEnabled('globalFrontendApiCache')) {
|
if (this.config.flagResolver.isEnabled('globalFrontendApiCache')) {
|
||||||
await this.refreshData();
|
await this.refreshData();
|
||||||
@ -114,4 +138,20 @@ export class GlobalFrontendApiCache extends EventEmitter {
|
|||||||
}
|
}
|
||||||
return token.environment;
|
return token.environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private mapFeatures(
|
||||||
|
features: Record<string, Record<string, IFeatureToggleClient>>,
|
||||||
|
): 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,11 @@ test('proxy service fetching features from global cache', async () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
getToggle(name: string, token: IApiUser): FeatureInterface {
|
||||||
|
return this.getToggles(token).find(
|
||||||
|
(t) => t.name === name,
|
||||||
|
) as FeatureInterface;
|
||||||
|
},
|
||||||
} as GlobalFrontendApiCache;
|
} as GlobalFrontendApiCache;
|
||||||
const proxyService = new ProxyService(
|
const proxyService = new ProxyService(
|
||||||
{ getLogger: noLogger } as unknown as Config,
|
{ getLogger: noLogger } as unknown as Config,
|
||||||
|
Loading…
Reference in New Issue
Block a user