fune/browser/components/newtab/content-src/lib/init-store.js
Mike Conley 4c243e84be Bug 1633913 - Move the queueEarlyMessageMiddleware before the rehydrationMiddleware in the Activity Stream middleware chain. r=Mardak
The queueEarlyMessageMiddlware is designed to hold any messages from the about:home/about:newtab
document until the first message from the parent process is received. This is important because
it's possible for the about:home/about:newtab document to finish loading before Activity Stream
has finished initializing.

The other changes in this patch series allow the rehydrationMiddlware to skip dispatching
actions for the cached about:home document during the startup window. Unfortunately, because
the rehydrationMiddleware has been earlier in the chain than queueEarlyMessageMiddlware, this
means that those ignored startup actions didn't trigger the early queued messages to be sent
to the parent. This prevented the timestamps.about_home_topsites_first_paint scalar from being
set, which broke a Talos test.

By moving the queueEarlyMessageMiddleware earlier, it has an opportunity to send those early
actions to the parent before the rehydrationMiddleware has an opportunity to discard actions.

Depends on D80999

Differential Revision: https://phabricator.services.mozilla.com/D82735
2020-07-08 21:09:34 +00:00

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/frame-script */
import {
actionCreators as ac,
actionTypes as at,
actionUtils as au,
} from "common/Actions.jsm";
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); // eslint-disable-line no-console
dump(
`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${
ex.stack
}`
);
}
});
}
return store;
}