diff --git a/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts b/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts index 46a813c558..3c821285b0 100644 --- a/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts +++ b/src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts @@ -35,6 +35,10 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore { return this.lifecycles[feature] || []; } + async delete(feature: string): Promise { + this.lifecycles[feature] = []; + } + async stageExists(stage: FeatureLifecycleStage): Promise { const lifecycle = await this.get(stage.feature); return Boolean(lifecycle.find((s) => s.stage === stage.stage)); diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts index 6c7cfacdab..6f0adf6d35 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts @@ -3,6 +3,7 @@ import { FEATURE_ARCHIVED, FEATURE_COMPLETED, FEATURE_CREATED, + FEATURE_REVIVED, type IEnvironment, type IUnleashConfig, type StageName, @@ -82,6 +83,14 @@ test('can insert and read lifecycle stages', async () => { { stage: 'completed', enteredStageAt: expect.any(Date) }, { stage: 'archived', enteredStageAt: expect.any(Date) }, ]); + + eventStore.emit(FEATURE_REVIVED, { featureName }); + await reachedStage('initial'); + const initialLifecycle = + await featureLifecycleService.getFeatureLifecycle(featureName); + expect(initialLifecycle).toEqual([ + { stage: 'initial', enteredStageAt: expect.any(Date) }, + ]); }); test('ignores lifecycle state updates when flag disabled', async () => { diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts index 7d21fe244b..9dc79825bd 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts @@ -3,6 +3,7 @@ import { FEATURE_ARCHIVED, FEATURE_COMPLETED, FEATURE_CREATED, + FEATURE_REVIVED, type IEnvironmentStore, type IEventStore, type IFlagResolver, @@ -93,6 +94,11 @@ export class FeatureLifecycleService extends EventEmitter { this.featureArchived(event.featureName), ); }); + this.eventStore.on(FEATURE_REVIVED, async (event) => { + await this.checkEnabled(() => + this.featureRevived(event.featureName), + ); + }); } async getFeatureLifecycle(feature: string): Promise { @@ -155,4 +161,9 @@ export class FeatureLifecycleService extends EventEmitter { ]); this.emit(STAGE_ENTERED, { stage: 'archived' }); } + + private async featureRevived(feature: string) { + await this.featureLifecycleStore.delete(feature); + await this.featureInitialized(feature); + } } diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-store-type.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-store-type.ts index 963a451d0c..48447ba4b3 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-store-type.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-store-type.ts @@ -11,4 +11,5 @@ export interface IFeatureLifecycleStore { insert(featureLifecycleStages: FeatureLifecycleStage[]): Promise; get(feature: string): Promise; stageExists(stage: FeatureLifecycleStage): Promise; + delete(feature: string): Promise; } diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts index f7bbb3036c..f4d44446dc 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-store.ts @@ -45,6 +45,10 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore { })); } + async delete(feature: string): Promise { + await this.db('feature_lifecycles').where({ feature }).del(); + } + async stageExists(stage: FeatureLifecycleStage): Promise { const result = await this.db.raw( `SELECT EXISTS(SELECT 1 FROM feature_lifecycles WHERE stage = ? and feature = ?) AS present`, diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts index c4fd946af9..f9b33fd446 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts @@ -8,6 +8,7 @@ import { CLIENT_METRICS, FEATURE_ARCHIVED, FEATURE_CREATED, + FEATURE_REVIVED, type IEventStore, type StageName, } from '../../types'; @@ -117,4 +118,7 @@ test('should return lifecycle stages', async () => { ]); await expectFeatureStage('archived'); + + eventStore.emit(FEATURE_REVIVED, { featureName: 'my_feature_a' }); + await reachedStage('initial'); });