You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

304 lines
14 KiB

import { BehaviorSubject, Observable } from 'rxjs';
import { ObservableStoreSettings, StateHistory, ObservableStoreGlobalSettings, StateWithPropertyChanges, ObservableStoreExtension } from './interfaces';
import ObservableStoreBase from './observable-store-base';
/**
* Executes a function on `state` and returns a version of T
* @param state - the original state model
*/
export type stateFunc<T> = (state: T) => Partial<T>;
/**
* Core functionality for ObservableStore
* providing getState(), setState() and additional functionality
*/
export class ObservableStore<T> {
// Not a fan of using _ for private fields in TypeScript, but since
// some may use this as pure ES2015 I'm going with _ for the private fields.
// private _stateDispatcher$ = new BehaviorSubject<T>(null);
private _settings: ObservableStoreSettings;
private _stateDispatcher$ = new BehaviorSubject<T>(null);
private _stateWithChangesDispatcher$ = new BehaviorSubject<StateWithPropertyChanges<T>>(null);
/**
* Subscribe to store changes in the particlar slice of state updated by a Service.
* If the store contains 'n' slices of state each being managed by one of 'n' services, then changes in any
* of the other slices of state will not generate values in the `stateChanged` stream.
* Returns an RxJS Observable containing the current store state (or a specific slice of state if a `stateSliceSelector` has been specified).
*/
stateChanged: Observable<T>;
/**
* Subscribe to store changes in the particlar slice of state updated by a Service and also include the properties that changed as well.
* Upon subscribing to `stateWithPropertyChanges` you will get back an object containing state (which has the current slice of store state)
* and `stateChanges` (which has the individual properties/data that were changed in the store).
*/
stateWithPropertyChanges: Observable<StateWithPropertyChanges<T>>;
/**
* Subscribe to global store changes i.e. changes in any slice of state of the store. The global store may consist of 'n'
* slices of state each managed by a particular service. This property notifies of a change in any of the 'n' slices of state.
* Returns an RxJS Observable containing the current store state.
*/
globalStateChanged: Observable<T>;
/**
* Subscribe to global store changes i.e. changes in any slice of state of the store and also include the properties that changed as well.
* The global store may consist of 'n' slices of state each managed by a particular service.
* This property notifies of a change in any of the 'n' slices of state. Upon subscribing to `globalStateWithPropertyChanges` you will get
* back an object containing state (which has the current store state) and `stateChanges`
* (which has the individual properties/data that were changed in the store).
*/
globalStateWithPropertyChanges: Observable<StateWithPropertyChanges<T>>;
/**
* Retrieve state history. Assumes trackStateHistory setting was set on the store.
*/
get stateHistory(): StateHistory<T>[] {
return ObservableStoreBase.stateHistory;
}
constructor(settings: ObservableStoreSettings) {
this._settings = { ...ObservableStoreBase.settingsDefaults, ...settings, ...ObservableStoreBase.globalSettings };
this.stateChanged = this._stateDispatcher$.asObservable();
this.globalStateChanged = ObservableStoreBase.globalStateDispatcher.asObservable();
this.stateWithPropertyChanges = this._stateWithChangesDispatcher$.asObservable();
this.globalStateWithPropertyChanges = ObservableStoreBase.globalStateWithChangesDispatcher.asObservable();
ObservableStoreBase.services.push(this);
}
/**
* get/set global settings throughout the application for ObservableStore.
* See the [Observable Store Settings](https://github.com/danwahlin/observable-store#store-settings-per-service) documentation
* for additional information. Note that global settings can only be set once as the application first loads.
*/
static get globalSettings() {
return ObservableStoreBase.globalSettings;
}
static set globalSettings(settings: ObservableStoreGlobalSettings) {
// ObservableStore['isTesting'] used so that unit tests can set globalSettings
// multiple times during a suite of tests
if (settings && (ObservableStore['isTesting'] || !ObservableStoreBase.globalSettings)) {
ObservableStoreBase.globalSettings = settings;
}
else if (!settings) {
throw new Error('Please provide the global settings you would like to apply to Observable Store');
}
else if (settings && ObservableStoreBase.globalSettings) {
throw new Error('Observable Store global settings may only be set once when the application first loads.');
}
}
/**
* Provides access to all services that interact with ObservableStore. Useful for extensions
* that need to be able to access a specific service.
*/
static get allStoreServices() {
return ObservableStoreBase.services;
}
/**
* Used to add an extension into ObservableStore. The extension must implement the
* `ObservableStoreExtension` interface.
*/
static addExtension(extension: ObservableStoreExtension) {
ObservableStoreBase.addExtension(extension);
}
/**
* Used to initialize the store's starting state. An error will be thrown if this is called and store state already exists.
* No notifications are sent out when the store state is initialized by calling initializeStoreState().
*/
static initializeState(state: any) {
ObservableStoreBase.initializeState(state);
}
/**
* Used to reset the state of the store to a desired value for all services that derive
* from ObservableStore<T> to a desired value.
* A state change notification and global state change notification is sent out to subscribers if the dispatchState parameter is true (the default value).
*/
static resetState(state: any, dispatchState: boolean = true) {
ObservableStoreBase.setStoreState(state);
if (dispatchState) {
ObservableStore.dispatchToAllServices(state);
}
}
/**
* Clear/null the store state for all services that use it.
*/
static clearState(dispatchState: boolean = true) {
ObservableStoreBase.clearStoreState();
if (dispatchState) {
ObservableStore.dispatchToAllServices(null);
}
}
/**
* Determines if the ObservableStore has already been initialized through the use of ObservableStore.initializeState()
*/
static get isStoreInitialized(): boolean {
return ObservableStoreBase.isStoreInitialized;
}
private static dispatchToAllServices(state: any) {
const services = ObservableStore.allStoreServices;
if (services) {
for (const service of services) {
service.dispatchState(state);
}
}
}
/**
* Retrieve store's state. If using TypeScript (optional) then the state type defined when the store
* was created will be returned rather than `any`. The deepCloneReturnedState boolean parameter (default is true) can be used
* to determine if the returned state will be deep cloned or not. If set to false, a reference to the store state will
* be returned and it's up to the user to ensure the state isn't change from outside the store. Setting it to false can be
* useful in cases where read-only cached data is stored and must be retrieved as quickly as possible without any cloning.
*/
protected getState(deepCloneReturnedState: boolean = true): T {
return this._getStateOrSlice(deepCloneReturnedState);
}
/**
* Retrieve a specific property from the store's state which can be more efficient than getState()
* since only the defined property value will be returned (and cloned) rather than the entire
* store value. If using TypeScript (optional) then the generic property type used with the
* function call will be the return type.
*/
protected getStateProperty<TProp>(propertyName: string, deepCloneReturnedState: boolean = true): TProp {
return ObservableStoreBase.getStoreState(propertyName, deepCloneReturnedState);
}
/**
* Retrieve a specific property from the store's state which can be more efficient than getState()
* since only the defined property value will be returned (and cloned) rather than the entire
* store value. If using TypeScript (optional) then the generic property type used with the
* function call will be the return type.
* If a `stateSliceSelector` has been set, the specific slice will be searched first.
*/
protected getStateSliceProperty<TProp>(propertyName: string, deepCloneReturnedState: boolean = true): TProp {
if (this._settings.stateSliceSelector) {
const state = this._getStateOrSlice(deepCloneReturnedState);
if (state.hasOwnProperty(propertyName)) {
return state[propertyName];
}
}
return null;
}
/**
* Set store state. Pass the state to be updated as well as the action that is occuring.
* The state value can be a function [(see example)](https://github.com/danwahlin/observable-store#store-api).
* The latest store state is returned.
* The dispatchState parameter can be set to false if you do not want to send state change notifications to subscribers.
* The deepCloneReturnedState boolean parameter (default is true) can be used
* to determine if the state will be deep cloned before it is added to the store. Setting it to false can be
* useful in cases where read-only cached data is stored and must added to the store as quickly as possible without any cloning.
*/
protected setState(state: Partial<T> | stateFunc<T>,
action?: string,
dispatchState: boolean = true,
deepCloneState: boolean = true): T {
// Needed for tracking below (don't move or delete)
const previousState = this.getState(deepCloneState);
switch (typeof state) {
case 'function':
const newState = state(this.getState(deepCloneState));
this._updateState(newState, deepCloneState);
break;
case 'object':
this._updateState(state, deepCloneState);
break;
default:
throw Error('Pass an object or a function for the state parameter when calling setState().');
}
if (this._settings.trackStateHistory) {
ObservableStoreBase.stateHistory.push({
action,
beginState: previousState,
endState: this.getState(deepCloneState)
});
}
if (dispatchState) {
this.dispatchState(state as any);
}
if (this._settings.logStateChanges) {
const caller = (this.constructor) ? '\r\nCaller: ' + this.constructor.name : '';
console.log('%cSTATE CHANGED', 'font-weight: bold', '\r\nAction: ', action, caller, '\r\nState: ', state);
}
return this.getState(deepCloneState);
}
/**
* Add a custom state value and action into the state history. Assumes `trackStateHistory` setting was set
* on store or using the global settings.
*/
protected logStateAction(state: any, action: string) {
if (this._settings.trackStateHistory) {
ObservableStoreBase.stateHistory.push({
action,
beginState: this.getState(),
endState: ObservableStoreBase.deepClone(state)
});
}
}
/**
* Reset the store's state history to an empty array.
*/
protected resetStateHistory() {
ObservableStoreBase.stateHistory = [];
}
private _updateState(state: Partial<T>, deepCloneState: boolean) {
ObservableStoreBase.setStoreState(state, deepCloneState);
}
private _getStateOrSlice(deepCloneReturnedState: boolean): Readonly<Partial<T>> {
const storeState = ObservableStoreBase.getStoreState(null, deepCloneReturnedState);
if (this._settings.stateSliceSelector) {
return this._settings.stateSliceSelector(storeState);
}
return storeState;
}
/**
* Dispatch the store's state without modifying the store state. Service state can be dispatched as well as the global store state.
* If `dispatchGlobalState` is false then global state will not be dispatched to subscribers (defaults to `true`).
*/
protected dispatchState(stateChanges: Partial<T>, dispatchGlobalState: boolean = true) {
// Get store state or slice of state
const clonedStateOrSlice = this._getStateOrSlice(true);
// Get full store state
const clonedGlobalState = ObservableStoreBase.getStoreState();
// includeStateChangesOnSubscribe is deprecated
if (this._settings.includeStateChangesOnSubscribe) {
console.warn('includeStateChangesOnSubscribe is deprecated. ' +
'Subscribe to stateChangedWithChanges or globalStateChangedWithChanges instead.');
this._stateDispatcher$.next({ state: clonedStateOrSlice, stateChanges } as any);
ObservableStoreBase.globalStateDispatcher.next({ state: clonedGlobalState, stateChanges });
}
else {
this._stateDispatcher$.next(clonedStateOrSlice);
this._stateWithChangesDispatcher$.next({ state: clonedStateOrSlice, stateChanges });
if (dispatchGlobalState) {
ObservableStoreBase.globalStateDispatcher.next(clonedGlobalState);
ObservableStoreBase.globalStateWithChangesDispatcher.next({ state: clonedGlobalState, stateChanges })
};
}
}
}