libs/ngx-pfe/services/pfe-state-service/state.service.ts
The PfeStateService is the central location to hold the app state.
We need to tell the Angular compiler that the NGX_PFE_INITIAL_STATE token with a Promise is ok: It doesn't like the Promise there, as that could cause issues during runtime (Check runs due to strictMetadataEmit). This will not be required anymore, once the library builds are also switched to ivy.
constructor(ngxPfeModuleConfiguration: NgxPfeModuleConfiguration, ngxPfeStateKeyConfig: PfeStateSessionStorageKeyConfig, restoreStateToken: Promise<PfeUserInputState>, pfeRestService: PfeRestService, pfeAppStateFactory: ModelFactory<PfeUserInputState>, pfeUtilService: PfeUtilService, appRef: ApplicationRef, pfeConditionsService: PfeConditionsService, pfeConfigService: PfeConfigurationService, _document: Document, logger: NgxLoggerService)
|
||||||||||||||||||||||||||||||||||||
Parameters :
|
analyzePageForStale | ||||||
analyzePageForStale(page: StaleStateConfig)
|
||||||
Parameters :
Returns :
void
|
cleanStaleStateByServiceActivator |
cleanStaleStateByServiceActivator()
|
Method is triggered by serviceActivator
Returns :
void
|
getFullState |
getFullState()
|
Returns a copy of the current PfeAppState. To change data within the state, the StateService methods need to be used.
Returns :
PfeUserInputState
|
getObservableForExpressionKey | ||||||||||||||||
getObservableForExpressionKey(expression: JsonPathExpression, triggerForUndefined?: boolean, disableDeepEqual?: boolean)
|
||||||||||||||||
Returns an observable for am expression in the state. Use subscribe to react to changes. The observable gets only triggered, if the actual value of the expression changes. If it is set again with the same value, the observable won't trigger. Do not forget to unsubscribe, once the subscription is not needed anymore.
Parameters :
Returns :
Observable<any>
|
getObservableForKey | ||||||||||||||||
getObservableForKey(key: string, triggerForUndefined?: boolean, disableDeepEqual?: boolean)
|
||||||||||||||||
Returns an observable for a specific key in the state. Use subscribe to react to changes. The observable gets only triggered, if the actual value of the key changes. If it is set again with the same value, the observable won't trigger. Do not forget to unsubscribe, once the subscription is not needed anymore.
Parameters :
Returns :
Observable<any>
|
getStateValueByExpression | ||||||
getStateValueByExpression(jsonPathExpression: JsonPathExpression)
|
||||||
Parameters :
Returns :
any
|
getValue | ||||||||
getValue(key: string)
|
||||||||
Returns previously stored data by its key.
Parameters :
Returns :
any
|
isSessionStorageEnabled |
isSessionStorageEnabled()
|
Returns :
Promise<boolean>
|
removePageFromState | |||||||||
removePageFromState(pageIds: string[], currentPageId: string)
|
|||||||||
Parameters :
Returns :
void
|
removeStateInSessionStorage | ||||||||
removeStateInSessionStorage(stateKey: string)
|
||||||||
Parameters :
Returns :
Promise<void>
|
resetState |
resetState()
|
Returns :
Promise<PfeUserInputState>
|
Public restoreState | |||||||||
restoreState(state: PfeUserInputState, resolve?: (value?: PfeUserInputState) => void)
|
|||||||||
Parameters :
Returns :
void
|
restoreStateFromBackend |
restoreStateFromBackend()
|
Returns :
Promise<PfeUserInputState>
|
restoreStateFromBackendOrSessionStorage |
Please switch to the PfeBusinessService restoreState() method. |
restoreStateFromBackendOrSessionStorage()
|
Returns :
Promise<any>
|
retrieveStateFromSessionStorage |
retrieveStateFromSessionStorage()
|
Returns :
any
|
setFullState | ||||||
setFullState(pfeUserInputState: PfeUserInputState)
|
||||||
Parameters :
Returns :
void
|
setStateIds | ||||||
setStateIds(formIds: string[])
|
||||||
Parameters :
Returns :
void
|
storeState |
storeState()
|
Returns :
any
|
storeStateInSessionStorage |
storeStateInSessionStorage()
|
Returns :
Promise<void>
|
Async storeStateOnBackend |
storeStateOnBackend()
|
Returns :
any
|
storeValue | ||||||||||||
storeValue(key: string, value: any)
|
||||||||||||
Store a value under the key in the state. Existing values with the same key, are overwritten.
Parameters :
Returns :
void
|
storeValueByExpression | ||||||||||||
storeValueByExpression(expression: JsonPathExpression, value: any)
|
||||||||||||
Store a value under the expression in the state. Existing values with the same key, are overwritten.
Parameters :
Returns :
void
|
Public updateStateValues | ||||||||
updateStateValues(stateUpdates: StateUpdates[] | undefined)
|
||||||||
This method analyze if a navConfig have define userInputState updates and apply them
Parameters :
Returns :
void
|
Readonly _staleState |
Type : StaleStateInstance
|
Default value : {
pages: {},
currentPageId: '',
removePageFromState: [],
}
|
staleState |
Public pfeUserInputState$ |
Type : Observable<PfeUserInputState>
|
This observable triggers for every change of the whole model. It should only be used if absolutely necessary. |
import { NgxLoggerService } from '@allianz/ngx-logger';
import { DOCUMENT } from '@angular/common';
import { ApplicationRef, Inject, Injectable, Optional } from '@angular/core';
import equal from 'fast-deep-equal/es6';
import { Observable, identity } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { Model, ModelFactory } from '../../3rd-party/angular-extensions/model/model';
import { JsonPathExpression } from '../../models/jsonpath-expression-type.model';
import { StateOperations, StateUpdates } from '../../models/navigation-config.model';
import { NGX_PFE_CONFIGURATION, NgxPfeModuleConfiguration } from '../../models/ngx-pfe-module-configuration.model';
import { PfeUserInputState } from '../../models/pfe-state/user-input-state.model';
import { PfeConditionsService } from '../../pfe-conditions/public-api';
import { ARRAY_STATE_EXPRESSION, PfeUrlParameters, PfeUtilService, STATE_EXPRESSION } from '../../pfe-util/services/util.service';
import { clone } from '../../util/clone';
import { isObject } from '../../util/type-checks';
import { PfeConfigurationService } from '../pfe-config-service/config-service.service';
import { PfeRestService } from '../pfe-rest-service/rest.service';
import { PFE_STATE_INTERNAL_STATE_KEY } from './pfe-state-keys';
import { NGX_PFE_INITIAL_STATE, NGX_PFE_STATE_SESSION_STORAGE_KEY_CONFIG, PfeStateSessionStorageKeyConfig } from './state.model';
export type StateID = string;
export interface StaleStateInstance {
pages: Record<string, StaleState>;
currentPageId: string;
removePageFromState: string[];
}
export interface StaleState {
keys: string[];
staleKeys: string[];
revisit: boolean;
}
export interface StaleStateConfig {
pageId: string;
removePageFromState?: string[];
}
const PFE_INTERNAL_VALUES = ['pfeHistory', 'pfeVisitedPages', 'validationErrorData', 'lastVisitedPage', '_pfe'];
/**
* The PfeStateService is the central location to hold the app state.
*
* We need to tell the Angular compiler that the NGX_PFE_INITIAL_STATE token with a Promise is ok:
* @dynamic
* It doesn't like the Promise there, as that could cause issues during runtime (Check runs due to strictMetadataEmit).
* This will not be required anymore, once the library builds are also switched to ivy.
*
* @export
*/
@Injectable()
export class PfeStateService {
// To be backwards compatible, the pfeUserInputState$ is kept and exposed:
/**
* This observable triggers for every change of the whole model.
* It should only be used if absolutely necessary.
*/
public pfeUserInputState$: Observable<PfeUserInputState>;
/**
* staleState
*/
// Keeping the previous naming for now, as it would be potentially breaking (Even with the _)
// eslint-disable-next-line @typescript-eslint/naming-convention
readonly _staleState: StaleStateInstance = {
pages: {},
currentPageId: '',
removePageFromState: [],
};
private _stateID: StateID | undefined;
private pfeUserInputState: Model<PfeUserInputState>;
/**
* This observable triggers for every change of the whole model.
* Keep the number of direct/unfiltered subscriptions low.
*/
private pfeUserInputStateInternal$: Observable<PfeUserInputState>;
private stateKey = 'state';
constructor(
@Inject(NGX_PFE_CONFIGURATION) private ngxPfeModuleConfiguration: NgxPfeModuleConfiguration,
@Optional() @Inject(NGX_PFE_STATE_SESSION_STORAGE_KEY_CONFIG) private ngxPfeStateKeyConfig: PfeStateSessionStorageKeyConfig,
@Optional() @Inject(NGX_PFE_INITIAL_STATE) private restoreStateToken: Promise<PfeUserInputState>,
private pfeRestService: PfeRestService,
private pfeAppStateFactory: ModelFactory<PfeUserInputState>,
private pfeUtilService: PfeUtilService,
private appRef: ApplicationRef,
private pfeConditionsService: PfeConditionsService,
private pfeConfigService: PfeConfigurationService,
@Inject(DOCUMENT) private _document: Document,
private logger: NgxLoggerService
) {
// Replace with a nicer solution, once
// https://github.com/Microsoft/TypeScript/issues/10853 and
// https://github.com/Microsoft/TypeScript/issues/11304
// are resolved.
const pfeUserInputState: PfeUserInputState = {
[PFE_STATE_INTERNAL_STATE_KEY]: {},
staleState: this._staleState,
};
/**
* The pfeUserInputState is created without the immutable behavior here.
* That means, this needs to be done manually in every location where the pfeUserInputState or pfeUserInputState$
* is returned to the outside world. Wether it be in full or only as a slice from within the data.
*
* The background for this is, that the immutable version of the Model always copies the whole state
* when it is accessed. This results in a worsening performance the bigger the data in the state is.
*
* Copying the whole state is not necessary, when only a slice of data is accessed. For example via the getObservableFor* methods.
* I those cases is is enough to only copy the slice of data that is actually returned.
*
* For this reason, the state itself is created as mutable and only the returned data to outside this service is
* copied.
*
* Additionally, the performance when updating the state via the store* methods is improved as the state is now only copied once instead of twice.
*/
this.pfeUserInputState = this.pfeAppStateFactory.createMutable(pfeUserInputState);
this.pfeUserInputStateInternal$ = this.pfeUserInputState.data$;
this.pfeUserInputState$ = this.pfeUserInputStateInternal$.pipe(map((data) => clone(data)));
const stateIDFomURL = this.pfeUtilService.getURLParam(PfeUrlParameters.STATE_ID);
this.stateID = stateIDFomURL;
this.readURLStateParameters();
this.setupHotkeys();
this.setUpStateKey();
}
private set stateID(stateID: StateID) {
if (!stateID) {
return;
}
this._stateID = stateID;
this.storeValue('pfeStateID', this._stateID);
}
/**
* Store a value under the key in the state.
* Existing values with the same key, are overwritten.
*
* @param key The key has to be a string value.
* @param value The value can be of any type. Typecasting needs to be handled by the retrieving component.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
storeValue(key: string, value: any) {
this.addKeyToStale(key);
const pfeAppState = this.pfeUserInputState.get();
pfeAppState[key] = value;
this.pfeUserInputState.set(clone(pfeAppState));
}
/**
* Store a value under the expression in the state.
* Existing values with the same key, are overwritten.
*
* @param expression The expression has to be a valid JsonPathExpression.
* @param value The value can be of any type. Typecasting needs to be handled by the retrieving component.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
storeValueByExpression(expression: JsonPathExpression, value: any) {
const pfeAppState = this.pfeUserInputState.get();
PfeUtilService.setValueFromJsonExpression(pfeAppState, expression, value);
this.pfeUserInputState.set(clone(pfeAppState));
// store the userInput state
this.addKeyToStale(expression);
}
/**
* Returns previously stored data by its key.
*
* @param key The key in the state
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getValue(key: string): any {
const pfeAppState = this.pfeUserInputState.get();
return clone(pfeAppState[key]);
}
getStateValueByExpression(jsonPathExpression: JsonPathExpression) {
const pfeAppState = this.pfeUserInputState.get();
return clone(PfeUtilService.getValueOrList(pfeAppState, jsonPathExpression));
}
async storeStateOnBackend() {
if (!this.ngxPfeModuleConfiguration.stateServiceEndpoint) {
return;
}
const currentState: PfeUserInputState = clone(this.pfeUserInputState.get());
const stateSize = Object.keys(currentState).length;
if (stateSize > 0) {
if (!this._stateID) {
await this.createStateAndGetStateID(currentState);
} else {
await this.updateState(this._stateID, currentState);
}
}
}
public restoreState(state: PfeUserInputState, resolve?: (value: PfeUserInputState) => void) {
this.pfeUserInputState.set(clone(state));
this.populateBrowserHistory(state);
this.readURLStateParameters();
this.appRef.tick();
if (resolve) {
resolve(state);
}
this.pfeUtilService.trackingStateRestoredSubject$.next(state);
}
restoreStateFromBackend(): Promise<PfeUserInputState> {
if (!this.ngxPfeModuleConfiguration.stateServiceEndpoint || !this._stateID) {
return Promise.reject(undefined);
}
return new Promise((resolve, reject) => {
if (!this._stateID) {
reject();
} else {
this.pfeRestService.getClientState(this._stateID).subscribe(
(data) => {
const state: PfeUserInputState = { ...data };
const stateSize = Object.keys(state).length;
if (stateSize > 0) {
this.restoreState(state, resolve);
} else if (resolve) {
resolve(state);
}
},
(error) => {
// TODO: Error handling
this.logger.errorToServer('restoreStateFromBackend', error);
reject();
}
);
}
});
}
/**
* Returns a copy of the current PfeAppState.
* To change data within the state, the StateService methods need to be used.
*/
getFullState(): PfeUserInputState {
return clone(this.pfeUserInputState.get());
}
setFullState(pfeUserInputState: PfeUserInputState) {
// TODO: track stale state key when using setFullState function
this.pfeUserInputState.set(clone(pfeUserInputState));
}
resetState(): Promise<PfeUserInputState> {
return new Promise((resolve) => this.restoreState({}, resolve));
}
/**
* Returns an observable for a specific key in the state.
* Use subscribe to react to changes.
* The observable gets only triggered, if the actual value of the key changes.
* If it is set again with the same value, the observable won't trigger.
*
* Do not forget to unsubscribe, once the subscription is not needed anymore.
*
* @param key The key of the value in the state.
* @param triggerForUndefined Trigger the observable, also when the value is undefined.
* @param disableDeepEqual By default the observable only triggers for a deepEqual change. This behavior can be disabled. Default: false
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getObservableForKey(key: string, triggerForUndefined?: boolean, disableDeepEqual?: boolean): Observable<any> {
return this.getObservableForAccessor((entry) => entry[key], triggerForUndefined, disableDeepEqual);
}
/**
* Returns an observable for am expression in the state.
* Use subscribe to react to changes.
* The observable gets only triggered, if the actual value of the expression changes.
* If it is set again with the same value, the observable won't trigger.
*
* Do not forget to unsubscribe, once the subscription is not needed anymore.
*
* @param expression JsonPathExpression to get the value in the state.
* @param triggerForUndefined Trigger the observable, also when the value is undefined. Default: false
* @param disableDeepEqual By default the observable only triggers for a deepEqual change. This behavior can be disabled. Default: false
*/
getObservableForExpressionKey(
expression: JsonPathExpression,
triggerForUndefined?: boolean,
disableDeepEqual?: boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Observable<any> {
return this.getObservableForAccessor(
(entry) => PfeUtilService.getValueOrList(entry, expression),
triggerForUndefined,
disableDeepEqual
);
}
retrieveStateFromSessionStorage() {
return this.isSessionStorageEnabled().then((value) => {
if (value) {
return this.restoreStateFromSessionStorage().catch(() => {
// do nothing here
});
}
});
}
storeStateInSessionStorage(): Promise<void> {
return this.isSessionStorageEnabled().then((value) => {
if (value) {
return this.storeStateOnSessionStorage();
}
});
}
removeStateInSessionStorage(stateKey: string = this.stateKey): Promise<void> {
return this.isSessionStorageEnabled().then((value) => {
if (value) {
return this.removeStateOnSessionStorage(stateKey);
}
});
}
isSessionStorageEnabled(): Promise<boolean> {
return this.pfeConfigService.getConfig().then((config) => !!config?.appConfiguration?.pfeConfig?.enableSessionStorageState);
}
/**
* Tries to restore a state from these sources:
*
* 1. NGX_PFE_RESTORED_STATE injection token
* 2. Backend
* 3. SessionStorage
*
* Errors from the NGX_PFE_RESTORED_STATE injection token are forwarded.
* Errors from the other sources are swallowed.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
automaticallyRestoreState(): Promise<any> {
if (this.restoreStateToken) {
return this.restoreStateFromToken(this.restoreStateToken);
} else {
return this.restoreStateFromBackend()
.then((data) => {
const state: PfeUserInputState = { ...data };
const stateSize = Object.keys(state).length;
if (!(stateSize > 0)) {
return this.retrieveStateFromSessionStorage();
}
})
.catch(() => this.retrieveStateFromSessionStorage());
}
}
/**
* @deprecated Please switch to the PfeBusinessService restoreState() method.
*/
restoreStateFromBackendOrSessionStorage() {
return this.automaticallyRestoreState();
}
storeState() {
return Promise.all([this.storeStateOnBackend(), this.storeStateInSessionStorage()]);
}
/**
*
* @param page
*/
analyzePageForStale(page: StaleStateConfig) {
if (this.pfeConfigService?.pfeApplicationConfiguration?.pfeConfig?.enableStateCleaning) {
/**
* check for page excluded from stale analysis
*/
const excludedState = this.pfeConfigService.pfeApplicationConfiguration.pfeConfig.excludeFromStaleState;
if (excludedState && this.contains(excludedState, page.pageId)) {
return;
}
this.startStaleForPage(page.pageId);
/**
* check if page configuration contains pages to remove from state
*/
if (Object.prototype.hasOwnProperty.call(page, 'removePageFromState')) {
if (page.removePageFromState) {
this.removePageFromState(page.removePageFromState, page.pageId);
}
}
}
}
/**
* @param pageIds
* @param currentPageId
*/
removePageFromState(pageIds: string[], currentPageId: string) {
pageIds.forEach((pageId: string) => {
if (this.pfeConfigService?.pfeApplicationConfiguration?.pfeConfig?.autoCleanStaleState) {
this.removeStaleKeysFromState(pageId, true);
} else if (!this.contains(this._staleState.removePageFromState, pageId)) {
this._staleState.removePageFromState.push(pageId);
}
});
const pageIndexInRemoveState = this._staleState.removePageFromState.indexOf(currentPageId);
if (-1 < pageIndexInRemoveState) {
this._staleState.removePageFromState.splice(pageIndexInRemoveState, 1);
}
}
/**
* Method is triggered by serviceActivator
*/
cleanStaleStateByServiceActivator() {
const pfeAppState = this.pfeUserInputState.get();
const allPagesKeys = this.getAllPagesKeys(this._staleState.removePageFromState);
// remove stalePages
this._staleState.removePageFromState.forEach((pageId) => this.removePageFromStaleState(pageId, pfeAppState, allPagesKeys));
this._staleState.removePageFromState.length = 0;
// save state
this.pfeUserInputState.set(pfeAppState);
}
/**
* @param formIds
*/
setStateIds(formIds: string[]) {
formIds.forEach((key) => this.addKeyToStale(key));
}
/**
* This method analyze if a navConfig have define userInputState updates and apply them
* @param stateUpdates navigation config to be analyze
*/
public updateStateValues(stateUpdates: StateUpdates[] | undefined) {
// Exit if no updates are define
if (!stateUpdates || stateUpdates.length === 0) {
return;
}
try {
// Filter the updates that could be done
stateUpdates
.filter((update) => {
if (!update.conditions) {
return true;
}
return this.pfeConditionsService.evaluateConditions(update.conditions, this.getFullState());
})
.forEach((update) => {
if (update.operation) {
this.executeOperationState(update);
} else if (
typeof update.value === 'string' &&
(update.value.startsWith(STATE_EXPRESSION) || update.value.startsWith(ARRAY_STATE_EXPRESSION))
) {
const object = this.getStateValueByExpression(update.value);
this.storeValueByExpression(update.key, object);
} else {
this.storeValueByExpression(update.key, update.value);
}
});
} catch (error) {
this.logger.error(`Error during stateUpdates "${(error as Error).message}, used in config : `, stateUpdates);
}
}
private async createStateAndGetStateID(currentState: PfeUserInputState) {
try {
const generatedStateID = (await this.pfeRestService.createClientState(currentState)).id;
this.stateID = encodeURIComponent(generatedStateID);
} catch (error) {
// TODO: Error handling:
this.logger.errorToServer('createStateAndGetStateID', error);
}
}
private async updateState(stateID: StateID, currentState: PfeUserInputState) {
try {
await this.pfeRestService.updateClientState(stateID, currentState);
} catch (error) {
// TODO: Error handling:
this.logger.errorToServer('updateState', error);
}
}
private restoreStateFromToken(tokenPromise: Promise<PfeUserInputState | undefined>): Promise<PfeUserInputState> {
return tokenPromise.then((state) => {
const validState: PfeUserInputState = isObject(state) ? state : this.pfeUserInputState.get();
return new Promise((resolve) => this.restoreState(validState, resolve));
});
}
/**
* Called by the public getObservableForExpressionKey and getObservableForKey
* to get the same pipe with different accessors.
*/
private getObservableForAccessor(
accessor: (value: PfeUserInputState, index: number) => unknown,
triggerForUndefined: boolean | undefined,
disableDeepEqual: boolean | undefined
) {
return this.pfeUserInputStateInternal$.pipe(
map(accessor),
filter((entry) => {
if (triggerForUndefined) {
return true;
} else {
return entry != null;
}
}),
map((data) => clone(data)),
!disableDeepEqual ? distinctUntilChanged((prev: unknown, next: unknown) => equal(prev, next)) : identity
);
}
private readURLStateParameters() {
const urlParams = this.pfeUtilService.getUrlParams(this._document.location.search, '?');
this.copyValuesToState(urlParams);
const hashParams = this.pfeUtilService.getUrlParams(this._document.location.hash, '#');
this.copyValuesToState(hashParams);
}
private copyValuesToState(parameters: Record<string, string>) {
for (const key in parameters) {
if (Object.prototype.hasOwnProperty.call(parameters, key)) {
const value = parameters[key];
this.saveUrlParamsToState(key, value);
}
}
}
private saveUrlParamsToState(key: string, value: string) {
this.pfeConfigService.getAppConfiguration().then((appConfiguration) => {
if (appConfiguration?.pfeConfig?.urlParametersInState && appConfiguration.pfeConfig.urlParametersInState.length > 0) {
appConfiguration.pfeConfig.urlParametersInState.forEach((urlParametersInState) => {
if (urlParametersInState.key === key) {
this.storeValueByExpression(urlParametersInState.stateKeyExpression, value);
}
});
}
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private populateBrowserHistory(state: PfeUserInputState) {
// TODO: Implement me
}
private async setupHotkeys() {
if (!this.ngxPfeModuleConfiguration.disableStateDebuggingHotkey) {
this._document.addEventListener('keydown', (event) => {
if ((event.altKey || event.metaKey) && event.ctrlKey && event.key === 'l') {
const state = this.getFullState();
console.log(state);
return false; // Prevent bubbling
}
});
}
}
private setUpStateKey() {
if (this.ngxPfeStateKeyConfig?.keySuffix) {
this.stateKey = `state_${this.ngxPfeStateKeyConfig.keySuffix}`;
}
}
private restoreStateFromSessionStorage(): Promise<PfeUserInputState> {
const state = sessionStorage.getItem(this.stateKey);
if (state) {
return new Promise((resolve) => this.restoreState(JSON.parse(state), resolve));
} else {
return Promise.reject();
}
}
private storeStateOnSessionStorage() {
const currentState: PfeUserInputState = this.pfeUserInputState.get();
const stateSize = Object.keys(currentState).length;
if (stateSize > 0) {
sessionStorage.setItem(this.stateKey, JSON.stringify(currentState));
}
}
private removeStateOnSessionStorage(stateKey: string) {
sessionStorage.removeItem(stateKey);
}
/**
*
* @param stateKeys
* @param pfeStateObj
*/
private removeKeysFromState(stateKeys: string[], pfeStateObj: PfeUserInputState) {
stateKeys.forEach((key) => this.getValueForKey(key, pfeStateObj, true));
}
/**
*
* @param key
* @param pfeStateObj
*/
private getValueForKey(key: string, pfeStateObj: PfeUserInputState, removeKey?: boolean) {
const props = key.split('.'),
last: keyof PfeUserInputState | undefined = props.pop();
let context = pfeStateObj;
props.forEach((dKey) => {
context = context && context[dKey] ? context[dKey] : null;
});
if (removeKey && context && last) {
delete context[last];
}
return context;
}
/**
* registers a page for staling
* @param pageID
*/
private startStaleForPage(pageID: string) {
this._staleState.currentPageId = pageID;
if (!Object.prototype.hasOwnProperty.call(this._staleState.pages, pageID)) {
this._staleState.pages[pageID] = {
keys: [],
staleKeys: [],
revisit: false,
};
} else {
const state = this._staleState.pages[pageID];
state.revisit = true;
// eslint-disable-next-line prefer-spread
state.staleKeys.push.apply(
state.staleKeys,
state.keys.filter((key) => !this.contains(state.staleKeys, key))
);
state.keys.length = 0;
}
}
/**
*
* @param pageId
* @param pfeAppState
*/
private cleanUpRevisitedPage(pageId: string, pfeAppState: PfeUserInputState) {
const state = this._staleState.pages[pageId];
if (state && state.revisit) {
/**
* remove the values from old state
*/
this.removeKeysFromState(
state.staleKeys.filter((key) => !this.contains(state.keys, key)),
pfeAppState
);
state.staleKeys.length = 0;
}
}
private getAllPagesKeys(exclude: string[]): string[] {
const keys: string[] = [];
this.staleForEach((key: string) => {
const state = this._staleState.pages[key];
if (!this.contains(exclude, key)) {
// eslint-disable-next-line prefer-spread
keys.push.apply(
keys,
state.keys.filter((id) => !this.contains(keys, id))
);
}
});
return keys;
}
/**
*
* @param list
* @param key
*/
private contains(list: string[], key: string) {
return list.includes(key);
}
/**
*
* @param pageId
* @param pfeAppState
*/
private removePageFromStaleState(pageId: string, pfeAppState: PfeUserInputState, allPagesKeys: string[]) {
if (Object.prototype.hasOwnProperty.call(this._staleState.pages, pageId)) {
const state = this._staleState.pages[pageId];
const keysToRemove = state.keys.concat(state.staleKeys).reduce((acc: string[], key: string) => {
if (!this.contains(acc, key) && !this.contains(allPagesKeys, key)) {
acc.push(key);
}
return acc;
}, []);
this.removeKeysFromState(keysToRemove, pfeAppState);
delete this._staleState.pages[pageId];
const pageIndexInVisitedPage = pfeAppState.pfeVisitedPages.indexOf(pageId);
/**
* remove pageId values from pfeStateObject
*/
if (-1 < pageIndexInVisitedPage) {
pfeAppState.pfeVisitedPages.splice(pageIndexInVisitedPage, 1);
delete pfeAppState[pageId];
}
}
}
/**
*
* @param key
*/
private addKeyToStale(key: string) {
if (
this.pfeConfigService.pfeApplicationConfiguration &&
this.pfeConfigService.pfeApplicationConfiguration.pfeConfig &&
this.pfeConfigService.pfeApplicationConfiguration.pfeConfig.enableStateCleaning
) {
/**
* remove expression from key
*/
key = (key.split('$.')[1] || key).replace(/(\[)/g, '.').replace(/(])/g, '').replace(/['"]/g, '');
if (
this._staleState.currentPageId &&
!this.contains(PFE_INTERNAL_VALUES, key) &&
!Object.prototype.hasOwnProperty.call(this._staleState.pages, key)
) {
const state = this._staleState.pages[this._staleState.currentPageId];
if (!this.contains(state.keys, key)) {
state.keys.push(key);
}
}
}
}
/**
*
* @param pageId
* @param removeFromStateObj
*/
private removeStaleKeysFromState(pageId: string, removeFromStateObj = false) {
const pfeAppState = this.pfeUserInputState.get();
if (removeFromStateObj) {
const allPagesKeys = this.getAllPagesKeys([pageId]);
this.removePageFromStaleState(pageId, pfeAppState, allPagesKeys);
} else {
this.cleanUpRevisitedPage(pageId, pfeAppState);
}
this.pfeUserInputState.set(pfeAppState);
}
private staleForEach(method: (value: string, index: number, array: string[]) => void) {
Object.keys(this._staleState.pages).forEach(method);
}
/**
* Execute state operations
* @param state
*/
private executeOperationState(state: StateUpdates) {
switch (state.operation) {
case StateOperations.REMOVE:
// Only trigger the remove operation if the value actually exists in the state.
// The background of this is, that parents of a non existing expression would be automatically created in that case.
// As we do not want that to happen, we first check if the value exists.
if (this.getStateValueByExpression(state.key) !== undefined) {
this.storeValueByExpression(state.key, undefined);
}
break;
case StateOperations.PUSH: {
// Create or retrieve list
let object = this.getStateValueByExpression(state.key);
object = !object ? [] : object;
// Retrieve value if state one or use it
let insertVal = state.value;
if (
typeof state.value === 'string' &&
(state.value.startsWith(STATE_EXPRESSION) || state.value.startsWith(ARRAY_STATE_EXPRESSION))
) {
insertVal = this.getStateValueByExpression(state.value);
}
// If we want to push something using spread assignation, we should have an array
if (!Array.isArray(insertVal)) {
insertVal = [insertVal];
}
// Store new list
this.storeValueByExpression(state.key, [...(object as unknown[]), ...insertVal]);
break;
}
case StateOperations.INCREASE: {
const object = this.getStateValueByExpression(state.key);
if (typeof state.value === 'number' && typeof object === 'number') {
this.storeValueByExpression(state.key, object + state.value);
}
break;
}
case StateOperations.DECREASE: {
const object = this.getStateValueByExpression(state.key);
if (typeof state.value === 'number' && typeof object === 'number') {
this.storeValueByExpression(state.key, object - state.value);
}
break;
}
case StateOperations.JOIN: {
const object = this.getStateValueByExpression(state.key);
if (Array.isArray(object)) {
this.storeValueByExpression(state.key, object.join(state.value));
}
break;
}
}
}
}