import { iif, Observable, of } from 'rxjs' import { distinctUntilChanged, filter, map, mergeMap, switchMap, takeWhile, tap } from 'rxjs/operators' import { assert, isArray, isFunction, isObject, isString } from '../helpers' import { BaseStore } from './base-store' import { StoreConfigOptions } from './config' import { Actions, ProjectsOrKeys, QueryableStore, QueryContext, WriteableStore } from './store-accessories' /** * @example * const createWeatherState: WeatherState = () => { * return { * isRaining: true, * isSunny: false, * precipitation: { * mm: 7 * } * } * } * * `@`StoreConfig({ // <= decorator * name: 'WEATHER-STORE' * }) * export class WeatherStore extends Store { * * constructor() { * super(createWeatherState()) * } * } */ export class Store extends BaseStore implements QueryableStore, WriteableStore { protected readonly _whenLoaded$: Observable | null> = this.isLoading$ .pipe( filter(x => !x), switchMap(() => this._value$), ) //#region constructor /** * Synchronous initialization. */ constructor(initialValue: T, storeConfig?: StoreConfigOptions) /** * Asynchronous or delayed initialization. * The store will be set into loading state till initialization. */ constructor(initialValue: null, storeConfig?: StoreConfigOptions) /** * Dynamic initialization. */ constructor(initialValue: T | null, storeConfig?: StoreConfigOptions) constructor(initialValueOrNull: T | null, storeConfig?: StoreConfigOptions) { super(initialValueOrNull, storeConfig) } //#endregion constructor //#region query-methods protected _select$( projectsOrKeys?: ProjectsOrKeys, action?: Actions | string ): Observable> { const mapProject: (value: Readonly) => T | R | any[] | T[K] | Pick = (() => { if (isArray(projectsOrKeys) && projectsOrKeys.length) { if ((<((value: Readonly) => R)[]>projectsOrKeys).every(x => isFunction(x))) { return (value: Readonly) => (<((value: Readonly) => R)[]>projectsOrKeys).map(x => x(value)) } if ((projectsOrKeys).every(x => isString(x))) { return (value: Readonly) => { const result = {}; (projectsOrKeys).forEach((x: string) => result[x] = value[x]) return result } } } if (isString(projectsOrKeys)) return (value: Readonly) => value[projectsOrKeys as string] if (isFunction(projectsOrKeys)) return projectsOrKeys return (x: Readonly) => x })() const takeWhilePredicate = () => { return !queryContext.isDisposed } const actionFilterPredicate = () => !action || action === this._lastAction const iffCondition = () => this.isLoading && action != Actions.loading const mainFilterPredicate = (value: Readonly | null): value is Readonly => { return !this.isPaused && !queryContext.isDisposed && !!value } const queryContext: QueryContext = { wasHardReset: false, isDisposed: false, observable: this._value$.asObservable() .pipe( takeWhile(takeWhilePredicate), filter(actionFilterPredicate), mergeMap(x => iif(iffCondition, this._whenLoaded$, of(x))), filter(mainFilterPredicate), map(mapProject), distinctUntilChanged((prev, curr) => { if (queryContext.wasHardReset) return false return (isObject(prev) && isObject(curr)) ? this._compare(prev, curr) : prev === curr }), tap(() => queryContext.wasHardReset = false), map(x => isObject(x) ? this._clone(x) : x), ) } this._queryContextsList.push(queryContext) return queryContext.observable } protected _update( valueOrFunction: ((value: Readonly) => Partial) | Partial | T, isMerge: boolean, actionName: string, onUpdate?: (nextState: T, prevState: Readonly) => void | T, ): void { if (this.isPaused) return assert(this.isInitialized, `Store: "${this._storeName}" can't be updated before it was initialized`) this._setState(value => { valueOrFunction = isFunction(valueOrFunction) ? valueOrFunction(value) : valueOrFunction const tmpValue = valueOrFunction let newValue: T = isMerge ? this._merge(this._clone(value), this._clone(valueOrFunction)) : valueOrFunction as T if (this._isClassHandler) { assert(!!this._instancedValue, `Store: "${this._storeName}" instanced handler is configured but an instanced value was not provided.`) newValue = this._handleClasses(this._instancedValue, this._clone(newValue)) } if (isFunction(onUpdate)) { const newModifiedValue: T | void = onUpdate(this._clone(newValue), value) if (newModifiedValue) newValue = this._clone(newModifiedValue) } return tmpValue == newValue ? this._clone(newValue) : newValue }, actionName) } /** * Returns the state's value as an Observable. * @example * weatherStore.select$().subscribe(value => { * // do something with the weather... * }); */ public select$(): Observable /** * Returns the extracted partial state's value as an Observable based on the provided projection method. * @example * weatherStore.select$(value => value.isRaining) * .subscribe(isRaining => { * if (isRaining) { * // do something when it's raining... * } * }); */ public select$(project: (value: Readonly) => R): Observable /** * Returns an array values as an Observable based on the provided projections methods. * @example * weatherStore.select$([value => value.isRaining, value => value.precipitation]) * .subscribe(result => { * const [isRaining, precipitation] = result * // do something... * }); */ public select$, M extends ((value: Readonly) => any)>(projects: M[]): Observable /** * Returns an array values as an Observable based on the provided projections methods. * - This overload allows to manually define the return type. * @example * weatherStore.select$([value => value.isRaining, value => value.precipitation]) * .subscribe(result => { * const [isRaining, precipitation] = result * // do something... * }); */ public select$(projects: ((value: Readonly) => any)[]): Observable /** * Returns as single the state's value property based on the provided key. * @example * weatherStore.select$('precipitation') * .subscribe(precipitation => { * // do something... * }); */ public select$(key: K): Observable /** * Returns the extracted partial state's value as an Observable based on the provided keys. * @example * weatherStore.select$(['precipitation', 'isRaining']) * .subscribe(result => { * if (result.isRaining && result.precipitation.mm > 10) { * // do something... * } * }); */ public select$(keys: K[]): Observable> /** * This is an dynamic overload. Use this approach only id necessary because it's not strongly typed. * @example * function selectFactory(dynamic?) { * return weatherStore.select$(dynamic?); * } */ public select$(dynamic?: ProjectsOrKeys): Observable public select$(projectsOrKeys?: ProjectsOrKeys): Observable> { return this._select$(projectsOrKeys) } public onAction(action: Actions | string): Pick, 'select$'> { return { select$: (projectsOrKeys?: ProjectsOrKeys) => this._select$(projectsOrKeys, action) } } //#endregion query-methods //#region write-methods /** * Updates the store's state using a partial state. The provided partial state will be then merged with current store's state. * - If you want to override the current store's state, use the `override` method. * @example * weatherStore.update({ isWindy: true }); */ public update(value: Partial, actionName?: string): void /** * Updates the store's state using a function that will be called by the store. * The function will be provided with the current state as a parameter and it must return a new partial state. * The returned partial state will be then merged with current store's state. * - If you want to override the current store's state, use the `override` method. * @example * weatherStore.update(state => ({ * isSunny: !state.isRaining * })); */ public update(valueFunction: (value: Readonly) => Partial, actionName?: string): void public update(valueOrFunction: ((value: Readonly) => Partial) | Partial, actionName?: string): void { this._update(valueOrFunction, /** isMerge */ true, actionName || Actions.update, this.onUpdate) } /** * Overrides the current state's value completely. */ public override(newValue: T, actionName?: string): void { this._update(newValue, /** isMerge */ false, actionName || Actions.override, this.onOverride) } //#endregion write-methods }