/** 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. * */ import {createReactor} from 'redux-reactors'; import type {ReactorAction} from 'redux-reactors'; import type {Reducer, Store} from 'redux'; export type ActionType = { type: string; payload: any; }; type RPCReactorsType = { start: ReactorAction; success: ReactorAction; failure: ReactorAction; }; type RPCReducersType = { start?: Reducer; success?: Reducer; failure?: Reducer; }; type NormalizedRPCReducersType = { start: Reducer; success: Reducer; failure: Reducer; }; 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 = keyof ActionNamesType; const types: Array = ['start', 'success', 'failure']; function createActionNames(rpcId: string): ActionNamesType { const rpcActionName = camelUpper(rpcId); // @ts-expect-error const names: ActionNamesType = {}; types.forEach((type) => { names[type] = `${rpcActionName}_${type.toUpperCase()}`; }); return names; } type Action = { type: TType; payload: TPayload; }; type RPCActionsType = { [T in ActionTypesType]: (payload: any) => Action; }; export function createRPCActions(rpcId: string): RPCActionsType { const actionNames = createActionNames(rpcId); // @ts-expect-error const obj: RPCActionsType = {}; types.forEach((type) => { obj[type] = (payload: any) => { return {type: actionNames[type], payload}; }; }); return obj; } function getNormalizedReducers( reducers: RPCReducersType ): NormalizedRPCReducersType { // @ts-expect-error const obj: NormalizedRPCReducersType = {}; types.forEach((type) => { // $FlowFixMe obj[type] = reducers[type] || noopReducer; }); return obj; } export function createRPCReducer( rpcId: string, reducers: RPCReducersType, // @ts-expect-error todo: this does not look correct startValue: S = {} ): Reducer { 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( rpcId: string, reducers: RPCReducersType ): 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] as any ); obj[type] = reactor; return obj; }, {}); return reactors as any as 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: any = 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; }); }; }