fune/browser/components/newtab/content-src/lib/init-store.js

175 lines
5.8 KiB
JavaScript

/* 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.sys.mjs";
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";
export const EARLY_QUEUED_ACTIONS = [at.SAVE_SESSION_PERF_DATA];
/**
* 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 = store => 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);
};
};
/**
* This middleware queues up all the EARLY_QUEUED_ACTIONS until it receives
* the first action from main. This is useful for those actions for main which
* require higher reliability, i.e. the action will not be lost in the case
* that it gets sent before the main is ready to receive it. Conversely, any
* actions allowed early are accepted to be ignorable or re-sendable.
*/
export const queueEarlyMessageMiddleware = ({ 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.earlyActionQueue = [];
getState.receivedFromMain = false;
return next => action => {
if (getState.receivedFromMain) {
next(action);
} else if (au.isFromMain(action)) {
next(action);
getState.receivedFromMain = true;
// Sending out all the early actions as main is ready now
getState.earlyActionQueue.forEach(next);
getState.earlyActionQueue.length = 0;
} else if (EARLY_QUEUED_ACTIONS.includes(action.type)) {
getState.earlyActionQueue.push(action);
} else {
// Let any other type of action go through
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,
global.RPMAddMessageListener &&
applyMiddleware(
queueEarlyMessageMiddleware,
rehydrationMiddleware,
messageMiddleware
)
);
if (global.RPMAddMessageListener) {
global.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;
}