init-store.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. /* eslint-env mozilla/frame-script */
  2. import {actionCreators as ac, actionTypes as at, actionUtils as au} from "common/Actions.jsm";
  3. import {applyMiddleware, combineReducers, createStore} from "redux";
  4. export const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
  5. export const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain";
  6. export const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent";
  7. export const EARLY_QUEUED_ACTIONS = [at.SAVE_SESSION_PERF_DATA];
  8. /**
  9. * A higher-order function which returns a reducer that, on MERGE_STORE action,
  10. * will return the action.data object merged into the previous state.
  11. *
  12. * For all other actions, it merely calls mainReducer.
  13. *
  14. * Because we want this to merge the entire state object, it's written as a
  15. * higher order function which takes the main reducer (itself often a call to
  16. * combineReducers) as a parameter.
  17. *
  18. * @param {function} mainReducer reducer to call if action != MERGE_STORE_ACTION
  19. * @return {function} a reducer that, on MERGE_STORE_ACTION action,
  20. * will return the action.data object merged
  21. * into the previous state, and the result
  22. * of calling mainReducer otherwise.
  23. */
  24. function mergeStateReducer(mainReducer) {
  25. return (prevState, action) => {
  26. if (action.type === MERGE_STORE_ACTION) {
  27. return {...prevState, ...action.data};
  28. }
  29. return mainReducer(prevState, action);
  30. };
  31. }
  32. /**
  33. * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary
  34. */
  35. const messageMiddleware = store => next => action => {
  36. const skipLocal = action.meta && action.meta.skipLocal;
  37. if (au.isSendToMain(action)) {
  38. RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
  39. }
  40. if (!skipLocal) {
  41. next(action);
  42. }
  43. };
  44. export const rehydrationMiddleware = store => next => action => {
  45. if (store._didRehydrate) {
  46. return next(action);
  47. }
  48. const isMergeStoreAction = action.type === MERGE_STORE_ACTION;
  49. const isRehydrationRequest = action.type === at.NEW_TAB_STATE_REQUEST;
  50. if (isRehydrationRequest) {
  51. store._didRequestInitialState = true;
  52. return next(action);
  53. }
  54. if (isMergeStoreAction) {
  55. store._didRehydrate = true;
  56. return next(action);
  57. }
  58. // If init happened after our request was made, we need to re-request
  59. if (store._didRequestInitialState && action.type === at.INIT) {
  60. return next(ac.AlsoToMain({type: at.NEW_TAB_STATE_REQUEST}));
  61. }
  62. if (au.isBroadcastToContent(action) || au.isSendToOneContent(action) || au.isSendToPreloaded(action)) {
  63. // Note that actions received before didRehydrate will not be dispatched
  64. // because this could negatively affect preloading and the the state
  65. // will be replaced by rehydration anyway.
  66. return null;
  67. }
  68. return next(action);
  69. };
  70. /**
  71. * This middleware queues up all the EARLY_QUEUED_ACTIONS until it receives
  72. * the first action from main. This is useful for those actions for main which
  73. * require higher reliability, i.e. the action will not be lost in the case
  74. * that it gets sent before the main is ready to receive it. Conversely, any
  75. * actions allowed early are accepted to be ignorable or re-sendable.
  76. */
  77. export const queueEarlyMessageMiddleware = store => next => action => {
  78. if (store._receivedFromMain) {
  79. next(action);
  80. } else if (au.isFromMain(action)) {
  81. next(action);
  82. store._receivedFromMain = true;
  83. // Sending out all the early actions as main is ready now
  84. if (store._earlyActionQueue) {
  85. store._earlyActionQueue.forEach(next);
  86. store._earlyActionQueue = [];
  87. }
  88. } else if (EARLY_QUEUED_ACTIONS.includes(action.type)) {
  89. store._earlyActionQueue = store._earlyActionQueue || [];
  90. store._earlyActionQueue.push(action);
  91. } else {
  92. // Let any other type of action go through
  93. next(action);
  94. }
  95. };
  96. /**
  97. * initStore - Create a store and listen for incoming actions
  98. *
  99. * @param {object} reducers An object containing Redux reducers
  100. * @param {object} intialState (optional) The initial state of the store, if desired
  101. * @return {object} A redux store
  102. */
  103. export function initStore(reducers) {
  104. const store = createStore(
  105. mergeStateReducer(combineReducers(reducers)),
  106. global.RPMAddMessageListener && applyMiddleware(rehydrationMiddleware, queueEarlyMessageMiddleware, messageMiddleware)
  107. );
  108. store._didRehydrate = false;
  109. store._didRequestInitialState = false;
  110. if (global.RPMAddMessageListener) {
  111. global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => {
  112. try {
  113. store.dispatch(msg.data);
  114. } catch (ex) {
  115. console.error("Content msg:", msg, "Dispatch error: ", ex); // eslint-disable-line no-console
  116. dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`);
  117. }
  118. });
  119. }
  120. return store;
  121. }