@equinor/fusion-observable
Version:
156 lines • 5.43 kB
JavaScript
import { asyncScheduler, BehaviorSubject, EMPTY, from, Observable, Subject, } from 'rxjs';
import { catchError, distinctUntilChanged, filter, map, mergeMap, observeOn, scan, } from 'rxjs/operators';
import { filterAction } from './operators';
/**
* A specialized Observable that maintains internal state, which can be mutated by dispatching actions.
* Actions are processed sequentially by a reducer function to produce new state values.
* This class extends from Observable to allow subscribers to react to state changes over time.
*/
export class FlowSubject extends Observable {
/**
* The internal subject for actions.
* @private
*/
#action = new Subject();
/**
* The internal behavior subject for state management.
* @private
*/
#state;
/**
* Observable stream of actions dispatched to the subject.
*/
get action$() {
return this.#action.asObservable();
}
/**
* The current value of state.
*/
get value() {
return this.#state.value;
}
/**
* Flag to indicate if the observable is closed.
*/
get closed() {
return this.#state.closed || this.#action.closed;
}
/**
* Initializes a new instance of the FlowSubject class.
*
* @param reducer A reducer with an initial state or a reducer function.
* @param initialState The initial state of the subject.
*/
constructor(reducer, initialState) {
super((subscriber) => {
return this.#state.subscribe(subscriber);
});
const initial = 'getInitialState' in reducer ? reducer.getInitialState() : initialState;
this.#state = new BehaviorSubject(initial);
this.#action.pipe(scan(reducer, initial), distinctUntilChanged()).subscribe(this.#state);
this.reset = () => this.#state.next(initial);
}
/**
* Resets the state to the initial value.
*/
reset;
/**
* Dispatches an action to the subject.
*
* @param action The action to dispatch.
*/
next(action) {
this.#action.next(action);
}
/**
* Selects a derived state from the observable state and emits only when the selected state changes.
*
* @param selector - A function that takes the current state and returns a derived state.
* @param comparator - An optional function that compares the previous and current derived states to determine if a change has occurred.
* @returns An observable that emits the derived state whenever it changes.
*/
select(selector, comparator) {
return this.#state.pipe(map(selector), distinctUntilChanged(comparator));
}
/**
* Adds an effect that listens for actions and performs side effects.
*
* @param actionTypeOrFn The type of action to listen for, an array of action types, or the effect function itself.
* @param fn The effect function to execute when the action is dispatched, if the first parameter is an action type.
* @returns A subscription to the effect.
*/
addEffect(actionTypeOrFn, fn) {
const action$ = typeof actionTypeOrFn === 'function'
? this.action$
: Array.isArray(actionTypeOrFn)
? this.action$.pipe(filterAction(...actionTypeOrFn))
: this.action$.pipe(filterAction(actionTypeOrFn));
const mapper = (fn ? fn : actionTypeOrFn);
return action$
.pipe(mergeMap((action) => from(new Promise((resolve, reject) => {
try {
resolve(mapper(action, this.value));
}
catch (err) {
reject(err);
}
})).pipe(catchError((err) => {
console.warn('unhandled effect', err);
return EMPTY;
}))), filter((x) => !!x), observeOn(asyncScheduler))
.subscribe(this.#action);
}
/**
* Deprecated. Use `addFlow` instead.
*
* @deprecated use `addFlow`
*
* @param fn The flow function to execute.
* @returns A subscription to the flow.
*/
addEpic(fn) {
return this.addFlow(fn);
}
/**
* Adds a flow that listens for actions and performs side effects.
*
* @param fn The flow function to execute.
* @returns A subscription to the flow.
*/
addFlow(fn) {
const epic$ = fn(this.action$, this);
if (!epic$) {
throw new TypeError(`addEpic: one of the provided effects "${fn.name || '<anonymous>'}" does not return a stream. Double check you're not missing a return statement!`);
}
return epic$
.pipe(catchError((err) => {
console.trace('unhandled exception, epic closed!', err);
return EMPTY;
}), observeOn(asyncScheduler))
.subscribe(this.#action);
}
/**
* Unsubscribes from actions and removes subscribers.
*/
unsubscribe() {
this.#action.unsubscribe();
this.#state.unsubscribe();
}
/**
* Finalizes the subject and completes observers.
*/
complete() {
this.#action.complete();
this.#state.complete();
}
/**
* Clones the subject to a simple observable.
*
* @returns An observable of the state.
*/
asObservable() {
return this.#state.asObservable();
}
}
export default FlowSubject;
//# sourceMappingURL=FlowSubject.js.map