/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* eslint-env mozilla/remote-page */ import { actionCreators as ac, actionTypes as at, actionUtils as au, } from "../../common/Actions.mjs"; // We disable import checking here as redux is installed via the npm packages // at the newtab level, rather than in the top-level package.json. // eslint-disable-next-line import/no-unresolved import { applyMiddleware, combineReducers, createStore } from "redux"; export const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE"; export const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain"; export const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent"; /** * A higher-order function which returns a reducer that, on MERGE_STORE action, * will return the action.data object merged into the previous state. * * For all other actions, it merely calls mainReducer. * * Because we want this to merge the entire state object, it's written as a * higher order function which takes the main reducer (itself often a call to * combineReducers) as a parameter. * * @param {function} mainReducer reducer to call if action != MERGE_STORE_ACTION * @return {function} a reducer that, on MERGE_STORE_ACTION action, * will return the action.data object merged * into the previous state, and the result * of calling mainReducer otherwise. */ function mergeStateReducer(mainReducer) { return (prevState, action) => { if (action.type === MERGE_STORE_ACTION) { return { ...prevState, ...action.data }; } return mainReducer(prevState, action); }; } /** * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary */ const messageMiddleware = () => next => action => { const skipLocal = action.meta && action.meta.skipLocal; if (au.isSendToMain(action)) { RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action); } if (!skipLocal) { next(action); } }; export const rehydrationMiddleware = ({ getState }) => { // NB: The parameter here is MiddlewareAPI which looks like a Store and shares // the same getState, so attached properties are accessible from the store. getState.didRehydrate = false; getState.didRequestInitialState = false; return next => action => { if (getState.didRehydrate || window.__FROM_STARTUP_CACHE__) { // Startup messages can be safely ignored by the about:home document // stored in the startup cache. if ( window.__FROM_STARTUP_CACHE__ && action.meta && action.meta.isStartup ) { return null; } return next(action); } const isMergeStoreAction = action.type === MERGE_STORE_ACTION; const isRehydrationRequest = action.type === at.NEW_TAB_STATE_REQUEST; if (isRehydrationRequest) { getState.didRequestInitialState = true; return next(action); } if (isMergeStoreAction) { getState.didRehydrate = true; return next(action); } // If init happened after our request was made, we need to re-request if (getState.didRequestInitialState && action.type === at.INIT) { return next(ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST })); } if ( au.isBroadcastToContent(action) || au.isSendToOneContent(action) || au.isSendToPreloaded(action) ) { // Note that actions received before didRehydrate will not be dispatched // because this could negatively affect preloading and the the state // will be replaced by rehydration anyway. return null; } return next(action); }; }; /** * initStore - Create a store and listen for incoming actions * * @param {object} reducers An object containing Redux reducers * @param {object} intialState (optional) The initial state of the store, if desired * @return {object} A redux store */ export function initStore(reducers, initialState) { const store = createStore( mergeStateReducer(combineReducers(reducers)), initialState, globalThis.RPMAddMessageListener && applyMiddleware(rehydrationMiddleware, messageMiddleware) ); if (globalThis.RPMAddMessageListener) { globalThis.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { try { store.dispatch(msg.data); } catch (ex) { console.error("Content msg:", msg, "Dispatch error: ", ex); dump( `Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ ex.stack }` ); } }); } return store; }