UNPKG

vuse-rx

Version:

First-class rxjs support for Vue 3

259 lines (225 loc) 7.79 kB
import type { Observable, PartialObserver, Subscription } from 'rxjs'; import type { DeepReadonly, UnwrapNestedRefs } from 'vue'; import { isObservable, merge, of, Subject } from 'rxjs'; import { map, mergeScan, scan, tap } from 'rxjs/operators'; import { reactive, readonly } from 'vue'; import { untilUnmounted } from '../operators/until'; import { MutationStrategy } from './strategies/common'; import { defaultBuiltin, deepReplaceBuiltin, DeepReplaceBuiltinMutation } from './strategies/deepReplaceBuiltin'; // #region options-type export interface RxStateOptions<S extends Record<PropertyKey, any>, Mutaiton> { mutationStrategy?: MutationStrategy<S, Mutaiton>; strategyContext?: any; } // #endregion options-type export const defaultOptions = { mutationStrategy: deepReplaceBuiltin, strategyContext: defaultBuiltin }; /** * Allows to bind reducers to a state and an observable. * * First accepts state's default value and a state merge strategy, * then accepts a map of state reducers. * * @param initialState - a factory or initial value for the reactive state * @param options - options to customize the behavior, for example - to apply a custom strategy of merging a mutation with an old state */ export function useRxState<T extends Record<PropertyKey, any>, Mutation = DeepReplaceBuiltinMutation<UnwrapNestedRefs<T>>>( initialState: T | (() => T), options?: Partial<RxStateOptions<UnwrapNestedRefs<T>, Mutation>> ): CreateRxState<UnwrapNestedRefs<T>, Mutation> { const { mutationStrategy, strategyContext } = { ...defaultOptions as RxStateOptions<UnwrapNestedRefs<T>, Mutation>, ...options }; const mergeKeys = mutationStrategy!.bind(strategyContext); return function (reducers, map$?) { type S = UnwrapNestedRefs<T>; type ReducerResult = ReturnType<StateReducer<S, Mutation>>; type Actions = ReducerActions<typeof reducers>; const state = reactive(callMeMaybe(initialState)); const actions = <Actions> {}; const actions$ = <ReducerObservables<Actions, S>> {}; const actions$Arr = <Observable<S>[]> []; let complete = false; let error: any = undefined; const context: MutationContext = { error: e => { error = e; }, complete: () => { complete = true; } }; for (const key in reducers) { const mutations$ = new Subject<ReducerResult>(); actions[key] = <ReducerAction<typeof reducers[typeof key]>>( (...args: any[]) => mutations$.next( reducers[key].apply(reducers, args) ) ); actions$Arr.push( actions$[`${key}$` as const] = ( mergeScan((prev: S, curr: ReducerResult) => { curr = callMeMaybe(curr, prev, context); return ( isObservable(curr) ? curr : of(curr) ).pipe( map(mergeKeys(prev, mergeKeys)), tap({ next: () => error ? error = mutations$.error(error) : complete && mutations$.complete() }) ) }, state)(mutations$) ) ); } const merged$ = merge(...actions$Arr); return createRxResult({ actions, state: readonly(state as T), state$: untilUnmounted( map$ ? map$( merged$, reducers, state, actions$, context, ).pipe( scan((prev, curr) => mergeKeys(prev, mergeKeys)(curr), state) ) : merged$ ), actions$: actions$ as ReducerObservables<Actions, DeepReadonly<S>>, }); }; } /** * Extracts state type from the result of the first useRxState call, * so that the state type can be defined declaratively from the object itself to use later. * * @example ```ts * const countState = useRxState({ count: 0 }); * type CountState = State<typeof countState>; * // CountState = { count: number } * ``` */ export type State<C extends CreateRxState<any, any>> = C extends CreateRxState<infer S, any> ? S : never; type CreateRxState<S, Mutation> = { /** * Allows to bind reducers to a state and an observable. * * Accepts a map of state reducers. * * Each reducer can either return: * * an updated part of the state: * ``` * (v) => ({ value: v }) * ``` * * an observable that emits an updated part of the state: * ``` * (v) => new BehaviorSubject({ value: v }) * ``` * * a function that accepts the old state and returns either of the previous types: * ``` * (v) => (oldState) => ({ * value: oldState.value > v ? oldState.value : v * }) * ``` * * @param reducers a map of reducers to mutate the state * @param map$ a function to modify the resulting observable */ <R extends StateReducers<S, Mutation>>( reducers: R, map$?: ( state$: Observable<S>, reducers: R, state: S, actions$: Record<Action$<Extract<keyof R, string>>, Observable<S>>, context: MutationContext ) => Observable<Mutation> ): SubscribableRxResult<ReducerActions<R>, S>; }; const createRxResult = <S, Actions>(result: { actions: Actions, state: DeepReadonly<S>, state$: Observable<S>, actions$: ReducerObservables<Actions, DeepReadonly<S>> }): SubscribableRxResult<Actions, S> => ({ ...result, subscribe: (...args: any[]) => ({ ...result, subscription: result.state$.subscribe(...args), }), }) const callMeMaybe = <T, A extends any[]>( fn: T | ((...args: A) => T), ...args: A ) => ( typeof fn === 'function' ? (fn as (...args: A) => T)(...args) : fn ); /** * Context of a mutation. * * It is used to inform observables about errors or make them complete * from within the reducers. */ export interface MutationContext { error(error: any): void; complete(): void; } export type StatefulMutation<S, Mutation> = (state: S, mutation?: MutationContext) => Mutation | Observable<Mutation>; /** * A general type for a reducer of the observable state */ export type StateReducer<S, Mutation = DeepReplaceBuiltinMutation<S>, Args extends any[] = any[]> = ( (...args: Args) => | Mutation | Observable<Mutation> | StatefulMutation<S, Mutation> ); /** * A simple stateful reducer type that is returned by a stateful reducer upon the first call */ export type SimpleReducer<S> = (state: S) => Partial<S>; /** * A named collection of state reducers */ export type StateReducers<S, Mutation> = Record<string, StateReducer<S, Mutation>>; /** * A method action generated from a StateReducer */ export type ResAction<A extends any[] = []> = (...args: A) => void; type Action$<Name extends string> = `${Name}$`; type ReducerObservables<H, R> = { [key in Action$<Extract<keyof H, string>>]: Observable<R>; }; /** * Resulting RX bindings: * * * actions - a named collection of ResAction-s * * state - a reactive vue state * * state$ - an rxjs observable */ export interface RxResult<Actions, State> { readonly actions: Actions; readonly state: DeepReadonly<State>; readonly state$: Observable<State>; readonly actions$: ReducerObservables<Actions, DeepReadonly<State>>; }; export interface SubscribableRxResult<Actions, State> extends RxResult<Actions, State> { readonly subscribe: { (observer?: PartialObserver<State>): SubscriptionRxResult<Actions, State>; (...args: Parameters<Observable<State>['subscribe']>): SubscriptionRxResult<Actions, State>; }; }; export interface SubscriptionRxResult<Actions, State> extends RxResult<Actions, State> { readonly subscription: Subscription; } type ReducerAction<R> = R extends StateReducer<any, any, infer Args> ? ResAction<Args> : never; type ReducerActions<R> = { [key in keyof R]: ReducerAction<R[key]> };