From 9ee0e5ee3ec8ac93c7ef10212723faaa25d453d6 Mon Sep 17 00:00:00 2001 From: Robert Duda <robert.duda@hyland.com> Date: Wed, 30 Apr 2025 16:59:50 +0200 Subject: [PATCH] AAE-19688 Persist feature flag overrides in session storage (#10832) * AAE-19688 Persist feature flag overrides in session storage * unit tests --- .../services/debug-features.service.spec.ts | 32 +++++++++--------- .../lib/services/debug-features.service.ts | 33 ++++++++++++------- .../services/storage-features.service.spec.ts | 29 ++++++++-------- .../lib/services/storage-features.service.ts | 29 +++++++++------- 4 files changed, 71 insertions(+), 52 deletions(-) diff --git a/lib/core/feature-flags/src/lib/services/debug-features.service.spec.ts b/lib/core/feature-flags/src/lib/services/debug-features.service.spec.ts index 550adff8d8..f6b0f9dc09 100644 --- a/lib/core/feature-flags/src/lib/services/debug-features.service.spec.ts +++ b/lib/core/feature-flags/src/lib/services/debug-features.service.spec.ts @@ -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); }); diff --git a/lib/core/feature-flags/src/lib/services/debug-features.service.ts b/lib/core/feature-flags/src/lib/services/debug-features.service.ts index bacf205a48..0a6434b5b8 100644 --- a/lib/core/feature-flags/src/lib/services/debug-features.service.ts +++ b/lib/core/feature-flags/src/lib/services/debug-features.service.ts @@ -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)); + } } diff --git a/lib/core/feature-flags/src/lib/services/storage-features.service.spec.ts b/lib/core/feature-flags/src/lib/services/storage-features.service.spec.ts index 10451e3459..054f49edf8 100644 --- a/lib/core/feature-flags/src/lib/services/storage-features.service.spec.ts +++ b/lib/core/feature-flags/src/lib/services/storage-features.service.spec.ts @@ -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(); }); diff --git a/lib/core/feature-flags/src/lib/services/storage-features.service.ts b/lib/core/feature-flags/src/lib/services/storage-features.service.ts index 56c2b414de..6dc9b07afe 100644 --- a/lib/core/feature-flags/src/lib/services/storage-features.service.ts +++ b/lib/core/feature-flags/src/lib/services/storage-features.service.ts @@ -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)); + } }