From c2f393d249585c5fe35b2bfad81ccbb9e59b9615 Mon Sep 17 00:00:00 2001 From: Nino Righi Date: Wed, 6 Aug 2025 15:47:49 +0000 Subject: [PATCH] Merged PR 1911: hotfix(isa-app-store, core-storage): prevent caching of erroneous user state hotfix(isa-app-store, core-storage): prevent caching of erroneous user state Remove shareReplay(1) operator from user state observable to ensure fresh state retrieval on each request. This prevents the system from retaining and reusing failed or invalid state data across multiple operations. The current implementation now makes two API calls (GET + POST) per set operation to guarantee the latest state is always used, trading performance for reliability in error scenarios. Refs: #5270, #5249 --- .../src/app/store/root-state.service.ts | 47 +++++++++---------- .../storage/src/lib/user.storage-provider.ts | 36 +++++++++----- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/apps/isa-app/src/app/store/root-state.service.ts b/apps/isa-app/src/app/store/root-state.service.ts index 59e437a85..32c99dc3d 100644 --- a/apps/isa-app/src/app/store/root-state.service.ts +++ b/apps/isa-app/src/app/store/root-state.service.ts @@ -1,18 +1,18 @@ -import { Injectable } from '@angular/core'; -import { Logger, LogLevel } from '@core/logger'; -import { Store } from '@ngrx/store'; -import { debounceTime, switchMap, takeUntil } from 'rxjs/operators'; -import { RootState } from './root.state'; -import packageInfo from 'packageJson'; -import { environment } from '../../environments/environment'; -import { Subject } from 'rxjs'; -import { AuthService } from '@core/auth'; -import { injectStorage, UserStorageProvider } from '@isa/core/storage'; -import { isEqual } from 'lodash'; +import { Injectable } from "@angular/core"; +import { Logger, LogLevel } from "@core/logger"; +import { Store } from "@ngrx/store"; +import { debounceTime, switchMap, takeUntil } from "rxjs/operators"; +import { RootState } from "./root.state"; +import packageInfo from "packageJson"; +import { environment } from "../../environments/environment"; +import { Subject } from "rxjs"; +import { AuthService } from "@core/auth"; +import { injectStorage, UserStorageProvider } from "@isa/core/storage"; +import { isEqual } from "lodash"; -@Injectable({ providedIn: 'root' }) +@Injectable({ providedIn: "root" }) export class RootStateService { - static LOCAL_STORAGE_KEY = 'ISA_APP_INITIALSTATE'; + static LOCAL_STORAGE_KEY = "ISA_APP_INITIALSTATE"; #storage = injectStorage(UserStorageProvider); @@ -29,14 +29,17 @@ export class RootStateService { ); } - window['clearUserState'] = () => { + window["clearUserState"] = () => { this.clear(); }; } async init() { await this.load(); - this._store.dispatch({ type: 'HYDRATE', payload: RootStateService.LoadFromLocalStorage() }); + this._store.dispatch({ + type: "HYDRATE", + payload: RootStateService.LoadFromLocalStorage(), + }); this.initSave(); } @@ -50,14 +53,10 @@ export class RootStateService { const data = { ...state, version: packageInfo.version, - sub: this._authService.getClaimByKey('sub'), + sub: this._authService.getClaimByKey("sub"), }; RootStateService.SaveToLocalStorageRaw(JSON.stringify(data)); - return this.#storage.set('state', { - ...state, - version: packageInfo.version, - sub: this._authService.getClaimByKey('sub'), - }); + return this.#storage.set("state", data); }), ) .subscribe(); @@ -68,7 +67,7 @@ export class RootStateService { */ async load(): Promise { try { - const res = await this.#storage.get('state'); + const res = await this.#storage.get("state"); const storageContent = RootStateService.LoadFromLocalStorageRaw(); @@ -88,7 +87,7 @@ export class RootStateService { async clear() { try { this._cancelSave.next(); - await this.#storage.clear('state'); + await this.#storage.clear("state"); await new Promise((resolve) => setTimeout(resolve, 100)); RootStateService.RemoveFromLocalStorage(); await new Promise((resolve) => setTimeout(resolve, 100)); @@ -112,7 +111,7 @@ export class RootStateService { try { return JSON.parse(raw); } catch (error) { - console.error('Error parsing local storage:', error); + console.error("Error parsing local storage:", error); this.RemoveFromLocalStorage(); } } diff --git a/libs/core/storage/src/lib/user.storage-provider.ts b/libs/core/storage/src/lib/user.storage-provider.ts index 04f330e05..9dc3be7ed 100644 --- a/libs/core/storage/src/lib/user.storage-provider.ts +++ b/libs/core/storage/src/lib/user.storage-provider.ts @@ -1,27 +1,42 @@ -import { inject, Injectable } from '@angular/core'; -import { StorageProvider } from './storage-provider'; -import { UserStateService } from '@generated/swagger/isa-api'; -import { firstValueFrom, map, shareReplay } from 'rxjs'; +import { inject, Injectable } from "@angular/core"; +import { StorageProvider } from "./storage-provider"; +import { UserStateService } from "@generated/swagger/isa-api"; +import { catchError, firstValueFrom, map, of } from "rxjs"; +import { isEmpty } from "lodash"; -@Injectable({ providedIn: 'root' }) +@Injectable({ providedIn: "root" }) export class UserStorageProvider implements StorageProvider { #userStateService = inject(UserStateService); private state$ = this.#userStateService.UserStateGetUserState().pipe( map((res) => { - if (res.result?.content) { + if (res?.result?.content) { return JSON.parse(res.result.content); } return {}; }), - shareReplay(1), + catchError((err) => { + console.warn( + "No UserStateGetUserState found, returning empty object:", + err, + ); + return of({}); // Return empty state fallback + }), + // shareReplay(1), #5249, #5270 Würde beim Fehlerfall den fehlerhaften Zustand behalten + // Aktuell wird nun jedes mal 2 mal der UserState aufgerufen (GET + POST) + // Damit bei der set Funktion immer der aktuelle Zustand verwendet wird ); - async set(key: string, value: unknown): Promise { + async set(key: string, value: Record): Promise { const current = await firstValueFrom(this.state$); - firstValueFrom( + const content = + current && !isEmpty(current) + ? { ...current, [key]: value } + : { [key]: value }; + + await firstValueFrom( this.#userStateService.UserStateSetUserState({ - content: JSON.stringify({ ...current, [key]: value }), + content: JSON.stringify(content), }), ); } @@ -32,7 +47,6 @@ export class UserStorageProvider implements StorageProvider { } async clear(key: string): Promise { - const current = await firstValueFrom(this.state$); delete current[key]; firstValueFrom(this.#userStateService.UserStateResetUserState());