From 0fbbdf7b7181567002bafccd49b4b62f24a3d9fb Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Mon, 1 Oct 2018 11:59:31 +0200 Subject: [PATCH] changed "observeChanges" utility method --- src/actions/SelectedAccountActions.js | 55 ++++++++++++++--------- src/actions/SendFormActions.js | 27 ++++++----- src/actions/WalletActions.js | 28 ++++++++---- src/reducers/utils/index.js | 64 +++++++++++++++++++++------ src/services/WalletService.js | 18 ++++---- 5 files changed, 129 insertions(+), 63 deletions(-) diff --git a/src/actions/SelectedAccountActions.js b/src/actions/SelectedAccountActions.js index f10d1414..53e8b62f 100644 --- a/src/actions/SelectedAccountActions.js +++ b/src/actions/SelectedAccountActions.js @@ -1,11 +1,15 @@ /* @flow */ - +import { LOCATION_CHANGE } from 'react-router-redux'; +import * as WALLET from 'actions/constants/wallet'; import * as ACCOUNT from 'actions/constants/account'; +import * as DISCOVERY from 'actions/constants/discovery'; +import * as TOKEN from 'actions/constants/token'; +import * as PENDING from 'actions/constants/pendingTx'; + import * as reducerUtils from 'reducers/utils'; -import { initialState } from 'reducers/SelectedAccountReducer'; import type { - AsyncAction, + PayloadAction, Action, GetState, Dispatch, @@ -61,7 +65,7 @@ const getAccountStatus = (state: State, selectedAccount: SelectedAccountState): if (blockchain && !blockchain.connected) { return { type: 'backend', - title: 'Backend is not connected', + title: `${network.name} backend is not connected`, shouldRender: false, }; } @@ -142,25 +146,29 @@ const getAccountStatus = (state: State, selectedAccount: SelectedAccountState): return null; }; -export const observe = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - // ignore actions dispatched from this process - if (action.type === ACCOUNT.UPDATE_SELECTED_ACCOUNT) { - return; - } +// list of all actions which has influence on "selectedAccount" reducer +// other actions will be ignored +const actions = [ + LOCATION_CHANGE, + WALLET.SET_SELECTED_DEVICE, + WALLET.UPDATE_SELECTED_DEVICE, + ...Object.values(ACCOUNT).filter(v => typeof v === 'string' && v !== ACCOUNT.UPDATE_SELECTED_ACCOUNT), // exported values got unwanted "__esModule: true" as first element + ...Object.values(DISCOVERY).filter(v => typeof v === 'string'), + ...Object.values(TOKEN).filter(v => typeof v === 'string'), + ...Object.values(PENDING).filter(v => typeof v === 'string'), +]; + +/* +* Called from WalletService +*/ +export const observe = (prevState: State, action: Action): PayloadAction => (dispatch: Dispatch, getState: GetState): boolean => { + // ignore not listed actions + if (actions.indexOf(action.type) < 0) return false; const state: State = getState(); const { location } = state.router; - if (!location.state.account) { - // displayed route is not an account route - if (state.selectedAccount.location.length > 1) { - // reset "selectedAccount" reducer to default - dispatch({ - type: ACCOUNT.UPDATE_SELECTED_ACCOUNT, - payload: initialState, - }); - } - return; - } + // displayed route is not an account route + if (!location.state.account) return false; // get new values for selected account const account = reducerUtils.getSelectedAccount(state); @@ -186,7 +194,11 @@ export const observe = (prevState: State, action: Action): AsyncAction => async newState.notification = status || null; newState.shouldRender = status ? status.shouldRender : true; // check if newState is different than previous state - const stateChanged = reducerUtils.observeChanges(prevState.selectedAccount, newState, ['location', 'account', 'network', 'discovery', 'tokens', 'pending', 'notification', 'shouldRender']); + const stateChanged = reducerUtils.observeChanges(prevState.selectedAccount, newState, { + account: ['balance', 'nonce'], + discovery: ['accountIndex', 'interrupted', 'completed', 'waitingForBlockchain', 'waitingForDevice'], + }); + if (stateChanged) { // update values in reducer dispatch({ @@ -194,4 +206,5 @@ export const observe = (prevState: State, action: Action): AsyncAction => async payload: newState, }); } + return stateChanged; }; diff --git a/src/actions/SendFormActions.js b/src/actions/SendFormActions.js index 1302d611..f26664f3 100644 --- a/src/actions/SendFormActions.js +++ b/src/actions/SendFormActions.js @@ -2,6 +2,7 @@ import TrezorConnect from 'trezor-connect'; import BigNumber from 'bignumber.js'; +import * as ACCOUNT from 'actions/constants/account'; import * as NOTIFICATION from 'actions/constants/notification'; import * as SEND from 'actions/constants/send'; import * as WEB3 from 'actions/constants/web3'; @@ -45,10 +46,21 @@ export type SendFormAction = { type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR, } | SendTxAction; +// list of all actions which has influence on "sendForm" reducer +// other actions will be ignored +const actions = [ + ACCOUNT.UPDATE_SELECTED_ACCOUNT, + WEB3.GAS_PRICE_UPDATED, + ...Object.values(SEND).filter(v => typeof v === 'string'), +]; + /* -* Called from WalletService on EACH action +* Called from WalletService */ export const observe = (prevState: ReducersState, action: Action): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + // ignore not listed actions + if (actions.indexOf(action.type) < 0) return; + const currentState = getState(); // do not proceed if it's not "send" url if (!currentState.router.location.state.send) return; @@ -75,16 +87,9 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction = let shouldUpdate: boolean = false; // check if "selectedAccount" reducer changed - const selectedAccountChanged = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, ['account', 'tokens', 'pending']); - if (selectedAccountChanged) { - // double check - // there are only few fields that we are interested in - // check them to avoid unnecessary calculation and validation - const accountChanged = reducerUtils.observeChanges(prevState.selectedAccount.account, currentState.selectedAccount.account, ['balance', 'nonce']); - const tokensChanged = reducerUtils.observeChanges(prevState.selectedAccount.tokens, currentState.selectedAccount.tokens); - const pendingChanged = reducerUtils.observeChanges(prevState.selectedAccount.pending, currentState.selectedAccount.pending); - shouldUpdate = accountChanged || tokensChanged || pendingChanged; - } + shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, { + account: ['balance', 'nonce'], + }); // check if "sendForm" reducer changed if (!shouldUpdate) { diff --git a/src/actions/WalletActions.js b/src/actions/WalletActions.js index 983b86b2..0d572f4f 100644 --- a/src/actions/WalletActions.js +++ b/src/actions/WalletActions.js @@ -1,5 +1,7 @@ /* @flow */ +import { LOCATION_CHANGE } from 'react-router-redux'; +import { DEVICE } from 'trezor-connect'; import * as WALLET from 'actions/constants/wallet'; import * as reducerUtils from 'reducers/utils'; @@ -8,7 +10,7 @@ import type { TrezorDevice, RouterLocationState, ThunkAction, - AsyncAction, + PayloadAction, Action, Dispatch, GetState, @@ -78,18 +80,26 @@ export const clearUnavailableDevicesData = (prevState: State, device: Device): T } }; +// list of all actions which has influence on "selectedDevice" field in "wallet" reducer +// other actions will be ignored +const actions = [ + LOCATION_CHANGE, + ...Object.values(DEVICE).filter(v => typeof v === 'string'), +]; + +/* +* Called from WalletService +*/ +export const observe = (prevState: State, action: Action): PayloadAction => (dispatch: Dispatch, getState: GetState): boolean => { + // ignore not listed actions + if (actions.indexOf(action.type) < 0) return false; -export const observe = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - // ignore actions dispatched from this process - if (action.type === WALLET.SET_SELECTED_DEVICE || action.type === WALLET.UPDATE_SELECTED_DEVICE) { - return; - } const state: State = getState(); - const locationChanged = reducerUtils.observeChanges(prevState.router.location, state.router.location, ['pathname']); + const locationChanged = reducerUtils.observeChanges(prevState.router.location, state.router.location); const device = reducerUtils.getSelectedDevice(state); const selectedDeviceChanged = reducerUtils.observeChanges(state.wallet.selectedDevice, device); - + // handle devices state change (from trezor-connect events or location change) if (locationChanged || selectedDeviceChanged) { if (device && reducerUtils.isSelectedDevice(state.wallet.selectedDevice, device)) { @@ -103,5 +113,7 @@ export const observe = (prevState: State, action: Action): AsyncAction => async device, }); } + return true; } + return false; }; \ No newline at end of file diff --git a/src/reducers/utils/index.js b/src/reducers/utils/index.js index 8a3ded63..d3a8e740 100644 --- a/src/reducers/utils/index.js +++ b/src/reducers/utils/index.js @@ -116,22 +116,58 @@ export const getWeb3 = (state: State): ?Web3Instance => { return state.web3.find(w3 => w3.network === locationState.network); }; -export const observeChanges = (prev: ?(Object | Array), current: ?(Object | Array), fields?: Array): boolean => { - if (prev !== current) { - // 1. one of the objects is null/undefined - if (!prev || !current) return true; - // 2. object are Arrays and they have different length - if (Array.isArray(prev) && Array.isArray(current)) return prev.length !== current.length; - // 3. no nested field to check - if (!Array.isArray(fields)) return true; - // 4. validate nested field - if (prev instanceof Object && current instanceof Object) { - for (let i = 0; i < fields.length; i++) { - const key = fields[i]; - if (Array.isArray(prev[key]) && Array.isArray(current[key]) && prev[key].length !== current[key].length) return true; - if (prev[key] !== current[key]) return true; +export const observeChanges = (prev: ?Object, current: ?Object, filter?: {[k: string]: Array}): boolean => { + // 1. both objects are the same (solves simple types like string, boolean and number) + if (prev === current) return false; + // 2. one of the objects is null/undefined + if (!prev || !current) return true; + + const prevType = Object.prototype.toString.call(current); + const currentType = Object.prototype.toString.call(current); + // 3. one of the objects has different type then other + if (prevType !== currentType) return true; + + if (currentType === '[object Array]') { + // 4. Array length is different + if (prev.length !== current.length) return true; + // observe array recursive + for (let i = 0; i < current.length; i++) { + if (observeChanges(prev[i], current[i], filter)) return true; + } + } else if (currentType === '[object Object]') { + const prevKeys = Object.keys(prev); + const currentKeys = Object.keys(prev); + // 5. simple validation of keys length + if (prevKeys.length !== currentKeys.length) return true; + + // 6. "prev" has keys which "current" doesn't have + const prevDifference = prevKeys.find(k => currentKeys.indexOf(k) < 0); + if (prevDifference) return true; + + // 7. "current" has keys which "prev" doesn't have + const currentDifference = currentKeys.find(k => prevKeys.indexOf(k) < 0); + if (currentDifference) return true; + + // 8. observe every key recursive + for (let i = 0; i < currentKeys.length; i++) { + const key = currentKeys[i]; + if (filter && filter.hasOwnProperty(key) && prev[key] && current[key]) { + const prevFiltered = {}; + const currentFiltered = {}; + for (let i2 = 0; i2 < filter[key].length; i2++) { + const field = filter[key][i2]; + prevFiltered[field] = prev[key][field]; + currentFiltered[field] = current[key][field]; + } + if (observeChanges(prevFiltered, currentFiltered)) return true; + } else if (observeChanges(prev[key], current[key])) { + return true; } } + } else if (prev !== current) { + // solve simple types like string, boolean and number + return true; } + return false; }; diff --git a/src/services/WalletService.js b/src/services/WalletService.js index 881f293b..76db7638 100644 --- a/src/services/WalletService.js +++ b/src/services/WalletService.js @@ -21,7 +21,7 @@ import type { /** * Middleware */ -const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => { +const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => async (action: Action): Promise => { const prevState = api.getState(); // Application live cycle starts HERE! @@ -88,14 +88,14 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state)); } - // observe send form props changes - api.dispatch(SendFormActionActions.observe(prevState, action)); - - // update common values in WallerReducer - api.dispatch(WalletActions.observe(prevState, action)); - - // update common values in SelectedAccountReducer - api.dispatch(SelectedAccountActions.observe(prevState, action)); + // observe common values in WallerReducer + if (!await api.dispatch(WalletActions.observe(prevState, action))) { + // if "selectedDevice" didn't change observe common values in SelectedAccountReducer + if (!await api.dispatch(SelectedAccountActions.observe(prevState, action))) { + // if "selectedAccount" didn't change observe send form props changes + api.dispatch(SendFormActionActions.observe(prevState, action)); + } + } return action; };