mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: deleted feature names should come from event (#8978)
This is still raw and experimental. We started to pull deleted features from event payload. Now we put full query towards read model. Co-Author: @FredrikOseberg
This commit is contained in:
parent
b211c9c33f
commit
a257ca4474
@ -1,24 +1,26 @@
|
||||
import type {
|
||||
IEventStore,
|
||||
IFeatureToggleClient,
|
||||
IFeatureToggleCacheQuery,
|
||||
IFeatureToggleQuery,
|
||||
IFlagResolver,
|
||||
} from '../../../types';
|
||||
import type { FeatureConfigurationClient } from '../../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||
import type ConfigurationRevisionService from '../../feature-toggle/configuration-revision-service';
|
||||
import { UPDATE_REVISION } from '../../feature-toggle/configuration-revision-service';
|
||||
import { RevisionCache } from './revision-cache';
|
||||
import type { IClientFeatureToggleCacheReadModel } from './client-feature-toggle-cache-read-model-type';
|
||||
import type {
|
||||
FeatureConfigurationCacheClient,
|
||||
IClientFeatureToggleCacheReadModel,
|
||||
} from './client-feature-toggle-cache-read-model-type';
|
||||
|
||||
type DeletedFeature = {
|
||||
name: string;
|
||||
project: string;
|
||||
};
|
||||
|
||||
export type ClientFeatureChange = {
|
||||
updated: IFeatureToggleClient[];
|
||||
removed: DeletedFeature[];
|
||||
export type RevisionCacheEntry = {
|
||||
updated: FeatureConfigurationCacheClient[];
|
||||
revisionId: number;
|
||||
removed: DeletedFeature[];
|
||||
};
|
||||
|
||||
export type Revision = {
|
||||
@ -132,9 +134,11 @@ export class ClientFeatureToggleCache {
|
||||
|
||||
async getDelta(
|
||||
sdkRevisionId: number | undefined,
|
||||
environment: string,
|
||||
projects: string[],
|
||||
): Promise<ClientFeatureChange | undefined> {
|
||||
query: IFeatureToggleQuery,
|
||||
): Promise<RevisionCacheEntry | undefined> {
|
||||
const projects = query.project ? query.project : ['*'];
|
||||
const environment = query.environment ? query.environment : 'default';
|
||||
// TODO: filter by tags, what is namePrefix? anything else?
|
||||
const requiredRevisionId = sdkRevisionId || 0;
|
||||
|
||||
const hasCache = this.cache[environment] !== undefined;
|
||||
@ -153,6 +157,7 @@ export class ClientFeatureToggleCache {
|
||||
sdkRevisionId !== this.currentRevisionId &&
|
||||
!this.cache[environment].hasRevision(sdkRevisionId))
|
||||
) {
|
||||
//TODO: populate cache based on this?
|
||||
return {
|
||||
revisionId: this.currentRevisionId,
|
||||
// @ts-ignore
|
||||
@ -201,44 +206,40 @@ export class ClientFeatureToggleCache {
|
||||
.map((event) => event.featureName!),
|
||||
),
|
||||
];
|
||||
const newToggles = await this.getChangedToggles(
|
||||
changedToggles,
|
||||
latestRevision, // TODO: this should come back from the same query to not be out of sync
|
||||
);
|
||||
|
||||
// TODO: Discussion point. Should we filter events by environment and only add revisions in the cache
|
||||
// for the environment that changed? If we do that we can also save CPU and memory, because we don't need
|
||||
// to talk to the database if the cache is not initialized for that environment
|
||||
for (const key of keys) {
|
||||
this.cache[key].addRevision(newToggles);
|
||||
const removed = changeEvents
|
||||
.filter((event) => event.featureName && event.project)
|
||||
.filter((event) => event.type === 'feature-deleted')
|
||||
.map((event) => ({
|
||||
name: event.featureName!,
|
||||
project: event.project!,
|
||||
}));
|
||||
|
||||
// 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,
|
||||
changedToggles,
|
||||
);
|
||||
this.cache[environment].addRevision({
|
||||
updated: newToggles,
|
||||
revisionId: latestRevision,
|
||||
removed,
|
||||
});
|
||||
}
|
||||
|
||||
this.currentRevisionId = latestRevision;
|
||||
}
|
||||
|
||||
async getChangedToggles(
|
||||
environment: string,
|
||||
toggles: string[],
|
||||
revisionId: number,
|
||||
): Promise<ClientFeatureChange> {
|
||||
): Promise<FeatureConfigurationCacheClient[]> {
|
||||
const foundToggles = await this.getClientFeatures({
|
||||
// @ts-ignore removed toggleNames from the type, we should not use this method at all,
|
||||
toggleNames: toggles,
|
||||
environment: 'development',
|
||||
environment,
|
||||
});
|
||||
|
||||
const foundToggleNames = foundToggles.map((toggle) => toggle.name);
|
||||
const removed = toggles
|
||||
.filter((toggle) => !foundToggleNames.includes(toggle))
|
||||
.map((name) => ({
|
||||
name,
|
||||
project: 'default', // TODO: this needs to be smart and figure out the project . IMPORTANT
|
||||
}));
|
||||
|
||||
return {
|
||||
updated: foundToggles as any, // impressionData is not on the type but should be
|
||||
removed,
|
||||
revisionId,
|
||||
};
|
||||
return foundToggles;
|
||||
}
|
||||
|
||||
public async initEnvironmentCache(environment: string) {
|
||||
@ -262,8 +263,8 @@ export class ClientFeatureToggleCache {
|
||||
}
|
||||
|
||||
async getClientFeatures(
|
||||
query: IFeatureToggleQuery,
|
||||
): Promise<FeatureConfigurationClient[]> {
|
||||
query: IFeatureToggleCacheQuery,
|
||||
): Promise<FeatureConfigurationCacheClient[]> {
|
||||
const result =
|
||||
await this.clientFeatureToggleCacheReadModel.getAll(query);
|
||||
return result;
|
||||
|
@ -10,7 +10,7 @@ import type { Logger } from '../../logger';
|
||||
|
||||
import type { FeatureConfigurationClient } from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||
import type {
|
||||
ClientFeatureChange,
|
||||
RevisionCacheEntry,
|
||||
ClientFeatureToggleCache,
|
||||
} from './cache/client-feature-toggle-cache';
|
||||
|
||||
@ -43,15 +43,10 @@ export class ClientFeatureToggleService {
|
||||
|
||||
async getClientDelta(
|
||||
revisionId: number | undefined,
|
||||
projects: string[],
|
||||
environment: string,
|
||||
): Promise<ClientFeatureChange | undefined> {
|
||||
query: IFeatureToggleQuery,
|
||||
): Promise<RevisionCacheEntry | undefined> {
|
||||
if (this.clientFeatureToggleCache !== null) {
|
||||
return this.clientFeatureToggleCache.getDelta(
|
||||
revisionId,
|
||||
environment,
|
||||
projects,
|
||||
);
|
||||
return this.clientFeatureToggleCache.getDelta(revisionId, query);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Calling the partial updates but the cache is not initialized',
|
||||
|
@ -33,7 +33,7 @@ import {
|
||||
} from '../../openapi/spec/client-features-schema';
|
||||
import type ConfigurationRevisionService from '../feature-toggle/configuration-revision-service';
|
||||
import type { ClientFeatureToggleService } from './client-feature-toggle-service';
|
||||
import type { ClientFeatureChange } from './cache/client-feature-toggle-cache';
|
||||
import type { RevisionCacheEntry } from './cache/client-feature-toggle-cache';
|
||||
|
||||
const version = 2;
|
||||
|
||||
@ -300,24 +300,20 @@ export default class FeatureController extends Controller {
|
||||
|
||||
async getDelta(
|
||||
req: IAuthRequest,
|
||||
res: Response<ClientFeatureChange>,
|
||||
res: Response<RevisionCacheEntry>,
|
||||
): Promise<void> {
|
||||
if (!this.flagResolver.isEnabled('deltaApi')) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
const query = await this.resolveQuery(req);
|
||||
const etag = req.headers['if-none-match'];
|
||||
const meta = await this.calculateMeta(query);
|
||||
|
||||
const currentSdkRevisionId = etag ? Number.parseInt(etag) : undefined;
|
||||
const projects = query.project ? query.project : ['*'];
|
||||
const environment = query.environment ? query.environment : 'default';
|
||||
|
||||
const changedFeatures =
|
||||
await this.clientFeatureToggleService.getClientDelta(
|
||||
currentSdkRevisionId,
|
||||
projects,
|
||||
environment,
|
||||
query,
|
||||
);
|
||||
|
||||
if (!changedFeatures) {
|
||||
|
Loading…
Reference in New Issue
Block a user