AAE-19688 Persist feature flag overrides in session storage (#10832)

* AAE-19688 Persist feature flag overrides in session storage

* unit tests
This commit is contained in:
Robert Duda 2025-04-30 16:59:50 +02:00 committed by GitHub
parent 61a1fb64bf
commit 9ee0e5ee3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 71 additions and 52 deletions

View File

@ -17,37 +17,37 @@
import { TestBed } from '@angular/core/testing';
import { DebugFeaturesService } from './debug-features.service';
import { StorageService } from '../../../../src/lib/common/services/storage.service';
import { OverridableFeaturesServiceToken, WritableFeaturesServiceToken } from '../interfaces/features.interface';
import { OverridableFeaturesServiceToken, WritableFeaturesServiceConfigToken, WritableFeaturesServiceToken } from '../interfaces/features.interface';
import { DummyFeaturesService } from './dummy-features.service';
import { StorageFeaturesService } from './storage-features.service';
import { take } from 'rxjs/operators';
describe('DebugFeaturesService', () => {
let service: DebugFeaturesService;
const mockStorage = {
getItem: () =>
JSON.stringify({
feature1: {
current: true
},
feature2: {
current: false,
fictive: true
}
}),
setItem: () => {}
};
let mockStorageKey: string;
let mockStorage;
beforeEach(() => {
mockStorageKey = 'storage-key-test';
mockStorage = { [mockStorageKey]: true };
TestBed.configureTestingModule({
providers: [
DebugFeaturesService,
{ provide: StorageService, useValue: mockStorage },
{
provide: WritableFeaturesServiceConfigToken,
useValue: { storageKey: mockStorageKey }
},
{ provide: WritableFeaturesServiceToken, useClass: StorageFeaturesService },
{ provide: OverridableFeaturesServiceToken, useClass: DummyFeaturesService }
]
});
spyOn(sessionStorage, 'getItem').and.callFake((key) => JSON.stringify(mockStorage[key]));
spyOn(sessionStorage, 'setItem').and.callFake((key, value) => {
mockStorage[key] = value;
});
service = TestBed.inject(DebugFeaturesService);
});

View File

@ -16,8 +16,8 @@
*/
import { Inject, Injectable, Optional } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { skip, switchMap } from 'rxjs/operators';
import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
import {
IDebugFeaturesService,
IFeaturesService,
@ -29,12 +29,12 @@ import {
FlagSet,
IWritableFeaturesService
} from '../interfaces/features.interface';
import { StorageService } from '@alfresco/adf-core';
@Injectable()
export class DebugFeaturesService implements IDebugFeaturesService {
private isInDebugMode: BehaviorSubject<boolean>;
private isInDebugMode$: Observable<boolean>;
private readonly isInDebugModeSubject = new BehaviorSubject<boolean>(false);
private readonly isInDebugMode$ = this.isInDebugModeSubject.asObservable();
private readonly initSubject = new BehaviorSubject<boolean>(false);
get storageKey(): string {
return `${this.config?.storageKey || 'feature-flags'}-override`;
@ -43,14 +43,15 @@ export class DebugFeaturesService implements IDebugFeaturesService {
constructor(
@Inject(OverridableFeaturesServiceToken) private overriddenFeaturesService: IFeaturesService,
@Inject(WritableFeaturesServiceToken) private writableFeaturesService: IFeaturesService & IWritableFeaturesService,
private storageService: StorageService,
@Optional() @Inject(WritableFeaturesServiceConfigToken) private config?: WritableFeaturesServiceConfig
) {
this.isInDebugMode = new BehaviorSubject<boolean>(JSON.parse(this.storageService.getItem(this.storageKey) || 'false'));
this.isInDebugMode$ = this.isInDebugMode.asObservable();
this.init();
this.isInDebugMode.pipe(skip(1)).subscribe((debugMode) => {
this.storageService.setItem(this.storageKey, JSON.stringify(debugMode));
combineLatest({
debugMode: this.isInDebugModeSubject,
init: this.waitForInitializationToFinish()
}).subscribe(({ debugMode }) => {
sessionStorage.setItem(this.storageKey, JSON.stringify(debugMode));
});
}
@ -87,10 +88,20 @@ export class DebugFeaturesService implements IDebugFeaturesService {
}
enable(on: boolean): void {
this.isInDebugMode.next(on);
this.isInDebugModeSubject.next(on);
}
isEnabled$(): Observable<boolean> {
return this.isInDebugMode$;
}
private init() {
const storedOverride = JSON.parse(sessionStorage.getItem(this.storageKey) || 'false');
this.isInDebugModeSubject.next(storedOverride);
this.initSubject.next(true);
}
private waitForInitializationToFinish(): Observable<boolean> {
return this.initSubject.pipe(filter((initialized) => !!initialized));
}
}

View File

@ -17,17 +17,20 @@
import { TestBed } from '@angular/core/testing';
import { StorageFeaturesService } from './storage-features.service';
import { StorageService } from '../../../../src/public-api';
import { FlagSet, WritableFeaturesServiceConfigToken } from '../interfaces/features.interface';
import { skip, take } from 'rxjs/operators';
describe('StorageFeaturesService', () => {
let storageFeaturesService: StorageFeaturesService;
describe('if flags are present in LocalStorage', () => {
const mockStorage = {
getItem: () =>
JSON.stringify({
describe('if flags are present in sessionStorage', () => {
let mockStorageKey: string;
let mockStorage;
beforeEach(() => {
mockStorageKey = 'storage-key-test';
mockStorage = {
[mockStorageKey]: {
feature1: {
current: true
},
@ -35,23 +38,23 @@ describe('StorageFeaturesService', () => {
current: false,
fictive: true
}
}),
setItem: () => {}
};
}
};
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: StorageService, useValue: mockStorage },
{
provide: WritableFeaturesServiceConfigToken,
useValue: {
storageKey: 'storage-key-test'
}
useValue: { storageKey: mockStorageKey }
}
]
});
spyOn(sessionStorage, 'getItem').and.callFake((key) => JSON.stringify(mockStorage[key]));
spyOn(sessionStorage, 'setItem').and.callFake((key, value) => {
mockStorage[key] = value;
});
storageFeaturesService = TestBed.inject(StorageFeaturesService);
storageFeaturesService.init();
});

View File

@ -16,8 +16,8 @@
*/
import { Inject, Injectable, Optional } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { map, skip } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import {
FlagChangeset,
IFeaturesService,
@ -28,21 +28,21 @@ import {
WritableFeaturesServiceConfig
} from '../interfaces/features.interface';
import { FlagSetParser } from './flagset.parser';
import { StorageService } from '@alfresco/adf-core';
@Injectable({ providedIn: 'root' })
export class StorageFeaturesService implements IFeaturesService, IWritableFeaturesService {
private currentFlagState: WritableFlagChangeset = {};
private flags = new BehaviorSubject<WritableFlagChangeset>({});
private flags$ = this.flags.asObservable();
private readonly flags = new BehaviorSubject<WritableFlagChangeset>({});
private readonly flags$ = this.flags.asObservable();
private readonly initSubject = new BehaviorSubject<boolean>(false);
constructor(
private storageService: StorageService,
@Optional() @Inject(WritableFeaturesServiceConfigToken) private config?: WritableFeaturesServiceConfig
) {
this.flags.pipe(skip(1)).subscribe((flags) => {
constructor(@Optional() @Inject(WritableFeaturesServiceConfigToken) private config?: WritableFeaturesServiceConfig) {
combineLatest({
flags: this.flags,
init: this.waitForInitializationToFinish()
}).subscribe(({ flags }) => {
this.currentFlagState = flags;
this.storageService.setItem(this.storageKey, JSON.stringify(FlagSetParser.serialize(flags)));
sessionStorage.setItem(this.storageKey, JSON.stringify(FlagSetParser.serialize(flags)));
});
}
@ -51,9 +51,10 @@ export class StorageFeaturesService implements IFeaturesService, IWritableFeatur
}
init(): Observable<WritableFlagChangeset> {
const storedFlags = JSON.parse(this.storageService.getItem(this.storageKey) || '{}');
const storedFlags = JSON.parse(sessionStorage.getItem(this.storageKey) || '{}');
const initialFlagChangeSet = FlagSetParser.deserialize(storedFlags);
this.flags.next(initialFlagChangeSet);
this.initSubject.next(true);
return of(initialFlagChangeSet);
}
@ -133,4 +134,8 @@ export class StorageFeaturesService implements IFeaturesService, IWritableFeatur
this.flags.next(mergedFlags);
}
private waitForInitializationToFinish(): Observable<boolean> {
return this.initSubject.pipe(filter((initialized) => !!initialized));
}
}