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;
}
}
}
}