rxjs-loading-state
Version:
Eliminates manual state management for loading and error states by transforming Observables into a LoadingState
161 lines (160 loc) • 4.68 kB
JavaScript
import { BehaviorSubject } from "rxjs";
import { LoadingState } from "./loading-state";
class IllegalStateTransitionError extends Error {
constructor(message) {
super(message);
this.name = "IllegalStateTransitionError";
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
}
}
/**
* Handles transitions between different loading state and holds the context data that is related to the current state.
* @class LoadingStateMachine
*/
export class LoadingStateMachine {
_state$;
_data;
_error;
constructor() {
this._state$ = new BehaviorSubject(LoadingState.NotStarted);
}
/**
* Creates a new observable that represents the current state of the machine
*
* @returns {Observable<LoadingState>} Observable that emits the machine state
*/
asObservable() {
return this._state$.asObservable();
}
/**
* Data of the current state. Depending on the current state, this may be undefined.
*/
get data() {
return this._data;
}
/**
* Error of the current state. Depending on the current state, this may be undefined.
*/
get error() {
return this._error;
}
/**
* The current LoadingState
*/
get state() {
return this._state$.getValue();
}
/**
* Update data while in loading state
* @param {T} newData
*/
update(newData) {
if (!this.isLoading()) {
throw new Error(`Update is only allowed during ${LoadingState.Loading} state`);
}
this._data = newData;
this._state$.next(LoadingState.Loading);
}
/**
* Starts loading
*/
start() {
if (this.isLoading()) {
throw new IllegalStateTransitionError(`Transition from ${this.state} to ${LoadingState.Loading} not allowed`);
}
this._state$.next(LoadingState.Loading);
}
/**
* Transition to success state
* @param {T} data
*/
succeed(data) {
if (!this.isLoading()) {
throw new IllegalStateTransitionError(`Transition from ${this.state} to ${LoadingState.Success} not allowed`);
}
this._data = data;
this._error = undefined;
this._state$.next(LoadingState.Success);
}
/**
* Transition to error state
* @param {any} error
*/
fail(error) {
if (!this.isLoading()) {
throw new IllegalStateTransitionError(`Transition from ${this.state} to ${LoadingState.Error} not allowed`);
}
this._data = undefined;
this._error = error;
this._state$.next(LoadingState.Error);
}
/**
* Resets machine to not started
*/
reset() {
if (this.isNotStarted()) {
throw new IllegalStateTransitionError(`Transition from ${this.state} to ${LoadingState.NotStarted} not allowed`);
}
this._error = undefined;
this._data = undefined;
this._state$.next(LoadingState.NotStarted);
}
/**
* @returns {Boolean} True if machine if loading has not been started or reset
*/
isNotStarted() {
return this.state === LoadingState.NotStarted;
}
/**
* @returns {Boolean} True if machine is in loading state
*/
isLoading() {
return this.state === LoadingState.Loading;
}
/**
*
* @returns {Boolean} True if machine is in error state
*/
isError() {
return this.state === LoadingState.Error;
}
/**
* @returns {Boolean} True if machine is in success state
*/
isSuccess() {
return this.state === LoadingState.Success;
}
/**
* Factory to create a new machine in error state
* @param {any} error
* @returns {LoadingStateMachine<T>} The new LoadingStateMachine
*/
static asError(error) {
const state = new LoadingStateMachine();
state.start();
state.fail(error);
return state;
}
/**
* Factory to create a new machine in success state
* @param {T} data
* @returns {LoadingStateMachine<T>} The new LoadingStateMachine
*/
static asSuccess(data) {
const state = new LoadingStateMachine();
state.start();
state.succeed(data);
return state;
}
/**
* Factory to create a new machine in loading state
* @param {T | undefined} data
* @returns {LoadingStateMachine<T>} The new LoadingStateMachine
*/
static asLoading(data = undefined) {
const state = new LoadingStateMachine();
state.start();
state.update(data);
return state;
}
}