Skip to content

Instantly share code, notes, and snippets.

@fport
Created January 10, 2023 15:56
Show Gist options
  • Save fport/791ead9e68c266e3cd967653f9825743 to your computer and use it in GitHub Desktop.
Save fport/791ead9e68c266e3cd967653f9825743 to your computer and use it in GitHub Desktop.
Provides state synchronization between tabs
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.initMessageListener = exports.initStateWithPrevTab = exports.withReduxStateSync = exports.createReduxStateSync = exports.createStateSyncMiddleware = exports.WINDOW_STATE_SYNC_ID = exports.INIT_MESSAGE_LISTENER = exports.RECEIVE_INIT_STATE = exports.SEND_INIT_STATE = exports.GET_INIT_STATE = undefined;
exports.generateUuidForAction = generateUuidForAction;
exports.isActionAllowed = isActionAllowed;
exports.isActionSynced = isActionSynced;
exports.MessageListener = MessageListener;
var _broadcastChannel = require('broadcast-channel');
var lastUuid = 0;
var GET_INIT_STATE = exports.GET_INIT_STATE = '&_GET_INIT_STATE';
var SEND_INIT_STATE = exports.SEND_INIT_STATE = '&_SEND_INIT_STATE';
var RECEIVE_INIT_STATE = exports.RECEIVE_INIT_STATE = '&_RECEIVE_INIT_STATE';
var INIT_MESSAGE_LISTENER = exports.INIT_MESSAGE_LISTENER = '&_INIT_MESSAGE_LISTENER';
var defaultConfig = {
channel: 'redux_state_sync',
predicate: null,
blacklist: [],
whitelist: [],
broadcastChannelOption: undefined,
prepareState: function prepareState(state) {
return state;
},
receiveState: function receiveState(prevState, nextState) {
return nextState;
}
};
var getIniteState = function getIniteState() {
return { type: GET_INIT_STATE };
};
var sendIniteState = function sendIniteState() {
return { type: SEND_INIT_STATE };
};
var receiveIniteState = function receiveIniteState(state) {
return { type: RECEIVE_INIT_STATE, payload: state };
};
var initListener = function initListener() {
return { 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
var WINDOW_STATE_SYNC_ID = exports.WINDOW_STATE_SYNC_ID = guid();
// export for test
function generateUuidForAction(action) {
var stampedAction = action;
stampedAction.$uuid = guid();
stampedAction.$wuid = WINDOW_STATE_SYNC_ID;
return stampedAction;
}
// export for test
function isActionAllowed(_ref) {
var predicate = _ref.predicate,
blacklist = _ref.blacklist,
whitelist = _ref.whitelist;
var allowed = function allowed() {
return true;
};
if (predicate && typeof predicate === 'function') {
allowed = predicate;
} else if (Array.isArray(blacklist)) {
allowed = function allowed(action) {
return blacklist.indexOf(action.type) < 0;
};
} else if (Array.isArray(whitelist)) {
allowed = function allowed(action) {
return whitelist.indexOf(action.type) >= 0;
};
}
return allowed;
}
// export for test
function isActionSynced(action) {
return !!action.$isSync;
}
// export for test
function MessageListener(_ref2) {
var channel = _ref2.channel,
dispatch = _ref2.dispatch,
allowed = _ref2.allowed;
var isSynced = false;
var tabs = {};
this.handleOnMessage = function (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;
}
var createStateSyncMiddleware = exports.createStateSyncMiddleware = function createStateSyncMiddleware() {
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultConfig;
var allowed = isActionAllowed(config);
var channel = new _broadcastChannel.BroadcastChannel(config.channel, config.broadcastChannelOption);
var prepareState = config.prepareState || defaultConfig.prepareState;
var messageListener = null;
return function (_ref3) {
var getState = _ref3.getState,
dispatch = _ref3.dispatch;
return function (next) {
return function (action) {
// create message receiver
if (!messageListener) {
messageListener = new MessageListener({ channel: channel, dispatch: dispatch, allowed: allowed });
}
// post messages
if (action && !action.$uuid) {
var 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
var createReduxStateSync = exports.createReduxStateSync = function createReduxStateSync(appReducer) {
var receiveState = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultConfig.receiveState;
return function (state, action) {
var initState = state;
if (action.type === RECEIVE_INIT_STATE) {
initState = receiveState(state, action.payload);
}
return appReducer(initState, action);
};
};
// init state with other tab's state
var withReduxStateSync = exports.withReduxStateSync = createReduxStateSync;
var initStateWithPrevTab = exports.initStateWithPrevTab = function initStateWithPrevTab(_ref4) {
var dispatch = _ref4.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
*/
var initMessageListener = exports.initMessageListener = function initMessageListener(_ref5) {
var dispatch = _ref5.dispatch;
dispatch(initListener());
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment