mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
feat: initial design for feature lifecycle service (#6777)
This commit is contained in:
parent
0a247ab704
commit
e2fabcafd4
22
src/lib/features/feature-lifecycle/createFeatureLifecycle.ts
Normal file
22
src/lib/features/feature-lifecycle/createFeatureLifecycle.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import FakeEventStore from '../../../test/fixtures/fake-event-store';
|
||||||
|
import { FakeFeatureLifecycleStore } from './fake-feature-lifecycle-store';
|
||||||
|
import { FeatureLifecycleService } from './feature-lifecycle-service';
|
||||||
|
import FakeEnvironmentStore from '../project-environments/fake-environment-store';
|
||||||
|
|
||||||
|
export const createFakeFeatureLifecycleService = () => {
|
||||||
|
const eventStore = new FakeEventStore();
|
||||||
|
const featureLifecycleStore = new FakeFeatureLifecycleStore();
|
||||||
|
const environmentStore = new FakeEnvironmentStore();
|
||||||
|
const featureLifecycleService = new FeatureLifecycleService({
|
||||||
|
eventStore,
|
||||||
|
featureLifecycleStore,
|
||||||
|
environmentStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
featureLifecycleService,
|
||||||
|
featureLifecycleStore,
|
||||||
|
eventStore,
|
||||||
|
environmentStore,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import type {
|
||||||
|
FeatureLifecycleStage,
|
||||||
|
IFeatureLifecycleStore,
|
||||||
|
FeatureLifecycleView,
|
||||||
|
} from './feature-lifecycle-store-type';
|
||||||
|
|
||||||
|
export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
|
||||||
|
private lifecycles: Record<string, FeatureLifecycleView> = {};
|
||||||
|
|
||||||
|
async insert(featureLifecycleStage: FeatureLifecycleStage): Promise<void> {
|
||||||
|
const existing = await this.get(featureLifecycleStage.feature);
|
||||||
|
this.lifecycles[featureLifecycleStage.feature] = [
|
||||||
|
...existing,
|
||||||
|
{
|
||||||
|
stage: featureLifecycleStage.stage,
|
||||||
|
enteredStageAt: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(feature: string): Promise<FeatureLifecycleView> {
|
||||||
|
return this.lifecycles[feature] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async stageExists(stage: FeatureLifecycleStage): Promise<boolean> {
|
||||||
|
const lifecycle = await this.get(stage.feature);
|
||||||
|
return Boolean(lifecycle.find((s) => s.stage === stage.stage));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
CLIENT_METRICS,
|
||||||
|
FEATURE_ARCHIVED,
|
||||||
|
FEATURE_COMPLETED,
|
||||||
|
FEATURE_CREATED,
|
||||||
|
type IEnvironment,
|
||||||
|
} from '../../types';
|
||||||
|
import { createFakeFeatureLifecycleService } from './createFeatureLifecycle';
|
||||||
|
|
||||||
|
function ms(timeMs) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, timeMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('can insert and read lifecycle stages', async () => {
|
||||||
|
const { featureLifecycleService, eventStore, environmentStore } =
|
||||||
|
createFakeFeatureLifecycleService();
|
||||||
|
const featureName = 'testFeature';
|
||||||
|
async function emitMetricsEvent(environment: string) {
|
||||||
|
await eventStore.emit(CLIENT_METRICS, { featureName, environment });
|
||||||
|
await ms(1);
|
||||||
|
}
|
||||||
|
await environmentStore.create({
|
||||||
|
name: 'my-dev-environment',
|
||||||
|
type: 'development',
|
||||||
|
} as IEnvironment);
|
||||||
|
await environmentStore.create({
|
||||||
|
name: 'my-prod-environment',
|
||||||
|
type: 'production',
|
||||||
|
} as IEnvironment);
|
||||||
|
await environmentStore.create({
|
||||||
|
name: 'my-another-dev-environment',
|
||||||
|
type: 'development',
|
||||||
|
} as IEnvironment);
|
||||||
|
await environmentStore.create({
|
||||||
|
name: 'my-another-prod-environment',
|
||||||
|
type: 'production',
|
||||||
|
} as IEnvironment);
|
||||||
|
featureLifecycleService.listen();
|
||||||
|
|
||||||
|
await eventStore.emit(FEATURE_CREATED, { featureName });
|
||||||
|
|
||||||
|
await emitMetricsEvent('unknown-environment');
|
||||||
|
await emitMetricsEvent('my-dev-environment');
|
||||||
|
await emitMetricsEvent('my-dev-environment');
|
||||||
|
await emitMetricsEvent('my-another-dev-environment');
|
||||||
|
await emitMetricsEvent('my-prod-environment');
|
||||||
|
await emitMetricsEvent('my-prod-environment');
|
||||||
|
await emitMetricsEvent('my-another-prod-environment');
|
||||||
|
|
||||||
|
await eventStore.emit(FEATURE_COMPLETED, { featureName });
|
||||||
|
await eventStore.emit(FEATURE_ARCHIVED, { featureName });
|
||||||
|
|
||||||
|
const lifecycle =
|
||||||
|
await featureLifecycleService.getFeatureLifecycle(featureName);
|
||||||
|
|
||||||
|
expect(lifecycle).toEqual([
|
||||||
|
{ stage: 'initial', enteredStageAt: expect.any(Date) },
|
||||||
|
{ stage: 'pre-live', enteredStageAt: expect.any(Date) },
|
||||||
|
{ stage: 'live', enteredStageAt: expect.any(Date) },
|
||||||
|
{ stage: 'completed', enteredStageAt: expect.any(Date) },
|
||||||
|
{ stage: 'archived', enteredStageAt: expect.any(Date) },
|
||||||
|
]);
|
||||||
|
});
|
@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
CLIENT_METRICS,
|
||||||
|
FEATURE_ARCHIVED,
|
||||||
|
FEATURE_COMPLETED,
|
||||||
|
FEATURE_CREATED,
|
||||||
|
type IEnvironmentStore,
|
||||||
|
type IEventStore,
|
||||||
|
} from '../../types';
|
||||||
|
import type {
|
||||||
|
FeatureLifecycleView,
|
||||||
|
IFeatureLifecycleStore,
|
||||||
|
} from './feature-lifecycle-store-type';
|
||||||
|
|
||||||
|
export class FeatureLifecycleService {
|
||||||
|
private eventStore: IEventStore;
|
||||||
|
|
||||||
|
private featureLifecycleStore: IFeatureLifecycleStore;
|
||||||
|
|
||||||
|
private environmentStore: IEnvironmentStore;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
eventStore,
|
||||||
|
featureLifecycleStore,
|
||||||
|
environmentStore,
|
||||||
|
}: {
|
||||||
|
eventStore: IEventStore;
|
||||||
|
environmentStore: IEnvironmentStore;
|
||||||
|
featureLifecycleStore: IFeatureLifecycleStore;
|
||||||
|
}) {
|
||||||
|
this.eventStore = eventStore;
|
||||||
|
this.featureLifecycleStore = featureLifecycleStore;
|
||||||
|
this.environmentStore = environmentStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen() {
|
||||||
|
this.eventStore.on(FEATURE_CREATED, async (event) => {
|
||||||
|
await this.featureInitialized(event.featureName);
|
||||||
|
});
|
||||||
|
this.eventStore.on(CLIENT_METRICS, async (event) => {
|
||||||
|
await this.featureReceivedMetrics(
|
||||||
|
event.featureName,
|
||||||
|
event.environment,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.eventStore.on(FEATURE_COMPLETED, async (event) => {
|
||||||
|
await this.featureCompleted(event.featureName);
|
||||||
|
});
|
||||||
|
this.eventStore.on(FEATURE_ARCHIVED, async (event) => {
|
||||||
|
await this.featureArchived(event.featureName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFeatureLifecycle(feature: string): Promise<FeatureLifecycleView> {
|
||||||
|
return this.featureLifecycleStore.get(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
async featureInitialized(feature: string) {
|
||||||
|
await this.featureLifecycleStore.insert({ feature, stage: 'initial' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async stageReceivedMetrics(feature: string, stage: 'live' | 'pre-live') {
|
||||||
|
const stageExists = await this.featureLifecycleStore.stageExists({
|
||||||
|
stage,
|
||||||
|
feature,
|
||||||
|
});
|
||||||
|
if (!stageExists) {
|
||||||
|
await this.featureLifecycleStore.insert({ feature, stage });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async featureReceivedMetrics(feature: string, environment: string) {
|
||||||
|
const env = await this.environmentStore.get(environment);
|
||||||
|
if (!env) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (env.type === 'production') {
|
||||||
|
await this.stageReceivedMetrics(feature, 'live');
|
||||||
|
} else if (env.type === 'development') {
|
||||||
|
await this.stageReceivedMetrics(feature, 'pre-live');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async featureCompleted(feature: string) {
|
||||||
|
await this.featureLifecycleStore.insert({
|
||||||
|
feature,
|
||||||
|
stage: 'completed',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async featureArchived(feature: string) {
|
||||||
|
await this.featureLifecycleStore.insert({ feature, stage: 'archived' });
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
export type StageName =
|
||||||
|
| 'initial'
|
||||||
|
| 'pre-live'
|
||||||
|
| 'live'
|
||||||
|
| 'completed'
|
||||||
|
| 'archived';
|
||||||
|
|
||||||
|
export type FeatureLifecycleStage = {
|
||||||
|
feature: string;
|
||||||
|
stage: StageName;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FeatureLifecycleStageView = {
|
||||||
|
stage: StageName;
|
||||||
|
enteredStageAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FeatureLifecycleView = FeatureLifecycleStageView[];
|
||||||
|
|
||||||
|
export interface IFeatureLifecycleStore {
|
||||||
|
insert(featureLifecycleStage: FeatureLifecycleStage): Promise<void>;
|
||||||
|
get(feature: string): Promise<FeatureLifecycleView>;
|
||||||
|
stageExists(stage: FeatureLifecycleStage): Promise<boolean>;
|
||||||
|
}
|
@ -30,6 +30,7 @@ export const FEATURE_STRATEGY_REMOVE = 'feature-strategy-remove' as const;
|
|||||||
export const DROP_FEATURE_TAGS = 'drop-feature-tags' as const;
|
export const DROP_FEATURE_TAGS = 'drop-feature-tags' as const;
|
||||||
export const FEATURE_UNTAGGED = 'feature-untagged' as const;
|
export const FEATURE_UNTAGGED = 'feature-untagged' as const;
|
||||||
export const FEATURE_STALE_ON = 'feature-stale-on' as const;
|
export const FEATURE_STALE_ON = 'feature-stale-on' as const;
|
||||||
|
export const FEATURE_COMPLETED = 'feature-completed' as const;
|
||||||
export const FEATURE_STALE_OFF = 'feature-stale-off' as const;
|
export const FEATURE_STALE_OFF = 'feature-stale-off' as const;
|
||||||
export const DROP_FEATURES = 'drop-features' as const;
|
export const DROP_FEATURES = 'drop-features' as const;
|
||||||
export const FEATURE_ENVIRONMENT_ENABLED =
|
export const FEATURE_ENVIRONMENT_ENABLED =
|
||||||
|
Loading…
Reference in New Issue
Block a user