ayanami
Version:
A better way to react with state
180 lines (179 loc) • 6.97 kB
JavaScript
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];
}