import { ReplaySubject, Subject, catchError, concatMap, defer, delayWhen, firstValueFrom, map, merge, timer, } from "rxjs"; import { SemVer } from "semver"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction"; import { ServerConfig } from "../../abstractions/config/server-config"; import { EnvironmentService, Region } from "../../abstractions/environment.service"; import { LogService } from "../../abstractions/log.service"; import { StateService } from "../../abstractions/state.service"; import { ServerConfigData } from "../../models/data/server-config.data"; import { StateProvider } from "../../state"; const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600; export class ConfigService implements ConfigServiceAbstraction { private inited = false; protected _serverConfig = new ReplaySubject(1); serverConfig$ = this._serverConfig.asObservable(); private _forceFetchConfig = new Subject(); protected refreshTimer$ = timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS); // after 1 hour, then every hour cloudRegion$ = this.serverConfig$.pipe( map((config) => config?.environment?.cloudRegion ?? Region.US), ); constructor( private stateService: StateService, private configApiService: ConfigApiServiceAbstraction, private authService: AuthService, private environmentService: EnvironmentService, private logService: LogService, private stateProvider: StateProvider, // Used to avoid duplicate subscriptions, e.g. in browser between the background and popup private subscribe = true, ) {} init() { if (!this.subscribe || this.inited) { return; } const latestServerConfig$ = defer(() => this.configApiService.get()).pipe( map((response) => new ServerConfigData(response)), delayWhen((data) => this.saveConfig(data)), catchError((e: unknown) => { // fall back to stored ServerConfig (if any) this.logService.error("Unable to fetch ServerConfig: " + (e as Error)?.message); return this.stateService.getServerConfig(); }), ); // If you need to fetch a new config when an event occurs, add an observable that emits on that event here merge( this.refreshTimer$, // an overridable interval this.environmentService.environment$, // when environment URLs change (including when app is started) this._forceFetchConfig, // manual ) .pipe( concatMap(() => latestServerConfig$), map((data) => (data == null ? null : new ServerConfig(data))), ) .subscribe((config) => this._serverConfig.next(config)); this.inited = true; } getFeatureFlag$(key: FeatureFlag, defaultValue?: T) { return this.serverConfig$.pipe( map((serverConfig) => { if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) { return defaultValue; } return serverConfig.featureStates[key] as T; }), ); } async getFeatureFlag(key: FeatureFlag, defaultValue?: T) { return await firstValueFrom(this.getFeatureFlag$(key, defaultValue)); } triggerServerConfigFetch() { this._forceFetchConfig.next(); } private async saveConfig(data: ServerConfigData) { if ((await this.authService.getAuthStatus()) === AuthenticationStatus.LoggedOut) { return; } const userId = await firstValueFrom(this.stateProvider.activeUserId$); await this.stateService.setServerConfig(data); await this.environmentService.setCloudRegion(userId, data.environment?.cloudRegion); } /** * Verifies whether the server version meets the minimum required version * @param minimumRequiredServerVersion The minimum version required * @returns True if the server version is greater than or equal to the minimum required version */ checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) { return this.serverConfig$.pipe( map((serverConfig) => { if (serverConfig == null) { return false; } const serverVersion = new SemVer(serverConfig.version); return serverVersion.compare(minimumRequiredServerVersion) >= 0; }), ); } }