UNPKG

ayanami

Version:
180 lines (179 loc) 6.97 kB
import { merge, Subject, Subscription, NEVER } from 'rxjs'; import { map, catchError, takeUntil, filter } from 'rxjs/operators'; import mapValues from 'lodash/mapValues'; import produce from 'immer'; import { createState, getEffectActionFactories, getOriginalFunctions } from './utils'; import { logStateAction } from '../redux-devtools-extension'; import { ikariSymbol } from './symbols'; import { TERMINATE_ACTION } from '../ssr/terminate'; import { isSSREnabled } from '../ssr/flag'; function catchRxError() { return catchError((err) => { console.error(err); return NEVER; }); } export function combineWithIkari(ayanami) { const ikari = Ikari.getFrom(ayanami); if (ikari) { return ikari; } else { const { effects, reducers, immerReducers, defineActions } = getOriginalFunctions(ayanami); Object.assign(ayanami, mapValues(defineActions, ({ observable }) => observable)); return Ikari.createAndBindAt(ayanami, { nameForLog: ayanami.constructor.name, defaultState: ayanami.defaultState, effects, reducers, immerReducers, defineActions, effectActionFactories: getEffectActionFactories(ayanami), }); } } export function destroyIkariFrom(ayanami) { const ikari = Ikari.getFrom(ayanami); if (ikari) { ikari.destroy(); Reflect.deleteMetadata(ikariSymbol, ayanami); } } export class Ikari { // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility constructor(ayanami, config) { this.ayanami = ayanami; this.config = config; this.state = createState(this.config.defaultState); this.effectActionFactories = this.config.effectActionFactories; this.triggerActions = {}; this.subscription = new Subscription(); // @internal this.terminate$ = new Subject(); this.log = ({ originalActionName, effectAction, reducerAction }) => { if (effectAction && effectAction !== TERMINATE_ACTION) { logStateAction(this.config.nameForLog, { params: effectAction.params, actionName: `${originalActionName}/👉${effectAction.ayanami.constructor.name}/️${effectAction.actionName}`, }); } if (reducerAction) { logStateAction(this.config.nameForLog, { params: reducerAction.params, actionName: originalActionName, state: reducerAction.nextState, }); } }; this.handleAction = ({ effectAction, reducerAction }) => { if (effectAction) { if (effectAction !== TERMINATE_ACTION) { const { ayanami, actionName, params } = effectAction; combineWithIkari(ayanami).triggerActions[actionName](params); } else { this.terminate$.next(effectAction); } } if (reducerAction) { this.state.setState(reducerAction.nextState); } }; const [effectActions$, effectActions] = setupEffectActions(this.config.effects, this.state.state$); const [reducerActions$, reducerActions] = setupReducerActions(this.config.reducers, this.state.getState); const [immerReducerActions$, immerReducerActions] = setupImmerReducerActions(this.config.immerReducers, this.state.getState); this.triggerActions = { ...effectActions, ...reducerActions, ...immerReducerActions, ...mapValues(this.config.defineActions, ({ next }) => next), }; let effectActionsWithTerminate$; if (!isSSREnabled()) { effectActionsWithTerminate$ = effectActions$; } else { effectActionsWithTerminate$ = effectActions$.pipe(takeUntil(this.terminate$.pipe(filter((action) => action === null)))); } this.subscription.add(effectActionsWithTerminate$.subscribe((action) => { this.log(action); this.handleAction(action); })); this.subscription.add(reducerActions$.subscribe((action) => { this.log(action); this.handleAction(action); })); this.subscription.add(immerReducerActions$.subscribe((action) => { this.log(action); this.handleAction(action); })); } static createAndBindAt(target, config) { const createdIkari = this.getFrom(target); if (createdIkari) { return createdIkari; } else { const ikari = new Ikari(target, config); Reflect.defineMetadata(ikariSymbol, ikari, target); return ikari; } } static getFrom(target) { return Reflect.getMetadata(ikariSymbol, target); } destroy() { this.subscription.unsubscribe(); this.triggerActions = {}; } } function setupEffectActions(effectActions, state$) { const actions = {}; const effects = []; Object.keys(effectActions).forEach((actionName) => { const payload$ = new Subject(); actions[actionName] = (payload) => payload$.next(payload); const effect$ = effectActions[actionName](payload$, state$); effects.push(effect$.pipe(map((effectAction) => ({ effectAction, originalActionName: actionName, })), catchRxError())); }); return [merge(...effects), actions]; } function setupReducerActions(reducerActions, getState) { const actions = {}; const reducers = []; Object.keys(reducerActions).forEach((actionName) => { const reducer$ = new Subject(); reducers.push(reducer$); const reducer = reducerActions[actionName]; actions[actionName] = (params) => { const nextState = reducer(getState(), params); reducer$.next({ reducerAction: { params, actionName, nextState }, originalActionName: actionName, }); }; }); return [merge(...reducers), actions]; } function setupImmerReducerActions(immerReducerActions, getState) { const actions = {}; const immerReducers = []; Object.keys(immerReducerActions).forEach((actionName) => { const immerReducer$ = new Subject(); immerReducers.push(immerReducer$); const immerReducer = immerReducerActions[actionName]; actions[actionName] = (params) => { const nextState = produce(getState(), (draft) => { immerReducer(draft, params); }); immerReducer$.next({ reducerAction: { params, actionName, nextState }, originalActionName: actionName, }); }; }); return [merge(...immerReducers), actions]; }