Skip to content

Instantly share code, notes, and snippets.

@flensrocker
Created January 22, 2022 13:38
Show Gist options
  • Save flensrocker/c9c8e047c5186219208898e6ed369065 to your computer and use it in GitHub Desktop.
Save flensrocker/c9c8e047c5186219208898e6ed369065 to your computer and use it in GitHub Desktop.
Simple reactive state management
import { Observable, BehaviorSubject, Subject, Subscription, queueScheduler } from "rxjs";
import { distinctUntilChanged, map, observeOn, scan, shareReplay, takeUntil, tap } from "rxjs/operators";
export type BsStoreAction<TState> = (lastState: TState) => TState;
export type BsStoreDispatcher<TState> = (...actions: BsStoreAction<TState>[]) => void;
export type BsStoreSelector<TState> = <R>(projection: (state: TState) => R) => Observable<R>;
export type BsStoreEffect<TState> = Observable<BsStoreAction<TState>>;
export interface BsStore<TState> {
readonly stateChanges: Observable<TState>;
readonly dispatch: BsStoreDispatcher<TState>;
readonly select: BsStoreSelector<TState>;
readonly addEffect: (effect: BsStoreEffect<TState>) => Subscription;
}
export const identityAction = <TState>(lastState: TState): TState => lastState;
export const composeActions = <TState>(...actions: BsStoreAction<TState>[]): BsStoreAction<TState> => {
if (actions == null || actions.length === 0) {
return identityAction;
}
if (actions.length === 1) {
return actions[0];
}
return (lastState: TState) => actions.reduce((state, action) => action(state), lastState);
};
export class BsRootStore<TState> implements BsStore<TState> {
private readonly _actions = new BehaviorSubject<BsStoreAction<TState>>(identityAction);
public readonly stateChanges: Observable<TState>;
public readonly dispatch: BsStoreDispatcher<TState>;
public readonly select: BsStoreSelector<TState>;
public readonly addEffect: (effect: BsStoreEffect<TState>) => Subscription;
public logState = false;
constructor(initialState: TState, onDestroy: Observable<unknown>) {
this.stateChanges = this._actions.pipe(
observeOn(queueScheduler),
scan((lastState, action) => action(lastState), initialState),
tap(state => {
if (this.logState) {
console.log("state:", state);
}
}),
takeUntil(onDestroy),
shareReplay({ bufferSize: 1, refCount: true })
);
this.dispatch = (...actions: BsStoreAction<TState>[]) => this._actions.next(composeActions(...actions));
this.select = <R>(projection: (state: TState) => R): Observable<R> => this.stateChanges.pipe(map(projection), distinctUntilChanged());
this.addEffect = (effect: BsStoreEffect<TState>): Subscription =>
effect.pipe(takeUntil(onDestroy)).subscribe({
next: this.dispatch,
error: err => console.error("unexpected error from effect:", err),
});
// heartbeat
this.stateChanges.pipe(takeUntil(onDestroy)).subscribe();
}
}
export const updateFeature = <TParentState, TFeature extends keyof TParentState>(
lastState: TParentState,
feature: TFeature,
action: BsStoreAction<TParentState[TFeature]>
): TParentState => {
if (lastState == null || feature == null || action == null) {
return lastState;
}
return {
...lastState,
[feature]: action(lastState[feature]),
};
};
const createFeatureDispatcher = <TParentState, TFeature extends keyof TParentState>(
store: BsStore<TParentState>,
feature: TFeature
): BsStoreDispatcher<TParentState[TFeature]> => {
return (...actions) => store.dispatch(s => updateFeature(s, feature, composeActions(...actions)));
};
export class BsFeatureStore<TParentState, TFeature extends keyof TParentState> implements BsStore<TParentState[TFeature]> {
public readonly stateChanges: Observable<TParentState[TFeature]>;
public readonly dispatch: BsStoreDispatcher<TParentState[TFeature]>;
public readonly select: BsStoreSelector<TParentState[TFeature]>;
public readonly addEffect: (effect: BsStoreEffect<TParentState[TFeature]>) => Subscription;
constructor(parentStore: BsStore<TParentState>, feature: TFeature) {
const onDestroy = new Subject<true>();
parentStore.stateChanges.subscribe({
error: _ => {
onDestroy.next(true);
onDestroy.complete();
},
complete: () => {
onDestroy.next(true);
onDestroy.complete();
},
});
this.stateChanges = parentStore.select(s => s[feature]);
this.dispatch = createFeatureDispatcher(parentStore, feature);
this.select = projection => this.stateChanges.pipe(map(projection), distinctUntilChanged());
this.addEffect = effect =>
effect.pipe(takeUntil(onDestroy)).subscribe({
next: this.dispatch,
error: err => console.error("unexpected error from effect:", err),
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment