1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-13 11:17:26 +02:00
unleash.unleash/src/lib/features/feature-toggle/configuration-revision-service.ts
Gastón Fournier 7ea0c9ca9b
feat: support different etags per environment (#10512)
## About the changes
This PR introduces environment-specific etags. This way clients will not
react by updating features when there are changes in environments the
SDK doesn't care about.

## Details
There's a bit of scouting work (please don't make me split this 🙏)
and other details are in comments, but the most relevant for the lazy
ones:
- Important **decision** on how we detect changes, unifying polling and
delta:
https://github.com/Unleash/unleash/pull/10512#discussion_r2285677129
- **Decision** on how we update revision id per environment:
https://github.com/Unleash/unleash/pull/10512#discussion_r2291888401
- and how we do initial fetch on the read path:
https://github.com/Unleash/unleash/pull/10512#discussion_r2291884777
- The singleton pattern that gave me **nightmares**:
https://github.com/Unleash/unleash/pull/10512#discussion_r2291848934
- **Do we still have ALL_ENVS tokens?**
https://github.com/Unleash/unleash/pull/10512#discussion_r2291913249

## Feature flag
To control the rollout introduced `etagByEnv` feature:
[0da567d](0da567dd9b)
2025-08-22 10:35:17 -03:00

117 lines
3.6 KiB
TypeScript

import type { Logger } from '../../logger.js';
import type {
IEventStore,
IFlagResolver,
IUnleashConfig,
IUnleashStores,
} from '../../types/index.js';
import EventEmitter from 'events';
export const UPDATE_REVISION = 'UPDATE_REVISION';
export default class ConfigurationRevisionService extends EventEmitter {
private static instance: ConfigurationRevisionService | undefined;
private logger: Logger;
private eventStore: IEventStore;
private revisionId: number;
private maxRevisionId: Map<string, number> = new Map();
private flagResolver: IFlagResolver;
private constructor(
{ eventStore }: Pick<IUnleashStores, 'eventStore'>,
{
getLogger,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
) {
super();
this.logger = getLogger('configuration-revision-service.ts');
this.eventStore = eventStore;
this.flagResolver = flagResolver;
this.revisionId = 0;
}
static getInstance(
{ eventStore }: Pick<IUnleashStores, 'eventStore'>,
{
getLogger,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
) {
if (!ConfigurationRevisionService.instance) {
ConfigurationRevisionService.instance =
new ConfigurationRevisionService(
{ eventStore },
{ getLogger, flagResolver },
);
}
return ConfigurationRevisionService.instance;
}
async getMaxRevisionId(environment?: string): Promise<number> {
if (environment && !this.maxRevisionId[environment]) {
await this.updateMaxEnvironmentRevisionId(environment);
}
if (
environment &&
this.maxRevisionId[environment] &&
this.maxRevisionId[environment] > 0
) {
return this.maxRevisionId[environment];
}
if (this.revisionId > 0) {
return this.revisionId;
} else {
return this.updateMaxRevisionId();
}
}
async updateMaxEnvironmentRevisionId(environment: string): Promise<number> {
const envRevisionId = await this.eventStore.getMaxRevisionId(
this.maxRevisionId[environment],
environment,
);
if (this.maxRevisionId[environment] ?? 0 < envRevisionId) {
this.maxRevisionId[environment] = envRevisionId;
}
return this.maxRevisionId[environment];
}
async updateMaxRevisionId(emit: boolean = true): Promise<number> {
if (this.flagResolver.isEnabled('disableUpdateMaxRevisionId')) {
return 0;
}
const revisionId = await this.eventStore.getMaxRevisionId(
this.revisionId,
);
if (this.revisionId !== revisionId) {
this.logger.debug(
`Updating feature configuration with new revision Id ${revisionId} and all envs: ${Object.keys(this.maxRevisionId).join(', ')}`,
);
await Promise.allSettled(
Object.keys(this.maxRevisionId).map((environment) =>
this.updateMaxEnvironmentRevisionId(environment),
),
);
this.revisionId = revisionId;
if (emit) {
this.emit(UPDATE_REVISION, revisionId);
}
}
return this.revisionId;
}
destroy(): void {
ConfigurationRevisionService.instance?.removeAllListeners();
ConfigurationRevisionService.instance = undefined;
}
}