summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/content-src/lib/init-store.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/content-src/lib/init-store.js')
-rw-r--r--browser/components/newtab/content-src/lib/init-store.js175
1 files changed, 175 insertions, 0 deletions
diff --git a/browser/components/newtab/content-src/lib/init-store.js b/browser/components/newtab/content-src/lib/init-store.js
new file mode 100644
index 0000000000..77ae5363bf
--- /dev/null
+++ b/browser/components/newtab/content-src/lib/init-store.js
@@ -0,0 +1,175 @@
+/* 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;
+}