init-store.mjs (4751B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { 6 actionCreators as ac, 7 actionTypes as at, 8 actionUtils as au, 9 } from "../../common/Actions.mjs"; 10 // We disable import checking here as redux is installed via the npm packages 11 // at the newtab level, rather than in the top-level package.json. 12 // eslint-disable-next-line import/no-unresolved 13 import { applyMiddleware, combineReducers, createStore } from "redux"; 14 15 export const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE"; 16 export const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain"; 17 export const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent"; 18 19 /** 20 * A higher-order function which returns a reducer that, on MERGE_STORE action, 21 * will return the action.data object merged into the previous state. 22 * 23 * For all other actions, it merely calls mainReducer. 24 * 25 * Because we want this to merge the entire state object, it's written as a 26 * higher order function which takes the main reducer (itself often a call to 27 * combineReducers) as a parameter. 28 * 29 * @param {function} mainReducer reducer to call if action != MERGE_STORE_ACTION 30 * @return {function} a reducer that, on MERGE_STORE_ACTION action, 31 * will return the action.data object merged 32 * into the previous state, and the result 33 * of calling mainReducer otherwise. 34 */ 35 function mergeStateReducer(mainReducer) { 36 return (prevState, action) => { 37 if (action.type === MERGE_STORE_ACTION) { 38 return { ...prevState, ...action.data }; 39 } 40 41 return mainReducer(prevState, action); 42 }; 43 } 44 45 /** 46 * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary 47 */ 48 const messageMiddleware = () => next => action => { 49 const skipLocal = action.meta && action.meta.skipLocal; 50 if (au.isSendToMain(action)) { 51 RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action); 52 } 53 if (!skipLocal) { 54 next(action); 55 } 56 }; 57 58 export const rehydrationMiddleware = ({ getState }) => { 59 // NB: The parameter here is MiddlewareAPI which looks like a Store and shares 60 // the same getState, so attached properties are accessible from the store. 61 getState.didRehydrate = false; 62 getState.didRequestInitialState = false; 63 return next => action => { 64 if (getState.didRehydrate || window.__FROM_STARTUP_CACHE__) { 65 // Startup messages can be safely ignored by the about:home document 66 // stored in the startup cache. 67 if ( 68 window.__FROM_STARTUP_CACHE__ && 69 action.meta && 70 action.meta.isStartup 71 ) { 72 return null; 73 } 74 return next(action); 75 } 76 77 const isMergeStoreAction = action.type === MERGE_STORE_ACTION; 78 const isRehydrationRequest = action.type === at.NEW_TAB_STATE_REQUEST; 79 80 if (isRehydrationRequest) { 81 getState.didRequestInitialState = true; 82 return next(action); 83 } 84 85 if (isMergeStoreAction) { 86 getState.didRehydrate = true; 87 return next(action); 88 } 89 90 // If init happened after our request was made, we need to re-request 91 if (getState.didRequestInitialState && action.type === at.INIT) { 92 return next(ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST })); 93 } 94 95 if ( 96 au.isBroadcastToContent(action) || 97 au.isSendToOneContent(action) || 98 au.isSendToPreloaded(action) 99 ) { 100 // Note that actions received before didRehydrate will not be dispatched 101 // because this could negatively affect preloading and the the state 102 // will be replaced by rehydration anyway. 103 return null; 104 } 105 106 return next(action); 107 }; 108 }; 109 110 /** 111 * initStore - Create a store and listen for incoming actions 112 * 113 * @param {object} reducers An object containing Redux reducers 114 * @param {object} intialState (optional) The initial state of the store, if desired 115 * @return {object} A redux store 116 */ 117 export function initStore(reducers, initialState) { 118 const store = createStore( 119 mergeStateReducer(combineReducers(reducers)), 120 initialState, 121 globalThis.RPMAddMessageListener && 122 applyMiddleware(rehydrationMiddleware, messageMiddleware) 123 ); 124 125 if (globalThis.RPMAddMessageListener) { 126 globalThis.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { 127 try { 128 store.dispatch(msg.data); 129 } catch (ex) { 130 console.error("Content msg:", msg, "Dispatch error: ", ex); 131 dump( 132 `Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ 133 ex.stack 134 }` 135 ); 136 } 137 }); 138 } 139 140 return store; 141 }