mAAdhaTTah/brookjs

View on GitHub
packages/brookjs-eddy/src/eddy.ts

Summary

Maintainability
A
35 mins
Test Coverage
import {
StoreCreator as ReduxStoreCreator,
Reducer,
StoreEnhancer,
Store,
Action,
createStore as reduxCreateStore,
applyMiddleware,
PreloadedState,
} from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
import { Delta } from 'brookjs-types';
import { observeDelta } from './observeDelta';
 
const $$loop = Symbol('@brookjs/loop');
const NONE = Symbol('@brookjs/none');
 
export type Dispatchable<A extends Action> = A | typeof NONE;
export type ResultRight<A extends Action> = Dispatchable<A> | Dispatchable<A>[];
export type Result<L, A extends Action> = [L, ResultRight<A>] & {
[$$loop]: true;
};
 
export type EddyReducer<S, A extends Action> =
| ((state: S | undefined, action: A) => S | Result<S, A>)
| ((state: S, action: A) => S | Result<S, A>);
 
const isResult = <S, A extends Action>(results: any): results is Result<S, A> =>
results[$$loop] === true;
 
export const loop = <S, A extends Action>(
state: S,
action: ResultRight<A>,
): Result<S, A> =>
Object.assign([state, action] as [S, A], {
[$$loop]: true as true,
});
 
loop.NONE = NONE;
 
const normalizeResults = <S, A extends Action>(
results: S | Result<S, A>,
): Result<S, A> => (isResult(results) ? results : (loop(results, NONE) as any));
 
type HandleCmd<A extends Action> = (cmd: A) => void;
 
const iterateCmd = <A extends Action>(
cmd: ResultRight<A>,
handleCmd: HandleCmd<A>,
) => {
if (cmd !== loop.NONE) {
if (Array.isArray(cmd)) {
cmd.forEach(c => iterateCmd(c, handleCmd));
} else {
handleCmd(cmd);
}
}
};
 
export const upgradeReducer = <L, A extends Action>(
reducer: (...args: any[]) => L | Result<L, A>,
handleCmd: HandleCmd<A>,
): ((...args: Parameters<typeof reducer>) => L) => (...args) => {
const [nextState, cmd] = normalizeResults(reducer(...args));
 
iterateCmd(cmd, handleCmd);
 
return nextState;
};
 
Function `runCommands` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring.
const runCommands = <A extends Action>(
run: ResultRight<A>[],
dispatch: (action: A) => A,
) => {
for (const cmd of run) {
if (cmd === NONE) {
continue;
}
 
if (Array.isArray(cmd)) {
runCommands(cmd, dispatch);
} else {
dispatch(cmd);
}
}
};
 
export const eddy = () => (createStore: ReduxStoreCreator) => <
S,
A extends Action,
Ext,
StateExt
>(
reducer: Reducer<S, A> | EddyReducer<S, A>,
state?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
enhancer?: StoreEnhancer<Ext, StateExt>,
): Store<S & StateExt, A> & Ext => {
let queue: ResultRight<A>[] = [];
const handleCmd = (cmd: A) => queue.push(cmd);
 
if (typeof state === 'function') {
enhancer = state as StoreEnhancer<Ext, StateExt>;
state = undefined;
}
 
const store = createStore(
upgradeReducer(reducer, handleCmd),
state,
enhancer,
);
 
const dispatch = (action: A) => {
store.dispatch(action);
const run = queue;
queue = [];
 
runCommands(run, dispatch);
 
return action;
};
 
const replaceReducer = (reducer: Reducer<S, A> | EddyReducer<S, A>) =>
store.replaceReducer(upgradeReducer(reducer, handleCmd));
 
return {
...store,
dispatch,
replaceReducer,
} as any;
};
 
type ReducerMapObject<S, A extends Action> = {
[K in keyof S]: EddyReducer<S[K], A>;
};
 
export interface LiftedLoopReducer<S, A extends Action> {
(state: S | undefined, action: A): Result<S, A>;
}
 
export const combineReducers = <S, A extends Action>(
reducerMap: ReducerMapObject<S, A>,
): LiftedLoopReducer<S, A> => {
const reducerKeys = Object.keys(reducerMap) as (keyof S)[];
 
return (state: S = {} as S, action: A): Result<S, A> => {
let hasChanged = false;
const nextState = {} as S;
const cmds: ResultRight<A> = [];
 
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
const reducer = reducerMap[key];
const previousStateForKey = state[key];
const [nextStateForKey, cmd] = normalizeResults(
reducer(previousStateForKey, action),
);
 
if (cmd !== NONE) {
cmds.push(...(Array.isArray(cmd) ? cmd : [cmd]));
}
 
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
nextState[key] = nextStateForKey;
}
 
return loop(hasChanged ? nextState : state, cmds);
};
};
 
export const createStore = <S, A extends Action<string>>(
reducer: EddyReducer<S, A>,
initialState: PreloadedState<S> | undefined,
delta: Delta<A, S>,
): Store<S, A> => {
const compose = composeWithDevTools({
name: 'test-app',
});
 
const store = reduxCreateStore(
reducer as Reducer<S, A>,
initialState,
compose(applyMiddleware(observeDelta(delta)), eddy()),
);
 
return store;
};