File

libs/ngx-pfe/services/pfe-state-service/state.service.ts

Index

Properties

Properties

pageId
pageId: string
Type : string
removePageFromState
removePageFromState: string[]
Type : string[]
Optional
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;
      }
    }
  }
}

results matching ""

    No results matching ""