diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1363,11 +1363,13 @@ }; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); +export type DispatchSource = 'tunnelbroker' | 'tab-sync'; export type SuperAction = { type: string, payload?: ActionPayload, loadingInfo?: LoadingInfo, error?: boolean, + dispatchSource?: DispatchSource, }; type ThunkedAction = (dispatch: Dispatch) => void; export type PromisedAction = (dispatch: Dispatch) => Promise; diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -122,17 +122,17 @@ export type Action = | BaseAction - | { type: 'UPDATE_NAV_INFO', payload: Partial } + | { +type: 'UPDATE_NAV_INFO', +payload: Partial } | { - type: 'UPDATE_WINDOW_DIMENSIONS', - payload: WindowDimensions, + +type: 'UPDATE_WINDOW_DIMENSIONS', + +payload: WindowDimensions, } | { - type: 'UPDATE_WINDOW_ACTIVE', - payload: boolean, + +type: 'UPDATE_WINDOW_ACTIVE', + +payload: boolean, } - | { +type: 'SET_CRYPTO_STORE', payload: CryptoStore } - | { +type: 'SET_INITIAL_REDUX_STATE', payload: InitialReduxState }; + | { +type: 'SET_CRYPTO_STORE', +payload: CryptoStore } + | { +type: 'SET_INITIAL_REDUX_STATE', +payload: InitialReduxState }; function reducer(oldState: AppState | void, action: Action): AppState { invariant(oldState, 'should be set'); @@ -162,6 +162,7 @@ }); } return validateStateAndProcessDBOperations( + action, oldState, { ...state, @@ -183,6 +184,7 @@ ); } else if (action.type === updateWindowDimensionsActionType) { return validateStateAndProcessDBOperations( + action, oldState, { ...state, @@ -192,6 +194,7 @@ ); } else if (action.type === updateWindowActiveActionType) { return validateStateAndProcessDBOperations( + action, oldState, { ...state, @@ -340,10 +343,16 @@ communityPickerStore, }; - return validateStateAndProcessDBOperations(oldState, state, storeOperations); + return validateStateAndProcessDBOperations( + action, + oldState, + state, + storeOperations, + ); } function validateStateAndProcessDBOperations( + action: Action, oldState: AppState, state: AppState, storeOperations: StoreOperations, @@ -454,10 +463,19 @@ }; } - void processDBStoreOperations( - storeOperations, - state.currentUserInfo?.id ?? null, - ); + // The operations were already dispatched from the main tab + + // For now the `dispatchSource` field is not included in any of the + // redux actions and this causes flow to throw an error. + // As soon as one of the actions is updated, this fix (and the corresponding + // one in tab-synchronization.js) can be removed. + // $FlowFixMe + if (action.dispatchSource !== 'tab-sync') { + void processDBStoreOperations( + storeOperations, + state.currentUserInfo?.id ?? null, + ); + } return state; } diff --git a/web/redux/tab-synchronization.js b/web/redux/tab-synchronization.js new file mode 100644 --- /dev/null +++ b/web/redux/tab-synchronization.js @@ -0,0 +1,36 @@ +// @flow + +import type { Middleware, Store } from 'redux'; + +import type { Action, AppState } from './redux-setup.js'; + +const WEB_REDUX_CHANNEL = new BroadcastChannel('shared-redux'); + +const tabSynchronizationMiddleware: Middleware = + () => next => action => { + const result = next(action); + // For now the `dispatchSource` field is not included in any of the + // redux actions and this causes flow to throw an error. + // As soon as one of the actions is updated, this fix (and the corresponding + // one in redux-setup.js) can be removed. + // $FlowFixMe + if (action.dispatchSource === 'tunnelbroker') { + WEB_REDUX_CHANNEL.postMessage(action); + } + return result; + }; + +function synchronizeStoreWithOtherTabs(store: Store) { + WEB_REDUX_CHANNEL.onmessage = event => { + // We can be sure that we only pass actions through the broadcast channel. + // Additionally we know that this is one of the actions that contains the + // `dispatchSource` field. + const action: Action = ({ + ...event.data, + dispatchSource: 'tab-sync', + }: any); + store.dispatch(action); + }; +} + +export { tabSynchronizationMiddleware, synchronizeStoreWithOtherTabs }; diff --git a/web/root.js b/web/root.js --- a/web/root.js +++ b/web/root.js @@ -27,6 +27,10 @@ import InitialReduxStateGate from './redux/initial-state-gate.js'; import { persistConfig } from './redux/persist.js'; import { type AppState, type Action, reducer } from './redux/redux-setup.js'; +import { + synchronizeStoreWithOtherTabs, + tabSynchronizationMiddleware, +} from './redux/tab-synchronization.js'; import history from './router-history.js'; import Socket from './socket.react.js'; @@ -36,8 +40,11 @@ const store: Store = createStore( persistedReducer, defaultWebState, - composeWithDevTools({})(applyMiddleware(thunk, reduxLoggerMiddleware)), + composeWithDevTools({})( + applyMiddleware(thunk, reduxLoggerMiddleware, tabSynchronizationMiddleware), + ), ); +synchronizeStoreWithOtherTabs(store); const persistor = persistStore(store); const RootProvider = (): React.Node => (