fune/browser/components/newtab/content-src/lib/init-store.js
Mike Conley 302950a392 Bug 1865536 - Queue and flush early messages from about:newtab after the parent-process is ready. r=Mardak
Before this patch, the about:home/about:newtab Redux store code had some
middleware that queued any messages sent from the page before the parent
had sent any messages. Presumably this was so that those messages wouldn't
be dropped if they were sent while the parent process was still setting
up its Feeds.

Unfortunately, there's a race here - if the parent process _is_ ready and
just chooses not to send any messages right away, the loaded about:home/about:newtab
document will just hold on to any actions until the parent process has
sent something down to it.

The Talos test that was failing here was waiting for the initial about:home
page to send a message which would record a Telemetry probe. That message
wasn't arriving in time. Presumably, _eventually_ the parent process would
have sent a message down to the about:home page which would flush the actions,
but the Talos test would time out before that would occur.

This patch changes things by having the _parent_ process queue any messages
sent from the content in the event that the ActivityStreamMessageChannel
is not yet set up. Once it is set up, those messages are dispatched after
the simulated NEW_TAB_INIT and NEW_TAB_LOAD for those early tabs are
sent to the parent process Redux store.

Differential Revision: https://phabricator.services.mozilla.com/D195179
2023-12-01 18:29:06 +00:00

140 lines
4.5 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";
/**
* 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);
};
};
/**
* 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(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;
}