Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: deleted feature names should come from event #8978

Merged
merged 5 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down
Loading