fusion-rpc-redux/src/index.js (152 lines of code) (raw):
/** Copyright (c) 2018 Uber Technologies, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {createReactor} from 'redux-reactors';
import type {ReactorAction} from 'redux-reactors';
import type {Reducer, Store} from 'redux';
export type ActionType = {
type: string,
payload: *,
};
type RPCReactorsType<TType, TPayload> = {
start: ReactorAction<TType, TPayload>,
success: ReactorAction<TType, TPayload>,
failure: ReactorAction<TType, TPayload>,
};
type RPCReducersType<S, A: ActionType> = {
start?: Reducer<S, A>,
success?: Reducer<S, A>,
failure?: Reducer<S, A>,
};
type NormalizedRPCReducersType<S, A: ActionType> = {
start: Reducer<S, A>,
success: Reducer<S, A>,
failure: Reducer<S, A>,
};
function camelUpper(key: string): string {
return key.replace(/([A-Z])/g, '_$1').toUpperCase();
}
const noopReducer: Reducer<*, *> = state => state;
type ActionNamesType = {failure: string, start: string, success: string};
type ActionTypesType = $Keys<ActionNamesType>;
const types: Array<ActionTypesType> = ['start', 'success', 'failure'];
function createActionNames(rpcId: string): ActionNamesType {
const rpcActionName = camelUpper(rpcId);
return types.reduce((names, type) => {
names[type] = `${rpcActionName}_${type.toUpperCase()}`;
return names;
}, {});
}
type Action<TType, TPayload> = {
type: TType,
payload: TPayload,
};
type ConvertToAction = <T>(T) => (payload: any) => Action<T, *>;
type RPCActionsType = $ObjMap<ActionNamesType, ConvertToAction>;
export function createRPCActions(rpcId: string): RPCActionsType {
const actionNames = createActionNames(rpcId);
return types.reduce((obj, type) => {
obj[type] = (payload: any) => {
return {type: actionNames[type], payload};
};
return obj;
}, {});
}
function getNormalizedReducers<S, A: ActionType>(
reducers: RPCReducersType<S, A>
): NormalizedRPCReducersType<S, A> {
return types.reduce((obj, type) => {
// $FlowFixMe
obj[type] = reducers[type] || noopReducer;
return obj;
}, {});
}
export function createRPCReducer<S, A: ActionType>(
rpcId: string,
reducers: RPCReducersType<S, A>,
// $FlowFixMe
startValue: S = {}
): Reducer<S, A> {
const actionNames = createActionNames(rpcId);
const normalizedReducers = getNormalizedReducers(reducers);
return function rpcReducer(state: S = startValue, action: A) {
if (actionNames.start === action.type) {
return normalizedReducers.start(state, action);
}
if (actionNames.success === action.type) {
return normalizedReducers.success(state, action);
}
if (actionNames.failure === action.type) {
return normalizedReducers.failure(state, action);
}
return state;
};
}
// TODO(#107): Improve flow types with reactors
export function createRPCReactors<S, A: ActionType>(
rpcId: string,
reducers: RPCReducersType<S, A>
): RPCReactorsType<*, *> {
const actionNames = createActionNames(rpcId);
const normalizedReducers = getNormalizedReducers(reducers);
const reactors = types.reduce((obj, type) => {
if (!normalizedReducers[type]) {
throw new Error(`Missing reducer for type ${type}`);
}
const reactor: () => ReactorAction<*, *> = createReactor(
actionNames[type],
(normalizedReducers[type]: any)
);
obj[type] = reactor;
return obj;
}, {});
return ((reactors: any): RPCReactorsType<*, *>);
}
// FYI 2018-05-10 - Improve type definition for RPCHandlerType
type RPCHandlerType = (args: any) => any;
export function createRPCHandler({
actions,
store,
rpc,
rpcId,
mapStateToParams,
transformParams,
}: {
actions?: RPCActionsType,
store: Store<*, *, *>,
rpc: any,
rpcId: string,
mapStateToParams?: (state: any, args?: any) => any,
transformParams?: (params: any) => any,
}): RPCHandlerType {
if (!actions) {
actions = createRPCActions(rpcId);
}
return (args: any) => {
if (mapStateToParams) {
args = mapStateToParams(store.getState(), args);
}
if (transformParams) {
args = transformParams(args);
}
store.dispatch(actions && actions.start(args));
return rpc
.request(rpcId, args)
.then(result => {
try {
store.dispatch(actions && actions.success(result));
} catch (e) {
e.__shouldBubble = true;
throw e;
}
return result;
})
.catch(e => {
if (e.__shouldBubble) {
delete e.__shouldBubble;
throw e;
}
const error = Object.getOwnPropertyNames(e).reduce((obj, key) => {
obj[key] = e[key];
return obj;
}, {});
delete error.stack;
error.initialArgs = args;
store.dispatch(actions && actions.failure(error));
return e;
});
};
}