UNPKG

redux-state-sync

Version:

A middleware for redux to sync state in different tabs

160 lines (145 loc) 5.72 kB
import { BroadcastChannel } from 'broadcast-channel'; let lastUuid = 0; export const GET_INIT_STATE = '&_GET_INIT_STATE'; export const SEND_INIT_STATE = '&_SEND_INIT_STATE'; export const RECEIVE_INIT_STATE = '&_RECEIVE_INIT_STATE'; export const INIT_MESSAGE_LISTENER = '&_INIT_MESSAGE_LISTENER'; const defaultConfig = { channel: 'redux_state_sync', predicate: null, blacklist: [], whitelist: [], broadcastChannelOption: undefined, prepareState: state => state, receiveState: (prevState, nextState) => nextState, }; const getIniteState = () => ({ type: GET_INIT_STATE }); const sendIniteState = () => ({ type: SEND_INIT_STATE }); const receiveIniteState = state => ({ type: RECEIVE_INIT_STATE, payload: state }); const initListener = () => ({ type: INIT_MESSAGE_LISTENER }); function s4() { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1); } function guid() { return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; } // generate current window unique id export const WINDOW_STATE_SYNC_ID = guid(); // export for test export function generateUuidForAction(action) { const stampedAction = action; stampedAction.$uuid = guid(); stampedAction.$wuid = WINDOW_STATE_SYNC_ID; return stampedAction; } // export for test export function isActionAllowed({ predicate, blacklist, whitelist }) { let allowed = () => true; if (predicate && typeof predicate === 'function') { allowed = predicate; } else if (Array.isArray(blacklist)) { allowed = action => blacklist.indexOf(action.type) < 0; } else if (Array.isArray(whitelist)) { allowed = action => whitelist.indexOf(action.type) >= 0; } return allowed; } // export for test export function isActionSynced(action) { return !!action.$isSync; } // export for test export function MessageListener({ channel, dispatch, allowed }) { let isSynced = false; const tabs = {}; this.handleOnMessage = stampedAction => { // Ignore if this action is triggered by this window if (stampedAction.$wuid === WINDOW_STATE_SYNC_ID) { return; } // IE bug https://stackoverflow.com/questions/18265556/why-does-internet-explorer-fire-the-window-storage-event-on-the-window-that-st if (stampedAction.type === RECEIVE_INIT_STATE) { return; } // ignore other values that saved to localstorage. if (stampedAction.$uuid && stampedAction.$uuid !== lastUuid) { if (stampedAction.type === GET_INIT_STATE && !tabs[stampedAction.$wuid]) { tabs[stampedAction.$wuid] = true; dispatch(sendIniteState()); } else if (stampedAction.type === SEND_INIT_STATE && !tabs[stampedAction.$wuid]) { if (!isSynced) { isSynced = true; dispatch(receiveIniteState(stampedAction.payload)); } } else if (allowed(stampedAction)) { lastUuid = stampedAction.$uuid; dispatch( Object.assign(stampedAction, { $isSync: true, }), ); } } }; this.messageChannel = channel; this.messageChannel.onmessage = this.handleOnMessage; } export const createStateSyncMiddleware = (config = defaultConfig) => { const allowed = isActionAllowed(config); const channel = new BroadcastChannel(config.channel, config.broadcastChannelOption); const prepareState = config.prepareState || defaultConfig.prepareState; let messageListener = null; return ({ getState, dispatch }) => next => action => { // create message receiver if (!messageListener) { messageListener = new MessageListener({ channel, dispatch, allowed }); } // post messages if (action && !action.$uuid) { const stampedAction = generateUuidForAction(action); lastUuid = stampedAction.$uuid; try { if (action.type === SEND_INIT_STATE) { if (getState()) { stampedAction.payload = prepareState(getState()); channel.postMessage(stampedAction); } return next(action); } if (allowed(stampedAction) || action.type === GET_INIT_STATE) { channel.postMessage(stampedAction); } } catch (e) { console.error("Your browser doesn't support cross tab communication"); } } return next( Object.assign(action, { $isSync: typeof action.$isSync === 'undefined' ? false : action.$isSync, }), ); }; }; // eslint-disable-next-line max-len export const createReduxStateSync = (appReducer, receiveState = defaultConfig.receiveState) => (state, action) => { let initState = state; if (action.type === RECEIVE_INIT_STATE) { initState = receiveState(state, action.payload); } return appReducer(initState, action); }; // init state with other tab's state export const withReduxStateSync = createReduxStateSync; export const initStateWithPrevTab = ({ dispatch }) => { dispatch(getIniteState()); }; /* if don't dispath any action, the store.dispath will not be available for message listener. therefor need to trigger an empty action to init the messageListener. however, if already using initStateWithPrevTab, this function will be redundant */ export const initMessageListener = ({ dispatch }) => { dispatch(initListener()); };